0%

Agent 上下文压缩:长任务状态治理的工程全景

背景

构建 Agent 过程中,都会遇到上下文窗口不够用的问题。

很多人觉得这不是事儿——现在模型上下文窗口动辄 128K、200K,塞就完了。但我们做 Agent 的人都知道,窗口大不等于能用得久。原因很直接:

  • 成本:token 是按量计费的,200K 上下文跑一轮可能就要几毛钱,一天跑几百轮,月底账单能吓死人
  • 延迟:上下文越长,首 token 延迟越高,用户体验直线下降
  • 噪声:塞进去的信息越多,模型越容易”走神”,关键指令被淹没在历史对话的海洋里

所以上下文压缩不是”要不要做”的问题,而是”怎么做好”的问题。

这篇文章从工程视角系统拆解 Agent 上下文压缩这件事——不是简单做摘要,而是一套完整的长任务状态治理体系


一、核心矛盾:信息保真 vs 窗口有限

1.1 一个形象的比喻

想象你是一个项目经理,工位上只有一张 1 米宽的桌子。桌上要放:当前任务的资料、历史沟通记录、团队通讯录、公司规章制度、客户反馈……

桌子就这么大,你不可能把所有东西都摊开。你的做法是:

  • 手边放当前正在处理的文件
  • 抽屉里放最近几天的沟通记录
  • 文件柜存历史项目档案
  • 脑子里记着关键决策和客户偏好

桌子就是你的上下文窗口,你的信息管理策略就是上下文压缩

1.2 Agent 面临同样的困境

Agent 的”桌子”就是模型的上下文窗口,需要同时容纳:

信息类型 说明 示例
系统指令 角色定义、行为约束、工具描述 “你是一个代码助手,不要执行删除操作”
用户目标 当前任务、约束条件、截止时间 “在今天 5 点前完成用户模块重构”
对话历史 多轮交互的上下文 用户之前的提问和纠正
工具结果 API 返回、文件内容、搜索结果 数据库查询结果、文件读取内容
中间状态 已完成的步骤、待办事项、中间结论 “已完成身份验证模块,还差权限校验”
外部知识 RAG 召回的文档片段、参考资料 相关技术文档、历史 case

当任务只有 2-3 轮对话时,全部塞进去毫无压力。但当任务进行到 50 轮、100 轮时,这些信息的总量会远超窗口限制。

核心矛盾就在这里:长任务需要的信息量 > 模型能装下的信息量。

压缩的本质,就是在有限的窗口里,最大化当前决策所需的信息密度


二、分层架构:不是所有信息都值得同等对待

最直觉的做法是”全塞进去”——把历史对话、工具结果、外部知识一股脑丢给模型。这种做法的结局我们在前面已经说了:成本高、延迟大、噪声多。

正确的思路是分层管理——不同信息有不同的保留策略,就像项目经理不会把所有文件都摊在桌上一样。

2.1 四层信息架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌───────────────────────────────────────────────────────┐
│ 上下文窗口(有限空间) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ L1:热区 — 原文保留 │ │
│ │ 最近 N 轮对话、当前任务状态、未完成步骤 │ │
│ │ 特点:一字不改,完整保留 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ L2:温区 — 结构化摘要 │ │
│ │ 早期对话摘要、已完成任务的关键结论 │ │
│ │ 特点:保留决策和结论,丢弃过程 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ L3:冷区 — 按需召回 │ │
│ │ 历史工具结果、旧文件内容、早期搜索结果 │ │
│ │ 特点:只存索引和引用,需要时再拉取 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ L4:冻结区 — 外部记忆 │ │
│ │ 用户偏好、项目规范、历史经验教训 │ │
│ │ 特点:持久化存储,跨会话可用 │ │
│ └─────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘

每层的”温度”不同,访问频率不同,保留策略也不同。热区信息每次都完整进模型,冷区信息只带一个索引,冻结区信息需要显式召回。

2.2 层间流转

信息不是静态分层的,它会随着任务推进从热变冷

1
2
3
4
5
第 1 轮对话 → 在热区(原文保留)
第 5 轮对话 → 还在热区
第 20 轮对话 → 移入温区(摘要化)
第 50 轮对话 → 移入冷区(只存引用)
用户偏好 → 写入冻结区(持久化记忆)

这个流转过程是自动的、渐进的,不是一刀切地截断。


三、压缩的五种武器

