Spring AI 会话记忆实战:从内存存储到 MySQL + Redis 双层缓存架构

为什么需要聊天记忆?

大语言模型(LLM)本质上是无状态的。这意味着每次向模型发送请求时,它都"忘记"了之前的对话内容。这在需要多轮交互的场景中是致命的------比如用户说:"我叫张三",接着问:"你能复述我的名字吗?",模型大概率会回答"我不知道"。

为了解决这个问题,Spring AI 提供了 ChatMemory 抽象,允许我们在多次与 LLM 的交互中存储和检索对话历史,从而实现"记忆"功能。

本文将带你:

  1. 快速上手 Spring AI 的基础聊天记忆功能;
  2. 深入理解 ChatMemory 的设计原理;
  3. 实战:使用 阿里云通义千问(DashScope) 模型;
  4. 进阶:不使用默认的 JDBC 存储 ,而是构建一个 MySQL + Redis 双层缓存 的高性能会话存储系统。

准备工作

1. 搭建 Spring Boot 项目

创建一个标准的 Spring Boot 项目,确保使用较新的 Spring Boot 版本以兼容 Spring AI。

2. 添加 Spring AI 与通义千问依赖

由于我们使用的是阿里云的通义千问模型,需引入对应的 Starter:

xml 复制代码
<properties>
   <java.version>21</java.version>
   <spring.ai.version>1.0.0</spring.ai.version>
   <spring.ai.alibaba.version>1.0.0.3</spring.ai.alibaba.version>
</properties>
<!-- Spring AI 阿里云通义千问 Starter -->
<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    <version>${spring.ai.alibaba.version}</version>
</dependency>

<!-- 聊天记忆 JDBC 支持 -->
<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-memory-jdbc</artifactId>
    <version>${spring.ai.alibaba.version}</version>
</dependency>

<!-- MySQL 驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.32</version>
</dependency>

<!-- Redis 缓存支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

3. 配置通义千问 API Key

yaml 复制代码
spring:
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY}

💡 提示 :请将 DASHSCOPE_API_KEY 设置为环境变量,避免密钥泄露。

Spring AI 聊天记忆基础

Spring AI 会自动注册一个 ChatMemory 的默认实现------MessageWindowChatMemory。它使用内存中的 ConcurrentHashMap(通过 InMemoryChatMemoryRepository)存储消息,默认保留最近的 20 条消息。

聊天记忆功能通过 Advisor(顾问/拦截器) 实现。MessageChatMemoryAdvisor 是 Spring AI 提供的默认顾问,负责在请求前后与 ChatMemory 交互。

java 复制代码
@RestController
@RequestMapping("/chat")
public class ChatController {

    private final ChatClient chatClient;
    private final ChatMemory chatMemory; // Spring AI 自动注入默认实现

    public ChatController(ChatClient.Builder chatClientBuilder, ChatMemory chatMemory) {
        this.chatClient = chatClientBuilder.build();
        this.chatMemory = chatMemory;
    }

    @GetMapping("/memory")
    public String chatWithMemory(String userInput) {
        return chatClient.prompt()
                .advisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) // 启用记忆功能
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "session-007")) // 指定会话ID
                .user(userInput)
                .call()
                .content();
    }
}

测试:

  1. 访问 http://localhost:8080/chat/memory?userInput=你好,我叫李雷
  2. 再访问 http://localhost:8080/chat/memory?userInput=请告诉我你的名字

如果第二次请求模型能正确回答"你叫李雷",说明聊天记忆已生效!

ChatMemory 框架设计

Spring AI 的 ChatMemory 设计非常优雅,采用分层架构

复制代码
+------------------+
|   ChatMemory     |  <- 接口,定义 add/get/clear 行为
+------------------+
         |
         v
+--------------------------+
| MessageWindowChatMemory  |  <- 默认实现,管理消息窗口(如保留最近20条)
+--------------------------+
         |
         v
+---------------------------+
|  ChatMemoryRepository     |  <- 接口,定义数据持久化行为
+---------------------------+
         |
   +------+------+
   |             |
   v             v
