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组合则是企业级落地的最佳方案------它解决了默认内存存储的痛点,实现了会话记忆的分布式共享、持久化存储、高性能序列化

相关推荐
还是鼠鼠16 小时前
【Spring AI智能体实战 02】Spring Boot 3.4整合Spring AI:项目创建与依赖配置
java·人工智能·spring boot·maven·spring ai
还是鼠鼠2 天前
【Spring AI智能体实战 01】Java开发者接入大模型:国内平台、Token与API Key入门
java·大模型·spring ai·deepseek·阿里云百炼
默辨4 天前
Spring AI Alibaba 核心知识点
java·ai·spring ai·spring alibaba
Muscleheng7 天前
Spring Boot 3.x 集成 DeepSeek 实现 Function Calling(工具调用)
人工智能·spring boot·后端·ai·spring ai·deepseek
佛祖让我来巡山8 天前
SpringAI保姆级实战教程
spring ai·springai·springai教程·springai入门
BerryS3N8 天前
Java 后端转型大模型:Demo 能跑不等于能上线
java·人工智能·python·java后端·spring ai·langchain4j·大模型转型
lichuangcsdn10 天前
【Spring AI 学习(三)】实现简单的对话
java·人工智能·学习·spring·spring ai
lichuangcsdn11 天前
【Spring AI 学习(一)】spring ai是什么
人工智能·学习·spring·spring ai
码上解惑15 天前
从 Dify 工作流说起:常用节点怎么选、怎样组合?
java·人工智能·ai·agent·dify·智能体·spring ai
荣--16 天前
业务智能体实战笔记:分层消除不确定性(五·终)|完全脱离 LLM 的确定性判定 + 评测闭环
ai agent·spring ai·业务智能体