Skip to content
Duang

Code Mode 深度解读:Agent 的工具目录正在坍缩成一个 exec()

Author Duang Filed under Digest· 18 min read

2026-11-03 · 最新速递

本文挂在 最新速递。原文发布于 2026-11-01。

开篇:这篇文章为什么值得读

这篇文章解读的是 Philipp Schmid 在 2026 年 11 月 1 日发布的短文《Why Agents Are Soon Only Going to Have 1 Tool》。它讨论的是过去两年 Agent 工具调用范式的一次反转:构建一个能力强大的 Agent,不再等于设计一堆工具、琢磨每个 schema 该多细,而是把整个工具目录坍缩成一个 exec(code) 工具,让模型在隔离沙箱里写一段脚本,一次性串联、组合、等待多个动作。这个范式叫 Code Mode,也就是程序化工具调用(Programmatic Tool Calling)。

这篇文章写得好的地方在于,它没有停留在概念层:为什么现在到了临界点、内部怎么工作、为什么选 JS/Python 沙箱而不是 bash、业界已经怎么用,一层层都讲透了,最后还附了一个可运行的最小实现。对做 Agent harness、Agent 应用开发的人来说,这是一篇可以直接拿来当决策参考的短文,值得推荐。

核心命题:把几十个工具坍缩成一个 exec(code)

过去两年构建 Agent 的主流做法是:为模型设计一批工具,想清楚哪些动作能帮它完成任务、每个工具参数 schema 该多细、系统指令模板怎么迭代。标准工具调用的形态是模型生成一个结构化对象(通常是 JSON),定义一次动作,等原始输出返回,把结果塞回提示历史,再推理下一步。即便后来用工具检索(tool search)和渐进式披露(progressive disclosure)解决了大目录放不进上下文的问题,工具数量从几个扩展到几十个 MCP 服务器,仍然受制于同一个瓶颈:一个回合一个动作。

Code Mode 把方向反过来。harness 不再把八十个 JSON Schema 工具对象一次性喂给模型 API,而是把模型表面的工具收成一个唯一的 exec(code)。模型写一段简短的 TypeScript 或 Python 脚本,在隔离沙箱里调用、过滤、组合多个动作;沙箱执行完所有底层调用,只在最后把一小段输出(例如一行 text() 摘要)送回模型上下文。

这里的关键不是工具数量变少,而是把推理与编排从模型上下文窗口里拿出来、放进可执行代码里。标准工具调用中,每跨一步都要经历一次完整的模型往返,中间结果全部堆在上下文里;Code Mode 中,链式依赖在脚本内部用 await 一次走完,中间载荷不出沙箱。模型的注意力被省下来,用在真正需要判断的地方。

链式依赖进了脚本,上下文里就只剩下要判断的那一行。

演化路径:为什么现在到了临界点

从少量工具、到几十个 MCP 服务器、再到工具检索与渐进式披露,最后收敛到 Code Mode,有几个因素在近期同时到位。

  • 模型的代码能力。前沿模型被大量代码训练,写脚本、做过滤、处理异步比十年前可靠得多;部分厂商已经开始针对 Code Mode 做专门的 post-training。
  • MCP 的 outputSchema。MCP 服务器越来越多地在 inputSchema 之外提供 outputSchema(2025 年 6 月 18 日的协议修订加入,配合调用结果里的结构化内容 structuredContent)。模型在运行前就知道工具 A 的返回类型,可以在同一段脚本里把 A 的结果直接接给 B,而不必等运行时输出。
  • 串联能力的跃升。作者基于自己的实践观察到一个明显跳变:模型在单次 pass 里可靠地组合、串联依赖动作的能力,最近进步很快。

作者列了四类典型收益,都是标准工具调用做起来很别扭的事情:

  • 调研与过滤。不用第一轮抓来源、第二轮逐篇读、第三轮交叉比对;一段脚本里查询来源、过滤无关结果、提取答案一次完成。
  • 多文件编辑与内联验证。标准编辑工具一次调用通常只改一个文件;Code Mode 可以跨多个文件做协调修改,跑验证,看输出,再在脚本里按通过或失败分支。
  • 浏览器与电脑操作。点击、填表、等待选择器、翻页循环,这类动作链条往往很明确,不必为每次点击付一次完整模型往返,脚本一次把序列或循环跑完,只在页面状态变化时返回。
  • 并行扇出与子代理。跑二十次查询、并行调用多个子模型、再把结果喂给第二个 API:脚本里 await Promise.all(...),转换数组,直接调下一个函数,不用在步骤之间反复往返上下文窗口。

四类收益

调研与过滤

查询来源、滤掉无关结果、抽出答案,放在同一段脚本里。

多文件编辑

跨文件改完,跑验证,再按通过或失败往下走。

浏览器与电脑操作

点击、填表、等待、翻页一次跑完。页面状态变了再返回。

并行扇出

