背景
Agent 开发中,上下文窗口不够用是最常遇到的工程问题。128K 甚至 200K 的窗口看起来很大,但 token 按量计费,200K 上下文跑一轮就要几毛钱,一天跑几百轮成本迅速攀升。上下文越长,首 token 延迟越高,用户体验直线下降。更关键的是,塞进去的信息越多,模型越容易走神,关键指令被淹没在历史对话的噪声中。上下文压缩不是要不要做的问题,而是怎么做好的问题。
这篇文章从工程视角拆解 Agent 上下文压缩的完整方案,它不是简单的做摘要,而是一套长任务状态治理体系。
核心矛盾
Agent 的上下文窗口需要同时容纳多种信息。系统指令定义角色和行为约束,用户目标描述当前任务和截止时间,对话历史保留多轮交互的上下文,工具结果包含 API 返回和文件内容,中间状态记录已完成的步骤和待办事项,外部知识则是 RAG 召回的文档片段和参考资料。任务只有 2-3 轮对话时,全部塞进去毫无压力。但当任务进行到 50 轮、100 轮时,这些信息的总量会远超窗口限制。
长任务需要的信息量大于模型能装下的信息量,这就是核心矛盾。压缩的本质是在有限的窗口里最大化当前决策所需的信息密度。
分层架构
最直觉的做法是把历史对话、工具结果、外部知识一股脑丢给模型,结果是成本高、延迟大、噪声多。正确的思路是分层管理,不同信息有不同的保留策略。
| 层级 | 名称 | 保留策略 | 信息类型 |
|---|---|---|---|
| L1 | 热区 | 原文保留,一字不改 | 最近 N 轮对话、当前任务状态、未完成步骤 |
| L2 | 温区 | 结构化摘要 | 早期对话摘要、已完成任务的关键结论 |
| L3 | 冷区 | 只存索引和引用 | 历史工具结果、旧文件内容、早期搜索结果 |
| L4 | 冻结区 | 持久化存储,跨会话可用 | 用户偏好、项目规范、历史经验教训 |
热区信息每次都完整进模型,冷区信息只带一个索引,冻结区信息需要显式召回。
信息会随着任务推进从热变冷。第 1 轮对话在热区原文保留,第 20 轮移入温区摘要化,第 50 轮移入冷区只存引用,用户偏好则直接写入冻结区持久化。这个流转过程是自动的、渐进的,不是一刀切地截断。
压缩方法
对话摘要
对早期对话生成摘要时,常见的误区是把摘要写成缩短版的对话。差的摘要在讲故事:用户问了什么,助手建议了什么,最后决定是什么。好的摘要在保存状态,只记录已完成的决策和原因,以及待完成的任务清单。Agent 拿着好的摘要能直接继续工作,拿着差的摘要还得重新理解一遍来龙去脉。摘要的原则是记结论不记过程,记决策不记讨论。
工具结果裁剪
Agent 调用工具后,返回结果往往很长,一个数据库查询可能返回几千行。全部塞进上下文,一两轮就把窗口吃光了。裁剪的核心是提取字段名和类型、保留关键值和异常值、列表类结果只保留 Top-K 加上聚合统计、保留错误码和错误信息,其余数据丢弃但存入对象存储并分配引用 ID。
例如 Agent 查询用户表返回 3000 条记录,压缩后只保留总量、字段列表、状态分布统计和前 10 条记录的摘要,加上一个引用 ID 供后续回溯。从 5000 行压缩到不到 20 行,关键信息没有丢失。
重复检测
Agent 处理文件时经常多次读取同一个文件,不做处理的话同一份内容会在上下文里出现好几次。去重的做法很简单,第二次读取时只保留一条引用说明,指向第一次展示的完整内容。这一步在实际工程中非常有效,据 Claude Code 的实现(参考本博客之前的文章 Context 管理:四级压缩与无限对话的秘密),仅去重就能节省大量 token。
状态提取
这是最容易被忽略但最关键的压缩手段。多轮任务中,很多关键约束散落在对话各处,比如第 2 轮用户提到数据库用的是 PostgreSQL,第 5 轮确认接口格式用 REST 不用 GraphQL,第 8 轮说了部署环境是 K8s 且有内存限制。这些信息一旦被摘要化就可能丢失,而它们是硬约束,丢了就会导致后续工作做错方向。
压缩前需要做状态提取,把散落的信息整理成一个结构化的状态面板。面板包含当前目标、硬约束清单(数据库类型、接口风格、部署环境、截止时间)、已完成步骤、进行中的任务、待办事项,以及当前的风险和阻塞。摘要负责背景,状态面板负责保存关键约束,两者分离才能保证压缩后不丢关键信息。
渐进式折叠
压缩不应该一次性完成,而是根据上下文用量逐步加强。用量 60% 时不做压缩,原文保留;用量 75% 时执行去重加工具结果裁剪,属于无损压缩;用量 85% 时开始早期对话摘要化,属于有损压缩但保留状态面板;用量 95% 时全面压缩,只保留最近 3 轮加状态面板加摘要。
类比手机存储空间管理:空间充足时什么照片都留着,空间紧张时先删视频,快满时才删不重要的截图。越紧急压缩越激进,但状态面板始终保留。
信息完整性保障
压缩后怎么验证关键信息没有丢失,才是真正的工程难题。
状态验证
每次压缩完成后,需要做一次状态验证。压缩前提取关键约束清单,执行压缩生成摘要后,检查摘要中是否包含所有关键约束。具体检查用户目标是否还在、硬约束有没有丢、未完成的步骤清单是否完整、有没有跟原始记录矛盾的地方。如果检查通过则继续使用,否则补拉原文修正摘要。
冲突检测
比丢信息更隐蔽的问题是摘要跟原始记录矛盾。第 3 轮用户说用 MySQL,第 15 轮用户改主意说用 PostgreSQL,压缩摘要只摘了第 3 轮写成了使用 MySQL,Agent 拿着错误摘要继续干活,后面所有数据库操作都会做错方向。更糟糕的是错误会累积,第 20 轮的摘要基于第 15 轮的摘要,形成错误链。
处理原则有四条:原始记录优先,当摘要和原始记录冲突时以原始记录为准;用户确认优先,用户明确确认过的信息优先级最高;标记冲突来源,发现矛盾时标记是哪一轮的摘要出了问题;触发重压缩,发现错误摘要后不是简单修补而是重新压缩。宁可让系统慢一点重新压缩,也不能让错误摘要继续进入后续链路。
可回放性
很多系统只做向前看的压缩,保留当前状态就够了。但实际工程中向后看同样重要。任务执行到一半出了错需要回溯是哪一步出了问题,用户说之前的方案不对需要退回之前的版本,Agent 做了一个决策但结果不好需要复盘决策过程,这些场景都需要回放能力。如果压缩后只留了一个摘要,这些场景全部无法支持。
正确的做法是每一步都保存一个轻量级的 checkpoint。checkpoint 包含当前步骤编号、用户输入、工具调用记录(含引用 ID 指向原始结果)、输出摘要,以及这一步对全局状态的改变(完成了什么、修改了哪些文件)。工具调用的原始结果不在 checkpoint 里,而是存了一个引用 ID,需要回放时通过引用 ID 拉回原始数据,正常执行时只占很少的 token。
与 RAG 的配合
上下文压缩和 RAG 解决的问题不同。上下文压缩控制进入模型的信息量,核心动作是裁剪、摘要、分层,信息来源是对话本身的历史。RAG 提供模型不具备的外部知识,核心动作是检索、重排、注入,信息来源是外部知识库。两者在实际系统中必须配合工作。
RAG 召回的结果也要经过压缩处理。如果召回了 20 个片段,每个 2000 token,那就是 40K token,可能直接把窗口吃满。压缩后的片段需要保留元数据来源,标注文档名称、版本、更新时间和权限级别。压缩的是内容,不是来源,每条注入的信息都应该能追溯到原始出处。
评估体系
最常见的错误指标是 Token 减少了多少。压缩 90% 但任务失败了,那不叫优化,叫丢信息。正确的评估维度包括任务成功率(压缩后 Agent 能不能正确完成任务)、约束保留率(用户提出的约束条件在压缩后还剩多少)、答案一致性(压缩前后的回答是否一致)、延迟变化和成本变化。
常规对话的压缩效果通常不错,真正拉开差距的是边界场景。长对话(50 轮以上)测试压缩系统能不能保持状态连贯,工具调用失败测试失败信息是否被正确保留而不是被压缩掉,用户纠正测试纠正信息有没有被保留,约束变更测试压缩后是否反映了最新需求,冲突场景测试摘要和原始记录矛盾时系统怎么处理。边界场景才是压缩系统的试金石。
完整的压缩流水线
每轮对话开始时,系统执行以下流程:计算当前上下文用量并确定处于哪个压缩阶段,从最近对话中提取关键约束并更新状态面板,按需执行压缩(去重、工具结果裁剪、对话摘要化、全面压缩),对比压缩前后的约束清单检查是否有遗漏或矛盾,保存本轮 checkpoint(输入、工具调用、输出摘要、状态变化),最后组装最终上下文(状态面板加最近 N 轮原文加历史摘要加 RAG 注入)送入模型。
这套流水线在每轮对话时都会执行,但大部分步骤是轻量级的,只有上下文用量超过阈值时才会触发重度压缩。
能压的是过程、冗余和重复,不能压的是决策、约束和用户意图。把这条线划清楚,压缩系统就不会出大问题。
系列文章导航:
- Context 管理:四级压缩与无限对话的秘密 — Claude Code 的具体压缩实现
- Agent 记忆系统设计规范 — 冻结区(外部记忆)的设计详解
- Multi-Agent 系统设计原理 — 多 Agent 协作下的上下文管理