MySql-JDBC 聊天记忆存储实现指南-最新Spring-Ai-Alibaba 1.1.2
JDBC 聊天记忆存储实现指南
适用项目:恋爱大师ai-agent 技术栈:Spring Boot 3.5.3 / JDK 21 / Spring AI 1.1.2 / MySQL 8.0.33(mysql-connector-java)/ DashScope(spring-ai-alibaba 1.1.2.0) 核心依赖:
spring-ai-starter-model-chat-memory-repository-jdbc:1.1.2存储表:SPRING_AI_CHAT_MEMORY
一、实现原理
1.1 组件协作关系
▼text复制代码用户对话 │ ▼ ChatClient(defaultAdvisors 装配) │ ▼ MessageChatMemoryAdvisor ──► ChatMemory(MessageWindowChatMemory,窗口上限 20 条) │ ▼ ChatMemoryRepository │ ├── JdbcChatMemoryRepository(本项目,MySQL 持久化) ├── InMemoryChatMemoryRepository(内存兜底,仅开发用) └── FileBasedChatMemory(本项目自定义,Kryo 文件持久化)
1.2 一次对话的完整读写链路
▼text复制代码请求前(before): ① chatMemory.get(chatId) → SELECT content, type FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ? ② 将历史消息拼入 Prompt 发给大模型 请求后(after): ③ chatMemory.add(chatId, 新消息) └─ 内部先 findByConversationId() 查出历史 └─ 合并窗口裁剪后 saveAll()(事务内:DELETE 该会话全部 → 批量 INSERT 全部)
1.3 存储语义(重要)
- 快照语义:
saveAll每次"先全删、再全插",表内永远是该会话的完整最新消息列表,不存在追加残留; - 事务保障:删除 + 批量插入包在
TransactionTemplate中,中途失败自动回滚; - 消息顺序:
timestamp以秒为单位,通过AtomicLong自增保证同批消息严格有序(该列仅作排序号,不追求精确时间); - TOOL 消息:读取时还原为空内容
ToolResponseMessage(JDBC 方言不保存工具调用细节)。
二、项目环境配置
2.1 引入依赖(pom.xml)
已添加(版本 1.1.2,与 spring-ai-alibaba 1.1.2.0 对齐):
▼xml复制代码<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId> <version>1.1.2</version> <scope>compile</scope> </dependency>
2.2 数据源配置(application.yaml)
项目当前配置(本机 MySQL,库名 xm-kimoaiagent):
▼yaml复制代码spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/xm-kimoaiagent?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false username: root password: abc123
2.3 建表
Spring AI 的 spring.ai.chat.memory.repository.jdbc.initialize-schema 默认值为 embedded,即只对嵌入式数据库(H2 等)自动建表,MySQL 不会自动建表。两种方式任选:
方式一(推荐,手动建表):
▼sql复制代码CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY ( conversation_id VARCHAR(36) NOT NULL, content TEXT NOT NULL, type VARCHAR(20) NOT NULL, timestamp TIMESTAMP NOT NULL, KEY idx_conversation_timestamp (conversation_id, timestamp) );
⚠️ 表名大小写:Spring AI MySQL 方言的 SQL 硬编码为全大写
SPRING_AI_CHAT_MEMORY。
- Windows 本机 MySQL 默认
lower_case_table_names=1,大小写不敏感,建小写表也能命中;- Linux 服务器默认大小写敏感,必须建大写表名,否则报"Table doesn't exist"。
方式二(自动建表),在 application.yaml 追加:
▼yaml复制代码spring: ai: chat: memory: repository: jdbc: initialize-schema: always
三、代码集成(LoveApp 当前实现)
▼java复制代码@Component @Slf4j public class LoveApp { private final ChatClient chatClient; /** * 构造函数注入 JdbcChatMemoryRepository(关键:必须构造注入,不能字段注入) */ public LoveApp(ChatModel dashscopeChatModel, JdbcChatMemoryRepository jdbcChatMemoryRepository) { // 基于 JDBC 的窗口记忆:最多保存 20 条消息 MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder() .chatMemoryRepository(jdbcChatMemoryRepository) .maxMessages(20) .build(); // 注入带记忆的对话顾问 chatClient = ChatClient.builder(dashscopeChatModel) .defaultSystem(SYSTEM_PROMPT + "每次对话后都要生成恋爱结果,标题为{用户名}的恋爱报告,内容为建议列表") .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build(), new MyLoggerAdvisor(), new ReReadingAdvisor()) .build(); } public String doChat(String userMessage, String chatId) { ChatResponse chatResponse = chatClient.prompt() .user(userMessage) .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId)) .call() .chatResponse(); return chatResponse.getResult().getOutput().getText(); } }
要点:
MessageChatMemoryAdvisor通过ChatMemory.CONVERSATION_ID参数区分会话,调用方必须传chatId;JdbcChatMemoryRepository由 Spring AI 自动配置(@ConditionalOnMissingBean),无需手动声明 Bean;- 记忆窗口
maxMessages=20:超过后优先保留 System 消息、裁剪最旧的非 System 消息。
四、常见坑与排查
4.1 【高频坑】@Resource 字段注入导致记忆"失效但无报错"
▼java复制代码// ❌ 错误:字段注入发生在构造函数之后 @Resource JdbcChatMemoryRepository jdbcChatMemoryRepository; public LoveApp(ChatModel model) { // 这里 jdbcChatMemoryRepository 还是 null! MessageWindowChatMemory.builder().chatMemoryRepository(jdbcChatMemoryRepository)... }
Spring 创建 Bean 的生命周期:构造函数 → 字段注入 → @PostConstruct。
而 MessageWindowChatMemory.Builder.build() 对 null 有静默容错:
▼java复制代码if (this.chatMemoryRepository == null) { this.chatMemoryRepository = new InMemoryChatMemoryRepository(); // 静默回退纯内存! }
症状:测试中多轮对话记忆正常(模型记得上一轮说的话),但数据库表始终为空、且不报任何错误。
解决:改为构造函数注入(见上文 LoveApp 写法)。
4.2 表建了但写不进去
- 确认
initialize-schema已按 2.3 配置(外部 MySQL 默认不建表); - 确认表名大小写与 MySQL 的
lower_case_table_names匹配(Linux 必须大写); - 确认
timestamp列类型为TIMESTAMP且非空(方言 SQL 直接写该列)。
4.3 每次对话数据库有 4 条 SQL,是否压力大?
见同目录文档《Redis缓存必要性分析.md》:单次对话 DB 耗时约 5~10ms,占对话总耗时 < 0.5%,现阶段无需 Redis。
五、验证方法
- 运行
mvn test -Dtest=LoveAppTest(两轮对话 + 结构化输出测试); - 查询数据库:
▼sql复制代码SELECT conversation_id, type, LEFT(content, 30), timestamp FROM SPRING_AI_CHAT_MEMORY ORDER BY conversation_id, timestamp;
- 预期:每个测试会话出现 USER / ASSISTANT 成对记录,第二轮对话的模型回复能正确引用第一轮的用户信息。
