先说我为什么要花一晚上拆它
我每天真正在烧钱的地方不是对话,是工具输出。一次 grep 刷出 200 条匹配,一次 git log 带 300 个 commit,一次 Playwright 快照把整棵无障碍树 dump 出来——这些字节全都进了上下文窗口,而模型其实只需要其中三四行。
过去半年我测过两代解法。Headroom 的做法是在工具输出到达模型之前压一遍,代码搜索场景 17,765 token 压到 1,408;Caveman 更糙一点,代理层按内容类型压,400 行 JSON 从 31,446 压到 2,135。两个都属于「先让它进来,再想办法瘦身」。
Context Mode 换了前提:凭什么要进来? 它给模型一个 12 种语言的执行器,模型写脚本自己把数据分析完,只 console.log() 一个结果。数据从磁盘到答案,全程没经过上下文窗口。
我没先看功能表,先造了一份数据:20 万行、19.6 MB 的 nginx 访问日志,问它状态码分布和平均响应字节数。
19.6 MB 进沙箱,139 个字符出来
装完直接跑,返回长这样(这是模型实际看到的内容,一个字都没裁):
lines 200000 status {"200":100330,"301":24635,"404":25253,"429":25060,"500":24722} avg_bytes 4592
top path /api/orders
耗时 0.15 秒,五个状态码加起来正好 200,000,平均 4,592 字节我抽查过是对的。整个 MCP 响应 569 个字符,其中 430 个是它把我写的脚本原样回显了一遍——真正的答案只有 139 个字符。
再换三个场景,都记了数:
| 我让它干的事 | 原始数据 | 回到上下文的 | 耗时 |
|---|---|---|---|
| 20 万行访问日志 → 状态码分布 | 19.6 MB / 200,000 行 | 569 字符(含 430 字符脚本回显) | 0.15 s |
| 扫 1,434 个文件、16.3 万行,找最大的 5 个 | 整个 npm 包源码树 | 1,002 字符 | 0.12 s |
| 打印 5,000 行 stdout | 约 200 KB | 256 字符(自动索引,只回一句提示) | 0.10 s |
| 抓一个 10.4 KB 的 MCP 规范页 | 10.4 KB | 3,829 字符(带一段预览) | 0.40 s |
前三条的结果很硬:19.6 MB 的数据,模型一个字没读到,但答案是对的。第四条是它自己宣传里的破绽,后面单说。
装的这坨东西:npm 包 1.0.169,装完 54 MB,跑 context-mode doctor 六项 hook 全 PASS、FTS5 原生模块 PASS。11 个 MCP 工具,六个干活(ctx_execute、ctx_execute_file、ctx_batch_execute、ctx_index、ctx_search、ctx_fetch_and_index),五个管自己(stats、doctor、upgrade、purge、insight)。
它拿 SQLite 当缓存,而不是拿 LLM 当存储器
模型随时会忘,但磁盘不会。它每次会话把文件改动、git 操作、错误、用户决策写成事件,塞进项目目录下的 SQLite;上下文一压缩,它不把这些东西重新灌进 prompt,而是按当前任务做一次 BM25 检索,只捞相关的那几条。索引那边是 FTS5 虚表,两路召回——porter 词干("caching" 能命中 "cached")加 trigram 子串("useEff" 能命中 "useEffect")——再用 Reciprocal Rank Fusion 合并排序。
这套东西在信息检索里是标准答案,没什么花活。但它放在 agent 身上合理:会话记忆的检索需求就是「关键词模糊、要能容忍拼错、要能只回片段」,正好是 FTS5 的主场,不需要向量库、不需要 embedding API,一个 3 MB 的 SQLite 文件全搞定。
真正值得看的是 hook,不是工具
MCP 工具再好,模型不调用等于零。这个项目的重量级代码全在 hooks/ 里(5,119 行),逻辑是:在工具执行前拦一道,判断这次调用会不会灌爆上下文,会就改掉你的命令。
我把 hook 当纯函数直接喂进去,用真实 payload 测了六种调用:
| 模型想调的工具 | hook 返回 | 大小 |
|---|---|---|
pwd | 什么都不做,放行 | 0 字符 |
cat access.log | 注入一段建议,不拦 | 765 字符 ≈ 218 token |
npm test | 同上 | 765 字符 |
Read 大文件 | 建议改用 ctx_execute_file | 482 字符 |
Grep | 建议改用 ctx_batch_execute | 589 字符 |
curl https://api.github.com/... | 直接 deny,命令被换成一句 echo | 660 字符 |
最后那行是重点。curl 不是被劝,是被拒。返回的 JSON 是 permissionDecision: deny,理由里写清楚「要抓 URL 就调 ctx_execute,原始响应体留在沙箱里」,模型只能换个方式重试。这比在 system prompt 里写「请不要用 curl」硬得多,也更有效——指令是商量,hook 是物理拦截。
源码里还有一份一百多条正则的白名单,用来判断哪些命令「输出天生有界,别去烦模型」:pwd、git status、git log -20、ls(不许 -R)、cp/mv/rm(不许 -v,否则批量操作一行一个文件照样刷屏)。注释里写着理由:对 pwd 也弹一次「可能输出很大」的提示,是纯噪音,而噪音会训练模型忽略提示。这个观察是对的,我测出来一次提示 765 字符,一天弹五十次就是一万多字符的纯浪费。
但这道门有个开关,我把它关了
我按 README 的流程装上、验证 curl 被拦,然后顺手把 MCP 进程杀掉,再喂同一个 payload。
curl 直接放行,没有任何输出。
原因在 routing.mjs 第 28 行,短得让人无语:
function mcpRedirect(result, mcpToolsAvailable = true) {
if (!mcpToolsAvailable) return null;
if (!isMCPReady()) return null; // ← 这里
return result;
}
isMCPReady() 的判断方式是去 /tmp 里找一个 sentinel 文件 context-mode-mcp-ready-<PID>,文件 mtime 在 90 秒内就算 MCP 活着。MCP 进程一退出、sentinel 一过期,所有「重写命令」的拦截全部静默失效,退回成只发提示词。设计上这叫 fail-open——为了不让工具坏掉时把你的 agent 卡死,宁可放行。
合理,但要知道代价:这套东西的 98% 是有条件的 98%。MCP 崩了、插件升级到一半、sentinel 被 tmpwatch 清了,你的 agent 就会在毫无提示的情况下恢复成每 30 分钟烧掉半个窗口的那个版本,而且日志里一个字都不会说。
它跟压缩派不是一条路
这波工具有个共同敌人:上下文窗口。但解法分两派,取舍完全不同。
| 压缩派(Headroom / Caveman) | 重算派(Context Mode) | |
|---|---|---|
| 介入位置 | 工具输出之后、进模型之前 | 工具执行之前,把调用改道 |
| 做法 | 识别内容类型,压 JSON、压日志、AST 压代码 | 不传数据,让模型写代码在子进程里算出结论 |
| 能省多少 | JSON 类 60-95%,代码类 40-60%,文本 15-30% | 取决于问题,我这次是 19.6 MB → 110 字符 |
| 信息有没有丢 | 有损,靠 retrieve 工具找回原文 | 没丢,原文还在磁盘上,只是没进窗口 |
| 代价 | 压缩本身要跑模型/规则,还可能压掉关键细节 | 模型得会写代码;脚本写错就得到错答案 |
| Star | Headroom 75.8K / Caveman 113.4K | 30.7K(2,253 fork,356 open issue) |
两条路不是替代关系。压缩派对「一次性读一遍就够」的场景更省事——你不必为每个问题写脚本。重算派在「数据量 >> 答案量」时碾压:日志、CSV、API 响应、仓库统计,答案通常只有一行,那 99.9% 的字节凭什么要过一遍 tokenizer。
我的用法是两个都留着:查单个文件、看一个函数,走压缩;统计、扫描、跨文件聚合,一律让模型写脚本。判断标准就一句——如果你的问题能用 wc -l 回答,就别把文件读进来。
踩到的坑,按严重程度排
1. 这个「沙箱」不是沙箱
README 里反复出现 sandbox、isolated subprocess、complete isolation。我以为是容器或者 namespace 隔离,测了一下:
uid=1000(ubuntu) gid=1001(ubuntu) groups=...27(sudo)...
hostname: VM-0-16-ubuntu ← 和宿主一样
/etc/passwd 37 行,全读得到
sudo -n true → rc=0 ← 免密 sudo 直接过
它就是 spawn 出来的一个普通子进程,跑在你的 uid 下,网络直通,能 require('child_process'),能读任何你有权限读的文件。它隔离的只有「你看得见什么」,不是「它能做什么」。
这不是 bug,是产品定位——它要解决的是上下文污染,不是沙箱安全。但宣传语会让人误判风险。给模型开 ctx_execute,在权限意义上等于给它开了一个不记录输出、不看权限、还带网络和 shell 的后门。真要隔离,得自己在外面套容器或者降权跑。
2. 那条 deny 规则,只在 hook 那一层生效
README 的原话是:你配的 permissions.deny 会「extend 到 MCP sandbox」,「如果你封了 sudo,在 ctx_execute 里也封」。我照它给的示例配了一份:
{"permissions":{"deny":["Bash(sudo *)","Read(/etc/passwd)","Read(**/.env*)"]}}
然后分三个入口测同一件事:
- hook 拦
Bash: sudo apt install→ deny,拦住了。这块是真的。 - hook 拦
Read: /tmp/cmtest/.env→ 没拦,只回了一段「读文件建议用 ctx_execute_file」的建议。 ctx_execute里跑sudo -n true→ rc=0;读.env→ 明文返回,JS 和 shell 两种都一样。
也就是说,deny 规则只对「经过 hook 的 Bash 命令字符串」有效。一旦模型学会走 ctx_execute,所有文件级 deny、所有命令级 deny 全部作废——而这恰恰是它希望你走的路。这不是我故意绕,我用的是它自己文档里的配置和它自己推荐的调用方式。
3. 脚本回显吃掉了大半收益
19.6 MB → 569 字符听起来完美,但拆开看:430 字符是我自己写的脚本被原样贴回来,真正有用的 110 字符。ctx_execute 每次返回都带 ```javascript <your code> ```,脚本越长,这块越贵。写个 40 行的分析脚本,光回显就两三千字符——省下来的钱又还回去一部分。
4. CLI 的索引按目录分库,跨目录静默空手而归
我在 /tmp/cmtest 下 context-mode index big-doc.md,返回「Indexed 45 sections,DB: .../content/3adc7811....db」。换个目录执行 context-mode search "BM25 ranking",它开了另一个哈希的库,回答我:
No matches for: BM25 ranking
DB: .../content/e9671acd....db
没有报错,没有提示「你可能查错库了」,就是干净的 No matches——看输出会以为文档里没这个词。项目级分库本身合理(不同项目不该串),但至少该在零命中时提示一句当前库里有几个 source。
5. 它给自己算的账,水分不小
跑完第一次调用,ctx stats 给我看:
1.6M tokens saved · 100.0% reduction · ~$7.91 saved (Opus)
6.0 MB kept out of your conversation — 7102× longer sessions before compact
那次调用实际返回 891 字节。6.0 MB 是它「估算模型本来要读的原始数据量」,这个口径可以理解,但推论是错的:真实基线不是「把 1,434 个文件全读进上下文」,而是「跑三条 find + wc 然后把结果贴进来」。以「100.0% reduction」「7102× longer sessions」这种写法当卖点,跟 Caveman 首页那个后来被自己撤销的 65% 是同一个毛病。
6. 抓网页那条,跟宣传的 40 字节差两个数量级
10.4 KB 的 MCP 规范页,ctx_fetch_and_index 返回 3,829 字符,里面带了一整段页面正文预览。压缩率大概 63%,不是它表格里写的 60 KB → 40 B。抓完直接搜索的模式确实好用(原文进了 FTS5,后面可以反复查),但第一次调用的成本别按 40 字节算。
7. 许可证是 ELv2,不是开源
30.7K Star 的项目,LICENSE 是 Elastic License 2.0:可以用、可以改、可以分发,但不能拿它做托管服务,也不能去掉许可声明。作者自己说明了理由(防止被包一层做成闭源 SaaS 卖)。逻辑成立,但「30K Star 的开源项目」这个说法不准确,公司合规扫一遍会打回来。另外建仓 8 个月、356 个 open issue,迭代很猛,接口和配置文件都在动,锁版本比较稳。
我会怎么用它
会留,但用法很窄。
- 当只读分析器用。日志统计、CSV 聚合、API 响应里挖字段、仓库代码统计——这四类我以后都走
ctx_execute,一是我实测快(0.1 秒级),二是这些场景答案只有一行,压缩派再怎么压也压不到 110 字符。 - 不给高权限环境用。它拦不住 sudo、拦不住读
.env。要在生产环境给 agent 开这个,先把它塞进容器、用非 root 跑、网络出口收紧,别指望它自带的 deny 规则。 - 长会话才装 hook 版。只用 MCP 版(
npx -y context-mode)省 60%,装全插件省 98%——差的这 38% 全靠 hook 拦截。但 hook 依赖 sentinel,MCP 一停就静默降级,所以别把它当唯一保险。 - 别指望它替你想。它给的是执行器,不是分析能力。脚本写错、聚合口径错,它会非常自信地把错答案用 200 字节返回给你。这一点比压缩派更危险:压缩至少保留了原文可查,重算是把原文扔了,只留结论。
这一年上下文工具卷的方向基本是「怎么把 token 数压下去」,压到最后都在拼压缩率小数点。Context Mode 提醒了另一件事:有些数据压根不该进对话,该进的是那段处理它的代码。模型不该当数据处理器,它该当那个写处理程序的人。这个转向我认为是对的,它只是还没把「沙箱」这个词的含金量补齐——隔离是真没有,权限是真没延伸进去,98% 是真的但要在 MCP 活着的时候才算数。
项目地址:github.com/mksglu/context-mode(文档:context-mode.com,ELv2)