欢迎光临
我们一直在努力

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

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

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

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

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

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

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


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

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

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

  • 上层:ChatMemory(记忆管理器)

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

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

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

    这一点非常关键:

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


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

    1. Maven 依赖

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

    <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 记忆功能,并指定建表脚本:

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

    这几行配置干了两件事:

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

  • 3. 数据库表结构:SPRING_AI_CHAT_MEMORY

    建表脚本大致如下:

    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 到底做了什么?

    看下配置类

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

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

  • JdbcChatMemoryRepository 来源

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

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

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

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

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


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

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

    @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 里提供的统一持久化接口。
    • Starter 会根据你配置的 JDBC 方案,自动提供一个 JdbcChatMemoryRepository 实例,并同时以接口类型 ChatMemoryRepository 注入到 Spring 容器中。
    • 因此在 Controller 里,只需要:

    private final ChatMemoryRepository chatMemoryRepository;

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

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

    当你调用:

    List<Message> messages = chatMemoryRepository.findByConversationId(chatId);

    底层发生的事情是:

  • 在 SPRING_AI_CHAT_MEMORY 表中,按 conversation_id = chatId 查询所有记录;
  • 按 timestamp 升序排序;
  • 把每一行的 content 字段反序列化成 Spring AI 的 Message 对象(其中包含角色、文本、工具调用等信息);
  • 最终返回一个 List<Message>。
  • 在项目中,又通过MessageVO做了一层转换,使前端拿到的是简单的:

    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:如何“一键清空某个会话的记忆”

    当你调用:

    chatMemoryRepository.deleteByConversationId(chatId);

    底层则是:

  • 在 SPRING_AI_CHAT_MEMORY 表中,按 conversation_id = chatId 执行 DELETE;
  • 这个会话的所有消息(包括用户问、AI 答)都会被彻底清除。
  • 配合业务表 spring_ai_chat_record 的删除:

    recordService.removeById(chatId);

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


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

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

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

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

    六、总结

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

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

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

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

    • 你在 ChatHistoryController 中注入的 ChatMemoryRepository 既是 ChatClient 的底层存储,又是你实现「会话列表/详情/删除」的入口。
  • 理解了这两层分工,再回头看你的 Controller 和配置 Bean,就会非常顺畅:

    • 配置里说明的是:记忆如何落库 + 如何控制窗口;

    • Controller 里用的是:底层仓库提供的查/删接口。


    七、 隐藏的危机:你的记忆真的“永久”吗?

    看到这里,你可能觉得一切都很完美:Spring AI 帮我们自动落库,自动管理上下文,简直是开发者的福音。

    但是,随着系统上线运行一段时间后,我发现了一个诡异的现象:

    当某个会话聊得比较久,消息数量超过了我们配置的 maxMessages(例如 50 条)时,数据库里最早的那些聊天记录竟然凭空消失了!

    明明我们用的是 MySQL 做持久化,为什么数据会被自动删除?这难道不是违背了“持久化”的初衷吗?如果我们想做全量数据的审计、分析,或者让用户翻看很久以前的记录,岂不是都没了?

    经过深入排查源码,我发现这是 MessageWindowChatMemory 一个设计上的“特性”(或者说是坑)。它不仅裁减了发给大模型的上下文,竟然连数据库里的底表也一并裁减了!

    如何解决这个问题?如何实现“数据库永久保存全量历史”但“大模型只传最近 N 条”的完美双赢?

    请看我的下一篇续集,带你深入源码,手把手教你重写 Spring AI 核心组件,彻底解决这个痛点:

    相关阅读: Spring AI 源码深度解读:为什么持久化到 MySQL 的聊天记录会被自动删除?(附完美解决方案)

    如果这篇文章对你有帮助,请 点赞、收藏、关注 一波!你的支持是我持续输出高质量技术干货的最大动力!!!

    赞(0)
    未经允许不得转载:171主机测评 » Spring AI + MySQL 实现会话记忆持久化:彻底搞懂 ChatMemoryRepository
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址