Spring AI 1.0.0 + MySQL 实现会话记忆持久化:彻底搞懂 ChatMemoryRepository

在做大模型应用时,“会话记忆”几乎是刚需:

  • 用户希望多轮对话是连续的;
  • 系统希望对话历史能存数据库,方便会话列表、历史回看、数据分析

Spring AI 1.0.0 已经把这一块封装得很优雅,其中的关键角色就是这两个:

  • ChatMemoryRepository:负责「消息如何落到 MySQL、如何查出来」
  • ChatMemory(MessageWindowChatMemory):负责「每次调用大模型时,用哪些历史消息」

本文以我的项目为例,重点讲清楚这件事:

我的 Controller 层只注入了一个 ChatMemoryRepository Bean,就能完成会话记忆的管理和 MySQL 持久化。这个 Bean 到底做了什么?为什么不用自己写 SQL?


一、整体架构:两层「记忆」分工

先把大的图讲清楚,避免一上来就被各种类名绕晕。

可以把 Spring AI 的会话记忆拆成两层来看:

  1. 上层:ChatMemory(记忆管理器)

    • 对 ChatClient 提供「添加消息」「获取历史」的统一接口。
    • 决定:
      • 每次调用大模型时,带多少条历史消息(记忆窗口);
      • 用什么策略裁剪历史(比如只保留最近 50 条)。
  2. 下层:ChatMemoryRepository(持久化存储引擎)

    • 负责和 MySQL 打交道。
    • 决定:
      • 一条对话消息如何序列化成表里的 content 字段;
      • conversation_id + 时间顺序查出整段对话;
      • 删除指定会话的所有消息。

在我的项目里:

  • ChatClient 使用的是上层的 ChatMemory(由 MessageWindowChatMemory + JdbcChatMemoryRepository 组成),负责「让模型有记忆」;
  • 而对外提供「历史记录接口」的 ChatHistoryController,直接注入的是下层的 ChatMemoryRepository,负责「查历史、删历史」。

这一点非常关键:

你在 Controller 里看到的 ChatMemoryRepository,就是那条连通 Spring AI 和 MySQL 的“地线”。


二、配置内容:项目里到底配了什么?

1. Maven 依赖

pom.xml 中,我使用 Spring AI 官方 JDBC 记忆模块 + MySQL 驱动:

xml
复制代码
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency>

Spring Boot + Starter 的好处就是:只要依赖在,配好数据源,Spring AI 会自动帮你创建 JdbcChatMemoryRepository 这种 Bean。


2. application 配置:启用 JDBC 记忆 + 自动建表

我开启了 Spring AI 的 JDBC 记忆功能,并指定建表脚本:

yaml
复制代码
spring: ai: chat: memory: repository: jdbc: initialize-schema: always # 自动建表 schema: classpath:sql/schema-mysql.sql # 表结构脚本 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/campusai?... username: root password: root

这几行配置干了两件事:

  1. 告诉 Spring AI:
    • 我要用 JDBC 作为 ChatMemoryRepository 的实现(其实就是项目里的 JdbcChatMemoryRepository)。
  2. 告诉 Spring Boot:
    • 启动时从 sql/schema-mysql.sql 里加载建表 SQL,如果表不存在就建表。

3. 数据库表结构:SPRING_AI_CHAT_MEMORY

建表脚本大致如下:

