你有没有这种感觉------AI应用聊着聊着,突然问你"咱们刚才聊到哪了"。恭喜,你遇到了金鱼脑AI。
作为一个被Spring AI折腾了半年的Javaer,今天聊聊ChatMemory这个让人又爱又恨的组件。从内存版到Redis持久化,再到分层记忆架构,踩过的坑比想象的多。
为什么AI需要记忆?
先说个真实场景。
我做了个简历优化工具,用户A花了20分钟描述自己的项目经验,AI给出了详细的优化建议。第二天用户A回来想继续打磨,结果AI一脸懵逼:"您好,请介绍一下您的项目经验。"
20分钟白聊了。
没有Memory的Agent,本质上是个复读机------每次对话都是独立的,不记得任何上下文。这就是为什么我们需要ChatMemory。
Spring AI ChatMemory入门:5行代码跑起来
Spring AI的ChatMemory设计得很简洁,核心就一个接口ChatMemory,两种实现:
java
// 内存版配置
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository())
.maxMessages(20)
.build();
}
然后在对话时:
java
ChatClient.create(chatModel).prompt()
.user("我的简历想优化")
.advisors(new MessageChatMemoryAdvisor(chatMemory, sessionId))
.call()
.content();
sessionId是会话标识,不同用户、不同话题用不同的sessionId就OK。
优点 :零配置,零依赖,跑起来贼快。
致命缺点:服务一重启,Memory全清空。
坑1:内存版重启全丢
当时我的简历优化工具测试用一个小时打磨了一篇完全符合JD的文章,结果改了点配置一重启,之前和大模型对话的记忆全丢了,我就知道图省事用的InMemoryChatMemoryRepository该换了。
教训:小测试用内存记忆没问题,后期或生产环境必须上持久化。
升级Redis持久化:配置so easy
Spring AI Alibaba官方提供了Redis版ChatMemoryRepository,改造成本极低:
引入redis持久化依赖
xml
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-memory-redis</artifactId>
</dependency>
java
@Bean
public RedisChatMemoryRepository chatMemoryRepository(RedisTemplate<String, Object> redisTemplate) {
return RedisChatMemoryRepository.builder()
.redisTemplate(redisTemplate)
.build();
}
@Bean
public ChatMemory chatMemory(RedisChatMemoryRepository repository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository)
.maxMessages(20)
.build();
}
会话隔离:每个面试会话独立的Memory
关键点------用sessionId做Redis的key前缀:
java
// 推荐做法:ChatClient 可以是无状态的,状态在 Memory 里
@Autowired
private ChatMemory chatMemory; // 这是一个单例 Bean,内部包含 Redis Repository
public String chat(String sessionId, String userMessage) {
// 每次创建新的 Client,但复用同一个 Memory 实例
// Memory 会根据 sessionId 自动去 Redis 找该用户的记录
return ChatClient.builder(chatModel)
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
.build()
.prompt()
.user(u -> u.text(userMessage).param("sessionId", sessionId)) // 传递 sessionId
.call()
.content();
}
这样每个面试场景、每个用户都有独立的Memory空间,互不干扰。
坑2:每次new Agent,Memory都丢了
我踩过另一个大坑:每次new ChatClient,Memory都丢了。
java
// 错误做法:每次请求都new ChatClient
ChatClient chatClient = ChatClient.builder()
.chatModel(chatModel)
.memory(memory) // 新实例,没有历史记忆
.build();
解法:用ConcurrentHashMap缓存Agent实例:
java
private final Map<String, ReactAgent> agentCache = new ConcurrentHashMap<>();
public ReactAgent getOrCreateAgent(String sessionId, ChatMemory memory) {
return agentCache.computeIfAbsent(sessionId,
id -> ReactAgent.builder()
.chatModel(chatModel)
.memory(memory)
.build());
}
同一个sessionId复用同一个ChatClient实例,Memory得以保留。
⚠️ 但这里有个隐形坑 :agentCache会无限增长,长期运行必然OOM。必须配合sessionId过期清理:
java
// 定期清理过期Agent实例(比如会话超过2小时未活跃)
agentCache.entrySet().removeIf(entry -> {
LocalDateTime lastActive = sessionLastActive.get(entry.getKey());
return lastActive != null && lastActive.isBefore(LocalDateTime.now().minusHours(2));
});
别问我怎么知道的------线上跑了一周,4G堆直接打满 🤡
坑3:对话太长token溢出
简历优化场景有个典型用法:用户A是高级工程师,10轮面试模拟下来积累了20+条消息。
结果:context window溢出了。
AI开始"失忆",答非所问,甚至忘记之前约定的简历优化方向。
根本原因:MessageWindowChatMemory的maxMessages设置的是消息条数,不是token数。当对话变长,历史消息累积,prompt越来越长,token不够用了。
MessageWindowChatMemory:滑动窗口的正确打开方式
Spring AI的MessageWindowChatMemory本质上是个滑动窗口,只会保留最近N条消息:
java
MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository())
.maxMessages(20) // 只保留最近20条
.build();
认知误区 :maxMessages=20指的是20条消息,不是20轮对话。一轮对话可能包含user message + assistant response = 2条消息。
建议设置合理的窗口大小,避免早期重要信息被"挤出"。
Memory容量控制
Redis持久化后,数据不会自动清理。长期运行会导致Redis内存持续增长。
解法1:设置TTL过期
java
redisTemplate.expire("interview:memory:" + sessionId, Duration.ofHours(2));
解法2:主动清理
java
redisTemplate.delete("interview:memory:" + sessionId);
建议用解法1兜底,解法2作为主动管理。
进阶:ChatGPT分层记忆的启示
聊完基础实现,说说更进阶的思考。
ChatGPT等产品采用的是分层记忆架构:
| 层级 | 内容 | Spring AI对应 |
|---|---|---|
| L1 原始对话 | 完整对话记录 | RedisChatMemoryRepository ✅ |
| L2 用户档案卡 | 结构化用户画像 | 自行实现 ❌ |
| L3 近期摘要 | AI压缩的关键信息摘要 | 自行实现 ❌ |
| L4 滑动窗口 | 最近N轮对话 | MessageWindowChatMemory ✅ |
现实很骨感:Spring AI目前只覆盖了L1和L4。L2用户档案卡、L3摘要记忆需要自己实现。
我的实践
对于简历优化工具,我额外维护了用户画像:
java
// L2: 用户档案卡
public class UserProfile {
private String userId;
private String name;
private String targetPosition;
private List<String> skills;
private List<String> projects;
private LocalDateTime lastActive;
}
每次对话结束后更新用户画像,后续对话时把画像作为system prompt注入。
这样即使用户聊了50轮,只要打开简历优化相关的对话,AI都能快速定位到"这位用户是Java后端,投递字节跳动,擅长Spring Cloud"。
L3摘要记忆:用AI压缩AI的对话
L3是最有想象力的一层------让ChatModel自己压缩历史对话。
思路很简单:当对话超过N轮,用一次LLM调用把早期对话压缩成摘要,摘要替换原始消息塞回Memory。
java
// 伪代码:L3摘要压缩
public String compressHistory(List<Message> oldMessages) {
String conversationText = oldMessages.stream()
.map(Message::getText)
.collect(Collectors.joining("\n"));
return chatModel.call("请将以下对话历史压缩为关键信息摘要,保留:\n"
+ "1. 用户的简历特点\n2. 已确认的优化方向\n3. 待处理的问题\n\n"
+ conversationText);
}
压缩后,50轮对话的历史从几千token压缩到几百token,但关键信息不丢。这就是ChatGPT"remember"功能的底层逻辑------不是真的记住每一句话,而是记住摘要。
总结对比表
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| InMemoryChatMemory | 开发调试 | 零配置,速度快 | 重启丢失 |
| RedisChatMemory + 滑动窗口 | 生产环境单用户 | 持久化+容量控制 | 需要Redis依赖 |
| Redis + 用户档案卡 | 多轮复杂场景 | 个性化强 | 实现成本高 |
| 分层记忆架构 | 企业级应用 | 全方位记忆 | 需要自研 |
建议:从Redis + 滑动窗口起步,快速跑通业务。后续根据需求逐步添加用户画像、摘要记忆等分层能力。
你在Chat Memory上踩过什么坑? 重启丢数据、Agent生命周期、对话溢出------这三类你遇到过哪种?评论区聊聊 👇