Harness 工程 · 上下文压缩
学习舱 / Harness 工程 / 上下文压缩

上下文压缩体系:microcompact、collapse 与 autocompact

整理中 深度 · 应用 更新 2026-09-22

前置 Harness 概览、模型调用

上下文压缩体系:microcompact、collapse 与 autocompact

一句话定位:上下文压缩体系是 Harness 给部件 ①上下文 配的自动保洁系统——对话只进不出、窗口却是硬边界,这套体系按代价从零排到不可逆分成五级(L1–L5),能扫垃圾绝不装订,能装订绝不重写。前置:什么是 Harness、模型调用与容错(压缩发生在调用层把请求发出去之前)。分工:Context 工程讲「该往上下文里装什么」的方法论,本篇讲「装不下时怎么自动腾」的机制;「装得下多少」的模型侧极限见长上下文。

先从一下午的「失忆」说起

你用 Claude Code 干了一下午活:读文件、跑测试、翻日志,几十轮下来上下文从两万 token 涨到十五万,一切顺滑。

然后两件事先后发生。你注意到它「忘了」一小时前刚读过的某个文件的关键行——你没删任何东西,那部分内容其实被静默清理了;再往后,屏幕上跳出 Compacting conversation 的提示,整个对话被推倒重写成一份摘要,重写完它连你五分钟前交代过的话都要靠摘要里那句转述才想得起来。

同样是空间不够,为什么有时只是悄悄扫掉一些旧东西,有时却要伤筋动骨地重写整场对话?Harness 靠什么决定动用哪一档?

答案是:压缩不是一道命令,而是一套分级体系。五级手段按代价从零排到不可逆,每级各自防一种失效——大多数会话一辈子停在低档位,只有快撞墙的才动用重武器。

压缩五级阶梯:按代价从轻到重 L1 microcompact 微压缩 清掉过期旧工具结果,留最近几条 零 API 调用 L2 snip 剪枝 剪掉不再影响决策的簿记残渣,腾出量要记账 零 API 调用 L3 工具结果预算 超大结果整体落盘,上下文只留 2KB 预览与路径 零 API 调用 L4 collapse 折叠压缩 早期片段折成摘要投影,原消息不删、可回滚 零 API 调用 L5 autocompact 自动压缩 派一个子代理把整场对话重写成九段摘要,推倒重来 一次不可逆调用 绝大多数会话根本走不到 L5——设计目标是让低档位消化掉所有日常压力

图:压缩五级阶梯——L1 到 L4 都是零成本就地处理,只有 L5 要花钱且不可逆;编号全文沿用

渐进拆解

把上下文窗口想成一张书桌——桌面就这么大,活要接着干,东西只多不少,你必须整理。五级阶梯对应五种整理力度:

  • L1 微压缩 = 把拆完的快递盒扔掉:旧工具结果(一小时前读的文件内容)多半已经没用了
  • L2 剪枝 = 撕掉写完就废的重复便签:重复的包装话术、不再影响下一步的簿记残渣
  • L3 预算落盘 = 大件装箱进储藏室,桌上留张索引卡:完整输出存进磁盘,桌面上只剩预览和地址
  • L4 折叠 = 把整摞旧文件装订成册放进抽屉,目录留桌上:要时按目录取回,原稿一张没丢
  • L5 自动压缩 = 照着相册重新布置房间:新布局按摘要来摆,清爽能继续住,但原来的摆放永远回不去了

例子:水位线上的五级接力

把一条典型的长会话画成水位线,看五级怎么接力——注意每次回落发生在哪个水位:

一局长会话:水位涨落与五级接力 L5 触发线 ≈167K(200K 窗口) 开局 20K 130K 干活三小时 L1 清旧结果 155K 继续推进 L3 落盘大结果 168K 又干一小时 L5 重写(fork 子代理) 45K 九段摘要 + 工作集重建 水位每次逼近上一级也腾不出来的位置,才动用下一级;数字为示意量级,触发阈值见下一节

图:水位线视角——L1/L3 各打一次「清理回落」,最后 L5 一杆打回低位,会话得以继续

