Skip to content

第 13 章 上下文的剪裁与压缩 ​

本章要解决的问题

第 11 章让 Agent 按需读取代码,第 12 章又用分层和渐进加载阻止无关内容过早进入。但与任务相关的 grep、read、测试日志和工具调用仍会不断进入对话历史。前两章解决“取什么、什么时候取”,本章处理“已经取进来以后怎么管”。任务运行得足够久以后,模型不仅会遇到长度上限,还会被大量已经失效的细节干扰。

本章不只回答“怎样生成一份摘要”,而是把完整过程讲清楚:什么内容在进入历史时就该限长,什么时候开始清理,旧历史从哪里切开,摘要失败后怎样恢复,以及 codex、grok-build、dsh、pi 分别怎样取舍。

13.1 先看一个会翻车的任务 ​

用户让 Agent 修一个测试失败。Agent 先读了配置文件,又运行了全量测试,随后搜索了十几个调用点。到第 30 轮时,对话里可能同时存在:

  • 用户最初的目标和后来补充的限制;
  • 已经被推翻的三个猜测;
  • 多次重复读取的同一段代码;
  • 一份几万行的测试日志;
  • 已修改文件、当前差异和还没跑完的命令;
  • 最近一次失败真正需要的二十行输出。

最容易想到的做法是“窗口满了就总结一下”。这还不够,因为此时至少有四个不同问题:

动作处理对象常见做法主要风险
限长单次过大的工具结果只返回开头、结尾和“省略了多少内容”关键内容可能恰好在中间
剪裁已经进入历史的低价值内容删除或替换旧日志、重复结果删除后可能无法重取
压缩一段仍有价值、但原文太长的历史让 LLM 生成可继续工作的摘要摘要会误解、遗漏或截断
隔离预计会产生大量中间信息的子任务交给子 Agent,只返回结论和产物位置子 Agent 得不到主任务的隐含背景

这四种动作不是同义词。限长和剪裁可以完全由程序执行,结果稳定、便宜;压缩需要模型理解任务,信息保留得更聪明,但也更慢、更难验证;隔离是在信息进入主上下文之前解决问题。

13.2 一套完整的处理流程 ​

上下文从工具输出限长到压缩提交的完整流程

这张图里最重要的不是某个百分比,而是处理顺序:

  1. 入口先限长。 一次命令返回十万行时,不要先把十万行全部塞进历史,再指望后面压缩。
  2. 每轮重新计量。 模型窗口不仅要容纳已有输入,还要为下一次回复、工具参数和错误修复留空间。
  3. 先做确定性的剪裁。 能从文件重新读取的日志、重复工具结果,优先用程序缩短。
  4. 仍有压力再生成摘要。 摘要处理的是“不能直接丢,但没必要保留全部原文”的历史。
  5. 摘要成功后再切换。 生成摘要和替换历史是两个步骤;中间崩溃时,旧历史必须仍然可用。

一个最小但不草率的控制逻辑可以写成下面这样:

text
工具结果进入历史前先限长
重新计算下一轮需要的 token

如果空间足够:继续执行
否则:
  剪裁可重取的旧工具结果
  重新计算

  如果仍然不足:
    选择不会拆开工具调用和结果的旧区间
    生成“任务交接摘要”
    检查摘要是否完整、是否真的更短
    完整保存成功结果后,下一轮改用“摘要 + 近期原文”

真正困难的部分都藏在这些普通动词里:怎样估算、什么算“可重取”、切点是否合法、摘要是否合格、保存到一半崩溃怎么办。下面逐项展开。

13.3 什么时候开始处理:不要等窗口已经爆掉 ​

假设模型窗口是 128K token,当前输入已经占了 120K。理论上还剩 8K,但这 8K 未必够用:模型可能需要输出 6K,又调用一次测试工具带回 10K,随后还要读文件修错。

因此触发条件更接近:

text
已有上下文
+ 下一轮最大输出预留
+ 可能进入历史的工具结果余量
+ 失败恢复余量
>= 可用窗口

四个 Agent 并没有采用同一个固定比例:

  • codex 主要使用模型配置给出的自动压缩上限,同时受模型硬窗口约束;切换到更小窗口的模型时,也会重新判断是否要先压缩。它不是统一写死一个百分比。
  • grok-build 默认在窗口使用率约 85% 时自动触发,并同时在模型请求前检查工具结果是否已经把估算值推过硬上限。
  • dsh 默认在窗口的 80% 产生压力时启动处理。这个阈值可以按具体 provider 和模型覆盖。
  • pi 使用 contextWindow - reserveTokens。默认预留 16,384 token 给模型输出,而不是把触发点表达为固定百分比。

