小崔挠头:“我昨天问课程助手的对话,今天它全忘了。”白歌说:“用 ChatMemory,把对话历史持久化。InMemory 只适合开发,生产环境用 JDBC 或 Cassandra。”
多轮对话与 ChatMemory
定义与作用
ChatMemory 是 Spring AI 中管理对话历史的组件。它将每轮对话的用户消息和 AI 回复存储起来,在后续请求中自动注入到 Prompt,让 AI "记住"上下文。
没有 ChatMemory 时,每次请求都是独立的——AI 不知道上一轮说了什么。有了 ChatMemory,AI 能像真人一样进行连贯的多轮对话。
核心原理:ChatMemoryAdvisor 的消息注入流程
图释:ChatMemoryAdvisor 在每次请求前从 ChatMemory 读取历史消息注入 Prompt,在收到响应后把新消息写入 ChatMemory。整个流程对应用代码透明。
ChatMemory 接口
public interface ChatMemory {
void add(String conversationId, List<Message> messages);
List<Message> get(String conversationId, int lastN);
void clear(String conversationId);
}
add:追加消息到指定会话get:获取最近 N 条消息(控制上下文长度)clear:清除会话历史
ChatMemory 实现对比
| 实现 | 持久化 | 适用场景 | 依赖 |
|---|---|---|---|
InMemoryChatMemory | 否(内存) | 开发/测试/演示 | 无 |
JdbcChatMemory | 是(数据库) | 中小规模生产 | spring-jdbc + 数据库 |
CassandraChatMemory | 是(分布式) | 大规模生产 | spring-data-cassandra |
完整示例一:带 ChatMemory 的课程问答机器人
场景说明
孔蓝要求课程问答支持多轮对话——学生可以先问"离散数学有哪些前置课程",再追问"这两门课分别多少学分"。
关键代码
@Configuration
public class ChatMemoryConfig {
@Bean
public ChatMemory chatMemory() {
// 开发环境用 InMemory,生产环境替换为 JdbcChatMemory
return new InMemoryChatMemory();
}
@Bean
public ChatClient chatClient(ChatClient.Builder builder,
ChatMemory chatMemory) {
return builder
.defaultSystem("""
你是飞翔科技大学的课程顾问。你了解所有专业的课程设置。
回答时引用具体的课程编号和学分。
""")
.defaultAdvisors(new ChatMemoryAdvisor(chatMemory))
.build();
}
}
@RestController
public class CourseAdvisorController {
private final ChatClient chatClient;
public CourseAdvisorController(ChatClient chatClient) {
this.chatClient = chatClient;
}
// conversationId 通过请求参数传递,用于区分不同学生的会话
@GetMapping("/course-advisor")
public String ask(@RequestParam String question,
@RequestParam(defaultValue = "default") String conversationId) {
return chatClient.prompt()
.user(question)
.advisors(advisor -> advisor
.param("chat_memory_conversation_id", conversationId))
.call()
.content();
}
@GetMapping("/course-advisor/clear")
public String clear(@RequestParam(defaultValue = "default") String conversationId) {
// 需要注入 ChatMemory 来手动清除
return "会话 " + conversationId + " 已清除(需注入 ChatMemory 调用 clear)";
}
}
运行结果
第一轮:GET /course-advisor?question=离散数学有哪些前置课程&conversationId=stu001
→ "离散数学(CS201)的前置课程是《高等数学A》(MATH101,5学分)和
《线性代数》(MATH102,4学分)。"
第二轮:GET /course-advisor?question=这两门课分别多少学分&conversationId=stu001
→ "根据之前的回答,《高等数学A》(MATH101)5学分,
《线性代数》(MATH102)4学分。"
↑ 不需要重新说"离散数学的前置"
第三轮:GET /course-advisor?question=这两门课分别多少学分&conversationId=stu002
→ "请问你指的是哪两门课程?"
↑ 不同 conversationId,没有历史记录
完整示例二:限制对话历史长度
场景说明
上下文窗口有限,需要只保留最近 N 轮对话。ChatMemoryAdvisor 支持通过 defaultConversationHistoryLimit 控制。
关键代码
@Bean
public ChatClient chatClient(ChatClient.Builder builder,
ChatMemory chatMemory) {
return builder
.defaultAdvisors(
new ChatMemoryAdvisor(
chatMemory,
"default", // 默认 conversation ID
10 // 只保留最近 10 条消息
)
)
.build();
}
易错场景与面试考点
易错场景一:ChatMemory 不持久化导致对话丢失
// ❌ 错误:生产环境使用 InMemoryChatMemory
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory();
// 应用重启 → 所有学生的对话历史丢失,体验极差
}
问题分析
InMemoryChatMemory 存储在 JVM 堆内存中,应用重启或节点故障时数据丢失。虽然开发和演示阶段方便,但用户会话数据丢失会影响体验。
// ✅ 正确:生产环境使用 JdbcChatMemory
@Bean
public ChatMemory chatMemory(DataSource dataSource) {
return new JdbcChatMemory(dataSource);
// 对话历史持久化到数据库表 chat_memory
}
JdbcChatMemory 需要的表结构(自动创建):
CREATE TABLE IF NOT EXISTS chat_memory (
conversation_id VARCHAR(100),
message_index INT,
message_type VARCHAR(20),
message_content TEXT,
PRIMARY KEY (conversation_id, message_index)
);
易错场景二:conversationId 未传导致所有用户共享历史
// ❌ 错误:所有请求共用一个 conversationId
@GetMapping("/ask")
public String ask(@RequestParam String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
// 所有学生共享对话历史!张三问"我叫什么"会得到李四的名字
}
问题分析
ChatMemoryAdvisor 默认使用 "default" 作为 conversationId。所有不传 conversationId 的请求共享同一会话。
// ✅ 正确:每个学生用独立 conversationId
@GetMapping("/ask")
public String ask(@RequestParam String question,
@RequestParam String studentId) {
return chatClient.prompt()
.user(question)
.advisors(advisor -> advisor
.param("chat_memory_conversation_id", studentId))
.call()
.content();
}
面试高频题
Q1:ChatMemory 和 ChatMemoryAdvisor 是什么关系?
ChatMemory 是存储组件(存/取消息),ChatMemoryAdvisor 是 Advisor(拦截器),在请求前从 ChatMemory 读取历史注入 Prompt,在响应后将新消息写回。两者配合实现多轮对话的自动上下文管理。
Q2:上下文窗口有限时,ChatMemory 如何避免 Token 超限?
通过
ChatMemoryAdvisor的defaultConversationHistoryLimit参数限制注入的历史消息数量(如只取最近 10 条)。在大型应用中可结合 Token 计数器动态裁剪历史。
Q3:生产环境该如何选择 ChatMemory 实现?
中小规模(单实例):
JdbcChatMemory,利用现有数据库。大规模(分布式多实例):CassandraChatMemory,支持水平扩展和高可用。开发/测试:InMemoryChatMemory。
本章小结
- ChatMemory 是对话历史管理的核心接口,三种实现覆盖开发到生产
- ChatMemoryAdvisor 在 Advisor 链中自动注入历史消息,对应用代码透明
conversationId用于隔离不同用户的会话,生产环境务必正确传递defaultConversationHistoryLimit控制上下文长度,防止 Token 超限- InMemory 仅适合开发,生产用 JdbcChatMemory 或 CassandraChatMemory