Spring AI 教程

自定义 ChatMemoryRepository(Redis 存储)

提示:如果不能访问 OpenAI,请点击 AiCode API 注册账号,通过代理访问。  

前面介绍了 Spring AI 内置的 ChatMemoryRepository 实现,本章节将介绍自定义实现 ChatMemoryRepository,将回话历史存储到 Redis。

Redis 简介

Redis(Remote Dictionary Server)是一款开源的高性能键值对(Key-Value)数据库,基于内存存储并支持持久化,核心特点是“快”和“灵活”,广泛用于缓存、会话存储、消息队列等场景。

Redis 数据库的数据默认存储在内存中,读写速度极快(单机 QPS 可达 10 万 +),远超传统磁盘数据库(如 MySQL),适合高频访问场景(如商品详情缓存、用户 Token 存储)。

简单来说,Redis 的核心价值是 “用内存换速度”,解决传统数据库在高频访问场景下的性能瓶颈,是分布式系统中 “提升性能、降低数据库压力” 的关键工具。

Redis 支持多种数据类型,适合多种场景:

数据结构

核心用途

String

存储文本、数字(如用户昵称、商品库存)

Hash

存储对象(如用户信息、商品属性)

List

实现队列、栈、消息列表(如评论列表、任务队列)

Set

无序唯一集合(如用户标签、共同好友)

Sorted Set(ZSet)

有序唯一集合(如排行榜、延时任务)

Bitmap

二进制位存储(如用户签到、在线状态)

注意:Redis 支持通过 “持久化机制” 将内存数据同步到磁盘,避免重启后数据丢失。

简单示例

下面通过一个简单的实例演示如何自定义 ChatMemoryRepository 实现。

添加 Redis 配置

添加如下配置信息到 application.yml 配置文件:

spring:
  # Redis 配置
  data:
    redis:
      host: localhost
      port: 6379
      password: aaaaaa
      timeout: 5000
      database: 1
      jedis:
        pool:
          enabled: true  # 显式启用连接池
          max-active: 16  # 最大活跃连接数(根据并发量调整,建议为 CPU核心数 * 2)
          max-wait: 2000  # 最大等待时间(添加时间单位,避免默认毫秒被误读)
          max-idle: 8  # 最大空闲连接(保持与 max-active 合理比例,避免资源浪费)
          min-idle: 4  # 最小空闲连接(保留核心连接,减少重建开销)

自定义ChatMemoryRepository

这里通过 RedisTemplate 操作 Redis 数据库,具体代码如下:

package com.hxstrive.springai.springai_openai.chatMemoryRepository.chatMemoryRepository5;

import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.messages.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import java.util.*;

/**
 * 自定义 ChatMemoryRepository,将数据存储到 Redis
 * @author hxstrive.com
 */
public class RedisChatMemoryRepository implements ChatMemoryRepository {
    // 存储到Redis键的前缀
    private static final String DATAS_REDIS_PREFIX = "ai:history:data:";
    private final RedisTemplate<String, String> redisTemplate;

    public RedisChatMemoryRepository(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
        System.out.println(redisTemplate);
    }

    @Override
    public List<String> findConversationIds() {
        List<String> retKeys = new ArrayList<>();
        Optional.of(redisTemplate.opsForHash().keys(DATAS_REDIS_PREFIX + "*")).ifPresent(keys -> {
            keys.forEach(k -> {
                if(Objects.nonNull(k)) {
                    retKeys.add(k.toString());
                }
            });
        });
        return retKeys;
    }

    @Override
    public List<Message> findByConversationId(String conversationId) {
        List<String> list = redisTemplate.opsForList().range(DATAS_REDIS_PREFIX + conversationId, 0, -1);
        if(Objects.isNull(list) || list.isEmpty()) {
            return Collections.emptyList();
        }

        final List<Message> retList = new ArrayList<>();

        for(String json : list) {
            if(!StringUtils.hasText(json)) {
                continue;
            }

            JSONObject jsonObject = JSONObject.parseObject(json);
            String type = jsonObject.getString("messageType");
            String text = jsonObject.getString("text");
            if(!StringUtils.hasText(type) || !StringUtils.hasText(text)) {
                continue;
            }

            if("USER".equalsIgnoreCase(type)) {
                UserMessage userMessage = new UserMessage(text);
                retList.add(userMessage);
            } else if("SYSTEM".equalsIgnoreCase(type)) {
                SystemMessage systemMessage = new SystemMessage(text);
                retList.add(systemMessage);
            } else if("ASSISTANT".equalsIgnoreCase(type)) {
                AssistantMessage assistantMessage = new AssistantMessage(text);
                retList.add(assistantMessage);
            } else if("TOOL".equalsIgnoreCase(type)) {
                ToolResponseMessage toolResponseMessage = new ToolResponseMessage(jsonObject.getList("responses",
                        ToolResponseMessage.ToolResponse.class));
                retList.add(toolResponseMessage);
            } else {
                System.err.println("未知消息类型 type=" + type + ", data=" + json);
            }
        }

        return retList;
    }

