Spring AI Alibaba 记忆持久化到 Redis 实战指南

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.42.17.2 方法签名不同)。

解决 :在 pom.xml 中通过 dependencyManagement 统一 Jackson 版本至 2.17.2(或与 Spring Boot 默认版本一致)。详见本文 pom.xml 配置。


5.4 NumberFormatException: For input string: "5000ms"

现象 :启动时 RedisConfig 字段注入失败。

原因application.ymltimeout: 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)进一步优化长对话,或引入长期记忆(向量数据库)实现更复杂的场景。


参考资料