Armin Ronacher 博客 · 原文 2026-10-06 · 译文 2026-10-24 · 难度 进阶
精读 · 译文(非本站原创)
来自资源池 · Armin Ronacher 博客 ↗ · 原文:What is Codemode ↗ · 版权归 Armin Ronacher 博客 所有 · 译文为个人学习的非官方翻译,以原文为准;如权利方有疑虑可联系公众号「疯了再说」删除
导读 · Codemode=模型写 JavaScript 在 harness 侧沙箱(QuickJS/WASM)内编排工具调用——Pi 1.0 的组合层,也是 MCP 的落地方式

为什么精读这篇Pi 1.0 架构最深的公开切片:脑(harness,可信)手(执行环境,沙箱)分离模型、QuickJS-in-WASM 沙箱边界(无网络/无文件/无定时器/限内存)、并发工具上限 4+排队、store() 跨调用持久化、内部模型 API(生图/Jev 分类器)只经 codemode 暴露省上下文。最重要的一手澄清:**Pi 不把 MCP 工具直接暴露给模型**——agent 在 codemode 里做 tool search 渐进发现,system prompt 只声明有哪些 server——「无 MCP」的旧叙事由此更新为「MCP 不占上下文」。对理解「极简内核上长出组合层」与工具调用的上下文经济学,这是必读一手。

  • 语境:Armin 一年前写《Code Is All You Need》《MCP needs code》劝人别往上下文塞工具/MCP、多用脚本;Pi 1.0 经 Codemode 加入 MCP 支持——「Is this a reversal of what I wrote a year ago where I encouraged CLIs? I don't think so.」(MCP 生态恰好吸收了「代码才是正解」)
  • 脑手分离(Brains vs Hands):脑=harness(可信、跑在一台机器上),手=执行环境(工具真正执行的地方,Pi 的叫法);分界线意味着不同文件系统、不同信任级——用 Gondolin 沙箱时 bash 被沙箱包住,harness 本身不在内
  • Codemode 定义:LLM 写 JavaScript 在 **harness 侧**发起并编排工具调用,跑在自己的沙箱里——Pi 用 **QuickJS in WASM**,故意限制:无网络、无文件系统、无定时器、内存受限——「The only way is to call more tools.」;命名归功 Cloudflare
  • 上下文经济学:bash 常规工具调用只把末尾 2000 行放进上下文(超了看 overflow 文件);走 codemode 则结构化拿到更大输出;JS 能表达并发与工作流——常见模式=先探测 5-10 项看长什么样,再写脚本处理剩余;store() 把数据扔进 transcript,同 session 下次调用可读回(注意:存在 harness 主机,不在沙箱)
  • Pi 侧实现细节:并发工具执行上限 4、其余排队(文中 Promise.all 由此安全);image() 把图像作为 image content 注回 LLM 并落盘临时工件;**内部模型 API 只经 codemode 暴露**(models.generateImages 生图、models.classify 调 Jev 分类器)——做常规工具会白白占上下文;默认仅 MCP 启用时开 codemode,也可在设置里给 defaultTools 加 +codemode 强制打开
  • MCP 的正确姿势:**不给模型暴露任何 MCP 工具**——agent 在 codemode 里先 tool search 渐进发现能干什么;system prompt 只声明有哪些 server(不算瞎猜);Sentry 示例里 agent 直呼 mcp__sentry__find_organizations,因为 RL 阶段已学过 Sentry MCP 长什么样
  • 现代 MCP 之痛:codemode-in-codemode 嵌套(Cloudflare 把 codemode 做进 MCP server)=双重 JSON 转义、小模型易晕、内层调不了外层工具;四条改进建议=结构化返回(outputSchema)/结果形状一致(按结果集大小做 token 优化会让探测成功、满批失败)/大二进制支持(现在靠预签名 URL 绕)/可组合的 tool search(跨多 server fan-out 无机制)
  • 未来:durable 是难点(或借鉴 durable workflow 引擎做调用快照);Starlark 的确定性或比 JS 更适合作组合语言;小模型驾驭不了这个模式——「it's quite a useful pattern that I expect us to leverage more」
25 段 · 约 15 分钟

More than a year ago I wrote a few posts here that recommended people not to load custom tools into their context (or MCP servers) but to just use more scripts. Most importantly I wrote that Code Is All You Need and I wrote about that MCP needs code. With Pi 1.0 we now added MCP support via Codemode which in some ways is a long time coming, but then also maybe somewhat surprising to some. So I want to share some updated thoughts on this blog on what this all means.

一年多前我在这里写过几篇,劝大家别把自定义工具(或 MCP server)装进上下文,直接多用脚本就好。最重要的两篇是《Code Is All You Need》和《MCP needs code》。现在 Pi 1.0 经由 Codemode 加入了 MCP 支持——某种意义上这是姗姗来迟,但对某些人来说也许有点意外。所以我想在这篇里更新一下我对这一切的看法。

What Are Tools · 工具是什么

When a harness like Pi provides tools for an LLM to call, it does so by supplying some tool definitions which then translate into some token structure on the server side. Whether a model is encouraged to call a tool is the result of the reinforcement learning process. Something I wrote about before if you want to learn more.

当 Pi 这样的 harness 给 LLM 提供工具时,它提供的是一些工具定义,然后在服务端翻译成某种 token 结构。模型是否倾向调用某个工具,是强化学习过程的结果——细节我之前写过(《Better Models: Worse Tools》)。

One of the reasons we strongly lean towards CLI and bash is because it allows easy composition of calls, and because the model also learns how the file system works when it's trained. So when it invokes a tool like `echo foo > /tmp/test.txt` the model also learns that after that tool call, there is now a file called `test.txt` in `/tmp`.

我们强烈偏向 CLI 和 bash 的原因之一,是它让调用组合变得容易;而且模型在训练时也学会了文件系统怎么运作——当它调用 `echo foo > /tmp/test.txt` 这样的工具时,它同时学到:这条调用之后,`/tmp` 里会多出一个叫 `test.txt` 的文件。

However bash has one fundamental limitation which is that it can only compose programs that run. And there are some things, which are not programs, but native tools to the LLM and they sort of have to be.

但 bash 有一个根本局限:它只能组合「能跑的程序」。有些东西不是程序,而是 LLM 的原生工具——而且多多少少必须如此。

The most obvious example here is `read` or `view_image`. If a multimodal model needs to read an image, it cannot use `cat` for that because the harness needs to inject the actual image payload into the protocol of the LLM.

最明显的例子是 `read` 或 `view_image`:多模态模型要读一张图时,没法用 `cat`——因为 harness 必须把真实的图像载荷注入 LLM 的协议里。

Another quite vivid example are sub agents. In order to spawn and orchestrate sub agents, it's tricky to avoid tools that are provided by the harness. While in theory the agent could provide a CLI tool that talks to the outer harness via environment variables and Unix sockets, it's a rather crude process. It however has another issue, and that is where the code runs.

另一个鲜活的例子是 sub agent。要生成并编排 sub agent,很难绕开 harness 提供的工具。理论上 agent 可以提供一个 CLI 工具、靠环境变量和 Unix socket 跟外层 harness 通信,但那是个相当粗糙的过程。而且它还有另一个问题——代码到底跑在哪。

Brains vs Hands · 脑与手

To better understand that, it's important to think a bit more about where all the bits and pieces run. There really usually are two different systems involved. The first is the brain, the harness: it runs on one machine. It's trusted. The second is *often* the same machine, but it's really where the tools are executing: the hands. In Pi we now call this the execution environment, but you can think of it as the target of all the operations.

要理解这一点,得先想清楚各个零件都跑在哪。通常涉及两个不同的系统:第一个是脑,也就是 harness——它跑在一台机器上,是可信的。第二个*经常*是同一台机器,但它的本质是工具执行的地方:手。Pi 现在称之为执行环境(execution environment),你可以把它理解为所有操作的目标。

Crucially what is important for us, is that there is a dividing line between the harness brain and the target environment that runs bash and executes the tools.

对我们来说关键在于:harness 之脑与「跑 bash、执行工具」的目标环境之间,存在一条分界线。

And splitting this in half has some really important consequences. For a start it means that they are running on different file systems and they have different levels of trust. If you for instance use a sandboxing solution like Gondolin your bash stuff will be sandboxed just fine, but the harness itself will not be.

把这两半劈开有一些非常重要的后果。首先,它们跑在不同的文件系统上,信任级别不同。比如你用 Gondolin 这样的沙箱方案,你的 bash 会被沙箱包得很好——但 harness 本身不在沙箱里。

Orchestrating The Harness · 编排 harness

Which brings us to what Codemode really does: it's a way for the LLM to express and orchestrate complex operations on the harness side, but not the execution environment side. Codemode runs in the harness, in its own sandbox. In case of Pi it's running in QuickJS within a WASM runtime with intentional limitations: no network, no file system, no timers, limited RAM. The only way is to call more tools. You could also imagine that Codemode could run Scheme or some other language as well.

这就引出了 Codemode 真正做的事:它是 LLM 在 **harness 侧**(而非执行环境侧)表达并编排复杂操作的方式。Codemode 跑在 harness 里、跑在它自己的沙箱里。在 Pi 里,它跑在 WASM 运行时内的 QuickJS 中,带着故意的限制:无网络、无文件系统、无定时器、内存受限。唯一的出路是调用更多工具。你也可以想象 Codemode 跑 Scheme 或别的语言。

If you are not familiar with Codemode, it's basically just a way to issue tool calls from within some language, in our case JavaScript. That allows you to compose those calls without necessarily going through the LLM's context. Credit for naming goes to our friends at Cloudflare who coined it.

如果你不熟悉 Codemode:它本质上就是「在某种语言里发起工具调用」的方式——在我们的场景里是 JavaScript。这让你可以组合这些调用,而不必经过 LLM 的上下文。命名的功劳归 Cloudflare 的朋友们。

For instance if you issue a bash call as a regular tool call in the LLM, then we only throw the trailing 2000 lines into the context and if the agent wants more, it needs to look at the overflow file itself. If however the agent issues that invocation via Codemode, then the Codemode side gets larger outputs sent structurally.

举例来说:如果 agent 用常规工具调用发起 bash,我们只把末尾 2000 行放进上下文,agent 想看更多就得自己去翻 overflow 文件。但如果 agent 经由 Codemode 发起这次调用,Codemode 侧就能结构化地拿到更大的输出。

Most importantly, because Codemode is JavaScript the agent can express concurrent operations and basic workflows. A common way in which you see agents now use this, is to first probe at 5-10 items from some tool response to see what it looks like, and to then write a Codemode script that processes the next n items.

最重要的是:因为 Codemode 是 JavaScript,agent 可以表达并发操作和基础工作流。如今常见的用法是:先从工具响应里探测 5-10 项看看长什么样,然后写一个 Codemode 脚本去处理接下来的 n 项。

Codemode also allows you to throw state into the transcript! That means that one Codemode invocation can stash away data, that the next call in the session can load again. And remember: this is on the harness host, not the sandbox.

Codemode 还允许你把状态扔进 transcript!也就是说,一次 Codemode 调用可以把数据存起来,同 session 里的下一次调用可以再读回来。记住:这存在 harness 主机上,不在沙箱里。

In case of Pi, Codemode also allows you to issue calls that naturally do not make any sense in Pi's traditional interface. For instance if you want to generate images with an image model or you want to classify some text with a one shot classifier model, those Pi APIs are exposed via Codemode, but not via regular tools where they would just waste context.

在 Pi 里,Codemode 还能发起那些在传统接口下「天然说不通」的调用。比如用图像模型生图、或用一次性分类器模型给文本分类——这些 Pi API 经由 Codemode 暴露,而不做成常规工具(做成常规工具只会白白浪费上下文)。

Note that Codemode is by default only enabled in Pi when MCP is enabled, but you can turn it on with `"defaultTools": ["+codemode"]` in the settings. Just ask Pi to enable it for you.

注意:Pi 里 Codemode 默认只在启用 MCP 时开启;你也可以在设置里用 `"defaultTools": ["+codemode"]` 打开——直接让 Pi 帮你开就行。

Generating Images · 生成图像

Let's start simple with image generation. Image generation is a feature that Pi supports in the AI SDK core, but it's not a tool that the agent can use. In the past the only way to use image models has been to write a bespoke extension or to have the agent run node itself and use the internal image APIs. However because we expose quite a few of the internal model APIs within Codemode, it means that the agent can use it:

从简单的开始:生图。图像生成是 Pi 在 AI SDK 核心里支持的功能,但它不是 agent 能用的工具。过去用图像模型的唯一办法是写定制扩展,或让 agent 自己跑 node 调内部图像 API。而现在我们在 Codemode 里暴露了相当多内部模型 API,agent 就能直接用:

const [painter] = await models.getAvailableOfType("image");
const result = await models.generateImages(painter, {
  input: [{ type: "text", text: "A cute little puppy sitting on a grassy " +
    "lawn, soft natural light, photorealistic" }],
});
if (result.stopReason !== "stop") return result.errorMessage;

for (const block of result.output) {
  if (block.type === "image") image(block);
  else text(block.text);
}

Note that the call to `image()` sends the image back as image content to the LLM. On the harness side it feeds it directly into both the agent, as well as onto disk as a temporary artifact in case the agent wants to be able to pass that image back to bash.

注意 `image()` 这个调用会把图像作为 image content 发回给 LLM。在 harness 侧,它一边直接喂给 agent,一边落盘成临时工件——万一 agent 想把这张图传回给 bash 用。

Calling MCP Servers · 调用 MCP server

Lastly, Codemode obviously is great for calling MCP servers. And because we do not actually expose any of the MCP tools to the LLM, the agent first uses provided APIs to issue a tool search within Codemode to discover what it might be able to do with the connected servers. This form of progressive discovery makes the whole MCP business work well enough for a lot of use cases today.

最后,Codemode 显然很适合调 MCP server。而且因为我们**并不把任何 MCP 工具暴露给 LLM**,agent 会先用提供的 API 在 Codemode 里发起 tool search,探索连上的 server 能干什么。这种渐进发现的形式,让 MCP 这摊事在今天对很多用例都够好用了。

Here for instance you can see the agent reach for the Sentry MCP straight away, even without discovering the tools, presumably because it has learned during the RL process already about what the Sentry MCP looks like. But it learns from what we inject into the system prompt, that the Sentry server is available to begin with. It's not completely guessing here.

比如下面这个例子里,agent 没做发现就直接伸手够 Sentry MCP——多半是因为它在 RL 过程中早就学过 Sentry MCP 长什么样。但它「知道 Sentry server 存在」这件事,来自我们注入 system prompt 的声明。它并不是在纯瞎猜。

const orgs = await tools.mcp__sentry__find_organizations({});
const { organizations } = orgs.structuredContent;
const results = await Promise.allSettled(organizations.map(org =>
  tools.mcp__sentry__find_projects({
    organizationSlug: org.slug,
    regionUrl: org.regionUrl,
  })
));
return organizations.map((org, i) => {
  const r = results[i];
  if (r.status !== "fulfilled") return { org: org.slug, error: String(r.reason) };
  if (r.value.isError) return { org: org.slug, error: r.value.content };
  return {
    org: org.slug,
    projects: r.value.structuredContent.projects.map(p => p.slug),
  };
});

Modern MCP Is A Fight · 现代 MCP 是一场搏斗

I really don't want to talk too much about MCP here, but MCP is in fact a protocol that greatly benefits from Codemode. The problem in parts is that MCP in practice often targets harnesses that do not (yet?) use Codemode. But the tide is shifting. In the meantime, a temporary crutch has been to do what Cloudflare did, and do Codemode within the MCP server. But now we have Codemode in Codemode which is pretty bad. It means double JSON escaping, easy for smaller models to get confused by and the inner code cannot call the outer tools.

这里我不想多谈 MCP,但 MCP 确实是一个能从 Codemode 获益巨大的协议。问题部分在于:MCP 实践中往往面向「还没用上 Codemode 的 harness」。风向在变。在过渡期,一个临时拐杖是学 Cloudflare——把 Codemode 做进 MCP server 里。但这就成了 Codemode 套 Codemode,相当糟糕:双重 JSON 转义、小模型很容易被绕晕、而且内层代码调不了外层工具。

MCP Desires · MCP 的愿望清单

So to end things off: how well does Codemode work with MCP today? Well … not amazingly well. That's because MCP servers are not really targeting harnesses that use Codemode yet (though at this point I think most harnesses support it).

收个尾:Codemode 今天跟 MCP 配合得怎么样?嗯……没那么惊艳。因为 MCP server 还不是为「用 Codemode 的 harness」设计的(虽然我觉得此刻大多数 harness 都已支持)。

For this to work well some recommendations: **Structured content:** Codemode wants calls to return some nicely formatted JSON. So that needs to come back from the server, and many don't do that yet. The `outputSchema` system in MCP is great for that. **Consistent results:** an interesting failure case is when an MCP server does not return consistent data. For instance because it tries to token optimize things depending on how many items are in the result set. This can cause an initial probe with 5 items to succeed, but then fail when the server returns the maximum batch size. **Large binary data:** today MCP does not yet support large binary data so quite a few use cases that are really interesting do not work well at all yet. You end up with all kinds of weird workarounds such as pre-signed URLs to allow file uploads then to happen through non MCP channels. **Composable tool search:** the MCP server might know better than the MCP client which tool is appropriate for a task. But there is no good mechanism today that allows a harness to fan out tool searches across multiple MCP servers. It's all emergent behavior and it does not scale well to multiple active servers.

