解决 Spring AI Chat Memory 持久化到 Redis 的序列化难题

前言

个人网站:lihainuo.com

代码仓库地址:github.com/leehainuo/s...

Spring AI 为 Java 程序员带来了开发AI应用的高效方式,小nuo这几天上手体验了感觉非常的不错,但是也发现了框架的不足,因为比较新吧,所以有一些功能还没实现,小nuo今天就是要写一篇文章,解决 Spring AI 持久化存储到 Redis!😎

构建可序列化的 Message 包装类

自定义 ChatMemory 并不难,难的就是Redis存储与读取时需要序列化与反序列化。 Spring AI 中的 Message 体系结构如下:

小nuo查看源码发现,SystemMessageUserMessage 等实现类均未实现 Serializable 接口,直接存储会导致序列化失败。解决方案是创建可序列化的包装类。

SerializableMessage类核心代码如下:

java 复制代码
// ...
/**
 * 可序列化的 Message 包装类
 */
@Getter
public class SerializableMessage implements Serializable {
    // 定义序列化版本号, 用于版本控制
    private static final long serialVersionUID = 1L;

    // 明确 Message 接口实现类的枚举
    public enum MessageType { USER, ASSISTANT, SYSTEM }

    private final MessageType messageType;
    private final String textContent;
    private final Map<String, Object> metadata;

    public SerializableMessage(Message message) {
        this.messageType = getMessageType(message);
        this.textContent = message.getText();
        this.metadata = message.getMetadata();
    }

    // 判断属于哪种具体的实现类的类型
    private MessageType getMessageType(Message message) {
        if (message instanceof SystemMessage) return MessageType.SYSTEM;
        if (message instanceof UserMessage) return MessageType.USER;
        if (message instanceof AssistantMessage) return MessageType.ASSISTANT;
        throw new IllegalArgumentException("Unknown message type: " + message.getClass().getName());
    }

    // 将 SerializableMessage 转换为 Message
    public Message toMessage() {
        switch (messageType) {
            case SYSTEM:
                return new SystemMessage(textContent);
            case ASSISTANT:
                return new AssistantMessage(textContent, metadata);
            case USER:
                return new UserMessage(textContent);
            default:
                throw new IllegalArgumentException("Unknown message type: " + messageType);
        }
    }

// ...

}

✨小nuo的逻辑思路:

  • 采用枚举明确定义消息类型,避免序列化时的类型丢失
  • 保留核心字段:文本内容和元数据
  • 提供双向转换方法,无缝对接原有 Message 体系

RedisTemplate 配置:关键在于序列化器

Spring Data Redis 的核心是 RedisTemplate,而序列化配置是实现分布式存储的关键。

首先引入Redis依赖:

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

配置配置文件连接Redis:

yaml 复制代码
spring:
    data:
        redis:
            host: localhost
            port: 6379

模板配置

模板配置为难点,分如下四个步骤实现:

  1. 声明自定义的RedisTemplate<String, SerializableMessage>模板实例 注意:Value 的泛型指定 SerializableMessage 类型
  2. 配置支持多态的 Jackson 映射器 原因:多态场景下(如父类引用指向子类对象),序列化时若不存储类型信息,反序列化会丢失 子类型细节,导致数据还原失败(ClassCastException异常)
  3. 创建自定义序列化工具 注意:自定义序列化工具基于上一步的 Jackson 映射器去创建
  4. Key使用String序列化:Value使用自定义序列化工具
java 复制代码
@Configuration
public class RedisConfig {

    // 声明自定义的 RedisTemplate - Spring AI 序列化器
    @Bean("AiRedisTemplate")
    public RedisTemplate<String, SerializableMessage> AiRedisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, SerializableMessage> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // 配置支持多态的 Jackson 序列化器
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.activateDefaultTyping(                 // 启用多态类型处理
                objectMapper.getPolymorphicTypeValidator(), // 获取多态类型验证器
                ObjectMapper.DefaultTyping.EVERYTHING,      // 指定类型信息添加范围:所有类型(包括 final 类型)
                JsonTypeInfo.As.PROPERTY                    // 指定类型信息添加方式:属性方式
        );

