日习一技 - Headroom
type: channel
link: https://i-study-everyday.online/category/Headroom/
description: Headroom学习笔记
items: 学习Headroom 的输出 token 优化
items: 学习 Headroom 的压缩管线
items: 学习 Headroom 的跨 agent 记忆与失败学习
items: 学习 Headroom 的 CCR 可逆压缩
items: 学习 Headroom 的三大压缩器
items: 学习 Headroom 的整体架构
items: Headroom 上手：wrap 与 proxy 两种接入方式实战
items: Headroom 介绍：给 AI agent 装一层可逆的上下文压缩

学习Headroom 的输出 token 优化
type: item
date: 2026-08-14T00:58:00+00:00
description: 在前面几篇里，我们把 Headroom 的输入侧基本都聊过了：compress() 入口和管线生命周期、ContentRouter 怎么分流内容、SmartCrusher / CodeAwareCompressor / Kompress 三个压缩器各自压什么、CCR 可逆压缩怎么把原文缓存在本地、跨 agent 记忆和 headroom learn 又怎么把经验沉淀成长期知识。它们有一个共同点：围着的都是发给模型这一路。工具输出、日志、RAG 片段、文件、对话历史，在进入大模型之前先被压缩。今天这一篇换个方向，看 Headroom 怎么压模型写回来的东西。这部分能力叫 Output token reduction（输出 token 削减），对应的模块是 Output Shaper（输出整形器）。什么是输出 token 优化众所周知，输出 token 比输入贵，比如 Opus 级别的模型，输出 token 的单价大约是输入的 5 倍。同样是一万 token，写回来的那一万比发出去的那一万贵得多。所以输入侧压得再狠，如果模型每一轮都长篇大论地写回来，账单还是下不去。那模型的输出里，哪些部分是可以省的？官方 README 把浪费点归纳成三类：寒暄与收尾语（preamble / postamble）：回答正文前的一句 Great, let me...（好的，我来...），以及正文后的一句总结陈词。这些话对结果没有信息量。复述已有上下文：模型把你刚发给它的代码、文件内容、diff、工具输出，原样再抄一遍到回复里。你已经有这些内容了，它抄一遍纯属多花钱。对机械步骤过度思考（thinking）：thinking 指模型在正式作答前的一段推理草稿，它同样按输出 token 计费。当这一轮只是读了个文件、跑了个通过的测试，续写本来是很机械的动作，却还调动高档的深度思考，这笔思考 token 就浪费了。Headroom 代理本身不生成任何输出 token，它只是个透明反向代理。所以它能动的只有请求：通过改写发出去的请求，去影响模型愿意写回来多少。Output Shaper 的两根杠杆都是这个思路。打开输出整形器Output Shaper 默认是关闭的，通过环境变量开启：export HEADROOM_OUTPUT_SHAPER=1它的配置全部走环境变量，OutputShaperSettings 这个数据类负责从环境里把设置读出来，逻辑在 output_shaper.py：@dataclass(frozen=True) class OutputShaperSettings: enabled: bool = False # HEADROOM_OUTPUT_SHAPER verbosity_level: int = 2 # HEADROOM_VERBOSITY_LEVEL，0~4 effort_router_enabled: bool = True # HEADROOM_EFFORT_ROUTER mechanical_effort: str = "low" # 机械续写时降到哪一档 @classmethod def from_env(cls) -> OutputShaperSettings: enabled = runtime_env.getenv("HEADROOM_OUTPUT_SHAPER", "").lower() in ( "1", "true", "yes", ) # ... 读取 level、router、mech，并把 level 夹在 0~4 之间 return cls(enabled=enabled, ...)四个字段各对应一个环境变量：HEADROOM_OUTPUT_SHAPER：总开关，设成 1 / true / yes 才算开，其它值都是关。整个 Output Shaper 就靠它启用。HEADROOM_VERBOSITY_LEVEL：详略级别，0 到 4 的整数，默认 2。它控制第一根杠杆「详略引导」的力度，往系统提示词里追加多强的简洁指令，0 是不干预、4 是电报体，各级指令的原文下一节会看到。HEADROOM_EFFORT_ROUTER：第二根杠杆「努力档位路由」的开关，默认是开的，只有显式设成 0 / false / no 才关。它管的是另一件事：给机械续写的轮次降低思考档位。HEADROOM_MECHANICAL_EFFORT：机械续写时把思考档位降到哪一档，默认 low，填了不认识的档位名也会回退到 low。关于后三个环境变量的含义和用法，后面详讲。这个类只负责把开关读出来，真正的整形入口是 shape_request，它的主干很直白：def shape_request(body, settings=None, …

