聊天记忆机制
本教程共 45 篇 · 第 21 篇 · 更新于 2026-08-16 · 约 7 分钟阅读
本节目标:搞懂 Spring AI 的记忆抽象 ChatMemory 和记忆注入机制。学完你能说清聊天记忆与聊天历史的区别,会用 MessageChatMemoryAdvisor 给 ChatClient 装上多轮记忆。
21.1 模型不记事
先做个实验。问模型”我叫张三”,它答得很好。紧接着问”我叫什么名字”,它一脸茫然。这不是模型笨,是设计如此。每次调用大模型,请求都是独立的,模型不保留上一轮的对话。官方文档的原话:LLM 是无状态的。
无状态带来一个直接问题:没法做多轮对话。客服机器人、AI 助手这类产品,用户希望它记得刚才聊了什么。方案是自己维护历史消息,每次请求时一起发给模型。这就是聊天记忆要解决的事。
21.2 聊天记忆与聊天历史
动手之前,先分清两个概念。官方文档专门强调过它们的区别。
聊天记忆(Chat Memory),是模型在对话中保留、用来维持上下文感知的信息。它服务于当前对话,不需要全部历史,只要”相关的那部分”。
聊天历史(Chat History),是完整的对话记录,包含用户和模型交换过的所有消息。它用于存档、审计、回溯,讲究完整。
ChatMemory 抽象管理的是前者。它存储和检索与当前上下文相关的消息。要保存完整历史,官方建议另用方案,比如基于 Spring Data 自己做存储。简单记:记忆是精选,历史是全量。
21.3 ChatMemory 接口
Spring AI 的记忆核心是一个接口,在 org.springframework.ai.chat.memory 包里:
public interface ChatMemory {
String CONVERSATION_ID = "chat_memory_conversation_id";
void add(String conversationId, Message message);
void add(String conversationId, List<Message> messages);
List<Message> get(String conversationId);
void clear(String conversationId);
}
四个方法各管一件事。add 把消息写进指定会话,get 把某个会话的历史取出来,clear 清空某个会话。所有方法都以 conversationId 为钥匙,区分不同的对话。
和它配套的还有 ChatMemoryRepository 接口,专职存储和读取消息。两个角色分得很清:Repository 只负责消息的存取,ChatMemory 决定留哪些消息、什么时候删。比如保留最近 N 条、保留一段时间内的消息、按 token 总量限制。留哪些、删哪些,都是策略问题,交给 ChatMemory 的实现去定。
21.4 记忆与上下文窗口
为什么要裁剪?因为模型有上下文窗口。窗口是模型一次能处理的 token 上限,常见的有 8K、32K、128K 不等。对话越长,历史消息占的 token 越多,早晚顶到上限。
顶到上限会怎样?请求直接报错,或者被截断。更实际的问题是成本:大模型按 token 计费,历史越长越贵。把全部历史每轮都塞给模型,既不经济也不可行。
所以记忆要做取舍。刚发生的对话最相关,几轮之前的逐渐过时。滑动窗口保留最近 N 条,是平衡效果与成本的常用策略,下一章讲它的实现。
算一笔账就明白了。假设每轮对话平均消耗 500 token,聊了 20 轮,全量历史就是 10000 token。模型窗口只有 8K 时,第 21 轮就塞不进去了。只保留最近 10 轮,请求稳定在 5000 token 左右,效果和成本都可控。
21.5 Advisor 把记忆注入对话
有了记忆对象,还得让它参与每次请求。第 5 章讲过,Advisor 是 ChatClient 的扩展点,挂在调用链上,请求前响应后都能动手。记忆正是靠 Advisor 生效的。
官方提供两个记忆类 Advisor。MessageChatMemoryAdvisor 用 ChatMemory 管理会话记忆,每次交互把历史取出来,作为消息列表塞进请求。VectorStoreChatMemoryAdvisor 用向量库管理记忆,把检索到的历史拼进系统消息,适合海量历史,RAG 相关章节再展开。
MessageChatMemoryAdvisor 的流程:请求进来,按 conversationId 取历史,拼在用户消息前面一起发给模型;响应回来,把这一轮的消息写回记忆。存取全自动,业务代码不用碰。
拼装后的消息列表大致是这样:系统消息在最前,接着是历史对话,最后是当前这轮的用户消息。模型看到完整的前因后果,回答自然连贯。这也是为什么记忆必须挂在 Advisor 上而不是在业务代码里手拼,顺序和时机都由框架保证。
21.6 在 ChatClient 里用起来
先建记忆,再挂 Advisor,最后在请求里传 conversationId:
ChatMemory chatMemory = MessageWindowChatMemory.builder().build();
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
String conversationId = "user-001";
chatClient.prompt()
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.user("我叫张三")
.call()
.content();
String answer = chatClient.prompt()
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.user("我叫什么名字?")
.call()
.content();
第二次回答会正确说出”张三”。这里手动建了 ChatMemory,是为了把配置讲清楚。实际项目里 Spring AI 会自动配置默认的 ChatMemory Bean,直接注入就行。
conversationId 不能省。所有记忆 Advisor 都强制要求这个参数,漏了会在运行时抛 IllegalArgumentException。它没有默认值,每个会话都得显式指定。
流式调用同样支持记忆。把 call() 换成 stream(),Advisor 照常工作,因为 MessageChatMemoryAdvisor 同时实现了同步和流式两个接口。
想验证记忆有没有生效,把日志级别打开:
logging.level.org.springframework.ai.chat.client.advisor=DEBUG
控制台会打印每次请求的完整消息列表,历史消息有没有拼进去一目了然。调试完记得关掉,避免日志泄露对话内容。
Note记忆 Advisor 目前不保存工具调用过程中的中间消息,这是官方文档标注的已知限制。需要保存这类消息时,可参考工具调用章节的用户控制工具执行方案。
21.7 不用 ChatClient 的手动方案
如果直接操作 ChatModel,就得自己管理记忆。流程是:消息加进记忆,取历史构造 Prompt,调用模型,再把响应写回记忆。官方示例改写成这样:
ChatMemory chatMemory = MessageWindowChatMemory.builder().build();
String conversationId = "007";
UserMessage userMessage1 = new UserMessage("我的名字是詹姆斯");
chatMemory.add(conversationId, userMessage1);
ChatResponse response1 = chatModel.call(new Prompt(chatMemory.get(conversationId)));
chatMemory.add(conversationId, response1.getResult().getOutput());
UserMessage userMessage2 = new UserMessage("我叫什么名字?");
chatMemory.add(conversationId, userMessage2);
ChatResponse response2 = chatModel.call(new Prompt(chatMemory.get(conversationId)));
chatMemory.add(conversationId, response2.getResult().getOutput());
第二轮响应里会出现”詹姆斯”。这套手动流程和 Advisor 内部做的事一样,Advisor 只是帮你封装了。日常开发用 ChatClient 就够了。
21.8 小结
模型无状态,多轮对话要靠外部记忆。聊天记忆是精选的上下文,聊天历史是完整的记录。ChatMemory 管取舍,ChatMemoryRepository 管存取。MessageChatMemoryAdvisor 把记忆自动注入每次请求,conversationId 是会话的钥匙。下一章看默认的内存实现和滑动窗口。