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

本文详解Spring AI 2.0中ChatMemory实现多轮对话记忆的核心机制,涵盖滑窗记忆原理、CONVERSATION_ID会话隔离要求、内存/JDBC/MongoDB等持久化方案选型与配置,以及上下文裁剪和token成本控制策略。同时指出ChatMemory在2.1版本将被Session API废弃的演进方向,并汇总高频踩坑点如串话、记忆丢失、滑窗误删约束等。

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高可用大规模分布式,运维重
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 是怎么设计的;源码见置顶评论。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值