Spring AI实战|ChatMemory Advisor记忆优化:Redis + Kryo持久化方案

在Spring AI多轮对话开发中,有一个高频痛点:LLM天然无状态,无法记住上下文。该框架下提供的ChatMemory体系的核心接口,就是解决这个问题;而ChatMemory Advisor(记忆顾问)则是"无感记忆"的关键,它能自动拦截对话请求、注入历史上下文、保存新消息,无需手动处理会话状态。

今天就给大家带来一套企业级落地方案:Spring AI ChatMemory Advisor + Redis分布式存储 + Kryo高效序列化,实现会话记忆持久化、高可用、高性能,数据不丢失、跨节点共享。

一、先搞懂:ChatMemory Advisor到底是什么?

很多人用不好记忆功能,本质是没搞懂三层架构的分工,而ChatMemory Advisor正是串联起整个体系的"桥梁"。

1. Spring AI记忆三层架构

Spring AI的记忆系统采用"策略+存储+拦截"的分层设计,解耦清晰,便于扩展,三层各司其职:

  1. Advisor层(拦截层):核心就是ChatMemory Advisor,相当于"拦截器",负责拦截ChatClient的请求和响应,自动从存储中读取历史记忆注入请求,再将新的对话消息保存到存储中,全程无感。
  2. ChatMemory层(策略层):定义记忆的管理策略,比如最常用的MessageWindowChatMemory(滑动窗口策略),负责决定保留哪些历史消息、淘汰哪些消息(比如只保留最近20条,避免Token溢出)。
  3. ChatMemoryRepository层(存储层):负责记忆的持久化存储,默认是InMemoryChatMemoryRepository(内存存储),需要替换成Redis实现分布式存储,解决内存存储的痛点。

简单来说:ChatMemory Advisor负责"自动干活",ChatMemory负责"怎么干活",ChatMemoryRepository负责"把活的结果存起来"。

2. 为什么需要Redis+Kryo组合?

默认方案的痛点的很明显,企业级场景必须解决:

  • 默认InMemory存储:服务重启、扩缩容后,会话记忆全部丢失,用户体验极差;
  • 默认序列化(Jackson):Spring AI的Message是接口,有多个子类(UserMessage、SystemMessage 等),Jackson序列化时需要额外处理类型信息,效率低、易出错;
  • 分布式部署:多节点部署时,内存存储无法共享会话,导致同一用户在不同节点对话时,上下文断裂。

而Redis+Kryo组合正好解决这些问题:

  • Redis:分布式存储,支持跨节点共享会话,持久化机制(RDB/AOF)确保记忆不丢失;
  • Kryo:高性能序列化框架,比Jackson快5-10倍,支持接口、无参构造函数的类序列化,完美适配Spring AI的Message接口。

二、Redis+Kryo持久化完整实现

第一步:引入核心依赖

需要引入Spring AI核心、Redis存储、Kryo序列化、Redis客户端等依赖:

xml 复制代码
<properties>
    <java.version>17</java.version>
    <spring-ai.version>1.0.0</spring-ai.version>
    <kryo.version>5.6.2</kryo.version>
</properties>

<dependencies>
    <!-- Spring Boot 核心 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring AI 核心 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-core</artifactId>
        <version>${spring-ai.version}</version>
    </dependency>

    <!-- Spring AI Chat 客户端 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-chat-client</artifactId>
        <version>${spring-ai.version}</version>
    </dependency>

    <!-- Redis 存储依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- Kryo 序列化 -->
    <dependency>
        <groupId>com.esotericsoftware</groupId>
        <artifactId>kryo</artifactId>
        <version>${kryo.version}</version>
    </dependency>

    <!-- Redis 客户端(Jedis) -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>5.2.0</version>
    </dependency>

    <!-- 可选:Kryo 连接池(提升性能) -->
    <dependency>
        <groupId>cn.jizuiba</groupId>
        <artifactId>kryo-pool-spring-boot-starter</artifactId>
        <version>1.0.0</version>
        <!-- 注意:此依赖未上传中央仓库,需手动下载安装到本地仓库 -->
    </dependency>
</dependencies>

注意:kryo-pool-spring-boot-starter依赖未上传中央仓库,可通过 GitHub 下载 后,手动安装到本地Maven仓库。

第二步:配置Redis + Kryo序列化

在application.yml中配置Redis连接信息、Kryo连接池参数,以及Spring AI的记忆相关配置:

