Spring AI 1.x 系列【28】基于内存和 MySQL 的多轮对话实现案例

文章目录

  • [1. 前言](#1. 前言)
  • [2. 内存记忆](#2. 内存记忆)
    • [2.1 构建 ChatMemory](#2.1 构建 ChatMemory)
    • [2.2 构建记忆增强器](#2.2 构建记忆增强器)
    • [2.3 构建对话客户端](#2.3 构建对话客户端)
    • [2.4 会话 ID](#2.4 会话 ID)
      • [2.4.1 重要性](#2.4.1 重要性)
      • [2.4.2 多会话列表实现](#2.4.2 多会话列表实现)
    • [2.5 多轮会话测试](#2.5 多轮会话测试)
  • [3. 数据库记忆](#3. 数据库记忆)
    • [3.1 引入依赖](#3.1 引入依赖)
    • [3.2 属性配置](#3.2 属性配置)
    • [3.3 数据库结构初始化](#3.3 数据库结构初始化)
    • [3.4 方言支持](#3.4 方言支持)
    • [3.5 创建 JdbcChatMemoryRepository](#3.5 创建 JdbcChatMemoryRepository)
    • [3.6 构建对话客户端](#3.6 构建对话客户端)
    • [3.7 多轮会话测试](#3.7 多轮会话测试)

1. 前言

大模型的 "记忆" 本质是把历史信息塞进本次请求的上下文,而非模型真的记住了内容。

实现多轮对话的核心是维护一个 messages 数组。每一轮对话都需要将用户的最新提问和模型的回复追加到此数组中,并将其作为下一次请求的输入。

示例,首先第一轮对话添加用户消息:

java 复制代码
        UserMessage userMessage1 = UserMessage.builder().text("我叫张三,今年30岁,养了一只叫"可乐"的柯基犬,家住上海。").build();
        String content1 = zhiPuAiChatClient.prompt()
                .messages(userMessage1)
                .call()
                .content();

第二轮对话时,消息数组添加第一次对话用户消息、大模型回复内容、用户的最新提问:

java 复制代码
        AssistantMessage assistantMessage = AssistantMessage.builder().content(content1).build();
        UserMessage userMessage2 = UserMessage.builder().text("你知道我叫什么名字吗?").build();
        String content2 = zhiPuAiChatClient.prompt()
                .messages(userMessage1, assistantMessage, userMessage2)
                .call()
                .content();
        System.out.println(content1);
        System.out.println(content2);

输出示例:

2. 内存记忆

基于 ConcurrentHashMap 存储消息,仅在应用内存中生效,应用重启后数据丢失,适用于无依赖、轻量、高性能,适合开发测试、临时演示场景。

2.1 构建 ChatMemory

Spring AI 只提供了一个 MessageWindowChatMemory (滑动窗口机制)实现,核心特点:

  • 维持一个固定大小的消息窗口,当消息数量超过上限时,自动淘汰旧消息。
  • 特殊处理 SystemMessage(系统消息):
    • 新增系统消息时,自动删除所有旧的系统消息(保证只有最新的系统指令生效)。
    • 消息淘汰时,优先保留系统消息,只淘汰普通对话消息。
  • 默认窗口大小:20 条消息,可自定义。

通过 builder() 方法构造:

java 复制代码
        MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
                .maxMessages(10)
                .build();

2.2 构建记忆增强器

记忆功能会由 MessageChatMemoryAdvisor 自动管理,两种聊天记忆 Advisor 核心对比:

特性 MessageChatMemoryAdvisor PromptChatMemoryAdvisor
记忆方式 直接拼接消息列表 嵌入系统提示词文本
系统消息 强制置顶 保留原位置, 内部嵌入记忆
灵活性 低(固定消息格式) 高(自定义提示词模板)
适用场景 通用场景,简单直接 提示词工程,精准控制上下文
实现复杂度 中(基于提示词工程)

这里使用 MessageChatMemoryAdvisor 传递 MessageWindowChatMemory

java 复制代码
        MessageChatMemoryAdvisor memoryAdvisor = MessageChatMemoryAdvisor
                .builder(chatMemory)
                .conversationId("default_conversationId") // 默认对话 `ID` 默认为 default 
                .build();

2.3 构建对话客户端

MessageChatMemoryAdvisor 传递给 ChatClient

java 复制代码
        ChatClient chatClient = ChatClient.builder(zhiPuAiChatModel)
                .defaultSystem("你是一个智能助手")
                .defaultAdvisors(memoryAdvisor)
                .build();

2.4 会话 ID

2.4.1 重要性

会话 ID 是会话的唯一主键,用于将同一会话的所有消息(用户 / 助手 / 系统)归为一组,并与其他会话彻底隔离。

作用 说明 无会话 ID 的后果
会话隔离 区分不同用户 / 不同会话的对话历史,避免消息混乱 所有用户共享同一份记忆,回答相互干扰
上下文关联 保证同一会话内多轮对话的连贯性,AI 能基于历史生成连贯回答 每次请求都是 "单轮对话",AI 失忆
记忆寻址 让 ChatMemory 快速定位并加载当前会话的历史消息 无法加载历史,上下文丢失
多用户支持 支撑多用户并发场景,为每个用户维护独立对话状态 多用户会话互相串线,数据泄露
持久化标识 持久化记忆(如 Redis/MySQL)中,作为会话消息的外键关联 重启后会话记忆丢失,无法续聊

会话 IDChatMemory 接口的核心操作参数,所有记忆实现均围绕它展开:

java 复制代码
public interface ChatMemory {
    // 向指定会话添加消息
    void add(String conversationId, List<Message> messages);
    // 获取指定会话的历史消息
    List<Message> get(String conversationId);
    // 清空指定会话的记忆
    void clear(String conversationId);
}

Spring AI 统一使用 ChatMemory.CONVERSATION_ID 作为上下文传递的键名:

java 复制代码
	String DEFAULT_CONVERSATION_ID = "default";

	String CONVERSATION_ID = "chat_memory_conversation_id";

BaseChatMemoryAdvisor 中定义了获取会话 ID 的核心逻辑:

java 复制代码
 default String getConversationId(Map<String, Object> context, String defaultConversationId) {
    // 1. 参数合法性校验
    Assert.notNull(context, "context cannot be null");
    Assert.noNullElements(context.keySet().toArray(), "context cannot contain null keys");
    Assert.hasText(defaultConversationId, "defaultConversationId cannot be null or empty");
    
    // 2. 优先从上下文获取对话ID,无则使用默认值
    return context.containsKey(ChatMemory.CONVERSATION_ID) 
            ? context.get(ChatMemory.CONVERSATION_ID).toString()
            : defaultConversationId;
}

2.4.2 多会话列表实现

例如豆包、元宝等 AI 助手中,左侧有多个对话窗口,使用同一个用户 UID + 不同的窗口会话 ID

  1. 用户新建对话 → 后端生成一个新的会话 ID
  2. 每个会话 ID 对应一条独立的对话记录
  3. 切换对话窗口 → 切换会话 ID,加载对应历史
  4. 记忆完全隔离,互不干扰

定义一个简单的 ID 生成器:

java 复制代码
/**
 * 分步式会话 ID 生成器
 * 流程:先生成随机ID → 传入用户ID+随机ID → 生成最终会话ID
 */
public final class ConversationIdGenerator {

    // 会话ID前缀
    private static final String CONVERSATION_PREFIX = "conv";
    // 短ID长度
    private static final int SHORT_ID_LENGTH = 8;

    // 禁止实例化
    private ConversationIdGenerator() {
    }

    /**
     * 第一步:生成纯随机短ID(无任何前缀)
     * @return 8位随机字符串
     */
    public static String generateRandomShortId() {
        return UUID.randomUUID().toString()
                .replace("-", "")
                .substring(0, SHORT_ID_LENGTH);
    }

    /**
     * 第二步:根据 用户ID + 第一步生成的随机ID,生成最终会话ID
     * @param userId 业务用户ID(必填)
     * @param randomId 第一步生成的随机ID(必填)
     * @return 标准会话ID:conv:userId:randomId
     */
    public static String generateConversationId(String userId, String randomId) {
        if (userId == null || userId.isBlank()) {
            throw new IllegalArgumentException("用户ID不能为空");
        }
        if (randomId == null || randomId.isBlank()) {
            throw new IllegalArgumentException("随机ID不能为空");
        }
        return String.format("%s:%s:%s", CONVERSATION_PREFIX, userId, randomId);
    }
}

2.5 多轮会话测试

模拟多轮会话:

java 复制代码
        // 1. 业务用户ID(你的系统用户ID)
        String userId = "user_10086";

        // 2. 第一步:先生成独立随机ID(模拟新建一个会话窗口,同一个会话窗口中,都需要将这个ID 传过来)
        String randomId = ConversationIdGenerator.generateRandomShortId();
        System.out.println("生成的随机ID:" + randomId); // 示例:a7f29d3c

        // 3. 第二步:传入 用户ID + 随机ID,生成最终会话ID
        String conversationId = ConversationIdGenerator.generateConversationId(userId, randomId);
        System.out.println("最终会话ID:" + conversationId); // 示例:conv:user_10086:a7f29d3c


        // 4. 第一轮对话
        String response1 = chatClient.prompt()
                .user("我叫张三,今年30岁,养了一只叫"可乐"的柯基犬,家住上海。")
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, conversationId))
                .call()
                .content();
        System.out.println("=== 第一轮对话 ===");
        System.out.println(response1);

        // 5. 第二轮对话
        String response2 = chatClient.prompt()
                .user("你知道我叫什么名字吗?")
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, conversationId))
                .call()
                .content();
        System.out.println("\n=== 第二轮对话 ===");
        System.out.println(response2);

控制台输出:

java 复制代码
生成的随机ID:58990584
最终会话ID:conv:user_10086:58990584
=== 第一轮对话 ===
你好张三!很高兴认识你。30岁,住在上海,还养了一只可爱的柯基犬"可乐",听起来是很棒的生活!柯基犬以其短腿、大耳朵和友好性格而闻名,"可乐"真是个可爱的名字。

你平时和"可乐"一起在上海有哪些活动呢?比如喜欢去哪些公园散步,或者有什么特别的养狗心得可以分享?

=== 第二轮对话 ===
是的,我记得你叫张三。你之前告诉我你今年30岁,养了一只叫"可乐"的柯基犬,并且住在上海。

ChatMemory 可以看到内存中存储的数据:

3. 数据库记忆

JdbcChatMemoryRepository 是一个内置实现,使用 JDBC 将消息存储在关系数据库中。它开箱即用支持多个数据库,适合需要持续存储聊天内存的应用。

3.1 引入依赖

需要引入 JDBC 存储记忆对应的依赖:

xml 复制代码
        <!-- Spring AI Chat Memory Repository JDBC -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
        </dependency>

MySQL 驱动:

xml 复制代码
        <!-- MySQL Driver -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

3.2 属性配置

配置数据库连接:

yml 复制代码
spring:
  application:
    name: ai-chat-demo
  # MySQL 数据库配置(用于对话记忆持久化)
  datasource:
    url: jdbc:mysql://192.168.1.235:3306/admin?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root

数据库记忆存储相关配置:

属性 描述 默认值
spring.ai.chat.memory.repository.jdbc.initialize-schema 控制数据库结构初始化时机,可选值:embedded、always、never embedded
spring.ai.chat.memory.repository.jdbc.schema 数据库初始化脚本路径,支持classpath路径与平台占位符 classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-@@platform@@.sql
spring.ai.chat.memory.repository.jdbc.platform 当使用@@platform@@占位符时的数据库平台 自动检测

3.3 数据库结构初始化

自动配置会在启动时自动创建 SPRING_AI_CHAT_MEMORY 表,使用对应数据库的 SQL 脚本。默认仅对嵌入式数据库(H2HSQLDerby 等)执行结构初始化。

可通过以下配置控制初始化行为:

yml 复制代码
# 仅对嵌入式数据库执行(默认)
spring.ai.chat.memory.repository.jdbc.initialize-schema=embedded
# 始终执行初始化
spring.ai.chat.memory.repository.jdbc.initialize-schema=always
# 从不执行(配合Flyway/Liquibase使用)
spring.ai.chat.memory.repository.jdbc.initialize-schema=never

自定义初始化脚本路径:

java 复制代码
spring.ai.chat.memory.repository.jdbc.schema=classpath:/custom/path/schema-mysql.sql

数据表执行脚本:

java 复制代码
-- admin.SPRING_AI_CHAT_MEMORY definition

CREATE TABLE `SPRING_AI_CHAT_MEMORY` (
  `conversation_id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL,
  `content` text COLLATE utf8mb4_unicode_ci NOT NULL,
  `type` enum('USER','ASSISTANT','SYSTEM','TOOL') COLLATE utf8mb4_unicode_ci NOT NULL,
  `timestamp` timestamp NOT NULL,
  KEY `SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX` (`conversation_id`,`timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

3.4 方言支持

Spring AI 会从你的 jdbc:mysql:// jdbc:postgresql:// URL 中自动识别数据库,无需配置。

支持通过方言抽象处理多个关系数据库,以下数据库开箱即用支持:

  • PostgreSQL
  • MySQL / MariaDB
  • SQL Server
  • HSQLDB
  • Oracle

使用 JdbcChatMemoryRepositoryDialect.from(DataSource) 时,框架会自动从 JDBC URL 中检测出正确的数据库方言。

示例:

java 复制代码
import org.springframework.ai.chat.memory.JdbcChatMemory;
import org.springframework.ai.chat.memory.JdbcChatMemoryRepositoryDialect;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;

// 1. 注入 Spring 数据源 (DataSource)
private final DataSource dataSource;

// 2. 自动检测数据库方言(核心代码)
JdbcChatMemoryRepositoryDialect dialect = JdbcChatMemoryRepositoryDialect.from(dataSource);

// 3. 构建 JdbcTemplate
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

// 4. 构建 数据库版聊天记忆(持久化会话ID、对话历史)
JdbcChatMemory jdbcChatMemory = new JdbcChatMemory(jdbcTemplate, dialect);

如果你的数据库不在自动检测支持列表(如达梦、人大金仓、OceanBase),手动实现接口即可。

实现方言接口:

java 复制代码
import org.springframework.ai.chat.memory.JdbcChatMemoryRepositoryDialect;

/**
 * 自定义数据库方言(示例:适配达梦数据库)
 */
public class DmChatMemoryDialect implements JdbcChatMemoryRepositoryDialect {

    // 返回建表SQL(存储对话历史的表)
    @Override
    public String getCreateChatMemoryTableSql() {
        return """
                CREATE TABLE IF NOT EXISTS AI_CHAT_MEMORY (
                    id BIGINT PRIMARY KEY AUTO_INCREMENT,
                    conversation_id VARCHAR(255) NOT NULL,
                    message_type VARCHAR(50) NOT NULL,
                    content TEXT NOT NULL,
                    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
                """;
    }

    // 插入消息SQL
    @Override
    public String getInsertMessageSql() {
        return "INSERT INTO AI_CHAT_MEMORY (conversation_id, message_type, content) VALUES (?, ?, ?)";
    }

    // 查询历史消息SQL
    @Override
    public String getSelectMessagesByConversationIdSql() {
        return "SELECT message_type, content FROM AI_CHAT_MEMORY WHERE conversation_id = ? ORDER BY create_time ASC";
    }

    // 清空会话SQL
    @Override
    public String getDeleteMessagesByConversationIdSql() {
        return "DELETE FROM AI_CHAT_MEMORY WHERE conversation_id = ?";
    }
}

使用自定义方言:

java 复制代码
// 手动指定自定义方言
JdbcChatMemoryRepositoryDialect dialect = new DmChatMemoryDialect();

// 构建持久化聊天记忆
JdbcChatMemory jdbcChatMemory = new JdbcChatMemory(jdbcTemplate, dialect);

3.5 创建 JdbcChatMemoryRepository

Spring AI 提供自动配置 JdbcChatMemoryRepository,可以直接使用:

java 复制代码
@Autowired
JdbcChatMemoryRepository chatMemoryRepository;

ChatMemory chatMemory = MessageWindowChatMemory.builder()
    .chatMemoryRepository(chatMemoryRepository)
    .maxMessages(10)
    .build();

也可以手动创建:

java 复制代码
ChatMemoryRepository chatMemoryRepository = JdbcChatMemoryRepository.builder()
    .jdbcTemplate(jdbcTemplate)
    .dialect(new PostgresChatMemoryRepositoryDialect())
    .build();

ChatMemory chatMemory = MessageWindowChatMemory.builder()
    .chatMemoryRepository(chatMemoryRepository)
    .maxMessages(10)
    .build();

当前示例:

java 复制代码
    /**
     * 手动配置 JdbcChatMemoryRepository
     */
    @Bean
    public JdbcChatMemoryRepository jdbcChatMemoryRepository(JdbcTemplate jdbcTemplate) {
        return JdbcChatMemoryRepository.builder()
                .jdbcTemplate(jdbcTemplate)
                .build();
    }

    /**
     * 基于数据库持久化的对话记忆
     */

    @Bean
    public ChatMemory chatMemory(JdbcChatMemoryRepository repository) {
        return MessageWindowChatMemory.builder()
                .chatMemoryRepository(repository)
                .maxMessages(20)  // 最多保留20条历史消息
                .build();
    }

3.6 构建对话客户端

PromptChatMemoryAdvisor 传递给 ChatClient

java 复制代码
    @Bean("zhiPuAiChatClient")
    public ChatClient zhiPuAiChatClient(ZhiPuAiChatModel zhiPuAiChatModel, ChatMemory chatMemory) {

        PromptChatMemoryAdvisor memoryAdvisor = PromptChatMemoryAdvisor
                .builder(chatMemory)
                .conversationId("default_conversationId") // 默认对话 `ID` 默认为 default
                .build();

        ChatClient chatClient = ChatClient.builder(zhiPuAiChatModel)
                .defaultSystem("你是一个智能助手")
                .defaultAdvisors(memoryAdvisor)
                .build();
        return chatClient;
    }

3.7 多轮会话测试

模拟多轮会话:

java 复制代码
        // 1. 业务用户ID(你的系统用户ID)
        String userId = "user_10086";

        // 2. 第一步:先生成独立随机ID(模拟新建一个会话窗口,同一个会话窗口中,都需要将这个ID 传过来)
        String randomId = ConversationIdGenerator.generateRandomShortId();
        System.out.println("生成的随机ID:" + randomId); // 示例:a7f29d3c

        // 3. 第二步:传入 用户ID + 随机ID,生成最终会话ID
        String conversationId = ConversationIdGenerator.generateConversationId(userId, randomId);
        System.out.println("最终会话ID:" + conversationId); // 示例:conv:user_10086:a7f29d3c


        // 4. 第一轮对话
        String response1 = chatClient.prompt()
                .user("我叫张三,今年30岁,养了一只叫"可乐"的柯基犬,家住上海。")
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, conversationId))
                .call()
                .content();
        System.out.println("=== 第一轮对话 ===");
        System.out.println(response1);

        // 5. 第二轮对话
        String response2 = chatClient.prompt()
                .user("你知道我叫什么名字吗?")
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, conversationId))
                .call()
                .content();
        System.out.println("\n=== 第二轮对话 ===");
        System.out.println(response2);

查看数据库,根据会话 ID 保存了每条记录,没有传会话 ID 的则使用默认的:

相关推荐
耿雨飞2 小时前
DeerFlow 系列教程 第五篇 | 配置与 Docker 部署全指南:从香港首建到内陆迁移
人工智能·deer-flow·llm应用开发平台
深蓝轨迹2 小时前
#Python零基础机器学习入门教程
人工智能·python·机器学习
Lyyaoo.2 小时前
【JAVA基础面经】String、StringBuffer、StringBuilder
java·开发语言
EMQX2 小时前
S3 正在吞噬一切:AI 时代的基础软件架构革命
人工智能·物联网·mqtt·flowmq
QC777LX2 小时前
传统法务工作重复度高,AI法律顾问正在改变格局
人工智能
枫叶林FYL2 小时前
【自然语言处理 NLP】7.2.2.3 隐私泄露评估(Privacy Leakage via Memorization)
人工智能·深度学习·机器学习
jarvisuni2 小时前
Claude官网克隆之Opus4.6
人工智能·ai编程
财经资讯数据_灵砚智能2 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(夜间-次晨)2026年4月9日
大数据·人工智能·信息可视化·自然语言处理·ai编程
TeamDev2 小时前
JxBrowser 8.18.2 版本发布啦!
java·前端·跨平台·桌面应用·web ui·jxbrowser·浏览器控件