Spring AI 官方 starter 竟有坑!ChatMemory 持久化踩坑实录

这是 Spring AI 系列第 3 篇。还没看过前两篇的:


一、内存版的致命缺陷

上一篇文章我实现了 Spring AI 的多轮对话记忆,用的是内存版 MessageWindowChatMemory。本地测试跑得飞起,AI 记住我说过的每句话。

但当我把应用重启后——

你叫什么名字?
抱歉,您还没有告诉我您的名字。

我:???我上一秒才说的啊。

再一查代码,瞬间明白了:内存版的 ChatMemory 存在 JVM 内存里,应用一重启,内存清空,所有对话记录全丢了。

问题有三个:

  1. 重启丢失:应用重启 / 容器重启,内存数据清空
  2. 多实例不同步:两个实例跑在两台机器上,实例 A 的对话记录实例 B 看不到
  3. 数据无审计:无法查询"某用户上周聊了什么",内存里的数据转瞬即逝

对于生产环境,这 3 个问题都是不可接受的


二、官方的 JDBC 持久化方案

Spring AI 官方提供了 spring-ai-starter-model-chat-memory-repository-jdbc,两个关键组件:

  1. JdbcChatMemoryRepository:负责读写数据库
  2. 自动建表脚本:启动时自动创建 SPRING_AI_CHAT_MEMORY

理论上,只需要:

Step 1:加依赖

<!-- ChatMemory JDBC 持久化 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

Step 2:改 ChatMemoryConfig

@Configuration
public class ChatMemoryConfig {

    @Bean
    public ChatMemory chatMemory(JdbcChatMemoryRepository repository) {
        return MessageWindowChatMemory.builder()
                .chatMemoryRepository(repository)
                .maxMessages(20)
                .build();
    }
}

Step 3:配 datasource

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/hello_ai
    username: ${MYSQL_USER:root}
    password: ${MYSQL_PASSWORD:root}
    driver-class-name: com.mysql.cj.jdbc.Driver

然后启动应用,应该自动建表 + 一切正常


三、踩坑:官方 jar 竟然没有 MySQL 建表脚本 🔥

我按上述配置启动,结果报错:

No schema scripts found at location 'classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-mysql.sql'

第一反应:yml 配置错了?再检查一遍 datasource.url,正确。MySQL 连接正常,还能打印出 HikariPool Start completed。

第二反应:依赖版本错了?再看一遍 pom.xml,spring-ai.version1.0.0,正确。

最后决定:不猜了,直接去 jar 里看真相。用命令解压 spring-ai-model-chat-memory-repository-jdbc-1.0.0.jar,列出里面所有的 .sql 文件:

jar tf spring-ai-model-chat-memory-repository-jdbc-1.0.0.jar | grep sql

结果震惊了:

org/springframework/ai/chat/memory/repository/jdbc/schema-postgresql.sql   ✓
org/springframework/ai/chat/memory/repository/jdbc/schema-sqlserver.sql    ✓
org/springframework/ai/chat/memory/repository/jdbc/schema-hsqldb.sql       ✓
org/springframework/ai/chat/memory/repository/jdbc/schema-mariadb.sql      ✓

没有 schema-mysql.sql!!!

jar 里有 MysqlChatMemoryRepositoryDialect.class(说明支持 MySQL),但建表脚本漏掉了。这是 Spring AI 1.0.0 的一个官方缺陷——估计是打包时漏了。


四、解决方案:自己提供 schema-mysql.sql

既然官方没提供,就自己写一个。参考 jar 里其他数据库的脚本,仿照 schema-mariadb.sql(MySQL 语法最接近),写了 schema-mysql.sql:

CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
    conversation_id VARCHAR(36) NOT NULL,
    content         TEXT NOT NULL,
    type            VARCHAR(10) NOT NULL,
    `timestamp`     TIMESTAMP NOT NULL,
    CONSTRAINT SPRING_AI_CHAT_MEMORY_TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL'))
);

表结构 4 个字段:

  • conversation_id:会话 ID
  • content:消息内容
  • type:消息类型(USER / ASSISTANT / SYSTEM / TOOL)
  • timestamp:时间戳