        // 创建自定义的序列化工具
        GenericJackson2JsonRedisSerializer serializer =
                new GenericJackson2JsonRedisSerializer(objectMapper);

        // Key 使用 String 序列化:value - 使用自定义的序列化工具
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        return template;
    }
}

实现 ChatMemory

自定义 ChatMemory 非常简单😎,通过查看源码可以看到实现 ChatMemory 需要实现其4个功能,我们只需要照着其中的 InMemoryChatMemory 重构功能,改造成使用 Redis 操作即可:

java 复制代码
/**
 * 自定义 Redis 存储 Memory
 * todo: 需要将 Message -> SerializableMessage
 */
public class RedisChatMemory implements ChatMemory {

    private final RedisTemplate<String, SerializableMessage> redisTemplate;

    /**
     * 构造器注入
     * @param redisTemplate Redis 模板
     */
    public RedisChatMemory(RedisTemplate<String, SerializableMessage> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void add(String conversationId, Message message) {
        // 转换数据
        SerializableMessage serializableMessage = new SerializableMessage(message);
        // 向 Redis 列表中添加数据 左侧添加
        redisTemplate.opsForList().leftPush(conversationId, serializableMessage);
    }

    @Override
    public void add(String conversationId, List<Message> messages) {
        // 转换数据
        List<SerializableMessage> serializableMessages = messages.stream()
                .map(SerializableMessage::new)
                .collect(Collectors.toList());
        // 向 Redis 列表中添加数据 左侧添加
        if (serializableMessages != null && !serializableMessages.isEmpty()) {
            redisTemplate.opsForList().leftPushAll(conversationId, serializableMessages);
        }
    }

    @Override
    public List<Message> get(String conversationId, int lastN) {
        // 需要将 SerializableMessage -> Message
        List<SerializableMessage> serializableMessages = redisTemplate.opsForList()
                .range(conversationId, 0, lastN - 1);
        // 处理空结果
        if (serializableMessages == null || serializableMessages.isEmpty()) {
            return List.of();
        }
        // 处理非空结果
        return serializableMessages.stream()
                .map(SerializableMessage::toMessage)
                .collect(Collectors.toList());
    }

    @Override
    public void clear(String conversationId) {
        // 清空 Redis 列表
        redisTemplate.delete(conversationId);
    }
}

然后给 ChatClient 配置即可:

java 复制代码
 ChatMemory chatMemory = new RedisChatMemory(redisTemplate);
    chatClient = ChatClient.builder(dashscopeChatModel)
        .defaultSystem(SYSTEM_PROMPT)
        .defaultAdvisors(
        new MessageChatMemoryAdvisor(chatMemory),
        // 自定义日志 Advisor 可按需开启
        new CustomLoggerAdvisor()
        // 自定义增强推理能力 Advisor 可按需开启
        // new ReReadingAdvisor()
        )
        .build();

总结

至此就完成了 Redis的序列化难题,小nuo自己做了一个Starter供大家快捷使用与学习,如果能给小nuo一个star⭐真的万分感激!!!有什么疑问与更好的方法,小nuo欢迎大家在评论区一起讨论。

仓库地址:github.com/leehainuo/s...

相关推荐
七七&5569 分钟前
Spring全面讲解(无比详细)
android·前端·后端
Java水解12 分钟前
【MySQL基础】MySQL复合查询全面解析:从基础到高级应用
后端·mysql
手握风云-14 分钟前
JavaEE初阶第九期:解锁多线程,从 “单车道” 到 “高速公路” 的编程升级(七)
java·开发语言
码农小灰19 分钟前
单体VS微服务:如何选择最适合的架构?
java·微服务·架构
小马哥聊DevSecOps33 分钟前
将 RustFS 用作 GitLab 对象存储后端
后端
生无谓34 分钟前
java中的异常
后端
参宿四南河三37 分钟前
还在使用 Java 8 语法?Java实用新特性来一波
java
lifallen38 分钟前
Paimon INSERT OVERWRITE
java·大数据·数据库·flink
鼠鼠我捏,要死了捏40 分钟前
Java并发编程性能优化实践指南:锁分离与无锁设计
java·concurrency·performance-optimization
哪个旮旯的啊44 分钟前
你要的synchronized锁升级与降级这里都有
java