有了分层架构,接下来要解决具体问题:怎么压?针对不同类型的信息,有不同的压缩策略。

3.1 对话摘要:把过程变成结论

最基础的压缩手段——对早期对话生成摘要。

但这里有个常见误区:摘要不是”缩短版的对话”,而是提取决策链

差的摘要:

1
2
用户问了怎么实现登录功能,助手建议使用 JWT,
然后讨论了 token 过期时间,最后决定设为 24 小时。

好的摘要:

1
2
3
4
5
6
7
## 已完成决策
- 认证方案:JWT(原因:无状态,适合微服务架构)
- Token 过期时间:24 小时(原因:平衡安全性和用户体验)

## 待完成
- 邮箱验证功能
- 刷新 token 机制

区别在哪?差的摘要在讲故事,好的摘要在保存状态。Agent 拿着好的摘要能直接继续工作,拿着差的摘要还得重新理解一遍来龙去脉。

摘要的原则:记结论,不记过程;记决策,不记讨论。

3.2 工具结果裁剪:只留有用的部分

Agent 调用工具后,返回结果往往很长。一个数据库查询可能返回几千行,一个文件读取可能上万字。全部塞进上下文,一两轮就把窗口吃光了。

工具结果压缩的核心思路是结构化裁剪

1
2
3
4
5
6
7
原始返回(5000 行)

├── 提取字段名和类型 ──── 保留
├── 提取关键值/异常值 ─── 保留
├── 列表类结果 Top-K ──── 只保留前 10 条 + 聚合统计
├── 错误码和错误信息 ──── 保留
└── 其余数据 ──────────── 丢弃(存对象存储,带引用 ID)

举个例子,Agent 查了一下用户表:

1
2
SELECT * FROM users WHERE create_time > '2026-01-01';
-- 返回 3000 条记录

压缩后不是把 3000 条记录全塞进去,而是:

1
2
3
4
5
查询结果:共 3000 条用户记录
字段:id, name, email, status, create_time
状态分布:active=2400, inactive=500, banned=100
最近 10 条:[前 10 条记录的摘要]
引用 ID:result_20260701_001(需要完整数据时可通过此 ID 回溯)

从 5000 行压缩到不到 20 行,关键信息没丢。

3.3 重复检测:同样的内容不出现两次

Agent 在处理文件时,经常多次读取同一个文件。如果不做处理,同一个文件的内容会在上下文里出现好几次。

解决方式很直觉:去重

1
2
3
第 3 轮:读取了 src/auth.ts 的内容(完整展示)
第 8 轮:再次读取 src/auth.ts 的内容
→ 压缩为:[文件 src/auth.ts 内容已在第 3 轮展示,此处省略]

这看起来简单,但在实际工程中非常有效。据 Claude Code 的实现(参考本博客之前的文章 Context 管理:四级压缩与无限对话的秘密),仅去重这一步就能节省大量 token。

3.4 状态提取:把”隐性知识”变成”显性清单”

这是最容易被忽略、但最关键的压缩手段。

在多轮任务中,有很多关键约束是散落在对话各处的:

  • 第 2 轮用户提到”数据库用的是 PostgreSQL”
  • 第 5 轮确认了”接口格式用 REST,不用 GraphQL”
  • 第 8 轮说了”部署环境是 K8s,注意内存限制”

这些信息如果不显式提取出来,一旦被摘要化就可能丢失。而它们是硬约束,丢了就会导致后续工作做错方向。

所以压缩前要做一件事:状态提取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────┐
│ 当前任务状态(每轮自动更新) │
├─────────────────────────────────────────┤
│ 目标:重构用户模块 │
│ 约束: │
│ - 数据库:PostgreSQL 15 │
│ - 接口风格:RESTful │
│ - 部署:K8s,内存限制 512Mi │
│ - 截止时间:今天 17:00 │
│ 已完成: │
│ - ✅ 身份验证模块 (auth.service.ts) │
│ - ✅ 密码重置功能 │
│ 进行中: │
│ - 🔄 权限校验逻辑 │
│ 待办: │
│ - ⏳ 邮箱验证 │
│ - ⏳ 接口限流 │
│ 风险和阻塞: │
│ - ⚠️ Redis 集群今天维护,session 方案 │
│ 需要临时调整为 DB session │
└─────────────────────────────────────────┘

