Key Findings
  • .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 的「調音台」:

參數範圍功能直覺理解
temperature0.0 ~ 2.0控制隨機性0.0 = 嚴謹專家,0.7 = 平衡助手,1.5 = 狂野藝術家
top-p0.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)

不同場景的推薦參數組合:

場景模型TemperatureMax Tokens說明
客服問答gpt-4o0.3500精確回答,避免幻覺
程式碼生成gpt-4o0.02000零隨機性,確保一致性
創意寫作gpt-4o1.01000鼓勵多樣性
資料擷取gpt-4o-mini0.0300低成本、確定性輸出
RAG 知識庫gpt-4o0.2800忠於檢索文件,少量創意

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 推送
SSEServer-Sent Events,HTTP 長連線推送協定
Temperature0.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 物件。

前往 Lesson 3:Prompt Engineering 與結構化輸出 →