本文是 Spring AI Agent 开发学习系列的第 6 篇
开篇
你是否遇到过这样的场景?
• 用户说:"我叫张三" • 模型回复:"你好张三!" • 用户追问:"我叫什么名字?" • 模型回答:"抱歉,我不知道您的名字..."
这就是无记忆对话的典型问题。每次请求都是独立的,模型无法记住之前说过的话。
今天我们来解决这个问题——让 AI 模型拥有"记忆"。
读完本文,你将收获:
• ✅ ChatMemory 核心概念与工作原理 • ✅ MessageWindowChatMemory 的窗口机制 • ✅ MessageChatMemoryAdvisor 的正确配置 • ✅ 多用户会话管理实践 • ✅ 记忆持久化策略对比
一、为什么需要 ChatMemory?
1.1 HTTP 的"无状态"困境
REST API 天生是无状态的:
请求1: "我叫张三" → 模型不知道任何历史 → 回复"你好张三"请求2: "我叫什么?" → 模型不知道任何历史 → 回复"我不知道"模型每次看到的只有当前用户输入,没有任何上下文。
1.2 ChatMemory 的解决方案
ChatMemory 的核心思路很简单:
请求到来时: 1. 从存储中获取该用户的历史对话 2. 将历史消息作为 context 附加到当前请求 3. 模型看到完整对话历史,做出连贯回复请求完成后: 4. 将本轮对话保存回存储这样模型就能"记住"之前说过的话。
二、ChatMemory 核心组件
Spring AI 提供了清晰的组件分层:
┌─────────────────────────────────────────────────────────────┐│ MessageChatMemoryAdvisor ││ (请求拦截器,自动管理记忆的加载和保存) │└─────────────────────────────────────────────────────────────┘ ↓ 操作┌─────────────────────────────────────────────────────────────┐│ ChatMemory 接口 ││ add() / get() / clear() — 按会话ID管理消息历史 │└─────────────────────────────────────────────────────────────┘ ↓ 实现┌─────────────────────────────────────────────────────────────┐│ MessageWindowChatMemory ││ (滑动窗口,只保留最近N条消息,避免token超限) │└─────────────────────────────────────────────────────────────┘ ↓ 存储┌─────────────────────────────────────────────────────────────┐│ ChatMemoryRepository ││ InMemoryChatMemoryRepository / Redis / Database... │└─────────────────────────────────────────────────────────────┘2.1 ChatMemory 接口
核心操作就三个方法:
public interface ChatMemory { void add(String conversationId, List<Message> messages); // 添加消息 List<Message> get(String conversationId, int lastN); // 获取最近N条 void clear(String conversationId); // 清空会话}2.2 MessageWindowChatMemory
最常用的实现,带滑动窗口:
MessageWindowChatMemory memory = MessageWindowChatMemory.builder() .chatMemoryRepository(repository) // 指定存储 .maxMessages(20) // 只保留最近20条 .build();为什么要限制消息数量?
1. Token 限制:每条消息都占用 token,无限累积会超限 2. 成本控制:每次请求的历史消息越多,API 调用成本越高 3. 相关性:过久的消息通常与当前话题无关
2.3 ChatMemoryRepository 存储选项
三、MessageChatMemoryAdvisor 配置
3.1 Advisor 的作用
MessageChatMemoryAdvisor 是一个请求拦截器,在请求前后自动处理记忆:
Before Request: → 从 ChatMemory 获取历史消息 → 附加到当前请求的 contextAfter Response: → 将本轮对话(用户提问+模型回复)保存到 ChatMemory3.2 配置方式
@Configurationpublic class MemoryConfig { @Bean public ChatMemory chatMemory() { return MessageWindowChatMemory.builder() .chatMemoryRepository(new InMemoryChatMemoryRepository()) .maxMessages(20) .build(); } @Bean public MessageChatMemoryAdvisor memoryAdvisor(ChatMemory chatMemory) { return MessageChatMemoryAdvisor.builder(chatMemory).build(); }}3.3 关键参数:chat_memory_conversation_id
这是最容易踩坑的地方!
Advisor 需要知道哪个会话的记忆。参数名必须是:
chat_memory_conversation_id很多人以为是 conversationId,结果报错:
java.lang.IllegalArgumentException: conversationId cannot be null正确用法:
chatClient.prompt() .user("我叫张三") .advisors(a -> a.param("chat_memory_conversation_id", "user-123")) .call() .content();四、实战代码:有记忆 vs 无记忆对比
4.1 控制器设计
我们设计两个对比端点:
@RestController@RequestMapping("/api/memory")public class MemoryController { private final ChatClient chatClientWithMemory; private final ChatClient chatClientNoMemory; public MemoryController(ChatModel chatModel, MessageChatMemoryAdvisor memoryAdvisor) { // 无记忆客户端:独立builder,无advisor this.chatClientNoMemory = ChatClient.builder(chatModel).build(); // 有记忆客户端:添加默认advisor this.chatClientWithMemory = ChatClient.builder(chatModel) .defaultAdvisors(memoryAdvisor) .build(); }}注意:两个客户端必须用独立的 builder,否则 advisor 会"泄露"到无记忆客户端。
4.2 无记忆端点
@GetMapping("/no-memory")public String noMemory(@RequestParam String message) { return chatClientNoMemory.prompt() .user(message) .call() .content();}每次请求都是独立的,模型不记住任何历史。
4.3 有记忆端点
@GetMapping("/chat")public String chatWithMemory( @RequestParam(required = false) String sessionId, @RequestParam String message) { // 自动生成会话ID(如果未提供) String sid = (sessionId != null && !sessionId.isBlank()) ? sessionId : UUID.randomUUID().toString().substring(0, 8); String result = chatClientWithMemory.prompt() .user(message) .advisors(a -> a.param("chat_memory_conversation_id", sid)) .call() .content(); return "[" + sid + "] " + result;}关键点:
• 用 sessionId区分不同用户的会话• 每次请求带上 chat_memory_conversation_id参数• Advisor 自动加载该会话的历史消息
4.4 测试对比
无记忆测试:
# 第一句curl "http://localhost:8085/api/memory/no-memory?message=我的名字是张三"# 输出:你好,张三!很高兴认识你。# 第二句(模型不记得)curl "http://localhost:8085/api/memory/no-memory?message=我叫什么名字?"# 输出:作为一个人工智能助手,我无法知道您的真实姓名...有记忆测试:
# 创建会话curl "http://localhost:8085/api/memory/new"# 输出:{"sessionId": "36e0ca99", ...}# 第一句(自我介绍)curl "http://localhost:8085/api/memory/chat?sessionId=36e0ca99&message=我叫张三,是一名Java开发者"# 输出:[36e0ca99] 你好,张三!很高兴认识你...# 第二句(模型记得!)curl "http://localhost:8085/api/memory/chat?sessionId=36e0ca99&message=我叫什么名字?"# 输出:[36e0ca99] 你叫张三。刚才你自我介绍时提到了这一点。# 第三句(模型还记得!)curl "http://localhost:8085/api/memory/chat?sessionId=36e0ca99&message=我的工作是什么?"# 输出:[36e0ca99] 你是一名Java开发者...完美!模型能记住整个对话历史。
五、会话管理
5.1 会话生命周期

创建会话 → 使用会话(多次对话) → 清理会话实现简单的会话管理:
private final Map<String, String> sessionStore = new ConcurrentHashMap<>();// 创建新会话@GetMapping("/new")public Map<String, String> newSession() { String sessionId = UUID.randomUUID().toString().substring(0, 8); sessionStore.put(sessionId, sessionId); return Map.of("sessionId", sessionId, "message", "会话已创建");}// 列出活跃会话@GetMapping("/sessions")public Map<String, Object> listSessions() { return Map.of("activeSessions", sessionStore.size(), "sessions", sessionStore.keySet());}// 清理会话@GetMapping("/clear")public Map<String, String> clearSession(@RequestParam String sessionId) { String removed = sessionStore.remove(sessionId); if (removed != null) { return Map.of("status", "cleared", "sessionId", sessionId); } return Map.of("status", "not_found", "sessionId", sessionId);}5.2 会话清理的重要性
虽然 ChatMemory 有滑动窗口限制,但仍需手动清理:
1. 隐私合规:用户数据到期后应删除 2. 存储成本:长期累积的会话占用存储空间 3. 性能影响:历史消息越多,每次请求处理越慢
六、持久化策略
6.1 InMemoryChatMemoryRepository 的局限
new InMemoryChatMemoryRepository()优点:简单、零依赖
缺点:
• 应用重启后所有记忆丢失 • 多实例部署时无法共享 • 内存占用无上限
6.2 Redis 持久化(推荐生产使用)
Spring AI 提供了 Redis 支持:
@Beanpublic ChatMemoryRepository chatMemoryRepository(RedisTemplate<String, String> redisTemplate) { return new RedisChatMemoryRepository(redisTemplate);}特点:
• 持久化存储 • 分布式支持 • 可设置 TTL 自动过期
6.3 自定义持久化
实现 ChatMemoryRepository 接口即可对接任意存储:
public interface ChatMemoryRepository { List<String> getConversationIds(); void add(String conversationId, String messageJson); List<String> get(String conversationId); void clear(String conversationId);}可以对接 MySQL、PostgreSQL、MongoDB 等。
七、完整项目结构
module-04-memory/01-chat-memory/├── src/main/java/com/ai/hub/memory/│ ├── MemoryConfig.java # ChatMemory + Advisor 配置│ ├── controller/│ │ └── MemoryController.java # 有记忆/无记忆对比端点│ └── MemoryApplication.java # 启动类├── src/main/resources/│ └── application.yml # SiliconFlow API 配置└── pom.xml # spring-ai-client-chat 依赖7.1 配置文件
spring: ai: openai: api-key: ${AI_API_KEY} base-url: https://api.siliconflow.cn/v1 chat: options: model: Qwen/Qwen3-8Bserver: port: 80857.2 所有端点汇总
八、最佳实践
8.1 会话 ID 设计
推荐格式:
// 用户维度String sessionId = "user-" + userId;// 业务维度String sessionId = "order-" + orderId;// 时间维度String sessionId = "user-" + userId + "-" + DateUtils.formatDate(new Date(), "yyyyMMdd");8.2 消息窗口大小选择
原则:够用即可,不要过大
8.3 记忆清理时机
• 用户主动退出时立即清理 • 设置会话 TTL(Redis 方案) • 定期清理过期会话(数据库方案)
九、总结
今天我们掌握了 Spring AI 的对话记忆机制:
最易踩坑:参数名写错导致 advisor 找不到会话ID。
十、下篇预告
下一篇我们将深入 Advisor 机制:
• 自定义 Advisor 实现请求拦截 • 多 Advisor 组合使用 • Advisor 的执行顺序与依赖关系 • SafeGuardAdvisor 安全防护
敬请期待《第7篇:Advisor 机制与请求拦截》。
代码验证
本文所有代码已验证通过:
===== 无记忆测试 =====第一句:"我的名字是张三" → "你好,张三!"第二句:"我叫什么名字?" → "我无法知道您的真实姓名"===== 有记忆测试 =====Session: 36e0ca99第一句:"我叫张三,是一名Java开发者" → 模型记录第二句:"我叫什么名字?" → "你叫张三" ✓第三句:"我的工作是什么?" → "你是一名Java开发者" ✓清理会话 → 成功 ✓Spring AI Agent 开发学习系列 - 第6篇
夜雨聆风