Spring AI RAG - 03对话记忆 ChatMemory 实现多轮会话
文章目录

引言
单轮对话的 AI 就像一条金鱼——每次交互都是全新的开始,它不记得你上一句说了什么。在实际的知识库问答场景中,用户往往需要多轮追问来逐步深入一个话题。对话记忆(ChatMemory)正是解决这一问题的关键组件。
本篇将解析 Spring AI 的 ChatMemory 机制,包括它的存储模型、Advisor 集成方式,以及如何实现多用户之间的记忆隔离。
设计说明
多轮对话的核心挑战
大模型本身是无状态的——每次 API 调用都是独立的。要实现多轮对话,必须在每次请求时将历史对话作为上下文一并发送给模型。这带来了几个工程问题:
- 存储:历史消息存在哪里?内存、Redis、还是数据库?
- 注入:如何将历史消息无侵入地注入到每次请求中?
- 隔离:多用户场景下,如何确保 A 用户看不到 B 用户的对话?
- 窗口:历史消息不能无限增长,如何控制上下文窗口大小?
设计方案
Spring AI 通过 ChatMemory 接口 + MemoryAdvisor 的组合解决上述问题:
- ChatMemory:负责消息的存储与检索,支持多种后端实现(InMemory、JDBC、Redis 等)
- PromptChatMemoryAdvisor / MessageChatMemoryAdvisor:作为 Advisor 挂载到 ChatClient 的调用链中,在请求发出前自动注入历史消息
两种 MemoryAdvisor 的区别:
| Advisor | 注入方式 | 适用场景 |
|---|---|---|
| MessageChatMemoryAdvisor | 将历史消息作为独立的 Message 列表注入 | 模型原生支持多轮消息格式 |
| PromptChatMemoryAdvisor | 将历史消息序列化为文本,拼接到 System Prompt 中 | 兼容性更好,适合所有模型 |
原理方案
ChatMemory 的存储模型
ChatMemory 接口定义了三个核心方法:
public interface ChatMemory {
String CONVERSATION_ID = "conversationId";
void add(String conversationId, List<Message> messages);
List<Message> get(String conversationId, int lastN);
void clear(String conversationId);
}
add:追加消息到指定会话get:获取指定会话的最近 N 条消息clear:清空指定会话的所有消息
默认实现 InMemoryChatMemory 使用 ConcurrentHashMap 存储,适合开发和测试环境。生产环境建议使用 JDBC 或 Redis 实现持久化。
Advisor 链中的执行时机
用户请求 → PromptChatMemoryAdvisor.before() → [注入历史消息] → 其他 Advisor → ChatModel 调用
← PromptChatMemoryAdvisor.after() ← [保存本轮消息] ← 其他 Advisor ← 模型响应
PromptChatMemoryAdvisor 在 before 阶段从 ChatMemory 中读取历史消息并注入请求,在 after 阶段将本轮的用户消息和 AI 回复保存到 ChatMemory 中。
用户隔离机制
通过 ChatMemory.CONVERSATION_ID 参数实现隔离。每个用户使用自己的 userId 作为 conversationId,确保不同用户的对话历史完全独立:
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, userId))
代码解析
依赖引入
<!--对话记忆-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-memory</artifactId>
</dependency>
这个 starter 会自动装配 InMemoryChatMemory Bean(默认存储在 JVM 的 Map 中)。
ChatClient 集成记忆 Advisor
public AiRagController(ChatModel chatModel, ChatMemory chatMemory, VectorStore vectorStore) {
this.chatClient = ChatClient.builder(chatModel)
.defaultSystem("""
你是知识库系统的对话助手,请以乐于助人的方式进行对话,
今天的日期:{current_data}
""")
.defaultAdvisors(
PromptChatMemoryAdvisor.builder(chatMemory).build(),
SimpleLoggerAdvisor.builder().build()
)
.build();
this.vectorStore = vectorStore;
}
关键点:
ChatMemory chatMemory通过构造函数注入,由 Spring 自动装配提供PromptChatMemoryAdvisor.builder(chatMemory).build()创建记忆 Advisor,将其注册为默认 AdvisorSimpleLoggerAdvisor用于调试,会打印完整的请求/响应日志
请求时传入会话 ID
@PostMapping(value = "/rag")
@Loggable
public Flux<String> generatePost(@RequestParam String message) {
Long userId = BaseContext.getCurrentId();
return chatClient.prompt()
.user(message)
.system(a -> a.param("current_data", LocalDate.now().toString()))
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, userId))
.stream()
.content();
}
BaseContext.getCurrentId() 从 ThreadLocal 中获取当前登录用户的 ID(由 JWT 拦截器在请求进入时设置)。这个 ID 作为 CONVERSATION_ID 传递给 MemoryAdvisor,实现了:
- 同一用户的多次请求共享同一段对话历史
- 不同用户之间的对话完全隔离
BaseContext —— ThreadLocal 用户上下文
public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void removeCurrentId() {
threadLocal.remove();
}
}
JWT 拦截器在验证 token 成功后调用 BaseContext.setCurrentId(userId),后续业务代码通过 BaseContext.getCurrentId() 获取当前用户身份。
ChatController 中的另一种记忆方式
public ChatController(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatClient = builder
.defaultSystem("你是公司的客服代理")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
这里使用了 MessageChatMemoryAdvisor,它会将历史消息作为独立的 Message 对象注入到请求的消息列表中,而非拼接到 System Prompt。两种方式的效果类似,但 MessageChatMemoryAdvisor 更符合 OpenAI 等模型的多轮对话格式。
验证结果
多轮对话测试
第一轮:
POST /api/v1/ai/rag?message=我叫张三
响应:你好张三,很高兴认识你!有什么可以帮助你的吗?
第二轮:
POST /api/v1/ai/rag?message=我叫什么名字?
响应:你叫张三,你刚才告诉我的。
AI 成功记住了上一轮对话中的信息。
用户隔离测试
用户 A(userId=1):
POST /api/v1/ai/rag?message=我的项目是电商系统
用户 B(userId=2):
POST /api/v1/ai/rag?message=我的项目是什么?
响应:抱歉,你还没有告诉我你的项目是什么。
用户 B 无法获取用户 A 的对话上下文,隔离生效。
调试日志(SimpleLoggerAdvisor)
开启 debug 日志后,可以看到完整的请求内容:
logging:
level:
org.springframework.ai.chat.client.advisor: debug
日志输出会包含注入的历史消息,方便排查记忆相关问题。
生产环境建议
- 持久化存储:默认的 InMemoryChatMemory 在应用重启后会丢失所有记忆。生产环境应使用 JDBC 或 Redis 实现
- 窗口控制:通过
PromptChatMemoryAdvisor.builder(chatMemory).maxMessages(20).build()限制历史消息数量,避免超出模型上下文窗口 - 会话过期:配合 Redis TTL 或定时任务清理长时间不活跃的会话
- 异步保存:高并发场景下,消息保存可以异步化,避免阻塞主流程
小结
本篇介绍了 Spring AI 对话记忆的完整实现:
ChatMemory接口提供消息的存储与检索能力PromptChatMemoryAdvisor/MessageChatMemoryAdvisor以 Advisor 模式无侵入地注入历史消息- 通过
CONVERSATION_ID参数实现多用户记忆隔离 - 默认使用 InMemory 存储,生产环境建议持久化
下一篇将进入 RAG 的核心——文档上传与向量化入库,看看知识是如何被"喂"给系统的。

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)