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(); } }

要点:

  1. MessageChatMemoryAdvisor 通过 ChatMemory.CONVERSATION_ID 参数区分会话,调用方必须传 chatId
  2. JdbcChatMemoryRepository 由 Spring AI 自动配置(@ConditionalOnMissingBean),无需手动声明 Bean;
  3. 记忆窗口 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。

五、验证方法

  1. 运行 mvn test -Dtest=LoveAppTest(两轮对话 + 结构化输出测试);
  2. 查询数据库:
sql
复制代码
SELECT conversation_id, type, LEFT(content, 30), timestamp FROM SPRING_AI_CHAT_MEMORY ORDER BY conversation_id, timestamp;
  1. 预期:每个测试会话出现 USER / ASSISTANT 成对记录,第二轮对话的模型回复能正确引用第一轮的用户信息。
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP