LangChain4j 记忆架构:ChatMemory、持久化与跨会话状态
前言
多轮对话是 AI 应用的核心场景,LangChain4j 提供了完善的记忆架构,支持窗口滑动、Token 预算、持久化存储、跨会话状态管理。本文将深入剖析 ChatMemory 体系的设计与使用。
一、内存类型
1.1 MessageWindowChatMemory(窗口滑动)
基于消息数量的记忆管理:
java
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
// 创建窗口记忆
ChatMemory memory = MessageWindowChatMemory.builder()
.maxMessages(10) // 保留最近 10 条消息
.build();
// 添加消息
memory.add(SystemMessage.from("你是一个助手"));
memory.add(UserMessage.from("你好"));
memory.add(AiMessage.from("你好,有什么可以帮助你?"));
memory.add(UserMessage.from("我叫张三"));
memory.add(AiMessage.from("你好张三!"));
// 获取消息
List<ChatMessage> messages = memory.messages();
System.out.println("消息数量: " + messages.size());
1.2 TokenWindowChatMemory(Token 预算)
基于 Token 数量的记忆管理:
java
import dev.langchain4j.memory.chat.TokenWindowChatMemory;
import dev.langchain4j.model.Tokenizer;
// 创建 Token 窗口记忆
TokenWindowChatMemory tokenMemory = TokenWindowChatMemory.builder()
.maxTokens(2000) // 最大 2000 Tokens
.tokenizer(new OpenAiTokenizer()) // Token 计数器
.build();
// 添加消息(自动计算 Token)
tokenMemory.add(UserMessage.from("这是一个很长的文本..."));
// 当前 Token 数量
int currentTokens = tokenMemory.currentTokens();
System.out.println("当前 Token: " + currentTokens);
1.3 消息类型对比
| 特性 | MessageWindowChatMemory | TokenWindowChatMemory |
|---|---|---|
| 计量单位 | 消息数量 | Token 数量 |
| 优势 | 简单直观 | 精确控制上下文长度 |
| 劣势 | 无法精确控制 Token | 需要额外的 Token 计数器 |
| 适用场景 | 短对话、固定轮数 | 长对话、Token 限制严格 |
二、持久化适配
2.1 ChatMemoryStore 接口
java
import dev.langchain4j.memory.chat.ChatMemoryStore;
import dev.langchain4j.data.message.ChatMessage;
// 实现持久化存储
public class InMemoryChatMemoryStore implements ChatMemoryStore {
private final Map<String, List<ChatMessage>> store = new ConcurrentHashMap<>();
@Override
public List<ChatMessage> getMessages(Object memoryId) {
return store.getOrDefault(memoryId, Collections.emptyList());
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
store.put(memoryId.toString(), new ArrayList<>(messages));
}
@Override
public void deleteMessages(Object memoryId) {
store.remove(memoryId);
}
}
2.2 Redis 持久化
java
import redis.clients.jedis.JedisPooled;
import com.fasterxml.jackson.databind.ObjectMapper;
public class RedisChatMemoryStore implements ChatMemoryStore {
private final JedisPooled jedis;
private final ObjectMapper objectMapper;
public RedisChatMemoryStore(String host, int port, String password) {
this.jedis = new JedisPooled(host, port, password);
this.objectMapper = new ObjectMapper();
}
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String key = "chat_memory:" + memoryId;
String json = jedis.get(key);
if (json == null) {
return Collections.emptyList();
}
try {
return objectMapper.readValue(json,
new TypeReference<List<ChatMessage>>() {});
} catch (Exception e) {
throw new RuntimeException("反序列化失败", e);
}
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
String key = "chat_memory:" + memoryId;
try {
String json = objectMapper.writeValueAsString(messages);
jedis.set(key, json);
jedis.expire(key, 86400); // 24 小时过期
} catch (Exception e) {
throw new RuntimeException("序列化失败", e);
}
}
}
2.3 JDBC 持久化
java
import java.sql.*;
public class JdbcChatMemoryStore implements ChatMemoryStore {
private final Connection connection;
public JdbcChatMemoryStore(String url, String username, String password) throws SQLException {
this.connection = DriverManager.getConnection(url, username, password);
createTable();
}
private void createTable() throws SQLException {
String sql = """
CREATE TABLE IF NOT EXISTS chat_memory (
memory_id VARCHAR(255) PRIMARY KEY,
messages TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""";
try (Statement stmt = connection.createStatement()) {
stmt.execute(sql);
}
}
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String sql = "SELECT messages FROM chat_memory WHERE memory_id = ?";
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
pstmt.setString(1, memoryId.toString());
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
String json = rs.getString("messages");
return objectMapper.readValue(json,
new TypeReference<List<ChatMessage>>() {});
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return Collections.emptyList();
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
String sql = """
INSERT INTO chat_memory (memory_id, messages)
VALUES (?, ?)
ON CONFLICT (memory_id) DO UPDATE SET messages = ?, updated_at = CURRENT_TIMESTAMP
""";
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
pstmt.setString(1, memoryId.toString());
String json = objectMapper.writeValueAsString(messages);
pstmt.setString(2, json);
pstmt.setString(3, json);
pstmt.executeUpdate();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
三、跨会话记忆
3.1 用户级记忆(Per-User)
java
import dev.langchain4j.service.MemoryId;
@AiService
public interface UserMemoryAssistant {
@MemoryId // 指定记忆 ID
String chat(@MemoryId String userId, String message);
}
// 使用
UserMemoryAssistant assistant = AiServices.builder(UserMemoryAssistant.class)
.chatLanguageModel(model)
.chatMemoryProvider(MessageWindowChatMemory.builder()
.maxMessages(10)
.build())
.build();
// 用户 A
assistant.chat("user_A", "我叫张三");
assistant.chat("user_A", "我的名字是什么?"); // AI 记住:"张三"
// 用户 B(独立记忆)
assistant.chat("user_B", "我叫李四");
assistant.chat("user_B", "我的名字是什么?"); // AI 记住:"李四"
3.2 全局级记忆(Global)
java
@AiService
public interface GlobalMemoryAssistant {
// 不使用 @MemoryId,所有用户共享记忆
String chat(String message);
}
// 使用
GlobalMemoryAssistant assistant = AiServices.builder(GlobalMemoryAssistant.class)
.chatLanguageModel(model)
.chatMemoryProvider(TokenWindowChatMemory.builder()
.maxTokens(2000)
.build())
.build();
// 用户 A
assistant.chat("公司地址是北京市朝阳区");
assistant.chat("公司地址在哪里?"); // AI 回答:"北京市朝阳区"
// 用户 B(共享记忆)
assistant.chat("公司地址在哪里?"); // AI 回答:"北京市朝阳区"
3.3 跨会话记忆流程图
┌─────────────────────────────────────────────────────────────┐
│ 跨会话记忆管理流程 │
└─────────────────────────────────────────────────────────────┘
┌─────────┐ ┌──────────────┐ ┌──────────────┐
│ 用户 A │ -> │ AI Service │ -> │ ChatMemory │
│ 请求 1 │ │ @MemoryId │ │ Provider │
└────┬────┘ │ "user_A" │ └──────┬───────┘
│ └──────────────┘ │
│ │ │
│ ▼ ▼
│ ┌──────────────┐ ┌─────────┐
│ │ 添加到记忆 │ │RedisStore│
│ │ Memory_A │ │持久化 │
│ └──────────────┘ └─────────┘
│
┌─────────┐ ┌──────────────┐ ┌──────────────┐
│ 用户 A │ -> │ AI Service │ -> │ ChatMemory │
│ 请求 2 │ │ @MemoryId │ │ Provider │
└────┬────┘ │ "user_A" │ └──────┬───────┘
│ └──────────────┘ │
│ │ │
│ ▼ ▼
│ ┌──────────────┐ ┌─────────┐
│ │ 读取记忆 A │ │RedisStore│
│ │ 返回历史 │ │检索 │
│ └──────────────┘ └─────────┘
│
┌─────────┐ ┌──────────────┐ ┌──────────────┐
│ 用户 B │ -> │ AI Service │ -> │ ChatMemory │
│ 请求 1 │ │ @MemoryId │ │ Provider │
└────┬────┘ │ "user_B" │ └──────┬───────┘
│ └──────────────┘ │
│ │ │
│ ▼ ▼
│ ┌──────────────┐ ┌─────────┐
│ │ 添加到记忆 │ │RedisStore│
│ │ Memory_B │ │持久化 │
│ └──────────────┘ └─────────┘
│
└───────────────────────────────────────────────┘
记忆隔离:
- 用户 A:Memory_A(独立)
- 用户 B:Memory_B(独立)
- 全局:Shared_Memory(共享)
四、记忆压缩
4.1 总结(Summarization)策略
java
import dev.langchain4j.memory.chat.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
public class MemoryCompressor {
private final ChatLanguageModel model;
public MemoryCompressor(ChatLanguageModel model) {
this.model = model;
}
public ChatMemory compress(ChatMemory memory) {
List<ChatMessage> messages = memory.messages();
if (messages.size() <= 5) {
return memory; // 消息数量少,无需压缩
}
// 总结旧消息
List<ChatMessage> oldMessages = messages.subList(0, messages.size() - 5);
String summary = summarize(oldMessages);
// 创建新记忆
ChatMemory compressed = MessageWindowChatMemory.builder()
.maxMessages(10)
.build();
compressed.add(SystemMessage.from(summary));
// 保留最近 5 条消息
for (ChatMessage msg : messages.subList(messages.size() - 5, messages.size())) {
compressed.add(msg);
}
return compressed;
}
private String summarize(List<ChatMessage> messages) {
String context = messages.stream()
.map(msg -> msg.getClass().getSimpleName() + ": " + msg.text())
.collect(Collectors.joining("\n"));
String prompt = "请用一句话总结以下对话:\n" + context;
return model.generate(prompt);
}
}
4.2 关键信息提取
java
public class KeyInformationExtractor {
private final ChatLanguageModel model;
public String extractKeyInfo(List<ChatMessage> messages) {
String context = messages.stream()
.map(msg -> msg.text())
.collect(Collectors.joining("\n"));
String prompt = """
从以下对话中提取关键信息(用户姓名、需求、偏好等):
%s
请以 JSON 格式返回:
{
"user_name": "...",
"needs": "...",
"preferences": "..."
}
""".formatted(context);
return model.generate(prompt);
}
}
五、实战示例
5.1 跨会话客服系统
java
public class CustomerServiceSystem {
private final UserMemoryAssistant assistant;
private final ChatMemoryStore memoryStore;
public CustomerServiceSystem() {
// 1. 创建 Redis 存储器
this.memoryStore = new RedisChatMemoryStore("localhost", 6379, "password");
// 2. 创建记忆提供器
ChatMemoryProvider memoryProvider = MessageWindowChatMemory.builder()
.maxMessages(20)
.chatMemoryStore(memoryStore)
.build();
// 3. 创建 AI Service
this.assistant = AiServices.builder(UserMemoryAssistant.class)
.chatLanguageModel(model)
.chatMemoryProvider(memoryProvider)
.build();
}
public String handleCustomer(String userId, String message) {
return assistant.chat(userId, message);
}
}
// 使用
CustomerServiceSystem system = new CustomerServiceSystem();
// 客户张三
system.handleCustomer("customer_123", "我想查询订单");
system.handleCustomer("customer_123", "订单号 123456");
system.handleCustomer("customer_123", "订单状态是什么?"); // AI 记住订单号
// 客户李四(独立记忆)
system.handleCustomer("customer_789", "我要退货");
system.handleCustomer("customer_789", "原因是什么?"); // AI 记住退货意图
六、常见问题
Q1: 如何选择记忆类型?
A: 根据场景选择:
| 场景 | 推荐类型 | 配置 |
|---|---|---|
| 简单对话 | MessageWindowChatMemory | maxMessages=10 |
| 长对话 | TokenWindowChatMemory | maxTokens=2000 |
| 多用户系统 | MessageWindowChatMemory + Redis | +chatMemoryStore |
| 全局知识 | TokenWindowChatMemory | maxTokens=4000 |
Q2: 如何清理过期记忆?
A: 使用 TTL 和定时任务:
java
// Redis 自动过期
jedis.setex("chat_memory:" + userId, 86400, json); // 24 小时
// 定时清理
scheduler.scheduleAtFixedRate(() -> {
memoryStore.cleanupExpired(86400); // 清理超过 24 小时的记忆
}, 0, 1, TimeUnit.HOURS);
Q3: 如何实现记忆备份?
A: 定期导出到文件:
java
public void backupMemory(ChatMemoryStore store, String backupPath) {
// 获取所有记忆
Map<String, List<ChatMessage>> allMemories = store.getAll();
// 写入文件
try (FileWriter writer = new FileWriter(backupPath)) {
objectMapper.writer().writeValue(writer, allMemories);
} catch (Exception e) {
throw new RuntimeException("备份失败", e);
}
}
七、小结
本文深入剖析了 LangChain4j 记忆架构:
- 内存类型:MessageWindowChatMemory、TokenWindowChatMemory
- 持久化适配:ChatMemoryStore 接口、Redis/JDBC 实现
- 跨会话记忆:用户级记忆、全局级记忆
- 记忆压缩:总结策略、关键信息提取
- 实战示例:跨会话客服系统
核心思想: 通过统一的记忆抽象,实现多轮对话的上下文管理和持久化。
下一步学习:
- 文章 11:《LangChain4j 对话状态机:从简单聊天到复杂 Agent 工作流》