Promise.all 跑完一批查询或子模型,转换数组,直接调下一个函数。

Code Mode 的工作原理

模型表面只有一个 exec(code)。harness 把几十个工具 schema 编译成紧凑的 TypeScript 类型声明,模型需要跨系统组合动作时,就调用 exec 写一段脚本,通过 await tools.<server>.<tool>(args) 使用这些类型化接口。

// 把几十个 JSON Schema 工具编译成一组紧凑的类型声明
declare namespace tools {
  namespace linear {
    function listProjects(): Promise<Array<{ id: string; name: string }>>;
    function listIssues(args: { projectId: string }): Promise<Array<{ id: string; title: string; assignee?: string }>>;
    function archiveIssue(args: { id: string }): Promise<{ ok: boolean }>;
  }
}

// 多模态输出助手:只有显式传进这里的,才会进入模型上下文窗口
declare function text(output: string): void;
declare function image(data: string | Uint8Array, mimeType?: string): void;
declare function audio(data: string | Uint8Array, mimeType?: string): void;

// 跨轮状态助手:持久化在宿主内存里,不占上下文 token
declare function store(key: string, value: unknown): void;
declare function load<T = unknown>(key: string): T;

面对上百个工具时,不必全量展开命名空间:可以沿用工具检索的思路,或在沙箱里暴露一个全局 ALL_TOOLS 映射表,让模型用代码去遍历、过滤。多模态输出助手 text() / image() / audio() 决定哪些内容真正进入模型上下文;跨轮状态助手 store() / load() 让 Agent 把状态放在上下文之外,跨轮复用大结果不烧 token。

// 模型在单次 exec() 调用里发出的脚本
const projects = await tools.linear.listProjects();
const issueBatches = await Promise.all(
  projects.map((p) => tools.linear.listIssues({ projectId: p.id }))
);
const unassigned = issueBatches.flat().filter((issue) => !issue.assignee);
await Promise.all(unassigned.map((issue) => tools.linear.archiveIssue({ id: issue.id })));
text(`Archived ${unassigned.length} unassigned issues across ${projects.length} projects.`);

这段脚本在沙箱里执行了 55 次底层 API 调用,在内存里过滤载荷,最终只把一行 text() 字符串送回模型上下文:Archived 42 unassigned issues across 12 projects.。整个过程的架构分三层:模型面只看到类型声明与 exec(code);沙箱是内存里的 QuickJS / V8 隔离环境,无文件系统、无网络、无环境变量;宿主代理负责凭据注入、schema 校验与用户确认,并向 MCP 服务器、外部 API、子模型与内部 SDK 分发调用。

三层执行

模型面

只看到紧凑的类型声明,和唯一的 exec(code)。read / edit 这类单步操作留作兜底。

内存沙箱

QuickJS 或 V8 隔离环境。没有文件系统、网络和环境变量。store()、load()、text()、image() 在这里。

宿主代理

凭据注入、schema 校验、破坏性调用前的确认。再分发到 MCP、外部 API、子模型和内部 SDK。

中间载荷只留在沙箱。只有 text()、image()、audio() 显式传出的内容进入模型上下文。

标准工具调用与 Code Mode 的差异,可以对照下面这张表来看。

标准调用对上 Code Mode

动作粒度

标准调用一轮一个动作,至多一批互不依赖的并行调用。脚本里可以串联、循环、分支、并行。

中间状态

标准调用每一步的原始输出都进上下文。Code Mode 只留在沙箱内存,store() / load() 可以跨轮接着用。

依赖链

标准调用每跨一步都要一次模型往返。脚本里 await 一次走完。

多模态

shell 只有文本,看图要另挂 view 工具。image() / audio() 把媒体直接附给下一轮。

上下文成本

结果越大,标准调用每轮都背着它。Code Mode 只回 text() 摘要。

为什么是 JS/Python 沙箱,而不是 bash

bash 也能搭工具代理,但把工具编排放进 JavaScript 或 Python 这类语言运行时,有几个 bash 环境里很难做到的收益。

为什么用 JS / Python

类型化函数

MCP 和内部 API 直接映射成 await tools.service.method()。用对象、Promise.all、try/catch 处理错误。

免掉 CLI 探索

bash 里模型容易先跑 --help、读脚本源码。类型声明把契约放在前面,模型可以直接行动。

多模态上下文

shell 只回 stdout。沙箱里调用 image(screenshot) 或 audio(clip),宿主把真实媒体附到下一轮。

凭据与策略

每次 await tools.* 都跨到宿主:OAuth 在沙箱外注入,schema 先校验,破坏性调用前可以暂停确认。

状态放在上下文外

提示历史不再是唯一的状态。大查询结果用 KV 留着,下一轮不必再背一遍原始 JSON。

一个最小实现:Gemini Interactions API 示例

