
Spring AI 对话记忆:MessageChatMemoryAdvisor 最小接入
假设你做了一个客服 AI。
用户第一轮问:
线上服务怎么申请扩容?
模型回答完以后,用户第二轮接着问:
我刚才问的是什么?
如果这时模型说"不知道",不是它记性差。
而是大模型本身是无状态的。
第二次调用时,你没有把上一轮对话带进去,它当然接不上前文。
很多 Java 开发者会在 Service 里手动处理:
text
查历史消息
→ 拼到 prompt
→ 调模型
→ 保存本轮问题和回答
能跑,但项目一大就麻烦。
客服助手要记忆,知识库问答要记忆,日志分析 Agent 也要记住上下文。每个入口都手写一遍,最后业务代码会变成"对话记忆处理中心"。
Spring AI 里的 MessageChatMemoryAdvisor,就是为了解决这个问题。
它不是让模型真的拥有长期记忆,而是在每次调用前后,帮你维护当前会话需要的上下文。
一、先别手动拼历史消息
最常见的手写方式大概是这样:
java
List<Message> history = chatHistoryRepository.findRecentMessages(userId, 10);
List<Message> messages = new ArrayList<>();
messages.addAll(history);
messages.add(new UserMessage(question));
Prompt prompt = new Prompt(messages);
ChatResponse response = chatModel.call(prompt);
chatHistoryRepository.save(userId, question, response);
这段代码的问题不是不能跑,而是不适合长期维护。
第一,容易重复。
每个 AI 入口都要查历史、拼消息、保存回复。
第二,容易漏。
有的接口只查了历史,忘了保存本轮回复;有的只按 userId 查,用户开多个会话就串了。
第三,策略不好统一。
今天保留最近 5 轮,明天改成 10 轮,后天想做摘要压缩。如果逻辑散在各个 Service 里,改起来很累。
更合理的做法是:
业务代码只负责提问,对话记忆交给统一的 Advisor 处理。
二、Spring AI 怎么拆这件事
Spring AI 的对话记忆不是一个类包办,而是几层分工:
text
MessageChatMemoryAdvisor
→ 在 ChatClient 调用前后介入
ChatMemory
→ 决定给模型带哪些历史消息
MessageWindowChatMemory
→ 按窗口保留最近消息
ChatMemoryRepository
→ 负责存储和读取消息
这里最容易混的是 ChatMemory 和 ChatMemoryRepository。
ChatMemory 管的是"给模型看的上下文"。
它关心的是:
下一次调用模型时,要带哪些历史消息?
ChatMemoryRepository 管的是消息存储。
Spring AI 1.1.7 默认会自动配置:
text
MessageWindowChatMemory + InMemoryChatMemoryRepository
MessageWindowChatMemory 默认最多保留 20 条消息。
注意,是 20 条消息,不是 20 轮对话。
一轮对话通常包含一条用户消息和一条助手消息,所以 20 条消息大概就是最近 10 轮左右。超过窗口后,较早的消息会被移出,但 system message 会保留。
InMemoryChatMemoryRepository 是内存存储,适合 Demo。
生产环境别直接依赖它。应用一重启,历史就没了;多实例部署时,每个实例也各存各的。
三、最小接入方式
如果你的项目已经能正常注入 ChatModel,接入对话记忆主要三步。
1. 准备 ChatMemory
只跑 Demo,可以先用内存版:
java
@Configuration
public class ChatMemoryConfig {
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.maxMessages(20)
.chatMemoryRepository(new InMemoryChatMemoryRepository())
.build();
}
}
如果你不声明自己的 ChatMemory,Spring AI 也会按默认规则自动配置一套。
这里显式写出来,是为了看清两个配置:
maxMessages(20):控制窗口大小;chatMemoryRepository(...):控制消息存在哪里。
2. 配置 MessageChatMemoryAdvisor
接着把 MessageChatMemoryAdvisor 配到 ChatClient:
java
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory) {
return ChatClient.builder(chatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
}
配置完成后,每次通过这个 ChatClient 调用模型,Advisor 都会参与。
它会在调用前读取历史消息,把历史作为 Message 注入请求,同时记录本轮用户消息;调用后,再把模型回复写回记忆。
这点很重要:
MessageChatMemoryAdvisor不是把历史拼成一大段字符串,而是把历史作为消息列表交给模型。
3. 调用时传 conversationId
业务代码可以保持很干净:
java
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatClient chatClient) {
this.chatClient = chatClient;
}
public String chat(String conversationId, String question) {
return chatClient.prompt()
.user(question)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();
}
}
重点是这一行:
java
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
在 Spring AI 1.1.7 里,内置记忆 Advisor 必须传 ChatMemory.CONVERSATION_ID。
不传会抛 IllegalArgumentException。
原因很简单:Advisor 必须知道这次调用属于哪个会话,才能读取和维护对应的历史消息。
四、两轮对话时发生了什么
第一次调用:
java
chatService.chat("conv-001", "线上服务怎么申请扩容?");
这时 conv-001 还没有历史。
Advisor 会把当前用户问题发给模型,并把这条用户消息记下来。模型返回后,再把助手回复写入同一个会话。
第二次调用:
java
chatService.chat("conv-001", "我刚才问的是什么?");
这次 Advisor 会先取出 conv-001 的历史消息。
模型看到的就不只是当前问题,而是:
text
上一轮用户问题
上一轮模型回复
当前用户问题
所以它才能回答:
你刚才问的是线上服务怎么申请扩容。
业务代码没有手动查历史,也没有手动拼 prompt。
这些都交给 Advisor 处理了。
五、conversationId 别乱传
conversationId 是对话记忆里最容易踩坑的点。
不建议直接用 userId。
因为一个用户可能同时有多个会话。
比如他在客服助手里问扩容,在知识库助手里问报销,又在日志分析 Agent 里查异常。如果都用同一个 userId,历史就会混在一起。
更合适的做法是给每段连续对话一个稳定 ID:
- Web 场景:
chatId或sessionId - App 场景:
threadId或conversationUUID - 多 Agent 场景:
userId + agentType + chatId
也不要每次请求都重新生成一个新的 conversationId。
那样每次都是新会话,模型当然接不上前文。
记住一句话:
同一段连续对话里,conversationId 必须稳定;不同会话之间,conversationId 必须隔离。
六、生产环境注意两件事
1. 换掉内存存储
InMemoryChatMemoryRepository 适合本地开发,不适合生产。
它有三个问题:
- 应用重启后历史消息丢失;
- 多实例部署时,每个实例各存各的;
- 不方便统一排查和运维。
Spring AI 提供了多种 ChatMemoryRepository 实现,比如 JDBC、MongoDB、Neo4j、Cassandra、Cosmos DB。
如果你们公司已经有统一存储,也可以自己实现 ChatMemoryRepository。
2. 不要把 ChatMemory 当完整聊天记录表
ChatMemory 管的是"给模型看的上下文"。
它不是完整聊天记录库。
如果你需要用户查看历史、后台审计、客服质检、数据分析,建议单独设计业务聊天记录表。
可以这样分工:
text
ChatMemory
→ 给模型看的短期上下文
业务聊天记录表
→ 给用户、后台、审计和分析看的完整记录
这两个东西不要混在一起。
七、排查问题先看这三点
如果第二轮还是"记不住",先查三件事:
- 两轮调用的
conversationId是否一致; ChatClient有没有配置MessageChatMemoryAdvisor;- 是否用了内存存储,并且应用重启或部署了多个实例。
另外,maxMessages=20 不是 20 轮对话,而是 20 条消息。
记忆窗口也不是越大越好。
历史越多,上下文越长,成本越高,也更容易把模型带偏。一般先从默认窗口跑通,再根据实际效果调整。
写在最后
对话记忆的本质很简单:
每次调用前,把当前会话需要的历史消息带进去。
Spring AI 把这件事拆成了几层:
text
ChatMemory
→ 管理记忆窗口
ChatMemoryRepository
→ 存储和读取消息
MessageChatMemoryAdvisor
→ 调用前后维护记忆
conversationId
→ 区分不同会话
业务代码不用在每个 Service 里查历史、拼 prompt、保存回复。
把记忆交给 MessageChatMemoryAdvisor。
把会话隔离交给 conversationId。
把生产存储换成持久化 ChatMemoryRepository。
这套链路理清以后,Spring AI 的对话记忆就不难了。
我是 Dilee,11 年 Java 老兵,专注 AI 落地应用。
后续会继续更新 Spring AI、RAG、Memory、Tool Calling、MCP 等实战内容。
完整系列也会同步整理在公众号「AI Agent 实战有术」。