Spring AI 实战:用 JdbcChatMemory + MySQL 给 AI 接上「长期记忆」

Spring AI 实战:用 JdbcChatMemory + MySQL 给 AI 接上「长期记忆」

环境:Spring Boot 3.4.5 · Spring AI 1.1.6 · MySQL 8.x · Java 17


一、为什么需要对话记忆?

默认情况下,ChatClient 每次调用都是无状态的。你问 AI「我叫什么名字?」,它永远回答「我不知道」------因为上一句「我叫张三」已经消失了。

要实现真正的多轮对话,就需要把历史消息随每次请求一起发送给模型。Spring AI 提供了 ChatMemory 体系来解决这个问题,而 JdbcChatMemory 则把记忆持久化到关系型数据库,让服务重启后历史不丢失。


二、核心概念

在动手之前,先理清三个核心类的职责:

scss 复制代码
┌─────────────────────────────────────────────────────────┐
│                       ChatClient                        │
│                                                         │
│  prompt().user("...").advisors(...).call().content()    │
└──────────────────────┬──────────────────────────────────┘
                       │ 挂载
                       ▼
┌─────────────────────────────────────────────────────────┐
│              MessageChatMemoryAdvisor                   │
│                                                         │
│  • 请求前:从 ChatMemory 读取历史消息,拼入本次请求        │
│  • 响应后:将本轮 USER + ASSISTANT 消息写回 ChatMemory    │
└──────────────────────┬──────────────────────────────────┘
                       │ 依赖
                       ▼
┌─────────────────────────────────────────────────────────┐
│                     ChatMemory                          │
│              (Spring AI 自动配置的 Bean)                  │
└──────────────────────┬──────────────────────────────────┘
                       │ 使用
                       ▼
┌─────────────────────────────────────────────────────────┐
│             JdbcChatMemoryRepository                    │
│                                                         │
│  • 底层通过 JdbcTemplate 读写 MySQL                      │
│  • 表:SPRING_AI_CHAT_MEMORY                            │
└─────────────────────────────────────────────────────────┘
职责
MessageChatMemoryAdvisor AOP 式拦截,自动注入/保存历史
ChatMemory 记忆抽象接口,由 Spring AI 自动配置
JdbcChatMemoryRepository 实现 ChatMemoryRepository,持久化到 DB

三、完整实现

3.1 Maven 依赖

xml 复制代码
<properties>
    <spring-ai.version>1.1.6</spring-ai.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <!-- Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring AI OpenAI -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>

    <!-- Spring AI ChatMemory 自动配置(提供 ChatMemory Bean) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-autoconfigure-model-chat-memory</artifactId>
    </dependency>

    <!-- Spring AI JDBC 记忆存储实现 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-model-chat-memory-repository-jdbc</artifactId>
    </dependency>

    <!-- Spring Boot JDBC(提供 JdbcTemplate) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

注意spring-ai-model-chat-memory-repository-jdbc 没有内置 Spring Boot 自动配置,需要手动注册 Bean(见 3.4 节)。


3.2 application.yaml

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/your_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&createDatabaseIfNotExist=true
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver
  sql:
    init:
      mode: always   # 启动时自动执行 schema.sql 建表,生产改为 never

  ai:
    openai:
      api-key: your-api-key
      base-url: https://api.openai.com/
      chat:
        options:
          model: gpt-4o
          temperature: 0.7

3.3 建表 SQL(schema.sql)

放在 src/main/resources/schema.sql,Spring Boot 启动时自动执行:

sql 复制代码
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
    `conversation_id` VARCHAR(36)                              NOT NULL,
    `content`         TEXT                                     NOT NULL,
    `type`            ENUM('USER','ASSISTANT','SYSTEM','TOOL') NOT NULL,
    `timestamp`       TIMESTAMP                                NOT NULL,

    INDEX `SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX`
        (`conversation_id`, `timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

字段说明:

字段 类型 说明
conversation_id VARCHAR(36) 会话唯一标识,推荐用 UUID(恰好 36 位)
content TEXT 消息内容
type ENUM 消息角色:USER / ASSISTANT / SYSTEM / TOOL
timestamp TIMESTAMP 消息时间戳,按会话+时间联合索引

3.4 注册 JdbcChatMemoryRepository Bean

spring-ai-model-chat-memory-repository-jdbc 没有内置自动配置,需要手动声明。ChatMemoryAutoConfiguration 检测到 ChatMemoryRepository Bean 后会自动组装 ChatMemory

java 复制代码
// src/main/java/.../config/ChatMemoryConfig.java

@Configuration
public class ChatMemoryConfig {

    @Bean
    public JdbcChatMemoryRepository chatMemoryRepository(JdbcTemplate jdbcTemplate) {
        return JdbcChatMemoryRepository.builder()
                .jdbcTemplate(jdbcTemplate)
                .build();
    }
}

3.5 改造 Controller

java 复制代码
// src/main/java/.../controller/MyController.java

@RestController
class MyController {

    private final ChatClient chatClient;

    public MyController(ChatClient.Builder chatClientBuilder, ChatMemory chatMemory) {
        this.chatClient = chatClientBuilder
                // 挂载记忆 Advisor,每次请求自动读写历史
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }

    @GetMapping("/ai")
    String generation(
            @RequestParam String userInput,
            @RequestParam(defaultValue = "default-session") String conversationId) {
        return this.chatClient.prompt()
                .user(userInput)
                // 指定本次请求属于哪个会话
                .advisors(a -> a.param("chat_memory_conversation_id", conversationId))
                .call()
                .content();
    }
}

关键点解读:

  1. ChatMemory 由 Spring AI 自动注入(底层使用我们注册的 JdbcChatMemoryRepository
  2. defaultAdvisors(...) 在 ChatClient 级别全局挂载,所有请求都会经过记忆读写
  3. "chat_memory_conversation_id" 是 Spring AI 内部约定的 Key(1.1.x 未作为公共常量暴露)
  4. 不同的 conversationId 对应完全独立的历史记录

四、完整调用链路

scss 复制代码
GET /ai?userInput=我叫张三&conversationId=user-001
        │
        ▼
  MyController.generation()
        │
        ▼
  ChatClient.prompt().user("我叫张三").advisors(conversationId="user-001")
        │
        ▼  ← MessageChatMemoryAdvisor (before)
        │   从 DB 读取 user-001 的历史消息,拼入本次请求
        ▼
  调用 AI 模型 API
        │
        ▼  ← MessageChatMemoryAdvisor (after)
        │   将本轮 USER("我叫张三") + ASSISTANT("好的,我记住了") 写入 DB
        ▼
  返回 AI 响应内容

---

GET /ai?userInput=我叫什么名字&conversationId=user-001
        │
        ▼
  MessageChatMemoryAdvisor (before)
  │  从 DB 读取历史:["我叫张三", "好的,我记住了"]
  │  拼成:[历史消息..., "我叫什么名字"]
  ▼
  AI 模型收到完整上下文 → 回复「你叫张三」

五、集成测试

测试直接使用真实的 MySQL 和 AI 接口,不 Mock,每个用例结束后自动清理测试数据。

java 复制代码
@SpringBootTest
class MyControllerTest {

    @Autowired
    private MyController controller;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    private String conversationId;

    @AfterEach
    void cleanUp() {
        if (conversationId != null) {
            jdbcTemplate.update(
                "DELETE FROM SPRING_AI_CHAT_MEMORY WHERE CONVERSATION_ID = ?",
                conversationId);
        }
    }

    @Test
    @DisplayName("正常返回 AI 非空响应")
    void generation_returnsNonEmptyResponse() {
        conversationId = UUID.randomUUID().toString();

        String response = controller.generation("你好", conversationId);

        assertThat(response).isNotBlank();
    }

    @Test
    @DisplayName("同一会话内 AI 能记住上下文")
    void generation_remembersContextWithinSameConversation() {
        conversationId = UUID.randomUUID().toString();

        controller.generation("我的名字是张三,请记住我的名字", conversationId);
        String response = controller.generation("我叫什么名字?", conversationId);

        assertThat(response).containsIgnoringCase("张三");
    }

    @Test
    @DisplayName("不同会话 ID 的记忆相互隔离")
    void generation_isolatesMemoryBetweenConversations() {
        String sessionA = UUID.randomUUID().toString();
        String sessionB = UUID.randomUUID().toString();

        controller.generation("我的名字是李四", sessionA);
        String response = controller.generation("我叫什么名字?", sessionB);

        // sessionB 从未存过名字,AI 不应知道李四
        assertThat(response).doesNotContain("李四");

        jdbcTemplate.update("DELETE FROM SPRING_AI_CHAT_MEMORY WHERE CONVERSATION_ID = ?", sessionA);
        jdbcTemplate.update("DELETE FROM SPRING_AI_CHAT_MEMORY WHERE CONVERSATION_ID = ?", sessionB);
    }

    @Test
    @DisplayName("对话记录写入数据库")
    void generation_persistsMessageToDatabase() {
        conversationId = UUID.randomUUID().toString();

        controller.generation("记录这条消息", conversationId);

        int count = jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM SPRING_AI_CHAT_MEMORY WHERE CONVERSATION_ID = ?",
                Integer.class, conversationId);

        // USER 消息 + ASSISTANT 消息,至少 2 条
        assertThat(count).isGreaterThanOrEqualTo(2);
    }
}

运行结果:

yaml 复制代码
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

六、踩坑记录

实际接入过程中遇到了几个问题,记录下来供参考。

坑 1:artifact 名称写错

xml 复制代码
<!-- ❌ 错误:BOM 中不存在这个 artifact -->
<artifactId>spring-ai-autoconfigure-model-memory-jdbc</artifactId>

<!-- ✅ 正确:两个独立 artifact -->
<artifactId>spring-ai-autoconfigure-model-chat-memory</artifactId>
<artifactId>spring-ai-model-chat-memory-repository-jdbc</artifactId>

坑 2:常量 CONVERSATION_ID_KEY 不存在

java 复制代码
// ❌ 1.1.6 中该常量未对外暴露
.advisors(a -> a.param(MessageChatMemoryAdvisor.CONVERSATION_ID_KEY, conversationId))

// ✅ 直接用字符串字面量
.advisors(a -> a.param("chat_memory_conversation_id", conversationId))

坑 3:JDBC 记忆模块没有自动配置

spring-ai-model-chat-memory-repository-jdbc 的 jar 里没有 AutoConfiguration.imports,不会自动注册 JdbcChatMemoryRepository Bean。必须手动用 @Configuration + @Bean 声明,ChatMemoryAutoConfiguration 才能感知并组装 ChatMemory

坑 4:conversation_id 字段长度限制

SPRING_AI_CHAT_MEMORY 表的 conversation_idVARCHAR(36),刚好容纳一个标准 UUID。使用时注意:

java 复制代码
// ❌ "test-" + UUID = 41 字符,超长报错
conversationId = "test-" + UUID.randomUUID();

// ✅ 纯 UUID = 36 字符
conversationId = UUID.randomUUID().toString();

七、快速验证

启动应用后用 curl 测试多轮记忆效果:

bash 复制代码
# 第一轮:告诉 AI 你的名字
curl "http://localhost:8080/ai?userInput=我叫张三&conversationId=user-001"

# 第二轮:验证 AI 是否记住
curl "http://localhost:8080/ai?userInput=我叫什么名字?&conversationId=user-001"
# 预期输出包含「张三」

# 换一个会话,验证隔离
curl "http://localhost:8080/ai?userInput=我叫什么名字?&conversationId=user-002"
# 预期输出:AI 不知道你的名字

八、总结

步骤 操作
1 pom.xml 添加 4 个依赖(autoconfigure + jdbc-repo + spring-jdbc + mysql-driver)
2 application.yaml 配置数据源,开启 spring.sql.init.mode: always
3 添加 schema.sql 建表 DDL
4 新增 ChatMemoryConfig 手动注册 JdbcChatMemoryRepository Bean
5 Controller 注入 ChatMemory,挂载 MessageChatMemoryAdvisor,新增 conversationId 参数

整体改动只涉及 4 个文件 ,对业务代码侵入极低,AI 记忆的读写完全由 MessageChatMemoryAdvisor 在 AOP 层面透明处理。

相关推荐
我叫黑大帅2 小时前
最简单的生产-消费者,你都会遇到哪些问题?
后端·面试·go
swipe3 小时前
Agentic RAG:用 LangGraph 构建会路由、会纠错、会收敛的闭环 RAG
后端·langchain·llm
折哥的程序人生 · 物流技术专研3 小时前
《Java 100 天进阶之路》第23篇:缓冲区数据结构 ByteBuffer
java·开发语言·数据结构·后端·面试·求职招聘
还是鼠鼠3 小时前
AI掘金头条新闻系统 (Toutiao News)-获取新闻分类
后端·python·mysql·fastapi·web
超梦dasgg4 小时前
Spring Security 原理 + 生产环境认证授权实战
java·后端·spring
东方小月4 小时前
Claude Code Skill 完全指南:一个 markdown 文件,就是一个专家分身
前端·后端
DianSan_ERP4 小时前
抖店订单接口中消费者信息加密解密机制与安全履约全解析
前端·网络·数据库·后端·安全·团队开发·运维开发
紫洋葱_popo5 小时前
一文吃透 LangChain 流式输出:同步、异步、LCEL 链式穿透全解析
后端
松就是我902985 小时前
LLM 代理服务实现原理文档
后端