学习 Headroom 的压缩管线
type: item
date: 2026-08-10T05:10:00+00:00
description: 在上一篇里，我们从架构的角度把 Headroom 的压缩层俯瞰了一遍：内容进来之后先经过 ContentRouter 分流，再交给三大压缩器处理，中间还夹着 CacheAligner 和一整套贯穿全流程的生命周期事件。那一篇讲的是各个模块的分工和位置，属于总览。今天我们换个视角，钻进源码里，顺着一次 compress() 调用往下追，目标是把「入口函数收到一批消息之后，到底发生了什么」这条主线看清楚。我们从最外层的入口函数看起。compress() 入口Headroom 对外暴露的最简单用法就是一个函数：compress()，它定义在 headroom/compress.py 里。不需要起代理、不需要写配置，把消息传进去，拿回压缩后的消息就行。函数签名如下：def compress( messages: list[dict[str, Any]], model: str = "claude-sonnet-4-5-20250929", model_limit: int = 200000, optimize: bool = True, hooks: Any = None, config: CompressConfig | None = None, **kwargs: Any, ) -> CompressResult: ...几个参数值得留意：messages：一批消息，兼容 Anthropic 和 OpenAI 两种格式。model 和 model_limit：用于 token 计数和上下文窗口大小的判断，默认按 Claude Sonnet 的 20 万 token 上限算。optimize：是否真的压缩。传 False 会原样返回，方便做 A/B 对照（同一批消息，一边压一边不压，比较效果）。config 和 **kwargs：压缩选项，后者是前者的字段简写。先用传进来的 config（没有就取默认值），再拿 **kwargs 里对得上字段名的键去覆盖它。也就是说 compress(messages, protect_recent=0) 这种写法，等价于构造一个 CompressConfig(protect_recent=0)。CompressConfig：压缩到什么程度CompressConfig 是面向用户的压缩选项，控制「压什么、压多狠、用哪个模型」。它是一个 dataclass，几个默认值透露了 Headroom 的取向：@dataclass class CompressConfig: compress_user_messages: bool = False # 默认不压用户消息 compress_system_messages: bool = True # 默认压系统消息 protect_recent: int = 4 # 最近 4 条消息不动 protect_analysis_context: bool = True # 检测到分析/评审意图时保护代码 target_ratio: float | None = None # 保留比例，None = 模型自己决定 min_tokens_to_compress: int = 250 # 短于 250 token 的消息跳过 kompress_model: str | None = None # 文本压缩模型 ID savings_profile: str | None = None # 命名的高压缩档位dataclass 是 Python 标准库提供的一种类写法，专门用来定义「主要用来装数据」的类。只要在类上加一个 @dataclass 装饰器，它就会按你声明的字段自动生成 __init__、__repr__ 等方法，字段还能直接写默认值，省去手写字段赋值的样板代码。CompressConfig 这种纯配置类用它正合适。这些字段大致分三组。第一组决定压什么：compress_user_messages 和 compress_system_messages 分别控制用户消息、系统消息要不要参与压缩；protect_recent 把最后 N 条消息保护起来不动，因为它们是当前对话的活跃部分；protect_analysis_context 更进一步，一旦识别出「分析」「评审」这类意图，就会把相关代码保护起来不压。第二组决定压多狠：target_ratio 是 Kompress 的保留比例，设 None 就由模型自己定（默认激进，大约只留 15%）；min_tokens_to_compress 是参与压缩的最小 token 数，消息短于这个值就直接跳过，压缩本身有开销，太短不值得压。第三组决定用哪个模型、用哪套档位：kompress_model 是文本压缩用的模型 ID，默认是作…

