LangChain4j 会话记忆存数据库?手把手教你自定义 ChatMemoryStore 接口实现

张开发
2026/4/12 11:51:32 15 分钟阅读

分享文章

LangChain4j 会话记忆存数据库?手把手教你自定义 ChatMemoryStore 接口实现
LangChain4j 会话记忆持久化实战从接口设计到 MySQL 实现当开发者需要构建一个具备长期记忆能力的对话系统时LangChain4j 提供的ChatMemoryStore接口就像一把瑞士军刀——小巧但功能完备。这个看似简单的接口背后隐藏着对话系统持久化存储的核心逻辑。本文将带您深入理解接口设计哲学并手把手实现一个基于 MySQL 的存储方案。1. 理解 ChatMemoryStore 的设计哲学ChatMemoryStore接口只有三个方法这种极简设计体现了约定优于配置的理念。在 Spring Boot 项目中引入 LangChain4j 依赖后您会发现这个接口就像对话系统的记忆中枢dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.28.0/version /dependency接口的三个核心方法构成一个完整的 CRUD 循环getMessages对话系统的记忆读取操作updateMessages兼具插入和更新功能的记忆写入操作deleteMessages记忆清除功能这种设计有两大精妙之处将复杂的对话树结构序列化责任交给实现类通过合并 insert 和 update 操作简化接口提示在实际项目中updateMessages 的实现要特别注意线程安全问题特别是在高并发场景下。2. MySQL 存储方案设计与实现2.1 数据库表结构设计我们采用关系型数据库最经典的会话-消息双表结构。以下是经过生产环境验证的表设计CREATE TABLE chat_session ( id BIGINT NOT NULL AUTO_INCREMENT, session_id VARCHAR(64) NOT NULL COMMENT LangChain4j 生成的会话ID, user_id VARCHAR(64) NOT NULL COMMENT 业务系统用户ID, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_session (session_id), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE chat_message ( id BIGINT NOT NULL AUTO_INCREMENT, session_id VARCHAR(64) NOT NULL, message_id VARCHAR(64) NOT NULL COMMENT 消息唯一标识, message_type ENUM(SYSTEM,AI,USER) NOT NULL, content JSON NOT NULL COMMENT 结构化存储消息内容, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_message (message_id), KEY idx_session (session_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这个设计有几个关键优化点使用 JSON 类型存储序列化后的消息内容通过 created_at/updated_at 实现自动时间戳管理为所有查询条件建立合适索引2.2 MyBatis 实现数据访问层在 Spring Boot 项目中整合 MyBatis 和 MySQLdependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency定义 Mapper 接口时特别注意批量操作的方法Mapper public interface ChatMessageMapper { Select(SELECT content FROM chat_message WHERE session_id #{sessionId} ORDER BY created_at ASC) ListString findMessagesBySession(String sessionId); Insert({ script, INSERT INTO chat_message(session_id, message_id, message_type, content) VALUES , foreach collectionmessages itemmsg separator,, (#{sessionId}, #{msg.id}, #{msg.type}, #{msg.content}), /foreach, /script }) int insertMessages(Param(sessionId) String sessionId, Param(messages) ListChatMessageDTO messages); Delete(DELETE FROM chat_message WHERE session_id #{sessionId}) int deleteMessages(String sessionId); }3. 实现 ChatMemoryStore 接口完整的实现类需要考虑事务管理、异常处理和性能优化Component RequiredArgsConstructor public class MySQLChatMemoryStore implements ChatMemoryStore { private final ChatSessionMapper sessionMapper; private final ChatMessageMapper messageMapper; private final ObjectMapper objectMapper; Override Transactional(readOnly true) public ListChatMessage getMessages(Object memoryId) { String sessionId (String) memoryId; if (!sessionMapper.exists(sessionId)) { return Collections.emptyList(); } return messageMapper.findMessagesBySession(sessionId).stream() .map(json - ChatMessageDeserializer.messageFromJson(json)) .collect(Collectors.toList()); } Override Transactional public void updateMessages(Object memoryId, ListChatMessage messages) { String sessionId (String) memoryId; // 原子性的存在即更新不存在则插入 if (sessionMapper.exists(sessionId)) { sessionMapper.touch(sessionId); } else { sessionMapper.insert(new ChatSession(sessionId, default-user)); } // 先删除旧消息再插入新消息 messageMapper.deleteMessages(sessionId); ListChatMessageDTO dtos messages.stream() .map(msg - new ChatMessageDTO( UUID.randomUUID().toString(), msg.type().name(), ChatMessageSerializer.messageToJson(msg))) .collect(Collectors.toList()); messageMapper.insertMessages(sessionId, dtos); } Override Transactional public void deleteMessages(Object memoryId) { String sessionId (String) memoryId; messageMapper.deleteMessages(sessionId); sessionMapper.delete(sessionId); } }实现时的几个技术要点使用Transactional确保操作原子性采用批量插入提高性能通过 touch 操作更新会话时间戳合理处理对象序列化/反序列化4. 在 Spring Boot 中集成自定义存储最后将我们的实现注入到 LangChain4j 的对话系统中Configuration public class ChatConfig { Bean public ChatMemoryProvider chatMemoryProvider(MySQLChatMemoryStore memoryStore) { return memoryId - MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(20) .chatMemoryStore(memoryStore) .build(); } Bean public Assistant assistant(ChatLanguageModel model, ChatMemoryProvider provider) { return AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemoryProvider(provider) .build(); } public interface Assistant { String chat(MemoryId String sessionId, UserMessage String message); } }实际测试时您会发现即使重启应用对话历史也能完整恢复SpringBootTest class ChatMemoryTest { Autowired Assistant assistant; Test void testPersistence() { String sessionId test-session-1; // 第一次对话 String reply1 assistant.chat(sessionId, 我叫张三); assertThat(reply1).contains(你好); // 模拟应用重启... // 第二次对话 String reply2 assistant.chat(sessionId, 我叫什么名字); assertThat(reply2).contains(张三); } }对于需要处理高并发的场景可以在实现类中加入 Redis 缓存层形成MySQL Redis的双存储架构。缓存策略建议采用写穿透模式确保数据一致性。

更多文章