大白话:设计压缩体系前先回答三个问题。还能就地抢救吗——先扫垃圾(L1/L2),别急着重写;东西挪走后找得回来吗——落盘留路径、折叠留目录(L3/L4),丢的都能取回;真要重写时保什么——保结论、保进度、保工作集,不保推理的草稿纸(L5)。一句话:先清理、可再生、最后才重写,重写时丢过程留结论。

技术定义:五级各管一段

多级压缩体系是 Harness 中在每次请求前对消息列表做递进式瘦身的一组机制。先交代一个口径:这套机制的公开文档极少,下文的层级划分、参数与结构主要来自社区对 Claude Code 的逆向分析,凡关键数字都以「逆向口径」标注。

这套「每次请求前」的定位可以画成一条检查流水线——哪些级常驻、哪些级水位吃紧才上,一眼看清:

五级什么时候出手:每次请求前的递进检查 常驻:L1–L3 的预算与清理规则每次请求前都过一遍 L1 microcompact 微压缩L2 snip 剪枝L3 工具结果预算 清过期旧工具结果只留最近几条剪掉簿记残渣腾出的量记进 freed 账本单结果超 5 万字符不截断整体落盘,留 2KB 预览 水位仍吃紧,才逐级上 腾够就照常发请求 还不够,才动用重武器 L4 collapse 折叠压缩L5 autocompact 自动压缩工作集重建 早期片段折成投影原消息不删,可回滚快撞墙才触发(200K 窗口约 167K)fork 子代理重写九段摘要最近文件、激活技能、工具清单压缩牺牲历史,保住当下 请求发出后仍被服务端打回(Prompt-Too-Long 类错误):从最旧丢起逐段重试,最多三次,仍装不下才报错

图:五级在请求前检查流水线中的位置——L1–L3 常驻,腾不够才逐级上 L4/L5,重写后重建工作集;服务端打回另有从最旧丢起的兜底

L1 microcompact 微压缩

目标是「过期」的工具结果——那个一小时前读的文件,现在的决策大概率用不上。按缓存温度分两条互斥路径(为什么要分,见前文加餐):缓存还热时构造 cache_edits 编辑块,让服务端把旧结果从缓存里原地删除,本地消息不动、缓存不冷(社区逆向口径);缓存已冷(反正要重建,空闲超过阈值——逆向口径约 60 分钟)时才直接改写本地消息,把旧结果替换成 [Old tool result content cleared] 占位符,只保留最近几条。

两个条件可能同时成立,此时时间触发优先——直接改写并跳过缓存编辑(逆向口径)。这也是它和 L5 的本质区别:全程没有任何模型参与,纯文本手术。

两条路径的分岔画成图——决定走哪条的不是水位,是缓存温度:

L1 为什么分两条路:缓存温度说了算 目标:过期的旧工具结果 一小时前读的文件,现在的决策大概率用不上 先看缓存温度 缓存还热 → 缓存编辑缓存已冷 → 直接改写 构造 cache_edits,服务端原地删本地消息一字不动,缓存不冷空闲超过阈值(逆向口径约 60 分钟)旧结果换占位符,只留最近几条 两条路径互斥;两个条件同时成立时,时间触发优先——直接改写并跳过缓存编辑(逆向口径) 全程没有任何模型参与,纯文本手术;改写还热的消息会击穿缓存,迫使整个前缀重新计费

图:L1 的两条互斥路径——缓存温度决定走哪条,钱在这里决定了代码的形状

L2 snip 剪枝

剪掉写完就废的重复包装文本、冗余的簿记——零成本垃圾收集。它还有一个容易被忽略的职责:记账。剪掉多少 token 要如实记进 freed 账本,否则阈值计算仍按剪枝前的水位算,会出现「token 已经腾出来了、自动压缩还是被误触发」的怪事。

L3 工具结果预算(toolResultBudget)

单个结果超过预算(逆向口径默认 5 万字符)时不截断,而是把完整输出整体落盘,上下文里只留约 2KB 预览和文件路径。为什么不截断?截断意味着永久丢失——模型若在 500 行之后才发现真正要找的东西,就永远找不回来了;落盘则保留了「判断要不要回读」的通道。同一条用户消息内所有工具结果合计还有一层总预算(逆向口径 20 万字符)。

