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 工作流》
相关推荐
70asunflower2 小时前
CUDA基础知识巩固检验练习题【附有参考答案】(7)
c++·人工智能·cuda
生活予甜2 小时前
广柔扁平电缆在机器人AI技术创新应用中的前景探索
人工智能·机器人
找藉口是失败者的习惯2 小时前
从LLM到Agent:大语言模型核心概念指南
人工智能·语言模型·自然语言处理
vx-bot5556662 小时前
企业微信ipad协议的消息扩展字段与业务数据注入
java·企业微信·ipad
接着奏乐接着舞。2 小时前
5分钟本地跑起大模型
人工智能·llama
liliangcsdn2 小时前
OpenAI流模式下思考过程的获取示例
人工智能
workflower2 小时前
大型语言模型简史
人工智能·语言模型·自然语言处理·chatgpt·机器人·集成测试·ai编程
buyulian2 小时前
Bug防御体系:技术方案的优与劣
java·经验分享·bug·软件工程
發糞塗牆2 小时前
【Azure 架构师学习笔记 】- Azure AI(20) - Azure Agent实战落地
人工智能·ai·azure