← 返回文章归档

Headroom:把 AI Agent 的上下文压缩做成本地代理层

拆解 chopratejas/headroom:它不是单个压缩算法,而是用 wrap、透明代理、provider-specific live zone、块级压缩器和 CCR,把工具输出压缩变成对 Agent 透明的一层本地基础设施。

#Headroom#AI Agent#Context Compression#LLM#Rust#Python

项目:chopratejas/headroom
分析版本:main 分支 bc12ace
一句话结论:Headroom 的核心不是“压缩文本”,而是把 Agent 发给模型前的上下文变成一条可拦截、可判定、可局部改写、可追回原文的本地代理链路。

先说它在解决什么

AI Agent 的上下文浪费,通常不是来自用户手写的几句话,而是来自 Agent 自己读到的东西:工具输出、日志、搜索结果、RAG chunk、文件内容、git diff、历史消息。它们往往有三个特点:

  • 长,几千到几十万 token 很常见。
  • 结构化程度高,比如 JSON 数组、编译日志、grep 结果、统一 diff。
  • 真正有价值的信息密度低,模型不需要逐字看到全部原文。

普通的“压缩库”只能解决一小段文本怎么变短的问题。Headroom 试图解决的是另一个层级的问题:不改 Agent 主体逻辑,也尽量不改模型调用代码,在请求进入 LLM provider 之前,把上下文中适合压缩的部分改写掉。

README 里把它称为 Context Optimization Layer。源码拆开看,这个说法是准确的:Headroom 不是一个单点算法,而是 Python CLI、Rust proxy、provider-specific JSON walker、内容检测器、压缩器、CCR 存储和 MCP retrieve tool 组成的一层本地基础设施。

Headroom 总览架构图

图 1:Headroom 的主路径是“Agent/App -> 本地代理 -> live zone 定位 -> 内容检测 -> 压缩器 -> CCR -> LLM provider”。

仓库结构:Python 做入口,Rust 做热路径

这个仓库有两个明显的语言边界。

Python 侧主要在 headroom/

  • headroom/cli/:命令行入口,包含 proxywrapmcpmemorylearn 等命令。
  • headroom/providers/:不同 Agent/provider 的启动环境适配,比如 Claude、Codex。
  • headroom/api/headroom/core/:给 Python 用户直接调用的库接口和兼容层。

Rust 侧在 workspace 里:

  • crates/headroom-core:核心压缩、live zone、CCR、内容检测。
  • crates/headroom-proxy:HTTP 透明代理,负责拦截、分类、压缩、转发。
  • crates/headroom-py:通过 maturin 暴露给 Python 的扩展模块。
  • crates/headroom-parity:用于锁定 Python/Rust 行为一致性的校验。

pyproject.toml 里包名是 headroom-ai,核心依赖包括 tiktokenlitellmclickrich。proxy extra 里则有 fastapiuvicornmcpzstandardonnxruntimesqlite-vec 等依赖。这个组合说明它不是只想做一个纯算法包,而是要承担运行时代理、CLI、MCP 和本地存储。

Rust workspace 的 serde_json 打开了 preserve_orderarbitrary_precisionraw_value。这点很关键:Headroom 不是把 JSON 读出来再随便序列化回去,它需要尽可能保持请求字节稳定,尤其是 prompt cache 相关区域。

接入方式:library、proxy、wrap 和 MCP

Headroom 给了几种层级不同的接入方式。

最直接的是 library:你把一段内容交给 Headroom,它返回压缩后的内容。这个适合自己控制调用链的应用。

更有意思的是 proxy:Agent 仍然调用 OpenAI、Anthropic 这类 provider 兼容接口,但 base URL 指向本地 Headroom 代理。代理收到请求后,判断是否可压缩,局部改写请求体,再转发给真实 provider。

headroom wrap 则把这件事再往前包了一层。比如启动 Claude 或 Codex 时,它会先拉起本地 proxy,再把对应的环境变量指向 proxy:

cmd = [sys.executable, "-m", "headroom.cli", "proxy", "--port", str(port)]

Codex 适配里会把 OpenAI base URL 指向本地代理:

def proxy_base_url(port: int) -> str:
    return f"http://127.0.0.1:{port}/v1"

