AI -- 实现多轮对话且将消息数据基于ChatMemory持久化到 Redis 示例

AI -- 实现多轮对话且将消息数据持久化到 Redis 示例

  • [实现多轮对话且将消息数据持久化到 Redis 示例](#实现多轮对话且将消息数据持久化到 Redis 示例)
    • [简单描述 ChatMemory 、ChatMemoryRepository 、MessageWindowChatMemory](#简单描述 ChatMemory 、ChatMemoryRepository 、MessageWindowChatMemory)
    • 代码:
    • 测试:
      • [第一次发送消息:数据持久化到 redis 的全过程](#第一次发送消息:数据持久化到 redis 的全过程)
      • 发送三次消息的打印:

实现多轮对话且将消息数据持久化到 Redis 示例

简单描述 ChatMemory 、ChatMemoryRepository 、MessageWindowChatMemory

ChatMemory 是上层对话历史管理接口,它负责提供"聊天记忆"的读取、写入和裁剪能力,是业务层调用聊天记忆的统一入口,而不关心底层存储实现。

ChatMemoryRepository 是聊天消息的存储接口,只负责"存/取/删消息",不关心裁剪、业务逻辑或多轮对话,只做纯存储操作。-- 数据默认是存在内存中。

ChatMemory 是高层接口,负责管理和操作对话"内存",而 ChatMemoryRepository 是底层接口,负责实际的消息存储。ChatMemory 内部依赖 ChatMemoryRepository 来读写数据

MessageWindowChatMemory 是 ChatMemory 的实现类(业务层封装),负责对聊天消息进行"窗口裁剪",也就是只保留最近 N 条消息,并对外提供统一的聊天记忆接口。

ChatMemory、MessageWindowChatMemory、ChatMemoryRepository 三者的关系:

ChatMemory 接口由 MessageWindowChatMemory 实现,负责负责裁剪消息、管理多轮上下文,内部调用 ChatMemoryRepository 完成实际存储(存在内存中)。

为了将聊天数据持久化到 Redis,我们自定义了 RedisChatMemoryRepository 实现 ChatMemoryRepository 接口,从而替换默认存储,使消息保存在 Redis 中

代码:

1、添加依赖

java 复制代码
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、配置Redis连接

这里我是自己在linux弄一个redis,这个就自行处理就好

java 复制代码
spring:
  ai:
    openai:
      base-url: 用的 DeepSeek
      # 注意:个人学习就可以直接把api key 写到配置类,生产就不行
      api-key: xxxxxxxxxxxxxxxxxxxxxxxxxx自己的api key
      chat:
        options:
          model: deepseek-chat        # DeepSeek-V3 的模型名
          temperature: 0.7            # 创意度,0-2,越高越随机
          max-tokens: 2048        # 最大输出 Token 数
          top-p: 1.0              # top-p 是核采样参数,用于限制模型在概率累计达到 p 的候选词集合中进行随机采样,从而控制生成文本的多样性。
  
  data:
    # Redis 配置
    redis:
      # 虚拟机
      host: xxxxxxxxxx
      port: 6379
      password: xxxxxxxxxxx
      database: 0

3、controller

java 复制代码
package cn.ljh.springai.controller.chatmemory;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 用 redis 存储
 *
 * @author lujinhong
 * @since 2026/3/20  星期五
 */
@RestController
@RequestMapping("/redis-memory-chat")
public class MemoryChatController_03 {

    private final ChatClient chatClient;

    // 这里有参构造器注入的其实已经是我自定义的那个 ChatMemory 的 bean 了,这个bean的数据是存在redis里面的。
    private final ChatMemory chatMemory;

    // 有参构造器
    public MemoryChatController_03(ChatClient.Builder builder , ChatMemory chatMemory) {
        this.chatMemory = chatMemory;
        this.chatClient = builder
                .defaultSystem("你是一个 Java 技术助手")
                .build();
    }

    /**
     * 多轮对话接口
     */
    @GetMapping("/chat")
    public String chat(
            @RequestParam String message,
            @RequestParam(defaultValue = "default") String conversationId) {

        return chatClient.prompt()
                .user(message)
                // 核心:advisors = 一组"请求增强器 / 拦截器",在调用大模型之前,对 prompt 做增强,"自动帮你拼聊天记录"
                .advisors(MessageChatMemoryAdvisor.builder(chatMemory)
                        .conversationId(conversationId)
                        .build())
                .call()
                .content();
    }
}

4、自定义 ChatMemory Bean

java 复制代码
package cn.ljh.springai.config;

import cn.ljh.springai.memory.RedisChatMemoryRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;

/**
 * @author lujinhong
 * @since 2026-03-21
 */

@Configuration
public class ChatMemoryConfig {

    /**
     * 自定义 ChatMemory Bean(基于 Redis 持久化)。
     *
     * 原因说明:
     * 1. Spring AI 的默认 ChatMemory Bean 使用了 @ConditionalOnMissingBean(ChatMemory.class) 这个条件注解,
     *    只有在容器中不存在 ChatMemory 类型的 Bean 时才会创建。
     * 2. 这里我们自定义了 ChatMemory Bean,所以 Spring 容器发现已有 Bean,
     *    默认的 ChatMemory Bean 就不会被创建。
     *
     * 功能说明:
     * - 使用 RedisChatMemoryRepository 进行消息持久化
     * - 最多保留 20 条消息
     */
    // Bean 注册好之后,Controller 直接注入 ChatMemory 使用,代码和内存版完全一样,只是底层换成了 Redis
    @Bean
    public ChatMemory chatMemory(StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper) {

        RedisChatMemoryRepository redisChatMemoryRepository
                = new RedisChatMemoryRepository(stringRedisTemplate, objectMapper);

        // 底层走 Redis 持久化,上层限制最多保留 20 条消息
        MessageWindowChatMemory messageWindowChatMemory = MessageWindowChatMemory.builder()
                .chatMemoryRepository(redisChatMemoryRepository)
                .maxMessages(20)
                .build();
        return messageWindowChatMemory;
    }
}

5、实现 ChatMemoryRepository 接口,重写方法

java 复制代码
package cn.ljh.springai.memory;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * 持久化储存:把数据存到redis中
 *
 * 实现 ChatMemoryRepository 接口,将聊天消息以 JSON 形式存储到 Redis 中,并提供会话消息的读取、保存和删除能力
 *
 * <p>
 * ChatMemoryRepository --纯存储层,负责读写所有消息,不做裁剪
 * MessageWindowChatMemory -- 包装 Repository ,对外暴露 chatMemory ,负责按条数裁剪
 * <p>
 * "裁剪"在聊天上下文中,通常指 控制会话消息的数量,避免数据无限增长。
 * 举例:
 * Redis 中一个会话可能有 1000 条消息
 * 你只希望对话上下文保留最近 20 条(或者最近 N 条)消息来做模型输入
 * 裁剪操作就是 取出最近 N 条消息 或 删除过旧消息
 * <p>
 * 发送消息只会用到:findByConversationId 和 saveAll 这两个方法
 *
 * @author lujinhong
 * @since 2026-03-20
 */

public class RedisChatMemoryRepository implements ChatMemoryRepository {

    // 前缀
    private static final String KEY_PREFIX = "chat_memory";

    // redis 客户端
    private final StringRedisTemplate redisTemplate;

    // 工具类:对象 和 JSON 格式互转
    private final ObjectMapper objectMapper;

    int i = 1;

    // 有参构造器注入
    public RedisChatMemoryRepository(StringRedisTemplate redisTemplate, ObjectMapper objectMapper) {
        this.redisTemplate = redisTemplate;
        this.objectMapper = objectMapper;
    }


    /**
     * 返回所有会话 ID(扫描 Redis 中匹配前缀的 key)
     * 作用:扫描 Redis 中所有以 chat_memory 前缀开头的 key,并返回对应的会话 ID 列表。
     * 注意点:返回的是 key 去掉前缀后的部分,即真正的 conversationId。
     */
    @Override
    public List<String> findConversationIds() {
        Set<String> keys = redisTemplate.keys(KEY_PREFIX + "*");
        List<String> list = keys.stream().map(key -> key.substring(KEY_PREFIX.length())).toList();
        return list;
    }

    /**
     *
     * 发送消息会先执行这个方法,然后再执行 saveAll方法
     *
     * 返回该会话的全部消息,裁剪逻辑由外层 MessageWindowChatMemory 处理
     * <p>
     * 作用:根据会话 ID 从 Redis 中取出该会话的全部消息,并将 JSON 字符串反序列化为 Message 对象列表(UserMessage 或 AssistantMessage)
     * 特点:不做裁剪,裁剪逻辑交给外层 MessageWindowChatMemory 去处理。
     */
    @Override
    public List<Message> findByConversationId(String conversationId) {
        String key = KEY_PREFIX + ":" + conversationId;
        // 获取 Redis 这个 List 里的"所有元素"
        List<String> range = redisTemplate.opsForList().range(key, 0, -1);
        List<Message> messages = new ArrayList<>();
        for (String s : range) {
            try {
                // 把 JSON 字符串 → 转成 Java 对象(反序列化)
                MessageRecord messageRecord = objectMapper.readValue(s, MessageRecord.class);
                if ("USER".equals(messageRecord.role())) {
                    messages.add(new UserMessage(messageRecord.content()));
                } else if ("ASSISTANT".equals(messageRecord.role())) {
                    messages.add(new AssistantMessage(messageRecord.content()));
                }
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
        return messages;
    }

    /**
     * 作用:保存一整个会话的消息列表到 Redis。追加消息并刷新过期时间
     * 具体操作:
     * 1、先删除 Redis 中原有的这个会话的 key。
     * 2、遍历消息,将每条消息封装成 MessageRecord 并序列化成 JSON 存到 Redis List 的右侧(尾部)。
     * 3、给 key 设置过期时间 5 天。
     * 效果:保证每次保存都是"全量覆盖",并且 Redis 中数据不会无限增长
     */
    @Override
    public void saveAll(String conversationId, List<Message> messages) {

        String key = KEY_PREFIX + ":" + conversationId;
        // 先删除历史数据,不然就会出现重复消息一直添加进去(比如:1,12,123,如果不把前面是删掉,就会出现 1 1 2 1 2 3 这种重复数据)
        // 直接删除数据,然后重新全量写入
        redisTemplate.delete(key);
        // 存数据
        for (Message message : messages) {
            System.err.println("第【 " + i++ + " 】 条消息内容:" + message.getText());
            MessageRecord messageRecord = new MessageRecord(message.getMessageType().name(), message.getText());
            // redisTemplate.opsForList() 操作 redis 里面的 list 列表:把 messageRecord 插入到 Redis List 的"右边(尾部)"
            try {
                redisTemplate.opsForList().rightPush(key, objectMapper.writeValueAsString(messageRecord));
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
        // key 的过期时间:5天
        redisTemplate.expire(key, 5, TimeUnit.DAYS);
    }

    /**
     * 删除会话
     * 作用:删除指定会话的所有消息(直接删除 Redis 对应的 key)
     */
    @Override
    public void deleteByConversationId(String conversationId) {
        redisTemplate.delete(KEY_PREFIX + ":" + conversationId);
    }

    // 定义一个消息记录对象
    // 作用:作为存储到 Redis 的中间格式,包含 role(用户或助手)和 content(消息文本)。
    // 特点:简洁、可序列化为 JSON,用于在 Redis 中存储和读取。
    record MessageRecord(String role, String content) {
    }

}

测试:

第一次发送消息:数据持久化到 redis 的全过程

这个时候就得到 AI 的响应了

接着准备删除旧数据,重新全量写入(提问+回答的数据)

顺序是这样的:我发送第一条消息:我是谁

1、把用户发送的消息【我是谁】,存到redis中

2、然后代码继续走,接着得到AI的响应后,此时messages就有两条数据:一条我问的,一条AI回复的

3、然后就走到saveAll 方法,把刚存到redis的消息删掉,然后重新把这两条消息全量写入

第一次发送消息的全过程,消息就是这么写入和删除的,然后再全量写入

发送三次消息的打印:

这个是发送三次消息的打印:

通过打印看看,每次发送消息,得到回复后,都会删除掉历史数据,然后重新全量写入。

相关推荐
zhensherlock2 小时前
Protocol Launcher 系列:Trae AI 编辑器的深度集成
javascript·人工智能·vscode·ai·typescript·编辑器·ai编程
Zender Han2 小时前
从 0 到 1:如何设计与编写高质量 Skills(AI Agent 技能开发指南)
人工智能·ai
何政@2 小时前
通过python 快速完成ai 构建
人工智能·python·ai·大模型·love l
Cha0DD2 小时前
【由浅入深探究langchain】第四集-(RAG)语义搜索-数据入库
人工智能·ai·langchain
gao_tjie3 小时前
영화 같은 느낌, 대규모 팀 필요 없음: Veo 비디오 생성 API, AI에 영화 언어 넘기기 (다양한 예제 및 설명 포함)
ai
吾无法无天3 小时前
Ubuntu24安装OpenClaw并配置(浏览器自动化,飞书,一些skill)
ai·openclaw
marsh02063 小时前
14 openclaw模板引擎使用:高效渲染动态内容
java·前端·spring·ai·编程·技术
AC赳赳老秦3 小时前
使用OpenClaw tavily-search技能高效撰写工作报告:以人工智能在医疗行业的应用为例
运维·人工智能·python·flask·自动化·deepseek·openclaw
智算菩萨3 小时前
【Generative AI For Autonomous Driving】5 生成式AI在自动驾驶中的六大应用场景:从数据合成到智慧交通
论文阅读·人工智能·机器学习·ai·自动驾驶·感知