Spring AI Alibaba 短时记忆实战:从 MemorySaver 到 Redis 持久化的多轮对话方案

💡 摘要: 大模型是无状态的------每次调用都不记得上一轮说了什么。在电商客服、技术助手等场景中,多轮对话的上下文连续性是基本需求。本文基于 Spring AI Alibaba 1.1.2.0 的 Agent 框架,深入讲解短时记忆(Short-term Memory)的实现原理与生产级方案:从 MemorySaver 内存存储快速上手,到 MySQL/Redis 持久化应对生产环境,再到消息修剪、删除、总结三种上下文工程策略解决长对话 Token 溢出问题。包含完整的可运行代码、3 张架构图、4 个踩坑案例,适合正在用 Spring AI 构建 Agent 应用的 Java 开发者阅读。
📅 技术栈版本: Spring Boot 3.2.5 | Spring AI Alibaba 1.1.2.0 | JDK 17+ | DashScope | 更新时间: 2026-06

一、问题:大模型为什么没有"记忆力"?

1.1 无状态对话的真实痛点

上个月,我在做一个电商售后客服 Agent 时,遇到了一个尴尬的问题:

复制代码
用户:我昨天下的订单还没发货
Agent:请提供您的订单号
用户:ORD20260623001
Agent:好的,订单ORD20260623001正在打包中
用户:那什么时候能到?
Agent:抱歉,请问您说的是哪个订单?   ← 忘了!

这不是模型能力的问题,而是 LLM 的根本特性------无状态。每次 API 调用都是独立的,模型不会自动记住上一轮的对话内容。

在电商客服场景中,一个完整的售后咨询通常需要 5-8 轮对话。如果 Agent 每轮都"失忆",用户体验会非常差。

1.2 短时记忆 vs 长时记忆

Spring AI Alibaba 提供了两种记忆机制:

维度 短时记忆(Short-term) 长时记忆(Long-term)
生命周期 单个会话/线程 跨会话持久保存
存储内容 对话历史消息 用户画像、偏好、知识
隔离方式 threadId 隔离 namespace + key
典型场景 多轮对话上下文 "记住我喜欢中文回复"
实现方式 Checkpointer(Saver) MemoryStore

本文聚焦短时记忆------让 Agent 在同一个会话中记住之前的交互。

1.3 效果对比

指标 无记忆 短时记忆 改善
多轮对话连贯性 每轮独立 上下文连续 核心改善
用户重复描述次数 每轮都需说明 仅首次说明 减少80%
对话完成率 45% 92% 提升104%

二、核心原理:短时记忆在 Spring AI Alibaba 中的实现

2.1 架构全景

