LangChain4j 记忆功能

LangChain4j 记忆功能

薛定谔的汪 Lv5

LangChain4j 记忆功能——从内存存储到分布式持久化

一、AI的”记忆”到底是什么?

你有没有遇到过这种情况:跟AI聊了几句后,问它”我刚才说了什么?”,它一脸茫然地回答”抱歉,我没有之前的对话记录”。

这就是AI的”健忘症”——每一次对话请求本质上都是独立的,AI不会天然”记住”你之前说过什么。

在LangChain4j中,ChatMemory就是解决这个问题的核心组件。它不是简单地保存所有对话,而是一套智能的记忆管理机制,包括:

  • 淘汰策略:防止对话过长超出Token限制
  • 持久化:让记忆在服务重启后依然存在
  • 多用户隔离:不同用户的对话互不干扰

官方文档明确指出:”记忆”和”历史”是两个不同概念:

  • 历史:完整的对话记录,用户能在UI上看到的内容
  • 记忆:经过处理后的信息,只保留对AI有用的部分

LangChain4j目前只提供”记忆”功能,如果需要完整历史记录,需要自己实现。

二、三种记忆策略:选对方案很重要

2.1 MessageWindowChatMemory:按消息数量滑动窗口

这是最简单的实现,像一个固定大小的队列,只保留最近N条消息。

1
2
3
4
// 创建保留最近10条消息的记忆
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(10)
.build();

优点:实现简单、性能好
缺点:不考虑Token数量,可能导致超限
适用场景:快速原型开发、短会话场景

2.2 TokenWindowChatMemory:按Token精确控制(推荐)

这是更专业的选择,基于Token数量进行淘汰,精确控制上下文长度。

1
2
3
4
5
// 创建保留最近2000个Token的记忆
Tokenizer tokenizer = new OpenAiTokenizer("gpt-3.5-turbo");
ChatMemory chatMemory = TokenWindowChatMemory.builder()
.maxTokens(2000, tokenizer)
.build();

优点:精确控制Token使用,不会超限
缺点:需要Tokenizer,有额外计算开销
适用场景:生产环境、长对话场景

2.3 摘要记忆:让AI”记住”更久远的事

这是一个进阶策略,当对话超过阈值时,自动调用LLM对历史对话进行摘要压缩。

1
2
3
// 需要自己实现摘要逻辑
// 核心思路:当消息数量超限时,调用LLM生成摘要
// 用摘要替换旧消息,保留关键信息

这种方式的优势是可以在有限Token内保留更多有效信息,适合需要长期记忆的场景。

2.4 三种策略对比

策略 淘汰依据 精确度 复杂度 推荐场景
MessageWindowChatMemory 消息条数 原型开发
TokenWindowChatMemory Token数量 生产环境首选
摘要记忆 语义压缩 长对话、知识问答

三、开箱即用的内存存储

最简单的使用方式——直接创建ChatMemory并接入AiServices:

java

1
2
3
4
5
6
7
8
9
10
// 1. 创建ChatMemory
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(10)
.build();

// 2. 创建AI服务
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 {
// 获取指定memoryId的所有消息
List<ChatMessage> getMessages(Object memoryId);

// 更新指定memoryId的消息列表
void updateMessages(Object memoryId, List<ChatMessage> messages);

// 删除指定memoryId的所有消息
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) // 使用Redis存储
.build();

5.3 MySQL存储实现

适合需要审计、长期归档的场景

关键设计messagesJson字段存储整个对话序列化后的JSON,查询简单但更新时需整体替换。

5.4 各存储方案对比

存储方案 优势 劣势 适用场景
Redis 高性能、支持TTL 数据可能丢失 高并发、分布式
MySQL 数据安全、支持事务 性能相对较低 审计、长期归档

六、分布式场景最佳实践

6.1 为什么需要分布式存储?

单机内存存储的三大痛点:

  1. 服务重启丢失:重启后所有对话记忆消失
  2. 无法水平扩展:多节点部署时,用户请求分发到不同节点,记忆不共享
  3. 内存压力:高并发下内存占用爆炸

6.3 会话隔离的关键设计

在分布式环境中,必须确保会话标识全局唯一:

java

1
2
3
4
5
// 推荐:用户ID + 会话ID 组合
String memoryId = userId + ":" + sessionId;

// 使用
String reply = assistant.chat(memoryId, userMessage);

这样既能区分用户,又能支持一个用户多个会话(如不同设备、不同话题)。

6.4 淘汰策略的特别说明

当消息被ChatMemory淘汰时,updateMessages()方法会被调用,传入的消息列表不包含被淘汰的消息。这意味着存储层会自动同步淘汰,无需额外处理。

七、高级特性

7.1 SystemMessage的特殊处理

SystemMessage在ChatMemory中有特殊行为:

  • 一旦添加,永远保留
  • 同时只能存在一条
  • 相同内容的新消息会被忽略
  • 不同内容的新消息会替换旧的

7.2 工具消息的自动清理

如果包含ToolExecutionRequestAiMessage被淘汰,后续孤立的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
// 1. 定义AI服务接口
@AiService
public interface ChatService {
@SystemMessage("你是一个智能助手,请基于对话历史回答问题")
String chat(@MemoryId String sessionId, @UserMessage String message);
}

// 2. 配置类
@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();
}
}

// 3. Controller使用
@RestController
public class ChatController {
@Autowired
private ChatService chatService;

@PostMapping("/chat")
public String chat(@RequestParam String userId, @RequestBody String message) {
// sessionId = userId:topic,支持多会话
String sessionId = userId + ":" + UUID.randomUUID().toString();
return chatService.chat(sessionId, message);
}
}

九、总结

需求场景 推荐方案
本地测试/原型 MessageWindowChatMemory + 内存存储
生产环境单机 TokenWindowChatMemory + 内存存储
分布式部署 TokenWindowChatMemory + Redis存储
高并发大流量 二级缓存(Caffeine + Redis)+ 会话分片
需要长期审计 MySQL持久化 + 定时清理策略
  • Title: LangChain4j 记忆功能
  • Author: 薛定谔的汪
  • Created at : 2025-04-15 18:01:54
  • Updated at : 2026-04-07 19:27:15
  • Link: https://www.zhengyk.cn/2025/04/15/ai/langchain4j_03_memory/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments