Codemode:把工具调用写成脚本,为什么删掉了 context-mode
前言
写插件那篇的时候,清单里有一个 context-mode,主打「省 98% 的上下文窗口」。它确实好用,用了大概两个月。
后来把 Pi 升到 1.1.0,又仔细读了一遍 codemode 的文档,才发现一直在为 context-mode 的核心功能付两遍成本:一份是它自己的,一份是 Pi 本体早就内置的。
这篇文章讲清楚三件事:
codemode到底是什么,它和「让模型跑一段代码」有什么本质区别;- 为什么它比
context-mode更彻底,能把「工具调用的中间结果」挡在上下文之外; - 删掉
context-mode之后,它原来那两块能力(沙箱执行 + FTS5 知识库)分别去哪儿了。
先回到那个具体场景
context-mode 最有用的就是这个场景。要统计一个 Qt 项目里 widget 目录有多少行 C++ 代码,但又不想让 .cpp / .h 的全文进上下文。它的做法是把一小段遍历目录、逐行计数的程序丢进沙箱跑,最后只回一行「142 个源文件、约 3 万行」的汇总。
这个思路是对的:中间结果不该进入模型的上下文,只有结论该。
问题在于,这套东西 Pi 本体现在就有了,而且做得更底层。
Codemode 是什么
codemode 不是第三方包,而是 Pi 的内建扩展(builtin:codemode)。它做的事只有一件:
让模型写一段 JavaScript,在沙箱里调用 Pi 的其他工具,只有这段脚本的输出回到模型。
开启方式是在 ~/.pi/agent/settings.json 的 defaultTools 里加一项 +codemode。本机的配置就是 +codemode 和 +ls 常驻,原来的 read / bash / edit / write 一个没丢。
它不是「执行代码」,是「编排工具」
这是 codemode 和普通代码沙箱最大的区别。脚本里没有 require、没有文件系统、没有网络、没有定时器,唯一通向外部世界的入口是「调用 Pi 的工具」。模型写的不是「一段能自己读文件的程序」,而是「一段指挥现有工具的脚本」。
这个区别听起来小,但决定了它能不能替代沙箱执行。context-mode 提供一个 Node 沙箱,让使用者自己写文件读取逻辑;codemode 直接把 read、bash、ffgrep、lens_diagnostics 这些已经存在的能力变成脚本里可以调用的函数。
回到刚才那个场景:如果要知道项目里哪些头文件声明了 Q_OBJECT,不需要把几十个头文件读进上下文。脚本可以并行发起几十次读取,就地判断每个文件里有没有这个宏,最后只返回一个「多少个 / 共多少」。模型最终看到的是一行文本——那几十次读取的内容从来没有进入上下文,因为它们本来就只是脚本里的局部变量。
失败的调用也不会污染上下文
工具调用失败时脚本会收到一个错误。多个调用可以各自独立,成功的部分照样保留,不会因为其中一个失败就全部作废。脚本本身失败时,也会保留失败前已经产生的那部分输出,后面跟一段错误说明。
代价是:在失败之前已经执行的工具调用是真实的、不会回滚。所以它适合读、查询、编排,不适合把写操作塞进一个可能中途出错的脚本。
为什么它比 context-mode 更彻底
context-mode 的沙箱执行和 codemode 的能力确实重叠,但成本结构完全不同。
| 维度 | context-mode | codemode |
|---|---|---|
| 来源 | 第三方 npm 包 + 独立 MCP server | Pi 内建扩展,零额外依赖 |
| 沙箱 | 它自己的 Node 沙箱 | QuickJS,无文件系统 / 网络 / 定时器 |
| 能力来源 | 沙箱内自己实现 | 直接复用 Pi 的工具集 |
| 工具注册 | 新增一组 ctx_* 工具 | 不新增工具名,脚本里动态查找 |
| MCP 集成 | 无 | 是 MCP 工具的默认暴露层 |
| 审计面 | 多一个第三方包 + 一个 server 进程 | 少一个包、少一个进程 |
真正促成删除的,是上下文税这一项。
context-mode 的沙箱执行、索引、检索是一组常驻工具。工具 schema 每轮都要发给模型,MCP server 还要占一个连接描述位。而 codemode 处理 MCP 工具的方式恰恰相反:默认把 MCP 工具延迟暴露,既不声明给模型,也不写进自己的描述。模型要用的时候,在脚本里按名字现场查找。
这台机器上的 MCP server 全部走这条路。结果就是:系统提示词里只有一段 server 摘要,一行一个 server;每个工具的完整 schema 一个都不占位。对比一下 context-mode 时期——原本是为了「省上下文」才装的包,结果它自己又往上下文里塞了一组工具。省下来的和加进去的,很大一部分互相抵消了。
codemode 的方向是反的——它不只不增加上下文,还是减少上下文的那一层。
顺带把 MCP 编排也解决了
延迟暴露还有第二个好处:一次脚本可以串起多个 server。比如查一个 Qt API 时,先让本地的代码图服务定位仓库里的调用点,再去云端文档库拉官方说明,最后把两边拼成一段结论返回。
两个 MCP server、多次外部调用,回到模型的只有两段截断后的文本。调用过程中传输的全部中间数据,都被脚本吃掉了。
顺带发现的两个能力
读文档时还注意到,codemode 还能跑非 LLM 模型——分类器和图像模型。这两个都走 session 已有的凭据,不需要额外配置。
分类器回答的是带类型的结构化问题:单选题、打分题、判断题,返回的是标签、概率和置信度,不是一段自由文本。比如把工具返回的一批反馈按情绪和紧急度排个序,几十条数据丢给分类器,主模型只看到一张排好序的表。它基本不占用主模型的上下文,也不消耗主模型的推理预算。需要注意的是分类器不抛异常,要自己检查它的结束原因和错误字段。
图像生成则是一条「不经过主模型、直接调用图像模型」的路径,产出可以直接展示。展示时它会顺便把图片存到临时文件并返回路径,方便后续回合复制或移动。对需要配图的任务挺有用。
store / load:脚本之间的小状态
codemode 提供了一对用来在多次脚本调用之间保存 JSON 小值的全局函数:一个存、一个取。适合放 ID、游标、摘要这一类小状态。
写入只在脚本成功时生效,并且会作为一条记录进 session——恢复会话、切换分支时,各自只看到自己路径上写过的值。要注意它是给「小状态」用的:单个值有大小上限,所有值合计也有上限,图片数据不要往这里塞。
那 context-mode 的 FTS5 知识库呢?
context-mode 的另一半能力是「把大段文档索引进本地全文索引,再按需检索」。这部分没有用 codemode 去复刻,因为它的 store 是给状态用的,不是全文索引。
这里的做法是把「检索」按对象拆给更专职的层:
| 要检索什么 | 现在用什么 |
|---|---|
| 仓库里的代码结构与符号 | codegraph MCP(一次调用返回相关符号的源码 + 调用路径) |
| 当前文件的符号 / 诊断 / 结构 | pi-lens(符号检索 / 诊断 / 语法结构规则) |
| 文件内容 / 文件名 | pi-fff(ffgrep / fffind) |
| 外部库文档 | context7 MCP |
| 跨会话的记忆和决策 | hindsight |
之前 context-mode 是一个「什么都能塞进去的全文索引桶」,现在每个桶只装一种东西。检索质量更高,而且没有一个桶需要额外维护索引。
诚实的边界
codemode 不是万能的,有几条限制需要接受:
- 只跑 JavaScript(QuickJS)。要跑 Python,就在脚本里调用
bash去执行解释器,而不是指望沙箱自己会。 - 没有定时器,也不能嵌套脚本。一个永远等不到结果的脚本会立刻失败。
- 单脚本内存有上限。要靠聚合和过滤,而不是把大文件累加在内存里。
- 输出不是无限的。超过阈值会保留首尾、把全文写进临时文件;脚本的总文本量也有上限。
- 失败前的工具调用不回滚。这是「真实调用」的代价。
这些限制都是可接受的——本来就不该在一个「帮模型省上下文」的沙箱里做重活。
迁移清单:删掉了什么
删掉 context-mode 之后,本机配置少了一个 npm 包、一个 MCP server、一组常驻工具名。具体动作是:
- 从
settings.json的packages里移除它; - 从
mcp.json里移除对应的 server 条目; - 把它的沙箱执行用法换成
codemode脚本(调用bash和read做遍历、并行读取和聚合); - 把它的索引 / 检索用法按上表拆给
codegraph/pi-lens/pi-fff/context7/hindsight。
~/.pi/agent/npm/package.json 的 allowScripts 里还留着一条它的历史记录,下次清理时删掉即可——它不影响实际加载。
结语
codemode 重新解释了「省上下文」这件事:真正的节省不是把大结果截断,而是从一开始就不让中间结果进入上下文。
context-mode 用「沙箱执行」实现了这一点,但代价是引入一个包、一个 server 和一组常驻工具。codemode 是同一个思路的内建版本,还顺手把 MCP 工具的暴露方式从「全部塞进系统提示词」改成了「脚本里按需查找」。
这两件事叠加起来,正好是想要的效果:内核更小,能力更多,而模型看到的上下文更少。
下一篇回来说 Hindsight——context-mode 走掉之后,跨会话记忆这一块是怎么接上的。