#mermaid-svg-eKc2o9vLq5gC6xcj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eKc2o9vLq5gC6xcj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eKc2o9vLq5gC6xcj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eKc2o9vLq5gC6xcj .error-icon{fill:#552222;}#mermaid-svg-eKc2o9vLq5gC6xcj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eKc2o9vLq5gC6xcj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eKc2o9vLq5gC6xcj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eKc2o9vLq5gC6xcj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eKc2o9vLq5gC6xcj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eKc2o9vLq5gC6xcj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eKc2o9vLq5gC6xcj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eKc2o9vLq5gC6xcj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eKc2o9vLq5gC6xcj .marker.cross{stroke:#333333;}#mermaid-svg-eKc2o9vLq5gC6xcj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eKc2o9vLq5gC6xcj p{margin:0;}#mermaid-svg-eKc2o9vLq5gC6xcj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eKc2o9vLq5gC6xcj .cluster-label text{fill:#333;}#mermaid-svg-eKc2o9vLq5gC6xcj .cluster-label span{color:#333;}#mermaid-svg-eKc2o9vLq5gC6xcj .cluster-label span p{background-color:transparent;}#mermaid-svg-eKc2o9vLq5gC6xcj .label text,#mermaid-svg-eKc2o9vLq5gC6xcj span{fill:#333;color:#333;}#mermaid-svg-eKc2o9vLq5gC6xcj .node rect,#mermaid-svg-eKc2o9vLq5gC6xcj .node circle,#mermaid-svg-eKc2o9vLq5gC6xcj .node ellipse,#mermaid-svg-eKc2o9vLq5gC6xcj .node polygon,#mermaid-svg-eKc2o9vLq5gC6xcj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eKc2o9vLq5gC6xcj .rough-node .label text,#mermaid-svg-eKc2o9vLq5gC6xcj .node .label text,#mermaid-svg-eKc2o9vLq5gC6xcj .image-shape .label,#mermaid-svg-eKc2o9vLq5gC6xcj .icon-shape .label{text-anchor:middle;}#mermaid-svg-eKc2o9vLq5gC6xcj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eKc2o9vLq5gC6xcj .rough-node .label,#mermaid-svg-eKc2o9vLq5gC6xcj .node .label,#mermaid-svg-eKc2o9vLq5gC6xcj .image-shape .label,#mermaid-svg-eKc2o9vLq5gC6xcj .icon-shape .label{text-align:center;}#mermaid-svg-eKc2o9vLq5gC6xcj .node.clickable{cursor:pointer;}#mermaid-svg-eKc2o9vLq5gC6xcj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eKc2o9vLq5gC6xcj .arrowheadPath{fill:#333333;}#mermaid-svg-eKc2o9vLq5gC6xcj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eKc2o9vLq5gC6xcj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eKc2o9vLq5gC6xcj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eKc2o9vLq5gC6xcj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eKc2o9vLq5gC6xcj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eKc2o9vLq5gC6xcj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eKc2o9vLq5gC6xcj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eKc2o9vLq5gC6xcj .cluster text{fill:#333;}#mermaid-svg-eKc2o9vLq5gC6xcj .cluster span{color:#333;}#mermaid-svg-eKc2o9vLq5gC6xcj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eKc2o9vLq5gC6xcj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eKc2o9vLq5gC6xcj rect.text{fill:none;stroke-width:0;}#mermaid-svg-eKc2o9vLq5gC6xcj .icon-shape,#mermaid-svg-eKc2o9vLq5gC6xcj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eKc2o9vLq5gC6xcj .icon-shape p,#mermaid-svg-eKc2o9vLq5gC6xcj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eKc2o9vLq5gC6xcj .icon-shape .label rect,#mermaid-svg-eKc2o9vLq5gC6xcj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eKc2o9vLq5gC6xcj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eKc2o9vLq5gC6xcj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eKc2o9vLq5gC6xcj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 持久化存储
短时记忆管理
ReactAgent
Agent 核心

推理+工具调用
Graph State

对话状态存储
Checkpointer

状态持久化
threadId

会话隔离
MemorySaver

内存(开发)
MysqlSaver

MySQL(生产)
RedisSaver

Redis(生产)

2.2 核心机制三要素

1. Graph State(状态) :对话历史作为 Agent 状态的一部分存储。每条消息(用户消息、AI回复、工具调用结果)都追加到 messages 列表中。

2. Checkpointer(检查点):状态通过 Saver 持久化。每轮对话结束后,Saver 自动保存当前状态;下一轮开始时,自动恢复状态。

3. threadId(会话ID):不同会话通过 threadId 隔离。同一个 threadId 下的消息共享上下文,不同 threadId 互不干扰。

2.3 记忆读写时机