Claude 适配则走 Anthropic 风格的 base URL,并且会处理 MCP 配置。这里的关键不是“自动启动一个服务”这么简单,而是把不同 Agent 的 provider 配置差异收口在 wrap 层:用户继续启动原来的 Agent,Headroom 插进网络路径。

MCP 的角色也很重要。压缩后如果上下文里只留下一个 CCR marker,模型需要在确实需要原文时调用 headroom_retrieve 取回原始 payload。也就是说,Headroom 不是单纯丢掉信息,而是把“全量塞进 prompt”变成“必要时按 key 取回”。

代理门控:不是所有请求都压缩

透明代理最怕过度聪明。Headroom 的 proxy 入口在 crates/headroom-proxy/src/proxy.rs,实际拦截前有很明确的门控条件:

let should_intercept = state.config.compression
    && method == Method::POST
    && compression::is_compressible_path(uri.path())
    && is_application_json(req.headers());

这四个条件把范围压得很窄:

  • 开启压缩配置。
  • 只处理 POST。
  • 只处理已知可压缩 provider endpoint。
  • 只处理 JSON 请求体。

endpoint 分类在 crates/headroom-proxy/src/compression/mod.rs

pub enum CompressibleEndpoint {
    AnthropicMessages,
    OpenAiChatCompletions,
    OpenAiResponses,
}

对应路径主要是 /v1/messages/v1/chat/completions/v1/responses。不在这个列表里的请求,代理会按原始请求转发。这个设计牺牲了一些覆盖面,但换来了更清晰的安全边界:只有理解 shape 的请求才进入压缩路径。

代理还会根据认证模式构造 CompressionPolicy。源码里能看到对 auth mode、PAYG fallback、volatile detector、cache drift detector 的处理。这里隐含的工程判断是:压缩上下文不是纯文本变换,它会碰到计费模式、provider 语义、缓存稳定性和请求兼容性。

live zone:只改可以改的那一段

Headroom 最值得拆的是 live zone 这个概念。它不是简单遍历整个 JSON,把所有长字符串都压缩掉。crates/headroom-core/src/transforms/live_zone.rs 里明确把压缩限制在 message 内部,并且要找出一段可以变化而不破坏 prompt cache 的区域。

Headroom live-zone 决策流

图 2:Headroom 先判断请求是否可压缩,再按 provider shape 找 live zone,最后只对 live zone 内的块做内容检测和压缩。

为什么需要 live zone?因为很多 Agent 请求会依赖 provider 的 prompt cache。前缀一旦变化,缓存就会失效。Headroom 的策略是:

  • frozen prefix 尽量不碰。
  • 最新一轮用户消息、工具结果、日志输出等更可能是可变区域。
  • tool_usethinkingredacted_thinkingcompaction 等块会形成热区边界。
  • 只对边界内的候选块做压缩。

这个设计把“节省 token”和“保住缓存命中”放在同一个问题里处理。很多压缩方案只关注压缩率,但 Agent 真实成本里还有缓存命中率、延迟和 provider 的请求格式兼容性。

更关键的是,Headroom 不是重建整个 JSON。live zone 模块强调 byte-range surgery:定位要改的字节范围,只替换那段内容,外部字节保持不变。测试里也专门检查压缩块前后的 prefix/suffix 字节哈希保持一致。

Provider-specific walker:不要假装所有聊天协议长一样

Headroom 没有写一个“通用 JSON 递归遍历器”去猜哪里能压。它为不同 provider 做了不同 walker。

Anthropic messages 的结构是 messages[].content[] block。live_zone_anthropic.rs 会解析 messages,计算 frozen count,再交给 core 的 compress_anthropic_live_zone。它关心最新 user message、tool_result、text block,以及 Anthropic 自己的 thinking/tool_use 等块。

OpenAI Chat Completions 的 shape 不同。live_zone_openai.rs 里把 scope 限定在最新 tool message 和最新 user text,而且明确不改 toolstool_choice 这些定义区。它还对 n > 1 的请求直接 passthrough,因为多候选输出会让行为更难界定。

OpenAI Responses 又是另一套 item shape。于是 proxy 入口先把 endpoint 分出来,再走对应压缩函数:

match endpoint {
    CompressibleEndpoint::AnthropicMessages => { /* Anthropic */ }
    CompressibleEndpoint::OpenAiChatCompletions => { /* OpenAI Chat */ }
    CompressibleEndpoint::OpenAiResponses => { /* OpenAI Responses */ }
}

