news 2026/6/18 20:30:51

Spring AI 对话记忆入门:让模型记住上一轮问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring AI 对话记忆入门:让模型记住上一轮问题

Spring AI 对话记忆入门:让模型记住上一轮问题

假设你做了一个客服 AI。

用户第一轮问:

线上服务怎么申请扩容?

模型回答完以后,用户第二轮接着问:

我刚才问的是什么?

如果这时模型说“不知道”,不是它记性差。

而是大模型本身是无状态的。

第二次调用时,你没有把上一轮对话带进去,它当然接不上前文。

很多 Java 开发者会在 Service 里手动处理:

查历史消息 → 拼到 prompt → 调模型 → 保存本轮问题和回答

能跑,但项目一大就麻烦。

客服助手要记忆,知识库问答要记忆,日志分析 Agent 也要记住上下文。每个入口都手写一遍,最后业务代码会变成“对话记忆处理中心”。

Spring AI 里的MessageChatMemoryAdvisor,就是为了解决这个问题。

它不是让模型真的拥有长期记忆,而是在每次调用前后,帮你维护当前会话需要的上下文。


一、先别手动拼历史消息

最常见的手写方式大概是这样:

List<Message>history=chatHistoryRepository.findRecentMessages(userId,10);List<Message>messages=newArrayList<>();messages.addAll(history);messages.add(newUserMessage(question));Promptprompt=newPrompt(messages);ChatResponseresponse=chatModel.call(prompt);chatHistoryRepository.save(userId,question,response);

这段代码的问题不是不能跑,而是不适合长期维护。

第一,容易重复。

每个 AI 入口都要查历史、拼消息、保存回复。

第二,容易漏。

有的接口只查了历史,忘了保存本轮回复;有的只按userId查,用户开多个会话就串了。

第三,策略不好统一。

今天保留最近 5 轮,明天改成 10 轮,后天想做摘要压缩。如果逻辑散在各个 Service 里,改起来很累。

更合理的做法是:

业务代码只负责提问,对话记忆交给统一的 Advisor 处理。


二、Spring AI 怎么拆这件事

Spring AI 的对话记忆不是一个类包办,而是几层分工:

MessageChatMemoryAdvisor → 在 ChatClient 调用前后介入 ChatMemory → 决定给模型带哪些历史消息 MessageWindowChatMemory → 按窗口保留最近消息 ChatMemoryRepository → 负责存储和读取消息

这里最容易混的是ChatMemoryChatMemoryRepository

ChatMemory管的是“给模型看的上下文”。

它关心的是:

下一次调用模型时,要带哪些历史消息?

ChatMemoryRepository管的是消息存储。

Spring AI 1.1.7 默认会自动配置:

MessageWindowChatMemory + InMemoryChatMemoryRepository

MessageWindowChatMemory默认最多保留 20 条消息。

注意,是 20 条消息,不是 20 轮对话。

一轮对话通常包含一条用户消息和一条助手消息,所以 20 条消息大概就是最近 10 轮左右。超过窗口后,较早的消息会被移出,但 system message 会保留。

InMemoryChatMemoryRepository是内存存储,适合 Demo。

生产环境别直接依赖它。应用一重启,历史就没了;多实例部署时,每个实例也各存各的。


三、最小接入方式

如果你的项目已经能正常注入ChatModel,接入对话记忆主要三步。

1. 准备 ChatMemory

只跑 Demo,可以先用内存版:

@ConfigurationpublicclassChatMemoryConfig{@BeanpublicChatMemorychatMemory(){returnMessageWindowChatMemory.builder().maxMessages(20).chatMemoryRepository(newInMemoryChatMemoryRepository()).build();}}

如果你不声明自己的ChatMemory,Spring AI 也会按默认规则自动配置一套。

这里显式写出来,是为了看清两个配置:

  • maxMessages(20):控制窗口大小;
  • chatMemoryRepository(...):控制消息存在哪里。

2. 配置 MessageChatMemoryAdvisor