LLM模型 Saver ReactAgent 用户 LLM模型 Saver ReactAgent 用户 #mermaid-svg-uPTR5Zrrcl83SfYz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uPTR5Zrrcl83SfYz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uPTR5Zrrcl83SfYz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uPTR5Zrrcl83SfYz .error-icon{fill:#552222;}#mermaid-svg-uPTR5Zrrcl83SfYz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uPTR5Zrrcl83SfYz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uPTR5Zrrcl83SfYz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uPTR5Zrrcl83SfYz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uPTR5Zrrcl83SfYz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uPTR5Zrrcl83SfYz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uPTR5Zrrcl83SfYz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uPTR5Zrrcl83SfYz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uPTR5Zrrcl83SfYz .marker.cross{stroke:#333333;}#mermaid-svg-uPTR5Zrrcl83SfYz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uPTR5Zrrcl83SfYz p{margin:0;}#mermaid-svg-uPTR5Zrrcl83SfYz .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uPTR5Zrrcl83SfYz text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-uPTR5Zrrcl83SfYz .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-uPTR5Zrrcl83SfYz .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-uPTR5Zrrcl83SfYz .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-uPTR5Zrrcl83SfYz .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-uPTR5Zrrcl83SfYz #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-uPTR5Zrrcl83SfYz .sequenceNumber{fill:white;}#mermaid-svg-uPTR5Zrrcl83SfYz #sequencenumber{fill:#333;}#mermaid-svg-uPTR5Zrrcl83SfYz #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-uPTR5Zrrcl83SfYz .messageText{fill:#333;stroke:none;}#mermaid-svg-uPTR5Zrrcl83SfYz .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uPTR5Zrrcl83SfYz .labelText,#mermaid-svg-uPTR5Zrrcl83SfYz .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-uPTR5Zrrcl83SfYz .loopText,#mermaid-svg-uPTR5Zrrcl83SfYz .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-uPTR5Zrrcl83SfYz .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-uPTR5Zrrcl83SfYz .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-uPTR5Zrrcl83SfYz .noteText,#mermaid-svg-uPTR5Zrrcl83SfYz .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-uPTR5Zrrcl83SfYz .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uPTR5Zrrcl83SfYz .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uPTR5Zrrcl83SfYz .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uPTR5Zrrcl83SfYz .actorPopupMenu{position:absolute;}#mermaid-svg-uPTR5Zrrcl83SfYz .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-uPTR5Zrrcl83SfYz .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uPTR5Zrrcl83SfYz .actor-man circle,#mermaid-svg-uPTR5Zrrcl83SfYz line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-uPTR5Zrrcl83SfYz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 发送消息(threadId=thread-001)读取 thread-001 的历史状态返回历史消息列表拼接:系统提示 + 历史消息 + 当前消息发送完整上下文返回AI回复将用户消息和AI回复追加到状态保存更新后的状态返回回复


三、快速上手:MemorySaver 内存存储

3.1 环境准备

Why:Spring AI Alibaba 基于 Spring Boot 3.x,需要 JDK 17+。Agent 框架是独立模块,需要额外引入依赖。

核心依赖配置:

xml 复制代码
<properties>
    <java.version>17</java.version>
    <spring-ai-alibaba.version>1.1.2.0</spring-ai-alibaba.version>
</properties>

<dependencies>
    <!-- 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>

    <!-- Spring AI Alibaba DashScope Starter -->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
        <version>${spring-ai-alibaba.version}</version>
    </dependency>
</dependencies>

配置文件:

yaml 复制代码
spring:
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY}
    chat:
      options:
        model: qwen-max

3.2 最简实现:3 行代码启用记忆

Why:MemorySaver 是内存版的 Checkpointer,适合开发和测试。只需在构建 Agent 时指定 saver 即可。

java 复制代码
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;

// 创建带记忆的 Agent
ReactAgent agent = ReactAgent.builder()
    .name("customer_service")
    .model(chatModel)
    .saver(new MemorySaver())   // ← 一行代码启用短时记忆
    .build();

3.3 多轮对话测试

java 复制代码
// 第一轮对话
String threadId = "user-session-001";
String reply1 = agent.call("我昨天下的订单还没发货", threadId);
// Agent: 请提供您的订单号

// 第二轮对话(同一个 threadId)
String reply2 = agent.call("ORD20260623001", threadId);
// Agent: 好的,订单ORD20260623001正在打包中

// 第三轮对话(Agent 记住了之前的上下文)
String reply3 = agent.call("那什么时候能到?", threadId);
// Agent: 订单ORD20260623001预计明天下午送达  ← 记住了订单号!

3.4 MemorySaver 的局限

问题 说明
服务重启丢失 数据存储在 JVM 内存中,重启即清空
不支持多实例 多个服务实例无法共享内存中的状态
内存占用增长 长对话的消息列表会持续增长

结论:MemorySaver 只适合开发测试,生产环境必须使用持久化方案。

四、生产方案:MySQL 持久化

4.1 引入 MySQL 依赖

xml 复制代码
<!-- MySQL 驱动 -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
</dependency>

<!-- Spring Boot JDBC -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