文章给了一个可运行的最小示例(原文标注为伪代码性质):给 gemini-4-argon 只挂一个 exec 工具,用 node:vm 的 runInNewContext 在独立上下文里执行模型生成的脚本。流程是这样的:模型第一轮直接生成一段脚本,取 PR #42 里改动超过 50 行的 .ts 文件,跑安全扫描,有发现就回 PR 评论,最后用 text() 输出一行摘要;harness 在 vm 里执行脚本;第二轮只把那一行摘要作为 function_result 传回去。中间的原始文件对象和扫描载荷从不进入模型上下文。

// 1. 只暴露一个 exec 工具,描述里直接带上 .d.ts 签名
const execTool = {
  type: "function" as const,
  name: "exec",
  description: `Run an async JavaScript body in an isolated sandbox.
Available globals:
declare namespace tools {
  function listPRFiles(args: { pr: number }): Promise<Array<{ path: string; additions: number }>>;
  function runSecurityScan(args: { path: string }): Promise<{ findings: string[] }>;
  function postPRComment(args: { pr: number; body: string }): Promise<{ posted: boolean }>;
}
declare function text(summary: string): void;`,
  parameters: { type: "object", properties: { code: { type: "string" } }, required: ["code"] },
};

// 2. 模型第一轮生成一段脚本,harness 在独立上下文里执行
const fcStep = turn1.steps.find((s: any) => s.type === "function_call");
const output: string[] = [];
await vm.runInNewContext(`(async () => { ${fcStep.arguments.code} })()`, {
  tools, // 宿主侧的真实能力,只在这个上下文里可见
  text: (msg: string) => output.push(msg),
});

// 3. 第二轮只把 text() 那一行摘要作为 function_result 传回
const turn2 = await client.interactions.create({
  model: "gemini-4-argon",
  previous_interaction_id: turn1.id,
  tools: [execTool],
  input: [{
    type: "function_result",
    name: fcStep.name,
    call_id: fcStep.id,
    result: [{ type: "text", text: output.join("\n") }],
  }],
});
console.log(turn2.output_text);

这个例子把 Code Mode 的工程要点都点出来了:模型只负责生成代码与判断,harness 负责执行与安全隔离,工具的真实能力全部藏在宿主侧。vm.runInNewContext 只是教学演示,生产环境要用 QuickJS 或 isolated-vm 这类隔离更硬的运行时。

行业已经在默认采用 Code Mode

这不是概念先行。Pi、Factory Droid、OpenAI Codex 等 harness 已经自带或默认启用 Code Mode。Factory 把 Droid 1.0 切到 Code Mode,按原文口径 Terminal-Bench 准确率提升 3.5 个点、模型花费下降 15%;Droid 1.0 于 2026 年 10 月 29 日发布,把程序化工具调用设为默认,官方称在 Terminal-Bench 4 上配合 Claude Opus 5.5 达到 65.2%、每个通过任务约 4.73 美元。框架侧,Pydantic AI、LangChain、Cloudflare Agents SDK 已经支持类似的 Code Mode 工具。

我的点评:范式反转的启示、边界与风险

三条启示。第一,工具接口要重新设计。很多现成的工具、CLI、Agent Skills 是为 bash 和人类可读文本优化的,不会天然契合类型化代码执行。文章提醒,开始用 Code Mode 时很快会发现它们接不进来,这会逼你退一步审计 Agent 真正需要什么。我认同这一点:Code Mode 的收益上限,取决于工具能否暴露成类型化、可组合的接口,而不是脚本本身写得多花哨。

第二,安全边界没有消失,只是移动了。沙箱无文件系统、无网络、无环境变量;凭据注入、schema 校验、破坏性操作的用户确认全部落在宿主代理层。每个 await tools.* 都跨一道结构化桥,这反而比传统工具调用更容易做出统一的审计与策略点。

第三,成本结构变了。模型花费下降不只是因为少了几轮往返,更因为上下文里不再堆中间载荷,跨轮状态用 store() / load() 放在上下文之外。代价是 harness 复杂度上升:执行隔离、类型编译、结果回传都要自己实现。

三条启示

接口要重做

为 bash 和人读文本准备的工具,接不进类型化执行。收益上限看接口能不能组合。

安全边界搬到宿主

沙箱不碰文件、网络和凭据。注入、校验、确认都落在每次 tools.* 跨过去的那一层。

成本换了位置

上下文里不再堆中间载荷。隔离、类型编译、结果回传要 harness 自己做。

也要泼一点冷水。Code Mode 未必适合所有 Agent:对可审计性要求极高的场景,一段脚本吞掉几十个动作会让每一步在做什么变得更难核对;对纯顺序、低频、单动作的任务,标准工具调用仍然更简单直接;模型生成的代码本身也可能有缺陷,沙箱与宿主代理之间必须有清晰的失败回传与重试协议。文章自己也说,这是研究问题也是产品问题,而不只是 harness 工程问题。我的结论是:Code Mode 是工具调用范式的重大演进,但它解决的是编排与上下文成本,不解决模型判断力本身。

参考