本页文字稿为音频的配套解读,内容取自小山学堂课程素材,属二次演绎版本。
行号与源码核对日期为 2026-08-13;演示中的轮数与 token 数为课程化估算,非精确基准。
两件事:为什么让模型写一段程序,比逐个发工具调用又快又省;以及模型写的代码在沙箱里跑,宿主凭什么自保。
贯穿两块的线索:第一块的本质是省往返,第二块的本质是不信任。二者是一枚硬币的两面——正因为要把一堆动作打包交给别人执行,才必须在放手前把防护做足。省往返能省到什么程度,取决于你敢放手到什么程度。
| 名字 | 出处 | 说明 |
|---|---|---|
| PTC(程序化工具调用) | 官方发布文;code/preset.yml 第 1 行 name: PTC 模式 | 只在 UI 文案层 |
| code mode | 配置项 mode: code、工具名 run_code | 源码内部命名,全仓库搜不到 PTC |
跟人聊源码时说 code mode 才对得上号。
同一个任务:读 3 个日志文件 → 汇总统计 → 写 1 份报告。
| 维度 | 传统工具调用 | Code Mode |
|---|---|---|
| 采样轮数 | 5 轮(每动作一轮) | 1 轮 |
| 上下文内容 | 三份文件全文 + 报告 | 只有程序 + 一次性返回值 |
| 吞吐(课程化估算) | 约 18.3k token | 约 2.6k token |
| 中间结果去向 | 全部躺进上下文 | 留在沙箱变量里,只有 print / return 回模型 |
不在工具本身,而在"一步一采样"这个节奏。 每一轮采样模型都要把滚大的上下文重读一遍——文件越多,上下文越大,每轮代价越高,接近平方级增长,不是线性。
越到后面的轮次,模型为了决定一个越来越小的动作,要读的东西反而越来越多。
一轮采样里,模型写一段 TypeScript 小程序,用 await tools.<name>(args) 想调几次调几次,循环、分支都行。程序在沙箱里把活干完,中间结果全留在沙箱变量里,跑完只把 print 与 return 的内容送回模型。
工具描述原话(packages/core/tools/src/code-mode.ts 第 52 行):
"Only what you print or return comes back — curate it."
一句话概括:中间值永不进上下文。
apps/cli/config/agent-presets/code/agent.cordis.yml 第 1–6 行:
Thecodeagent preset: the standard coding agent, presented as Code Mode. Everything instandardis here unchanged. What is added is thetool-presentationrow: instead of one tool call per action, the model writes a TypeScript program against a generated SDK andrun_codeexecutes it, so a sequence that would be five round trips becomes one.
伏笔(第 8–11 行):这个 preset 改的只是工具的呈现方式,工具注册表本身留在宿主手里——这正是思路二"权限跟随"的地基。
网络编程几十年的老道理:往返贵,批处理便宜。数据库有批量写入,RPC 框架都在攒 batch。模型采样一轮远比一次网络往返贵,把 N 次往返合成一次收益只会更夸张。换语言重写这笔账照样成立——省的是往返次数本身,不是某个实现细节。
前提问题:沙箱里跑的是模型现写的代码,没人审过一行。图省事在宿主进程里跑,翻车方式随便挑:
宿主必须从第一天就假设对面会使坏。这个假设立住之后,剩下的都是工程题。
出处:packages/code-runtime/code-runtime-worker-thread/src/index.ts 第 378–387 行。
| 措施 | 针对 |
|---|---|
| 每次运行新开全新 worker,用完即弃 | 上次运行的痕迹不带到下次,攻击面不累积 |
| 环境变量清空 | 最常见的一类泄露——凭据藏在环境变量里 |
| 继承的加载器标志掐断 | 通过修改加载行为做手脚 |
| 堆上限焊死,爆了 worker 直接退出 | 资源耗尽;在这一层就终止,不抛可被捕获的异常 |
到了内存吃满这一步,程序已经不可信了——给它一次捕获异常的机会,就是给它一次挣扎的机会。
出处:同文件第 142–165 行;bootstrap.ts 第 324–326 行。
call / log / output-limit / done;tools 命名空间用 null-prototype 构建,伪造原型名摸不到任何东西。两个关键词:
若允许共享内存,程序可直接摸宿主地址空间,验证消息形状就完全没意义了。
出处:同文件第 534–545 行(时间)、第 169–229 行(字节)。
| 账本 | 类型 | 作用 |
|---|---|---|
computeMs | CPU 忙碌时间(轮询实测) | 热循环藏不住;干等慢工具不冤枉计费 |
maxWallMs | 墙上时间兜底 | 不管忙不忙,到点就终止 |
| 输出账本 | 字节 | worker 发送前自预检,宿主收到后再记一遍 |
为什么要两本时间账:只有墙钟 → 干等慢工具被冤枉;只有 CPU 时间 → 一个死等不消耗的调用就能无限挂住。两本一起上才既不错杀也不放过。
凡是让不可信的一方自己上报消耗,都要在可信侧再记一遍。自报的数字只能当参考,不能当事实。
自报本身就是一个可被攻击的接口——程序可以先干一堆消耗再只报一个小数;只要有一本账捏在自己手里,美化就没用。计费、配额、限流系统的铁律:账本必须在你信任的那一侧。
轮询的意义:边跑边量才能抓住"跑了很久但中途才开始作恶"的情况;跑完再量只能得到一个总数,分不清时间花在哪。
出处:packages/core/tools/src/code-mode.ts 第 545、477、470 行;第 601–608 行。
await tools.xxx 被宿主包装成子调度,带父调用 token,走和原生模式同一条 pre-execute 审批瀑布;callId:code:n,事件里全程留痕;mode: code 下想绕开 run_code 直发原生调用,进策略管线之前就被拒为 UNKNOWN_TOOL。入口收窄了,但权限没换门。
packages/code-runtime/code-runtime-worker-thread/README.zh.md 开门见山:
这是隔离措施,而非安全边界:其信任立场有意与 bash 等价……但提供 bash 没有的隔离:独立 isolate、空环境、堆上限与强制终止。
展开:很多人一听到沙箱就默认它是牢不可破的墙。这里的说法恰恰相反——信任立场有意与 bash 等价。别因为代码跑在沙箱里就降低警惕,该走的审批一条都不能少。
| 含义 | |
|---|---|
| 隔离 | 出事时把损失圈住 |
| 安全边界 | 保证不出事 |
两者的区别,决定了你会不会在沙箱外面再补一层防线。
| 产品 | 有无等价机制 | 说明 |
|---|---|---|
| Claude Code | 无 | bash 是通用逃生舱,模型可写脚本再执行,但 Read / Edit 等工具 API 不作为可编程绑定暴露给脚本,脚本内部动作也不走各工具自己的管线。Anthropic 官方博客《Code execution with MCP》提出同思路,截至核对日期未见产品内置同类 run_code |
| Grok Build | 无 | 全库检索 run_code 与 code mode,只有遥测事件名字面撞词。工具体系走原生调用加 toolset preset 组合 |
| Codex CLI / Cloudflare | 思路相通 | DSH 设计笔记明确引用 Cloudflare 博客,核心观察是模型写代码的能力好于连发工具调用(.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md)。本课未核对这两家源码 |
评估"让模型写程序去编排工具"这类设计时:
deepseek-harness-master 本地仓库核对。凡模型要连续做一串动作,先问:能不能合成一次?省的不只是 token,还有上下文变长带来的每一轮代价。
让执行环境替你拿着中间结果,只把结论送回来。上下文不是日志,别什么都往里塞。 更关键的是,把"什么值得进上下文"的决定权交还给程序。
不复用就不积累攻击面。资源从启动参数那一层就开始焊死——别指望代码自觉。到资源耗尽那一步,直接终止,不要给捕获异常的机会。
窄接口本身就是防御,重建比验证更可靠。你能验证的东西越少,验证得就越彻底。
凡涉及计费、配额、限流,账本必须在你信任的那一侧,且要边跑边量而非事后统计。
程序 A 是同步热循环while (true) {},程序 B 是await new Promise(() => {})(永不 resolve)。A 和 B 分别被computeMs还是maxWallMs终止?为什么 A 不能靠挂一个待完成的工具调用躲过计费?
computeMs 轮询到的忙碌时间不断累加 → 被 computeMs 终止。它藏不住,因为它在真跑 CPU。computeMs 增长极慢 → 只能靠 maxWallMs 墙上时间兜底终止。这正是"只有 CPU 时间账就会被无限挂住"的反例。computeMs 是轮询 worker 实测的忙碌时间,热循环期间 CPU 一直忙,时间照记;而待完成的工具调用属于"干等",本来就不计入 CPU 忙碌时间——两者不冲突,忙的时间该记还是记。UNKNOWN_TOOL。来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本内容改编自小山学堂课程素材,为二次演绎版本。