学习 Headroom 的跨 agent 记忆与失败学习
type: item
date: 2026-08-10T00:14:00+00:00
description: 在上一篇里，我们看了 Headroom 的 CCR（Compress-Cache-Retrieve，可逆压缩）：压缩时把原文按哈希缓存在本地，模型觉得信息不够，就拿着哈希把原文取回来。它管的是「一次会话内」的信息不丢。这一篇我们看另外两块和「记住事情」有关的能力：一个是跨会话、跨 agent 的共享记忆，你在 Claude Code 里定过的偏好、积累的经验，能不能让 Codex、Gemini 下次也用上；另一个是 headroom learn，它会翻你过去的编程会话记录，自动找出反复踩的坑，把纠正写进各 agent 的上下文文件里。这两块的源码分别在 headroom/memory/ 和 headroom/learn/ 目录下。headroom memory：跨 agent 的共享记忆在第二篇的学习里，我们其实已经见过记忆的命令了，运行 headroom wrap claude --memory，代理会在流量里自动注入和提取记忆，什么都不用改。想在自己的代码里用，库提供了一个包装函数 with_memory()：from openai import OpenAI from headroom import with_memory # 一行套上，之后照常使用 client = with_memory(OpenAI(), user_id="alice") # 第一个会话：随口告诉它你的偏好 client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "I prefer Python for backend work"}] ) # 之后换一个全新会话： client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "What language should I use?"}] ) # 回答会引用上个会话记下的 Python 偏好效果就是：第一个会话里你只随口说了句偏好，新会话里模型就能据此回答。这背后 with_memory() 在每次调用里做了三件事：第一件是注入相关记忆：按当前的用户消息做语义检索，把查到的相关记忆拼进消息发给模型。拼的位置有讲究，不是塞进系统提示词，而是拼到第一条用户消息里。系统提示词是缓存热区，动了会让 prompt cache 整段失效，这和之前的 CacheAligner 是同一个考量。第二件是指导模型如何记忆：Headroom 会在系统提示词里加一段固定指令，大意是，如果这轮对话里有值得长期记住的事实（用户偏好、身份、当前目标这类可复用的信息），就在回答之后输出一个 <memory> 块；寒暄、一次性问题、已经知道的信息不要记。格式是 XML 标签包一段 JSON：<memory>{"memories": [{"content": "..."}]}</memory>，没什么可记的，模型就输出空的 {"memories": []}。第三件是解析并保存记忆：Headroom 拿到响应后把这个块解析出来，逐条调 add() 存进记忆库里（下一节介绍）；然后把块从响应里剥掉再返回，你看到的回答是干净的。这整个提取内联在同一次调用里完成，没有额外的 API 调用。攒下的记忆可以用 headroom memory 系列命令直接管理：headroom memory list # 看存了哪些记忆 headroom memory list --scope USER # 只看 user 级（跨会话持久）的 headroom memory list --since 7d # 最近 7 天的 headroom memory stats # 统计 headroom memory export --output backup.json # 导出备份 headroom memory prune --older-than 30d # 清理 30 天前的记忆的使用比较简单，下面看它内部是怎么组织的。分层记忆上面说到，Headroom 从响应里解析出 <memory> 块之后，会逐条调 add() 把事实存进记忆库。这个 add() 就是记忆系统的核心入口，它所在的类是 core.py 里的 HierarchicalMemory。它把持久化存储、向量索引、全文索引、Embedding、缓存这些组件拼成一套统一的记忆 API，外界只用跟它打交道。看它的实现，一条记忆是怎么进来的：async def add(self, content, user_id, session_id=None, agent_id=None, tur…