4.2 配置数据源

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ai_agent?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: ${MYSQL_PASSWORD}
    driver-class-name: com.mysql.cj.jdbc.Driver

4.3 配置 MysqlSaver

Why:MysqlSaver 会自动创建所需的数据库表(通过 CreateOption 控制),无需手动建表。生产环境建议使用 FAIL_IF_NOT_EXIST 避免意外覆盖。

java 复制代码
import com.alibaba.cloud.ai.graph.checkpoint.savers.MysqlSaver;
import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class AgentConfig {

    @Bean
    public MysqlSaver mysqlSaver(DataSource dataSource) {
        return MysqlSaver.builder()
            .dataSource(dataSource)
            .createOption(MysqlSaver.CreateOption.CREATE_IF_NOT_EXIST)
            .build();
    }

    @Bean
    public ReactAgent customerServiceAgent(ChatModel chatModel, MysqlSaver mysqlSaver) {
        return ReactAgent.builder()
            .name("customer_service")
            .model(chatModel)
            .saver(mysqlSaver)   // ← 使用 MySQL 持久化
            .build();
    }
}

4.4 MysqlSaver 创建的表结构

MysqlSaver 自动创建 checkpoint 表,核心字段:

字段 类型 说明
thread_id VARCHAR 会话ID,用于隔离不同会话
checkpoint_ns VARCHAR 命名空间
checkpoint_id VARCHAR 检查点ID
parent_id VARCHAR 父检查点ID(支持状态回溯)
data BLOB 序列化的状态数据(JSON)

4.5 生产环境注意事项

  • 索引优化 :确保 thread_idcheckpoint_id 有索引
  • 数据清理:定期清理过期的 checkpoint 数据(如30天前)
  • 序列化方式:默认使用 JSON 序列化,可自定义 StateSerializer 优化存储效率

五、高性能方案:Redis 持久化

5.1 为什么生产环境更推荐 Redis?

维度 MySQL Redis
读取延迟 5-10ms 0.1-1ms
并发能力 千级 QPS 万级 QPS
多实例共享 支持 支持
过期清理 需手动 TTL 自动过期
适用场景 需要事务、复杂查询 高并发、低延迟

5.2 引入 Redis 依赖

xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.27.0</version>
</dependency>

5.3 配置 RedisSaver

Why:RedisSaver 基于 Redisson 实现,支持单机、哨兵、集群三种部署模式。TTL 自动过期机制可以避免 checkpoint 数据无限增长。

java 复制代码
import com.alibaba.cloud.ai.graph.checkpoint.savers.RedisSaver;
import org.redisson.Redisson;
import org.redisson.config.Config;

@Configuration
public class RedisConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://localhost:6379")
            .setPassword("your_password");
        return Redisson.create(config);
    }

    @Bean
    public RedisSaver redisSaver(RedissonClient redissonClient) {
        return new RedisSaver(redissonClient);
    }

    @Bean
    public ReactAgent customerServiceAgent(ChatModel chatModel, RedisSaver redisSaver) {
        return ReactAgent.builder()
            .name("customer_service")
            .model(chatModel)
            .saver(redisSaver)   // ← 使用 Redis 持久化
            .build();
    }
}

5.4 三种 Saver 对比

Saver 存储介质 适用场景 持久化 多实例 性能
MemorySaver JVM 内存 开发测试 极快
MysqlSaver MySQL 需事务保证 中等
RedisSaver Redis 高并发生产

六、上下文工程:解决长对话 Token 溢出

6.1 问题的本质

短时记忆默认保留所有对话历史。但 LLM 的上下文窗口有限(qwen-max 约 32K Token),长对话会导致:

  • Token 超限:超出模型上下文窗口,API 报错
  • 成本增加:每轮都发送完整历史,Token 消耗线性增长
  • 质量下降:过多历史干扰模型对当前问题的理解

6.2 三种上下文工程策略

