数据截至 (上游 commit b77d61291399)
Headroom — 架构与原理
30 秒导读: Headroom 是一层跑在你本机、夹在 agent 和 LLM 之间的上下文压缩层。它不删历史消息,只重写「最新一轮刚到的那几千字节」(活区),按内容类型交给 JSON / 代码 / 日志 / 搜索各自的专用压缩器,原文留在本地并给模型一个
headroom_retrieve(hash)工具随时要回来。
1. 这是什么(零基础也能懂)
一句话定义
一个本地的上下文压缩中间层: 你的 agent 要发给大模型的那一大包 prompt,先经过它,变小,再发出去。
解决什么问题
假设你在用 Claude Code 改一个大项目。你让它「找出所有调用 parse_config 的地方」,ripgrep 吐回 17,000 token 的搜索结果;你让它跑测试,pytest 吐回 60,000 token 的日志。这些字节每一轮都要重新发一次给模型,你为它们反复付钱,而模型真正需要的可能只是其中十几行。
Headroom 干的事:在这堆东西离开你机器之前把它压小。
它面向三类人:
| 谁 | 怎么用 |
|---|---|
| 用编码 agent 的工程师 | headroom wrap claude,一条命令,不改任何代码 |
| 自己写 agent 循环的人 | from headroom import compress,一个函数 |
| 跑任何语言 / 任何框架的团队 | 起一个本地代理,把 base URL 指过去 |
它能做什么
- 库:
compress(messages),Python / TypeScript 都有,行内调用。 - 代理:
headroom proxy --port 8787,零改代码,任何语言都能接。 - agent 包装:
headroom wrap claude|codex|cursor|...,自动起代理并改好 agent 的 base URL,headroom unwrap撤销。 - MCP 服务器:对外暴露
headroom_compress/headroom_retrieve/headroom_stats三个工具。 - 输出侧省钱:还能压模型写回来的那部分(见 §3 第 6 章)。
- 可逆:压掉的原文留在本地,模型随时能按 hash 取回。
依据:headroom/compress.py:171 compress · headroom/cli/wrap.py:4609 wrap · headroom/ccr/mcp_server.py:73-75 CCR_TOOL_NAME / COMPRESS_TOOL_NAME / STATS_TOOL_NAME
用起来什么样
最小的库调用长这样:
# 示意,非源码 —— 演示 compress() 的位置:发请求之前,插一行
from anthropic import Anthropic
from headroom import compress
messages = [{"role": "user", "content": huge_tool_output}]
result = compress(messages, model="claude-sonnet-4-5-20250929") # 就这一行
print(result.tokens_saved, result.compression_ratio) # 省了多少
client = Anthropic().messages.create(
model="claude-sonnet-4-5-20250929",
messages=result.messages, # 换成压过的
)
重点看:输入输出是同一种 message 格式,所以它能塞进任何 SDK、任何框架的任何一行之前。
依据:headroom/compress.py:1-55(模块 docstring 里的 Anthropic / OpenAI / LiteLLM / httpx 四种接法)· headroom/compress.py:150-168 CompressResult
零改代码的接法则是包装 agent:
headroom wrap claude # 起本地代理 + 把 ANTHROPIC_BASE_URL 指过去 + 拉起 claude
headroom dashboard # 看实时省了多少
headroom unwrap claude # 撤销
依据:headroom/cli/wrap.py:5010(注入 ANTHROPIC_BASE_URL)· headroom/cli/wrap.py:4655 unwrap
一句话直觉
它像 gzip,但压的不是文件而是 prompt,而且知道自己在压什么。
普通压缩对模型没用——模型读不懂 gzip。Headroom 做的是语义层的有选择丢弃:1000 行 JSON 里只留 20 行代表性的、一段 Python 只留签名和导入、一坨日志只留不重复的那几行。丢掉的部分不是消失,是被存进本地一个按 hash 索引的柜子,模型觉得不够就自己去取。
2. 顶层全景(它大概怎么转)
一次请求的主线
怎么读这张图:从上往下就是一次请求在 Headroom 里的处理顺序,每一步都可能原样放行。
agent 发出的一整包请求
(system + 全部历史消息 + 最新一轮工具输出)
│
▼
┌───────────────────────────────────────────────┐
│ ① 划线 │
│ 前面 N 条 = 冻结区(provider 已缓存,不碰) │
│ 剩下的 = 活区(刚到的这轮,还没缓存) │
└───────────────────────┬───────────────────────┘
▼ 只有活区往下走
┌───────────────────────────────────────────────┐
│ ② 探测:这块内容是什么 │
│ JSON / 代码 / 搜索结果 / 日志 / diff / │
│ HTML / 表格 / 配置 / 纯文本 │
└───────────────────────┬───────────────────────┘
▼
┌───────────────────────────────────────────────┐
│ ③ 无损先行:能纯折叠的先折叠 │
│ (自带逆变换,折不动就原样返回) │
└───────────────────────┬───────────────────────┘
▼ 折完还嫌大才走有损
┌───────────────────────────────────────────────┐
│ ④ 分派:交给对应的专用压缩器 │
│ SmartCrusher · CodeCompressor · │
│ LogCompressor · SearchCompressor · Kompress │
└───────────────────────┬───────────────────────┘
▼
┌───────────────────────────────────────────────┐
│ ⑤ 存原文 + 插标记(CCR) │
│ 原文进本地 store,正文里留 hash=xxxx │
└───────────────────────┬───────────────────────┘
▼
发给 provider(Anthropic / OpenAI / Gemini …)
冻结区字节一模一样 → prompt cache 照旧命中
模型缺料 → 调 headroom_retrieve(hash) 要回原文
部件一句话职责
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
compress() | 一函数入口:配置归一、跑管线、失败兜底 | headroom/compress.py |
TransformPipeline | 按顺序跑 transform、计时、熔断、出指标 | headroom/transforms/pipeline.py |
CacheAligner | 只检测不改写:发现 system prompt 里的 UUID / 时间戳这类会打断缓存的东西,发警告 | headroom/transforms/cache_aligner.py |
ContentRouter | 探测内容类型 → 选策略 → 无损先行 → 调用压缩器 → 记录路由日志 | headroom/transforms/content_router.py |
| 各压缩器 | 每种内容一个,各自保证「不破坏什么」 | headroom/transforms/{smart_crusher,code_compressor,log_compressor,search_compressor,kompress_compressor}.py |
| CCR | 原文入库、注入 headroom_retrieve 工具、拦截模型的取回调用 | headroom/ccr/ |
CompressionCache(缓存层的那个) | 算冻结区边界、跨轮复用已压结果。注意重名:content_router.py:1213 里另有一个同名类,那是块级的 skip/result 两层缓存,和冻结区无关(见 01 章 §4.6) | headroom/cache/compression_cache.py |
| 代理 | FastAPI 服务,把上面这套挂到 /v1/messages 等真实端点上 | headroom/proxy/server.py、headroom/proxy/handlers/ |
三种壳,同一个内核
上面这条主线只写了一遍,外面套三层不同的壳:
| 壳 | 用法 | 代码入口 |
|---|---|---|
| 库 | compress(messages) | headroom/compress.py:171 |
| 代理 | headroom proxy --port 8787 | headroom/proxy/server.py:2525 create_app |
| MCP | headroom mcp serve | headroom/ccr/mcp_server.py |
headroom wrap 不是第四种,它是「起代理 + 改 agent 配置」的自动化脚本。
一个容易忽略的事实:内核有一半是 Rust
Python 侧的 SmartCrusher 已经退成一层薄壳,真正干活的是 PyO3 编译出来的 headroom._core;代理也有一份 Rust 实现,活区调度器在 crates/headroom-proxy/src/compression/live_zone_anthropic.rs。
依据:headroom/transforms/smart_crusher.py:1-20(「Python 实现已退休,无 Python fallback」)· crates/headroom-proxy/src/compression/live_zone_anthropic.rs:149 compress_anthropic_request
3. 阅读地图
建议按顺序读;只关心某一块的话,查下表直接跳。
| 章节 | 讲什么 | 什么时候读 |
|---|---|---|
| 压缩主干 — 一段内容怎么被路由到对的压缩器 | TransformPipeline 的执行顺序、ContentRouter 的探测与策略映射、混合内容切段、无损先行、熔断与失败兜底 | 想搞懂「一次压缩到底发生了什么」——先读这章 |
| 压缩器全家桶 — 每种内容各有各的「不能破坏什么」 | SmartCrusher(JSON 数组去重)、CodeCompressor(AST,保证输出仍能解析)、LogCompressor、SearchCompressor、Kompress(ML 文本压缩)各自的取舍 | 想知道某类内容会被怎么处理、会丢什么 |
| CCR — 把有损压缩做成可逆:原文不删,模型可以要回来 | 原文入库、marker 格式、headroom_retrieve 工具注入、流式与批量场景下的取回、TTL 这个薄弱环节 | 关心「压掉的东西还找得回吗」 |
| 活区与缓存安全 — 为什么「删历史消息」是错的 | 冻结区怎么算、活区为什么必须留一条、字节保真、工具数组排序陷阱、按 auth mode 分档的压缩策略 | 本项目最有价值的一章;也是理解它和其他上下文管理方案分歧的关键 |
| 代理层与 wrap — 零改代码接进任何 agent | FastAPI 代理的请求路径、Anthropic / OpenAI / Gemini 三套 handler、wrap 干了哪些环境改动、如何撤销 | 要部署、要排障、要看它到底改了你机器上什么 |
| 输入之外的三条省 法 — 让模型少写、让下次少说 | 输出侧的 verbosity steering 与 effort routing、跨 agent 记忆、headroom learn 把失败会话变成 CLAUDE.local.md 规则 | 输入已经压到底了,还想再省 |
4. 巧妙之处(可借鉴的技术)
这一节是要带走的精华。每条先说妙在哪,再给出处。
4.1 「活区」这个概念本身
妙在哪: 几乎所有上下文管理方案都在做「删旧消息 / 总结历史」。Headroom 认定那是错的——因为 provider 的 prompt cache 是按位置的前缀缓存:前 K 个字节缓存了,你动第 3 条消息,第 3 条之后全部作废,你把 0.1 倍价的缓存读变成了全价重写。
所以它只改最新一轮这块还没进过任何缓存的字节,并且把「按消息列表删东西」的那一整个阶段直接废掉了。
依据:headroom/transforms/pipeline.py:95-98(注释明说 IntelligentContextManager / RollingWindow 阶段已退休,live-zone-only 是唯一策略)· headroom/compress.py:415-416
4.2 冻结区必须留一条尾巴,否则压缩率归零
妙在哪: 冻结区的算法是「从头数连续稳定的消息」,但它硬性 min(count, len - 1)——最后一条永远不算冻结。
原因很实在:像 Cline、Aider 这类不用原生 tool message 格式的客户端,每条消息都会被判为稳定,于是整个列表全冻结、活区为空、每次都「压缩了 0 token」。代码注释里连复现日期和现象都写着。
依据:headroom/cache/compression_cache.py:287-324 compute_frozen_count
4.3 字节保真是被测试钉死的不变量
妙在哪: 「不碰冻结区」不是靠自觉,是双保险:
- Python 侧压完之后再走一遍
_restore_frozen_prefix,逐条比对,不一致就强行按原始请求覆盖回去。 - Rust 侧用字节区间手术改 body,被压块之外的字节原样 round-trip,CI 里有个测试专门钉 SHA-256 前后缀不变。
依据:headroom/proxy/handlers/anthropic.py:636-658 _restore_frozen_prefix · crates/headroom-proxy/src/compression/live_zone_anthropic.rs:34-39(cache-safety invariant 注释)
4.4 「无损先行,有损要交额外的钱」
妙在哪: 每块内容先跑一遍格式原生的无损折叠——grep 结果折成 --heading 形式、日志折叠重复行并剥 ANSI、diff 去掉 index 行。这些变换自带逆函数并且运行时自检:round-trip 不还原、或者压完没变小,就原样返回。
然后是那个漂亮的门槛:有损压缩要想顶掉已经拿到手的无损结果,必须比它多省至少 5%。少于这个数就不值得拿准确率换。
依据:headroom/transforms/lossless_compaction.py:1-13 · headroom/transforms/content_router.py:2567-2581 _lossless_first · headroom/transforms/content_router.py:1750 _DEFAULT_LOSSY_MIN_EXTRA_SAVINGS = 0.05
4.5 无标记的有损结果不许覆盖工具真值
妙在哪: 有几个压缩器(Kompress / 纯文本 / 代码)只在真的把原文存进 CCR 时才会吐 hash 标记。没标记就意味着不可恢复。
于是路由器维护一个 LOSSY_UNMARKED_STRATEGIES 集合:这几种策略的结果不准替换 role="tool" 的内容——工具输出是 ground truth,不能被一段不可逆的摘要顶掉。
依据:headroom/transforms/content_router.py:1731-1740 LOSSY_UNMARKED_STRATEGIES
4.6 相信原生探测器,别相信正则
妙在哪: 一段 Python 文件里有 dict / list 字面量,正则启发式会判成「JSON + 散文的混合内容」,于是被切段、逐段丢给 ML 文本压缩器——注释里记了实测后果:45K token 的 Python 压成 912 token,事实召回率 11%,代码对 agent 彻底作废。
修法很克制:当原生检测器(magika)以 ≥0.8 的置信度说这是源码时,推翻正则的混合判定。同一处还顺手把「不偏好代码压缩器」的语义从「退回 ML 压缩」改成「直接放行」——因为用户的本意是「别把我的代码搞坏」。
依据:headroom/transforms/content_router.py:2345-2357 _determine_strategy · headroom/transforms/content_router.py:2389-2405 _strategy_from_detection
4.7 排序工具定义可以省钱,但有 cache_control 时绝不能排
妙在哪: 把 tools 数组排成确定顺序有利于缓存前缀稳定。但只要有任何一个 tool 带了 cache_control 断点,排序就变成灾难:断点的含义是「缓存到我为止」,重排会改变前缀包含哪些 tool;两个不同 TTL 的断点还可能被排成 1h 在 5m 后面,直接被 Anthropic 拒绝。
所以一旦发现有标记,整个排序跳过,并打一行日志。Rust 侧有同名同义的守卫。
依据:headroom/proxy/handlers/anthropic.py:477-503 _sort_tools_deterministically
4.8 Read 的生命周期:只删「已被证伪」的那部分
妙在哪: 不是笼统地压缩历史里的文件读取结果,而是分类:文件后来被编辑过 → 这份内容事实上已经错了(STALE);同一文件后来又读了一次 → 冗余(SUPERSEDED)。这两类替换成 marker 是可证明安全的,新鲜的 Read 一个不碰。
实测数据也写在 docstring 里:67% stale + 12% superseded,只有 20% 是新鲜的。同一段注释还诚实记了一个被砍掉的机制——字节完全相同的重复 Read 只占 0.1%,不值得做。
依据:headroom/transforms/read_lifecycle.py:1-20
4.9 三层「绝不把事情搞砸」的兜底
妙在哪: 压缩是可选优化,不该有能力弄坏一次请求。于是有三道闸:
| 兜底 | 触发条件 | 行为 |
|---|---|---|
| 膨胀守卫 | 压完 token 反而变多 | 整个丢弃,退回原消息,标 inflation_guard:reverted |
| 熔断器 | 连续 3 次管线异常 | 60 秒内所有请求直接放行,不再重试 |
| 全局兜底 | 管线抛任何异常 | 记一条 metric,返回原消息 |
依据:headroom/compress.py:278-291(膨胀守卫)· headroom/transforms/pipeline.py:123-131、:210-224(熔断器)· headroom/compress.py:349-362
4.10 遥测再重要也得给结果让路
妙在哪: 「浪费信号检测」要把原始消息重新解析一遍,纯粹为了出报表。在超大 transcript 上这一步能花掉几十秒,把代理的压缩超时撑爆,导致已经算好的压缩结果被丢掉。
修法是加一条硬线:请求超过 100,000 token 就跳过这个诊断。诊断可以没有,结果不能丢。
依据:headroom/transforms/pipeline.py:34-43 MAX_WASTE_SIGNAL_DETECTION_TOKENS · headroom/transforms/pipeline.py:483-497
顺带说清边界(诚实优先)
- CCR 有 TTL。 默认 30 分钟,过期后取回失败——「无损 + 可取回」会静默退化成「有损」。代码注释自己承认这是无准确率损失承诺里最弱的一环。依据:
headroom/config.py:597-611 - SmartCrusher 是硬依赖 Rust 扩展。
headroom._core导不进来就直接失败,没有 Python fallback。依据:headroom/transforms/smart_crusher.py:17-20 - 自定义相关性打分器目前会抛
NotImplementedError。 项目选择「响亮地失败」而不是静默忽略用户传进来的 scorer。依据:headroom/transforms/smart_crusher.py:36-43 - Rust 活区调度器只覆盖 Anthropic 的
/v1/messages。 OpenAI Chat / Responses 和 Gemini 各有自己的 walker,共享同一套压缩后端但走不同代码路径。依据:crates/headroom-proxy/src/compression/live_zone_anthropic.rs:4-12 - 压缩激进程度按订阅形态分档。 Subscription 模式是 live-zone-only + 关掉 CacheAligner + TOIN 只读;PAYG / OAuth 更激进。同一份代码在不同账号下行为不同。依据:
headroom/transforms/compression_policy.py:233policy_for_mode(按 auth mode 出策略)·headroom/transforms/compression_policy.py:309resolve_policy(请求侧的实际入口,含 enforcement 开关与None兜底)
5. 代码地图(导航索引)
按符号名 grep 比按行号更抗上游漂移。
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 一函数入口 / 配置 / 结果 | headroom/compress.py | compress、CompressConfig、CompressResult |
| 管线编排、计时、熔断 | headroom/transforms/pipeline.py | TransformPipeline、TransformPipeline.apply、_build_default_transforms |
| 生命周期阶段与扩展点 | headroom/pipeline.py | PipelineStage、PipelineExtension、PipelineExtensionManager |
| 内容路由主体 | headroom/transforms/content_router.py | ContentRouter、ContentRouter.compress、_determine_strategy、_strategy_from_detection |
| 策略枚举与路由记录 | headroom/transforms/content_router.py | CompressionStrategy、RoutingDecision、RouterCompressionResult |
| 无损先行 / 不可恢复守卫 | headroom/transforms/content_router.py | _lossless_first、LOSSY_UNMARKED_STRATEGIES、_DEFAULT_LOSSY_MIN_EXTRA_SAVINGS |
| 内容类型探测 | headroom/transforms/content_detector.py | ContentType、DetectionResult、detect_content_type |
| 可逆无损折叠 | headroom/transforms/lossless_compaction.py | compact_lossless、collapse_runs、search_heading、diff_strip_index |
| JSON 数组压缩 | headroom/transforms/smart_crusher.py | SmartCrusher、SmartCrusherConfig、CrushResult、smart_crush_tool_output |
| 代码压缩(AST) | headroom/transforms/code_compressor.py | CodeAwareCompressor、CodeCompressorConfig、_compress_function_ast、_verify_syntax |
| Read 生命周期 | headroom/transforms/read_lifecycle.py | ReadLifecycleConfig、_READ_TOOL_NAMES、_MUTATING_TOOL_NAMES |
| 缓存对齐(仅检测) | headroom/transforms/cache_aligner.py | CacheAligner、VolatileFinding、get_alignment_score |
| 冻结区 / 跨轮复用 | headroom/cache/compression_cache.py | CompressionCache(与 content_router.py 的同名类不是一回事)、compute_frozen_count、apply_cached、update_from_result |
| 前缀增量追踪 | headroom/cache/prefix_tracker.py | extract_cache_stable_delta |
| 按 auth mode 的策略分档 | headroom/transforms/compression_policy.py | CompressionPolicy、policy_for_mode、resolve_policy、cache_write_multiplier_for_ttl |
| CCR 配置与 marker 模板 | headroom/config.py | CCRConfig、marker_template、PrefixFreezeConfig |
| CCR 工具注入 | headroom/ccr/tool_injection.py | CCR_TOOL_NAME、create_ccr_tool_definition、CCRToolInjector、scan_for_markers、verify_ownership |
| CCR 响应拦截 / 流式 | headroom/ccr/response_handler.py | CCRResponseHandler、StreamingCCRHandler、CCRToolCall |
| CCR 批量 API 支持 | headroom/ccr/batch_processor.py、headroom/ccr/batch_store.py | BatchResultProcessor、BatchContextStore |
| MCP 服务器 | headroom/ccr/mcp_server.py | HeadroomMCPServer、COMPRESS_TOOL_NAME、STATS_TOOL_NAME |
| 代理应用装配 | headroom/proxy/server.py | HeadroomProxy、create_app、run_server |
| Anthropic 请求路径与缓存守卫 | headroom/proxy/handlers/anthropic.py | _strict_previous_turn_frozen_count、_restore_frozen_prefix、_sort_tools_deterministically、_append_context_to_latest_non_frozen_user_turn |
| OpenAI / Gemini 路径 | headroom/proxy/handlers/openai.py、headroom/proxy/handlers/gemini.py | 各自的 handler 类 |
| 输出侧省钱 | headroom/proxy/output_shaper.py、headroom/proxy/output_steering.py | shape_request、route_effort、apply_verbosity_steering |
| agent 包装 / 撤销 | headroom/cli/wrap.py | wrap、unwrap、wrap_selfheal |
| 跨 agent 记忆 | headroom/memory/core.py | HierarchicalMemory |
| 从失败会话学规则 | headroom/learn/analyzer.py、headroom/learn/writer.py | extract_marker_block、Recommendation、RecommendationTarget |
| Rust 内核 | crates/headroom-core/src/transforms/ | live_zone.rs、smart_crusher/、content_detector.rs、magika_detector.rs |
| Rust 活区调度(Anthropic) | crates/headroom-proxy/src/compression/live_zone_anthropic.rs | compress_anthropic_request、any_tool_has_cache_control |