yaml 复制代码
spring:
  application:
    name: spring-ai-chatmemory-demo
  # Redis 配置
  redis:
    host: localhost  # 本地Redis地址,生产环境替换为实际地址
    port: 6379       # 默认端口
    # password: 123456  # 有密码则配置,无密码可省略
    timeout: 4000    # 连接超时时间(毫秒)
    jedis:
      pool:
        max-active: 8  # 连接池最大活跃连接数
        max-idle: 4    # 连接池最大空闲连接数
        min-idle: 2    # 连接池最小空闲连接数
  # Spring AI 配置(以通义千问为例,可替换为GPT、DeepSeek等)
  ai:
    qwen:
      api-key: ${QWEN_API_KEY}  # 自己的API_KEY
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
    # ChatMemory 配置
    chat:
      memory:
        redis:
          enabled: true  # 启用Redis存储记忆
        window:
          size: 20       # 滑动窗口大小,保留最近20条消息

# Kryo 连接池配置(提升序列化性能)
kryo:
  pool:
    max-total: 16       # 池中最大对象数量
    max-idle: 8         # 池中最大空闲对象数量
    min-idle: 2         # 池中最小空闲对象数量
    max-wait-millis: 3000  # 获取对象时的最大等待时间(毫秒)
    test-while-idle: true  # 空闲时验证对象有效性
    time-between-eviction-runs-millis: 30000  # 空闲对象检测间隔(毫秒)
    min-evictable-idle-time-millis: 60000     # 对象空闲最小时间(毫秒),超时可回收
    

第三步:实现Kryo序列化工具类

Spring AI的Message是接口,Kryo默认不支持接口序列化,需要自定义序列化工具类,处理Message接口及其子类的序列化/反序列化,同时避免线程安全问题(Kryo实例线程不安全,建议用局部实例或连接池):

java 复制代码
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.springframework.ai.chat.messages.Message;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;

/**
 * Kryo 序列化工具类,专门处理 Spring AI Message 接口序列化
 */
@Component
public class KryoMessageSerializer {

    /**
     * 序列化:将 Message 对象转为 Base64 字符串(便于 Redis 存储)
     */
    public String serialize(Message message) {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             Output output = new Output(baos)) {
            // 每次序列化创建新的 Kryo 实例,避免线程安全问题
            Kryo kryo = new Kryo();
            // 不要求强制注册类(简化配置,生产环境可按需注册提升性能)
            kryo.setRegistrationRequired(false);
            // 设置实例化策略,支持无参构造函数的类
            kryo.setInstantiatorStrategy(new com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy());
            // 写入对象(包含类信息,便于反序列化)
            kryo.writeClassAndObject(output, message);
            output.flush();
            // 转为 Base64 字符串,避免二进制数据存储异常
            return Base64.getEncoder().encodeToString(baos.toByteArray());
        } catch (IOException e) {
            throw new RuntimeException("Kryo 序列化 Message 失败", e);
        }
    }

    /**
     * 反序列化:将 Base64 字符串转为 Message 对象
     */
    public Message deserialize(String base64Str) {
        try (ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(base64Str));
             Input input = new Input(bais)) {
            Kryo kryo = new Kryo();
            kryo.setRegistrationRequired(false);
            kryo.setInstantiatorStrategy(new com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy());
            // 读取对象,强转为 Message 类型
            Object object = kryo.readClassAndObject(input);
            return (Message) object;
        } catch (IOException e) {
            throw new RuntimeException("Kryo 反序列化 Message 失败", e);
        }
    }
}
    

第四步:自定义Redis 记忆仓库(集成Kryo)

Spring AI提供了RedisChatMemoryRepository的基础接口,但默认使用Jackson序列化,需要自定义实现,替换为Kryo序列化,同时实现会话隔离、消息读写等核心功能:

java 复制代码
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.messages.Message;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * 自定义 Redis 记忆仓库,使用 Kryo 序列化
 */
@Component
public class KryoRedisChatMemoryRepository implements ChatMemoryRepository {

    // Redis 键前缀,格式:chat:memory:{conversationId}
    private static final String REDIS_KEY_PREFIX = "chat:memory:";

    private final StringRedisTemplate stringRedisTemplate;
    private final KryoMessageSerializer kryoSerializer;

