Spring AI 聊天记忆功能实战(二):自定义 Redis 聊天记忆外部存储实现

Spring AI 聊天记忆功能实战(二):自定义 Redis 聊天记忆外部存储实现

Spring AI 框架通过模块化的抽象设计允许我们将默认的内存存储替换为自定义的外部存储方式,从而获得更好的扩展性和数据持久性。本文将深入探讨如何通过自定义 Redis 存储实现聊天记忆的功能。

相较于内存存储,Redis 作为聊天记忆存储具有以下显著优势:

  1. 分布式支持:天然支持集群模式,适合微服务架构
  2. 数据持久化:提供 RDB 和 AOF 两种持久化方式,避免数据丢失
  3. 高可用性:通过 Sentinel 和 Cluster 实现自动故障转移
  4. 丰富数据结构:使用 List 或 Stream 结构可以更高效地管理消息序列

项目前期准备

通过上文可知,Spring AI 框架通过ChatMemoryRepositoryChatMemory抽象层实现聊天记忆及消息存储。那么 Spring AI 在 Chat Memory 上都做了哪些工作呢?

  • 如果我们想要自定义一个外部存储方式,首先需要实现 ChatMemoryRepository 接口。
  • 集成进 Spring AI 框架中,我们还需实现 自动配置 ChatMemoryRepository 的实现类 Bean 对象。

ChatMemoryRepository 接口提供了存储逻辑的统一抽象,这些方法会在模型交互时与 MessageWindowChatMemory 对象中产生调用:

java 复制代码
public interface ChatMemoryRepository {
    // 获取所有会话ID
	List<String> findConversationIds();
    // 获取指定会话ID的聊天消息
	List<Message> findByConversationId(String conversationId);
	// 存储整个会话ID的历史消息(替换式更新)
	void saveAll(String conversationId, List<Message> messages);
    // 清理指定会话ID中的聊天消息
	void deleteByConversationId(String conversationId);
}

项目准备阶段,创建新的项目模块,分别是:

spring-ai-model-chat-memory-repository-redis:ChatMemoryRepository的核心实现模块,Redis外部存储库的具体实现。

spring-ai-autoconfigure-model-chat-memory-repository-redis:ChatMemoryRepository的自动配置模块,包含Spring Boot自动配置类。

spring-ai-starter-model-chat-memory-repository-redis:Starter模块,提供开箱即用的依赖管理。

接下来让我们进行逐一实现。

ChatMemoryRepository的Redis实现

Redis 客户端有很多,这里我们采用更 spring 的方式来创建客户端,导入 spring-data-redis 依赖,结合 spring 通过 RedisTemplate 模板类控制与 Redis 服务的数据交互。

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

配置类定义

首先我们提供一个 Redis 客户端的 Config 配置类文件,配置写入数据时的需求参数,比如:管理 Redis 模板类、指定 key 前缀为"spring_ai_chat_memory:"、指定写入数据的 time-to-live 会话过期时间,参数可在应用配置中灵活调整,满足不同场景需求。

java 复制代码
public class RedisChatMemoryRepositoryConfig {
    private RedisTemplate<String, String> redisTemplate;
    private String keyPrefix;
    private long timeToLive;

    // ......
}

实现类定义

spring-ai-model-chat-memory-repository-redis 模块中新建 RedisChatMemoryRepository 类文件,并实现 ChatMemoryRepository 接口,完成聊天消息的存储、查询与删除,一个非常简单的增删改逻辑,重写方法如下:

根据会话 ID 删除消息

直接调用 redisTemplate.delete 方法删除 Redis 中对应键的数据,从而实现聊天记忆中会话消息的删除。

java 复制代码
@Override
public void deleteByConversationId(String conversationId) {
    Assert.hasText(conversationId, "conversationId cannot be null or empty");

    String key = config.getKeyPrefix() + conversationId;
    redisTemplate.delete(key);
}
保存消息

调用 deleteByConversationId 方法删除该会话原有的所有消息。使用 redisTemplate 的opsForList().rightPushAll 方法将这些 JSON 字符串以列表格式有序存入。若配置了过期时间,为该键设置对应的过期时长,实现自动清理不再使用的会话数据。

