长任务跑到一半「变笨」,多数时候不是模型不行,而是上下文窗口被日志、旧判断和过期搜索结果挤满,关键约束再也浮不上来。本文以 Claude Code 为例,讲清上下文窗口的预算构成、分层压缩与截断策略、子代理隔离机制,以及长任务下如何主动分配上下文预算。

为什么上下文是 Agent 的核心战场

把 Agent 拆开看,可以写成 Agent = Model + Harness:模型负责推理,Harness(宿主框架)负责取信息、调工具、推进任务。上下文管理恰恰属于 Harness 这一侧——它决定每一轮请求时,窗口里保留什么、清掉什么、把什么写到窗口之外。

上下文窗口就是模型的工作内存,容量有限。Claude Code 每次调用模型时,窗口里通常装着这些内容:

组成部分 内容 性质
会话初始化 System Prompt、CLAUDE.md、无条件规则、Auto Memory 索引 固定开销
工具描述 内置工具定义、MCP 工具名与已加载的 Schema 固定或按需
Skills 默认只加载名称和描述,正文调用后才进入 按需加载
对话历史 用户消息与模型回复 持续增长
工具调用及结果 参数、返回值、日志、文件内容 持续增长
子代理汇报 子代理回传的摘要与少量元数据 按需

固定开销决定会话起步时还剩多少余量;动态内容才是长任务里的主要增量。一次几百行的文件读取就是几千 Token,一次全量测试可能刷出几千行日志。同样是二十轮对话,纯聊天和每轮都读文件跑测试,窗口占用可能差出几倍——所以不能用「聊了多少轮」判断上下文压力。

窗口变长还会带来两个隐性退化:

  1. Lost in the Middle:模型对长上下文开头和结尾的内容利用较好,夹在中间的关键信息容易被忽略。第 3 轮定下的「不许动数据库 schema」,到第 30 轮可能已经被搜索结果埋在中间。
  2. Context Rot(上下文腐化):旧判断、重复搜索、已解决的问题和无用的日志持续占用注意力,信噪比越来越低。接近上限时,模型还可能因为剩余输出空间不足而过早收束任务,也就是所谓 Context Anxiety。

窗口变大只能推迟这些问题,不能消除。这就是为什么上下文管理值得当作一等工程问题来对待。

压缩与截断:按「能否重新获取」分层处理

Claude Code 治理上下文的总体顺序是:先清理可重新获取的工具结果,再压缩对话历史,仍接不住才考虑重置或拆分子代理。处理越靠后,信息损失和调度成本越高。

第一层:工具结果清理(引用化)

工具输出占空间最大,但大多可以重新获取——文件可以再读、命令可以再跑。这层处理的信息损失最低,也无需额外调用模型:

  • 大结果写盘:超过阈值(默认约 5 万字符,Bash、Grep 等工具阈值更低)的工具输出会写入会话存储目录,窗口里只留预览和文件引用。Read 工具是个例外,它豁免这条规则,否则会出现「读文件 → 太大写盘 → 按路径又读回来」的死循环。
  • MicroCompact:把较旧的工具结果内容替换为占位标记(如 [Old tool result content cleared]),调用关系和 tool_use ID 仍保留在消息链里。不直接删整条消息,是因为后续消息可能还引用前面的调用,删掉会导致消息链断裂,模型也无法判断哪些操作已经完成。
  • Snip:删除一段历史区间后重连消息链,释放出的空间会反馈给后续的压缩判断,避免刚腾出的空间又触发过度压缩。

边界很清楚:可重放的输出(读文件、搜索、日志查询)适合清理;有副作用的操作(Edit、Write)不能靠重放恢复状态,必须回到文件系统核对;子代理的分析结论这类一次性产物也不能随便清,丢了就真丢了。

第二层:历史压缩(摘要化)

工具结果清完还不够,才轮到对话历史。压缩不是简单一句「总结一下」,而是一条渐进流水线:先把已完成的消息组折叠成状态快照(本地保留完整历史,模型只看折叠视图),仍不够时才执行 AutoCompact,用结构化摘要整体替换旧历史。

摘要会保留任务目标、重要约束、关键决策、当前进度和代码线索,但不保留文件全文、检索结果和测试输出——后续需要精确内容时应重新读取或重新执行。手动 /compact 与自动压缩使用同类能力,区别在于手动调用可以显式指定保留重点:

/compact 保留数据库 schema、支付状态机和当前失败用例

压缩完成后,本地会话记录里会写入边界标记,告诉后续加载器「历史在这里已被摘要替换」。恢复时也不应把之前读过的文件全量重新加载,只恢复当前任务必需的文件,否则很容易陷入「压缩 → 膨胀 → 再压缩」的循环。系统还带有断路器保护:如果摘要后窗口迅速再次填满,连续失败数次后会停止自动压缩,防止反复空耗 API 调用。

子代理:用隔离换主会话干净

分析几千行日志、跨文件搜索、独立代码审查这类支线任务,主会话不需要看到全部过程。子代理(Sub-agent)在独立的上下文窗口里完成这些工作,只把结论和必要证据回传主会话;全文日志、中间试错和被推翻的猜测都留在子代理自己的历史里。

子代理有两种上下文模式:普通子代理不继承父会话的消息历史,但会加载工具权限、CLAUDE.md 和 Git 状态等运行上下文,适合隔离搜索噪声;Fork 模式则继承父会话上下文,适合背景依赖重的支线。主会话压缩后也不会重新载入子代理的完整过程,只恢复摘要。

适合拆出去的任务有个共同点:过程不重要,结论重要。任务小到几步能完成、子任务之间要频繁互相等待、或者边界本身说不清时,调度和回传摘要反而带来额外开销。

长任务的上下文预算分配

长任务落地的核心原则是一句话:窗口只放眼下要用的材料,可复查的状态写进文件,换会话时只交接下一步所需的结论。

具体可以拆成三条动作:

  1. 任务切分里程碑:把大任务切成阶段,每个阶段结束时主动压缩或更新状态文件,而不是等系统贴线被动触发。/context 命令可以随时查看当前各类占用。
  2. 状态外化:PLAN.md、design.md、任务列表等文件保存目标、取舍和下一步;行号、失败 SQL、接口细节留在文件里,不进窗口。压缩指令里写明要保留的约束和断点。
  3. 结构化 handoff:压缩多次后细节损失会叠加,到一定程度继续压不如重开。新会话要从断点继续,交接材料至少包含:任务目标与完成标准、已完成并验证过的部分、当前断点(精确到文件、函数或失败用例)、不能动的约束、已排除的方案及原因、当前故障的日志与复现步骤、接手后的第一个动作。只写一句「继续完成剩余任务」,等于让新会话在缺信息的状态下裸奔。

一个常见的经验法则是按任务规模选策略:单文件改动用单会话,工具结果自动清理就够;模块级任务主动在阶段末压缩;跨模块重构把搜索、审查、日志分析派给子代理;长期迭代则靠里程碑加 handoff 维持接力。

工程实践清单

  • 定期用 /context 查看窗口占用,出现重复搜索、忘记早期约束的苗头就主动压缩,并指明保留重点。
  • 大段日志和测试输出落盘,窗口里只留失败摘要、复现命令和关键调用栈。
  • 子代理的读写范围说清楚:默认多做只读和验证,主线修改留在主会话。
  • handoff 文件字段固定,交接前先核对有没有漏掉边界条件和失败原因。
  • 规则文件写得越长,起步余量越少——固定开销也是预算的一部分,别把 CLAUDE.md 写成百科全书。

小结

  1. 上下文窗口是有限的工作内存,固定开销决定起步余量,动态的工具结果和对话历史才是长任务的主要增量。
  2. 治理顺序按信息能否重新获取分层:先清理工具结果(写盘、占位替换),再压缩历史(折叠、结构化摘要),最后才是重置与交接。
  3. 子代理的价值是上下文隔离——支线过程留在子会话,主会话只收结论,并行提速只是副产品。
  4. 长任务靠「窗口放当前材料、文件系统存任务状态、handoff 传递下一步」三者配合;判断管理好不好,不看窗口装了多少,而看压缩、换会话之后任务还能不能接上。