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

相关新闻