学习 Headroom 的 CCR 可逆压缩
type: item
date: 2026-08-07T00:59:00+00:00
description: 在上一篇里，我们学习了 Headroom 的三大压缩器：处理 JSON 数组的 SmartCrusher、基于 tree-sitter 做 AST（抽象语法树） 感知的 CodeAwareCompressor，以及作者自训、在本地推理的文本压缩模型 Kompress。它们能把工具输出、日志、代码片段压掉一大半，token 随之骤减。不过压缩到这一步，有个绕不开的问题：压缩是会丢信息的。SmartCrusher 把 100 条搜索结果压成 10 条，剩下的 90 条并没有进模型的上下文。万一模型看完这 10 条，发现真正想要的答案在第 47 条上，怎么办？如果没有补救手段，那压缩省下的 token 就是以「模型可能答错」为代价换来的。Headroom 给这个问题的答案叫 CCR（Compress-Cache-Retrieve，可逆压缩）：压缩时把原文在本地缓存起来，同时告诉模型「你要是觉得不够，可以来取」。CCR 原理解析要实现压缩的「可逆」，必须得靠两样东西：一是原文不丢，按一个哈希键存进本地缓存（Python 侧默认是 ~/.headroom/ccr_store.db 这个 SQLite 库，Rust 侧还提供内存和 Redis 后端）；二是压缩产物里带上这个哈希键的标记，模型凭它知道「不够可以来取」。标记的形态不止一种。标准格式是方括号这样的：[100 items compressed to 10. Retrieve more: hash=a1b2c3d4e5f6a1b2c3d4e5f6]上一篇 SmartCrusher 压 JSON 数组时用的是另一种行内标记，作为一个哨兵元素嵌在数组末尾：[ {"ts": "10:00:01", "level": "INFO", "msg": "worker started"}, {"ts": "10:04:59", "level": "ERROR", "msg": "connection refused"}, {"_ccr_dropped": "<<ccr:a1b2c3d4e5f6 2_rows_offloaded>>"} ]两种形态作用都一样：告诉模型原文在哪、怎么取。模型如果只看压缩后的内容就够了，那什么都不用做，省下的 token 落袋为安；只有当它判断信息不够时，才拿着这个 hash 回来取原文。CCR 模块的职责定义在 headroom/ccr/__init__.py 文件里，分成四块：# 1. Tool Injection: 压缩发生时，代理往请求里注入 headroom_retrieve 工具 # 2. Response Handler: 拦截响应，自动处理模型发起的 CCR 工具调用 # 3. Context Tracker: 跨轮追踪被压缩的内容，按需主动展开 # 4. Batch Processing: 处理批量 API 结果里的 CCR 调用我们挨个看这四块，它们合起来覆盖了实时和异步两种场景下的完整取回链路。注入 retrieve 工具模型要能主动取原文，前提是它手里得有这么一个工具可用。这件事由 tool_injection.py 负责。它的核心是一个工具定义 create_ccr_tool_definition：CCR_TOOL_NAME = "headroom_retrieve" # Anthropic 格式（OpenAI / Google 各有一份，字段结构略有不同） { "name": CCR_TOOL_NAME, "description": ( "Retrieve original uncompressed content that was compressed to save tokens. " "Use this when you need more data than what's shown in compressed tool results. " # 取回被压缩掉的原始内容。当压缩结果里的数据不够用时调用它。 ), "input_schema": { "type": "object", "properties": { "hash": {"type": "string", "description": "Hash key from the compression marker"}, }, "required": ["hash"], }, }工具只有一个参数 hash，也就是压缩标记里那串哈希。注入的时机由 CCRToolInjector 控制，它的 scan_for_markers 会扫一遍请求里的所有消息，用一组正则去匹配各个压缩器留下的标记：_marker_patterns = [ # 标准格式: [N items compress…