java 复制代码
@Override
public void saveAll(String conversationId, List<Message> messages) {
    Assert.hasText(conversationId, "conversationId cannot be null or empty");
    Assert.notNull(messages, "messages cannot be null");
    Assert.noNullElements(messages, "messages cannot contain null elements");

    String key = config.getKeyPrefix() + conversationId;

    deleteByConversationId(conversationId);

    List<String> messageJsons = messages.stream()
        .map(message -> {
            try {
                message.getMetadata().put("timestamp", Instant.now().toString());
                return objectMapper.writeValueAsString(message);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("Error serializing message", e);
            }
        })
        .toList();

    redisTemplate.opsForList().rightPushAll(key, messageJsons);
    if (config.getTimeToLive() > 0) {
        redisTemplate.expire(key, config.getTimeToLive(), TimeUnit.SECONDS);
    }
}
根据会话 ID 查询消息

根据传入的会话 ID 获取该键对应的所有消息字符串,并反序列化为 Message 对象,最终组成消息列表返回。

java 复制代码
@Override
public List<Message> findByConversationId(String conversationId) {
    Assert.hasText(conversationId, "conversationId cannot be null or empty");

    String key = config.getKeyPrefix() + conversationId;
    List<String> messageStrings = redisTemplate.opsForList().range(key, 0, -1);
    if (messageStrings == null) {
        logger.debug("No messages found for conversationId: " + conversationId);
        return List.of();
    }

    List<Message> messages = new ArrayList<>();
    for (String messageString : messageStrings) {
        try {
            JsonNode jsonNode = objectMapper.readTree(messageString);
            messages.add(getMessage(jsonNode));
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Error deserializing message", e);
        }
    }
    return messages;
}
检索会话 ID

通过 ScanOptions 扫描所有匹配特定前缀的键,避免使用 KEYS 命令可能导致的阻塞风险。在遍历扫描结果时,对键进行分割处理,提取出会话 ID,最终返回包含所有会话 ID 的列表。

java 复制代码
@Override
public List<String> findConversationIds() {
    return redisTemplate.execute((RedisCallback<List<String>>) connection -> {
            var keys = new HashSet<String>();
            ScanOptions options =
                ScanOptions.scanOptions()
                .match(String.format("*%s*", config.getKeyPrefix()))
                .count(Integer.MAX_VALUE)
                .build();

            try (Cursor<byte[]> cursor = connection.keyCommands().scan(options)) {
                while (cursor.hasNext()) {
                    String[] key = new String(cursor.next(), StandardCharsets.UTF_8).split(":");
                    if (key.length > 0) {
                        keys.add(key[key.length - 1]);
                    }
                }
            }
            return new ArrayList<>(keys);
        });
}

通过自定义 RedisChatMemoryRepository,我们已成功将 Redis 融入 Spring AI 的聊天记忆存储体系,实现了基于 Redis 的聊天记忆外部存储。

RedisChatMemoryRepository的自动配置实现

自动配置

spring-ai-autoconfigure-model-chat-memory-repository-redis 模块中新建 RedisChatMemoryRepositoryAutoConfiguration 类文件,利用 Spring 的自动配置机制,通过注解驱动的方式,实现 Redis 聊天记忆存储类的自动配置集成。

这里我们默认使用 Lettuce 作为 Redis 客户端,选择默认注入 "stringRedisTemplate" 模板类对象:

java 复制代码
// 需确保在 RedisAutoConfiguration 配置完成后、ChatMemoryAutoConfiguration 配置前执行
@AutoConfiguration(after = RedisAutoConfiguration.class, before = ChatMemoryAutoConfiguration.class)
@ConditionalOnClass({RedisChatMemoryRepository.class, RedisTemplate.class})
@EnableConfigurationProperties({RedisChatMemoryRepositoryProperties.class})
public class RedisChatMemoryRepositoryAutoConfiguration {

    // 设置为默认注入 "stringRedisTemplate" 模板类对象
    @Bean
    @ConditionalOnMissingBean
    public RedisChatMemoryRepository redisChatMemoryRepository(
            @Qualifier(RedisChatMemoryRepositoryProperties.DEFAULT_REDIS_TEMPLATE) RedisTemplate<String, String> redisTemplate,
            RedisChatMemoryRepositoryProperties properties) {

        return RedisChatMemoryRepository.builder()
                .keyPrefix(properties.getKeyPrefix())
                .timeToLive(properties.getTimeToLive())
                .redisTemplate(redisTemplate)
                .build();
    }
}

Starter模块

最后,提供一个 RedisChatMemoryRepository 的 Starter模块,提供开箱即用的依赖管理。

在模块依赖项中填入我们先前创建好的新模块:

