Spring AI Alibaba 记忆持久化到 Redis 实战指南
基于 Spring AI Alibaba 1.1.2.0,实现 Agent 短期记忆的 Redis 持久化,解决多轮对话中的上下文管理与状态恢复问题。
Spring AI Alibaba 记忆(Memory)机制详解与完整实战指南:
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/162238591
上述会话记忆基于内存实现,下面基于redis实现。
一、引言
在构建智能对话 Agent 时,记忆能力是核心功能之一。Spring AI Alibaba 提供了灵活的短期记忆(Short‑term Memory)管理机制,通过 Checkpointer 将 Agent 的状态(包括对话历史)持久化到外部存储,从而实现会话级别的记忆隔离与恢复。使用 Redis 作为存储介质,不仅可以共享记忆到多个服务实例,还能避免应用重启导致的数据丢失。
二、环境要求
- JDK:17+
- Spring Boot:3.2.5
- Spring AI Alibaba:1.1.2.0
- Redis:6.x / 7.x(Windows 可选用 Memurai 或 WSL2)
- Maven:3.6+
- DashScope API Key(用于调用通义千问等模型)
三、核心概念
3.1 短期记忆(Short‑term Memory)
短期记忆让 Agent 在同一个会话(threadId)中记住之前的交互内容。在 Spring AI Alibaba 中,记忆以 Agent 状态(OverAllState)的形式存储在 Graph 的执行上下文中。
3.2 Checkpointer
Checkpointer 负责将 Agent 状态持久化到外部存储。框架提供了多种实现:
MemorySaver:内存存储,适合开发调试。RedisSaver:Redis 存储,适合生产环境。
3.3 StateSerializer
状态序列化器,将 Java 对象序列化为字节数组存储到 Redis。框架默认使用 SpringAIJacksonStateSerializer,基于 Jackson 实现 JSON 序列化。
3.4 消息修剪(Message Trimming)
为防止长对话超出 LLM 的上下文窗口,可以通过 MessagesModelHook 在调用模型前修剪消息列表(例如保留首条和最近 N 条),或使用消息摘要(Summarization)压缩历史。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
四、完整实现步骤
4.1 创建 Maven 项目
pom.xml
注意:必须管理 Jackson 版本 ,避免与框架依赖冲突导致 NoSuchMethodError。
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example.ai</groupId>
<artifactId>spring-ai-redis-memory-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<spring-ai-alibaba.version>1.1.2.0</spring-ai-alibaba.version>
<!-- 统一 Jackson 版本,避免 NoSuchMethodError -->
<jackson.version>2.17.2</jackson.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${jackson.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI Alibaba Agent Framework -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-agent-framework</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
<!-- DashScope Starter (用于模型调用) -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
<!-- Memory Redis Starter (提供 RedisSaver) -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-memory-redis</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
<!-- 显式声明 Jackson 组件(版本由 BOM 管理) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<!-- Redisson (Redis 客户端) -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.52.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
关键点 :通过
dependencyManagement引入jackson-bom确保所有 Jackson 模块版本一致,避免序列化方法签名不兼容。
4.2 配置文件 application.yml
yaml
server:
port: 885
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY} # 从环境变量读取
chat:
options:
model: deepseek-v4-flash # 或 qwen-max
data:
redis:
host: localhost
port: 6379
password: 123456 # 若无密码则留空
timeout: 5000 # 毫秒,不要加 "ms" 后缀
logging:
level:
com.alibaba.cloud.ai: debug
4.3 Java 源码实现
4.3.1 启动类
java
package com.badao.ai;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringAiDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAiDemoApplication.class, args);
}
}
4.3.2 Redis 配置(生成 RedissonClient)
java
package com.badao.ai.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host:localhost}")
private String redisHost;
@Value("${spring.data.redis.port:6379}")
private int redisPort;
@Value("${spring.data.redis.password:}")
private String redisPassword;
@Value("${spring.data.redis.timeout:5000}")
private int timeout;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
String address = "redis://" + redisHost + ":" + redisPort;
config.useSingleServer()
.setAddress(address)
.setTimeout(timeout)
.setConnectionPoolSize(10)
.setConnectionMinimumIdleSize(2);
if (redisPassword != null && !redisPassword.isEmpty()) {
config.useSingleServer().setPassword(redisPassword);
}
return Redisson.create(config);
}
}
4.3.3 Agent 配置(使用 RedisSaver)
java
package com.badao.ai.config;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.checkpoint.savers.redis.RedisSaver;
import com.alibaba.cloud.ai.graph.serializer.StateSerializer;
import com.alibaba.cloud.ai.graph.serializer.plain_text.jackson.SpringAIJacksonStateSerializer;
import com.alibaba.cloud.ai.graph.state.AgentStateFactory;
import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy;
import com.badao.ai.hook.MessageTrimmingHook;
import org.redisson.api.RedissonClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AgentConfig {
@Bean
public ReactAgent reactAgent(ChatModel chatModel, RedissonClient redissonClient) {
// 1. 创建状态工厂
AgentStateFactory<OverAllState> stateFactory = (inputs) -> {
OverAllState state = new OverAllState();
state.registerKeyAndStrategy("messages", new ReplaceStrategy());
state.input(inputs);
return state;
};
// 2. 创建序列化器
StateSerializer stateSerializer = new SpringAIJacksonStateSerializer(stateFactory);
// 3. 使用 Builder 构建 RedisSaver(注意方法名是 redisson,不是 redissonClient)
RedisSaver redisSaver = RedisSaver.builder()
.redisson(redissonClient) // ✅ 正确的方法名
.stateSerializer(stateSerializer)
.build();
// 4. 构建 Agent
return ReactAgent.builder()
.name("redis_memory_agent")
.model(chatModel)
.saver(redisSaver)
.hooks(new MessageTrimmingHook()) // 可选的消息修剪
.build();
}
}
注意事项:
RedisSaver.builder().redisson(...)而非redissonClient(...)。StateSerializer不是泛型类,无需写StateSerializer<?>。- 必须注册
messages字段的更新策略(如ReplaceStrategy),否则 Agent 无法正确存储对话历史。
4.3.4 消息修剪 Hook(可选)
java
package com.badao.ai.hook;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand;
import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook;
import com.alibaba.cloud.ai.graph.agent.hook.messages.UpdatePolicy;
import org.springframework.ai.chat.messages.Message;
import java.util.ArrayList;
import java.util.List;
@HookPositions({HookPosition.BEFORE_MODEL})
public class MessageTrimmingHook extends MessagesModelHook {
private static final int MAX_MESSAGES = 5;
@Override
public String getName() {
return "message_trimming";
}
@Override
public AgentCommand beforeModel(List<Message> previousMessages, RunnableConfig config) {
if (previousMessages.size() <= MAX_MESSAGES) {
return new AgentCommand(previousMessages);
}
// 保留第一条和最近3条
Message firstMsg = previousMessages.get(0);
List<Message> recentMessages = previousMessages.subList(
previousMessages.size() - 3, previousMessages.size()
);
List<Message> trimmed = new ArrayList<>();
trimmed.add(firstMsg);
trimmed.addAll(recentMessages);
return new AgentCommand(trimmed, UpdatePolicy.REPLACE);
}
}
4.3.5 服务层
java
package com.badao.ai.service;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.stereotype.Service;
@Service
public class AgentService {
private final ReactAgent reactAgent;
public AgentService(ReactAgent reactAgent) {
this.reactAgent = reactAgent;
}
public String chat(String userMessage, String sessionId) {
RunnableConfig config = RunnableConfig.builder()
.threadId(sessionId) // 会话隔离
.build();
AssistantMessage response = reactAgent.call(userMessage, config);
return response.getText();
}
}
4.3.6 控制器
java
package com.badao.ai.controller;
import com.badao.ai.service.AgentService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/api/agent")
public class AgentController {
private final AgentService agentService;
public AgentController(AgentService agentService) {
this.agentService = agentService;
}
@PostMapping("/chat/session")
public Map<String, Object> chatWithSession(
@RequestParam String message,
@RequestParam String sessionId) {
String response = agentService.chat(message, sessionId);
return Map.of(
"success", true,
"response", response,
"sessionId", sessionId
);
}
}
五、常见问题与解决方案
5.1 RedisSaver 构造方法 protected 访问错误
现象 :'RedisSaver(RedissonClient, StateSerializer)' has protected access
原因 :直接使用 new RedisSaver(...),但构造方法被设计为 protected。
解决 :使用 Builder 模式:RedisSaver.builder().redisson(...).stateSerializer(...).build()。
5.2 Cannot resolve method 'redissonClient' in 'Builder'
现象 :编译时找不到 redissonClient() 方法。
原因 :Builder 中的方法名是 redisson,不是 redissonClient。
解决 :改为 .redisson(redissonClient)。
5.3 NoSuchMethodError: ObjectMapper.treeToValue(TreeNode, TypeReference)
现象 :调用接口时报 NoSuchMethodError,堆栈指向 SerializationHelper.deserializeMetadata。
原因 :项目中 Jackson 版本不一致,框架编译时依赖的 Jackson 版本与实际运行时的版本不兼容(例如 2.15.4 与 2.17.2 方法签名不同)。
解决 :在 pom.xml 中通过 dependencyManagement 统一 Jackson 版本至 2.17.2(或与 Spring Boot 默认版本一致)。详见本文 pom.xml 配置。
5.4 NumberFormatException: For input string: "5000ms"
现象 :启动时 RedisConfig 字段注入失败。
原因 :application.yml 中 timeout: 5000ms 带有单位,但字段类型是 int,无法解析。
解决 :去掉 "ms" 后缀,直接写 timeout: 5000(单位默认为毫秒)。
5.5 记忆未生效
现象:每次请求 Agent 都像第一次对话,无法记住之前内容。
原因:
- 未配置
saver(Checkpointer)。 - 每次请求
threadId不一致。 StateSerializer未正确注册,无法反序列化状态。
解决:
- 确认 Agent 构建时调用了
.saver(redisSaver)。 - 确保客户端请求携带相同的
sessionId(即threadId)。 - 检查日志,确认 Redis 中已存储状态数据(可用
redis-cli KEYS '*'查看)。
六、验证测试
6.1 启动 Redis 和 Spring Boot 应用
Windows(解压版):
cmd
cd C:\Redis
redis-server.exe redis.windows.conf
启动 Spring Boot:
bash
export DASHSCOPE_API_KEY="你的API密钥"
mvn spring-boot:run
6.2 测试多轮对话
bash
# 第一轮:告诉名字
curl -X POST "http://localhost:885/api/agent/chat/session?message=你好,我叫张三&sessionId=test-001"
# 第二轮:询问名字(应能记住)
curl -X POST "http://localhost:885/api/agent/chat/session?message=我叫什么名字?&sessionId=test-001"
# 第三轮:新会话,记忆隔离
curl -X POST "http://localhost:885/api/agent/chat/session?message=我叫什么名字?&sessionId=test-002"
6.3 查看 Redis 存储的状态
cmd
redis-cli
127.0.0.1:6379> KEYS *
# 例如:agent:test-001:state
127.0.0.1:6379> GET "agent:test-001:state"

七、总结
通过本文,你学会了如何利用 Spring AI Alibaba 的 RedisSaver 将 Agent 的短期记忆持久化到 Redis,解决了多轮对话的上下文管理问题。重点包括:
- 使用
RedisSaver.builder()构建 Checkpointer,注意方法名redisson。 - 统一 Jackson 版本避免序列化冲突。
- 通过
threadId隔离不同会话的记忆。 - 可选地添加
MessagesModelHook控制上下文长度。
此方案可应用于生产环境,支持多实例共享记忆,且不易丢失。后续可结合消息摘要(Summarization)进一步优化长对话,或引入长期记忆(向量数据库)实现更复杂的场景。
参考资料: