Headroom
Headroom:给 AI 对话装一个上下文仪表盘

Headroom 是继 DAG-chat 之后,第二个自己搞得比较大的个人项目。
开发的初衷很简单:我自己算得上是 AI 重度用户——日常写代码的主力是 Claude Code、Codex、OpenCode 这类编程智能体,但网页版 AI 聊天同样天天在用,查资料、读长文、头脑风暴,很多活儿还是在网页端干。网页端有个绕不开的问题:对话聊得越多,AI 遗忘得越厉害。对重度用户来说,这不算什么新鲜事,背后的原理也都懂——对话内容超过了上下文窗口,早期的内容被截断或者压缩了,模型开始基于残缺的记忆推理。
但有一件事我一开始想不明白:为什么平台官方不做 token 消耗统计?
服务器端要拿到这个数字,技术上毫无难度,DeepSeek 的 API 甚至早就返回 accumulated_token_usage。和 Kimi 探讨交流了一下,大概能想到的原因无非这么几条:
- 需要这个功能的人是少数。大多数用户聊几轮就结束,根本碰不到上下文上限,给界面加一个侧边栏,对他们是噪音,还破坏聊天窗口的极简感;
- 标称值经不起测量。”1M 上下文”是招牌卖点,但这个数字经不起两点推敲:一是不少模型实际聊到 200K 左右就开始遗忘,标称和真实的有效上下文差着一个量级;二是算力紧张的时候,平台还会临时收缩上下文——2026 年 7 月 GPT-5.6 发布后的流量高峰里,OpenAI 官方就把 Codex 和 ChatGPT Work 的上下文设置从 372K 临时降到了 272K,挨降的包括 Plus、Business、Pro 付费用户。DeepSeek 免费网页版则更隐蔽:2026 年 3 月起,单会话的 token 上限明显收缩,没有任何公告,用户只能靠”对话说满就满”的体感去发现。没有仪表盘,这些差距和收缩无人察觉;一旦有了仪表盘,反而是自己把自己往火上烤;
- 数字一上台面,产品叙事就不好讲了。订阅制卖的是”不用想成本”的抽象,实时显示 token 消耗,等于让用户拿着尺子量每次对话的真实成本;况且这笔账算出来也没法跟用户解释清楚——工具调用、网页搜索的结果暂存、系统级提示词都会占用 context window,真要做统计,解释成本极高。
说到底,无论是技术上,还是商业上,AI 平台都没有动力去做这件事,恰恰相反,他们还有很多理由不去做这件事。但用户侧的痛是真实的——所以我决定自己做一个。
演示视频:Headroom:查看 AI 聊天中剩余的上下文窗口空间
核心思路
决策一:不打包真实 tokenizer,用六种书写系统的线性模型做估算。
每个平台用的 tokenizer 不一样:ChatGPT 用 tiktoken,Gemini 用 SentencePiece,Qwen 用 BBPE,国产平台用 BPE 变体。全打包进去,扩展体积会爆炸,而且跟模型版本强耦合。我的做法是:把文本按书写系统分类——CJK、假名、谚文按字符计数,西里尔、阿拉伯、拉丁按单词计数——每种乘以一个 per-platform 的系数。
这套系数最早真拍过一次脑袋。当时用的是圈内流传多年的经验值——”1 个汉字 ≈ 1.5-2.5 token”——按它折算的中文系数(1.3~1.8)上线后,实测高估了 2~3 倍。查了各平台的 tokenizer 才明白原因:这个经验值是 cl100k 时代的产物,老词表小,汉字基本一个字占一个 token;现在的词表动辄 13~26 万(ChatGPT 的 o200k 是 20 万,Qwen 到了 248K),多字词组会被整体打包成单个 token,实测 CJK 系数只有 0.58~0.83 token/字,所有平台都小于 1。经验值没有错,它只是恰好只在旧词表上成立。
所以系数必须实测,不许拍脑袋:把每个平台真实的 tokenizer 词表文件喂进最小二乘回归去拟合。实测下来,自然语言对话精度在 ±15% 以内,整个扩展打包后只有 70KB 左右。
精度边界是诚实的:重代码的英文对话会低估 19-29%,韩英混用会高估 20-36%。它是个仪表盘,不是计费表,这个定位我从一开始就明确了。
决策二:adapter 架构隔离平台差异。
7 个平台来自 6 家不同的公司,采用适配器模式,各自独立迭代,今天所有接口都用 POST,不代表明天也是。所以 background 和 content script 的管线是通用的,所有平台差异都封装在 adapters/*.ts 里。一个平台的 bug 修复,绝不允许改变另一个平台的行为。如果未来要新增某个 AI 网页端的支持,也就是增加一个 adapter 文件,再在几处注册一下的事儿。这个决策很快就被验证了:2026 年 7 月,ChatGPT 把发送接口从 /backend-api/conversation 迁到了 /backend-api/f/conversation——而且不是一次整体切换,是分 cohort 灰度,旧端点继续保留给未迁移用户兜底。整个迁移在 ChatGPT 的 adapter 里就两个字段:completionUrl 换成新地址,continueUrl 留着旧地址。其他六个平台对这个迁移完全无感。
决策三:用 Upstash Redis 做云端存储。 这个决策是一开始就做的,做完以后发现,它反而是最鸡肋的功能点。
初衷是跨设备:在公司电脑的 Chrome 里聊了 30 轮,回家想换 Edge 或者另一台电脑继续,数据不能丢。要存的内容以 KV/JSON 为主,调研下来只有 Upstash 提供一定量的免费额度,于是基于它设计了数据层,顺带把用户的 settings 也放了进去。
但开发调试完以后发现,这个场景基本不存在:第一,绝大多数用户切换浏览器和电脑的频率很低,很多人长期一台电脑、一个浏览器;第二,就算有多设备需求,也没必要为同步对话数据上云。实测下来,不配 Upstash 时,插件打开新对话页现场解析历史、逐轮估算 token,也不算慢。唯一的例外是 Gemini——它没有可用的历史接口,所有对话内容都在 DOM 里,30 轮的长对话必须滚动到最早一条才能解析完。
所以云端同步就显得很鸡肋了。值得上云的东西,最后只剩 settings:各平台默认的 context window、红黄绿告警阈值、各平台书写系统的 token 估算系数。
踩坑心得
浏览器扩展开发,坑比想象中多,而且分布不均匀——一半在浏览器 API 上,一半在逆向平台接口上。挑五个印象最深的。
1. MV3 的图标变灰 bug。 Chrome 一旦配置了 sidePanel,action.disable() 就不会在视觉上把工具栏图标变灰。这个 bug 吃掉了十几个调试轮次,五个 AI 助手问了个遍,试遍了 per-tab disable、declarativeContent、裸 chrome.action、灰度 PNG……全部无效。Chromium 团队的态度是 “Works As Intended”,issue 41419485 挂在那里连 bug 都不算:配置 sidePanel 后,工具栏点击被绑定成系统级行为,UI 优先级压过 disable() 的视觉状态,而 MV3 又砍掉了 MV2 pageAction 的 hide()/show()。社区里所有成熟的 sidePanel 扩展,用的都是同一个民间偏方——三维 per-tab ACL:手动切换彩色/灰色图标、拦截 onClicked、按 tab 开关 sidePanel。action.enable()/disable() 彻底废弃不用。
2. 侧边栏这个功能,三个浏览器三种行为。 Chrome 里侧边栏随时可以关,Firefox 的 sidebarAction.close() 却要求用户手势——tab 切换不算手势,这个限制硬编码在 C++ 层(Bug 1453355),四个 AI 助手加 Bugzilla、MDN 交叉验证,确认没有绕法,只能退而求其次:切到非平台页时用 setPanel() 把侧边栏内容换成”该页不支持”的提示页,而不是试图关掉它。Edge 则是反过来的问题:侧边栏关掉后切回来不会自动恢复,微软答复 “by design”,issue 从 2024 年 11 月挂到现在零更新。同样是官方的侧边栏 API,三个浏览器三种行为,这就是跨浏览器扩展的日常。
3. 逆向平台接口:”复制为 cURL” 会骗你。 平台的历史接口走 Bearer token 认证(token 藏在 localStorage 里),但浏览器的”复制为 cURL”出于安全考虑会把 Authorization 头剥掉——你拿到的 cURL 永远不会带 token,照抄必然 40003 INVALID_TOKEN,逆向认证逻辑必须看 DevTools Network 里的真实请求头。更麻烦的是这些会话凭证还绑定 IP(cf_clearance / ds_session_id),把别人机器上抓到的请求拿过来复现,同样是 INVALID_TOKEN。平台接口的逆向,只能在真实浏览器里做。
4. 虚拟列表让 DOM 计数彻底失效。 一开始想直接从 DOM 里数对话轮次,但 DeepSeek 用的是虚拟列表(ds-virtual-list),DOM 里同一时刻只保留可见的两条消息,更早的被卸载了——从第二轮开始,DOM 计数永远返回 2。最后所有平台统一改走历史接口:message_id 是轮的稳定身份,后端返回什么就是什么。DOM 只留给 Gemini 当兜底(它没有可用的历史接口,只能滚动解析全部对话)。
5. Edge 商店的审核体验。 首次审核一天就通过了,但后来加了一个 YouTube 视频介绍链接,顺手把 Description 重写得详细了一些,代码一行没改,纯 metadata 更新——结果卡了七天还在审,Chrome Web Store 几小时就过。微软的扩展仓库里,停滞审核、Certification Loops、无通知下架是常态,我提的 issue 下面全是同病相怜的开发者。
一些反思
“不存对话文本”这个决策,回头看是整件事最关键的一步。 它不仅是隐私问题——token 估算的结果是数字,数字天然支持合并、同步、去重。跨设备同步能做成 union-merge 而不是文本合并,前提就是这个。
估算器的边界要想清楚。 用户问过为什么不直接用 WASM 版的真实 tokenizer——精度更高,但 WASM 二进制动辄上 MB,会破坏”轻量扩展”这个核心体验。线性模型在 70KB 的约束下做到 ±15%,是目前的最优解,以后模型换 tokenizer 了,重新跑一遍校准脚本就行。
项目目前在 Chrome、Edge、Firefox 三个商店都上架了,全部免费,没有账号,没有 API key:
- Chrome:Chrome Web Store
- Edge:Microsoft Edge Add-ons
- Firefox:Firefox Add-ons
代码在 GitHub。