    @Override
    public void saveAll(String conversationId, List<Message> messages) {
        if(messages.isEmpty()) {
            return;
        }

        // 删除旧数据
        redisTemplate.delete(DATAS_REDIS_PREFIX + conversationId);

        // 插入最新数据
        final List<String> list = new ArrayList<>();
        messages.forEach(m -> {
            list.add(JSONObject.toJSONString(m));
        });
        redisTemplate.opsForList().rightPushAll(DATAS_REDIS_PREFIX + conversationId, list);
    }

    @Override
    public void deleteByConversationId(String conversationId) {
        redisTemplate.opsForHash().delete(DATAS_REDIS_PREFIX + conversationId, conversationId);
    }

}

上述代码中,findByConversationId() 方法通过解析 JSON 字符串手动创建各种类型的 Message 对象,这里没有采用自动序列化和反序列是因为 UserMessage、AssistantMessage 和 SystemMessage 没有提供默认构造方法,因此改为手动创建。

添加 AiConfig 配置类

创建 AiConfig 配置类,配置 ChatMemory 和自定义的 RedisChatMemoryRepository,如下:

package com.hxstrive.springai.springai_openai.chatMemoryRepository.chatMemoryRepository5;

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.RedisTemplate;

@Configuration
public class AiConfig {

    /**
     * 定义聊天记忆(ChatMemory)Bean
     * 采用窗口模式(MessageWindowChatMemory),限制最多保存的消息数量
     *
     * @param chatMemoryRepository 聊天记忆存储仓库(Redis实现)
     * @return 配置好的聊天记忆对象
     */
    @Bean
    public ChatMemory chatMemory(RedisChatMemoryRepository chatMemoryRepository) {
        return MessageWindowChatMemory.builder()
                .chatMemoryRepository(chatMemoryRepository) // 注入自定义存储仓库,指定消息持久化方式
                .maxMessages(20) // 设置窗口大小,最多保存20条消息(超过则自动丢弃最早的消息)
                .build();
    }

    // 创建自定义的 RedisChatMemoryRepository
    @Bean
    public RedisChatMemoryRepository redisChatMemoryRepository(RedisTemplate<String, String> redisTemplate) {
        return new RedisChatMemoryRepository(redisTemplate);
    }

}

注意,上述代码通过 @Bean 注解将 RedisChatMemoryRepository 放入到 Spring 容器中,且传递 RedisTemplate 对象到 RedisChatMemoryRepository 中操作 Redis。

客户端类

创建一个 CommandLineRunner 类,启动 Spring Boot 时自动执行 run() 方法,我们在 run() 方法中执行聊天逻辑,如下:

package com.hxstrive.springai.springai_openai.chatMemoryRepository.chatMemoryRepository5;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class SpringAiDemoApplication implements CommandLineRunner {

	@Autowired
	private ChatModel chatModel;

	@Autowired
	private ChatMemory chatMemory;

	public static void main(String[] args) {
		SpringApplication.run(SpringAiDemoApplication.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
		StringBuilder builder = new StringBuilder();

		// 唯一会话ID,同一个会话上下文中保持一致
		String sessionId = "USER-20251019172604";

		String question = "四川大学是985吗?";
		String ai = chat(sessionId, question);
		builder.append(String.format("用户问题:%s\n", question));
		builder.append(String.format("机器回复:%s\n", ai));

		question = "是211吗?";
		ai = chat(sessionId, question);
		builder.append(String.format("用户问题:%s\n", question));
		builder.append(String.format("机器回复:%s\n", ai));

		System.out.println(builder.toString());
	}

	private String chat(String sessionId, String question) {
		ChatClient chatClient = ChatClient.builder(chatModel)
				.defaultSystem("你是AI助手小粒,所有回复均使用中文。")
				.defaultAdvisors(
						new SimpleLoggerAdvisor() // 输出聊天日志
				).build();

		// 1. 将用户输入添加到记忆
		UserMessage userMessage = new UserMessage(question);
		chatMemory.add(sessionId, userMessage);

		// 2. 获取记忆中的所有消息(上下文),传递给 AI 模型
		ChatClient.CallResponseSpec responseSpec = chatClient.prompt().messages(chatMemory.get(sessionId)).call();

		// 3. 将 AI 回复添加到记忆,更新上下文
		AssistantMessage assistantMessage = responseSpec.chatResponse().getResult().getOutput();
		chatMemory.add(sessionId, assistantMessage);

		// 4. 返回 AI 回复内容
		return assistantMessage.getText();
	}

}

运行上述代码,输出如下:

用户问题:四川大学是985吗?
机器回复:是的,四川大学是“985工程”院校之一。
用户问题:是211吗?
机器回复:是的,四川大学同时也是“211工程”院校。

连接到本地 Redis,数据库中存储如下信息:

自定义 ChatMemoryRepository(Redis 存储)

上面仅仅提供自定义的简单示例,读者可以尝试创建更复杂,更符合自己业务场景的 RedisChatMemoryRepository 实现类。

  

提示:如果不能访问 OpenAI,请点击 AiCode API 注册账号,通过代理访问。  

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号