这个预算有一处显眼的豁免:读文件工具的阈值被显式设成无限大(逆向口径)。原因是一对循环依赖:预算超限靠「落盘后用读文件工具回读」来补救,若读文件工具自己的输出又进预算检查、再被落盘,补救通道自己就把自己的路堵死了——豁免它、靠它自身的输出上限控制体量,这个环才解得开。

L4 collapse 折叠压缩

像数据库的视图——底层消息数组一个字不删,只是查询时叠加一层「投影」:早期片段被折成摘要,摘要存在独立的存储里(暂存,类比 git 暂存区),查询时投影生效(提交),删掉投影就完整回滚。它和 L5 是竞争关系:两者抢同一份 token 空间,折叠激活时自动压缩会被抑制——重写会毁掉折叠精心保留的细粒度上下文。

压缩请求本身超限被服务端打回(Prompt-Too-Long 类错误)时,还有一道响应式压缩兜底:把最旧的轮次从上下文里逐段丢弃再重试,最多重试三次,仍装不下才向用户报错——主动体系按预算层层设防,被动兜底按「从最旧丢起」逐级降级,两条路都到头才认输。

L5 autocompact 自动压缩

最后手段,下一节展开。

作用域与工作集

这一整套都发生在单个会话之内——会话生来就是隔离的边界,各自的上下文、各自的压缩状态,A 会话的清理永不波及 B。

但隔离在压缩语境下还有一层更细的含义:逆向分析发现,缓存编辑型的微压缩只在主线程运行——fork 出去的子代理不该往共享状态里注册工具结果,否则主线程按自己会话的清单去清理时,会「删一个自己没有的东西」、徒增混乱;防误删的保障就是这条运行边界,具体实现细节两篇逆向分析都未展开。

而 L5 之后重建的那份「最近读过的文件(最近 5 个、单文件 5K token 以内)+ 激活中的技能(合计 25K 以内)+ 工具清单」,就是会话的**工作记忆(Working Memory)**的实体化:压缩牺牲历史,但必须保住当下干活的那一小摊。

动手:autocompact 的阈值、九段与 fork

五级里只有 L5 需要展开讲——它是唯一花钱、唯一不可逆、唯一动用模型的一级,下面专门拆它。

阈值怎么算

社区逆向的公式:触发线 = 窗口 − 最大输出 token − 安全缓冲(200K 窗口约 167K 处触发,即约 83%;Sonnet 5 的 1M 窗口官方口径约 967K 处触发,此前的口径约 934K)。注意这是绝对余量而不是固定百分比——不管窗口多大,都要给「接下来要生成的输出」留够位置。

还有个常被忽略的细节:当前水位不是客户端逐个数出来的(逆向口径:误差能到三成),而是以服务端每次响应报回的用量为锚点、只估算增量,误差压到 5% 以内——官方在 /context 命令的计数上也采用了同款策略(更新日志 2.1.261:计数 API 不可用时退回本地估算)。

为什么 fork——派生一个独立的子代理(产品篇叫分身)——去压缩,而不是主循环自己做

社区逆向分析里能考到的原因是缓存经济学:fork 出的子代理共享主会话的提示词缓存,压缩这次「输入是整场对话」的巨大请求,绝大部分输入命中缓存、实际只为压缩指令和输出付费;反过来若在主循环里自己压,改写中的上下文会把缓存搅冷,等于全价跑一次最贵的请求。

此外还有两条说得通的工程推断(作者分析):

  1. 主循环自己摘要等于「一边搬砖一边给自己写传记」,压缩指令和对话内容搅在同一个推理过程里,摘要质量差;子代理带着干净的任务专门走查,效果好得多。
  2. 压缩的中间态(半途的分析草稿、试错的请求)全部留在一次性子代理里,主循环的缓存与状态不被污染。

代价也很清楚:一次昂贵且不可逆的调用——所以它是五级的最后一级。

L5:一次不可逆的重写是怎么走完的 水位触线 167K / 200K fork 子代理 主循环暂停等待 两阶段走查 analysis 块逐条走查 summary 块九段成稿 丢过程留结论 analysis 不进结果 工作集重建 最近文件 · 激活技能 · 工具清单 没有这步恢复,模型会重读刚编辑的文件,做出前后矛盾的改动

图:L5 全流程——fork、两阶段走查、丢 analysis 留 summary、工作集重建,一步都不能省

九段摘要在保什么

逆向分析还原的压缩提示词要求先在 analysis 块里按时间顺序逐条走查整场对话(用户意图、方案、决策、文件名、报错与修复),再在 summary 块里产出固定九段:主任务与目标、关键技术概念、文件与代码改动、报错与修复、问题求解过程、用户说过的每句话、待办任务、当前进展、可选的下一步。走查是推理,成稿是结论——推理过程用完即弃(成稿时整块剥离),只留九段结论进场。特别值得注意的是「用户说过的每句话」这一段:把用户原话单独设段,等于宣告原话的地位高于任何转述。

伪代码把五级决策串起来(注释里的 L1–L5 呼应上文阶梯图;L1–L3 的预算与清理规则多数常驻生效,这里按「水位吃紧时的接力次序」简化呈现):

def before_each_call(ctx):                     # 每次请求前过一遍压缩检查
    used = server_anchored_tokens(ctx)         # 服务端用量为锚,只估增量
    threshold = WINDOW - MAX_OUTPUT - BUFFER   # 绝对余量:输出 + 缓冲
    ctx = microcompact(ctx)                    # L1 清旧工具结果(保缓存温度)
    ctx = snip(ctx)                            # L2 剪簿记,freed 如实记账
    if used - ctx.freed < threshold:
        return ctx
    ctx = budget_to_disk(ctx)                  # L3 大结果落盘,留 2KB 预览
    if used - ctx.freed < threshold:
        return ctx
    ctx = collapse(ctx)                        # L4 折叠成投影,可回滚
    if used - ctx.freed < threshold:
        return ctx
    return auto_compact(ctx)                   # L5 最后手段,不可逆

def auto_compact(ctx):
    summary = fork_agent.summarize(ctx)        # analysis 走查 + 九段 summary
    working_set = restore(ctx)                 # 最近文件/激活技能/工具清单
    return [summary, working_set]              # 丢推理过程,保工作集

它还有一道自己的保险丝:上线早期曾有过一段真实事故(逆向口径,2026-03-10 的遥测:一千多个会话压缩失败后连续重试五十次以上,最多的三千余次,合计每天浪费约 25 万次调用),后来加了连续失败上限 3 次的熔断——同一种失败连着三次(中途成功一次即重新计数),就不再自动触发,把会话交还到人手里。这个「压缩器自己也需要容错」的教训,与调用层的重试到顶换降级如出一辙。

同一概念的叫法对照:

本篇用词文献中的其他叫法深挖去处
压缩五级阶梯(编号为本篇按代价的排序)五层递进式上下文防护 · 压缩管线 · 纵深防御本篇
microcompact微压缩 · 工具结果清理 · cache_edits工具执行
collapse折叠压缩 · Context Collapse · 视图投影本篇
autocompact自动压缩 · compaction · 摘要压缩状态机与 Checkpoint
工作记忆Working Memory · Session Memory · 工作集记忆机制

边界与取舍:压缩是一场有损交易

越晚压,单次损失越大;压得越勤,节奏断得越碎

满到 90% 才压,意味着要一口气扔掉大半个会话,摘要再好也有细节永远消失;有开发者社区主张更早动手压缩、宁要多次小损失也不要一次大失忆——两种策略没有绝对对错,取决于任务对历史细节的依赖程度。两头的失败模式倒是确定的:从不主动管理、全靠 L5 兜底的会话,重写后「变笨」的概率显著更高。

静默清理是有信任代价的

被清的内容自己不知道被清——模型不会意识到「那个文件我读过了但内容已经被腾走了」,它只会自信地基于残缺记忆往下走。有用户把工具返回当外接记忆用,微压缩静默清空后 Agent 表现得健忘又武断,而且无从自我诊断(相关反馈在官方仓库以「未计划修复」关闭)。给你的启示:依赖工具结果长期驻留的设计本身就脆弱,重要事实该进记忆机制,而不是指望对话记录永远完整。

不是每个 Agent 都需要五级

一次性脚本对话、几分钟就结束的任务,L5 兜底甚至 L1 都未必触发;五级阶梯是为「一开一整天、上下文滚雪球」的长时程 Agent 配置的。级数跟着会话时长走——防护层级要和任务量级匹配,给短命会话配全套纵深防御是过度设计。

最后的坏味道预警

如果你的 Agent 一压缩就断片(重写后反复重读文件、推翻刚做的决定),缺的不是更好的摘要,而是压缩后的工作集重建;如果压缩反复触发停不下来,缺的是熔断。这两个坑的系统性解法——中断恢复与行为干预——在状态机与 Checkpoint里展开。

自测

合上文章先自己答,再展开对照:

autocompact 为什么要 fork 一个 Agent 去压缩而不是主循环自己做?

社区逆向可考的原因是缓存经济学:fork 出的子代理共享主会话的提示词缓存,压缩这次「输入是整场对话」的巨大请求绝大部分命中缓存、只为压缩指令与输出付费;主循环自己压反而会把缓存搅冷、全价跑最贵的请求。两条工程推断:其一,子代理带着干净任务专门走查,摘要质量好过压缩指令与对话内容搅在同一个推理过程里;其二,压缩的中间态全部留在一次性子代理里,主循环的缓存与状态不被污染。代价是一次昂贵且不可逆的调用,所以它是五级阶梯的最后一级;压完还必须做工作集重建(最近文件、激活技能、工具清单),否则模型会断片。

多级压缩是不是过度设计?每一级各自防什么失效?

不是过度设计,五级各防一种明确的失效。L1 微压缩防「过期工具结果占着位置」——30 分钟前的文件内容大概率没用了,且分缓存冷热两条路径是为了不击穿提示词缓存;L2 剪枝防「簿记残渣累积」并负责把腾出的量如实记账,防的是「已腾空间却被误判要压缩」;L3 预算落盘防「大结果截断即永久丢失」——落盘保留回读通道;L4 折叠防「早期上下文被粗暴丢弃」——原消息不删、可回滚,且与 L5 互斥以免毁掉细粒度上下文;L5 重写防「真的撞墙」——兜底但有信息损失。级数设计的原则是按代价递增排列、低档位消化绝大多数压力,短命会话可以只配低档位,所以说它不是过度设计,但也不是每个 Agent 都需要全套。

参考与延伸

  • Claude Code 官方更新日志(2026-09):2.1.247 记录 Sonnet 5 的 1M 窗口在约 967K token 处自动压缩、2.1.261 记录计数 API 不可用时退回本地估算 — code.claude.com/docs/en/changelog
  • Claude Code 压缩管线逆向分析(2026):五级管线、microcompact 双路径、九段结构、熔断器数据 — harrisonsec.com
  • Claude Code 上下文压缩逆向分析(2026):阈值公式、Read 工具豁免、压缩后恢复预算 — finisky.github.io
  • microcompact 静默清理工具结果的争议案例(2026,未计划修复)— github.com/anthropics/claude-code/issues/57788
  • Pi compaction 官方文档精读(2026-10-24,对照 pi main):触发线 contextWindow−reserveTokens(默认 16384,可按模型覆盖)、切点 keepRecentTokens(默认 20000)、绝不在 tool result 处切、摘要请求禁用 prompt-cache 写入、工具结果序列化截断 2000 字符、/tree 分支摘要与压缩双机制共用累积文件追踪 — 与本站「读 Pi 学 harness」18-22 篇源码锚点互证

本系列的后续学习路线(推荐顺序):工具执行 → 权限机制 → 记忆机制 · Skill 机制 → 状态机与 Checkpoint → 多 Agent 编排。

本节点大纲与自测题(8 个子点 · 2 问)

大纲

  • 多级压缩体系:为什么不用单一 autocompact
  • microcompact:时间触发型与缓存编辑型
  • collapse 折叠压缩与暂存-提交
  • autocompact:阈值计算、九段结构化压缩、fork Agent 压缩
  • toolResultBudget:为什么对 Read 工具不生效
  • Session 隔离与 Working Memory 底层
  • 阶梯降级 Compaction
  • 五层递进上下文防护(Token 爆炸)

自测——答不上来就是缺口

  • autocompact 为什么要 fork 一个 Agent 去压缩而不是主循环自己做?
  • 多级压缩是不是过度设计?每一级各自防什么失效?