学习 Headroom 的三大压缩器
type: item
date: 2026-08-03T07:09:00+00:00
description: 在上一篇中，我们跟着 compress() 走完了整条压缩管线。当时看到 ContentRouter 会先识别一段内容到底是 JSON、代码还是普通文本，再把它交给对应的压缩器处理。这三类文本对应的压缩器分别是 SmartCrusher、CodeAwareCompressor 和 Kompress，今天我们就来逐个学习下这三大压缩器。SmartCrusher：统计式压缩 JSON 数组工具调用返回的内容里，最常见的一种形态是一个很长的 JSON 数组，每个元素结构都差不多。比如调用一个 API 接口拉回来的一批记录，或者 Docker 的 JSON 日志一行一个对象，每条都是同一套字段。模型真正需要的往往是头部几条和尾部几条，中间几十条高度雷同的记录既占 token，又没带来新信息。SmartCrusher 做的就是统计这个数组的结构规律，保留最有代表性的若干条，把其余的丢进 CCR（Compress-Cache-Retrieve，可逆压缩）缓存。这个压缩器的 Python 实现已经在最新版本中整个搬到了 Rust。打开 smart_crusher.py，文件顶部有一行注释：所有数组压缩现在都走 headroom._core.SmartCrusher，也就是从 crates/headroom-py 编译出来的 Rust 扩展。Python 侧只留下了配置类和一层薄薄的转发。所以要讲清它的工作原理，得直接看 Rust 那边的实现。按数组分类选压缩策略SmartCrusher 不是一上来就压，它先让一个分析器把这个数组摸清楚。具体的做法是把数组里所有项的字段名合并起来，逐个字段统计一遍，给每个字段算一份 FieldStats。这份统计大致长这样：// crates/headroom-core/src/transforms/smart_crusher/types.rs（精简） pub struct FieldStats { pub field_type: String, // numeric / string / boolean / object / array pub count: usize, // 这个字段出现了多少次 pub unique_count: usize, // 有多少个不同的值 pub unique_ratio: f64, // 不同值占比 = unique_count / count pub is_constant: bool, // 是不是所有项都一样 // 数值字段专有：min_val / max_val / mean_val / variance / change_points // 字符串字段专有：avg_length / top_values（按频率排的高频值） }可以看出统计的维度挺有讲究：unique_ratio 看一个字段的取值有多「重样」，比如日志的 level 字段翻来覆去就 INFO、ERROR 几个值，这个比例就很低；而 message 字段每条都不太一样，比例就高。数值字段额外记方差和变化点（数值突然跳变的位置），字符串字段额外记平均长度和最高频的几个值。统计完每个字段，下一步把这些 FieldStats 翻译成这个数组「是什么类型」：有时间戳字段、数值字段又有方差的，是 time_series；同时存在一个高基数的文本字段（像 message）和一个低基数的级别字段（像 level）的，是 logs；有字段被判定为「像分数」的，是 search_results；都不沾边的，归为 generic认完类型，select_strategy 再据此选对应的压缩策略：// crates/headroom-core/src/transforms/smart_crusher/analyzer.rs（精简） pub fn select_strategy(&self, field_stats, pattern, item_count, ...) -> CompressionStrategy { if item_count < self.config.min_items_to_analyze { return CompressionStrategy::None; // 数组太短，不压 } if pattern == "time_series" && has_change_points { return CompressionStrategy::TimeSeries; // 时间序列，盯变化点 } if pattern == "logs" && message_field.unique_ratio < 0.5 { return CompressionStrategy::ClusterSample; // …