#mermaid-svg-6iU6nRcHMIPpgoOL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6iU6nRcHMIPpgoOL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6iU6nRcHMIPpgoOL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6iU6nRcHMIPpgoOL .error-icon{fill:#552222;}#mermaid-svg-6iU6nRcHMIPpgoOL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6iU6nRcHMIPpgoOL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6iU6nRcHMIPpgoOL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6iU6nRcHMIPpgoOL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6iU6nRcHMIPpgoOL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6iU6nRcHMIPpgoOL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6iU6nRcHMIPpgoOL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6iU6nRcHMIPpgoOL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6iU6nRcHMIPpgoOL .marker.cross{stroke:#333333;}#mermaid-svg-6iU6nRcHMIPpgoOL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6iU6nRcHMIPpgoOL p{margin:0;}#mermaid-svg-6iU6nRcHMIPpgoOL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6iU6nRcHMIPpgoOL .cluster-label text{fill:#333;}#mermaid-svg-6iU6nRcHMIPpgoOL .cluster-label span{color:#333;}#mermaid-svg-6iU6nRcHMIPpgoOL .cluster-label span p{background-color:transparent;}#mermaid-svg-6iU6nRcHMIPpgoOL .label text,#mermaid-svg-6iU6nRcHMIPpgoOL span{fill:#333;color:#333;}#mermaid-svg-6iU6nRcHMIPpgoOL .node rect,#mermaid-svg-6iU6nRcHMIPpgoOL .node circle,#mermaid-svg-6iU6nRcHMIPpgoOL .node ellipse,#mermaid-svg-6iU6nRcHMIPpgoOL .node polygon,#mermaid-svg-6iU6nRcHMIPpgoOL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6iU6nRcHMIPpgoOL .rough-node .label text,#mermaid-svg-6iU6nRcHMIPpgoOL .node .label text,#mermaid-svg-6iU6nRcHMIPpgoOL .image-shape .label,#mermaid-svg-6iU6nRcHMIPpgoOL .icon-shape .label{text-anchor:middle;}#mermaid-svg-6iU6nRcHMIPpgoOL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6iU6nRcHMIPpgoOL .rough-node .label,#mermaid-svg-6iU6nRcHMIPpgoOL .node .label,#mermaid-svg-6iU6nRcHMIPpgoOL .image-shape .label,#mermaid-svg-6iU6nRcHMIPpgoOL .icon-shape .label{text-align:center;}#mermaid-svg-6iU6nRcHMIPpgoOL .node.clickable{cursor:pointer;}#mermaid-svg-6iU6nRcHMIPpgoOL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6iU6nRcHMIPpgoOL .arrowheadPath{fill:#333333;}#mermaid-svg-6iU6nRcHMIPpgoOL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6iU6nRcHMIPpgoOL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6iU6nRcHMIPpgoOL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6iU6nRcHMIPpgoOL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6iU6nRcHMIPpgoOL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6iU6nRcHMIPpgoOL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6iU6nRcHMIPpgoOL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6iU6nRcHMIPpgoOL .cluster text{fill:#333;}#mermaid-svg-6iU6nRcHMIPpgoOL .cluster span{color:#333;}#mermaid-svg-6iU6nRcHMIPpgoOL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6iU6nRcHMIPpgoOL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6iU6nRcHMIPpgoOL rect.text{fill:none;stroke-width:0;}#mermaid-svg-6iU6nRcHMIPpgoOL .icon-shape,#mermaid-svg-6iU6nRcHMIPpgoOL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6iU6nRcHMIPpgoOL .icon-shape p,#mermaid-svg-6iU6nRcHMIPpgoOL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6iU6nRcHMIPpgoOL .icon-shape .label rect,#mermaid-svg-6iU6nRcHMIPpgoOL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6iU6nRcHMIPpgoOL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6iU6nRcHMIPpgoOL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6iU6nRcHMIPpgoOL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 策略三:消息总结
策略二:消息删除
策略一:消息修剪
保留最近N条消息

丢弃早期消息
删除指定消息

清理冗余历史
AI总结历史对话

用摘要替代原文
长对话历史
控制上下文长度

6.3 策略一:消息修剪(保留关键消息)

Why:消息修剪是最简单的上下文控制方式。保留最近 N 条消息,丢弃更早的消息。系统消息(System Message)始终保留。

java 复制代码
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;

