💡 摘要: 大模型是无状态的------每次调用都不记得上一轮说了什么。在电商客服、技术助手等场景中,多轮对话的上下文连续性是基本需求。本文基于 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_id和checkpoint_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();
总结的工作流程:
- 当消息数超过
summaryMaxMessages时触发 - Agent 调用 LLM 将历史消息总结为一段摘要
- 用摘要消息替换原始历史消息
- 后续对话基于摘要 + 最近几条消息继续
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 的真实开发实践。代码示例经过本地环境验证,踩坑经验来自实际项目开发过程。
互动话题:
- 你的 Agent 应用中,短时记忆用的什么存储方案?
- 消息总结策略中,如何平衡信息保留和 Token 节省?
- 你在 Spring AI Alibaba 中还遇到了哪些记忆相关的问题?欢迎评论区交流!