- AI 模型本質上是無狀態的——每次 API 呼叫都是獨立的,不會記得上一句對話。ChatMemory 透過自動附加對話歷史解決了這個問題
- Spring AI 的 Advisor 機制類似 AOP(面向切面程式設計),在 AI 呼叫前後自動注入邏輯——ChatMemory 只是其中一種 Advisor,還可以加日誌、安全過濾、RAG 等
- MessageWindowChatMemory 使用滑動視窗策略,保留最近 N 條訊息——當對話超過視窗大小,最舊的訊息會被自動丟棄,有效控制 Token 成本
- 透過 conversationId 實現多會話隔離——不同使用者的記憶互不干擾,是企業多租戶應用的基礎
為什麼 AI 沒有記憶?
在前面幾堂課的實作中,你可能已經注意到:每次呼叫 chatClient.prompt()...call() 都是一次全新的對話。AI 不記得你上一句說了什麼、不記得你叫什麼名字、不記得剛剛討論的主題。
這不是 Bug,而是 AI 模型的本質設計[3]:
第 1 次呼叫: "我叫小明,我喜歡 Kotlin" → AI: "你好小明!Kotlin 是很棒的語言..."
第 2 次呼叫: "我叫什麼名字?" → AI: "抱歉,我不知道您的名字..." 😱
每次 API 呼叫都是獨立的 HTTP Request,AI 模型不會在伺服器端保存任何狀態。解決方案是:每次呼叫時,把之前的對話歷史一起送給 AI。這就是 ChatMemory 做的事。
Step 1:環境準備
開啟課程專案的 Lesson5/Lesson5_ChatMemory.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()
println("✓ 環境準備完成")
Step 2:體驗「無記憶」的 AI
先用一般的 ChatClient 驗證 AI 確實沒有記憶:
val statelessClient = ChatClient.builder(chatModel).build()
// 第一次對話:自我介紹
val r1 = statelessClient.prompt()
.user("你好!我叫小明,我最喜歡的程式語言是 Kotlin。")
.call().content()
println("第 1 輪: $r1")
// 第二次對話:問 AI 是否記得
val r2 = statelessClient.prompt()
.user("我叫什麼名字?我喜歡什麼程式語言?")
.call().content()
println("第 2 輪: $r2")
預期輸出:
第 1 輪: 你好小明!Kotlin 確實是一門很棒的語言,簡潔又安全...
第 2 輪: 很抱歉,您還沒有告訴我您的名字和喜歡的程式語言...
AI 完全不記得你剛剛說過的話。接下來讓我們解決這個問題。
Step 3:認識 Advisor 機制
在加入 ChatMemory 之前,先理解 Spring AI 的 Advisor 架構[2]。Advisor 就像 Spring AOP 的 Interceptor——在 AI 呼叫的前後自動注入額外邏輯:
你的程式碼
│
▼
┌─── Advisor 鏈 ───┐
│ 📝 Memory Advisor │ ← 自動附加對話歷史
│ 📊 Log Advisor │ ← 記錄請求/回應日誌
│ 🛡️ Safety Advisor │ ← 過濾敏感內容
│ 📚 RAG Advisor │ ← 注入知識庫文件
└───────────────────┘
│
▼
AI 模型呼叫
│
▼
┌─── Advisor 鏈 ───┐
│ (反向執行後處理) │
└───────────────────┘
│
▼
回傳結果
ChatMemory Advisor 的工作就是:呼叫前自動把歷史訊息附加到 Prompt 中,呼叫後自動把新的對話存入記憶。
Step 4:啟用 ChatMemory
只需三步就能讓 AI 擁有記憶[1]:
import org.springframework.ai.chat.memory.MessageWindowChatMemory
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor
import org.springframework.ai.chat.memory.ChatMemory
// 1. 建立記憶體(滑動視窗,保留最近 20 條訊息)
val chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20)
.build()
// 2. 建立記憶 Advisor
val memoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory).build()
// 3. 建立帶記憶的 ChatClient
val memoryClient = ChatClient.builder(chatModel)
.defaultAdvisors(memoryAdvisor)
.build()
println("✓ ChatMemory 已啟用(視窗大小: 20)")
現在重新測試:
val m1 = memoryClient.prompt()
.user("你好!我叫小明,我最喜歡的程式語言是 Kotlin。")
.call().content()
println("第 1 輪: $m1")
val m2 = memoryClient.prompt()
.user("我叫什麼名字?我喜歡什麼程式語言?")
.call().content()
println("第 2 輪: $m2")
預期輸出:
第 1 輪: 你好小明!Kotlin 是一門非常出色的程式語言...
第 2 輪: 你叫小明,你最喜歡的程式語言是 Kotlin! 🎉
AI 現在記得你了!讓我們繼續更多輪對話:
val m3 = memoryClient.prompt()
.user("除了 Kotlin,我最近也在學 Spring AI。")
.call().content()
println("第 3 輪: $m3")
val m4 = memoryClient.prompt()
.user("幫我總結一下到目前為止你知道關於我的所有資訊。")
.call().content()
println("第 4 輪: $m4")
預期輸出:
第 4 輪: 根據我們的對話,以下是我知道的關於你的資訊:
1. 你叫小明
2. 你最喜歡的程式語言是 Kotlin
3. 你最近也在學習 Spring AI
你正在建立一個很棒的技術棧!
Step 5:多會話隔離——conversationId
在企業應用中,多個使用者同時使用同一個 AI 服務,每個使用者的記憶必須隔離。透過 conversationId 實現:
// Alice 的對話
val alice1 = memoryClient.prompt()
.user("我叫 Alice,我是 Java 開發者。")
.advisors { it.param(ChatMemory.CONVERSATION_ID, "alice-session") }
.call().content()
println("Alice: $alice1")
// Bob 的對話
val bob1 = memoryClient.prompt()
.user("我叫 Bob,我專精 Python 和機器學習。")
.advisors { it.param(ChatMemory.CONVERSATION_ID, "bob-session") }
.call().content()
println("Bob: $bob1")
// 驗證隔離:問 Alice 的 AI 關於 Bob 的事
val alice2 = memoryClient.prompt()
.user("我叫什麼名字?我會什麼程式語言?你知道 Bob 嗎?")
.advisors { it.param(ChatMemory.CONVERSATION_ID, "alice-session") }
.call().content()
println("Alice 的 AI: $alice2")
預期輸出:
Alice 的 AI: 你叫 Alice,你是 Java 開發者。
至於 Bob,在我們的對話中你沒有提到過這個人,我不認識他。
Alice 和 Bob 的記憶完全隔離——Alice 的 AI 不知道 Bob 的存在,反之亦然。這是多租戶 AI 應用的基礎架構。
Step 6:滑動視窗實驗——記憶會過期
MessageWindowChatMemory 使用滑動視窗策略:只保留最近 N 條訊息,超出的自動丟棄。這是控制 Token 成本的關鍵機制。
用一個小視窗來觀察記憶過期的行為:
// 建立一個很小的記憶視窗(只保留 10 條訊息)
val smallMemory = MessageWindowChatMemory.builder()
.maxMessages(10)
.build()
val smallClient = ChatClient.builder(chatModel)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(smallMemory).build())
.build()
// 第 1 輪:告訴 AI 名字
smallClient.prompt().user("記住:我的名字是「測試員 Alpha」。").call()
// 第 2~8 輪:大量對話把第 1 輪擠出視窗
for (i in 2..8) {
smallClient.prompt().user("這是第 $i 輪的隨機對話,請簡短回答。").call()
}
// 第 9 輪:測試 AI 是否還記得名字
val recall = smallClient.prompt()
.user("我之前告訴你我的名字是什麼?")
.call().content()
println("回憶測試: $recall")
當對話輪數超過視窗大小,最早的訊息(包含名字)已被丟棄,AI 將無法回答。
視窗大小的選擇建議:
| 場景 | 建議大小 | 說明 |
|---|---|---|
| 簡單問答 / FAQ | 5 ~ 10 | 對話短、上下文需求低 |
| 一般聊天助手 | 15 ~ 20 | 平衡記憶與成本 |
| 複雜技術討論 | 30 ~ 50 | 需要較多上下文 |
| 深度分析 / 程式碼審查 | 100+ | 成本較高,但需要完整歷史 |
成本考量: 視窗越大,每次 API 呼叫附帶的歷史訊息越多,消耗的 Token 也越多。20 條訊息的歷史可能佔用 2,000~5,000 Token,這在每次呼叫中都會計費。選擇合適的視窗大小是體驗與成本的平衡。
Step 7:記憶儲存後端
目前使用的 InMemoryChatMemory 把對話存在記憶體中——應用重啟就消失了。正式環境需要持久化儲存[1]:
| 儲存方式 | 適用場景 | 特點 |
|---|---|---|
| InMemoryChatMemory | 開發 / 測試 | 預設,重啟即消失 |
| JdbcChatMemory | 正式環境(關聯式資料庫) | PostgreSQL / MySQL,易於查詢與管理 |
| CassandraChatMemory | 大規模分散式系統 | 高可用、高擴展 |
| Neo4jChatMemory | 需要關係圖譜的場景 | 圖資料庫,適合社交/推薦 |
切換儲存後端只需替換一行程式碼——這就是 Spring AI 抽象層的威力:
// 開發環境:記憶體儲存
val devMemory = MessageWindowChatMemory.builder()
.maxMessages(20)
.build()
// 正式環境:JDBC 持久化(以 PostgreSQL 為例)
// val prodMemory = MessageWindowChatMemory.builder()
// .chatMemoryRepository(JdbcChatMemoryRepository(jdbcTemplate))
// .maxMessages(20)
// .build()
// ChatClient 的使用方式完全不變!
val client = ChatClient.builder(chatModel)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(devMemory).build())
.build()
Step 8:實戰——帶記憶的學習助手
結合本課所有概念,打造一個能記住學生姓名、學習進度的 Spring AI 學習助手:
val learningMemory = MessageWindowChatMemory.builder()
.maxMessages(30)
.build()
val learningClient = ChatClient.builder(chatModel)
.defaultSystem("""你是「Spring AI 學習助手」,一位耐心且專業的程式設計老師。
你會記住學生的名字、學習進度和偏好。
回答時使用繁體中文,適當使用程式碼範例。
當學生問到之前討論過的概念時,引用之前的對話內容。""")
.defaultAdvisors(MessageChatMemoryAdvisor.builder(learningMemory).build())
.build()
fun chat(sessionId: String, message: String): String {
return learningClient.prompt()
.user(message)
.advisors { it.param(ChatMemory.CONVERSATION_ID, sessionId) }
.call().content() ?: "(無回應)"
}
// 模擬學習對話
val sid = "student-001"
println(chat(sid, "嗨!我叫 Alex,我剛學完 Lesson 4 的 Function Calling。"))
println()
println(chat(sid, "我對 @Tool 的 description 怎麼寫比較好還不太確定。"))
println()
println(chat(sid, "可以幫我總結一下我目前的學習狀況嗎?"))
預期輸出(第三輪):
當然,Alex!根據我們的對話,你目前的學習狀況:
📚 已完成進度:到 Lesson 4(Function Calling)
🔍 目前疑問:@Tool 的 description 撰寫技巧
💡 建議下一步:Lesson 5(ChatMemory)你正在學了!
關於你的 @Tool description 問題,記住三個原則:
1. 寫給 AI 看,不是給人看
2. 包含功能說明 + 回傳內容 + 參數格式
3. 具體 > 抽象(「根據訂單編號查詢」比「查詢資料」好)
本課重點回顧
| 概念 | 重點 |
|---|---|
| AI 無狀態 | 每次 API 呼叫獨立,不會自動記住對話 |
| Advisor | AOP 風格的中介層,AI 呼叫前後自動注入邏輯 |
| MessageWindowChatMemory | 滑動視窗策略,保留最近 N 條訊息 |
| MessageChatMemoryAdvisor | 將記憶自動注入每次 AI 呼叫 |
| conversationId | 多會話隔離,不同使用者記憶互不干擾 |
| 視窗大小 | 5~10(簡單)、15~20(一般)、30~50(複雜) |
| 持久化儲存 | JDBC / Cassandra / Neo4j,切換只需一行 |
下一步:Lesson 6 — RAG 基礎
有了記憶後,AI 可以記住對話脈絡。但如果要讓 AI 存取企業專屬知識(產品文件、SOP、技術規格),就需要 RAG(檢索增強生成)。下一堂課將介紹:
- 向量嵌入(Embedding) — 把文字轉換為數學向量
- 向量資料庫 — 高效的語意搜尋引擎
- RAG Pipeline — 從文件匯入到智能問答的完整流程