学习 Headroom 的整体架构
type: item
date: 2026-07-27T01:14:00+00:00
description: 在前两篇里，我们先认识了 Headroom 是什么：一个替 AI agent 压缩上下文的中间层，在工具输出、日志、代码、对话历史进入大模型之前先把它们压小，token 能减少 60% 到 95%。然后又花了一整篇学习它怎么用，重点是 wrap 和 proxy 两种接入方式：headroom wrap claude 把它套在编程 agent 前面，headroom proxy 起一个本地代理，再用 headroom doctor、headroom dashboard 看健康状况和实时节省。用起来之后，自然会想知道它内部是怎么组织的。Headroom 表面上给了四种完全不同的用法（库、代理、agent 包裹、MCP server），但它们实际上用的都是同一套压缩逻辑。它还同时用了 Python 和 Rust 两种语言，这两边又是怎么拼在一起的。今天我们就从全局视角把这些理清楚，为后面几篇的源码拆解打个底。数据流总览先看最外层。不管你用哪种方式接入，Headroom 做的事情本质上只有一件：在请求发给大模型之前拦一道，把里面的内容压缩，再原样转发出去；响应回来时再看要不要处理。整条链路如下图所示：这里有两点值得注意。第一，Headroom 站在 agent 和大模型中间，对两边都尽量透明，agent 以为自己直接在跟模型说话，模型收到的则是已经压过的内容。第二，压缩是可逆的。图里最后那段 需要时取回原文 的交互，对应的是 Headroom 的 CCR（Compress-Cache-Retrieve，可逆压缩）机制：原文缓存在本地，模型真需要细节时可以通过一个专门的工具把它取回来。CCR 的细节我们留到后面单开一篇讲，这里只需知道压缩掉的东西并没有真的丢。四个入口，一条管线Headroom 对外暴露了四种用法，它们的调用姿势差别很大：用法接入方式典型命令 / 代码库在代码里直接调函数from headroom import compress代理起一个本地 HTTP 代理headroom proxy --port 8787agent 包裹把编程 agent 套进代理headroom wrap claudeMCP server作为工具挂给模型headroom mcp install看起来是四条路，但顺着源码往下走，它们最终都汇到同一个地方。库这条路最直白。compress() 定义在 headroom/compress.py 里，它内部通过一个懒加载的单例函数 _get_pipeline() 拿到压缩管线：def _get_pipeline() -> Any: """Get or create the singleton compression pipeline.""" global _pipeline if _pipeline is not None: return _pipeline with _pipeline_lock: # ... from headroom.transforms import TransformPipeline # 默认管线：CacheAligner → ContentRouter _pipeline = TransformPipeline() return _pipeline可以看到，compress() 拿到的是一个 TransformPipeline 实例。代理这条路呢？headroom proxy 启动的服务定义在 headroom/proxy/server.py，它的核心类 HeadroomProxy 在初始化时同样构造了 TransformPipeline，而且按不同厂商各建了一个：# headroom/proxy/server.py self.anthropic_pipeline = TransformPipeline(...) self.openai_pipeline = TransformPipeline(...)headroom wrap 又是什么？我们可以看下 headroom/cli/wrap.py 开头的说明，它做的是先起一个代理，再把编程 agent 的流量指过去。也就是说 wrap 是 proxy 的上层封装，它并没有另写一套压缩逻辑，压缩仍然发生在代理里的 TransformPipeline。MCP 这条路也一样。Headroom 的 MCP server 定义在 headroom/ccr/mcp_server.py，它对外暴露一个 headroom_compress 工具，模型主动调用这个工具时，底层走的还是同一套压缩入口。所以四种用法在架构上是一个漏斗：这个设计的好处很明显：新增一种接入方式，或者修一个压缩上的 bug，都只需要动 TransformPipeline 这一处…