这些数字反映的是产品选择,不是行业标准。长输出模型应该多留空间;工具结果可控、任务较短时可以晚一点;模型服务经常少报或迟报 token 时,应该更保守。

13.4 第一道防线:先处理单次过大的工具结果 ​

工具输出通常是上下文增长最快的来源。一次全量测试、构建日志或大文件读取,可能比之前几十轮对话还长。

dsh:先剪工具结果,再决定要不要调用模型摘要 ​

dsh 把工具结果剪裁做成独立步骤。默认情况下,一条文本工具结果超过 8,192 个 Unicode 字符时,保留前 4,096 个和后 1,024 个,中间替换为明确的省略标记。

这里有三个容易忽略的细节:

  1. 它按 Unicode 字符处理,不会把 emoji 这类字符从中间切坏;
  2. 非文本块仍保留原来的顺序;
  3. 剪裁结果作为新的历史节点保存,并记录它替换了哪一条原始结果,而不是偷偷修改内存里的字符串。

剪完以后会重新计量。如果已经回到安全范围,就不需要支付一次 LLM 摘要调用。这是 dsh 的一个核心取舍:便宜、确定性的处理先做,昂贵、有损的语义压缩最后做。

pi:主会话保留原结果,摘要请求只看短预览 ​

pi 在为“摘要模型”序列化历史时,会把每条工具结果限制在前 2,000 个字符,并说明还有多少字符被省略。它解决的是另一个问题:避免“为了压缩主上下文而发出的摘要请求”自己先超出窗口。

这不等于主会话里的原始工具结果已经被删除。pi 的完整会话树仍保留原事件,摘要模型只收到一个更短的文本版本。

codex:正常请求限长,远程压缩前还会再瘦身 ​

codex 在组装模型历史时会按模型策略限制函数调用输出。走远程压缩路径前,如果完整历史仍然放不进请求,还会先重写过大的函数输出,再把处理后的历史交给压缩服务。

grok-build:压缩输入有逐级降级,而不是一次粗暴截断 ​

grok-build 支持让摘要模型先看尽量接近原始的对话;启用这一配置时,如果摘要请求本身超出窗口,会按下面的顺序降级。未启用时则直接从精简表示开始:

text
完整输入
  → 仍保留原意,但裁到“窗口 - 摘要输出预留 - 工具说明”以内
  → 改用更精简的对话表示,并压到约 70% 窗口

这比“失败后再发送完全相同的请求”更合理:上下文超限是确定性错误,同样的输入重试多少次都不会成功。

grok-build 还实现了一种可选的“两段式预压缩”。在真正到达自动压缩线之前,它可以在后台先总结大约 95% 的旧前缀,得到一份临时笔记,并记录这段前缀的指纹。真正需要压缩时,再把临时笔记与刚刚新增的尾部一起处理。只有当前历史前缀仍与指纹一致,临时结果才会被采用。

这样能把一部分摘要延迟移到后台,但也会产生没有用上的模型调用:用户可能在到达压缩线前就结束任务,或者历史被编辑,导致预先生成的笔记作废。这是一种用额外成本换关键路径延迟的做法,不适合直接照搬到所有 Agent。

13.5 第二道防线:切点不能只按 token 一刀切 ​

假设历史是:

text
assistant: 调用 read_file,call_id = 42
tool:      call_id = 42,返回文件内容
assistant: 根据文件内容得出结论

如果在工具调用和工具结果之间切开,下一次请求里就会出现“没有结果的调用”或“没有调用的结果”。很多模型 API 会直接拒绝这种消息序列。

成熟实现至少要满足:

  • 不拆开工具调用和对应结果;
  • 尽量从完整用户轮次的边界切开;
  • 单个轮次本身太大时,要有专门的降级方案;
  • 近期正在修改的代码和运行中的任务不能只靠泛化摘要保留;
  • 切点选择后到摘要提交前,如果历史发生变化,不能继续覆盖旧范围。

四家的切点策略差异很明显。

codex:本地压缩后只保留近期用户消息和交接摘要 ​

codex 的本地压缩并不是简单保留“最近若干条完整消息”。摘要生成成功后,它会重建模型可见历史:保留最近的真实用户消息,总量最多约 20K token,然后追加交接摘要。旧的 assistant 回复和工具结果不再逐条保留。

这样做的好处是压缩幅度大、结构简单,用户后来追加的要求仍有原文。代价是最近工具结果也要依赖摘要表达;如果摘要漏掉某个精确错误或路径,原始消息不会继续出现在模型窗口中。

压缩发生在任务中途时,codex 还会把当前项目说明和运行状态重新放到合适位置;如果是轮次之间的压缩,则由下一轮正常的上下文组装重新注入。也就是说,动态历史可以重建,但系统要求和当前环境不能跟着旧历史一起丢掉。

dsh:保留按 token 计量的近期尾部 ​

dsh 从历史末尾向前累加,默认至少保留窗口的 16% 作为近期原文,更早的区间交给摘要模型。切点会向前调整,直到工具调用和结果在两侧都保持完整。

它不强制只能在完整用户轮次之间切开,所以一个特别大的轮次也可以分段压缩;但任何工具调用和结果都不能被拆散。这样的取舍比“整轮保留”更节省空间,也比任意按 token 切断更安全。

grok-build:摘要整段历史,但保留当前工作的尾巴 ​

grok-build 的编码 Agent 采用“整段替换”思路:摘要模型总结准备好的整段对话,成功后重新组装一份新的历史,而不是选一个长期保留的普通尾部比例。

新历史会重新放入:

  • 原系统要求;
  • 用户和项目环境信息;
  • 项目级说明文件;
  • 最后一个真实用户问题;
  • 最后一个用户问题之后仍在进行的工具或子任务结果;
  • 压缩摘要;
  • 已修改文件、运行任务、子 Agent 等当前状态提醒。

因此它虽然叫“整段替换”,也不会只剩一条摘要。稳定规则、最后需求、当前工作和运行状态由程序重新装配,长期历史才交给模型归纳。

pi:正常按轮次切,单轮过大时单独总结前半段 ​

pi 默认从后向前保留约 20K token 的近期内容。合法切点可以是用户消息、assistant 消息、Shell 执行或自定义消息,但不会选择工具结果作为切点。

通常它会在完整轮次边界切开。如果一个用户轮次本身就超过 20K,pi 不会被迫保留整个超大轮次,而是把它拆成两部分:

text
更早的完整历史 → 生成历史摘要
超大轮次的前半段 → 生成“本轮早期进展”摘要
超大轮次的后半段 → 保留原文

最后把两个摘要合并。这样最近的工具调用仍有原文,而模型也知道这一轮最初要解决什么、前面已经做了什么。

13.6 第三道防线:摘要必须像交接文档,而不是聊天摘要 ​

“请总结上面的对话”通常会得到一段看似流畅、却无法继续工作的文字。编码任务真正需要保留的是:

  • 用户目标怎样变化,最新要求是什么;
  • 已完成、进行中、被阻塞和未开始的工作;
  • 改了哪些文件,关键位置和当前差异是什么;
  • 哪些方案试过但失败,为什么不能再走一遍;
  • 仍在运行的命令、等待中的审批和外部副作用;
  • 精确路径、标识符、错误文本、数值和下一步。

摘要还必须知道当前任务 ​

同一份原始内容,在不同任务里不能生成同一份摘要:

  • 修测试失败时,要保留错误栈、相关文件、已排除的原因和最近修改;
  • 做安全审查时,不能因为某条可疑路径暂时没用上就删除;
  • 做资料调研时,结论后面还要保留来源链接,否则摘要一旦写错就无法回查;
  • 做文案创作时,关键例子和语气要求可能比中间推理更重要。

设计压缩方案时,至少要比较四个变量:逐条摘要还是合并摘要、是否带当前任务、是否保留引用、达到阈值后是否批量处理。逐页孤立摘要容易重复,也可能保留了局部细节却丢掉它与当前任务的关系。因此有一条更可靠的原则:压缩提示要带上当前目标和已经掌握的事实,不能只把一段旧消息孤立地交给摘要模型。

对于可以重新取得的内容,摘要最好保留一个无损索引:网页保留 URL,代码保留文件路径和符号名,长日志保留文件位置和查询方法。自然语言摘要可以有损,回查入口不要一起丢掉。

四家都在用结构化提示约束摘要,但侧重点不同:

  • codex 要求生成面向下一位模型的简洁交接,重点是进度、决定、限制、剩余工作和关键数据。
  • dsh 固定为八个部分:用户意图、技术要点、文件与代码、错误与修复、待完成工作、当前工作、下一步和关键背景。缺少的部分也不能省略。
  • grok-build 的结构更细,还要求保留所有用户消息、问题解决过程和最近工作。已有旧摘要时,必须把仍然有效的内容继续带入,避免多次压缩后逐轮失真。
  • pi 按目标、限制、进度、关键决定、下一步和关键背景组织,并由程序额外维护“读过哪些文件、改过哪些文件”,不把文件追踪完全交给摘要模型猜。