这个分支看起来不优雅,但很务实。Agent 请求不是抽象的“聊天消息”,而是 provider API 的具体 JSON。压缩层如果假装它们一样,最后很容易破坏工具调用、缓存语义或 provider 校验。

内容检测:先识别结构,再选压缩器

live zone 只是确定“哪里可以动”。下一步是判断“这段内容是什么”。content_detector.rs 把输入分成几类:

  • JSON 数组。
  • 源码。
  • 搜索结果。
  • 构建输出或日志。
  • Git diff。
  • HTML。
  • 普通文本。

这个检测器是规则型的,源码注释里也强调没有 ML、没有 I/O。这样做的好处是 proxy 热路径稳定、可预测、容易做 parity test。它不追求语义理解,而是把明显有结构的上下文交给对应压缩器。

压缩器大致可以按输入类型理解:

输入类型压缩器主要策略
JSON 数组SmartCrusher保 schema、抽样行、把丢弃行放进 CCR
构建日志LogCompressor按错误、警告、命令、路径等类别评分和裁剪
grep/ripgrep 结果SearchCompressor按文件和匹配行聚合,保留相关片段
unified diffDiffCompressor限制文件和 hunk,裁剪上下文
源码当前 Rust 路径多为 no-op保守处理,避免破坏代码语义

这里能看出 Headroom 的工程取舍:它优先压缩那些“人类也会摘要”的输出,比如日志、搜索结果、diff、大 JSON 数组;对源码这种细节敏感的输入,则暂时更保守。

SmartCrusher:压 JSON 不是截断 JSON

SmartCrusher 负责处理 JSON 数组。简单截断 JSON 很危险,因为模型经常需要知道字段 schema、字段分布、异常行和少量代表样本。Headroom 的做法是先把数组转成中间表示,再分析结构是否统一,最后输出更短但仍保留 schema 的表示。

默认配置也透露了它的思路:

min_items_to_analyze: 5,
min_tokens_to_crush: 200,
max_items_after_crush: 15,
enable_ccr_marker: true,

也就是说,小数组和短内容不值得压;真正要压时,先保留 schema 和代表行。对于被裁掉的原始行,不是直接消失,而是进入 CCR 存储,并在上下文里留下 marker。

SmartCrusher 的另一点是 lossless-first。它会优先尝试不丢信息的紧凑表达;只有收益不足时,才进入更激进的裁剪路径。这个顺序很重要,因为压缩层一旦默认丢信息,Agent 的行为就会变得很难解释。

CCR:线上 lossy,端到端 lossless

CCR 可以理解成 Headroom 的“原文逃生通道”。压缩器把大块原文存进本地 store,用 hash 生成 marker,prompt 里只留下类似这样的占位:

<<ccr:HASH>>

crates/headroom-core/src/ccr/mod.rs 里定义了 CcrStore,后端包括内存、SQLite 和 Redis。默认 TTL 是 1800 秒,默认容量是 1000,key 由 BLAKE3 派生并截取前 24 个十六进制字符。

这让 Headroom 的信息模型变成两层:

  • wire 上给模型的是短上下文,可能已经丢掉了部分原始行。
  • 本地 store 里保留原文,模型可以通过 MCP retrieve tool 按 marker 取回。

所以它不是“不可逆摘要器”。更准确地说,它把 prompt 里的长上下文改成了引用式上下文:默认不给全量,必要时再取。ccr_roundtrip 测试也围绕这个行为写:压缩、存储、取回、重建必须能闭环。

缓存稳定和失败回退

Headroom 的 proxy 路径有几个保守点值得注意。

第一,压缩只发生在 live zone 内。外部字节不动,是为了尽量不破坏 provider prompt cache,也避免重序列化 JSON 带来的格式漂移。

第二,压缩错误是可恢复的。live zone 的 outcome 分为 NoChangeModified,遇到不能处理的输入时可以回到原始 bytes 转发。proxy 里也有 Outcome::NoCompression 分支,直接使用 buffered 原请求。

第三,不同认证和计费模式下策略不同。源码里有 auth mode 分类、PAYG fallback,以及 PAYG-only normalization pass。也就是说,Headroom 不把所有请求都当成同一种商业路径处理。