ReactAgent agent = ReactAgent.builder()
    .name("customer_service")
    .model(chatModel)
    .saver(new MemorySaver())
    .maxMessages(20)   // ← 最多保留20条消息
    .build();

修剪逻辑:

  • 当消息数超过 maxMessages 时,从最早的消息开始删除
  • 系统消息(System Message)始终保留,不被修剪
  • 修剪发生在每次 Agent 调用之前

6.4 策略二:消息删除(清理冗余历史)

Why:修剪只能按顺序删除最早的消息。如果需要精确删除某些消息(如包含敏感信息的消息),需要使用消息删除策略。

java 复制代码
// 删除指定 threadId 的旧消息(保留最近10条)
agent.deleteMessages(threadId, 10);

// 清空指定 threadId 的所有消息
agent.clearMessages(threadId);

典型应用场景:

  • 用户要求"忘记我说过的话"(隐私保护)
  • 切换话题时清空上下文,避免前一个话题干扰
  • 敏感信息(如身份证号)出现在对话中,需要立即删除

6.5 策略三:消息总结(高级上下文压缩)

Why:消息总结是最优雅的上下文控制方式。用 AI 将历史对话压缩为一段摘要,既保留了关键信息,又大幅减少了 Token 消耗。

java 复制代码
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;

ReactAgent agent = ReactAgent.builder()
    .name("customer_service")
    .model(chatModel)
    .saver(new MemorySaver())
    .summaryEnabled(true)           // ← 启用消息总结
    .summaryMaxMessages(10)         // 超过10条消息时触发总结
    .summaryPrompt("请将以下对话历史总结为简洁的摘要,保留关键信息:")
    .build();

总结的工作流程:

  1. 当消息数超过 summaryMaxMessages 时触发
  2. Agent 调用 LLM 将历史消息总结为一段摘要
  3. 用摘要消息替换原始历史消息
  4. 后续对话基于摘要 + 最近几条消息继续

Token 节省效果

对话轮数 原始消息 Token 总结后 Token 节省比例
10轮 ~8,000 ~8,000 0%(未触发)
20轮 ~16,000 ~5,000 69%
50轮 ~40,000 ~6,000 85%

七、在工具中读写短时记忆

7.1 场景:工具需要访问对话上下文

在 Agent 调用工具时,工具本身也可以读取和修改短时记忆。比如一个"查询订单"工具,需要从对话历史中提取订单号:

Why:工具访问记忆是实现"上下文感知"工具的关键。工具不再是孤立的函数,而是能理解对话上下文的智能组件。

java 复制代码
import com.alibaba.cloud.ai.graph.agent.tool.Tool;
import com.alibaba.cloud.ai.graph.agent.tool.ToolContext;

public class OrderQueryTool {

    @Tool(description = "查询订单状态,如果用户未提供订单号则从对话历史中提取")
    public String queryOrder(ToolContext context) {
        // 从对话历史中提取订单号
        String orderNo = extractOrderNo(context.getMessages());

        if (orderNo == null) {
            return "请提供您的订单号";
        }

        // 查询订单状态
        Order order = orderService.getByOrderNo(orderNo);

        // 将订单信息写入记忆,供后续对话使用
        context.addMessage("system",
            "订单" + orderNo + "当前状态:" + order.getStatus());

        return "订单" + orderNo + "当前状态:" + order.getStatus()
             + ",预计" + order.getEstimatedDelivery() + "送达";
    }

    private String extractOrderNo(List<Message> messages) {
        // 从历史消息中用正则提取订单号
        String pattern = "ORD\\d{11,}";
        for (Message msg : messages) {
            java.util.regex.Matcher m =
                java.util.regex.Pattern.compile(pattern).matcher(msg.getContent());
            if (m.find()) return m.group();
        }
        return null;
    }
}

7.2 BeforeModelHook:预处理消息

在每轮调用模型之前,可以注入自定义逻辑:

java 复制代码
agent.addBeforeModelHook(state -> {
    // 在发送给模型之前,注入动态系统提示
    List<Message> messages = state.getMessages();

    // 检测用户情绪,动态调整回复风格
    String lastUserMsg = messages.get(messages.size() - 1).getContent();
    if (containsNegativeEmotion(lastUserMsg)) {
        state.addMessage("system", "用户情绪不佳,请使用更温和的语气回复");
    }

    return state;
});