这里可以看到两种思路:

  1. 让摘要模型自己回忆一切:实现简单,但精确状态容易丢;
  2. 模型总结语义,程序维护结构化事实:例如文件列表、运行任务、最后用户问题和项目说明,可靠性更高,但实现更复杂。

grok-build 和 pi 都明显采用了第二种思路。dsh 的持久化事件也让程序能够核对被替换范围和 token 数,而不只相信一段自然语言。

13.7 四个 Agent 压缩后,模型实际看见什么 ​

为了避免被“都用了摘要”这句话误导,可以直接比较模型下一轮收到的历史。

codex,本地压缩 ​

text
[系统与项目上下文:当前轮注入,或下一轮重新注入]
[最近的真实用户消息,合计最多约 20K token]
[一份交接摘要]

特点:压得最干净,最近的 assistant 和工具原文也会退出模型窗口。

dsh ​

text
[一份替代旧区间的结构化摘要]
[按 token 保留的近期原始消息,默认约为窗口的 16%]

特点:这是对历史表面的区间替换;完整替换关系、计量和摘要调用仍记录在会话日志里。

grok-build ​

text
[原系统要求]
[用户与项目环境]
[项目级说明]
[最后一个真实用户问题]
[当前轮仍需保留的工具或子任务结果]
[整段历史摘要]
[当前文件、任务、子 Agent 等状态提醒]

特点:不是普通的“摘要 + 固定比例尾部”,而是按信息职责重新装配一份新上下文。

pi ​

text
[系统要求]
[旧历史摘要,包含累计文件信息]
[从记录的近期起点开始的原始分支]

特点:模型视图变短,但原始 JSONL 会话树没有被物理删除;切换分支时还有独立的分支摘要。

Agent默认自动触发压缩前先做什么近期内容怎样保留重复压缩怎样处理
codex模型配置上限或硬窗口限制工具输出;远程路径还会预先瘦身本地路径保留近期用户原文,最多约 20K token生成新的交接摘要并重建可见历史
dsh窗口 80%先剪超过 8,192 字符的工具结果并重新计量默认保留窗口 16% 的近期原始消息旧检查点与新消息合并成一份新检查点
grok-build窗口约 85%,另有溢出检查摘要输入按完整、适配、精简三级降级保留最后用户问题和当前工作尾部旧摘要被视为早期历史的权威来源,再次归纳
pi窗口减去 16,384 token 预留摘要请求把每条工具结果缩到 2,000 字符默认保留约 20K token;超大单轮单独处理上一份摘要与新淘汰的历史一起更新

13.8 摘要生成成功,不等于压缩已经成功 ​

生产实现至少要防住下面五类失败。

1. 摘要自己被截断 ​

模型因为输出上限只返回半份摘要时,不能把它当作成功。dsh 会把“达到摘要输出上限”视为不完整检查点并拒绝提交;grok-build 会把过短或空摘要当作可重试失败;pi 会拒绝摘要请求中出现工具调用或明确的模型错误。

仅检查“字符串不为空”还不够。自建 Agent 至少应该检查必需标题、当前任务、下一步和关键路径是否存在,并确认摘要明显短于被替换内容。

2. 摘要期间历史发生变化 ​

生成摘要可能需要几十秒。期间用户又发来消息、后台任务结束,或者另一个流程修改了会话。如果仍然用旧切点覆盖,就会把新内容一起遮掉。

dsh 在提交前重新检查选中的历史范围。手动压缩期间如果范围变化,会关闭这次失败记录,但不替换模型可见历史。

3. 写到一半进程崩溃 ​

dsh 把压缩开始、摘要结果、替换范围和结束分别记录。只有完整结果落地后,模型视图才使用新检查点;程序重启时可以识别只有开始、没有结束的遗留操作。

pi 的做法是向只追加、不覆盖旧节点的会话树写入一条压缩记录,记录摘要、近期分支起点和压缩前 token。旧节点仍在,因此模型视图可以重建,分支也不会被破坏。

4. 压缩请求本身超出窗口 ​

codex 的本地路径会逐步移除最旧的压缩输入,直到请求能够发送;grok-build 会从完整输入降到适配窗口的输入,再降到有损精简输入。两者都说明:摘要器也必须有自己的输入预算。

5. 失败后无限重试 ​

网络断开可以重试,认证错误、消息结构错误和同一份输入必然超限则不应该原样重试。