第四,它会做只读检测。volatile detector 和 cache drift detector 不直接改请求,而是帮助判断上下文变化和缓存风险。压缩层要进入生产链路,光有压缩率不够,还要能解释“为什么这次压了、为什么这次没压”。

测试如何锁住行为

这个项目的测试不是只测函数返回值,还在锁几个关键不变量。

live_zone_dispatch 测试覆盖了内容类型到压缩器的路由:JSON tool_result 应该进 SmartCrusher,日志进 LogCompressor,diff 进 DiffCompressor,源码目前保持 no-op。

更重要的是字节保真测试。它会检查压缩块外部的 prefix 和 suffix 是否保持一致,并确认被压缩的块确实显著变短。这个测试直接对应 Headroom 的核心承诺:只改 live zone,不碰 frozen 区。

ccr_roundtrip 则测试 CCR 闭环:压缩后 marker 必须能在 store 里找到,取回后能恢复原始 payload。没有这个测试,CCR 很容易退化成“把内容删掉并留下一个看起来像引用的字符串”。

设计取舍:它强在哪里,也限制在哪里

Headroom 最强的点是插入位置。它不要求每个 Agent 框架都改 prompt 构造逻辑,而是站在 provider request 之前做透明处理。对于 Claude、Codex 这类已有工具链,wrap 启动本地 proxy 并改 base URL,是很现实的接入方式。

第二个强点是它把 provider shape 当成一等约束。很多上下文压缩方案停留在“文本太长,所以摘要一下”,但 Agent 请求里有工具定义、工具调用、thinking block、cache 边界、response format、multi-output 参数。Headroom 的 provider-specific walker 虽然写起来更重,但更符合真实 API。

第三个强点是 CCR。上下文压缩最大的争议通常是“你删掉的信息之后怎么办”。CCR 给了一个工程答案:删出 prompt,但不要删出系统。模型能按需 retrieve,用户也能在本地存储里保留原文。

限制也很明显:

  • 它只拦截明确支持的 endpoint 和 JSON 请求。
  • OpenAI Chat 的 n > 1 等场景会保守 passthrough。
  • Rust 路径下源码压缩目前很谨慎,很多 source code 输入不会被压。
  • CCR 把一部分可靠性转移到了本地 store 和 MCP retrieve tool。
  • 仓库里有不少 roadmap 和实验性能力,阅读时应该以源码和测试为准,而不是只看 README 的愿景列表。

这些限制不是坏事。对于一个坐在 Agent 和 provider 中间的代理层,保守边界比盲目覆盖更重要。

建议的源码阅读顺序

如果只想快速理解 Headroom,可以按这个顺序读:

  1. README.md:先看它对外宣称的接入方式和总架构。
  2. headroom/cli/wrap.py:理解它怎么启动 proxy、怎么给 Claude/Codex 注入 base URL。
  3. crates/headroom-proxy/src/proxy.rs:看请求从 HTTP 入口到压缩函数的门控。
  4. crates/headroom-proxy/src/compression/mod.rs:看可压缩 endpoint 的分类。
  5. crates/headroom-core/src/transforms/live_zone.rs:看 live zone、不变量和 provider dispatch。
  6. crates/headroom-core/src/transforms/content_detector.rs:看内容类型如何判定。
  7. crates/headroom-core/src/transforms/smart_crusher/log_compressor.rssearch_compressor.rsdiff_compressor.rs:看不同压缩器的策略。
  8. crates/headroom-core/src/ccr/:看原文如何存储、marker 如何生成、retrieve 如何闭环。
  9. crates/headroom-core/tests/live_zone_dispatch.rsccr_roundtrip.rs:看项目最关心哪些行为不能坏。

总结

Headroom 的价值不在于某一个压缩算法多精巧,而在于它把上下文压缩放到了正确的位置:Agent 输出之后,LLM 请求之前。

这个位置带来的问题很多:不同 provider 的 JSON shape、prompt cache、工具调用、thinking block、认证模式、失败回退、原文取回。Headroom 的源码基本都在围绕这些问题做边界设计。

所以它更像一层 Agent context runtime,而不是一个文本压缩工具。它让上下文不再只有“全塞进 prompt”或“提前摘要掉”两种选择,而是多了第三种路径:本地保留原文,prompt 里放结构化压缩结果和可追回引用,把 token 预算留给真正需要模型判断的部分。