接着把MessageChatMemoryAdvisor配到ChatClient

@ConfigurationpublicclassChatClientConfig{@BeanpublicChatClientchatClient(ChatModelchatModel,ChatMemorychatMemory){returnChatClient.builder(chatModel).defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()).build();}}

配置完成后,每次通过这个ChatClient调用模型,Advisor 都会参与。

它会在调用前读取历史消息,把历史作为Message注入请求,同时记录本轮用户消息;调用后,再把模型回复写回记忆。

这点很重要:

MessageChatMemoryAdvisor不是把历史拼成一大段字符串,而是把历史作为消息列表交给模型。

3. 调用时传 conversationId

业务代码可以保持很干净:

@ServicepublicclassChatService{privatefinalChatClientchatClient;publicChatService(ChatClientchatClient){this.chatClient=chatClient;}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,历史就会混在一起。

更合适的做法是给每段连续对话一个稳定 ID:

  • Web 场景:chatIdsessionId
  • App 场景:threadIdconversationUUID
  • 多 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
  • 是否用了内存存储,并且应用重启或部署了多个实例。

另外,maxMessages=20不是 20 轮对话,而是 20 条消息。

记忆窗口也不是越大越好。

历史越多,上下文越长,成本越高,也更容易把模型带偏。一般先从默认窗口跑通,再根据实际效果调整。


写在最后

对话记忆的本质很简单:

每次调用前,把当前会话需要的历史消息带进去。

Spring AI 把这件事拆成了几层:

ChatMemory → 管理记忆窗口 ChatMemoryRepository → 存储和读取消息 MessageChatMemoryAdvisor → 调用前后维护记忆 conversationId → 区分不同会话

业务代码不用在每个 Service 里查历史、拼 prompt、保存回复。

把记忆交给MessageChatMemoryAdvisor

把会话隔离交给conversationId

把生产存储换成持久化ChatMemoryRepository

这套链路理清以后,Spring AI 的对话记忆就不难了。


我是 Dilee,11 年 Java 老兵,专注 AI 落地应用。

关注我,后续会继续更新 Spring AI、RAG、Memory、Tool Calling、MCP 等实战内容。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/18 22:26:39

Python字节码反编译工具pycdc:如何突破Python 3.13的技术壁垒

Python字节码反编译工具pycdc&#xff1a;如何突破Python 3.13的技术壁垒 【免费下载链接】pycdc C python bytecode disassembler and decompiler 项目地址: https://gitcode.com/GitHub_Trending/py/pycdc pycdc是一个用C编写的Python字节码反编译器和反汇编器&#x…

作者头像 李华
网站建设 2026/6/18 20:13:55

l33t-hoster

这个题做了两天了拼尽全力卡在最后一步 最后决定先放放 感觉是平台flag有点问题&#xff08;之前有个题也是flag为空&#xff09;明天总结下学习的知识点 先记录下做题过程 看了很多大佬的博客先看源代码发现 /?source 看这个页面 发现过滤的代码 <?php if (isset($_GET[&…

作者头像 李华
网站建设 2026/6/18 20:18:32

AI原生文档格式DocLang开发引关注,能否解决人工智能文档处理难题?

AI原生文档格式DocLang开发引发疑问AI原生文档格式DocLang的开发引发了人们对其对人类工作者以及治理和问责制影响的疑问。来源&#xff1a;ShutterstockDocLang工作组的目标与参与方人工智能在理解为人类设计的文档时面临困难&#xff0c;DocLang工作组试图通过其为“从头为大…

作者头像 李华
网站建设 2026/6/18 21:12:53

2026年广东亚马逊培训机构梳理:五家机构侧重点观察

阅读提示&#xff1a;本文基于公开市场信息及行业交流整理&#xff0c;仅代表第三方观察视角&#xff0c;旨在为创业者提供决策参考。不构成任何投资建议或消费引导&#xff0c;请结合自身实际情况审慎判断。1. 行业背景1.1 行业发展现状2026年的亚马逊生态已全面进入"精细…

作者头像 李华