四家的共同原则是重试有上限,且区分“换个输入再试”与“原样重试”——上限数字与降级方式各不相同:

  • grok-build 默认最多尝试三次;确定性超限时改用更小输入,其他确定性失败会暂停自动压缩,直到相应条件变化。
  • pi 对摘要调用沿用有上限的临时错误重试;因为上下文溢出而压缩时,只做一次“压缩后重试原任务”,再次失败就停止循环。
  • dsh 分开限制普通压缩重试和溢出恢复重试,默认都只额外尝试一次。
  • codex 对可重试的流错误使用有上限的退避;如果压缩输入仍超限,则改变输入而不是只等待后重发。

13.9 压缩与前缀缓存:省 token,也会让一段缓存失效 ​

前缀缓存要求请求开头的字节保持一致。压缩会把旧历史换成新摘要,因此替换位置之后的旧缓存通常不能继续使用。

text
压缩前: [系统要求 | 工具说明] [旧历史 80K] [近期历史]
压缩后: [系统要求 | 工具说明] [新摘要 8K]  [近期历史]
          └ 共同前缀可能复用 ┘     └ 从摘要位置开始重新计算 ┘

需要分清两笔成本:

  1. 生成摘要的辅助调用;
  2. 主 Agent 下一轮从新摘要位置开始重新计算。

dsh 特意让摘要调用重放原请求的系统要求、工具定义和旧消息,再把压缩指令放在最后,争取复用压缩前的前缀缓存。pi 采取相反取舍:摘要是一条独立、一次性的请求,使用新的路由会话标识;服务支持时还会关闭这次请求的缓存写入,因为它大概率不会再次复用。

两种做法都合理:前者降低本次摘要延迟和输入计算成本,但摘要器与主请求绑定更紧;后者隔离清楚、扩展容易,但不能指望复用整段主会话缓存。

压缩是否划算,取决于后面还有多少轮。如果任务马上结束,生成摘要可能比直接完成更贵;如果还要执行几十轮,或者不压缩就无法继续,重建一次缓存通常值得。

13.10 自建时先做到什么 ​

普通团队不需要一次实现四家的全部能力。可以分三步推进。

第一版:能避免直接爆窗 ​

  • 每条工具结果有明确上限和省略标记;
  • 记录模型窗口、当前输入和输出预留;
  • 超过阈值时在完整轮次边界生成结构化摘要;
  • 摘要失败就保留旧历史,不做替换;
  • 提供手动压缩入口,方便用户在阶段边界触发。

第二版:能在真实编码任务里继续工作 ​

  • 切点不拆工具调用与结果;
  • 摘要保留目标、限制、文件、失败路径、当前工作和下一步;
  • 最近一段工作保留原文;
  • 文件清单、运行任务和审批状态由程序维护;
  • 上下文溢出后只允许有限次数的“压缩并重试”。

第三版:能恢复、能审计、能调优 ​

  • 原始会话与模型可见视图分开保存;
  • 压缩有开始、结果、替换范围和结束记录;
  • 提交前检查历史没有变化;
  • 区分临时失败、确定性失败和取消;
  • 记录压缩前后 token、摘要调用成本、后续任务成功率和再次压缩间隔;
  • 用真实任务比较摘要格式、保留尾部大小和触发阈值,而不是凭感觉调参。

判断实现是否可靠的一个问题

压缩完成后,换一个完全不知道前情的工程师,只看“摘要 + 保留内容 + 当前工作区”,能不能接着完成任务?如果不能,窗口虽然变短了,Agent 也已经失去继续工作的能力。


本章小结 ​

  • 上下文管理不是一次 LLM 总结,而是入口限长、历史剪裁、区间选择、摘要生成、原子切换和失败恢复组成的流程。
  • codex 本地路径会重建为“近期用户原文 + 交接摘要”;dsh 先剪工具结果,再替换安全旧区间;grok-build 按职责重新装配整份历史;pi 根据压缩记录重建“摘要 + 近期分支”,完整会话树仍保留。
  • 摘要最容易丢的是用户后来补充的限制、失败路径、精确标识符和当前工作。可以由程序维护的事实,不要全部交给模型回忆。
  • 压缩会让摘要插入点之后的旧前缀缓存失效,也会产生一笔额外摘要调用成本;触发时机要同时考虑窗口安全、任务阶段和后续轮数。
  • 最可靠的压缩不是“摘要写得像”,而是失败时不丢历史、重启后能重建、压缩后真的能继续完成任务。

到这里,上下文的问题——装什么、怎么取、怎么压——都有了答案。下一章转向另一个维度:程序崩溃、断线之后,怎样从记录恢复会话;外部资料(最新文档、issue、网页)怎样进入 Agent,见第 18 章。

基于 MIT 协议发布,欢迎转载与贡献。