LangChain4j 记忆功能——从内存存储到分布式持久化
一、AI的”记忆”到底是什么?
你有没有遇到过这种情况:跟AI聊了几句后,问它”我刚才说了什么?”,它一脸茫然地回答”抱歉,我没有之前的对话记录”。
这就是AI的”健忘症”——每一次对话请求本质上都是独立的,AI不会天然”记住”你之前说过什么。
在LangChain4j中,ChatMemory就是解决这个问题的核心组件。它不是简单地保存所有对话,而是一套智能的记忆管理机制,包括:
- 淘汰策略:防止对话过长超出Token限制
- 持久化:让记忆在服务重启后依然存在
- 多用户隔离:不同用户的对话互不干扰
官方文档明确指出:”记忆”和”历史”是两个不同概念:
- 历史:完整的对话记录,用户能在UI上看到的内容
- 记忆:经过处理后的信息,只保留对AI有用的部分
LangChain4j目前只提供”记忆”功能,如果需要完整历史记录,需要自己实现。
二、三种记忆策略:选对方案很重要
2.1 MessageWindowChatMemory:按消息数量滑动窗口
这是最简单的实现,像一个固定大小的队列,只保留最近N条消息。
1 2 3 4
| ChatMemory chatMemory = MessageWindowChatMemory.builder() .maxMessages(10) .build();
|
优点:实现简单、性能好
缺点:不考虑Token数量,可能导致超限
适用场景:快速原型开发、短会话场景
2.2 TokenWindowChatMemory:按Token精确控制(推荐)
这是更专业的选择,基于Token数量进行淘汰,精确控制上下文长度。
1 2 3 4 5
| Tokenizer tokenizer = new OpenAiTokenizer("gpt-3.5-turbo"); ChatMemory chatMemory = TokenWindowChatMemory.builder() .maxTokens(2000, tokenizer) .build();
|
优点:精确控制Token使用,不会超限
缺点:需要Tokenizer,有额外计算开销
适用场景:生产环境、长对话场景
2.3 摘要记忆:让AI”记住”更久远的事
这是一个进阶策略,当对话超过阈值时,自动调用LLM对历史对话进行摘要压缩。
这种方式的优势是可以在有限Token内保留更多有效信息,适合需要长期记忆的场景。
2.4 三种策略对比
| 策略 |
淘汰依据 |
精确度 |
复杂度 |
推荐场景 |
| MessageWindowChatMemory |
消息条数 |
低 |
低 |
原型开发 |
| TokenWindowChatMemory |
Token数量 |
高 |
中 |
生产环境首选 |
| 摘要记忆 |
语义压缩 |
高 |
高 |
长对话、知识问答 |
三、开箱即用的内存存储
最简单的使用方式——直接创建ChatMemory并接入AiServices:
java
1 2 3 4 5 6 7 8 9 10
| ChatMemory chatMemory = MessageWindowChatMemory.builder() .maxMessages(10) .build();
Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(chatMemory) .build();
|
这是共享记忆模式,所有用户共用同一个上下文。如果你的应用需要区分用户,往下看。
四、多会话记忆隔离:每次回话都由自己的记忆
生产环境必须区分用户的每次对话。LangChain4j提供了@MemoryId注解,优雅地解决这个问题:
java
1 2 3 4 5 6
| @AiService public interface SessionService { @SystemMessage("你是一个专业的客服助手") String chat(@MemoryId String sessonId, @UserMessage String message); }
|
框架内部会根据@MemoryId的值,自动从ChatMemoryStore中获取对应的记忆。
五、持久化:让记忆”断电不丢”
默认的ChatMemory存储在内存中,服务重启后记忆就没了。生产环境必须持久化。
5.1 ChatMemoryStore接口
LangChain4j提供了ChatMemoryStore接口,实现它就能接入任何存储:
java
1 2 3 4 5 6 7 8 9 10
| public interface ChatMemoryStore { List<ChatMessage> getMessages(Object memoryId); void updateMessages(Object memoryId, List<ChatMessage> messages); void deleteMessages(Object memoryId); }
|
LangChain4j提供了序列化工具类,方便将消息转为JSON:
ChatMessageSerializer.messagesToJson():序列化
ChatMessageDeserializer.messagesFromJson():反序列化
5.2 Redis存储实现(重点)
Redis是分布式场景的首选,高性能+天然支持过期。
java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @Component public class RedisChatMemoryStore implements ChatMemoryStore { @Autowired private RedisTemplate<String, String> redisTemplate; private static final String KEY_PREFIX = "chat:memory:"; private static final long TTL_HOURS = 24; @Override public List<ChatMessage> getMessages(Object memoryId) { String key = KEY_PREFIX + memoryId; String json = redisTemplate.opsForValue().get(key); if (Objects.isNull(json)) { return new ArrayList<>(); } return ChatMessageDeserializer.messagesFromJson(json); } @Override public void updateMessages(Object memoryId, List<ChatMessage> messages) { String key = KEY_PREFIX + memoryId; String json = ChatMessageSerializer.messagesToJson(messages); redisTemplate.opsForValue().set(key, json, TTL_HOURS, TimeUnit.HOURS); } @Override public void deleteMessages(Object memoryId) { String key = KEY_PREFIX + memoryId; redisTemplate.delete(key); } }
|
使用方式:
java
1 2 3 4 5 6
| ChatMemoryStore store = new RedisChatMemoryStore(); ChatMemory chatMemory = MessageWindowChatMemory.builder() .id("user_001") .maxMessages(20) .chatMemoryStore(store) .build();
|
5.3 MySQL存储实现
适合需要审计、长期归档的场景
关键设计:messagesJson字段存储整个对话序列化后的JSON,查询简单但更新时需整体替换。
5.4 各存储方案对比
| 存储方案 |
优势 |
劣势 |
适用场景 |
| Redis |
高性能、支持TTL |
数据可能丢失 |
高并发、分布式 |
| MySQL |
数据安全、支持事务 |
性能相对较低 |
审计、长期归档 |
六、分布式场景最佳实践
6.1 为什么需要分布式存储?
单机内存存储的三大痛点:
- 服务重启丢失:重启后所有对话记忆消失
- 无法水平扩展:多节点部署时,用户请求分发到不同节点,记忆不共享
- 内存压力:高并发下内存占用爆炸
6.3 会话隔离的关键设计
在分布式环境中,必须确保会话标识全局唯一:
java
1 2 3 4 5
| String memoryId = userId + ":" + sessionId;
String reply = assistant.chat(memoryId, userMessage);
|
这样既能区分用户,又能支持一个用户多个会话(如不同设备、不同话题)。
6.4 淘汰策略的特别说明
当消息被ChatMemory淘汰时,updateMessages()方法会被调用,传入的消息列表不包含被淘汰的消息。这意味着存储层会自动同步淘汰,无需额外处理。
七、高级特性
7.1 SystemMessage的特殊处理
SystemMessage在ChatMemory中有特殊行为:
- 一旦添加,永远保留
- 同时只能存在一条
- 相同内容的新消息会被忽略
- 不同内容的新消息会替换旧的
7.2 工具消息的自动清理
如果包含ToolExecutionRequest的AiMessage被淘汰,后续孤立的ToolExecutionResultMessage也会自动被淘汰,避免某些LLM提供商的兼容性问题。
7.3 AiServices的并发限制
官方文档特别提醒:
AI Service should not be called concurrently for the same @MemoryId, as it can lead to corrupted ChatMemory.
同一个@MemoryId的并发调用可能导致记忆损坏,需要在业务层做好同步控制。
7.4 记忆与RAG的结合
你可以将RAG检索到的内容也存储到ChatMemory中:
java
1 2 3 4
| AiServices.builder(Assistant.class) .chatMemory(chatMemory) .storeRetrievedContentInChatMemory(true) .build();
|
这样AI可以”记住”它从知识库中检索到的信息。
八、实战:完整的分布式记忆服务
结合以上所有知识点,给出一个可直接使用的完整示例:
java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| @AiService public interface ChatService { @SystemMessage("你是一个智能助手,请基于对话历史回答问题") String chat(@MemoryId String sessionId, @UserMessage String message); }
@Configuration public class ChatMemoryConfig { @Bean public RedisChatMemoryStore chatMemoryStore(RedisTemplate<String, String> redisTemplate) { return new RedisChatMemoryStore(redisTemplate); } @Bean public ChatMemoryProvider chatMemoryProvider(ChatMemoryStore store) { return (memoryId) -> TokenWindowChatMemory.builder() .id(memoryId) .maxTokens(4000, new OpenAiTokenizer("gpt-3.5-turbo")) .chatMemoryStore(store) .build(); } @Bean public ChatService chatService(ChatLanguageModel model, ChatMemoryProvider memoryProvider) { return AiServices.builder(ChatService.class) .chatLanguageModel(model) .chatMemoryProvider(memoryProvider) .build(); } }
@RestController public class ChatController { @Autowired private ChatService chatService; @PostMapping("/chat") public String chat(@RequestParam String userId, @RequestBody String message) { String sessionId = userId + ":" + UUID.randomUUID().toString(); return chatService.chat(sessionId, message); } }
|
九、总结
| 需求场景 |
推荐方案 |
| 本地测试/原型 |
MessageWindowChatMemory + 内存存储 |
| 生产环境单机 |
TokenWindowChatMemory + 内存存储 |
| 分布式部署 |
TokenWindowChatMemory + Redis存储 |
| 高并发大流量 |
二级缓存(Caffeine + Redis)+ 会话分片 |
| 需要长期审计 |
MySQL持久化 + 定时清理策略 |