ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring AI 多轮对话记忆怎么做?ChatMemory + 持久化(内存/JDBC/Mongo)完整实战

Spring AI 多轮对话记忆怎么做?ChatMemory + 持久化(内存/JDBC/Mongo)完整实战

Spring AI 多轮对话记忆怎么做?ChatMemory + 持久化(内存/JDBC/Mongo)完整实战

系列:Spring AI 深入浅出(第 7 篇)
版本:Spring AI 2.0.1 / Spring Boot 4.1.x / JDK 21
标签:#SpringAI #ChatMemory #多轮对话 #会话管理
前置阅读:第 6 篇《Advisor 责任链原理》
预计字数:6500 字|可复制源码 + 持久化对照表 + 踩坑 FAQ

本文将解决:多轮对话的本质是"把历史消息塞回上下文",但怎么塞、塞多少、怎么按用户隔离、怎么持久化才是工程问题。本文用ChatMemory+MessageChatMemoryAdvisor一次讲透。

你将学到:

  • 记忆的本质与 token 成本增长
  • MessageWindowChatMemory滑窗记忆(builder 用法)
  • ChatMemory.CONVERSATION_ID会话隔离(2.0 必传,不传会串话)
  • 持久化:ChatMemoryRepository(内存/JDBC/Mongo/Cassandra 等)
  • 上下文裁剪与 token 成本控制
  • 2.1 Session API 前瞻(ChatMemory 即将废弃)

一、版本标注

本文基于 Spring AI 2.0.1 / Boot 4.1 / JDK 21。两个 2.0 必知的 breaking:①InMemoryChatMemory类已移除,改用MessageWindowChatMemory.builder();②ChatMemory.DEFAULT_CONVERSATION_ID已删除,会话 id 必须显式传。

⚠️ 前瞻:Spring AI2.1(预计 2026-11)将引入 Session API 并废弃 ChatMemory。本文以 2.0 写法为主,文末给出迁移预告;若你读到本文时 2.1 已 GA,请优先用 Session API。


二、记忆的本质

大模型本身"无状态"——每次请求它都不记得上一句。多轮对话的做法是:把历史消息拼接进本次请求的上下文。

代价是token 成本随轮次线性增长:每轮都把前面所有内容重新发一遍。所以记忆不是"存得越多越好",而是"存得刚刚好"——这也是下面滑窗与裁剪的意义。


三、ChatMemory 与滑窗记忆

3.1 创建 ChatMemory Bean