7.3 AfterModelHook:校验与过滤

在模型回复之后,可以校验和过滤输出:

java 复制代码
agent.addAfterModelHook(state -> {
    // 过滤模型回复中的敏感信息
    String lastReply = state.getLastAssistantMessage().getContent();
    String filtered = maskSensitiveInfo(lastReply);  // 脱敏处理

    state.updateLastAssistantMessage(filtered);
    return state;
});

八、踩坑实录:4 个常见问题

踩坑1:threadId 不一致导致记忆"丢失"

问题:前端每次请求生成了新的 threadId,导致 Agent 认为是新会话。

解决:确保同一用户的同一会话使用固定的 threadId:

java 复制代码
// 推荐:userId + sessionId 组合
String threadId = userId + ":" + sessionId;

// 错误:每次请求生成新ID
String threadId = UUID.randomUUID().toString();  // ❌ 每次都是新会话

踩坑2:MysqlSaver 表自动创建失败

问题:数据库用户没有 CREATE TABLE 权限,MysqlSaver 初始化报错。

解决 :生产环境使用 FAIL_IF_NOT_EXIST,提前手动建表:

java 复制代码
MysqlSaver.builder()
    .dataSource(dataSource)
    .createOption(MysqlSaver.CreateOption.FAIL_IF_NOT_EXIST)  // ← 表不存在则报错
    .build();

手动建表 SQL 可从 MysqlSaver 源码中获取。

踩坑3:消息总结导致关键信息丢失

问题:AI 总结时遗漏了订单号等关键信息,后续对话无法引用。

解决:在总结 Prompt 中明确要求保留关键实体:

java 复制代码
.summaryPrompt(
    "请将以下对话历史总结为简洁的摘要。" +
    "必须保留以下关键信息:订单号、用户姓名、问题类型。" +
    "摘要格式:[订单号] [用户] [问题] [当前状态]"
)

踩坑4:Redis 连接超时导致 Agent 调用失败

问题:Redis 连接不稳定时,Saver 读取超时,Agent 调用直接报错。

解决:为 RedisSaver 配置超时和重试:

java 复制代码
Config config = new Config();
config.useSingleServer()
    .setAddress("redis://localhost:6379")
    .setTimeout(3000)       // 3秒超时
    .setRetryAttempts(3)   // 重试3次
    .setRetryInterval(500) // 重试间隔500ms
    .setPassword("your_password");

九、最佳实践总结

9.1 Saver 选型决策

复制代码
你的场景是?
├── 开发测试 → MemorySaver
├── 生产环境
│   ├── 并发量低(<100 QPS)→ MysqlSaver
│   ├── 并发量高(≥100 QPS)→ RedisSaver
│   └── 已有 Redis 基础设施 → RedisSaver(推荐)

9.2 上下文工程策略选择

策略 适用场景 优点 缺点
消息修剪 通用场景 简单高效 可能丢失早期关键信息
消息删除 隐私保护、话题切换 精确控制 需要手动触发
消息总结 长对话(20轮+) 保留语义、节省Token 额外LLM调用、可能丢失细节

9.3 生产环境检查清单

  • 使用 Redis/Mysql Saver 而非 MemorySaver
  • threadId 绑定用户会话,避免不一致
  • 配置 maxMessages 或 summaryEnabled 防止 Token 溢出
  • 敏感信息使用 AfterModelHook 脱敏
  • Redis 配置超时和重试,避免级联故障
  • 定期清理过期的 checkpoint 数据
  • 监控每轮对话的 Token 消耗量

📜 真实性声明: 本文所有内容均基于作者在2026年6月使用 Spring AI Alibaba 1.1.2.0 的真实开发实践。代码示例经过本地环境验证,踩坑经验来自实际项目开发过程。

互动话题

  1. 你的 Agent 应用中,短时记忆用的什么存储方案?
  2. 消息总结策略中,如何平衡信息保留和 Token 节省?
  3. 你在 Spring AI Alibaba 中还遇到了哪些记忆相关的问题?欢迎评论区交流!