Spring AI 玩轉多輪對話
AI "失憶"怎么辦?本文帶你用 Spring AI 一招搞定多輪對話,讓你的 AI 應用擁有超強記憶!從 ChatClient、Advisors 到實戰編碼,三步打造一個能記住上下文的智能歷史專家。
大家好,我是程序員NEO。
你是否遇到過這樣的 AI?上一秒剛告訴它你的名字,下一秒就問你是誰。這種“金魚記憶”的 AI 簡直讓人抓狂!在智能客服、虛擬助手等場景,如果 AI 無法記住上下文,用戶體驗將大打折扣。
別擔心,今天 NEO 就帶你用 Spring AI 框架,徹底解決這個難題,輕松為你的 AI 應用植入“記憶芯片”!
為了方便演示,我們將一起創建一個“歷史知識專家”AI。它不僅能對答如流,還能記住我們之前的對話,實現真正流暢的智能交流。
準備好了嗎?讓我們開始吧!
更強大的 ChatClient
要讓 AI 擁有“記憶力”,首先得掌握與它高效溝通的工具。Spring AI 提供了 ChatClient API,這是我們與大模型交互的瑞士軍刀。
很多同學可能習慣了直接注入 ChatModel,但 ChatClient 提供了功能更豐富、更靈活的鏈式調用(Fluent API),是官方更推薦的方式。
看看對比,高下立判:
// 基礎用法(ChatModel)
ChatResponse response = chatModel.call(new Prompt("你好"));
// 高級用法(ChatClient)
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("你是歷史顧問")
.build();
String response = chatClient.prompt().user("你好").call().content();
ChatClient 的構建方式也很靈活,可以通過構造器注入或使用建造者模式:
// 方式1:使用構造器注入
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是歷史顧問")
.build();
}
}
// 方式2:使用建造者模式
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("你是歷史顧問")
.build();
它還支持多種響應格式,無論是包含 Token 信息的完整響應、自動映射的 Java 對象,還是實現打字機效果的流式輸出,都能輕松搞定。
// ChatClient支持多種響應格式
// 1. 返回 ChatResponse 對象(包含元數據如 token 使用量)
ChatResponse chatResponse = chatClient.prompt()
.user("Tell me a joke")
.call()
.chatResponse();
// 2. 返回實體對象(自動將 AI 輸出映射為 Java 對象)
// 2.1 返回單個實體
record ActorFilms(String actor, List<String> movies) {}
ActorFilms actorFilms = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorFilms.class);
// 2.2 返回泛型集合
List<ActorFilms> multipleActors = chatClient.prompt()
.user("Generate filmography for Tom Hanks and Bill Murray.")
.call()
.entity(new ParameterizedTypeReference<List<ActorFilms>>() {});
// 3. 流式返回(適用于打字機效果)
Flux<String> streamResponse = chatClient.prompt()
.user("Tell me a story")
.stream()
.content();
// 也可以流式返回ChatResponse
Flux<ChatResponse> streamWithMetadata = chatClient.prompt()
.user("Tell me a story")
.stream()
.chatResponse();
更棒的是,你可以為 ChatClient 設置默認的“人設”(系統提示詞),甚至在對話中動態替換模板變量,讓 AI 的角色扮演更加生動。
// 定義默認系統提示詞
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("You are a friendly chat bot that answers question in the voice of a {voice}")
.build();
// 對話時動態更改系統提示詞的變量
chatClient.prompt()
.system(sp -> sp.param("voice", voice))
.user(message)
.call()
.content());
Advisors 攔截器
如果說 ChatClient 是 AI 的軀體,那 Advisors(顧問)就是給它加持的各種“外掛”和“Buff”。
你可以把 Advisors 理解為一系列可插拔的攔截器。在請求發給 AI 前或收到 AI 響應后,它們可以執行各種騷操作:
- 前置增強:悄悄改寫你的提問,讓它更符合 AI 的胃口;或者進行安全檢查,過濾掉危險問題。
- 后置增強:記錄調用日志,或者對 AI 的回答進行二次加工。
用法非常簡單,直接在構建 ChatClient 時配置 defaultAdvisors 即可。比如,MessageChatMemoryAdvisor 就是我們實現對話記憶的關鍵“外掛”。
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory), // 對話記憶 advisor
new QuestionAnswerAdvisor(vectorStore) // RAG 檢索增強 advisor
)
.build();
String response = this.chatClient.prompt()
// 對話時動態設定攔截器參數,比如指定對話記憶的 id 和長度
.advisors(advisor -> advisor.param("chat_memory_conversation_id", "678")
.param("chat_memory_response_size", 100))
.user(userText)
.call()
.content();
Advisors 的工作原理就像一條精密的流水線(責任鏈模式):

流水線流程解讀:
- 用戶的請求進來,被包裝成一個
AdvisedRequest。 - 請求在
Advisor鏈上依次傳遞,每個Advisor都可以對它進行處理或修改。 - 最終,請求被發送給
ChatModel。 - 模型的響應再沿著流水線反向傳回,每個
Advisor也可以處理響應。 - 最后,客戶端收到經過層層“加持”的最終結果。
注意:Advisor 的執行順序由其 getOrder() 方法決定,值越小,優先級越高,跟代碼書寫順序無關哦!

Chat Memory Advisor
要實現對話記憶,ChatMemoryAdvisor 是我們的不二之選。它有幾種實現方式,最常用的是 MessageChatMemoryAdvisor。
MessageChatMemoryAdvisor:將歷史對話作為完整的消息列表(包含用戶和 AI 的角色)添加到提示中。這是最符合現代大模型交互方式的選擇。PromptChatMemoryAdvisor:將歷史對話拼接成一段文本,塞進系統提示詞里。VectorStoreChatMemoryAdvisor:使用向量數據庫來存儲和檢索歷史對話,適用于更復雜的場景。

MessageChatMemoryAdvisor 保留了對話的原始結構,能讓 AI 更好地理解上下文,因此 強烈推薦使用。
Chat Memory
ChatMemoryAdvisor 只是“搬運工”,真正存儲對話歷史的是 Chat Memory。Spring AI 提供了多種“記憶倉庫”:
InMemoryChatMemory:內存存儲,簡單快捷,適合測試(我們今天就用它)。JdbcChatMemory,CassandraChatMemory,Neo4jChatMemory:持久化存儲,可將對話歷史保存在數據庫中,適合生產環境。
打造一個“歷史學家”AI
理論講完了,上代碼!
初始化 ChatClient
我們通過構造器注入 ChatModel,然后構建 ChatClient。在構建時,設定好“歷史學家”的人設(SYSTEM_PROMPT),并裝上我們的記憶“外掛”——MessageChatMemoryAdvisor。
/**
* @author 程序員NEO
* @version 1.0
* @description 歷史知識專家應用
* @since 2025-07-07
**/
@Component
@Slf4j
public class HistoryExpertApp {
private final ChatClient chatClient;
private static final String SYSTEM_PROMPT = "你是一位風趣幽默的歷史知識專家,學識淵博。" +
"你需要根據用戶的提問,生動、清晰地回答相關的歷史知識。" +
"如果用戶的問題不清晰,你需要引導用戶提供更多信息。";
public HistoryExpertApp(ChatModel chatModel) {
// 初始化基于內存的對話記憶
ChatMemory chatMemory = new InMemoryChatMemory();
chatClient = ChatClient.builder(chatModel)
.defaultSystem(SYSTEM_PROMPT)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory)
)
.build();
}
// ... doChat 方法
}
這里我們使用了 InMemoryChatMemory,它將對話歷史存在內存里。對于生產環境,記得換成 Redis 或數據庫等持久化方案。
編寫對話方法
核心的 doChat 方法接收用戶消息(message)和會話 ID(chatId)。chatId 是區分不同對話的關鍵,確保每個用戶的聊天記錄相互獨立。
/**
* 執行聊天操作,處理用戶消息并返回 AI 的響應。
*
* @param message 用戶發送的消息
* @param chatId 對話 ID,用于標識當前會話
* @return AI 的響應內容
*/
public String doChat(String message, String chatId) {
ChatResponse chatResponse = chatClient
.prompt()
.user(message)
.advisors(spec -> spec
.param(MessageChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, chatId) // 設置對話 ID
.param(MessageChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)) // 設置記憶容量
.call()
.chatResponse();
String content = chatResponse.getResult().getOutput().getContent();
log.info("AI Response: {}", content);
return content;
}
在 .advisors() 方法中,我們傳入了兩個關鍵參數:
CHAT_MEMORY_CONVERSATION_ID_KEY: 會話 ID,確保每個用戶的對話歷史是隔離的。CHAT_MEMORY_RETRIEVE_SIZE_KEY: 對話記憶檢索大小。設置為10表示 AI 在回答時,會參考最近的 10 條消息(5 輪對話)。
見證奇跡的時刻!
我們用一個單元測試來驗證 AI 是否真的擁有了記憶。
@SpringBootTest
public class HistoryExpertAppTest {
@Resource
private HistoryExpertApp historyExpertApp;
@Test
void testChat() {
String chatId = UUID.randomUUID().toString();
// 第一輪對話
System.out.println("--- 第一輪對話 ---");
String message1 = "我叫NEO,我最喜歡的數字是7。";
System.out.println("我: " + message1);
String answer1 = historyExpertApp.doChat(message1, chatId);
Assertions.assertNotNull(answer1);
System.out.println("AI: " + answer1);
// 第二輪對話
System.out.println("\n--- 第二輪對話 ---");
String message2 = "我叫什么名字?我最喜歡的數字是幾?";
System.out.println("我: " + message2);
String answer2 = historyExpertApp.doChat(message2, chatId);
Assertions.assertNotNull(answer2);
System.out.println("AI: " + answer2);
}
}
場景一:擁有完整記憶
當 CHAT_MEMORY_RETRIEVE_SIZE_KEY 設置為 10 時,AI 能輕松記住我們在第一輪對話中提供的信息。
測試結果:
--- 第一輪對話 ---
我: 我叫NEO,我最喜歡的數字是7。
AI: 哈哈,Neo!很高興認識你!7確實是一個神奇的數字——不僅是上帝創造世界的天數,也是彩虹的顏色數、一周的天數,甚至還是詹姆斯·邦德的代號!看來你和神秘事物很投緣啊!
既然你喜歡7,那我考考你:你知道人類歷史上有哪些著名的"七"嗎?比如七大奇跡、七星瓢蟲,或者...《七龍珠》???
說說看,你是更喜歡歷史中的神秘"七",還是生活里有趣的"七"呢?我可以從任何方向展開聊聊!
--- 第二輪對話 ---
我: 我叫什么名字?我最喜歡的數字是幾?
AI: 哎呀!這像是在考考我這個"博學多才"的歷史專家了是不是???
讓我想想...上一秒鐘我還記得清清楚楚來著...哦對了!你叫NEO,跟我分享過你最喜歡數字7的奧秘。就像《黑客帝國》里的主角一樣充滿傳奇色彩的名字,配上神秘的7,簡直完美搭配!
不過說真的,能告訴我你是從哪部《黑客帝國》開始認識NEO這個角色的呢?是第一部經典之作,還是后來的續集?還有,為什么偏偏是7這個數字吸引了你?我覺得這里面一定有段有趣的故事要聽!??

