goose 的 Code Mode 扩展:让 Agent「写代码」来调用 MCP 工具
当上下文里塞不下几十个 MCP 工具定义时,一个正在快速流行起来的解法是"Code Mode"(又称 Sandbox Mode):不再把工具一个个直接暴露给大模型,而是为这些工具生成一份 JS/TS 程序化接口,让模型自己去搜索、阅读并编写调用这些接口的代码。goose 在 v1.17.0 中把它实现为一个开源的平台扩展——Code Mode,并在主线代码仓库中持续演进。本文将基于 goose 官方博客《Code Mode MCP》与该扩展的源码实现,讲清这一范式的核心思路、三大收益、底层路由原理以及如何启用与调参。
一个正在兴起的新思路:为什么不用"写代码"代替"逐个调用工具"?
传统的 MCP 工具调用是"客户端把工具定义全部塞进上下文 → 模型逐条选择 → 每步往返一次"。当扩展和工具数量变多时,每个工具的 name、description、input schema 都会占用上下文,模型还要在大批定义里准确挑选,既费 token 又容易选错。
Cloudflare 与 Anthropic 分别在各自的工程博客中提出了同一种替代思路,业界把它称为 "code mode" 或 "sandbox mode",即把工具调用变成代码生成与代码执行。goose 官方博客将其概括为三步(见 documentation/blog/2025-12-15-code-mode-mcp/index.md):
- 不再直接把工具暴露给模型,而是由 MCP 客户端应用为这些工具生成一份程序化接口(通常是 JS / TS);
- 只给模型提供极少量"元工具":搜索可用的模块 / 工具源码、读取某个工具的源码、以及执行一段代码;
- 模型写出的、调用程序化 API 的代码,在沙箱化环境中被执行以保证安全。
这样做的三大收益
按官方博客的阐述,收益集中在三点:
- 渐进式发现,而不是一次性加载:模型可以在执行过程中逐步发现自己需要的工具,而无需在会话一开始就把所有 server 与工具定义全部放进上下文窗口。
- 链式调用结果不必回流模型:模型生成的代码可以把一次工具调用的结果直接作为下一次调用的输入,中间结果完全不需要在模型与工具之间往返。这一方面节省了 token,另一方面也避免了把潜在敏感数据不必要地暴露给模型。
- 模型更擅长"读大型 API 并写代码":预训练数据让大模型在分析大型程序化 API、并写出调用代码这件事上极其高效——相比只见过"人造的 MCP 工具调用示例",编写 JS/TS 调用恰恰是模型最擅长的能力域。
In goose:一个名为 Code Mode 的平台扩展
goose 在 v1.17.0 起引入了这一想法的开源实现,载体是一个新的平台扩展(Platform Extension),名为 Code Mode。它生成了一个代表"当前已连接 MCP 工具"的 JavaScript 接口,再让模型编写代码去运行。你可以把它理解为 goose 生态中一种"元扩展":它本身也是一个 MCP server,同时把其它扩展包装成 JS 模块暴露给模型。
它在扩展注册表中的形态
在 crates/goose/src/agents/platform_extensions/mod.rs 的 PLATFORM_EXTENSIONS 注册表中,Code Mode 的条目定义如下(源码结构可证):
- 扩展 key:
code_execution,显示名 "Code Mode"; - 描述:
"Goose will make extension calls through code execution, saving tokens"(通过代码执行来发起扩展调用,从而节省 token); default_enabled: false——默认关闭,需要手动启用;unprefixed_tools: true——其工具不带扩展名前缀、作为一等公民直接暴露;- 编译期使用
#[cfg(feature = "code-mode")]门控,对应 crates/goose/Cargo.toml 中的可选 feature:code-mode = ["dep:pctx_code_mode"],底层引擎依赖pctx_code_mode v0.5.0这一外部 crate。
一个值得注意的版本演进
官方博客写作于 2025-12-15(v1.17.0),当时描述的实现是在 boa(一个可嵌入的 JavaScript 引擎)中执行模型生成的 JS 代码,并利用了 boa 的 NativeFunction 概念:一个 NativeFunction 可以把嵌入 JS 环境里的某个函数,回调到一段原生 Rust 实现中去——这天然适合"JS 发起的调用被路由到底层 MCP server"。
而当前仓库主线 crates/goose/src/agents/platform_extensions/code_execution.rs 的实现已演化为基于 Deno / V8 运行时(代码与单测中多处出现 run_in_deno_runtime、"Deno runtime is not Send"、"pctx's process-wide V8 mutex" 等注释)。核心抽象并未改变:JS 代码里的每一个"工具函数",本质都是一个回调到 Rust 原生实现的原生函数。若你在阅读历史版本或升级版本时对运行时差异感到困惑,这便是原因。
源码视角:Code Mode 的四个执行阶段
结合 code_execution.rs(约 1100 行的完整实现与测试),可以把这个扩展的工作流程拆成四步:
① 收集工具,生成 JS 回调配置
load_callback_configs(源码 L78-L113)从 ExtensionManager 取出当前 session 除 Code Mode 自身外的所有已连接工具(get_prefixed_tools_excluding),并做两类过滤:
- 跳过带 resource URI 的工具、以及被标记为"对模型不可见"的工具(
is_tool_visible_to_model); - 对每个工具解析出命名空间与名字:形如
prefix__tool的名字会被拆为namespace=prefix;否则取工具 owner 作为命名空间。
随后为每个工具构造一个 CallbackConfig,包含 name、namespace、description 与 input_schema,交给 pctx_code_mode 生成 JS 模块与 TS 类型。
② 构建/缓存 CodeMode 实例
CodeModeState::new 把这些回调配置交给 CodeMode::default().with_callbacks(...) 生成可执行对象,任何添加失败的配置都会被汇总上报。由于重建成本高,实例会按回调配置的顺序无关哈希(CodeModeState::hash 对序列化后的配置排序后再哈希)做缓存:配置没变就直接复用上次构建的 CodeMode,配置变了才重建(源码 L115-L147、L631-L669)。
③ 模型只看到少量元工具
Code Mode 向模型暴露的工具面由 ToolDisclosure(工具披露策略)决定,共三档,见 list_tools 实现(源码 L434-L545):
| 披露档位 | 暴露给模型的工具 | 说明 |
|---|---|---|
catalog(默认) |
list_functions、get_function_details、execute_typescript |
模型先检索函数目录、查看函数签名,再写代码执行 |
filesystem |
execute_bash、execute_typescript |
模型可先用 bash 搜读工具签名文件,再执行 TS |
sidecar |
execute_typescript |
只保留执行工具,最精简 |
其中每个工具的 schema 都由 Rust 结构体通过 schemars::schema_for! 自动生成,例如 ExecuteWithToolGraph 在 execute_typescript 的标准入参之外,还允许模型附带一个描述调用依赖关系的 tool_graph(一个 DAG:每个节点是 server/tool 的一次调用,用 depends_on 标注数据流)。返回给模型的代码文本(list_functions、get_function_details 的输出)由 pctx_code_mode 生成——也就是说,"读源码"与"生成 TS 类型定义"都发生在引擎内部。
④ 执行 JS,并把调用路由回 MCP
handle_execute_typescript(源码 L250-L285)是核心入口:它解析 ExecuteWithToolGraph 参数,取出模型写的 TS 代码,为代码中的每个回调名(拼回 namespace__tool 全名)通过 create_tool_callback 注册到 PctxRegistry。回调真正做的事情在 create_tool_callback(源码 L359-L406)里一目了然:
JS 调用 →
ExtensionManager.dispatch_tool_call(ctx, tool_call, cancellation_token)→ 等 dispatch 结果 →callback_result_to_value转回 JS 值。
回调结果转换 callback_result_to_value 还做了隐私过滤:只拼接 audience 标注允许 assistant 看到的 Text 内容块,丢弃 structured_content、meta、image、resource 以及仅对 user 可见的内容;若文本整体是合法 JSON 则解析为对象,否则作为字符串返回(对应测试 callback_result_ignores_hidden_structured_content_and_meta 与 callback_result_uses_assistant_visible_text_and_preserves_json_parsing)。
安全与可靠性设计
- 脚本被要求在独立运行时内执行:Deno runtime 不是
Send,因此run_in_deno_runtime(源码 L310-L357)把执行放进tokio::task::spawn_blocking,每个脚本在独立的 current-thread tokio runtime 里跑;pctx内部以进程级 V8 互斥锁串行化所有执行,避免多 session 并发踩 V8 状态。 - 超时与取消可传播:执行受扩展超时限制(见下文"默认 300 秒"),同时在超时/取消时取消一个与 JS 内嵌工具调用共享的
dispatch_token子令牌,让正在进行的嵌套工具调用(例如一个长shell命令)收到取消信号、有机会清理子进程;随后留出 500ms(DISPATCH_DRAIN_TIMEOUT)排水期再放弃任务,防止脚本挂起把整个扩展的执行能力"焊死"。 - 针对以上行为的测试覆盖:
run_in_deno_runtime_times_out_on_hung_execution、run_in_deno_runtime_honors_cancellation、run_in_deno_runtime_cancels_dispatch_token_when_abandoned、real_v8_hung_script_times_out_and_frees_the_runtime(验证一个挂死脚本超时后,另一个正常脚本能立刻成功执行,证明 V8 互斥锁被释放)、execute_bash_annotations_require_approval(验证execute_bash被标注为可写、破坏性、非幂等)等测试都在 code_execution.rs 的tests模块中。
如何启用与配置
启用 Code Mode
在 v1.17.0 及之后的 goose 中:
- 桌面端:点击左侧边栏的 Extensions(扩展)入口,找到并启用 Code Mode;
- CLI:运行
goose configure,在扩展列表中选择启用 Code Mode。
注意,由于该扩展在编译期受 code-mode feature 门控(见 crates/goose/Cargo.toml),你所使用的二进制需要是开启了该 feature 的构建版本。
关键参数
| 参数 | 取值 | 说明 |
|---|---|---|
CODE_MODE_TOOL_DISCLOSURE |
catalog(默认)/ filesystem / sidecar |
控制向模型披露的工具面,见上文 ToolDisclosure 三档表格;读取逻辑见 get_tool_disclosure(code_execution.rs L623-L629),未设置时回退到 catalog |
| 扩展超时 | 默认 300 秒(DEFAULT_EXTENSION_TIMEOUT,定义于 crates/goose/src/config/extensions.rs) |
单次脚本执行上限,可经全局配置 GOOSE_DEFAULT_EXTENSION_TIMEOUT(见 crates/goose/src/config/base.rs 的 config_value! 宏)覆盖;超时后 Code Mode 会取消脚本及其嵌套调用 |
启用后的行为差异
一旦启用,Code Mode 的 get_moim(模型指令,源码 L584-L610)会持续向模型灌输同一条铁律——把多个工具操作批量放进一次 execute_typescript 调用:
ALWAYS batch multiple tool operations into ONE execute_typescript call.
- WRONG: Separate execute_typescript calls for read file, then write file
- RIGHT: One execute_typescript with an async run() function that reads AND writes AND logs/returns as little information as needed for the next step.
在 catalog 档下还会提示:execute_typescript 内部注册了 N 个回调函数,但不要直接把回调函数名当作工具去调用,先用 list_functions / get_function_details 确认签名,再写一次性的执行代码。
Code Mode 并不会取代 MCP
需要澄清一个常见误解:Code Mode 不是要"干掉" MCP。仓库中紧随其后的博文 documentation/blog/2025-12-21-code-mode-doesnt-replace-mcp/index.md 给出了一个贴切的类比——MCP 好比 HTTP 协议,Code Mode 则是建立在协议之上的架构模式。Code Mode 发现和执行的工具依然是 MCP 工具;goose 甚至把 Code Mode 本身实现成了一个 MCP server(平台扩展),本质是对"模型如何与 MCP 工具交互"这一层做优化。
该文作者还用 Claude Opus 4.5 开了 8 个扩展做了单次对照实验:同样的多步提示词,未开启 Code Mode 时任务通过 5 次独立工具调用完成、占用约 16% 的上下文窗口;开启 Code Mode 后模型先用发现工具找到所需模块、再写一段 JS 脚本一次性完成整个工作流,上下文占用降到约 3%。需要说明这是单个作者的一次性实测记录而非官方基准,但它直观展示了"工具面收窄 + 结果链不出上下文"带来的 token 收益。
总结与评估方向
goose 的 Code Mode 扩展把 Cloudflare / Anthropic 提出的"code mode / sandbox mode"从构想变成了可运行、可审计的开源实现:接入 MCP 工具 → 生成 JS/TS 程序化接口 → 模型在极少量元工具引导下编写代码 → 沙箱运行时(初版 boa,主线已演进为基于 Deno/V8 的 pctx_code_mode)执行并逐层路由回底层 MCP server。
官方博客对其寄予的期望集中在两点:提升 goose 的工具调用性能、以及更好应对大量工具的场景。如果你在 v1.17.0+ 上同时挂着十几个扩展、明显感到上下文吃紧,值得按上文步骤启用 Code Mode 亲自对比——重点观察它的批处理、渐进发现与超时保护机制在你的工作负载下是否成立。
延伸阅读(均在当前仓库内)
- 官方博客原文:documentation/blog/2025-12-15-code-mode-mcp/index.md
- 扩展完整实现与测试:crates/goose/src/agents/platform_extensions/code_execution.rs
- 扩展注册与 feature 门控:crates/goose/src/agents/platform_extensions/mod.rs、crates/goose/Cargo.toml
- Code Mode 与 MCP 关系的进一步解读:documentation/blog/2025-12-21-code-mode-doesnt-replace-mcp/index.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00