Headroom 上手：wrap 与 proxy 两种接入方式实战
type: item
date: 2026-07-24T00:34:00+00:00
description: 在上一篇中，我们认识了 Headroom 这个项目。它是一个面向 AI agent 的上下文压缩层：在工具输出、日志、RAG 片段、文件这些内容进入大模型之前先压缩一遍，官方称 token 能减少 60% 到 95%，而答案基本不变。我们也提到了它的四种用法：当库直接调用、当中间代理（proxy）使用、把编程 agent 整个包裹（wrap）起来、以及作为 MCP server 接入。其中 wrap 和 proxy 这两条路还有不少细节没展开，这一篇我们就拿它俩练手。动手之前先分清两者的区别：它们本质上都是让流量过一遍本地代理进程，差别只在谁把客户端指向代理。wrap 面向 Claude Code 这类现成的编程 agent，一条命令就把环境和配置替你设好，不想用了再用 unwrap 一键还原；proxy 只管起代理，把客户端指过来这一步交给你，适合任意 OpenAI 或 Anthropic 兼容的客户端。下面分别来看。headroom wrap claude 实战如果你日常就是用 Claude Code 写代码，wrap 是门槛最低的一条路。它的逻辑在 headroom/cli/wrap.py 里，wrap 是一个 Click 命令组，claude、codex、copilot、cursor、aider、opencode、cline、continue、goose 等一长串编程工具各是它下面的子命令。Click 是 Python 里最常用的命令行框架之一，用装饰器的方式定义命令、子命令、选项和参数，帮你处理参数解析、类型校验、帮助信息生成这些琐事。headroom 这个 CLI 就是用它搭起来的。直接运行：$ headroom wrap claude启动过程输出如下：这一条命令背后其实做了一连串事。顺着 wrap.py 里 claude() 函数读，主要步骤是这样的：检查 claude 在 PATH 里；启动本地代理子进程，等它就绪；配置上下文工具 rtk；注册 MCP 取回工具；设置代码图压缩器；把代理地址写进环境变量和 Claude Code 的配置文件；启动 Claude Code；会话退出时还原 base URL、停掉代理。我们逐一看几个关键动作。启动本地代理claude() 会调用 _ensure_proxy 起一个后台代理。实际上就是把自己作为一个子进程再拉起来：python -m headroom.cli proxy --port 8787，并把 --learn、--memory、--backend 等参数透传过去。起完之后它会轮询端口，等代理真正能接受连接了才继续：# headroom/cli/wrap.py（精简） for _i in range(timeout_seconds): time.sleep(1) if _check_proxy(port): click.echo(f" Logs: {log_path}") return proc if proc.poll() is not None: # ... 进程提前退出，读日志尾部报错 raise RuntimeError(...)代理启动时会把已经缓存好的机器学习组件加载进内存，然后 uvicorn（Python 的 ASGI 服务器）才绑定端口。模型都是延迟加载的：没下载过的模型启动时不会去拉，而是推迟到第一次真正用到时再下载，免得卡住启动。即便只加载已缓存的组件，初始化本身也要点时间，源码里默认给了 45 秒超时，检测到装了 torch 这类重型 ML 依赖时放宽到 90 秒，不够可以用环境变量 HEADROOM_WRAP_PROXY_TIMEOUT 调大。Headroom 在压缩链路上会用到几个模型，分工和加载时机各不相同：Magika（google/magika）：Google 开源的 AI 文件类型检测工具，用一个很小的深度学习模型读内容开头、中间、结尾的若干字节，就能判断这段内容到底是 JSON、源代码、日志还是纯文本，官方称在 200 多种类型上的准确率约 99%，Gmail、Google Drive 都用它做文件类型识别。在 Headroom 里，它负责给后面的 ContentRouter 做类型判断，好把每段路由到对的压缩器。它的模型（约 3 MB）随 pip 包自带，不走 HuggingFace，所以启动时就加载好了，不需要联网下载。Kompress（chopratejas/kompress-v2-base）：作者自己训练的文本压缩模型，专门处理纯文本，给每个 token 打重要性分、丢掉低价值的部分。它以 ONNX 格式发布，跑在本地 Rust 核心里推理。这个模型托管在 HuggingFace，首次真正压缩文本时才下载一次，之后走本…