+-----------+   +------------------+
| InMemory  |   | JdbcChatMemory   |
| Repository|   | Repository       |
+-----------+   +------------------+
                |
                v
           +-------------+
           |  Database   |
           +-------------+
  • ChatMemory : 顶层接口,定义了 add, get, clear 等核心方法。
  • MessageWindowChatMemory : 默认实现,内部持有 ChatMemoryRepository 实例,负责管理消息的窗口大小。
  • ChatMemoryRepository: 数据存储抽象层。
    • InMemoryChatMemoryRepository: 使用 ConcurrentHashMap 存储在内存中(默认)。
    • JdbcChatMemoryRepository: 使用关系型数据库存储。

关键点 :当项目中引入了 spring-ai-starter-model-chat-memory-repository-jdbc 依赖时,Spring AI 会自动将 JdbcChatMemoryRepository 注册为 ChatMemoryRepository 的 Bean,从而替代内存实现。

构建 MySQL + Redis 双层缓存架构

虽然 JdbcChatMemoryRepository 可以直接将数据存入 MySQL,但在高并发场景下,频繁的数据库读写会影响性能。

我们不满足于此,目标是:使用 Redis 作为高速缓存,MySQL 作为持久化存储,实现读写分离的双层架构

设计思路

  1. 写入流程 :新消息 → 同时写入 RedisMySQL
  2. 读取流程 :优先从 Redis 读取;若未命中,则从 MySQL 读取,并回填到 Redis。
  3. 容量控制 :使用 Redis 的 LIST 结构 + TRIM 命令,确保每个会话只保留最近 N 条消息。

实现步骤

1. 创建自定义 ChatMemory 实现
java 复制代码
@Service
public class CachedChatMemoryService implements ChatMemory {

    private final JdbcTemplate jdbcTemplate;
    private final RedisTemplate<String, Object> redisTemplate;
    private final int messageWindowSize;

    private static final String KEY_PREFIX = "chat:history:";

    public CachedChatMemoryService(JdbcTemplate jdbcTemplate, 
                                  RedisTemplate<String, Object> redisTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        this.redisTemplate = redisTemplate;
        this.messageWindowSize = 20; // 可配置
    }

    @Override
    public void add(String conversationId, List<Message> messages) {
        String key = KEY_PREFIX + conversationId;

        // 1. 写入 Redis
        messages.forEach(msg -> {
            redisTemplate.opsForList().rightPush(key, new ChatMemoryEntity(msg.getText(), msg.getMessageType().name()));
        });

        // 2. 修剪 Redis 列表,只保留最近 messageWindowSize 条
        redisTemplate.opsForList().trim(key, -messageWindowSize, -1);

        // 3. 批量写入 MySQL
        batchSaveToDatabase(conversationId, messages);
    }

    @Override
    public List<Message> get(String conversationId) {
        String key = KEY_PREFIX + conversationId;

        // 1. 先查 Redis
        List<Object> cached = redisTemplate.opsForList().range(key, 0, -1);
        if (cached != null && !cached.isEmpty()) {
            return convertToMessages(cached);
        }

        // 2. Redis 无数据,查 MySQL
        List<Message> dbMessages = jdbcTemplate.query(
            "SELECT content, type FROM ai_chat_memory WHERE conversation_id = ? ORDER BY timestamp DESC LIMIT ?",
            new MessageRowMapper(),
            conversationId, messageWindowSize
        );

        // 3. 回填 Redis
        if (!dbMessages.isEmpty()) {
            dbMessages.forEach(msg -> redisTemplate.opsForList().rightPush(key, new ChatMemoryEntity(msg.getText(), msg.getMessageType().name())));
            redisTemplate.opsForList().trim(key, -messageWindowSize, -1);
        }

        return dbMessages;
    }

    @Override
    public void clear(String conversationId) {
        String key = KEY_PREFIX + conversationId;
        redisTemplate.delete(key);
        jdbcTemplate.update("DELETE FROM ai_chat_memory WHERE conversation_id = ?", conversationId);
    }