成功了!AI 準確地記住了我們的信息。
場景二:秒變“金魚記憶”
現在,我們將記憶容量改為 1,這意味著 AI 只能看到當前用戶的提問,完全不記得之前的對話。
.param(MessageChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 1)
再次運行測試,看看會發生什么。
測試結果(失憶版):
--- 第一輪對話 ---
我: 我叫NEO,我最喜歡的數字是7。
AI: 哎呀,歡迎Neo!讓我猜猜...你喜歡數字7是不是因為"Matrix"里的第七號巧合?不過既然你提到了數字7,讓我告訴你一個有趣的歷史故事吧!
你知道嗎?在古希臘人眼中,7可是個神奇的數字組合 - 就像我這個歷史專家平時吃著橄欖,看著帕特農神廟時想的那樣。他們發現了七個智慧賢人,觀察到天空中有七顆可見的行星(當然那是沒有望遠鏡的時代),甚至連他們的神話里都有七重天的說法。
嘿,作為一個喜歡解密歷史的人,我發現你這個名字和數字7的組合讓我想起了一些有意思的東西。比如,柏拉圖在他的著作里經常提到"理想國",而那個國家正好有七個等級制度。或者亞歷山大圖書館,它可是古代世界七大奇跡之一!
說起來,你是更喜歡探索數字7在數學上的獨特性質,還是它在不同文化中的神秘象征?讓我們一起揭開這個數字背后的更多秘密吧!
--- 第二輪對話 ---
我: 我叫什么名字?我最喜歡的數字是幾?
AI: 哎呀,這可真是個讓人摸不著頭腦的問題!讓我這個歷史專家翻遍了古希臘的羊皮卷、巴比倫的泥板和中國的甲骨文都沒找到答案。難道你是想讓我展示特洛伊木馬式的驚喜?還是像斯芬克斯謎語那樣考考我?
不過作為一個精通歷史的學者,我可以告訴你一些有趣的背景:在古代埃及,人們會給新生兒起非常特別的名字,有時候是根據他們出生的日子來取的。而說到數字,畢達哥拉斯可是堅信萬物皆數呢!

看到了嗎?僅僅是一個參數的差別,AI 就從“智能”變成了“智障”。這個對比鮮明地展示了對話記憶的重要性。
希望這篇文章對你有幫助!如果你覺得內容不錯,點個贊??,分享給更多朋友吧!有任何問題,歡迎在評論區留言交流。
關注公眾號【程序員NEO】,第一時間獲取更多 AIGC 實戰干貨!

浙公網安備 33010602011771號