摘要负责背景,状态面板负责保存关键约束。 两者分离,才能保证压缩后不丢关键信息。

3.5 渐进式折叠:不要一刀切

最后要强调的是,压缩不应该是一次性完成的,而是渐进式的。

1
2
3
4
上下文用量 60%  → 不做压缩,原文保留
上下文用量 75% → 去重 + 工具结果裁剪(无损压缩)
上下文用量 85% → 早期对话摘要化(有损压缩,但保留状态面板)
上下文用量 95% → 全面压缩,只保留最近 3 轮 + 状态面板 + 摘要

这就像手机存储空间管理:空间充足时什么照片都留着;空间紧张时先删视频(占空间大但信息密度低);快满时才删不重要的截图。

越紧急,压缩越激进,但状态面板始终保留。


四、最难的问题:压缩后怎么知道没丢关键信息?

前面的策略都是”怎么压”,但真正的工程难题是”怎么保证压完之后信息没丢”。

4.1 状态验证机制

每次压缩完成后,不应该直接把压缩结果丢给模型,而是要做一次状态验证

1
2
3
4
5
6
7
压缩前:提取关键约束清单

执行压缩:生成摘要

验证:摘要中是否包含了所有关键约束?
├── 是 → 继续使用
└── 否 → 补拉原文,修正摘要

验证什么呢?主要是检查:

  • 用户目标是否还在
  • 硬约束(数据库类型、部署环境等)有没有丢
  • 未完成的步骤清单是否完整
  • 有没有跟原始记录矛盾的地方

4.2 冲突检测

比丢信息更隐蔽的问题是摘要跟原始记录矛盾

举个真实场景:

1
2
3
第 3 轮:用户说"用 MySQL"
第 15 轮:用户改主意了"算了,还是用 PostgreSQL"
压缩摘要:写成了"使用 MySQL"(只摘了第 3 轮,漏了第 15 轮的纠正)

这时候 Agent 拿着错误摘要继续干活,后面所有关于数据库的操作都会做错。

更糟糕的是,错误会累积。第 20 轮的摘要基于第 15 轮的摘要,如果第 15 轮就错了,第 20 轮会继续错下去,形成错误链

解决方案:

  1. 原始记录优先:当摘要和原始记录冲突时,以原始记录为准
  2. 用户确认优先:用户明确确认过的信息,优先级最高
  3. 标记冲突来源:发现矛盾时,标记是哪一轮的摘要出了问题
  4. 触发重压缩:发现错误摘要后,不是简单修补,而是重新压缩

核心原则:宁可让系统慢一点重新压缩,也不能让错误摘要继续进入后续链路。


五、可回放性:压缩不能只存结果

很多系统只做”向前看”的压缩——保留当前状态就够了。但实际工程中,向后看同样重要。

5.1 为什么需要回放?

考虑这些场景:

  • 任务执行到一半出了错,需要回溯是哪一步出了问题
  • 用户说”你刚才那个方案不对,退回到之前的版本”
  • Agent 做了一个决策,但结果不好,需要复盘决策过程

如果压缩后只留了一个摘要,这些场景全玩不转。

5.2 Checkpoint 机制

正确的做法是每一步都保存一个轻量级的 checkpoint:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"step": 5,
"input": "用户要求添加邮箱验证功能",
"tool_calls": [
{
"tool": "read_file",
"params": { "path": "src/auth/service.ts" },
"result_ref": "result_005_read_file"
}
],
"output_summary": "在 auth service 中添加了邮箱验证逻辑",
"state_delta": {
"completed": ["邮箱验证逻辑"],
"files_modified": ["src/auth/service.ts"]
}
}

