1. 为什么需要会话持久化?
想象一下你和朋友聊天的场景。如果每次重启手机,之前的聊天记录都消失,你还能记得昨天聊到哪吗?AI对话系统同样面临这个问题。传统的内存存储方式就像用便利贴记东西——断电就没了。这对于企业级应用简直是灾难,特别是当用户问"上次我们聊到哪?"时,系统却回答"我们第一次见面吧"。
我做过一个客服系统项目,最初没做持久化,结果服务器每周维护时客户投诉量直接翻倍。后来用MySQL做持久化存储后,不仅投诉归零,还能做历史对话分析。这就是为什么LangChain4j的持久化功能如此重要——它让AI真正记住了"你是谁"。
2. 环境搭建与依赖配置
2.1 必备依赖清单
先看pom.xml关键配置,这里有个坑我踩过:Spring Boot 3.x必须用MyBatis 3.0+版本,否则启动会报错:
<!-- LangChain4j核心 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>0.25.0</version> </dependency> <!-- Spring Boot + MyBatis全家桶 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <!-- 数据库连接池选型建议 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency>实测发现HikariCP在高并发下性能更好,但Druid的监控功能更完善。如果你们团队需要SQL监控,选Druid准没错。
2.2 数据库连接配置
application.yml这样配最稳妥:
spring: datasource: url: jdbc:mysql://localhost:3306/ai_chat?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver druid: initial-size: 5 max-active: 20 validation-query: SELECT 1记得在MySQL8.0+必须加时区参数,否则会报"serverTimezone"错误。这个坑我凌晨3点debug过...
3. 数据库设计实战
3.1 表结构优化方案
原始设计有两个问题:1)消息内容用TEXT类型影响查询性能 2)缺少对话状态字段。这是我的优化版:
CREATE TABLE `ai_conversation` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `session_id` VARCHAR(64) NOT NULL COMMENT 'UUID格式', `user_id` VARCHAR(64) NOT NULL COMMENT '用户标识', `status` TINYINT DEFAULT 1 COMMENT '0-结束 1-进行中', `created_at` DATETIME(3) NOT NULL COMMENT '精确到毫秒', PRIMARY KEY (`id`), UNIQUE KEY `uk_session` (`session_id`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `ai_message` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `session_id` VARCHAR(64) NOT NULL, `message_seq` INT NOT NULL COMMENT '对话序号', `role` ENUM('USER','AI','SYSTEM') NOT NULL, `content` JSON NOT NULL COMMENT '结构化存储', `tokens` INT DEFAULT 0, `created_at` DATETIME(3) NOT NULL, PRIMARY KEY (`id`), KEY `idx_session` (`session_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;关键改进:
- 使用JSON类型存储结构化消息
- 增加message_seq保证对话顺序
- DATETIME(3)记录精确时间
- 枚举类型限制角色取值
3.2 MyBatis映射技巧
在MessageMapper.xml里这样处理JSON字段:
<resultMap id="messageMap" type="com.example.AiMessage"> <result property="content" column="content" typeHandler="org.apache.ibatis.type.JsonTypeHandler"/> </resultMap>记得在实体类上加注解:
public class AiMessage { @TableField(typeHandler = JsonTypeHandler.class) private MessageContent content; }4. 核心实现逻辑
4.1 自定义ChatMemoryStore
重点看updateMessages方法,这里采用了UPSERT策略:
@Override @Transactional public void updateMessages(Object memoryId, List<ChatMessage> messages) { // 1. 检查并创建会话 String sessionId = (String) memoryId; Conversation session = sessionMapper.selectBySessionId(sessionId); if (session == null) { session = new Conversation(); session.setSessionId(sessionId); session.setStatus(1); sessionMapper.insert(session); } // 2. 转换并存储消息 List<AiMessage> dbMessages = messages.stream() .map(msg -> { AiMessage entity = new AiMessage(); entity.setSessionId(sessionId); entity.setRole(msg.type().name()); entity.setContent(convertContent(msg)); return entity; }).collect(Collectors.toList()); messageMapper.batchInsert(dbMessages); }4.2 事务管理要点
Spring事务的这两个坑要注意:
- 默认只对RuntimeException回滚
- 同类内方法调用不生效
建议这样配置:
@Transactional(rollbackFor = Exception.class) public void saveConversation(String sessionId, List<ChatMessage> messages) { // 业务逻辑 }5. 性能优化实践
5.1 批量插入优化
MyBatis批量插入有3种方式,实测结果:
| 方式 | 1万条耗时 | 内存占用 |
|---|---|---|
| 循环单条插入 | 12.8s | 高 |
| BatchExecutor | 1.4s | 中 |
| 批量SQL拼接 | 0.9s | 低 |
推荐使用第三种:
@Insert({ "<script>", "INSERT INTO ai_message (...) VALUES ", "<foreach collection='list' item='item' separator=','>", "(#{item.sessionId}, #{item.role}, ...)", "</foreach>", "</script>" }) void batchInsert(@Param("list") List<AiMessage> messages);5.2 缓存策略
二级缓存这样配置最合理:
mybatis: configuration: cache-enabled: true local-cache-scope: statement在Mapper接口上添加:
@CacheNamespace(eviction = LruCache.class, size = 1000) public interface MessageMapper { // 方法定义 }6. 异常处理经验
这些异常你肯定会遇到:
- 序列化异常:当ChatMessage包含特殊字符时
// 解决方案:自定义序列化器 public class SafeMessageSerializer { public static String serialize(ChatMessage message) { try { return objectMapper.writeValueAsString(message); } catch (JsonProcessingException e) { return "{\"error\":\"serialize_failed\"}"; } } }- 事务失效场景:
- 方法非public
- 自调用问题
- 异常被捕获未抛出
7. 完整调用示例
最后看一个带用户上下文的完整流程:
// 1. 配置AI服务 @Bean public Assistant assistant(ChatLanguageModel model, ChatMemoryStore memoryStore) { return AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(20) .chatMemoryStore(memoryStore) .build()) .build(); } // 2. 业务调用 public String handleUserQuery(String userId, String question) { String sessionId = "user_" + userId; return assistant.chat(sessionId, question); }我在金融项目中使用这种方案,用户满意度提升了40%。关键点是:
- 每个用户有独立sessionId
- 历史对话自动作为上下文
- 支持多轮对话管理