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 AI 2.1(预计 2026-11)将引入 Session API 并废弃 ChatMemory。本文以 2.0 写法为主,文末给出迁移预告;若你读到本文时 2.1 已 GA,请优先用 Session API。
二、记忆的本质
大模型本身"无状态"——每次请求它都不记得上一句。多轮对话的做法是:把历史消息拼接进本次请求的上下文。
代价是 token 成本随轮次线性增长:每轮都把前面所有内容重新发一遍。所以记忆不是"存得越多越好",而是"存得刚刚好"——这也是下面滑窗与裁剪的意义。
三、ChatMemory 与滑窗记忆
3.1 创建 ChatMemory Bean
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MemoryConfig {
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.maxMessages(20) // 只保留最近 20 条消息(默认也是 20)
.build();
}
}
注意:MessageWindowChatMemory 的构造器是私有的,必须用 builder(),直接 new 会编译报错。滑窗的意思是:超过 maxMessages 时,自动丢弃最旧的消息(保留 system 消息)。
3.2 挂到 ChatClient 上
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder, ChatMemory memory) {
return builder
.defaultAdvisors(MessageChatMemoryAdvisor.builder(memory).build())
.build();
}
}
MessageChatMemoryAdvisor 是一个内置 Advisor(第 6 篇的机制):它会在请求进入时把历史消息回填,在响应返回后把本轮对话写入记忆。
四、会话隔离:ChatMemory.CONVERSATION_ID(2.0 必传)
这是最容易踩的坑。2.0 删掉了默认会话 id,不传 conversationId 就没有隔离,所有用户会共用一段记忆,互相串话:
import org.springframework.ai.chat.memory.ChatMemory;
// 每个会话一个稳定的 id(如登录用户的 session、或前端生成的 UUID)
String conversationId = "user-10086";
String reply = 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 | 高可用大规模 | 分布式,运维重 |
| CosmosDB | Azure 生态 | 托管,免运维 |
六、上下文裁剪与 token 成本控制
记忆不是越大越好,几个控制手段:
- 滑窗条数:
maxMessages(20)控制保留条数,超出丢最旧。 - 主动裁剪:长对话里,只保留关键结论而非全部历史(结合第 5 篇的模板,把"摘要"而非"全文"回填)。
- token 预算:结合第 6 篇的 token 预算 Advisor,超预算短路或降级。
经验:问答类会话 maxMessages 设 10~20 够用;需要强上下文连贯的(如代码 review 长会话)可加大,但要盯着 token 成本。
七、原理图解:一次多轮对话的记忆流转
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 是怎么设计的;源码见置顶评论。
本文详解Spring AI 2.0中ChatMemory实现多轮对话记忆的核心机制,涵盖滑窗记忆原理、CONVERSATION_ID会话隔离要求、内存/JDBC/MongoDB等持久化方案选型与配置,以及上下文裁剪和token成本控制策略。同时指出ChatMemory在2.1版本将被Session API废弃的演进方向,并汇总高频踩坑点如串话、记忆丢失、滑窗误删约束等。
完整实战&spm=1001.2101.3001.5002&articleId=169233533&d=1&t=3&u=60b88add65df46ebbf23c58b0841ba40)
784

被折叠的 条评论
为什么被折叠?



