Spring AI ChatMemory踩坑实录:重启丢数据、Agent丢记忆、对话溢出

你有没有这种感觉------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生命周期、对话溢出------这三类你遇到过哪种?评论区聊聊 👇

相关推荐
壹方秘境2 小时前
我用Go语言开发了一个跨平台的HTTPS抓包和调试工具
前端·后端·ios
神秘面具男2 小时前
HarmonyOS 6.0跨端远程控制
前端·后端
苏三说技术2 小时前
全网爆火的Loop到底是什么?
后端
神奇小汤圆3 小时前
Loop Runtime 架构拆解:别再手动催 Agent,先把工程闭环跑起来
后端
浩风祭月3 小时前
Cursor + Claude Code实战:从需求分析到测试提交的完整流程
ai编程·claude·cursor
程序员cxuan3 小时前
幽默,一个 Github 名字叫“马尾辫”,但是他给你省了 80% 的 token
人工智能·后端·程序员
程序员晓琪3 小时前
约定大于配置:基于 Java 包名自动生成 API 版本路由的最佳实践
java·spring boot·后端
didadida2623 小时前
Isshin AI Agent:LLM 工具路由架构
ai编程