Headroom 介绍：给 AI agent 装一层可逆的上下文压缩
type: item
date: 2026-07-20T00:33:00+00:00
description: 用过 Claude Code、Codex 这类 coding agent 的人，大概都有过盯着它烧 token 的经历。你让它排查一个线上问题，它先 grep 一遍代码，再 cat 几个文件，跑一轮测试看输出，翻几屏日志，最后去 GitHub 上查相关 issue。这一连串动作里，每一步的输出都会被原封不动塞回对话，成为下一步推理的上下文，而这些内容大多又长又重复，真正有用的可能就那么几行。一个任务还没跑完，token 就已经哗哗地流出去了。这些 token 你既要按输入计费为它们付钱，它们还会挤占本就有限的上下文窗口。Headroom 就是冲着这个场景来的。它是 Netflix 工程师 Tejas Chopra 在 2026 年 1 月开源的一个工具，做的事情说穿了很简单：在一段内容进入大模型之前，先替你压缩一遍，把那些又长又重复的部分挤掉，官方称 token 能减少 60% 到 95%，而答案基本不变。这个系列我们就来好好研究下 Headroom。Headroom 介绍Headroom 是一个面向 AI agent 的上下文压缩层。它的定位很直接：在一段内容进入大模型之前，先把它压缩一遍，在不改变语义的前提下把喂给模型的 token 数砍下来。那为什么要专门做这么一层压缩？关键在于喂给 agent 的内容里有大量冗余。agent 干活靠的是不停调用各种工具，比如搜代码、读文件、执行命令、查日志，每次调用的返回都会被塞回对话里，作为下一步推理的上下文。而这些工具输出往往又长又啰嗦：一次代码搜索命中上百处，每处还带出成片的源代码，可真正有用的往往只是命中的函数签名和它所在的位置，大段函数体的实现细节模型多半用不上；一份线上事故的日志动辄几万行，真正有用的可能就那么几段异常堆栈；RAG 拉回来的文档片段，彼此之间常有大量冗余；一个大 JSON 数组里，几百个元素结构完全一样，模型其实看几个样本就够了。这些内容如果一股脑全部塞进上下文，带来两个后果。一是贵：输入 token 越多，每次调用越贵，而 agent 一个任务里可能要调用几十上百次模型。二是挤：上下文窗口是有上限的，垃圾内容占多了，真正重要的信息反而被挤出去或者被稀释，模型的表现还会下降。Headroom 的思路是在这些内容进模型之前先做一道压缩。它给出的官方压缩基准是这样的：场景压缩前 token压缩后 token节省代码搜索（100 条结果）17,7651,40892%SRE 事故排查65,6945,11892%GitHub issue 分诊54,17414,76173%代码库探索78,50241,25447%压缩幅度跟内容类型强相关：结构高度重复的工具输出（代码搜索、日志）能压掉九成，而信息密度本来就高的代码库探索只能压掉不到一半。这也符合直觉，冗余越多的地方越好压。光压得狠没用，关键是压完之后模型答得对不对。作者跑了几组标准评测集，给出的准确率对比是：评测集压缩前压缩后备注GSM8K（数学推理）0.8700.870持平TruthfulQA（事实性）0.5300.560+0.030SQuAD v2（阅读理解）97%97%压缩 19%BFCL（函数调用）97%97%压缩 32%从这组数字看，压缩在保住准确率的前提下把 token 砍了下来，个别评测集甚至略有提升。把无关的冗余去掉、让相关信息更突出，本来就有可能帮到模型。核心特性除了压缩比这个硬指标，Headroom 还有几个设计上的特点。内容感知路由：Headroom 不是拿一套算法压所有东西，而是先判断内容类型，再挑对应的压缩器。这个分发的活由 ContentRouter（内容路由器）来干，它能识别 JSON、源代码、纯文本、日志、diff 等类型，把每一段路由到最合适的压缩器上，混合内容还会被拆开、分段路由再拼回去。CCR 可逆压缩：压缩最让人担心的是丢信息，万一模型后面正好需要被压掉的那段原文怎么办？Headroom 的答案是 CCR（Compress-Cache-Retrieve，压缩-缓存-取回）。它在压缩的同时把原文缓存在本地，并给模型提供一个 headroom_retrieve 工具，模型需要更细的内容时可以主动调它，在缓存的存活时间（TTL）内把原文取回来。压缩因此是可逆的，而不是一刀切地删掉。跨 agent 记忆：现在很多人同时用 Claude、Codex、Gemini 几个 agent 干活，但它们之间的上下文是割裂的。Headroom 提供了一层跨 agent 的共享记忆（SharedContext），能在多个 agent 之间传递压缩后的上下文，还带来源标注和自动去重。本地优先：压缩、缓存、记忆都在本机完成，数据不出机器。它内置的文本压缩模型 chopratejas/kompress-v2-b…