注意这里的关键设计:

  • 工具调用的原始结果不在 checkpoint 里,而是存了一个引用 ID(result_ref
  • output_summary 是这步的摘要,不是完整输出
  • state_delta 记录了这一步对全局状态的改变

这样做的好处是:需要回放时,可以通过引用 ID 拉回原始数据;正常执行时,只占很少的 token。


六、与 RAG 的配合

上下文压缩和 RAG 经常被放在一起讨论,但它们解决的问题不同:

上下文压缩 RAG
解决什么 控制进入模型的信息量 提供模型不具备的外部知识
核心动作 裁剪、摘要、分层 检索、重排、注入
信息来源 对话本身的历史 外部知识库

但在实际系统中,两者必须配合工作。

6.1 配合模式

1
2
3
4
5
6
7
8
9
用户提问

RAG 检索 → 召回 20 个文档片段

重排序 → 按相关性排序

裁剪 → 只保留 Top-5 片段(上下文压缩介入)

注入上下文 → 连同对话摘要一起进入模型

关键点在于:RAG 召回的结果也要经过压缩处理。如果召回了 20 个片段,每个 2000 token,那就是 40K token,可能直接把窗口吃满。

6.2 保留可追踪性

压缩后还有一个容易丢的东西:引用来源

RAG 注入的信息如果没有标注来源,模型可能会出现幻觉——把检索到的知识说成是自己”知道”的,用户也无法验证信息的可靠性。

正确做法是在压缩后的片段上保留元数据:

1
2
[来源:技术文档 v2.3,第 5 章,2026-03-15 更新,权限:内部公开]
PostgreSQL 的连接池默认最大连接数为 100,建议在高并发场景下调整至 200...

压缩的是内容,不是来源。 每条注入的信息都应该能追溯到原始出处。


七、评估体系:怎么衡量压缩效果?

做了这么多优化,怎么知道效果好不好?

7.1 不能只看 Token 压缩率

最常见的错误指标是”Token 减少了多少”。压缩 90% 但任务失败了,那不叫优化,叫丢信息。

7.2 正确的评估维度

维度 说明 怎么测
任务成功率 压缩后 Agent 能不能正确完成任务 端到端测试
约束保留率 用户提出的约束条件在压缩后还剩多少 标注测试集 + 人工检查
答案一致性 同一个问题,压缩前后的回答是否一致 对比测试
延迟变化 压缩前后的首 token 延迟对比 性能监控
成本变化 压缩前后的 token 消耗对比 成本统计

7.3 重点测试的边界场景

常规对话的压缩效果通常不错,真正拉开差距的是边界场景:

  • 长对话(50 轮以上):压缩系统能不能保持状态连贯
  • 工具调用失败:失败信息是否被正确保留(而不是被压缩掉)
  • 用户纠正:用户说”不对,应该是 XXX”,这个纠正有没有被保留
  • 约束变更:用户中途改了需求,压缩后是否反映了最新需求
  • 冲突场景:摘要和原始记录矛盾时,系统怎么处理

边界场景才是压缩系统的试金石。


八、工程实践:一个完整的压缩流水线

把前面的内容串起来,一个完整的 Agent 上下文压缩系统长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
每轮对话开始时:

├── 1. 计算当前上下文用量
│ └── token 计数 → 确定当前处于哪个压缩阶段

├── 2. 状态面板更新
│ └── 从最近对话中提取关键约束、更新待办清单

├── 3. 按需执行压缩
│ ├── 去重(移除重复文件内容、工具结果)
│ ├── 工具结果裁剪(只保留关键字段 + 引用 ID)
│ ├── 对话摘要化(渐进式,先摘要最旧的)
│ └── 全面压缩(仅在极端情况下触发)

├── 4. 状态验证
│ └── 对比压缩前后的约束清单,检查是否有遗漏或矛盾

├── 5. Checkpoint 保存
│ └── 记录本轮的输入、工具调用、输出摘要、状态变化

└── 6. 组装最终上下文
└── 状态面板 + 最近 N 轮原文 + 历史摘要 + RAG 注入 → 送入模型

这套流水线在每轮对话时都会执行,但大部分步骤是轻量级的——只有上下文用量超过阈值时才会触发重度压缩。


九、总结

上下文压缩,不是简单的”做摘要”,而是一套完整的长任务状态治理体系。

核心要点:

  1. 分层管理:热区原文保留、温区摘要化、冷区按需召回、冻结区持久化
  2. 多种武器:对话摘要、工具裁剪、重复检测、状态提取、渐进折叠
  3. 状态优先:摘要负责背景,状态面板负责保存关键约束,两者分离
  4. 安全网:状态验证 + 冲突检测 + 原始记录优先
  5. 可回放:每步保存 checkpoint,能回溯、能还原
  6. 配合 RAG:压缩控制信息量,RAG 提供外部证据,引用来源不能丢
  7. 全面评估:不看压缩率,看任务成功率、约束保留率、答案一致性

能压的是过程、是冗余、是重复。不能压的是决策、是约束、是用户意图。把这条线划清楚,压缩系统就不会出大问题。


系列文章导航:

欢迎关注我的其它发布渠道