尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot整合LangChain4j实现持久化聊天记忆

Spring Boot整合LangChain4j实现持久化聊天记忆 1. 项目概述当聊天记忆遇上Spring Boot去年在开发一个智能客服系统时我遇到了一个典型问题用户每次刷新页面后对话历史就消失了。这就像每次打电话都换了个新接线员用户不得不重复说明需求。于是我开始研究如何实现持久化聊天记忆最终基于LangChain4j和Spring Boot构建了一套完整的解决方案。LangChain4j作为Java生态的LLM集成框架其ChatMemory组件原生支持对话记忆管理。但官方文档对持久化实现和多会话隔离的细节着墨不多而这恰恰是企业级应用的关键需求。本文将分享如何通过Spring Boot整合LangChain4j实现对话历史的数据库持久化存储基于用户ID的多会话记忆隔离动态记忆窗口调整策略生产环境下的性能优化技巧2. 核心架构设计2.1 技术选型决策为什么选择这样的技术组合在评估阶段我对比过几种方案方案优点缺点纯内存存储零延迟重启数据丢失无法扩展Redis存储高性能支持集群需要额外中间件成本较高关系型数据库数据持久化支持复杂查询需要ORM转换性能中等文档数据库(MongoDB)灵活Schema适合JSON数据学习曲线较陡最终选择JPAMySQL的组合因为项目已有MySQL基础设施结构化数据便于后期分析报表生成Spring Data JPA的Repository模式与LangChain4j的ChatMemoryStore接口天然契合2.2 记忆隔离设计方案多会话隔离的核心在于memoryId的生成策略。我们采用复合键方式// 格式{userId}_{sessionId} String memoryId userService.getCurrentUserId() _ sessionManager.getSessionId();这种设计带来三个优势同一用户不同终端会话互不干扰如PC端和移动端支持用户视角的完整对话历史查询会话过期后自动清理关联记忆3. 持久化实现细节3.1 数据库表设计创建专门的chat_message表存储对话记录CREATE TABLE chat_message ( id BIGINT AUTO_INCREMENT PRIMARY KEY, memory_id VARCHAR(255) NOT NULL, message_type ENUM(AI,USER,SYSTEM,TOOL) NOT NULL, content TEXT NOT NULL, tokens INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_memory_id (memory_id) );关键字段说明message_type对应LangChain4j的ChatMessageTypetokens记录消息的token数用于TokenWindow策略计算memory_id建立会话隔离的关联键3.2 实现ChatMemoryStore基于Spring Data JPA的核心实现Repository public class JpaChatMemoryStore implements ChatMemoryStore { PersistenceContext private EntityManager em; Override public ListChatMessage getMessages(Object memoryId) { return em.createQuery( SELECT new ChatMessageDTO(m.messageType, m.content, m.tokens) FROM ChatMessage m WHERE m.memoryId :memoryId ORDER BY m.createdAt ASC, ChatMessageDTO.class) .setParameter(memoryId, memoryId.toString()) .getResultList() .stream() .map(this::toChatMessage) .collect(Collectors.toList()); } Transactional Override public void updateMessages(Object memoryId, ListChatMessage messages) { // 先删除旧记录 deleteMessages(memoryId); // 批量插入新记录 for (int i 0; i messages.size(); i) { ChatMessage msg messages.get(i); em.persist(new ChatMessageEntity( memoryId.toString(), msg.type(), ChatMessageSerializer.messageToJson(msg), estimateTokens(msg), i )); } } private ChatMessage toChatMessage(ChatMessageDTO dto) { return ChatMessageDeserializer.messageFromJson( {\type\:\ dto.getMessageType() \,\text\:\ dto.getContent() \} ); } }重要提示updateMessages()必须加Transactional注解否则批量操作可能导致数据不一致4. 高级功能实现4.1 动态记忆窗口调整根据用户套餐级别动态调整记忆长度Bean public ChatMemory chatMemory(UserService userService) { return TokenWindowChatMemory.builder() .id(generateMemoryId()) .maxTokens(userService.getCurrentUser().getMemoryTokenLimit()) .chatMemoryStore(jpaChatMemoryStore) .tokenCountEstimator(new OpenAITokenCountEstimator()) .build(); }4.2 记忆压缩策略当对话超过token限制时采用智能压缩算法public class CompressingChatMemory extends TokenWindowChatMemory { Override protected void ensureFit(ChatMessage message) { while (calculateTotalTokens() estimateTokens(message) maxTokens) { if (!tryCompressOldestMessages()) { removeOldestMessage(); } } } private boolean tryCompressOldestMessages() { ListChatMessage oldestMessages getMessages().subList(0, 3); if (oldestMessages.stream().allMatch(m - m.type() USER)) { String compressed summarizer.summarize( oldestMessages.stream().map(ChatMessage::text).collect(Collectors.joining(\n)) ); removeMessages(oldestMessages); add(new UserMessage(compressed)); return true; } return false; } }5. 生产环境调优5.1 性能优化方案通过以下措施将95%请求延迟控制在200ms内二级缓存策略Cacheable(value chatMessages, key #memoryId) public ListChatMessage getMessages(String memoryId) { // 数据库查询 }批量写入优化Modifying Query(nativeQuery true, value INSERT INTO chat_message (memory_id, message_type, content, tokens, sequence) VALUES (?,?,?,?,?)) void bulkInsert(ListObject[] batch);5.2 监控指标设计建议监控以下关键指标指标名称采集方式报警阈值记忆加载延迟Micrometer Timer500ms记忆更新失败率Micrometer Counter1%平均对话token数自定义Gauge80%配额记忆压缩率日志分析30%压缩比6. 常见问题解决方案6.1 记忆污染问题现象不同用户的对话内容出现交叉排查步骤检查memoryId生成逻辑验证JPA查询的WHERE条件检查缓存Key设计解决方案// 在memoryId中加入租户ID String memoryId tenantId _ userId _ sessionId;6.2 性能陡降问题现象长时间对话后响应变慢优化方案添加消息数量上限Bean public ChatMemory chatMemory() { return TokenWindowChatMemory.builder() .maxTokens(2000) .maxMessages(50) // 新增限制 .build(); }定期归档旧对话Scheduled(cron 0 0 3 * * ?) public void archiveOldConversations() { chatMessageRepo.archiveOlderThan( LocalDateTime.now().minusDays(30)); }7. 扩展思考在实际项目中我们还实现了这些增强功能敏感信息过滤在持久化前自动脱敏信用卡号等敏感信息public class SanitizingChatMemoryStore implements ChatMemoryStore { private final ChatMemoryStore delegate; Override public void updateMessages(Object memoryId, ListChatMessage messages) { ListChatMessage sanitized messages.stream() .map(this::sanitize) .collect(Collectors.toList()); delegate.updateMessages(memoryId, sanitized); } private ChatMessage sanitize(ChatMessage message) { String text sensitiveDataFilter.filter(message.text()); return new ChatMessage(message.type(), text); } }跨会话记忆继承允许用户选择继续上次未完成的对话public void continuePreviousSession(String previousMemoryId) { ListChatMessage history chatMemoryStore.getMessages(previousMemoryId); chatMemoryStore.updateMessages(currentMemoryId, history); }这套方案已在生产环境稳定运行6个月支持日均10万对话。最大的收获是认识到好的记忆管理不仅要考虑技术实现更需要从用户体验角度设计遗忘策略——有时候忘记和记住同样重要。
返回列表