- Spring AI 提供三種 Prompt 管理策略:Fluent API(快速原型)、PromptTemplate(可重用模板)、外部 .st 檔案(生產環境最佳實踐)——根據專案規模選擇合適的方式
- 結構化輸出(entity())讓 AI 回應直接轉換為 Kotlin data class,不需手動解析 JSON——從「AI 回傳字串」升級為「AI 回傳物件」
- 透過動態角色切換,同一個 AI 模型可以扮演老師、面試官、架構師等不同角色,產生風格截然不同的回答
- 結合 PromptTemplate + entity() 的資料擷取器模式是企業 AI 應用的核心——從非結構化文本中自動提取結構化資訊
為什麼需要 Prompt 模板?
在 Lesson 1 和 Lesson 2 中,我們的 Prompt 都是直接寫死在程式碼裡的字串。這在學習階段沒問題,但進入正式開發後會遇到幾個痛點:
- 難以維護 — Prompt 散佈在各個 Service 方法中,改一個字就要改程式碼
- 無法重用 — 相似的 Prompt 在不同地方複製貼上,修改時容易遺漏
- 不好測試 — Prompt 和業務邏輯耦合,無法獨立測試 Prompt 的效果
- 協作困難 — 非工程師(PM、領域專家)無法直接調整 Prompt
Prompt 模板[1]的核心思想是:把 Prompt 當作可配置的模板,用變數取代寫死的值。就像 HTML 模板引擎(Thymeleaf、Freemarker)對 Web 頁面做的事一樣。
Step 1:環境準備
開啟課程專案的 Lesson3/Lesson3_PromptAndStructuredOutput.ipynb。這堂課多引入了一個重要依賴——jackson-module-kotlin[4],用於結構化輸出的 JSON 反序列化:
@file:DependsOn("org.springframework.ai:spring-ai-openai:1.0.0")
@file:DependsOn("org.springframework.ai:spring-ai-client-chat:1.0.0")
@file:DependsOn("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2")
@file:DependsOn("org.slf4j:slf4j-simple:2.0.16")
import org.springframework.ai.openai.OpenAiChatModel
import org.springframework.ai.openai.api.OpenAiApi
import org.springframework.ai.chat.client.ChatClient
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
val apiKey = System.getenv("OPENAI_API_KEY")
?: error("請先設定 OPENAI_API_KEY(參考 Lesson 0)")
val openAiApi = OpenAiApi.builder().apiKey(apiKey).build()
val chatModel = OpenAiChatModel.builder().openAiApi(openAiApi).build()
val chatClient = ChatClient.builder(chatModel).build()
val kotlinMapper = jacksonObjectMapper()
println("✓ 環境準備完成(含 Jackson Kotlin Module)")
為什麼需要 jackson-module-kotlin? Kotlin 的data class沒有無參數建構子(no-arg constructor),標準 Jackson 無法反序列化。jackson-module-kotlin透過 Kotlin 反射解決了這個問題。
Step 2:方法一——Fluent API 直接組裝
最快速的方式,適合簡單場景。直接用 Kotlin 的字串模板 ${} 嵌入變數:
val language = "日文"
val text = "人工智慧正在改變世界"
val result = chatClient
.prompt()
.system("你是一位專業翻譯,只回傳翻譯結果,不加任何解釋。")
.user("請將以下文字翻譯為${language}:${text}")
.call()
.content() ?: "(無回應)"
println("翻譯結果: $result")
預期輸出:
翻譯結果: 人工知能は世界を変えています。
這種方式簡單直覺,但 Prompt 和 Kotlin 程式碼綁在一起,適合原型開發或一次性腳本。
Step 3:方法二——PromptTemplate 模板類別
當 Prompt 變得複雜(多個變數、多行指令),使用 PromptTemplate 更好管理[3]。注意模板使用 {variable}(大括號,不是 Kotlin 的 ${}):
import org.springframework.ai.chat.prompt.PromptTemplate
val template = PromptTemplate("""
你是一位 {role},請用 {language} 回答。
請針對以下主題提供 {count} 個重點:
主題:{topic}
""".trimIndent())
val prompt = template.create(mapOf(
"role" to "資深軟體架構師",
"language" to "繁體中文",
"count" to "3",
"topic" to "微服務架構的優缺點"
))
val response = chatModel.call(prompt)
println(response.result.output.text)
預期輸出:
1. **獨立擴展性** — 每個微服務可以根據自身負載獨立擴展,不需要整體部署,
資源利用更有效率。
2. **技術多樣性** — 不同服務可以選擇最適合的技術棧(Java、Go、Python),
團隊可以使用最擅長的工具。
3. **複雜度提升** — 分散式系統帶來網路延遲、資料一致性、服務發現等挑戰,
需要額外的基礎設施(API Gateway、Service Mesh)支撐。
PromptTemplate 與 Fluent API 的關鍵差異:
| 特性 | Fluent API ${} | PromptTemplate {} |
|---|---|---|
| 語法 | Kotlin 字串模板 | StringTemplate 語法 |
| 變數時機 | 編譯期就確定 | 執行期動態填入 |
| 可重用性 | 低(嵌在程式碼中) | 高(模板物件可傳遞) |
| 外部化 | 不支援 | 支援載入外部檔案 |
| 適合場景 | 快速原型、簡單查詢 | 多變數、需要重用的正式開發 |
Step 4:方法三——外部檔案模板(生產環境最佳實踐)
在正式專案中,最推薦的做法是把 Prompt 模板放在外部檔案中。這樣 PM、領域專家可以直接編輯 Prompt,不需要動程式碼:
src/main/resources/prompts/
├── translator.st # 翻譯器
├── summarizer.st # 摘要產生器
├── code-reviewer.st # 程式碼審查
└── customer-service.st # 客服回覆
檔案格式使用 .st(StringTemplate)[3]。例如一個程式碼審查模板 code-reviewer.st:
你是一位嚴格的資深工程師,專精 {language} 程式語言。
請審查以下程式碼,從這些面向分析:
1. 潛在的 Bug 或例外處理問題
2. 效能優化建議
3. 程式碼可讀性與命名慣例
4. 安全性考量
請用列點方式回答,每個問題附上具體的改善建議。
程式碼:
{code}
在 Spring Boot 中載入外部模板:
import org.springframework.beans.factory.annotation.Value
import org.springframework.core.io.Resource
@Service
class CodeReviewService(
private val chatClient: ChatClient,
@Value("classpath:prompts/code-reviewer.st") private val reviewTemplate: Resource
) {
fun reviewCode(language: String, code: String): String {
val template = PromptTemplate(reviewTemplate)
val prompt = template.create(mapOf(
"language" to language,
"code" to code
))
return chatClient.prompt(prompt).call().content() ?: ""
}
}
三種方法的選擇指南:
| 場景 | 推薦方法 | 原因 |
|---|---|---|
| 1~2 個變數的簡單查詢 | Fluent API | 最快,不需額外設定 |
| 多變數、需要重用的中等複雜度 | PromptTemplate | 模板物件可傳遞、可測試 |
| 正式環境、團隊協作 | 外部 .st 檔案 | Prompt 與程式碼分離,非工程師可維護 |
Step 5:進階技巧——動態角色切換
SystemMessage 的威力在 Lesson 1 已經體驗過。現在結合 PromptTemplate,我們可以建立一個角色引擎,用同一個模型產生截然不同風格的回答:
val roles = mapOf(
"teacher" to "你是一位耐心的程式設計老師,用生活化的比喻解釋技術概念,讓初學者也能輕鬆理解。",
"interviewer" to "你是一位嚴格的技術面試官,回答要精確、有深度,並追問延伸問題。",
"architect" to "你是一位資深軟體架構師,從系統設計的角度分析,強調可擴展性與維護性。"
)
val question = "什麼是 Dependency Injection?"
roles.forEach { (role, systemPrompt) ->
val answer = chatClient.prompt()
.system(systemPrompt)
.user(question)
.call().content()
println("【${role.uppercase()}】")
println(answer)
println()
}
預期輸出:
【TEACHER】
想像你要做一道菜。與其自己去市場買所有食材(自己建立依賴),
不如有人幫你把食材準備好送到廚房(注入依賴)。
Dependency Injection 就是這個概念——你的程式不用自己建立需要的物件,
由框架(Spring)幫你準備好並「注入」進來。
【INTERVIEWER】
Dependency Injection 是一種實現控制反轉(IoC)的設計模式,
物件的依賴由外部容器提供而非自行建立。
追問:請比較 Constructor Injection 和 Field Injection 的優缺點,
以及為什麼 Spring 官方推薦 Constructor Injection?
【ARCHITECT】
從架構角度看,DI 是實現鬆耦合的核心機制。透過介面抽象與外部注入,
模組之間的依賴關係在編譯期可驗證,且可以在不同環境(測試、正式)
替換實作。這是微服務架構中每個服務能獨立部署的基礎前提。
Step 6:結構化輸出——讓 AI 回傳物件,不是字串
到目前為止,AI 的回應都是 String。但在實際應用中,你需要的是結構化的資料物件——書籍推薦要有書名、作者、推薦理由;API 分析要有端點、方法、參數。
Spring AI 的 entity() 方法[2]可以讓 AI 回應自動轉換為 Kotlin data class:
import org.springframework.ai.converter.BeanOutputConverter
data class BookRecommendation(
val title: String,
val author: String,
val reason: String
)
val bookConverter = BeanOutputConverter(BookRecommendation::class.java, kotlinMapper)
val book: BookRecommendation = chatClient
.prompt()
.system("你是一位技術書籍推薦專家。")
.user("推薦一本學習 Spring Boot 的書")
.call()
.entity(bookConverter)!!
println("書名: ${book.title}")
println("作者: ${book.author}")
println("推薦原因: ${book.reason}")
預期輸出:
書名: Spring Boot in Action
作者: Craig Walls
推薦原因: 這本書從實戰角度出發,涵蓋 Spring Boot 的核心概念與自動配置原理,
適合有 Java 基礎的開發者快速上手。
背後的運作原理是:BeanOutputConverter 在 Prompt 中自動附加 JSON Schema 指令,告訴 AI「請按照這個格式回傳 JSON」,然後用 Jackson 反序列化成 Kotlin 物件。你完全不用手動寫 JSON 解析邏輯。
Step 7:結構化輸出——List 回應
當 AI 需要回傳多個物件時,使用 ParameterizedTypeReference 處理泛型型別:
import org.springframework.core.ParameterizedTypeReference
data class FrameworkComparison(
val name: String,
val language: String,
val strengths: List<String>,
val bestFor: String
)
val listConverter = BeanOutputConverter(
object : ParameterizedTypeReference<List<FrameworkComparison>>() {},
kotlinMapper
)
val frameworks: List<FrameworkComparison> = chatClient
.prompt()
.user("比較 Spring AI、LangChain、LlamaIndex 這三個 AI 框架")
.call()
.entity(listConverter)!!
frameworks.forEach { fw ->
println("【${fw.name}】(${fw.language})")
println(" 優勢: ${fw.strengths.joinToString("、")}")
println(" 最適合: ${fw.bestFor}")
println()
}
預期輸出:
【Spring AI】(Java/Kotlin)
優勢: 企業級整合、Spring 生態系、型別安全
最適合: Java 技術棧的企業 AI 應用
【LangChain】(Python)
優勢: 豐富的 LLM 鏈組合、社群活躍、插件生態
最適合: 快速原型開發與 LLM 應用編排
【LlamaIndex】(Python)
優勢: 資料索引最佳化、多種資料源連接器、RAG 專精
最適合: 知識庫建構與文件檢索應用
Step 8:實戰——AI 資料擷取器
結合 PromptTemplate + entity() 的威力,打造一個從非結構化文本中自動擷取結構化資訊的工具。這是企業 AI 應用最常見的模式之一:
data class MeetingInfo(
val date: String,
val participants: List<String>,
val topics: List<String>,
val decisions: List<String>,
val nextActions: List<String>
)
val meetingNotes = """
上週五(3月14日)下午的技術會議,參加的有 Andy、Betty、Charlie 和 David。
主要討論了三個議題:第一是 Spring AI 整合進度,目前已完成 ChatClient 的封裝;
第二是 RAG 架構的向量資料庫選型,最終決定使用 PGVector;
第三是 API 效能問題,Betty 提出要加入快取機制。
會後決議:下週三之前 Andy 完成 PGVector 的 POC,Betty 負責設計快取策略,
Charlie 要準備 Demo 環境給客戶看。
""".trimIndent()
val meetingConverter = BeanOutputConverter(MeetingInfo::class.java, kotlinMapper)
val meetingInfo: MeetingInfo = chatClient
.prompt()
.system("你是一位專業的會議記錄整理專家。請從會議紀錄中精確擷取結構化資訊。")
.user("請分析以下會議紀錄:\n\n$meetingNotes")
.call()
.entity(meetingConverter)!!
println("📅 日期: ${meetingInfo.date}")
println("👥 參與者: ${meetingInfo.participants.joinToString("、")}")
println("📋 議題:")
meetingInfo.topics.forEachIndexed { i, t -> println(" ${i + 1}. $t") }
println("✅ 決議:")
meetingInfo.decisions.forEach { println(" • $it") }
println("📌 後續行動:")
meetingInfo.nextActions.forEach { println(" • $it") }
預期輸出:
📅 日期: 3月14日(週五)
👥 參與者: Andy、Betty、Charlie、David
📋 議題:
1. Spring AI 整合進度
2. RAG 架構向量資料庫選型
3. API 效能問題
✅ 決議:
• 向量資料庫選用 PGVector
• API 加入快取機制
📌 後續行動:
• Andy:下週三前完成 PGVector POC
• Betty:設計快取策略
• Charlie:準備 Demo 環境給客戶
從一段自然語言的會議紀錄,AI 自動擷取出日期、參與者、議題、決議、行動項目——而且直接就是 Kotlin 物件,可以立即存入資料庫或傳給下游 API。這就是結構化輸出在企業場景中的核心價值。
Step 9:練習——打造產品評論分析器
運用本課學到的技巧,試著建立一個產品評論分析器:
data class ProductReview(
val productName: String,
val rating: Int, // 1-5 分
val pros: List<String>, // 優點
val cons: List<String>, // 缺點
val summary: String // 一句話摘要
)
val reviewText = """
用了 iPhone 16 Pro 兩個月了,整體來說很滿意。相機提升最有感,
特別是夜拍模式簡直是黑科技,拍出來的照片比上一代好太多。
處理器很順暢,玩原神完全不卡。但電池續航還是老問題,
重度使用大概撐不到一天。價格也偏高,入門款就要三萬多。
整體給 4 顆星。
""".trimIndent()
val reviewConverter = BeanOutputConverter(ProductReview::class.java, kotlinMapper)
val review: ProductReview = chatClient
.prompt()
.system("你是一位產品分析師,擅長從用戶評論中擷取結構化資訊。")
.user("分析以下產品評論:\n\n$reviewText")
.call()
.entity(reviewConverter)!!
println("產品: ${review.productName}")
println("評分: ${"⭐".repeat(review.rating)} (${review.rating}/5)")
println("優點: ${review.pros.joinToString("、")}")
println("缺點: ${review.cons.joinToString("、")}")
println("摘要: ${review.summary}")
預期輸出:
產品: iPhone 16 Pro
評分: ⭐⭐⭐⭐ (4/5)
優點: 相機大幅提升、夜拍模式出色、處理器效能流暢
缺點: 電池續航不足、價格偏高
摘要: iPhone 16 Pro 在影像與效能表現優異,但電池續航與定價仍是短板。
本課重點回顧
| 概念 | 重點 |
|---|---|
| Fluent API | ${} 快速嵌入變數,適合簡單場景 |
| PromptTemplate | {variable} 佔位符,可重用、可測試 |
| 外部 .st 檔案 | Prompt 與程式碼分離,生產環境最佳實踐 |
| 動態角色切換 | 不同 SystemMessage 讓同一模型展現不同人格 |
| entity() | AI 回應自動轉換為 Kotlin 物件 |
| ParameterizedTypeReference | 處理 List<T> 等泛型回應 |
| 資料擷取器模式 | 非結構化文本 → 結構化物件,企業核心應用模式 |
下一步:Lesson 4 — Function Calling
結構化輸出讓 AI 能「回傳」物件。但如果 AI 需要主動「呼叫」你的程式碼呢?下一堂課將介紹 Spring AI 最強大的功能之一——Function Calling:
- Tool 註冊 — 讓 AI 知道有哪些工具可以使用
- 自動呼叫 — AI 判斷何時需要呼叫外部函式,並自動執行
- 實戰案例 — AI 直接查詢天氣 API、搜尋資料庫、計算匯率



