这几天盯 Agent 成本的人应该都刷到了 Headroom:GitHub 本周趋势第一,一周涨了 1.3 万星。我在写这篇文章前用 API 亲查了一下,headroomlabs-ai/headroom 目前 75,080 星、5,823 fork、Apache-2.0,仓库 10 月 11 日凌晨还在推送,热度不是媒体吹出来的。
它解决的问题很直白:AI 编程 Agent 一轮任务里,真正喂给模型的 token 大头往往不是你的问题,而是工具返回的东西——ls 出来的 500 个文件、一次 grep 的几千行日志、RAG 检索回的一坨文档。Headroom 的思路是在这些东西进模型之前先压一道,官方口径是「编码 Agent 省 20%,结构化 JSON 省 60-95%」。
口号谁都会喊。我花了半小时在本机把它的 Python 库拆开实测了一遍,结论比口号有意思:省多少,高度取决于你的数据长什么样,而且有一个默认行为,不知道的话你装完会发现「怎么一点没省」。
环境:不用 GPU、不用 API Key,五分钟装完
测试环境是 M4 MacBook Air 16GB,macOS 26.5.2,Python 3.13.12(受管 venv)。压缩本身是纯本地计算,不需要任何模型推理,所以一台普通电脑就够:
python3 -m venv headroom-env
source headroom-env/bin/activate
pip install headroom-ai tiktoken装的是 PyPI 上的 headroom-ai,当前版本 0.40.0(10 月 6 日发布)。tiktoken 是我额外装的,用来独立数 token,不想用它的输出被自家计数器「自证清白」。仓库在 GitHub,官方文档也是这两天刚整理过的状态。
实测一:中等规模 JSON,只省了 6.7%
第一组测试我造了 60 条 GitHub Issues 风格的数据(约 3 万字符),模拟 Agent 调完 GitHub API 拿到的返回。用最底层的 SmartCrusher 直接压:
import json
from headroom import SmartCrusher
issues = [{"id": i, "number": 1000 + i,
"title": f"Fix crash when parsing config file #{i}",
"state": "open" if i % 3 else "closed",
"user": {"login": f"dev{i:03d}", "id": 50000 + i, "type": "User"},
"labels": [{"id": j, "name": n} for j, n in enumerate(["bug", "python"][:1 + i % 2])],
"created_at": "2026-10-%02dT09:00:00Z" % (1 + i % 28)} for i in range(60)]
raw = json.dumps({"total_count": 60, "issues": issues})
res = SmartCrusher().crush(raw)
print(len(raw), "->", len(res.compressed), res.strategy)结果是 28,581 字符压到 26,659,只省 6.7%,策略标记 smart_sample(60->60)——60 条一条没丢,省的只是空白符和冗余字段。也就是说,数据规模不够大、重复度不够高时,它的压缩相当克制。这跟某些宣传里「随手 90%」的印象差距很大,是我这篇最想先告诉你的事实。
实测二:500 行文件列表,省 38.5%,策略才发力
把数据换成 Agent 更常见的场景——一次目录扫描返回 500 个文件的元数据,效果立刻不一样:
rows = [{"path": f"/Users/dev/project/src/module{i//10}/file_{i}.py",
"size": 1024 * i % 9000, "lines": 50 + i % 300,
"modified": f"2026-09-{1 + i % 30:02d}T10:00:00Z",
"kind": "python"} for i in range(500)]
big = json.dumps({"results": rows, "total": 500})
res = SmartCrusher().crush(big)69,152 字符压到 40,718,字节层面省 41.1%,耗时 0.01 秒。用 tiktoken 的 o200k_base 独立计数:25,953 token → 15,968 token,省 38.5%。策略是 lossless:table——它把重复键名折叠成表结构,数据一条不丢,这属于「白拿」的省钱,完全可以无脑开。
所以官方「JSON 省 60-95%」的口径要打个折理解:数据越规整、重复键越多、规模越大,越接近上限;我这种带大量唯一路径的文件列表,40% 上下是真实水位。
实测三:纯文本日志,crush 直接摆烂
第三个坑有点意外。我把 800 行 API 访问日志(纯文本)直接丢给 SmartCrusher().crush(),返回原样吐回来,策略标记 passthrough,节省 0%。翻了下行为规律:SmartCrusher 只处理结构化 JSON,纯文本它不认。想压日志,得走更高层的 compress() 全管线,而且——这是今天最重要的发现——喂法必须对。
实测四:最重要的坑——默认不动你的 user 消息
我用 compress() 全管线跑同一个 800 行日志。先把它作为 user 消息发给模型(「帮我看看这个日志有没有异常」+ 日志全文),结果 tokens_before 和 tokens_after 一模一样,27,217 → 27,217,省 0。诊断信息里写着 router:protected:user_message:用户消息默认受保护,管线根本不碰。
这是合理的安全设计(用户原话被改动是大忌),但也意味着最直觉的用法——把你贴的日志塞在提问里——一分钱都省不到。正确姿势是把日志放到 tool 角色里,模拟工具返回。同样 800 行日志,改成 tool 消息后:
from headroom import compress
msgs = [
{"role": "user", "content": "帮我看看这个日志有没有异常"},
{"role": "assistant", "content": None,
"tool_calls": [{"id": "c1", "type": "function",
"function": {"name": "read_log", "arguments": "{}"}}]},
{"role": "tool", "tool_call_id": "c1", "content": log},
]
r = compress(messages=msgs, model="gpt-4o", optimize=True)
print(r.tokens_before, r.tokens_after, r.compression_ratio)27,233 → 72 token,省 99.7%。但压缩后的内容让我后背一凉:[800 lines omitted: 800 INFO]、[800 lines compressed to 0. Retrieve more: hash=83aa… ]——保留行数是 0,正文一行没留,只剩摘要和取回哈希。
那如果日志里有真正的错误呢?这才是要命的地方。我往 800 行 INFO 里埋了一行 ERROR(500 报错带 traceback)和一行 WARN(429 限流),再压一次:27,246 → 567 token,省 97.9%,输出长这样:
2026-10-12 09:07:01 ERROR api.request path=/v1/users status=500 latency=812ms req_id=r-421 trace=Traceback (most recent call last): ConnectionRefusedError
2026-10-12 09:12:13 WARN api.request path=/v1/users status=429 latency=95ms req_id=r-733
[786 lines omitted: 1 ERROR, 1 WARN, 798 INFO]
[800 lines compressed to 14. Retrieve more: hash=ffb727678e749653cf1b4d06]异常行原文完整保留,前后各带几行上下文,末尾还有一行精确到 ERROR/WARN/INFO 条数的对账摘要。这个设计是懂排障的:模型看到 14 行就能定位问题,需要细节时凭哈希取回全文。但纯 INFO 日志压到 0 行仍然偏激进——万一你的「异常」不是 ERROR 级别能识别的(比如某个 latency 缓慢漂移),摘要行就是唯一线索了。
另外两个小细节:运行时终端会打一条 Native content detection requires ONNX Runtime 1.24+; using pure-Python detection 的警告,说明默认装的是纯 Python 检测,装 ONNX Runtime 才走原生路径;expand_compacted() 对日志格式的压缩块返回 None,取回全文要用哈希走 CCR 存储,这两处文档都没写清楚。
值不值得上?
三个场景建议直接用:Agent 的文件列表和目录扫描(lossless 表折叠,白拿 40%);数据库查询和搜索类 API 的结构化返回;长日志类工具输出配合异常保留机制(97%+,且不丢关键行)。
两个场景我不建议:一是需要逐行审计、合规留痕的日志——就算它号称可取回,摘要行的存在意味着模型第一眼看到的就不是全貌;二是你自己的分析笔记和提问内容——默认保护是好事,但也别指望它帮你省这部分。
它还有 MCP server 和代理两种形态,可以不改一行代码挂到 Claude Code 这类工具前面。我自己更倾向库的形态,因为控制力最强,出了问题也好排查。如果你的 Agent 每月 token 账单已经到了肉疼的量级,这半小时的接入成本绝对划算;如果只是偶尔跑跑脚本,先想清楚 tool 消息喂法再上手,不然你会像我第一轮那样,对着 0% 的节省怀疑人生。
顺带一提,省 token 和省模型是两条互补的路——上下文压掉 40% 之后,本地小模型也更容易塞得下关键内容,我之前写过的本地模型选型和联网能力层都可以和它组合使用。
相关阅读: