先说我为什么要花一晚上拆它

我每天真正在烧钱的地方不是对话,是工具输出。一次 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 KB256 字符(自动索引,只回一句提示)0.10 s
抓一个 10.4 KB 的 MCP 规范页10.4 KB3,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_file482 字符
Grep建议改用 ctx_batch_execute589 字符
curl https://api.github.com/...直接 deny,命令被换成一句 echo660 字符

最后那行是重点。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 工具找回原文没丢,原文还在磁盘上,只是没进窗口
代价压缩本身要跑模型/规则,还可能压掉关键细节模型得会写代码;脚本写错就得到错答案
StarHeadroom 75.8K / Caveman 113.4K30.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*)"]}}

然后分三个入口测同一件事:

也就是说,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,迭代很猛,接口和配置文件都在动,锁版本比较稳。

我会怎么用它

会留,但用法很窄。

这一年上下文工具卷的方向基本是「怎么把 token 数压下去」,压到最后都在拼压缩率小数点。Context Mode 提醒了另一件事:有些数据压根不该进对话,该进的是那段处理它的代码。模型不该当数据处理器,它该当那个写处理程序的人。这个转向我认为是对的,它只是还没把「沙箱」这个词的含金量补齐——隔离是真没有,权限是真没延伸进去,98% 是真的但要在 MCP 活着的时候才算数。

项目地址:github.com/mksglu/context-mode(文档:context-mode.com,ELv2)