    // 注入 Redis 模板和 Kryo 序列化工具
    public KryoRedisChatMemoryRepository(StringRedisTemplate stringRedisTemplate,
                                         KryoMessageSerializer kryoSerializer) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.kryoSerializer = kryoSerializer;
    }

    /**
     * 保存会话消息(追加到 Redis List 中)
     * @param conversationId 会话ID(唯一标识,如用户ID+会话编号)
     * @param messages 要保存的消息列表
     */
    @Override
    public void add(String conversationId, List<Message> messages) {
        String redisKey = getRedisKey(conversationId);
        // 序列化每条消息,批量添加到 Redis List
        List<String> serializedMessages = messages.stream()
                .map(kryoSerializer::serialize)
                .toList();
        stringRedisTemplate.opsForList().rightPushAll(redisKey, serializedMessages);
    }

    /**
     * 获取会话的所有历史消息
     * @param conversationId 会话ID
     * @return 反序列化后的消息列表
     */
    @Override
    public List<Message> get(String conversationId) {
        String redisKey = getRedisKey(conversationId);
        // 从 Redis List 中获取所有消息
        List<String> serializedMessages = stringRedisTemplate.opsForList().range(redisKey, 0, -1);
        if (serializedMessages == null || serializedMessages.isEmpty()) {
            return new ArrayList<>();
        }
        // 反序列化为 Message 对象
        return serializedMessages.stream()
                .map(kryoSerializer::deserialize)
                .filter(Objects::nonNull)
                .toList();
    }

    /**
     * 清除会话记忆
     * @param conversationId 会话ID
     */
    @Override
    public void clear(String conversationId) {
        String redisKey = getRedisKey(conversationId);
        stringRedisTemplate.delete(redisKey);
    }

    /**
     * 构建 Redis 键(避免键冲突)
     */
    private String getRedisKey(String conversationId) {
        return REDIS_KEY_PREFIX + conversationId;
    }
}
    

第五步:配置ChatMemory Advisor + 滑动窗口策略

将自定义的Redis记忆仓库、Kryo序列化集成到ChatMemory体系,配置滑动窗口策略(MessageWindowChatMemory),并创建ChatMemory Advisor,实现会话记忆的自动拦截和管理:

java 复制代码
import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor;
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;

/**
 * ChatMemory 核心配置:Advisor + 滑动窗口 + Redis 仓库
 */
@Configuration
public class ChatMemoryConfig {

    private final KryoRedisChatMemoryRepository redisChatMemoryRepository;

    public ChatMemoryConfig(KryoRedisChatMemoryRepository redisChatMemoryRepository) {
        this.redisChatMemoryRepository = redisChatMemoryRepository;
    }

    /**
     * 1. 配置滑动窗口 ChatMemory(策略层)
     * 滑动窗口:保留最近 N 条消息,避免 Token 溢出
     */
    @Bean
    public ChatMemory messageWindowChatMemory() {
        return MessageWindowChatMemory.builder()
                .chatMemoryRepository(redisChatMemoryRepository) // 关联 Redis 存储
                .windowSize(20) // 窗口大小,与 yml 配置一致
                .build();
    }

    /**
     * 2. 配置 ChatMemory Advisor(拦截层)
     * 自动拦截 ChatClient 请求,注入历史记忆、保存新消息
     */
    @Bean
    public PromptChatMemoryAdvisor promptChatMemoryAdvisor() {
        // 将 ChatMemory 注入 Advisor,实现自动记忆管理
        return PromptChatMemoryAdvisor.builder(messageWindowChatMemory())
                .build();
    }
}
    

第六步:创建ChatClient,测试记忆功能

配置ChatClient,将ChatMemory Advisor注入,实现多轮对话的记忆功能,测试Redis持久化和Kryo序列化是否生效:

java 复制代码
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.qwen.QwenChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * ChatClient 配置,注入记忆顾问
 */
@Configuration
public class ChatClientConfig {

    private final PromptChatMemoryAdvisor promptChatMemoryAdvisor;
    private final QwenChatModel qwenChatModel;

    public ChatClientConfig(PromptChatMemoryAdvisor promptChatMemoryAdvisor,
                            QwenChatModel qwenChatModel) {
        this.promptChatMemoryAdvisor = promptChatMemoryAdvisor;
        this.qwenChatModel = qwenChatModel;
    }

    @Bean
    public ChatClient chatClient() {
        return ChatClient.builder(qwenChatModel)
                .defaultAdvisors(promptChatMemoryAdvisor) // 注入记忆顾问
                .build();
    }
}
    

第七步:编写测试接口,验证效果

编写Controller,模拟多轮对话,测试会话记忆是否持久化(服务重启后仍能获取历史对话):

