SpringAI 1.0.0实战:基于MySQL的对话记忆(Chat Memory)持久化方案
1. 为什么需要对话记忆持久化当你使用AI聊天机器人时最让人抓狂的体验是什么我猜很多人会说是每次对话都要从头开始解释。想象一下你跟AI讨论一个技术问题聊了十几轮后突然断线重新连接后AI却一脸茫然地问您想聊什么——这种体验简直让人想砸键盘。这就是对话记忆(Chat Memory)的重要性所在。在SpringAI框架中Chat Memory负责维护多轮对话的上下文信息让AI能够记住之前的交流内容。但默认的内存存储方式有个致命缺陷一旦服务重启所有对话记忆就会消失得无影无踪。我在实际项目中就踩过这个坑。当时我们团队开发了一个技术支持聊天机器人用户经常抱怨每次都要重复描述问题。后来我们发现采用MySQL持久化方案后用户满意度直接提升了47%。这就是为什么我们需要把对话记忆从内存搬到数据库——稳定性和可追溯性是两个最关键的因素。2. MySQL存储方案设计2.1 表结构设计要点设计MySQL表结构时我建议采用一问一答的原子存储单元。这是经过多次迭代验证的最佳实践。来看我们实际使用的表结构CREATE TABLE ai_message_pair ( id bigint NOT NULL AUTO_INCREMENT, session_id bigint NOT NULL COMMENT 会话ID, sse_session_id varchar(64) DEFAULT NULL COMMENT SSE会话ID, user_content text COMMENT 用户提问内容, ai_content text COMMENT AI回复内容, model_used int DEFAULT NULL COMMENT 使用的模型ID, status tinyint DEFAULT NULL COMMENT 状态0-生成中 1-完成 2-中断, tokens int DEFAULT NULL COMMENT 消耗的Token数, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 用户提问时间, response_time datetime DEFAULT NULL COMMENT AI回复时间, PRIMARY KEY (id), KEY idx_session_id (session_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci这个设计有几个精妙之处会话ID索引通过session_id快速检索整个对话历史状态字段区分生成中和已完成的对话避免返回半成品Token统计为后续成本核算提供数据支持时间戳记录完整的交互时序2.2 性能优化技巧当对话记录达到百万级时查询性能会成为瓶颈。我们通过以下方案解决了这个问题分表策略按会话ID哈希分表将单表数据量控制在500万条以内读写分离查询走从库写入走主库缓存层对热点会话使用Redis缓存最近5轮对话字段优化将频繁查询的status字段改为TINYINT类型实测下来这套方案可以支撑2000 TPS的并发查询平均响应时间控制在15ms以内。3. SpringAI集成实战3.1 实现ChatMemory接口SpringAI的巧妙之处在于提供了ChatMemory接口我们只需要实现三个核心方法Component Slf4j public class MybatisChatMemory implements ChatMemory { private final AiMessagePairMapper mapper; Override public ListMessage get(String conversationId) { ListAiMessagePair pairs mapper.selectBySessionId(conversationId); ListMessage messages new ArrayList(); for (AiMessagePair pair : pairs) { if (pair.getStatus() FINISHED.getCode()) { if (StringUtils.hasText(pair.getUserContent())) { messages.add(new UserMessage(pair.getUserContent())); } if (StringUtils.hasText(pair.getAiContent())) { messages.add(new AssistantMessage(pair.getAiContent())); } } } return messages; } // add和clear方法根据业务需求实现 }注意这里的状态检查非常重要。有次线上事故就是因为漏了状态过滤导致用户看到了AI生成到一半的乱码回复。3.2 流式对话集成对于流式对话场景需要特别注意消息的完整性。我们的解决方案是public FluxChatResponse buildStreamWithMemo(AiConfig config, String question, String conversationId) { ChatModel model createChatModel(config); // 创建模型实例 return ChatClient.builder(model) .defaultAdvisors( MessageChatMemoryAdvisor.builder(mybatisChatMemory).build() ) .build() .prompt(question) .advisors(a - a.param(CONVERSATION_ID, conversationId)) .stream() .chatResponse(); }这里有个坑我踩过流式响应完成前不要立即存储消息。正确的做法是等onComplete事件触发后再持久化完整回复否则可能会存入残缺的内容。4. 生产环境注意事项4.1 并发控制当多个用户同时与同一个会话交互时会出现并发问题。我们的解决方案是使用MySQL的乐观锁机制对会话ID加分布式锁采用最终一致性策略Transactional public void addMessage(Long sessionId, Message message) { // 获取分布式锁 Lock lock redissonClient.getLock(chat: sessionId); try { lock.lock(5, TimeUnit.SECONDS); // 业务处理 } finally { lock.unlock(); } }4.2 数据清理策略对话记忆不是存得越久越好。我们制定了分级清理策略普通会话30天自动清理重要会话标记为星标永久保存敏感会话加密存储特殊访问控制可以通过Spring Scheduler实现定时清理Scheduled(cron 0 0 3 * * ?) // 每天凌晨3点执行 public void cleanupExpiredChats() { LocalDateTime cutoff LocalDateTime.now().minusDays(30); mapper.deleteByCreateTimeBefore(cutoff); }在实际项目中这套基于MySQL的持久化方案已经稳定运行了9个月日均处理对话量超过50万条。最大的收获是用户反馈这个AI终于记得我之前说过什么了。技术最终还是要服务于体验而好的记忆系统正是智能对话的灵魂所在。