LangChain4j-03 ChatMemory 详解:告别“金鱼脑”,实现多轮对话记忆
LangChain4j-03 ChatMemory 详解:告别“金鱼脑”,实现多轮对话记忆
本文基于 LangChain4j 官方文档,结合实战经验,系统讲解如何利用
ChatMemory机制让大模型记住对话上下文。
🙋♂️ 让我们保持联系
觉得这篇文章有帮助吗? 一个小小的互动对我意义重大:
点赞 👍 - 让我知道这篇文章对你有价值
收藏 🔖 - 遇到问题时能快速找到解决方案
关注 📌 - 不错过后续的深度技术分享
🧠 一、 为什么需要 ChatMemory?
手动维护List<ChatMessage>来记录聊天记录非常繁琐,且面临三大难题:
- 上下文溢出:LLM 有 Token 限制,历史记录不能无限增长。
- 状态丢失:默认内存存储,服务重启后对话记忆清零。
- 逻辑复杂:需要手动处理系统消息、工具消息的关联性。
解决方案:LangChain4j 提供了ChatMemory抽象层,它本质上是一个智能的、带自动清理策略的聊天消息容器。
📚 二、 核心概念与类图
1. 核心类关系图
2. “记忆” vs “历史”
| 维度 | Memory (记忆) | History (历史) |
|---|---|---|
| 目的 | 给 LLM 提供上下文 | 给用户查看完整记录 |
| 内容 | 部分消息(按策略筛选) | 所有原始消息 |
| 现状 | ✅ LangChain4j 内置 | ❌ 需手动实现(如存DB) |
⚙️ 三、 开箱即用的实现
1. MessageWindowChatMemory(按条数限制)
基于滑动窗口算法,保留最近的 N 条消息(User 和 AI 各算一条)。
// 示例:只保留最近2条对话ChatMemorychatMemory=MessageWindowChatMemory.builder().maxMessages(2).build();// 使用:通常在 AI Service 中绑定OpenAiChatModelmodel=OpenAiChatModel.builder().baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo")// 固定演示密钥.modelName("gpt-4o-mini").build();MyServiceaiService=AiServices.builder(MyService.class).chatModel(model).chatMemory(chatMemory)// 绑定记忆.build();System.out.println(aiService.chat("我是 zzn"));System.out.println(aiService.chat("我是java工程师"));//System.out.println(aiService.chat("我是谁?")); // 只保留最近2条对话,它已经不知道我是谁了!!!但是它知道我的职业//System.out.println(aiService.chat("我是什么职业?"));适用场景:快速原型开发,简单对话。
2. TokenWindowChatMemory(按Token限制)
更科学的实现,保留最近的 N 个 Token(需配合 Tokenizer)。
TokenCountEstimatorestimator=newOpenAiTokenCountEstimator("gpt-4o-mini");// 2. 构建 TokenWindowChatMemoryTokenWindowChatMemorychatMemory=TokenWindowChatMemory.builder().maxTokens(100,estimator)// ✅ 传入估算器.build();OpenAiChatModelmodel=OpenAiChatModel.builder().baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo")// 固定演示密钥.modelName("gpt-4o-mini").build();MyServiceaiService=AiServices.builder(MyService.class).chatModel(model).chatMemory(chatMemory)// 绑定记忆.build();System.out.println(aiService.chat("我是 zzn,是一名java工程师,我主攻Java领域,擅长高并发的分布式架构,帮我给出职业生涯规划!"));System.out.println(aiService.chat("我是谁,我的职业是什么?"));//因为一轮会话已经超过100个token,所以它已经不知道我是谁了. 如果把上面的maxtoken改成更大,如10000就能记住你是谁适用场景:生产环境,精确控制成本与上下文长度。
💾 四、 进阶实战:自定义持久化 (ChatMemoryStore)
默认内存存储重启即失,要实现跨会话记忆(如存Redis/MySQL),需实现ChatMemoryStore接口。
1. 核心接口方法
updateMessages(Object memoryId, List<ChatMessage> messages):最关键。当新增或驱逐消息时调用,用于全量更新存储。getMessages(Object memoryId):加载某次对话的所有消息。deleteMessages(Object memoryId):清除对话记录。
2. 基于 Redis 的持久化实现
Java 源码:RedisChatMemoryStore.java
/** * 基于RedisTemplate的ChatMemoryStore实现 */publicclassRedisChatMemoryStoreimplementsChatMemoryStore{privatestaticfinalStringKEY_PREFIX="chat:memory:";privatefinalRedisTemplate<String,String>redisTemplate;privatefinalLongttlSeconds;publicRedisChatMemoryStore(RedisTemplate<String,String>redisTemplate){this(redisTemplate,null);}publicRedisChatMemoryStore(RedisTemplate<String,String>redisTemplate,LongttlSeconds){this.redisTemplate=redisTemplate;this.ttlSeconds=ttlSeconds;}@OverridepublicList<ChatMessage>getMessages(ObjectmemoryId){Stringkey=KEY_PREFIX+memoryId;Stringjson=redisTemplate.opsForValue().get(key);if(json==null||json.isEmpty()){returnnewArrayList<>();}try{// ✅ 正确用法:使用ChatMessageDeserializerreturnChatMessageDeserializer.messagesFromJson(json);}catch(Exceptione){thrownewRuntimeException("Failed to deserialize chat messages from Redis",e);}}@OverridepublicvoidupdateMessages(ObjectmemoryId,List<ChatMessage>messages){Stringkey=KEY_PREFIX+memoryId;// ✅ 序列化:使用ChatMessageSerializerStringjson=ChatMessageSerializer.messagesToJson(messages);if(ttlSeconds!=null){redisTemplate.opsForValue().set(key,json,ttlSeconds,TimeUnit.SECONDS);}else{redisTemplate.opsForValue().set(key,json);}}@OverridepublicvoiddeleteMessages(ObjectmemoryId){Stringkey=KEY_PREFIX+memoryId;redisTemplate.delete(key);}/** * 清理测试数据 */publicvoidclearAll(){Stringpattern=KEY_PREFIX+"*";redisTemplate.delete(redisTemplate.keys(pattern));}}使用方式
RedisChatMemoryStorechatMemoryStore=newRedisChatMemoryStore(redisTemplate,1800L);// 示例:只保留最近2条对话ChatMemorychatMemory=MessageWindowChatMemory.builder().maxMessages(2).chatMemoryStore(chatMemoryStore).id("zzn").build();// 使用:通常在 AI Service 中绑定OpenAiChatModelmodel=OpenAiChatModel.builder().baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo")// 固定演示密钥.modelName("gpt-4o-mini").build();MyServiceaiService=AiServices.builder(MyService.class).chatModel(model).chatMemory(chatMemory)// 绑定记忆.build();System.out.println(aiService.chat("我是 zzn"));System.out.println(aiService.chat("我是java工程师"));System.out.println(redisTemplate.opsForValue().get("chat:memory:zzn"));//只有“我是java工程师”和Ai回复在redis中使用方式-ChatMemoryProvider&@MemoryId
RedisChatMemoryStorechatMemoryStore=newRedisChatMemoryStore(redisTemplate,1800L);// 示例:只保留最近2条对话ChatMemoryProviderchatMemoryProvider=memoryId->MessageWindowChatMemory.builder().id(memoryId).maxMessages(2).chatMemoryStore(chatMemoryStore).build();// 使用:通常在 AI Service 中绑定OpenAiChatModelmodel=OpenAiChatModel.builder().baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo")// 固定演示密钥.modelName("gpt-4o-mini").build();MyServiceaiService=AiServices.builder(MyService.class).chatModel(model).chatMemoryProvider(chatMemoryProvider)// 绑定记忆.build();System.out.println(aiService.chat("zzn","我是 zzn"));System.out.println(aiService.chat("zzn","我是java工程师"));System.out.println(redisTemplate.opsForValue().get("chat:memory:zzn"));//SystemMessage特权:只有SystemMessage和Ai回复在redis中⚠️ 五、 特殊消息处理机制
1. 系统消息 (SystemMessage)
SystemMessage拥有**“特权”。在MessageWindowChatMemory中,系统消息不会被自动驱逐**(除非手动清除),确保 LLM 始终遵循初始指令。
2. 工具消息 (Tool Messages)
为了防止孤儿消息(Orphaned ToolExecutionResultMessage)错误,框架做了特殊处理:
- 当一条包含
ToolExecutionRequest的AiMessage被驱逐时,其对应的ToolExecutionResultMessage也会被自动连带驱逐。 - 这是因为某些 LLM(如 OpenAI)严格禁止在请求中发送没有对应请求的执行结果。
🎯 六、 总结与最佳实践
- 选型建议:
- 开发/测试:直接用
MessageWindowChatMemory,简单粗暴。 - 生产环境:优先
TokenWindowChatMemory+RedisChatMemoryStore。
- 开发/测试:直接用
- ID 设计:
memoryId建议使用userId:conversationId格式,实现多租户隔离。 - 性能注意:
updateMessages会在每次交互时调用两次(用户输入+AI回复),高频场景建议使用批量写入或异步存储优化。
通过ChatMemory,你可以轻松构建出能进行连贯多轮对话的智能应用,而无需关心底层的消息管理与淘汰逻辑。
🙋♂️ 让我们保持联系
觉得这篇文章有帮助吗? 一个小小的互动对我意义重大:
点赞 👍 - 让我知道这篇文章对你有价值
收藏 🔖 - 遇到问题时能快速找到解决方案
关注 📌 - 不错过后续的深度技术分享