   private void batchSaveToDatabase(String conversationId, List<Message> messages) {
        String sql = "INSERT INTO ai_chat_memory (conversation_id, content, type, timestamp) VALUES (?, ?, ?, ?)";

        List<Object[]> batchArgs = new ArrayList<>();
        long baseTimestamp = Instant.now().toEpochMilli();

        for (int i = 0; i < messages.size(); i++) {
            Message message = messages.get(i);
            Object[] args = new Object[] {
                    conversationId,
                    message.getText(),
                    message.getMessageType().getValue().toUpperCase(),
                    new Timestamp(baseTimestamp + i) // 确保每条消息时间戳不同
            };
            batchArgs.add(args);
        }

        jdbcTemplate.batchUpdate(sql, batchArgs);
    }

    /**
     * 数据库行到 Message 对象的映射器
     */
    private static class MessageRowMapper implements RowMapper<Message> {
        @Nullable
        public Message mapRow(ResultSet rs, int i) throws SQLException {
            String content = rs.getString(1);
            MessageType type = MessageType.valueOf(rs.getString(2));
            Message message;
            switch (type) {
                case USER -> message = new UserMessage(content);
                case ASSISTANT -> message = new AssistantMessage(content);
                case SYSTEM -> message = new SystemMessage(content);
                case TOOL -> message = new ToolResponseMessage(List.of());
                default -> throw new IncompatibleClassChangeError();
            }
            return message;
        }
    }
}
2. 配置 ChatClient 使用自定义 ChatMemory
java 复制代码
@Configuration
public class ChatClientConfig {
    
    private static final String DEFAULT_PROMPT = "你是一个博学的智能聊天助手,请根据用户提问回答!";

    @Bean
    public ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory) {
        return ChatClient.builder(chatModel)
                .defaultSystem(DEFAULT_PROMPT)
                .defaultAdvisors(
                    MessageChatMemoryAdvisor.builder(chatMemory).build(),
                    new SimpleLoggerAdvisor()
                )
                .build();
    }

    @Bean
    public ChatMemory chatMemory(CachedChatMemoryService service) {
        return service; // 使用我们自定义的服务
    }
}
3.数据库表结构
sql 复制代码
CREATE TABLE ai_chat_memory (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    conversation_id VARCHAR(255) NOT NULL,
    content TEXT NOT NULL,
    type VARCHAR(50) NOT NULL, -- USER, ASSISTANT, SYSTEM
    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_conversation (conversation_id)
);

继续测试,查看Redis中的数据:

Spring AI 的扩展性极强,它不仅提供了开箱即用的解决方案,更鼓励开发者根据业务需求进行深度定制。掌握其设计思想,是构建企业级 AI 应用的关键。

相关推荐
Zz_waiting.3 小时前
Spring 原理
java·spring·spring自动管理
ARM+FPGA+AI工业主板定制专家5 小时前
基于GPS/PTP/gPTP的自动驾驶数据同步授时方案
人工智能·机器学习·自动驾驶
长鸳词羡5 小时前
wordpiece、unigram、sentencepiece基本原理
人工智能
ㄣ知冷煖★5 小时前
【GPT5系列】ChatGPT5 提示词工程指南
人工智能
科士威传动5 小时前
丝杆支撑座在印刷设备如何精准运行?
人工智能·科技·自动化·制造
taxunjishu6 小时前
DeviceNet 转 Modbus TCP 协议转换在 S7-1200 PLC化工反应釜中的应用
运维·人工智能·物联网·自动化·区块链
kalvin_y_liu7 小时前
智能体框架大PK!谷歌ADK VS 微软Semantic Kernel
人工智能·microsoft·谷歌·智能体
爱看科技7 小时前
智能眼镜行业腾飞在即,苹果/微美全息锚定“AR+AI眼镜融合”之路抢滩市场!
人工智能·ar
Juchecar10 小时前
LLM模型与ML算法之间的关系
人工智能