对话记忆系统设计
和ChatGPT聊过天的人都知道,它能记住你之前说的话。问它"我刚才说了什么",它能准确回答。这种能力叫做对话记忆。
对于AI应用来说,记忆能力至关重要。没有记忆,每次对话都是"新朋友",用户体验会很差。今天我们来深入聊聊Spring AI中对话记忆的设计与实现。
为什么需要对话记忆
先来理解一个基本事实:大模型本身是无状态的。
每次你调用大模型API,对它来说都是一次全新的请求。它不知道一秒钟前你问过什么,更不知道昨天聊了什么话题。
那ChatGPT是怎么做到"记住"对话的?答案是:把历史对话一起发给模型。
每次对话时,把之前的所有消息都带上,模型就能"理解"上下文。这就是对话记忆的本质。
大模型本身是无状态的,每次 API 调用对它来说都是全新请求。实现"记忆"的方式是:把历史对话消息一起发给模型,模型从完整的上下文中"理解"之前说了什么。
短期记忆 vs 长期记忆
AI的记忆能力分两种层次:
短期记忆(Session Memory)
指在单次对话会话中的记忆能力,特点是:
- 局限在当前对话窗口
- 关闭页面或开始新对话就消失
- 受限于模型的上下文窗口大小
比如你和AI聊天,说了10轮之后,第1轮的内容可能就被"遗忘"了——因为上下文窗口装不下了。
长期记忆(Persistent Memory)
指跨对话的记忆能力,特点是:
- 信息存储在外部(数据库、向量库等)
- 不受单次对话限制
- 需要主动管理(增删改)
比如你告诉AI"我对海鲜过敏",一周后再问它推荐餐厅,它能记得避开海鲜。
| 维度 | 短期记忆 | 长期记忆 |
|---|---|---|
| 生命周期 | 单次会话 | 持久存储 |
| 存储位置 | 内存/请求体 | 数据库/向量库 |
| 信息完整度 | 完整上下文 | 通常是摘要 |
| 适用场景 | 连续对话 | 用户偏好、历史信息 |
这篇文章主要聊短期记忆的实现,长期记忆涉及更复杂的RAG架构,后续专门讨论。
| 维度 | 短期记忆 | 长期记忆 |
|---|---|---|
| 生命周期 | 单次会话 | 持久存储 |
| 存储位置 | 内存/请求体 | 数据库/向量库 |
| 信息完整度 | 完整上下文 | 通常是摘要 |
| 适用场景 | 连续对话 | 用户偏好、历史信息 |
Spring AI的记忆实现方案
Spring AI提供了两种实现对话记忆的方式。
方案一:手动维护Message列表
最直观的方式是自己管理对话历史:
@RestController
@RequestMapping("/memory")
public class ManualMemoryController {
private final ChatModel chatModel;
// 用ConcurrentHashMap存储不同会话的历史
private final Map<String, List<Message>> sessionHistory = new ConcurrentHashMap<>();
@GetMapping("/chat")
public String chat(@RequestParam String sessionId,
@RequestParam String message) {
// 1. 获取或创建该会话的历史记录
List<Message> history = sessionHistory.computeIfAbsent(
sessionId, k -> new ArrayList<>());
// 2. 添加系统提示词(如果是第一条消息)
if (history.isEmpty()) {
history.add(new SystemMessage("你是一个智能理财顾问"));
}
// 3. 添加用户本次输入
history.add(new UserMessage(message));
// 4. 调用模型
Prompt prompt = new Prompt(history);
ChatResponse response = chatModel.call(prompt);
String aiReply = response.getResult().getOutput().getText();
// 5. 把AI回复也加入历史
history.add(new AssistantMessage(aiReply));
// 6. 控制历史长度,防止无限增长
trimHistory(history, 20); // 最多保留20条
return aiReply;
}
private void trimHistory(List<Message> history, int maxSize) {
// 保留系统消息 + 最近的对话
while (history.size() > maxSize) {
// 找到第一条非系统消息,删除它
for (int i = 0; i < history.size(); i++) {
if (!(history.get(i) instanceof SystemMessage)) {
history.remove(i);
break;
}
}
}
}
}
优点:完全可控,想怎么管理记忆就怎么管理
缺点:代码繁琐,需要自己处理很多细节
方案二:使用ChatMemoryAdvisor(推荐)
Spring AI提供了更优雅的方式——通过Advisor自动管理记忆:
@RestController
@RequestMapping("/memory")
public class AdvisorMemoryController {
private final ChatClient chatClient;
public AdvisorMemoryController(ChatModel chatModel) {
// 创建基于内存的记忆存储
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20) // 最多保留20条消息
.build();
// 构建带记忆能力的ChatClient
this.chatClient = ChatClient.builder(chatModel)
.defaultSystem("你是一个智能理财顾问")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
@GetMapping("/chat")
public Flux<String> chat(@RequestParam String sessionId,
@RequestParam String message) {
return chatClient.prompt()
.user(message)
// 指定会话ID,相同ID的对话共享记忆
.advisors(spec -> spec.param(
ChatMemory.CONVERSATION_ID, sessionId))
.stream()
.content();
}
}
代码简洁多了!核心是两个组件:
- MessageWindowChatMemory:记忆存储,采用滑动窗口机制
- MessageChatMemoryAdvisor:负责自动将历史消息注入请求
MessageWindowChatMemory详解
MessageWindowChatMemory是Spring AI内置的记忆实现,它的特点是:
- 为每个会话维护一个消息窗口
- 当消息数量超过上限时,自动淘汰最早的消息
- 默认存储在内存中
关键参数
MessageWindowChatMemory memory = MessageWindowChatMemory.builder()
.maxMessages(20) // 最大消息数(包括所有类型)
.chatMemoryRepository( // 底层存储实现
new InMemoryChatMemoryRepository()
)
.build();
注意:maxMessages是指所有消息的总数,包括UserMessage、AssistantMessage、SystemMessage,而不只是用户发送的消息。
maxMessages 计算的是所有类型消息的总条数,包括 SystemMessage、UserMessage 和 AssistantMessage。一轮对话(用户问+AI答)占用 2 条,加上系统消息后实际可用的对话轮数会比你以为的少。建议设置时留出余量。
两种MemoryAdvisor的区别
Spring AI提供了两种记忆Advisor:
MessageChatMemoryAdvisor
将历史消息添加到请求的messages列表中,这是标准做法:
{
"messages": [
{"role": "system", "content": "你是智能助手"},
{"role": "user", "content": "第一个问题"},
{"role": "assistant", "content": "第一个回答"},
{"role": "user", "content": "第二个问题"} // 当前问题
]
}
PromptChatMemoryAdvisor
将历史对话拼接到系统提示词中,适用于不支持多轮messages的模型:
{
"messages": [
{
"role": "system",
"content": "你是智能助手\n\n历史对话:\n用户:第一个问题\n助手:第一个回答\n"
},
{"role": "user", "content": "第二个问题"}
]
}
大多数情况用MessageChatMemoryAdvisor就行。
持久化记忆:保存到MySQL
内存记忆有个致命问题:应用重启后记忆就丢失了。生产环境必须要持久化存储。
InMemoryChatMemoryRepository(内存存储)在应用重启后会完全丢失所有对话记忆,不适合生产环境使用。生产环境必须切换到 JdbcChatMemoryRepository 或其他持久化存储方案。
Spring AI提供了基于JDBC的持久化方案。
引入依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
配置数据源
spring:
datasource:
url: jdbc:mysql://localhost:3306/ai_memory_db?useUnicode=true&characterEncoding=UTF-8
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
ai:
chat:
memory:
repository:
jdbc:
platform: mysql
创建数据表
Spring AI官方提供的SQL可能有兼容性问题,建议用这个:
CREATE TABLE `spring_ai_chat_memory` (
`conversation_id` varchar(36) NOT NULL COMMENT '会话ID',
`content` text NOT NULL COMMENT '消息内容',
`type` enum('USER','ASSISTANT','SYSTEM','TOOL') NOT NULL COMMENT '消息类型',
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
KEY `idx_conversation_timestamp` (`conversation_id`, `timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI对话记忆表';
配置JDBC记忆
@Configuration
public class PersistentMemoryConfig {
@Bean
public ChatMemory jdbcChatMemory(JdbcChatMemoryRepository repository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository) // 使用JDBC存储
.maxMessages(50)
.build();
}
@Bean
public ChatClient chatClient(ChatModel chatModel, ChatMemory jdbcChatMemory) {
return ChatClient.builder(chatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(jdbcChatMemory).build(),
new SimpleLoggerAdvisor()
)
.build();
}
}
使用方式
使用方式和内存记忆完全一样,只是底层存储换成了MySQL:
@GetMapping("/chat")
public Flux<String> chat(@RequestParam String sessionId,
@RequestParam String message) {
return chatClient.prompt()
.user(message)
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, sessionId))
.stream()
.content();
}
现在对话记录会存到数据库里,重启应用后记忆依然保留。
ChatMemoryRepository体系
Spring AI的记忆存储是可扩展的,核心接口是ChatMemoryRepository:
Spring AI支持多种存储后端:
| 实现类 | 适用场景 |
|---|---|
| InMemoryChatMemoryRepository | 开发测试,不需要持久化 |
| JdbcChatMemoryRepository | 关系型数据库(MySQL、PostgreSQL等) |
| CassandraChatMemoryRepository | 高并发、需要TTL自动过期 |
| Neo4jChatMemoryRepository | 需要图关系分析 |
| MongoChatMemoryRepository | 灵活的文档存储 |
| CosmosDBChatMemoryRepository | Azure云环境 |
记忆管理最佳实践
- 合理设置窗口大小:窗口太小上下文丢失,太大 token 消耗增加且可能超出模型限制,一般 20-50 条比较合理
- 重要信息放在 System 消息:系统消息通常不会被淘汰,用户偏好、用户身份等关键信息放在这里
- 定期清理过期会话:数据库存储会不断积累,建议设置定时任务清理超过 7 天的记录
- 敏感信息脱敏:对话记录存储前要对手机号、身份证号等进行脱敏处理
1. 合理设置窗口大小
窗口太小,上下文信息丢失;窗口太大,token消耗增加且可能超出模型限制。
// 根据模型能力和业务需求调整
MessageWindowChatMemory.builder()
.maxMessages(30) // 一般20-50条比较合理
.build();
2. 重要信息放在System消息
系统消息通常不会被淘汰,重要的设定放在这里:
chatClient.prompt()
.system("用户姓名:张三\n用户等级:VIP\n偏好:科技类产品")
.user(question)
.call()
.content();
3. 定期清理过期会话
数据库存储会不断积累数据,需要定期清理:
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
public void cleanExpiredSessions() {
// 删除7天前的会话记录
LocalDateTime threshold = LocalDateTime.now().minusDays(7);
jdbcTemplate.update(
"DELETE FROM spring_ai_chat_memory WHERE timestamp < ?",
threshold
);
}
4. 敏感信息脱敏
对话记录可能包含敏感信息,存储前考虑脱敏:
public class SensitiveDataAdvisor implements CallAdvisor {
@Override
public ChatClientResponse adviseCall(ChatClientRequest request,
CallAdvisorChain chain) {
// 在存储前对消息内容脱敏
ChatClientRequest sanitized = sanitizeRequest(request);
return chain.nextCall(sanitized);
}
private ChatClientRequest sanitizeRequest(ChatClientRequest request) {
// 手机号、身份证号等脱敏处理
// ...
}
}
小结
这篇文章系统介绍了Spring AI的对话记忆机制:
- 记忆本质:把历史对话一起发给模型,让它理解上下文
- 两种记忆:短期记忆(会话内)和长期记忆(跨会话)
- 实现方式:手动维护Message列表 或 使用ChatMemoryAdvisor
- 存储选择:内存、JDBC、Cassandra、MongoDB等
- 持久化配置:引入JDBC依赖 + 配置数据源 + 创建表结构
对话记忆是AI应用的基础能力,掌握好它,你的AI助手就能真正"记住"用户了。
到这里,Spring AI详细解析系列就全部结束了。我们从入门实战开始,一路讲到核心架构、流式输出、提示词工程、结构化输出、Advisor机制和对话记忆。
这些知识足够你在实际项目中用好Spring AI了。当然,大模型应用开发还有很多进阶话题,比如RAG、工具调用、Agent编排等,后续会详细的讲解。