跳到主要内容

headroom — 本课题摘录

读了哪几篇: 01-pipeline-and-router(压缩主干与分诊)、03-ccr(把有损压缩做成可逆)、04-cache-safety(活区与缓存安全)。 其余三篇(各压缩器内部、代理层、输出与记忆)本轮没读。

整个项目就干一件事:历史怎么压。所以它是本课题"决定一"这一支最深的一份材料。

它对本课题回答了什么

决定一最重要的一条:删历史消息是亏的,而且亏得离谱

它把"历史太长了,删几条老消息"这个第一直觉,在自己的代码库里判成了架构级错误。

先算账。 同一段前缀在厂商那里有三种计价:

token 类型相对普通输入的价钱
普通输入(没进缓存)1.0
缓存写入1.25(长档 2.0)
缓存读取0.1

关键在于:缓存命中要求字节完全相同,而且是"前缀"语义——你在第 K 条消息上改一个字节,第 K 条之后的所有缓存全废。

于是"删一条老消息省 2000 token"的真实收支:

省下: 2000 token 不再被读 → 2000 × 0.1 = 200
赔上: 它后面 50000 token 的缓存作废,
下一轮要重新写一遍 → 50000 × (1.25 − 0.1) = 57500

净亏两百多倍。

(依据:Agent 库 · Headroom · 为什么「删历史消息」是错的 —— 缓存读取只要 0.1 倍、写入 1.25 倍,而命中要求字节完全相同且是前缀语义;删一条老消息省 2000×0.1=200,却让后面 50000 token 缓存作废要 50000×(1.25-0.1)=57500,净亏两百多倍)

这条把本课题的"历史怎么压"整个翻了个面。 前面我们看了 codex 的"就地压缩"、opencode 的"总结+裁剪"、openmanus 的"滑窗切掉最前面"—— 按这份账,凡是动了历史前段的做法,都在赔钱。

它自己的整改结论一句话:"不动字节是神圣的,只压活区"。

决定一:活区 —— 唯一允许改的那个窄窗口

从请求体的顺序看,越靠上越"冷"(在缓存里、绝不能动),越靠下越"热":

┌───────────────────────────────┐
│ 系统提示 │ ← 缓存热区
├───────────────────────────────┤ 一个字节都不动
│ 工具定义 │
├───────────────────────────────┤
│ 客户自己标了缓存标记的前缀 │ ← 冻结区
├───────────────────────────────┤
│ 中间的历史轮次 / 最新助手消息 │ ← 仍然不动
├═══════════════════════════════┤
│ 最新的那条用户消息 │ ★ 活区(唯一可改)
│ ├ 工具结果块 → 可压 │
│ ├ 文本块 → 可压 │
│ └ 工具调用 / 思考 → 排除 │
└───────────────────────────────┘

(依据:Agent 库 · Headroom · 为什么「删历史消息」是错的 —— 活区是唯一可改的窗口——系统提示、工具定义、已标缓存的前缀、中间历史与最新助手消息一律不动,只有最新那条 user 消息里的 tool_result 与 text 块可压,tool_use/thinking 排除)

活区怎么划出来:一个地板、一个天花板、一张黑名单。

边界怎么定
地板不猜哪些消息被缓存了,读客户自己打的缓存标记,取最高那个下标 +1
天花板从尾巴往前找第一条用户消息;一旦扫到低于地板的下标就直接放弃
黑名单工具调用块、思考块等不能碰

"不猜,读标记"这一条是关键工程判断。 缓存边界在哪只有调用方知道,猜错的代价是几十倍的钱。

决定三:有损压缩配一张取货单,让它变成可逆

它先把有损压缩的死穴说清楚:

压缩器把 500 行的搜索结果砍成 20 行,省了 95%。 问题出在第 7 轮:模型突然要看第 213 行。 那行已经不在了,模型只能重跑一遍工具——省下的 token 连本带利吐回去,还多花一次工具调用。

它的判断写在源码注释第一句:可逆压缩胜过不可逆压缩。

三步:

谁干的干什么
压缩器砍内容,同时在砍掉的位置留一个带 hash 的标记
仓库原文按同一个 hash 存进本地仓库
代理 + 模型模型调一个"取回原文"的工具,代理查仓库、把原文塞回去、续跑对话

(依据:Agent 库 · Headroom · 把有损压缩做成可逆:原文不删,模型可以要回来 —— CCR 三步是压(砍内容并在原位留带 hash 的 marker)、存(原文按同一 hash 存进本地仓库)、取(模型调 headroom_retrieve,代理查仓库把原文塞回去续跑);源码注释第一句写的是「可逆压缩胜过不可逆压缩」)

这条把 griptape 的 off-prompt 推进了一步: griptape 是"大结果一开始就不进提示词,只给引用"; headroom 是"先进去,压小了,但留一张能换回原文的票"。 两者可以合用:小的直接进、大的转存、压掉的留票。

决定三补充:主干只做分诊,不压缩

主干代码一个字节都不压缩,它做的是分诊: 判断这段内容是 JSON、代码、搜索输出还是日志,交给对应的压缩器。

理由说得很实在:用同一个压缩器对付它们一定是灾难——把源码交给语义压缩器,标识符会被删掉;把日志交给 JSON 压缩器,它根本解析不了。 (依据:Agent 库 · Headroom · 一段内容怎么被路由到对的压缩器 —— 主干本身不压缩只做分诊,因为用同一个压缩器对付 JSON/代码/日志一定是灾难——源码交给语义压缩器会删掉标识符、日志交给 JSON 压缩器根本解析不了)

主干的七步里有两条兜底规则值得单独记:

规则说的是什么
无损先赢,有损兜底先试无损,不够才上有损
变大就退回原文压完反而更大,就当没压过
每一步失败都原样还回去任何一步出问题,把原始消息原样返回

"变大就退回"这条看着废话,但压缩器在小输入上经常真的会变大(元数据开销)。没有这条就会出现"越压越大"的荒诞情况。

它还有熔断:压缩本身出问题时整条链退化成不压。

它没回答什么

  • 循环怎么写——它是挂在循环外面的代理层。
  • 停止条件、工具调用——完全不管。

坑与代价

  • 整套判断建立在"厂商有前缀缓存且这么计价"之上。 换一家计价方式不同的厂商,"删历史是亏的"这个结论可能不成立。

    判断(无锚): 但"改历史前段会让缓存失效"这条是前缀缓存的本质,只要厂商用前缀缓存就成立;变的只是亏多少。 如果错,会错在: 如果我们用的模型根本没有前缀缓存(本地模型、小厂商),那这一整套的收益归零,直接删历史反而最简单。

  • "只压活区"意味着压缩能力有上限。 活区就那么大,再怎么压也压不出中间历史那么多空间。 长会话最终还是要面对"中间那一大坨怎么办"。
  • 可逆压缩要求有一个本地仓库,而且要管过期。 这是我们的最小原型暂时不需要的基础设施。

    判断(无锚): 但"压之前先存原文"这个动作本身几乎不要钱——第一版就可以做,哪怕只是存在内存字典里。 如果错,会错在: 如果压缩本来就很少触发(短任务),那存原文的机制永远用不上,是纯粹的复杂度。