importorg.springframework.ai.chat.memory.ChatMemory;importorg.springframework.ai.chat.memory.MessageWindowChatMemory;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;@ConfigurationpublicclassMemoryConfig{@BeanpublicChatMemorychatMemory(){returnMessageWindowChatMemory.builder().maxMessages(20)// 只保留最近 20 条消息(默认也是 20).build();}}

注意:MessageWindowChatMemory的构造器是私有的,必须用builder(),直接new会编译报错。滑窗的意思是:超过maxMessages时,自动丢弃最旧的消息(保留 system 消息)。

3.2 挂到 ChatClient 上

importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;@ConfigurationpublicclassChatClientConfig{@BeanpublicChatClientchatClient(ChatClient.Builderbuilder,ChatMemorymemory){returnbuilder.defaultAdvisors(MessageChatMemoryAdvisor.builder(memory).build()).build();}}

MessageChatMemoryAdvisor是一个内置 Advisor(第 6 篇的机制):它会在请求进入时把历史消息回填,在响应返回后把本轮对话写入记忆。


四、会话隔离:ChatMemory.CONVERSATION_ID(2.0 必传)

这是最容易踩的坑。2.0 删掉了默认会话 id,不传conversationId就没有隔离,所有用户会共用一段记忆,互相串话:

importorg.springframework.ai.chat.memory.ChatMemory;// 每个会话一个稳定的 id(如登录用户的 session、或前端生成的 UUID)StringconversationId="user-10086";Stringreply=chatClient.prompt().user("帮我查一下我的订单").advisors(a->a.param(ChatMemory.CONVERSATION_ID,conversationId)).call().content();

要点:

  • ChatMemory.CONVERSATION_ID的值是"chat_memory_conversation_id",作为 advisor 参数传入。
  • conversationId必须稳定:同一段对话的每一轮都用同一个 id,换新对话才换新 id。
  • 典型的取法:登录态用userId;游客态用前端生成并持久化的UUID。

五、持久化:ChatMemoryRepository

5.1 默认内存(重启即失)

不配持久化时,默认用内存实现,应用重启记忆就没了——只适合本地开发。

5.2 持久化实现

ChatMemory的持久化靠ChatMemoryRepository接口,2.0 内置实现有:JDBC、Cassandra、Neo4j、MongoDB、CosmosDB(各实现的具体类名与配置以 2.0.1 官方文档为准)。

说明:Redis 不是 Spring AI 2.0 内置的 ChatMemoryRepository 实现(不少教程写了 Redis,实际要自己实现ChatMemoryRepository接口对接)。若团队有 Redis 技术栈,通常会用 JDBC/Mongo 先落地,或自定义实现。

以 JDBC 为例的接入思路:

<!-- 需要 JDBC + 一个数据库驱动 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency>
// 思路:用 JdbcChatMemoryRepository 替换默认内存实现// 具体构造参数与建表语句以 2.0.1 官方文档为准

5.3 持久化选型对照表

实现适用特点
内存(默认)本地开发/测试重启丢失
JDBC已有关系库无需新组件,运维简单
MongoDB已有 Mongo文档型,扩展方便
Cassandra高可用大规模分布式,运维重
CosmosDBAzure 生态托管,免运维

六、上下文裁剪与 token 成本控制

记忆不是越大越好,几个控制手段:

  1. 滑窗条数:maxMessages(20)控制保留条数,超出丢最旧。
  2. 主动裁剪:长对话里,只保留关键结论而非全部历史(结合第 5 篇的模板,把"摘要"而非"全文"回填)。
  3. token 预算:结合第 6 篇的 token 预算 Advisor,超预算短路或降级。

经验:问答类会话maxMessages设 10~20 够用;需要强上下文连贯的(如代码 review 长会话)可加大,但要盯着 token 成本。


七、原理图解:一次多轮对话的记忆流转

模型ChatMemoryMessageChatMemoryAdvisor用户模型ChatMemoryMessageChatMemoryAdvisor用户第1轮:user(查订单) + conversationId读历史(空)只发本轮回复写入 [user, assistant]第2轮:user(那我的优惠券呢) + 同 conversationId读历史(第1轮 2条)历史2条 + 本轮回复(理解"那"指上一轮订单)追加写入

conversationId是这段流转的"键"——同一个 id 读同一段记忆。


八、2.1 前瞻:Session API 与 ChatMemory 废弃

Spring AI 2.1(预计 2026-11)将引入Session API并废弃 ChatMemory。对本文内容的影响:

  • MessageWindowChatMemory/MessageChatMemoryAdvisor这套 API 会进入废弃期
  • 迁移方向:用新的 Session API 管理多轮会话状态
  • 写作策略:2.0 项目继续用本文写法没问题;新项目若在 2.1 之后,直接学 Session API

我会在本合集置顶评论维护一张「2.1 变更速览」,把这套记忆 API 的迁移对照补上。


九、踩坑 FAQ

坑 1:多用户串话

现象:用户 A 的对话上下文跑到了用户 B 那里。
原因:没传conversationId(2.0 没有默认值兜底了)。
解决:每轮请求显式.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)),且 id 要稳定。

坑 2:滑窗截断丢了关键约束

现象:对话到后面,模型忘了前面设定的关键约束。
原因:maxMessages太小,最早的 system 约束/关键信息被滑出窗口。
解决:system 消息会保留;关键约束放defaultSystem(每次都会带),别只放最早一条 user 里。

坑 3:直接 new MessageWindowChatMemory 编译报错

现象:new MessageWindowChatMemory(20)报构造器私有。
原因:2.0 改为 builder 模式。
解决:用MessageWindowChatMemory.builder().maxMessages(20).build()。

坑 4:重启后记忆丢失

现象:重启应用,对话上下文全没了。
原因:用了默认内存实现。
解决:接ChatMemoryRepository持久化实现(JDBC/Mongo 等)。

坑 5:工具调用的中间消息污染记忆

现象:记忆里混入了工具调用的中间消息,导致上下文杂乱。
原因:历史上某些版本默认把工具中间消息也写入了记忆。
解决:2.0 默认会过滤掉工具调用的中间消息(只保留 user/assistant 对话),若你自定义了写入逻辑需注意。


十、小结与合集导航

本篇把"多轮对话记忆"从原理到落地讲完:

  • MessageWindowChatMemory.builder()滑窗记忆
  • MessageChatMemoryAdvisor挂载 +ChatMemory.CONVERSATION_ID会话隔离
  • ChatMemoryRepository持久化(内存/JDBC/Mongo/Cassandra)
  • token 成本控制与 2.1 Session API 前瞻

下一篇:《结构化输出》——让 AI 直接返回 Java 对象,并自动自纠正校验失败。


十一、互动钩子

投票:你的多轮对话记忆用什么持久化?

  • 内存(开发阶段)
  • JDBC
  • MongoDB
  • 其他

评论区聊聊你的 conversationId 是怎么设计的;源码见置顶评论。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进