LLM Token 优化实战:从 Headroom 看懂上下文压缩为何是 Agent 成本核心
很多人对 LLM 成本的判断仍停留在一句话上:模型越来越便宜,所以 token 不必太在意。
这个判断只说对了一半。单个 token 的价格确实在下降,但 Agent 的使用方式正在将 token 消耗成倍放大。普通聊天中,一次请求可能只有几千 tokens;到了 AI Coding、Deep Research、RAG、日志分析、多工具 Agent 等场景,一次任务可能包含几十次工具调用。
每次工具调用又会把文件内容、搜索结果、终端输出、JSON 响应、报错堆栈和历史对话塞回上下文。真正的成本不再来自“用户问了一句话”,而是来自“系统为了回答这一句话,把多少机器生成的冗余上下文反复发给模型”。
Headroom 官方将自己定义为 LLM 应用的 context optimization layer,重点处理 tool outputs、DB results、file reads、RAG results 等进入模型前的内容。这个定位很关键:它不是模型压缩、不是量化、不是蒸馏,而是解决“发给模型之前的上下文太臃肿”的问题。
一句话概括:
LLM Token 优化 = 让模型只阅读高密度上下文,而不是所有原始材料
为什么现在又开始讨论 token 压缩?
一个 AI Coding Agent 为了修一个 bug,可能要读目录、搜文件、打开源码、跑测试、看日志、分析报错、追调用链、改代码,再把新日志交给模型判断。
每一步都会产生上下文,而且这些上下文不是一次性消费,而是在多轮任务里不断累积。第 1 轮可能只有 5K tokens,第 3 轮到 30K,第 8 轮就可能接近 100K。越往后,每次请求携带的历史越长,成本越容易滚起来。
RAG 也类似。很多系统为了“召回更全”,把 top-k 设得很大,把 chunk 原样塞进 prompt。看似上下文更完整,实际上经常把低相关片段、重复内容、HTML 残留、JSON 元数据一起送给模型。
日志分析更典型。一个 SRE Agent 读取 5 万行日志,大部分都是正常请求、重复 warning、时间戳、线程名、traceId 和固定字段。真正关键的信息可能只有几个 error、exception、timeout 或状态突变。
所以第一条原则是:
不要把 LLM 当压缩软件、grep、数据库、搜索引擎和日志聚合器使用。
LLM 应该消费整理后的高密度信息,而不是所有原始材料。
token 成本公式:别只看 input tokens
LLM API 成本通常可以拆成四类:
总成本 = 输入 token 成本 + 输出 token 成本 + 缓存写入成本 + 缓存读取成本
进一步看:
Cost = input_tokens * input_price + output_tokens * output_price + cache_write_tokens * cache_write_price + cache_read_tokens * cache_read_price
这里有四个工程要点。
第一,输出 tokens 往往比输入 tokens 更贵,所以不能只盯输入,也要限制输出长度,避免模型反复生成大段解释。
第二,Agent 场景的输入 tokens 往往远大于输出 tokens。模型可能只输出几百到几千 tokens,但输入可能达到几十万 tokens。
第三,prompt cache 能降低重复前缀成本,但前提是上下文结构稳定。如果每次都把当前时间、随机 sessionId、临时路径和工具输出塞在前缀,就会破坏缓存命中。
第四,压缩和缓存不是替代关系。压缩解决“少发无用内容”,缓存解决“重复内容不要重复算”。
Headroom 解决的是上下文入口问题
从架构上看,Headroom 位于应用或 Agent 和 LLM Provider 中间,可以通过 SDK、Proxy、MCP、wrapper 等方式接入。它更像一层 LLM 应用里的 context middleware。
过去 Web 系统有 API Gateway、鉴权中间件、缓存中间件、日志中间件。现在 LLM 应用也需要上下文中间件,负责压缩工具输出、稳定缓存前缀、识别内容类型、控制上下文预算、保留原始内容以便回溯,并统计 token、成本和质量指标。
Headroom 的价值不在于某个宣传数字适合所有场景,而在于它指出了一个方向:在 Provider 之前,先做 context optimization。
Token 优化不是一种技术,而是一套系统工程
把 prompt 写短一点只是最浅的一层。真正的 token 优化至少包括七件事。
第一,Prompt 精简。删除无意义的礼貌语、重复约束、历史遗留说明和过度角色扮演,保留任务目标、输出格式、边界条件、工具调用规则和失败处理规则。
第二,上下文裁剪。多轮任务里保留最近任务、关键决策、用户偏好和当前状态,丢弃已经完成的中间探索。不要只用粗暴 rolling window,否则可能误删“不要改数据库结构”“必须兼容 Java 8”这类早期约束。
第三,检索前过滤。RAG 不应该“召回很多,再让 LLM 自己判断”。更合理的链路是 query rewrite、多路召回、去重、rerank、chunk 合并、query-aware compression,最后只发送高相关片段。
第四,工具输出压缩。shell 输出、JSON 响应、数据库查询结果、搜索结果、测试日志、文件列表和 API 返回值通常有强结构,适合保留 schema、异常项、统计分布、首尾样本、文件路径、行号和命中片段,而不是原样塞给模型。
第五,Prompt caching。system prompt、工具定义、输出格式、长期项目背景、API schema 和用户长期偏好适合放在稳定前缀;当前时间、随机 ID、临时路径、当前请求参数和每轮变化的工具输出应该放后面。
第六,输出长度控制。比如“只输出 JSON”“只输出 diff”“只输出 5 条以内结论”“错误时只返回 error_code 和 reason”。输出控制不是为了短,而是为了符合消费端需要。
第七,模型路由。不是所有上下文都需要昂贵模型处理。可以先用规则或便宜模型做过滤、分类、去重、摘要、字段提取,再把高价值信息交给强模型。
原始输入 -> 规则过滤 -> 小模型压缩 / rerank -> 强模型推理 -> 结构化输出
这相当于把强模型从“清洁工”变成“决策者”。
Headroom 的几个关键思路
CacheAligner:让 provider cache 真正命中
很多系统明明有大量重复 prompt,却命不中缓存,是因为前缀里混进了动态内容。例如把当前日期、sessionId 放在工具定义前面,就会导致后面的稳定内容也无法复用。
更合理的结构是:系统规则、工具定义、输出格式放在稳定前缀,当前时间、临时状态、工具输出放在后缀。这个优化看似简单,但 Agent 场景价值很高,因为工具定义和系统规则往往很长,而且每一轮都会重复发送。
ContentRouter:不同内容走不同压缩器
压缩不能一刀切。JSON、日志、代码、HTML、普通文本、搜索结果的压缩方式不同。
JSON arrays -> 结构化摘要 + 异常项 + 样本
Build logs -> 错误堆栈 + 前后窗口 + exit code
Search results -> 文件路径 + 行号 + 命中片段
Source code -> 保守保护当前代码上下文
Plain text -> 摘要,但保护否定词、数值和条件
错误压缩比不压缩更危险。把代码当普通文本压缩,可能删掉函数体;把日志当文章摘要,可能丢掉唯一关键异常;把 JSON 随机截断,可能破坏结构。
可逆压缩:压缩但不丢证据
普通压缩最大的问题是删掉的内容没了。压缩器不知道哪些信息后面会重要,一旦误删,模型也未必知道自己缺信息。
更可靠的思路是可逆压缩:prompt 中只放摘要、异常和代表样本,原始内容存入本地 cache,并提供 retrieval handle。模型需要时可以调用工具取回原文,而不是在压缩摘要上硬猜。
原始 JSON 1000 条 -> prompt 中放摘要 + 异常 + 代表样本 -> 原始 1000 条存本地 cache -> 模型需要时调用 retrieve(id)
长上下文不等于不需要压缩
现在很多模型支持 200K、1M 上下文,但长上下文解决的是“放得下”,不是“用得好”,更不是“用得便宜”。
长上下文仍然有成本高、延迟高、注意力不稳定三个问题。更工程化的做法是把上下文分成三层:
第一层:必须原样进入 prompt 的核心信息
第二层:压缩后进入 prompt 的辅助信息
第三层:不进入 prompt,但可通过工具按需检索的原始信息
用长上下文兜底,用压缩提高密度,用检索保证召回,用缓存降低重复成本。
最适合压缩的场景
AI Coding Agent 最典型。目录结构、搜索命中、大文件、构建日志、测试日志、package lock、依赖树、Git diff 和历史对话都容易产生大量冗余。优化策略是:目录只保留相关路径,搜索结果保留文件名、行号和命中片段,构建日志保留失败命令、错误堆栈和最后统计,当前分析代码默认不压缩。
RAG 问答系统的重点是减少低相关 chunk 和重复内容。不要把压缩当成召回质量差的补救,应该先解决召回和排序,再做 query-aware compression。
日志分析 / SRE Agent 天然适合压缩,因为日志重复度高、结构固定、异常稀疏。应该保留 error、exception、failed、异常前后窗口、状态变化、慢请求、超时、重试、traceId、requestId、host、pod,并聚合重复日志。
数据库查询结果 / API 响应也适合压缩。如果返回 1000 行,模型未必需要逐行阅读,可以压缩为字段说明、总数、分组统计、异常值、top/bottom、首尾样本和与问题匹配的记录。
哪些场景不能盲目压缩?
法律、医疗、金融等强合规场景不能随意摘要后让模型判断,必须保留证据链、原文片段和来源位置。
数学推理、算法证明、形式化验证不能压缩题干、公式、约束和已知条件。
当前正在修改的代码不应该压缩。代码修复需要精确上下文,尤其是函数体、条件判断、注解、泛型、事务边界和异常处理。
用户原始意图最好不要压缩。用户表达里的限制、偏好、否定、语气和优先级都可能影响最终结果。
短请求也没必要压缩。300 tokens 以内的普通对话,引入复杂压缩层可能反而增加延迟。
好的 token optimizer 不是“尽量压缩一切”,而是知道什么时候压缩、怎么压缩、压缩后怎么回退,以及什么时候坚决不压缩。
工程落地路线:从观测开始
如果要给自己的 LLM 应用做 token 优化,不建议一上来就接入复杂压缩器。更稳妥的路线是:
- 先做 token 观测,记录 input_tokens、output_tokens、cached_input_tokens、tool_output_tokens、rag_context_tokens、history_tokens、system_prompt_tokens、latency、TTFT、cost_estimate、compression_ratio 和 answer_quality_score。
- 拆分上下文来源,不要只看总 input tokens,要拆成 system prompt、tool definitions、conversation history、RAG chunks、tool outputs、user input。
- 给不同内容设置 token budget。超出预算时,先去重,再裁剪低相关内容,再结构化压缩,最后才摘要。
- 工具输出先结构化,再压缩。不要把原始 stdout 直接塞给模型,可以返回 summary、important_lines 和 full_log_ref。
- 缓存稳定前缀。系统提示词、工具定义、项目规范和输出格式放前面;当前用户问题、当前工具输出、临时信息放后面。
- 建立质量评测。不要只看 compression ratio,还要看准确率、工具调用成功率、RAG 忠实度、引用正确性、JSON 有效性、延迟、成本和回退检索率。
- 灰度上线。压缩层应该支持 off、audit、optimize 三种模式。先 audit,只统计不改变请求;确认低风险后再 optimize。压缩失败必须 fail open,返回原文。
结语:token 压缩不是抠成本
LLM Token 优化会变成 Agent 工程里的基础设施。
早期 LLM 应用只要会写 prompt 就能跑起来;下一阶段,真正的差距会出现在上下文管理上。谁能把有限 tokens 变成高密度信息,谁就能获得更低成本、更低延迟和更稳定的答案。
Headroom 的被关注不是偶然。它击中的不是小技巧,而是 Agent 时代的结构性问题:工具越来越多,上下文越来越长,模型越来越强,但发送给模型的信息也越来越脏。
不要让模型阅读垃圾。不要为重复上下文付费。不要把长上下文当成无限垃圾桶。
真正应该进入模型的,是经过筛选、压缩、排序、缓存和可回溯管理的高价值上下文。
错误速查卡
| 症状 | 根因 | 定位 | 修复 |
|---|---|---|---|
| prompt cache 命中率长期为 0,成本居高不下 | 稳定前缀中混入了 sessionId / 当前时间 / 临时路径 / 每轮工具输出 | 抓取连续 N 次请求,比对 prefix 前 1024 tokens 是否一致 | 引入 CacheAligner:system prompt + 工具定义 + 输出格式放前缀,动态内容放后缀;用 Cache-Control: ephemeral 显式控制 |
| 单次输入 tokens 暴涨但答案质量未提升 | 工具原始输出(shell / JSON / 日志)直接塞进 prompt | 统计 tool_output_tokens 与 answer_quality_score 相关性 |
工具输出结构化:返回 summary + important_lines + full_log_ref;引入 ContentRouter 分类型压缩 |
| AI Coding 任务第 8 轮后单次成本翻倍 | 多轮上下文不断累积,未做上下文裁剪 | 看 history_tokens 趋势 + 任务完成度 | 引入任务级 context budget,裁剪已完成步骤;保留关键决策、用户偏好、当前状态 |
| 压缩后模型开始“答非所问”或编造 | 一刀切摘要丢失关键异常 / 数值 / 否定词 | 比对压缩前后 answer_quality_score + 引用正确性 | 改用可逆压缩(摘要 + 本地 cache + retrieve handle);压缩失败必须 fail open 返回原文 |
| 压缩 JSON 后模型报“语法错误 / 字段缺失” | 随机截断破坏 JSON 结构 | 看压缩输出是否能 JSON.parse | ContentRouter 对 JSON 走“结构化摘要 + 异常项 + 样本”,不做截断式压缩 |
| 日志压缩后丢失唯一 FATAL / exception | 把日志当纯文本摘要 | grep 关键错误关键字是否在压缩结果中 | 日志走专用压缩:保留 error/exception/failed + 前后窗口 + exit code + 聚合重复行 |
| 长上下文模型依然贵、慢、注意力飘移 | 误以为“上下文长就不用压缩” | 测 TTFT、cost_per_task、长上下文 needle-in-haystack 准确率 | 引入三层上下文:核心原样 / 辅助压缩 / 检索按需 |
| 当前正在修改的代码被压缩后修复失败 | 代码压缩丢失函数体 / 条件 / 注解 / 事务边界 | 看 diff 是否引用了被压缩片段 | 把“当前编辑的代码文件”加入禁压列表;只压周边文件 |
| 合规/医疗/金融场景压缩掉证据链 | 一律摘要导致原文丢失 | 审计 prompt 是否含原文引用与来源位置 | 强合规场景走白名单:只压缩可公开字段,原文必须可检索 |
| RAG 召回质量差,寄希望于压缩补救 | 召回和排序未做就压缩 | 看 recall@K、context relevance | 先 query rewrite + 去重 + rerank + chunk 合并,再做 query-aware compression |
| 引入压缩层后延迟反而上升 | 300 tokens 以内的短请求也走压缩管线 | 看短请求 P50/P99 latency | 短请求(< 1K tokens)直接跳过压缩;长请求再启用 |
| prompt 中加入压缩层后输出变得更啰嗦 | 未对输出设长度控制 | 看 output_tokens 分布 | 加输出约束:“只输出 JSON”、“最多 5 条结论”、“错误时只返回 error_code 和 reason” |
| 模型路由后小模型浪费 token、大模型响应慢 | 所有请求都走强模型 / 所有请求都走弱模型 | 看 task_type → model 分布 + cost_per_task | 按任务分级:过滤/分类/去重走小模型或规则,决策/推理走强模型 |
headroom wrap 包装后工具调用报错 |
包装器与原 agent CLI 参数不兼容 | 看 wrap 启动日志 + 工具调用轨迹 | 升级到最新 wrap 模式;或在 SDK 模式做最小集成 |
| 压缩层故障导致整个 Agent 宕机 | 压缩层 fail closed,异常直接抛出 | 看异常日志 + 链路追踪 | 压缩层必须 fail open:异常时直接返回原文,不阻断业务 |
| audit 模式看到大量 compress_ratio 接近 1 | 压缩器对高密度内容强行压,浪费 CPU 且无收益 | 看 content_type × compression_ratio 分布 | 对压缩收益 < 5% 的内容直接跳过;记录白名单 |
| 第三方压缩器偷偷把数据外传 | 压缩器无本地化保证 | 看压缩器是否调用外网 / 是否经过审计 | 优先 local-first 方案(如 Headroom 的 local-first · reversible);外网压缩走严格白名单 |
headroom learn 写入的 CLAUDE.md 覆盖了项目原有规则 |
learn 未做变更范围控制 | diff CLAUDE.md 改动点 | 给 learn 设置写入白名单与 PR review;敏感目录加入 exclude |
参考来源
- Headroom GitHub 仓库:github.com/chopratejas…
- Headroom 官方文档:headroom-docs.vercel.app/docs
- 镜像仓库:github.com/gglucass/he…
- Headroom 实战解读:blog.csdn.net/forcedRegCs…
- 今日开源第 5 期 Headroom:www.cnblogs.com/zhang-yd/p/…