java 复制代码
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class ChatMemoryController {

    private final ChatClient chatClient;

    public ChatMemoryController(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    /**
     * 多轮对话接口,conversationId 用于区分不同会话
     * @param conversationId 会话ID(如用户ID)
     * @param message 用户消息
     * @return AI 响应(包含上下文记忆)
     */
    @PostMapping("/chat")
    public String chat(@RequestParam String conversationId,
                       @RequestBody String message) {
        // 调用 ChatClient,自动注入历史记忆
        return chatClient.prompt()
                .conversationId(conversationId) // 关联会话ID,实现会话隔离
                .user(message)
                .call()
                .getResult()
                .getOutput()
                .getContent();
    }
}
    

三、测试验证:确保记忆持久化生效

测试步骤简单,只需3步,验证Redis持久化和Kryo序列化是否正常:

  1. 启动Redis、Spring Boot服务;

  2. 发起两次请求,模拟多轮对话:

    • 第一次请求:POST /chat?conversationId=user123,Body:"我叫张三,帮我查一下最近的订单",AI响应后,消息会被Kryo序列化后存入Redis;

    • 第二次请求:POST /chat?conversationId=user123,Body:"这个订单能取消吗",AI会记住上一轮的"张三"和"订单"信息,直接响应,无需再次询问;

  3. 重启Spring Boot服务,再次发起请求:POST /chat?conversationId=user123,Body:"取消订单的进度怎么样了",AI仍能记住之前的对话,证明记忆持久化生效。

四、避坑指南+性能提升

1. 会话ID设计(关键)

conversationId必须唯一,建议采用"用户ID + 会话编号"的格式(如 user123:session001),避免不同用户的会话记忆冲突;同时可结合 UserContextHolder获取当前登录用户ID,实现会话自动关联。

2. Redis 优化

  • 设置消息过期时间:会话记忆无需永久保存,可给Redis键设置过期时间(如24小时),避免内存溢出;

  • 分布式锁:多节点部署时,避免消息并发读写冲突,可给Redis操作添加分布式锁;

  • 避免keys命令:生产环境中,不要使用keys chat:memory:* 命令查询所有会话,会阻塞Redis,建议用Redis扫描(scan)命令。

3. Kryo 优化

  • 使用Kryo连接池:频繁创建Kryo实例会影响性能,可使用kryo-pool-spring-boot-starter提供的连接池,复用Kryo实例;

  • 强制注册类:生产环境可提前注册Message接口的所有子类(UserMessage、SystemMessage 等),提升序列化效率,避免动态类加载的开销;

  • 异常处理:增加序列化/反序列化的异常捕获,避免单个消息序列化失败导致整个会话异常。

4. 滑动窗口优化

窗口大小(windowSize)需根据模型的上下文窗口调整,比如GPT-4o上下文窗口较大,可设置为30-50;Qwen-7B可设置为15-20,避免Token消耗过大。

五、总结

Spring AI的ChatMemory Advisor是实现多轮对话记忆的核心,而Redis+Kryo组合则是企业级落地的最佳方案------它解决了默认内存存储的痛点,实现了会话记忆的分布式共享、持久化存储、高性能序列化

相关推荐
中间件XL2 天前
ai-agent框架spring ai/alibaba原理源码分析(三) 外部调用III-skills
ai agent·calling·spring ai·springaialibaba·skills
大龄码农有梦想4 天前
Spring AI Alibaba和CrewAI:多智能体开源框架对比与选型
spring ai·crewai·多智能体协作框架·spring ai阿里巴巴·spring ai aliba·java智能体框架·python智能体框架
海兰5 天前
【第39篇】spring-ai-alibaba-graph-example学习路径概览
人工智能·spring boot·学习·spring·spring ai
海兰7 天前
【第35篇】文本摘要微服务
人工智能·spring boot·微服务·架构·spring ai
梵得儿SHI8 天前
(第三篇)Spring AI 架构设计与优化:容器化与云原生部署,基于 K8s 的 AI 应用全生命周期管理
java·ci/cd·docker·云原生·kubernetes·容器化·spring ai
行者-全栈开发9 天前
GPT-4 vs Claude vs 通义千问:Spring AI 接入三大模型对比测评(2026最新)
claude·通义千问·spring ai·企业级开发·chatmodel·大模型 api·多模型切换
海兰9 天前
【第32篇】场景示例项目
人工智能·spring boot·状态模式·spring ai
海兰11 天前
【第27篇】Micrometer + Zipkin
人工智能·spring boot·alibaba·spring ai
海兰11 天前
【第28篇】可观测性实战:LangFuse 方案详解
人工智能·spring boot·alibaba·spring ai