要配合好,有几条建议。**结构化内容**:Codemode 希望调用返回整洁的 JSON——这得由 server 返回,很多还没做;MCP 的 `outputSchema` 机制正合适。**结果形状一致**:一个有趣的失败模式是 server 返回的数据不有一致性——比如按结果集大小做 token 优化,结果探测 5 项时成功、返回最大批量时失败。**大二进制数据**:MCP 至今不支持大二进制,很多真正有趣的用例根本跑不好,只能靠预签名 URL 这类怪招让上传绕开 MCP 通道。**可组合的 tool search**:哪个工具适合哪个任务,MCP server 可能比 client 更清楚——但今天没有好机制让 harness 把 tool search 扇出到多个 server;全靠涌现行为,多 server 活跃时扩展性很差。

Future of Codemode · Codemode 的未来

So where does this leave us? Is this a reversal of what I wrote a year ago where I encouraged CLIs? I don't think so. In fact, the MCP ecosystem from my perspective picked up on exactly what we pointed out a year ago works: code. But Codemode goes beyond MCP in that it can act as a capable mechanism within the harness to express more freedom for the agent.

那么我们身处何地?这是对我一年前鼓励 CLI 的立场反悔吗?我不这么认为。事实上,在我看来 MCP 生态恰恰吸收了我们一年前指出的有效的东西:代码。而 Codemode 走得比 MCP 更远——它作为 harness 内的一套有力机制,为 agent 表达了更多自由。

There are however also some things that we still need to figure out. For one, durability with Codemode is trickier. We might have to adopt some ideas from durable workflow engines here to snapshot invocations. Or maybe, something like Starlark is a better composition language than JavaScript given its deterministic nature. Images, binary data and just the inability of this pattern to work with smaller models is also something that needs to be fleshed out. So it's for sure not a perfect solution yet, but it's quite a useful pattern that I expect us to leverage more.

当然还有些待解的问题。其一,Codemode 的 durable(持久化)更棘手——也许得借鉴 durable workflow 引擎的思路给调用做快照。又或者,鉴于其确定性,Starlark 之类也许比 JavaScript 更适合作组合语言。图像、二进制数据、以及这个模式对小模型的无能为力,也都待补。所以它肯定还不是完美方案,但它是一个相当有用的模式,我预计我们会用得越来越多。

回忆挑战

先不看答案自己回忆——讲不出来再回正文找。考的是关系,不是标签。

1「脑手分离」为什么是 Codemode 的前提?如果组合逻辑跑在执行环境侧会有什么问题?

脑(harness)可信且掌管会话真相,手(执行环境)不可信且被沙箱包裹。Codemode 的编排代码要读会话状态、调度工具、跨调用持久化——这些都必须在可信侧完成;若跑在手侧(比如让 bash 里的脚本编排),编排代码本身就在不可信环境里,既拿不到 harness 的内部 API(生图/分类器/store),也会被沙箱限制(无文件无网络),更谈不上把大输出结构化地留在 harness 侧不占上下文。QuickJS/WASM 沙箱(无网络/无文件/无定时器/限内存)保证了「LLM 写的代码无论干什么都只能通过调工具影响世界」——逃逸面被压到只剩工具调用。

2「Pi 不把 MCP 工具暴露给模型」与「Pi 支持 MCP」如何同时成立?这个设计解决了什么、代价是什么?

同时成立的方式:MCP 工具存在但只活在 codemode 里——system prompt 只声明「有哪些 server」(几十 token),agent 需要时在 codemode 里 tool search 渐进发现具体工具,再以代码方式调用。解决的是定调文里那笔账:Playwright MCP 21 工具 13.7k token、Chrome DevTools 26 工具 18k token 常驻上下文的问题(现在只花「声明存在」的钱)。代价:模型无法直接「看见」工具签名,得先 search 再用(多一步);且依赖模型在 RL 中见过常见 MCP(Sentry 示例里 agent 零发现直呼工具,是因为训练先验);小模型驾驭不了这个模式——这是 Armin 自己列的未解问题。

3为什么说「codemode-in-codemode」(Cloudflare 式 MCP server 内嵌 codemode)是个坏味道?它暴露了生态的什么错位?

Pi 的 agent 写 JS→经 codemode 调 Cloudflare MCP 的 execute 工具→该工具里又是一段 JS 字符串→在 Cloudflare 侧再执行一次:双重 JSON 转义(代码作为字符串嵌在参数里)、小模型极易被转义地狱搞晕、内层代码无法调用外层(Pi 侧)的工具。暴露的错位:MCP server 生态在为「不用 codemode 的 harness」设计,于是把组合层塞进了 server 侧;正确的分工是 server 返回结构化 JSON(用好 outputSchema)、组合发生在 harness 的 codemode 里。这是「协议层与组合层职责划分」尚无共识的症状。