- 將
.call()替換為.stream()就能實現逐字輸出的打字機效果——使用者不用等 AI 思考完才看到回應,體驗提升顯著 - Temperature 參數是控制 AI 創意度的關鍵旋鈕:0.0 適合精確任務(程式碼生成、資料擷取),0.7 是通用預設,超過 1.5 會產生不可控的隨機輸出
- Spring AI 的
OpenAiChatOptions允許每次呼叫動態切換模型與參數,實現開發用 gpt-4o-mini、正式環境用 gpt-4o 的企業策略 - 前後端 SSE 整合只需 Spring Boot 一個 Controller + 前端 EventSource API,即可打造像 ChatGPT 一樣的即時串流體驗
為什麼需要串流輸出?
在 Lesson 1 中,我們使用 .call() 呼叫 AI 模型——整個回應生成完畢後才一次返回。這在 API 後端處理沒有問題,但如果你在做一個面向使用者的聊天介面,等待 3~10 秒才看到回應是無法接受的體驗。
串流輸出(Streaming)[1]的原理是:AI 模型每生成一個 Token 就立即推送給前端,使用者看到的是文字逐字出現的打字機效果。這就是 ChatGPT、Claude 等產品的互動模式。
┌──────────┐ SSE 連線 ┌──────────────┐ API 呼叫 ┌──────────┐
│ 瀏覽器 │ ◄──────────── │ Spring Boot │ ◄──────────── │ AI 模型 │
│ (前端) │ Token 逐個推送 │ (後端) │ Token 逐個返回 │ (OpenAI) │
└──────────┘ └──────────────┘ └──────────┘
│ │ │
│ 顯示: "Spring" │ 收到 Token: "Spring" │ 生成中...
│ 顯示: "Spring AI" │ 收到 Token: " AI" │ 生成中...
│ 顯示: "Spring AI 是" │ 收到 Token: " 是" │ 生成中...
│ ...逐字顯示 │ ...逐個轉發 │ ...逐個生成
Step 1:環境準備
延續 Lesson 1 的環境,開啟課程專案的 Lesson2/Lesson2_StreamingAndChatModel.ipynb,引入依賴並初始化:
@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("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
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()
println("✓ 環境準備完成")
Step 2:call() vs stream() 實戰對比
最核心的改變只有一個字:把 call() 換成 stream()。
| 方法 | 回傳型別 | 行為 | 適用場景 |
|---|---|---|---|
.call().content() | String? | 等待完整回應後一次返回 | 後端處理、不需即時顯示 |
.stream().content() | Flux<String> | Token 逐個推送 | 前端即時顯示、打字機效果 |
實際執行串流呼叫[4]:
val flux = chatClient
.prompt()
.system("你是一位技術講師,擅長用簡潔的方式解釋概念。")
.user("什麼是 Server-Sent Events(SSE)?用三句話解釋。")
.stream()
.content()
// 逐 Token 印出,模擬打字機效果
print("AI 回應: ")
flux.doOnNext { token ->
print(token) // 每收到一個 Token 就立即印出
}.blockLast()
println()
你會看到文字逐字逐句地出現在螢幕上,而不是等待幾秒後一次全部顯示。這就是串流輸出的魅力。
技術細節:Flux<String>是 Project Reactor 的響應式型別。如果你用過 RxJava 的Observable,概念相同——它是一個「還沒結束的資料流」,每個元素在準備好時才推送。
Step 3:取得串流的完整回應資訊
串流模式下也能追蹤 Token 用量。使用 .chatResponse() 替代 .content() 來取得完整的回應物件:
import org.springframework.ai.chat.model.ChatResponse
val tokens = mutableListOf<String>()
val responseFlux = chatClient
.prompt()
.system("你是一位 Spring AI 專家。")
.user("串流輸出的主要優點是什麼?限 50 字。")
.stream()
.chatResponse()
var lastResponse: ChatResponse? = null
responseFlux.doOnNext { response ->
// 收集每個 Token
val text = response.result?.output?.text ?: ""
tokens.add(text)
print(text)
lastResponse = response
}.blockLast()
println()
// 從最後一個回應取得 Token 用量
val usage = lastResponse?.metadata?.usage
println("\n═══ Token 用量 ═══")
println("輸入: ${usage?.promptTokens}")
println("輸出: ${usage?.completionTokens}")
println("總計: ${usage?.totalTokens}")
Step 4:ChatModel 參數深入解析
AI 模型的回應品質不只取決於 Prompt,更受到模型參數的深刻影響。掌握這些參數,就像掌握了 AI 的「調音台」:
| 參數 | 範圍 | 功能 | 直覺理解 |
|---|---|---|---|
| temperature | 0.0 ~ 2.0 | 控制隨機性 | 0.0 = 嚴謹專家,0.7 = 平衡助手,1.5 = 狂野藝術家 |
| top-p | 0.0 ~ 1.0 | 詞彙選擇範圍 | 0.1 = 只挑最可能的詞,1.0 = 考慮所有可能 |
| max-tokens | 整數 | 回應長度上限 | 控制輸出長度,直接影響成本 |
| model | 字串 | 使用的模型 | 不同模型有不同的能力與價格 |
Step 5:Temperature 實驗——同一個問題,三種溫度
Temperature 是最常調整的參數。讓我們用實驗來直觀理解它的效果:
import org.springframework.ai.openai.OpenAiChatOptions
val question = "用一句話描述程式設計師的生活"
// Temperature 0.0 — 精確模式
val precise = chatClient.prompt()
.system("你是一位文學家。")
.user(question)
.options(OpenAiChatOptions.builder().temperature(0.0).build())
.call().content()
// Temperature 1.0 — 創意模式
val creative = chatClient.prompt()
.system("你是一位文學家。")
.user(question)
.options(OpenAiChatOptions.builder().temperature(1.0).build())
.call().content()
// Temperature 1.5 — 極端模式
val wild = chatClient.prompt()
.system("你是一位文學家。")
.user(question)
.options(OpenAiChatOptions.builder().temperature(1.5).build())
.call().content()
println("🎯 Temperature 0.0(精確):")
println(precise)
println("\n🎨 Temperature 1.0(創意):")
println(creative)
println("\n🌪️ Temperature 1.5(極端):")
println(wild)
典型結果:
🎯 Temperature 0.0(精確):
程式設計師的生活就是在 debug 中尋找意義,在 commit 中記錄成長。
🎨 Temperature 1.0(創意):
鍵盤是他的畫筆,螢幕是無盡的畫布,每一行程式碼都在編織數位世界的夢境與陷阱。
🌪️ Temperature 1.5(極端):
커피와 bugRace 우주の際限ないLoöp裡轉啊 spinning..."
重要觀察: Temperature 1.5 的輸出已經完全不可控——出現了韓文、日文、隨機符號混雜。這就是為什麼正式環境中,Temperature 通常設在 0.0~0.7 之間。超過 1.0 就要非常小心。
Step 6:多模型切換策略
不同模型有不同的能力與成本特性[2]。Spring AI 允許你每次呼叫動態指定模型:
| 模型 | 速度 | 能力 | 成本 | 推薦場景 |
|---|---|---|---|---|
| gpt-4o | 中等 | 最強 | 較高 | 複雜推理、正式環境 |
| gpt-4o-mini | 快 | 良好 | 低 | 開發測試、簡單任務 |
| o3-mini | 較慢 | 推理最強 | 中等 | 數學運算、邏輯推理 |
// 使用 gpt-4o-mini 進行快速開發測試
val miniResponse = chatClient.prompt()
.user("Spring AI 最核心的三個功能是什麼?簡要回答。")
.options(OpenAiChatOptions.builder()
.model("gpt-4o-mini")
.build())
.call().content()
println("gpt-4o-mini 回應:")
println(miniResponse)
企業環境中的最佳實踐是透過 Spring Profile 管理模型選擇:
# application-dev.yml — 開發環境:用便宜快速的模型
spring:
ai:
openai:
chat:
options:
model: gpt-4o-mini
temperature: 0.7
# application-prod.yml — 正式環境:用最強模型
spring:
ai:
openai:
chat:
options:
model: gpt-4o
temperature: 0.3
Step 7:組合多個參數
實際開發中,你通常會同時調整多個參數。OpenAiChatOptions 的 Builder 模式讓這件事很直覺:
val options = OpenAiChatOptions.builder()
.model("gpt-4o-mini")
.temperature(0.3)
.maxTokens(200)
.topP(0.9)
.build()
val response = chatClient.prompt()
.system("你是一位技術文件撰寫專家,回答要精確、結構化。")
.user("列出 Spring AI 的三個核心元件,每個用一句話說明。")
.options(options)
.call().content()
println(response)
不同場景的推薦參數組合:
| 場景 | 模型 | Temperature | Max Tokens | 說明 |
|---|---|---|---|---|
| 客服問答 | gpt-4o | 0.3 | 500 | 精確回答,避免幻覺 |
| 程式碼生成 | gpt-4o | 0.0 | 2000 | 零隨機性,確保一致性 |
| 創意寫作 | gpt-4o | 1.0 | 1000 | 鼓勵多樣性 |
| 資料擷取 | gpt-4o-mini | 0.0 | 300 | 低成本、確定性輸出 |
| RAG 知識庫 | gpt-4o | 0.2 | 800 | 忠於檢索文件,少量創意 |
Step 8:串流 + 參數組合實戰
將串流輸出與參數配置結合,這是最貼近真實應用的使用方式:
val streamOptions = OpenAiChatOptions.builder()
.model("gpt-4o-mini")
.temperature(0.7)
.maxTokens(300)
.build()
print("AI 串流回應: ")
chatClient.prompt()
.system("你是一位 Spring AI 技術顧問。")
.user("Spring AI 的 Advisor 機制是什麼?它能解決什麼問題?")
.options(streamOptions)
.stream()
.content()
.doOnNext { token -> print(token) }
.blockLast()
println()
Step 9:前後端 SSE 整合
在 Kotlin Notebook 中驗證完串流功能後,接下來看如何在正式的 Spring Boot 應用中實現前後端串流[3]。
後端:Spring Boot Controller
只需一個 Controller 方法,Spring Boot 會自動處理 SSE 協定:
import org.springframework.web.bind.annotation.*
import org.springframework.http.MediaType
import reactor.core.publisher.Flux
@RestController
@RequestMapping("/api/chat")
class ChatController(private val chatClient: ChatClient) {
@GetMapping(
value = ["/stream"],
produces = [MediaType.TEXT_EVENT_STREAM_VALUE] // 關鍵:宣告 SSE 格式
)
fun streamChat(@RequestParam message: String): Flux<String> {
return chatClient
.prompt()
.user(message)
.stream()
.content()
}
}
關鍵在於 produces = [MediaType.TEXT_EVENT_STREAM_VALUE]——這告訴 Spring Boot 用 SSE 協定回傳,瀏覽器會維持長連線並逐個接收 Token。
前端:EventSource API
瀏覽器原生支援 SSE,使用 EventSource API 即可接收串流:
// 建立 SSE 連線
const message = encodeURIComponent('什麼是 Spring AI?');
const eventSource = new EventSource(`/api/chat/stream?message=${message}`);
const output = document.getElementById('ai-output');
// 每收到一個 Token 就追加到畫面
eventSource.onmessage = (event) => {
output.textContent += event.data;
};
// 串流結束時關閉連線
eventSource.onerror = () => {
eventSource.close();
console.log('串流結束');
};
完整的資料流如下:
EventSource (瀏覽器) Spring Boot OpenAI API
│ │ │
│── GET /api/chat/stream ──►│── POST /v1/chat ──────►│
│ │ (stream: true) │
│◄─ data: "Spring" ────────│◄─ Token: "Spring" ─────│
│◄─ data: " AI" ──────────│◄─ Token: " AI" ────────│
│◄─ data: " 是" ──────────│◄─ Token: " 是" ────────│
│◄─ data: "..." ──────────│◄─ Token: "..." ────────│
│◄─ [DONE] ───────────────│◄─ [DONE] ──────────────│
│ eventSource.close() │ │
生產環境提醒: SSE 是單向的(伺服器→瀏覽器)。如果你需要雙向即時通訊(例如中斷生成),需要搭配 WebSocket 或使用 POST + ReadableStream 方案。
本課重點回顧
| 概念 | 重點 |
|---|---|
| stream() | 替代 call(),回傳 Flux<String>,實現逐 Token 推送 |
| SSE | Server-Sent Events,HTTP 長連線推送協定 |
| Temperature | 0.0 = 精確,0.7 = 平衡,>1.0 = 高風險 |
| 模型切換 | 開發用 gpt-4o-mini,正式環境用 gpt-4o |
| Max Tokens | 控制回應長度,直接影響成本與延遲 |
| OpenAiChatOptions | 透過 .options() 每次呼叫動態調整參數 |
| 前端整合 | EventSource API + TEXT_EVENT_STREAM_VALUE |
下一步:Lesson 3 — Prompt Engineering 與結構化輸出
掌握了串流輸出與參數調校後,下一堂課將進入 AI 應用開發最核心的技能之一——Prompt Engineering。你將學會:
- PromptTemplate — 用模板引擎動態生成 Prompt,告別字串拼接
- 結構化輸出 — 讓 AI 直接回傳 Kotlin 物件,不再手動解析 JSON
- OutputParser — 從純文字到強型別物件的自動轉換
掌握串流,繼續前進!
學會串流輸出與參數調校後,下一步掌握 Prompt 模板與結構化輸出——讓 AI 直接回傳 Kotlin 物件。


