Code Mode 深度解读:Agent 的工具目录正在坍缩成一个 exec()
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(...),转换数组,直接调下一个函数,不用在步骤之间反复往返上下文窗口。
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 分发调用。
标准工具调用与 Code Mode 的差异,可以对照下面这张表来看。
为什么是 JS/Python 沙箱,而不是 bash
bash 也能搭工具代理,但把工具编排放进 JavaScript 或 Python 这类语言运行时,有几个 bash 环境里很难做到的收益。
一个最小实现: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 复杂度上升:执行隔离、类型编译、结果回传都要自己实现。
也要泼一点冷水。Code Mode 未必适合所有 Agent:对可审计性要求极高的场景,一段脚本吞掉几十个动作会让每一步在做什么变得更难核对;对纯顺序、低频、单动作的任务,标准工具调用仍然更简单直接;模型生成的代码本身也可能有缺陷,沙箱与宿主代理之间必须有清晰的失败回传与重试协议。文章自己也说,这是研究问题也是产品问题,而不只是 harness 工程问题。我的结论是:Code Mode 是工具调用范式的重大演进,但它解决的是编排与上下文成本,不解决模型判断力本身。
参考
- Why Agents Are Soon Only Going to Have 1 Tool(Philipp Schmid,2026-11-01)
- MCP Tools:outputSchema / structuredContent
- Droid 1.0: Building a Frontier Harness(Factory,2026-10-29)