sql
复制代码
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY ( `id` BIGINT(19) NOT NULL AUTO_INCREMENT, `conversation_id` VARCHAR(36) NOT NULL, `content` TEXT NOT NULL, `type` VARCHAR(10) NOT NULL, `timestamp` TIMESTAMP NOT NULL, PRIMARY KEY (`id`), INDEX `SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX` (`conversation_id`, `timestamp`), CONSTRAINT TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL')) );

几个字段含义:

  • conversation_id:会话唯一标识(你在业务里用的 chatId)。
  • type:消息类型(用户 / 助手 / 系统 / 工具)。
  • content:消息内容及元数据(Spring AI 负责序列化/反序列化)。
  • timestamp:消息时间,用来按时间顺序还原整段对话。

三、核心配置:ChatMemory Bean 到底做了什么?

看下配置类

java
复制代码
@Bean public ChatMemory chatMemory(JdbcChatMemoryRepository chatMemoryRepository) { return MessageWindowChatMemory.builder() .chatMemoryRepository(chatMemoryRepository) .maxMessages(50) .build(); }

这段代码可以分三层理解:

  1. JdbcChatMemoryRepository 来源

    • 来自上面提到的 Starter + application 配置;
    • 它已经和 SPRING_AI_CHAT_MEMORY 表绑定好了:
      • save 时写表;
      • findByConversationId 时按 conversation_id + timestamp 查表。
  2. MessageWindowChatMemory 是一个“带记忆窗口的大脑”

    • 它实现了 ChatMemory 接口,但不直接操作数据库
    • 内部会调用 chatMemoryRepository 去读写 MySQL;
    • 它的核心职责是:
      • 每次调用大模型时,从仓库里取出最近 maxMessages 条消息;
      • 把新的问答消息再写回仓库。
  3. maxMessages(50):记忆窗口大小

    • 只把最近 50 条对话消息当成上下文传给大模型;
    • 这样可以避免无限累积上下文导致 Token 爆炸,同时又能兼顾一定的“记忆深度”。

一句话总结这段 Bean 配置:

ChatMemory = 记忆窗口策略(MessageWindowChatMemory) + 底层存储(JdbcChatMemoryRepository)

而这个 ChatMemory 会在创建 ChatClient 时被作为 Advisor 注入,负责「请求大模型时自动带上数据库里的对话历史」。


四、真正的主角:ChatMemoryRepository 是怎么“自动记忆 + 持久化”的?

Controller 里其实并没有用 ChatMemory,而是直接用了 ChatMemoryRepository

java
复制代码
@RestController @RequestMapping("/ai/history") @RequiredArgsConstructor public class ChatHistoryController { private final ChatMemoryRepository chatMemoryRepository; private final ISpringAiChatRecordService recordService; // 新建会话记录(只是保存会话元数据) @RequestMapping("/create") public void create(@RequestBody SpringAiChatRecord record) { recordService.save(record); } // 获取某个会话的聊天记录 @GetMapping("/info/{chatId}") public List<MessageVO> getChatHistory(@PathVariable("chatId") String chatId) { List<Message> messages = chatMemoryRepository.findByConversationId(chatId); return messages.stream().map(MessageVO::new).collect(Collectors.toList()); } // 删除某个会话 @GetMapping("/delete/{chatId}") public Result deleteChatHistory(@PathVariable("chatId") String chatId) { chatMemoryRepository.deleteByConversationId(chatId); recordService.removeById(chatId); return Result.ok(); } }

这里的关键点有三个:

1. ChatMemoryRepository 的来源

  • 它不是你自己写的接口,而是 Spring AI 在 1.0.0 里提供的统一持久化接口
  • Starter 会根据你配置的 JDBC 方案,自动提供一个 JdbcChatMemoryRepository 实例,并同时以接口类型 ChatMemoryRepository 注入到 Spring 容器中。
  • 因此在 Controller 里,只需要:
java
复制代码
private final ChatMemoryRepository chatMemoryRepository;

就能拿到完整的「会话消息持久化能力」。

2. findByConversationId:如何“查出一整段对话”

当你调用:

java
复制代码
List<Message> messages = chatMemoryRepository.findByConversationId(chatId);

底层发生的事情是:

  1. SPRING_AI_CHAT_MEMORY 表中,按 conversation_id = chatId 查询所有记录;
  2. timestamp 升序排序;
  3. 把每一行的 content 字段反序列化成 Spring AI 的 Message 对象(其中包含角色、文本、工具调用等信息);
  4. 最终返回一个 List<Message>

在项目中,又通过MessageVO做了一层转换,使前端拿到的是简单的:

java
复制代码
public class MessageVO { private String role; // user / assistant / system private String content; // 文本内容 public MessageVO(Message message) { this.role = switch (message.getMessageType()) { case USER -> "user"; case ASSISTANT -> "assistant"; case SYSTEM -> "system"; default -> ""; }; this.content = message.getText(); } }

这样,Controller 就可以直接把 VO 列表返回给前端用于渲染历史消息。

3. deleteByConversationId:如何“一键清空某个会话的记忆”

当你调用:

java
复制代码
chatMemoryRepository.deleteByConversationId(chatId);

底层则是:

  1. SPRING_AI_CHAT_MEMORY 表中,按 conversation_id = chatId 执行 DELETE
  2. 这个会话的所有消息(包括用户问、AI 答)都会被彻底清除。

配合业务表 spring_ai_chat_record 的删除:

java
复制代码
recordService.removeById(chatId);

就实现了「删除会话 = 删除会话元数据 + 删除会话所有记忆」。


五、从一次真实请求看完整流程

以“智能客服”接口为例,一次请求的流程大致如下(简化版):

  1. 前端调用:/ai/service?prompt=你好&chatId=abc123
  2. Controller 调用 ChatClient
    • 通过 .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))chatId 传给 ChatMemory
  3. ChatMemory(MessageWindowChatMemory) 调用 ChatMemoryRepository
    • findByConversationId("abc123"):从 MySQL 查出历史消息;
    • 截取最近 maxMessages(50) 条,作为上下文发给大模型;
  4. 大模型生成回复后,ChatMemory 再通过 ChatMemoryRepository
    • 把本轮 USER 消息和 ASSISTANT 消息写入 SPRING_AI_CHAT_MEMORY
  5. 当你访问历史接口 /ai/history/info/abc123 时:
    • ChatHistoryController 直接调用同一个 ChatMemoryRepository.findByConversationId("abc123"),把历史消息查出来给前端展示。

可以看到:

  • 写入/读取记忆:是由 ChatMemoryChatMemoryRepository 配合完成的;
  • 对外暴露历史接口:只需要直接用 ChatMemoryRepository 的查询/删除能力即可。

六、总结

结合这个项目,我们可以把 Spring AI + MySQL 会话记忆总结成三句话:

  1. ChatMemoryRepository = 会话消息的 MySQL 持久化引擎

    • 提供 findByConversationIddeleteByConversationId 等方法;
    • 自动映射到 SPRING_AI_CHAT_MEMORY 表,无需手写 SQL。
  2. ChatMemory(MessageWindowChatMemory) = 带窗口策略的“记忆大脑”

    • 上接 ChatClient,下接 ChatMemoryRepository;
    • 决定「每次请求带多少历史」以及「如何写回历史」。
  3. Controller 可以直接「拿仓库查历史」

    • 你在 ChatHistoryController 中注入的 ChatMemoryRepository
      既是 ChatClient 的底层存储,又是你实现「会话列表/详情/删除」的入口。

理解了这两层分工,再回头看你的 Controller 和配置 Bean,就会非常顺畅:

  • 配置里说明的是:记忆如何落库 + 如何控制窗口
  • Controller 里用的是:底层仓库提供的查/删接口
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
久睡成瘾
下载 APP