LangChain4j 记忆架构:ChatMemory、持久化与跨会话状态

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 记忆架构:

  1. 内存类型:MessageWindowChatMemory、TokenWindowChatMemory
  2. 持久化适配:ChatMemoryStore 接口、Redis/JDBC 实现
  3. 跨会话记忆:用户级记忆、全局级记忆
  4. 记忆压缩:总结策略、关键信息提取
  5. 实战示例:跨会话客服系统

核心思想: 通过统一的记忆抽象,实现多轮对话的上下文管理和持久化。

下一步学习:

  • 文章 11:《LangChain4j 对话状态机:从简单聊天到复杂 Agent 工作流》
相关推荐
合米AI SOP系统32 分钟前
传统产线如何快速上马落地 AI 防错?合米科技 AI SOP 7天即可上线。
大数据·人工智能·科技
思录Echo32 分钟前
什么决定具身智能的最终走向?多技术路线与落地现实辨析
大数据·人工智能
Despacito100639 分钟前
Java后端性能探查工具速查表
java·开发语言
ShallWeL1 小时前
Orin 上多模型常驻与显存预算
人工智能·嵌入式硬件·nvidia·orin
xiaohaiAIgeo1 小时前
【2026年】AI监控加行为分析守护实验室安全
大数据·人工智能·科普知识
IT_陈寒1 小时前
Python的多线程就是个假把式,我算是体验到了
前端·人工智能·后端
蓝桉柒71 小时前
下载idea,用idea输入一个程序
java·ide·intellij-idea
集和诚JHCTECH1 小时前
案例分享 | BRAV-7721助力印刷电路板(PCB)智能缺陷复判系统
人工智能·嵌入式硬件·边缘计算
衡石科技1 小时前
ChatBI性能优化与流式响应衡石自然语言问数毫秒级体验技术解析
人工智能·科技·性能优化·企业级bi