xml 复制代码
<dependency>
    <groupId>org.example</groupId>
    <artifactId>spring-ai-autoconfigure-model-chat-memory-repository-redis</artifactId>
    <version>${project.parent.version}</version>
</dependency>

<dependency>
    <groupId>org.example</groupId>
    <artifactId>spring-ai-model-chat-memory-repository-redis</artifactId>
    <version>${project.parent.version}</version>
</dependency>

SpringBoot应用与大模型聊天记忆功能的交互、总结序列图如下:

sequenceDiagram participant App as SpringBoot应用 participant AC as ChatMemoryAutoConfiguration participant Context as ApplicationContext App->>Context: 启动上下文 activate Context Context->>AC: 处理自动配置 AC->>AC: 检查配置属性(spring.ai.chat.memory.*) alt 配置redis存储 AC->>RedisConfiguration: 创建RedisChatMemoryRepository else 配置jdbc存储 AC->>JdbcConfiguration: 创建JdbcChatMemoryRepository else 默认 AC->>InMemoryConfiguration: 创建InMemoryChatMemoryRepository end AC->>Context: 注册ChatMemoryRepository Bean AC->>Context: 使用Repository构建MessageWindowChatMemory deactivate Context App->>Context: 注入ChatMemory使用

实战案例实现

接下来我们使用MessageWindowChatMemory+ 自定义RedisChatMemoryRepository 的方式实现一个AI聊天助手的功能。

RedisChatMemoryRepository 是我们自定义的一个 Redis 聊天记忆外部存储,实现是基于 spring-data-redis 模块,我们可通过 RedisTemplate 配置连接 Redis 服务。

1、导入模块

在上篇文章的案例项目中,我们添加自定义的 Starter模块 依赖:

xml 复制代码
<dependency>
    <groupId>org.example</groupId>
    <artifactId>spring-ai-starter-model-chat-memory-repository-redis</artifactId>
    <version>${project.parent.version}</version>
</dependency>

2、配置参数

在 application.yml 中,添加配置 spring.aiRedisTemplate 连接配置参数:

yaml 复制代码
spring:
  application:
    name: AI Assistant
  ai:
    openai:
      api-key: ${OPENAI_API_KEY:NONE}
      base-url: ${OPENAI_BASE_URL:NONE}
      chat:
        options:
          model: DeepSeek-V3
        completions-path: /v1/chat/completions
    chat:
      memory:
        repository:
          redis:
            key-prefix: "my_chat_memory:"
            time-to-live: "7d"
  data:
    redis:
      host: "localhost"
      port: 6379
      database: 0
      # username:
      # password:
      connect-timeout: 30s
      timeout: 30s
      lettuce:
        pool:
          min-idle: 0
          max-idle: 10
          max-active: 20
          max-wait: -1ms

3、创建 Bean 实例

我们在文章上述实现中为 RedisChatMemoryRepository 提供了自动配置,可直接在应用中使用。

java 复制代码
@Configuration
public class GeneralChatClientConfig {

    private final RedisChatMemoryRepository redisChatMemoryRepository;

    public GeneralChatClientConfig(RedisChatMemoryRepository redisChatMemoryRepository) {
        this.redisChatMemoryRepository = redisChatMemoryRepository;
    }

    @Bean(name = "messageWindowChatMemoryWithRedis")
    public MessageWindowChatMemory messageWindowChatMemoryWithRedis() {
        int maxMessages = 20;
        return MessageWindowChatMemory.builder()
                .chatMemoryRepository(redisChatMemoryRepository) // default: new InMemoryChatMemoryRepository()
                .maxMessages(maxMessages)
                .build();
    }
}

4、聊天记忆客户端

结合 MessageWindowChatMemoryPromptChatMemoryAdvisor 方式创建新的 ChatClient,配置如下:

java 复制代码
    @Bean(name = "deepseekV3ClientWithRedis")
    public ChatClient deepseekV3ClientWithRedis(
            @Value("${spring.ai.openai.base-url}") String baseUrl,
            @Value("${spring.ai.openai.chat.options.model}") String modelName,
            @Value("${spring.ai.openai.api-key}") String apiKey,
            @Qualifier("messageWindowChatMemoryWithRedis") MessageWindowChatMemory messageWindowChatMemoryWithRedis) {

        OpenAiApi build = OpenAiApi.builder().apiKey(apiKey).baseUrl(baseUrl).build();

        OpenAiChatModel openAiChatModel =
                OpenAiChatModel.builder()
                        .openAiApi(build)
                        .defaultOptions(OpenAiChatOptions.builder().model(modelName).build())
                        .build();

        return ChatClient.builder(openAiChatModel)
                .defaultAdvisors(PromptChatMemoryAdvisor.builder(messageWindowChatMemoryWithRedis).build())
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }

调用 ChatClient 时,ChatMemoryAdvisor 将自动管理记忆存储。系统会根据指定的会话 ID 从记忆库检索历史对话。

我们在已有的 Controller 中添加一个 Redis 外部存储的 ChatClient 方法,注入配置好的 ChatClient,定义一个api实现大模型的 chat 调用,代码如下:

java 复制代码
@RestController
@RequestMapping("/ai/v3/chat/")
@Slf4j
public class GeneralChatController {

    private final ChatClient redisChatClient;

    public GeneralChatController(@Qualifier("deepseekV3ClientWithRedis") ChatClient redisChatClient) {
        this.redisChatClient = redisChatClient;
    }

    @PostMapping(value = "/t2", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
    @ResponseBody
    public Object chatWithRedis(@RequestBody String body) {
        JSONObject entries = JSONUtil.parseObj(body);
        String text = entries.getStr("text");
        Boolean stream = entries.getBool("stream", false);
        String conversationId = entries.getStr("conversationId");

        log.info("开始对话聊天,会话ID:{}", conversationId);
        var request =
                redisChatClient
                        .prompt()
                        .system("你是乐观小王,回答问题简练精要。")
                        .advisors(advisor -> advisor.param(CONVERSATION_ID, conversationId))
                        .advisors(new SimpleLoggerAdvisor())
                        .user(text);

        try {
            log.info("开始生成回答,是否流式输出:{}", stream);
            return stream ? request.stream().content() : request.call().content();
        } catch (Exception e) {
            log.error("对话聊天发生异常,会话ID:{}", conversationId, e);
            throw e;
        }
    }
}

5、测试AI聊天功能

通过上述的简单配置和代码编写,我们已经实现了一个通用的带有记忆功能的AI聊天助手。

接下来让我们测试一下这个聊天助手的使用效果,设置几条 USER 的提问内容,具体内容如下:

http 复制代码
# Openai Chat API
### 我的名字叫小明,你叫什么名字?
### 我是谁?
### 我们之间的第一句问话是什么?
POST http://localhost:10001/ai/v3/chat/t1
Content-Type: application/json

{
"text": "我的名字叫小明,你叫什么名字?",
"stream": false,
"conversationId": "bnA9f525-l7ae-5c66-ae21-vh53547c96cf"
}

聊天消息记录会被存储到 Redis 中,http顺序请求后的大模型返回结果 如图所示:

小总结

在探索 Spring AI 聊天记忆功能的过程中,我们自定义了基于 Redis 外部存储的实现。通过实现ChatMemoryRepository抽象层,我们能够将会话数据存储到 Redis 中。

不过,当前的实现并非尽善尽美。对于ToolMessage以及media媒体文件等问答形式,我们的自定义实现尚未提供支持。从官方的开发动态和社区讨论来看,新版本的 Spring AI 有望带来对 Redis 外部存储更为完善的实现。

文章案例项目代码:spring-ai-model-chat-memory-repository-redis

相关推荐
璞华Purvar5 分钟前
2025年全国技术贸易创新实践案例:AI编辑助手平台助力“一带一路”数字媒体产业高质量发展
大数据·人工智能·媒体
麓殇⊙26 分钟前
操作系统期末复习--操作系统初识以及进程与线程
java·大数据·数据库
大师兄带你刨AI1 小时前
「AI大数据」| 《华为:面向智能制造的工业大模型标准化研究报告》
大数据·人工智能
Elastic 中国社区官方博客1 小时前
使用 Elasticsearch 提升 Copilot 能力
大数据·数据库·elasticsearch·搜索引擎·全文检索·copilot·mcp
F36_9_2 小时前
如何高效实现公司文件管理
大数据·数据库·人工智能
武子康3 小时前
大数据-16-Hive HiveServer2 HS2 允许客户端远程执行HiveHQL HCatalog
大数据·后端·apache hive
白总Server4 小时前
Web 架构之 GraphQL 与 REST API 对比与选型
java·大数据·前端·javascript·后端·架构·graphql
IT技术范4 小时前
2025商旅平台排行:国内主流商旅平台解析
大数据·人工智能
天氰色等烟雨5 小时前
Spring AI 聊天记忆功能实战(一):从接口设计到生产实践
大数据