把脚本放在 src/main/resources/schema-mysql.sql,然后改 application.yml:

spring:
  ai:
    chat:
      memory:
        repository:
          jdbc:
            # 关掉 starter 自带的建表初始化器(它找不到 schema-mysql.sql)
            initialize-schema: never
  # 用 Spring Boot 的 sql.init 指定我们的脚本
  sql:
    init:
      mode: always
      schema-locations: classpath:schema-mysql.sql

配置逻辑:

  1. initialize-schema: never:关掉 starter 的自动建表
  2. sql.init.mode: always:让 Spring Boot 在启动时执行我们的脚本
  3. schema-locations:指定脚本位置

重启应用,控制台打印:

CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (...)

启动成功!


五、效果验证:重启后记忆还在

启动后,跑这套测试(关键在第 2 步手动重启):

# 第 1 步:发消息
http://localhost:8080/ai/chat?sessionId=persist01&message=我叫张三,今年28岁

# 第 2 步:【手动重启应用】(Ctrl+F5)

# 第 3 步:重启后,用同一个 sessionId 再问
http://localhost:8080/ai/chat?sessionId=persist01&message=我叫什么名字?

结果:AI 回答**“您叫张三”**!

对比内存版(重启后失忆),这次重启后记忆还在,说明数据确实存进了 MySQL。

用 Navicat 查数据库,也能看到记录:

SELECT * FROM hello_ai.SPRING_AI_CHAT_MEMORY;

这就是持久化的铁证:数据落地,重启不丢。


六、生产环境最佳实践

6.1 索引优化

脚本里我没加索引,因为应用层已经用 maxMessages=20 限制了单会话数据量。但如果你数据量起来了,可以手动加索引:

CREATE INDEX SPRING_AI_CHAT_MEMORY_CID_TS_IDX
    ON SPRING_AI_CHAT_MEMORY (conversation_id, `timestamp`);

6.2 数据清理

对话表会无限增长,建议定期清理:

@Scheduled(cron = "0 0 2 * * ?")
public void cleanOldMessages() {
    jdbcTemplate.update(
        "DELETE FROM SPRING_AI_CHAT_MEMORY WHERE `timestamp` < DATE_SUB(NOW(), INTERVAL 30 DAY)"
    );
}

6.3 敏感信息脱敏

如果对话内容涉及用户隐私,存数据库前脱敏(需要自定义 Advisor):

.advisors(a -> a
    .param(ChatMemory.CONVERSATION_ID, sessionId)
    .beforeReply((context, request) -> {
        String content = request.getUserText().replaceAll("\\d{15,}", "***");
        // 修改 request 的 user text
    })
)

七、我学到了什么

  1. 内存版只能开发用,生产必须持久化——重启丢失是致命缺陷
  2. 官方库也有坑——Spring AI 1.0 的 MySQL schema 缺失就是例子
  3. 排查问题时去 jar 里看真相——不靠猜,直接解压 jar 看有什么文件
  4. Spring Boot 的 sql.init 比自动配置更可控——当 starter 不满足需求时,用底层的 sql.init 绕过

最大的体会:所谓"官方 starter",本质上是一堆约定好的配置 + 脚本。当它不满足你的需求时,跳出来自己配,往往更清晰


八、下一篇预告

这篇用了 MySQL 持久化,解决了"重启丢失"问题。但还有一个性能问题没解决:

当对话记录达到几千条、上万条时,SELECT * FROM chat_memory WHERE conversation_id = ? 会越来越慢。

下一篇我会写:

《ChatMemory 性能优化:从单表查询到 Redis 缓存》

包括:

  • 为什么单表查询会慢
  • Redis 持久化配置
  • MySQL + Redis 混合策略
  • 性能测试:从 1W 条到 100W 条的查询耗时对比

这是把 demo 推向高并发的必经之路。


写在最后

我是一名8 年 Java 后端,正在转型 AI 应用开发。Spring AI 系列会持续更新,从 hello world 到 RAG 到 Agent,一路踩坑一路写。

如果你也在转型 AI,关注我,一起走。有问题评论区聊,我会逐条回复。

如果这篇文章帮到了你,点个赞就是对我最大的鼓励 ❤️

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