在Spring AI多轮对话开发中,有一个高频痛点:LLM天然无状态,无法记住上下文。该框架下提供的ChatMemory体系的核心接口,就是解决这个问题;而ChatMemory Advisor(记忆顾问)则是"无感记忆"的关键,它能自动拦截对话请求、注入历史上下文、保存新消息,无需手动处理会话状态。
今天就给大家带来一套企业级落地方案:Spring AI ChatMemory Advisor + Redis分布式存储 + Kryo高效序列化,实现会话记忆持久化、高可用、高性能,数据不丢失、跨节点共享。
一、先搞懂:ChatMemory Advisor到底是什么?
很多人用不好记忆功能,本质是没搞懂三层架构的分工,而ChatMemory Advisor正是串联起整个体系的"桥梁"。
1. Spring AI记忆三层架构
Spring AI的记忆系统采用"策略+存储+拦截"的分层设计,解耦清晰,便于扩展,三层各司其职:
- Advisor层(拦截层):核心就是ChatMemory Advisor,相当于"拦截器",负责拦截ChatClient的请求和响应,自动从存储中读取历史记忆注入请求,再将新的对话消息保存到存储中,全程无感。
- ChatMemory层(策略层):定义记忆的管理策略,比如最常用的MessageWindowChatMemory(滑动窗口策略),负责决定保留哪些历史消息、淘汰哪些消息(比如只保留最近20条,避免Token溢出)。
- 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序列化是否正常:
-
启动Redis、Spring Boot服务;
-
发起两次请求,模拟多轮对话:
-
第一次请求:POST /chat?conversationId=user123,Body:"我叫张三,帮我查一下最近的订单",AI响应后,消息会被Kryo序列化后存入Redis;
-
第二次请求:POST /chat?conversationId=user123,Body:"这个订单能取消吗",AI会记住上一轮的"张三"和"订单"信息,直接响应,无需再次询问;
-
-
重启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组合则是企业级落地的最佳方案------它解决了默认内存存储的痛点,实现了会话记忆的分布式共享、持久化存储、高性能序列化。