Skip to content

LangChain4j 使用 MySQL 持久化 ChatMemory ​

模型不会自动记住上一次请求。所谓“记忆”,本质上是应用在下一轮请求前重新加载历史消息,并把这些消息加入 prompt。进程内记忆实现简单,但应用重启或多实例部署后会丢失状态;将记忆写入 MySQL,可以获得稳定的生命周期、备份和查询能力。

工作原理 ​

一次带记忆的请求通常经过以下步骤:

  1. 根据用户或会话生成稳定的 memoryId。
  2. 从持久化仓储读取该 ID 的历史消息。
  3. 按消息窗口或 Token 预算裁剪历史。
  4. 将系统消息、历史消息和本轮用户消息组装成请求。
  5. 调用聊天模型并得到助手消息。
  6. 把本轮用户消息和助手消息写回仓储。

MySQL 仓储只负责保存和恢复消息,不负责决定上下文保留多长。窗口策略应放在 ChatMemory 层,否则历史会无限增长并最终超过模型上下文限制。

表结构示例 ​

下面的表结构与具体框架无关,可作为自定义 JDBC 仓储的起点:

sql
CREATE TABLE chat_memory_message (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    memory_id VARCHAR(128) NOT NULL,
    message_index BIGINT NOT NULL,
    role VARCHAR(32) NOT NULL,
    content LONGTEXT NOT NULL,
    created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    UNIQUE KEY uk_memory_message (memory_id, message_index),
    KEY idx_memory_created (memory_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

message_index 用于保证同一会话内的顺序稳定,LONGTEXT 可以容纳较长消息以及工具调用内容。生产环境还应增加租户字段、过期时间、脱敏或加密策略,并明确删除和保留期限。

依赖与数据源 ​

版本号和持久化模块名称会随 LangChain4j 版本变化,发布前应以目标版本官方文档为准。基础依赖可以这样配置:

xml
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j</artifactId>
    <version>${langchain4j.version}</version>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>${mysql.version}</version>
</dependency>
yaml
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/ai_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

接入 ChatMemory ​

如果目标版本提供官方 JDBC/MySQL 仓储,优先使用它;下面示例用 MysqlChatMemoryStore 表示该仓储,类名和构造器需按实际版本调整:

java
ChatMemoryStore store = new MysqlChatMemoryStore(dataSource);

ChatMemory memory = MessageWindowChatMemory.builder()
        .id(memoryId)
        .maxMessages(20)
        .chatMemoryStore(store)
        .build();

Assistant assistant = AiServices.builder(Assistant.class)
        .chatLanguageModel(model)
        .chatMemory(memory)
        .build();

String answer = assistant.chat(userMessage);

当目标版本没有现成实现时,可以实现 ChatMemoryStore 适配器。适配器至少需要支持按 ID 读取完整消息列表、保存新增消息和删除指定会话。序列化时必须保留角色、文本、工具调用和必要元数据,只保存纯文本会导致下一轮无法正确恢复工具消息。

多用户隔离 ​

不要让一个 ChatMemory 单例承载所有用户的对话。Web 服务应通过 ChatMemoryProvider 或等价工厂按 userId、conversationId 创建独立实例:

java
ChatMemoryProvider provider = memoryId -> MessageWindowChatMemory.builder()
        .id(memoryId)
        .maxMessages(20)
        .chatMemoryStore(store)
        .build();

memoryId 必须经过授权校验,不能直接信任请求参数。否则攻击者只要猜到其他会话标识,就可能读取不属于自己的历史消息。多租户系统还应在查询条件中同时加入租户 ID。

并发写入 ​

同一会话的并发请求必须保证消息序号单调递增。单实例可以使用进程内锁,但多实例部署时应使用数据库事务、悲观锁或带版本号的乐观锁。唯一键冲突后应重新读取最新历史并重试,而不是覆盖已有消息。

建议把“读取历史、生成序号、写入本轮消息”设计成清晰的事务边界,并为失败请求记录可重试状态。数据库写入成功而缓存未失效时,也要有一致性处理策略。

验证与排障 ​

  • 重启应用后,用同一个 memoryId 提问,模型仍能引用上一轮事实。
  • 换用另一个 memoryId,不能看到前一个会话的内容。
  • 超过窗口上限后,最旧消息被裁剪,系统消息仍然保留。
  • 删除会话后,数据库和缓存中都无法再读取该历史。
  • 检查 utf8mb4、时区、连接池上限和慢查询。

常见问题包括只保存用户消息而漏保存助手消息、把无限历史全部放入 prompt、截断工具调用 JSON、将用户 ID 当作可猜测的数据库主键,以及没有处理并发写入冲突。

ChatMemory 与长期记忆 ​

会话历史适合放在 ChatMemory。用户偏好、业务事实和跨会话知识则应经过提取、去重和权限过滤后写入独立的用户画像或检索系统。不要把所有历史消息无差别地长期注入 prompt,这会增加 Token 成本、上下文噪声和隐私风险。

小结 ​

MySQL 持久化解决的是“历史消息可靠保存和恢复”问题,窗口记忆解决的是“本轮请求保留哪些消息”问题,多用户隔离和并发控制则决定了系统能否安全运行。先明确这三个边界,再根据 LangChain4j 目标版本选择官方仓储或自定义适配器,能避免把数据库误当成无限长期记忆。

参考资料 ​