1
00:00:00,100 --> 00:00:06,432
本内容改编自小山学堂《学 AI 产品，从入门到精通》，为二次演绎配音版。

2
00:00:06,382 --> 00:00:12,475
这是 DeepSeek Harness 系列的又一集，讲一个名字有点乱、机制却很干净的设计。

3
00:00:12,475 --> 00:00:14,387
先把名字说清楚。

4
00:00:14,387 --> 00:00:20,853
官方发布文叫它 PTC，程序化工具调用，预设元数据里也写着 PTC 模式。

5
00:00:20,853 --> 00:00:24,531
但你去源码里搜 PTC，一个都搜不到。

6
00:00:24,531 --> 00:00:30,649
内部命名从头到尾是 code mode，配置项叫 mode code，工具名叫 run_code。

7
00:00:30,649 --> 00:00:33,281
两个名字，同一个机制。

8
00:00:33,281 --> 00:00:36,646
跟人聊源码的时候说 code mode 才对得上号。

9
00:00:36,646 --> 00:00:38,605
这一集讲两件事。

10
00:00:38,605 --> 00:00:44,891
第一件，为什么让模型写一段程序，比让它逐个发工具调用又快又省。

11
00:00:44,891 --> 00:00:50,048
第二件，模型写的代码在沙箱里跑，宿主凭什么自保。

12
00:00:50,048 --> 00:00:51,826
这一集分四块。

13
00:00:51,826 --> 00:01:04,471
先讲遥控器问题，再讲一次往返的解法，然后拆三道防线和那条一寸不松的权限规矩，最后横向看看别家有没有等价物，收五条能带走的原则。

14
00:01:04,471 --> 00:01:07,548
先给一句能贯穿两块的线索。

15
00:01:07,548 --> 00:01:12,139
第一块的本质是省往返，第二块的本质是不信任。

16
00:01:12,139 --> 00:01:22,524
这两件事看着相反，其实是一枚硬币的两面，正是因为你要把一堆动作打包交给别人去执行，你才必须在放手之前把防护做足。

17
00:01:22,524 --> 00:01:27,007
省往返省到什么程度，取决于你敢放手到什么程度。

18
00:01:27,007 --> 00:01:34,711
顺带提醒，本集的行号都是二零二六年八月十三日核对源码的结果，别把行号当结论背。

19
00:01:34,852 --> 00:01:37,133
先说它解决什么问题。

20
00:01:37,083 --> 00:01:40,533
原生工具调用像给模型一个遥控器。

21
00:01:40,533 --> 00:01:44,896
按一下，读一个文件；结果回来，再按一下。

22
00:01:44,896 --> 00:01:52,288
每按一下都是一轮完整的采样，模型要把滚大的上下文重新读一遍，才能决定下一步。

23
00:01:52,288 --> 00:01:53,405
举个数。

24
00:01:53,405 --> 00:01:58,045
读三个日志文件再写一份报告，一共五轮采样。

25
00:01:58,045 --> 00:02:03,417
第一轮读第一个文件，四百多行全文回传，整段进上下文。

26
00:02:03,417 --> 00:02:06,879
第二轮读第二个文件，又是将近四百行。

27
00:02:06,879 --> 00:02:09,920
第三轮第三个文件，四百多行。

28
00:02:09,920 --> 00:02:14,667
第四轮对着上下文里的三份全文算统计、写报告。

29
00:02:14,667 --> 00:02:16,951
第五轮输出最终回答。

30
00:02:16,951 --> 00:02:19,631
换成五十个文件会怎样。

31
00:02:19,631 --> 00:02:21,819
预算和耐心一起烧完。

32
00:02:21,819 --> 00:02:24,499
请注意成本瓶颈在哪里。

33
00:02:24,499 --> 00:02:30,401
它不在工具本身，读一个文件是很快的，慢的是一步一采样这个节奏。

34
00:02:30,401 --> 00:02:35,280
每一轮采样，模型都要把之前所有的内容重新过一遍。

35
00:02:35,280 --> 00:02:41,627
文件越多，上下文越滚越大，每一轮的代价越高，这是个正反馈的坑。

36
00:02:41,627 --> 00:02:50,232
教学演示里给过一个吞吐对比，同样一个任务，传统模式大约一万八千 token，代码模式大约两千六。

37
00:02:50,232 --> 00:02:57,864
这个数字是课程化估算，用来展示结构差异，别当精确基准看，但数量级的差距是真的。

38
00:02:57,864 --> 00:03:02,348
再把这笔账拆细一点，你会发现问题比想象的更糟。

39
00:03:02,348 --> 00:03:13,165
上下文是滚雪球的，第一轮它只有指令，第二轮多了一整个文件的全文，第三轮再多一个，第五轮它装着三份全文加一份报告。

40
00:03:13,165 --> 00:03:20,737
也就是说，越到后面的轮次，模型为了决定一个越来越小的动作，要读的东西反而越来越多。

41
00:03:20,737 --> 00:03:23,862
这不是线性增长，接近平方级。

42
00:03:23,862 --> 00:03:31,134
所以这类任务的真正瓶颈，从来不是模型算得慢，而是它被迫反复读同一批内容。

43
00:03:31,134 --> 00:03:35,869
凡是让模型反复重读的设计，都值得重新想一想。

44
00:03:36,004 --> 00:03:37,528
思路是什么。

45
00:03:37,478 --> 00:03:48,211
一轮采样里，模型直接写一段 TypeScript 小程序，程序里用 await tools 点某个工具名这种形式，想调几次调几次，循环、分支都行。

46
00:03:48,211 --> 00:03:56,492
程序在沙箱里把活干完，中间结果全留在沙箱的变量里，跑完只把打印和返回的内容送回模型。

47
00:03:56,492 --> 00:04:03,776
工具描述里那句原话值得抄下来，只有你打印或者返回的东西会回来，所以要精心挑选。

48
00:04:03,776 --> 00:04:07,033
出处是代码模式源码第五十二行。

49
00:04:07,033 --> 00:04:10,964
一句人话概括，中间值永不进上下文。

50
00:04:10,964 --> 00:04:15,327
预设文件的头几行注释把设计意图写得明明白白。

51
00:04:15,327 --> 00:04:30,607
原话大意是，标准模式的所有内容在这里一行不动，新增的只是工具呈现这一行，模型不再一次调用做一个动作，而是写一段程序，交给 run_code 执行，于是原本五次的往返变成一次。

52
00:04:30,607 --> 00:04:33,636
本集标题就是从这句注释来的。

53
00:04:33,636 --> 00:04:36,076
这段注释还埋了个伏笔。

54
00:04:36,076 --> 00:04:42,326
它说这个预设改的只是工具的呈现方式，工具注册表本身留在宿主手里。

55
00:04:42,326 --> 00:04:46,761
这句话看着轻描淡写，其实是第二块内容的地基。

56
00:04:46,761 --> 00:04:54,056
正因为注册表还在宿主手里，程序里的每一次工具调用才能照走原来那条审批管线。

57
00:04:54,196 --> 00:04:56,093
为什么长期成立。

58
00:04:56,043 --> 00:05:00,910
这是网络编程几十年的老道理，往返贵，批处理便宜。

59
00:05:00,910 --> 00:05:05,309
数据库有批量写入，远程调用框架都在攒批。

60
00:05:05,309 --> 00:05:12,461
模型采样一轮比一次网络往返贵得多，把 N 次往返合成一次，收益只会更夸张。

61
00:05:12,461 --> 00:05:17,208
哪天这套系统换个语言重写，这笔账照样成立。

62
00:05:17,208 --> 00:05:21,319
因为它省的不是某个实现细节，是往返次数本身。

63
00:05:21,319 --> 00:05:24,252
再补一层容易被忽略的收益。

64
00:05:24,252 --> 00:05:28,651
程序化之后，模型表达控制流的能力也变强了。

65
00:05:28,651 --> 00:05:37,040
逐个发调用的时候，模型只能一轮一轮地想，循环和条件分支都要靠上下文里的自然语言去维持。

66
00:05:37,040 --> 00:05:44,468
写成程序之后，循环就是循环，条件就是条件，语义不会因为上下文变长而模糊。

67
00:05:44,468 --> 00:05:48,531
这不只是省 token，也是把意图表达得更精确。

68
00:05:48,531 --> 00:05:53,987
还有一层收益藏在「只把打印和返回的东西送回模型」这句话里。

69
00:05:53,987 --> 00:05:58,422
它其实把「什么值得进上下文」这个决定权交还给了程序。

70
00:05:58,422 --> 00:06:04,420
传统模式下，工具返回什么，上下文就装什么，模型没有选择权。

71
00:06:04,420 --> 00:06:12,593
代码模式下，模型自己写一段程序决定最后返回哪几个字段，等于给它加了一层信息筛选的能力。

72
00:06:12,593 --> 00:06:16,307
这一层的价值，往往比省下的 token 还大。

73
00:06:16,307 --> 00:06:25,562
所以工具描述里那句「要精心挑选」不是随口一说，它是在提醒模型，你现在拥有了裁剪信息的权力，请用好它。

74
00:06:25,708 --> 00:06:29,528
现在讲第二块，也是这一集更重要的部分。

75
00:06:29,478 --> 00:06:31,833
思路一有个前提没解决。

76
00:06:31,833 --> 00:06:36,245
沙箱里跑的是模型现写的代码，没人审过一行。

77
00:06:36,245 --> 00:06:41,088
要是图省事直接在宿主进程里跑，翻车方式随便挑。

78
00:06:41,088 --> 00:06:51,449
环境变量里的密钥随手读走；一个死循环把内存吃光，宿主跟着一起死；程序还能伪造消息，冒充工具结果骗过上层。

79
00:06:51,449 --> 00:06:55,692
所以宿主必须从第一天就假设对面会使坏。

80
00:06:55,692 --> 00:06:59,273
这个假设立住之后，剩下的都是工程题。

81
00:06:59,273 --> 00:07:01,894
第一道防线是资源焊死。

82
00:07:01,894 --> 00:07:05,800
每次运行新开一个全新 worker，用完即弃。

83
00:07:05,800 --> 00:07:18,216
启动参数把路堵死，环境变量清空，程序拿不到宿主的任何凭据；继承的加载器标志掐断；堆上限焊死，程序把堆吃爆，worker 直接退出。

84
00:07:18,216 --> 00:07:22,891
出处是运行时源码第三百七十八到三百八十七行。

85
00:07:22,891 --> 00:07:25,764
注意「用完即弃」这四个字的分量。

86
00:07:25,764 --> 00:07:32,374
不复用意味着上一次运行留下的任何痕迹都不会带到下一次，攻击面不会累积。

87
00:07:32,374 --> 00:07:35,343
再把这三根钉子逐个看一遍。

88
00:07:35,343 --> 00:07:45,211
环境变量清空，针对的是最常见的一类泄露，凭据就藏在环境变量里，很多事故都是一行代码把它们读走再发出去。

89
00:07:45,211 --> 00:07:51,184
加载器标志掐断，针对的是通过修改加载行为来做手脚的路子。

90
00:07:51,184 --> 00:07:59,898
堆上限焊死，针对的是资源耗尽，而且它的处理方式是让 worker 直接退出，不是抛一个可以被捕获的异常。

91
00:07:59,898 --> 00:08:10,343
为什么要在这一层就终止，因为到了内存吃满这一步，程序已经不可信了，给它一次捕获异常的机会，就是给它一次挣扎的机会。

92
00:08:10,492 --> 00:08:12,365
第二道防线是通信。

93
00:08:12,315 --> 00:08:16,954
宿主和 worker 之间只有一条消息端口，不共享内存。

94
00:08:16,954 --> 00:08:22,134
上行消息只有四种，调用、日志、输出超限、完成。

95
00:08:22,134 --> 00:08:28,745
每一条入站消息先验形状，再逐字段重建一份干净的，垃圾静默丢弃。

96
00:08:28,745 --> 00:08:33,541
出处是同一份源码的第一百四十二到一百六十五行。

97
00:08:33,541 --> 00:08:38,384
这里有两个词值得咬一下，先验形状，再逐字段重建。

98
00:08:38,384 --> 00:08:44,262
它不是验证完就直接用原始对象，而是照着白名单重新造一个。

99
00:08:44,262 --> 00:08:50,920
这样对象上挂的任何多余属性、任何原型链上的东西，都进不到宿主的世界里。

100
00:08:50,920 --> 00:08:56,485
这比「检查通过了就放行」要稳得多，后者总有你没想到要检查的字段。

101
00:08:56,485 --> 00:08:59,779
还有个细节特别能体现防御的深度。

102
00:08:59,779 --> 00:09:08,348
worker 侧的工具命名空间是用空原型构建的，那些想通过原型链做手脚的名字，在这里摸不到任何东西。

103
00:09:08,348 --> 00:09:13,504
出处是启动引导文件第三百二十四到三百二十六行。

104
00:09:13,504 --> 00:09:17,531
再补一句为什么消息种类要限死在四种。

105
00:09:17,531 --> 00:09:22,194
种类越少，要验证的形状越少，出错的面就越小。

106
00:09:22,194 --> 00:09:28,300
窄接口本身就是一种防御，跟前面几集讲过的闭合词汇表是同一个道理。

107
00:09:28,300 --> 00:09:32,675
再从攻击者的角度想一想这条防线挡住了什么。

108
00:09:32,675 --> 00:09:41,581
如果允许共享内存，程序可以直接去摸宿主地址空间里的对象，验证消息形状这件事就完全没意义了。

109
00:09:41,581 --> 00:09:48,492
如果消息种类不限，每新增一种就多一个要验证的形状，多一个可能漏掉的字段。

110
00:09:48,492 --> 00:09:55,548
现在只有一个端口、四种消息、逐字段重建，攻击者能做的动作被压到了最小。

111
00:09:55,548 --> 00:10:02,218
安全设计里有个朴素的道理，你能验证的东西越少，你验证得就越彻底。

112
00:10:02,356 --> 00:10:08,411
第三道防线是记账，而且两头各记一份，worker 自己报的数字不作数。

113
00:10:08,361 --> 00:10:10,236
时间上是两本账。

114
00:10:10,236 --> 00:10:14,479
一本叫 computeMs，轮询 worker 实测的忙碌时间。

115
00:10:14,479 --> 00:10:22,748
热循环藏不住，因为它在真跑 CPU；干等一个慢工具又不冤枉计费，因为那时候 worker 是闲的。

116
00:10:22,748 --> 00:10:28,289
另一本叫 maxWallMs，管兜底，不管你忙不忙，墙上时间到了就终止。

117
00:10:28,289 --> 00:10:31,955
两个预算任意一到点，直接强制终止。

118
00:10:31,955 --> 00:10:36,667
出处是运行时源码第五百三十四到五百四十五行。

119
00:10:36,667 --> 00:10:38,325
为什么要两本。

120
00:10:38,325 --> 00:10:46,955
只有一本墙钟，干等慢工具会被冤枉；只有一本 CPU 时间，一个死等不消耗的调用就能无限挂住。

121
00:10:46,955 --> 00:10:50,441
两本一起上，才既不错杀也不放过。

122
00:10:50,441 --> 00:10:52,508
字节上也是两头记。

123
00:10:52,508 --> 00:10:58,253
worker 发送前自己预检一次，宿主收到后用输出账本再记一遍。

124
00:10:58,253 --> 00:11:01,006
谁也别想只报个好听的数。

125
00:11:01,006 --> 00:11:05,537
出处是同文件第一百六十九到二百二十九行。

126
00:11:05,537 --> 00:11:13,638
这套设计的通用形状值得记住，凡是让不可信的一方自己上报消耗，都要在可信的那一侧再记一遍。

127
00:11:13,638 --> 00:11:17,544
自报的数字只能当参考，不能当事实。

128
00:11:17,544 --> 00:11:23,325
再深一层想，为什么一定要在可信侧记账，让 worker 自己报不行吗。

129
00:11:23,325 --> 00:11:27,340
因为自报这件事本身就是一个可以被攻击的接口。

130
00:11:27,340 --> 00:11:35,008
程序完全可以先干一堆消耗，然后只报一个很小的数，宿主要是信了它，预算就形同虚设。

131
00:11:35,008 --> 00:11:41,402
反过来说，只要有一本账捏在自己手里，程序再怎么美化自己的消耗也没用。

132
00:11:41,402 --> 00:11:48,530
凡是涉及计费、配额、限流的系统，这条都是铁律，账本必须在你信任的那一侧。

133
00:11:48,530 --> 00:11:51,715
还有一个细节能看出设计者的细致。

134
00:11:51,715 --> 00:11:58,337
轮询忙碌时间这件事，意味着它不是等程序跑完再去量，而是边跑边量。

135
00:11:58,337 --> 00:12:08,433
边跑边量才能抓住那种「跑了很久但中途才开始作恶」的情况，跑完再量就只能得到一个总数，分不清时间花在哪。

136
00:12:08,572 --> 00:12:11,286
然后是那条最关键的规矩。

137
00:12:11,236 --> 00:12:14,373
程序进了沙箱，审批一寸不松。

138
00:12:14,373 --> 00:12:23,171
每一次 await 工具调用，都被宿主包装成一次子调度，带着父调用的令牌，走和原生模式同一条审批瀑布。

139
00:12:23,171 --> 00:12:28,676
子调用的 id 形如调用号加 code 加序号，事件里全程留痕。

140
00:12:28,676 --> 00:12:34,229
出处是代码模式源码第五百四十五、四百七十七、四百七十行。

141
00:12:34,229 --> 00:12:41,741
程序能绑到的工具，正好是系统提示词里声明过的那些，受限工具在名单里直接消失。

142
00:12:41,741 --> 00:12:46,104
出处是同文件第六百零一到六百零八行。

143
00:12:46,104 --> 00:12:47,955
反过来也堵死了。

144
00:12:47,955 --> 00:12:55,551
在 code 模式下想绕开 run_code 直接发原生调用，进策略管线之前就被拒为未知工具。

145
00:12:55,551 --> 00:12:58,748
入口收窄了，但权限没换门。

146
00:12:58,748 --> 00:13:00,959
这里必须点破一句定位。

147
00:13:00,959 --> 00:13:14,072
官方 README 开门见山就说了，这是隔离措施，而非安全边界，它的信任立场有意与 bash 等价，但提供 bash 没有的隔离，独立隔离环境、空环境、堆上限与强制终止。

148
00:13:14,072 --> 00:13:18,099
这句话很重要，别把沙箱当成牢不可破的墙。

149
00:13:18,099 --> 00:13:26,488
它的意思是，跑模型写的代码和跑 bash 命令，在信任级别上是同一档，只是额外提供了几层隔离。

150
00:13:26,488 --> 00:13:30,046
安全边界要另外建立，不能指望沙箱。

151
00:13:30,046 --> 00:13:32,582
把这句定位再展开一层。

152
00:13:32,582 --> 00:13:40,034
很多人一听到沙箱，就默认它是牢不可破的墙，把不可信代码往里一扔就万事大吉。

153
00:13:40,034 --> 00:13:44,950
这里的说法恰恰相反，它的信任立场有意与 bash 等价。

154
00:13:44,950 --> 00:13:51,909
也就是说，别因为代码跑在沙箱里，就降低对它的警惕，该走的审批一条都不能少。

155
00:13:51,909 --> 00:13:57,895
沙箱提供的是隔离，是在出事的时候把损失圈住，而不是保证不出事。

156
00:13:57,895 --> 00:14:03,147
这两者的区别，决定了你会不会在沙箱外面再补一层防线。

157
00:14:03,147 --> 00:14:05,010
顺带看看别家。

158
00:14:05,010 --> 00:14:19,060
Claude Code 没有等价机制，bash 是通用逃生舱，模型可以写脚本再执行，但读取、编辑这些工具接口并不作为可编程绑定暴露给脚本，脚本内部的动作也不走各工具自己的管线。

159
00:14:19,060 --> 00:14:24,469
Grok Build 也没有，它的工具体系走原生调用加工具集预设组合。

160
00:14:24,469 --> 00:14:32,366
这套让模型写程序、由沙箱编排工具接口的通道，在这两家已核对的材料里都没见到。

161
00:14:32,500 --> 00:14:34,252
收五条原则。

162
00:14:34,202 --> 00:14:37,327
第一条，往返贵，批处理便宜。

163
00:14:37,327 --> 00:14:42,484
凡是模型要连续做一串动作，先问一句，能不能合成一次。

164
00:14:42,484 --> 00:14:47,532
省的不只是 token，还有上下文变长带来的每一轮代价。

165
00:14:47,532 --> 00:14:50,452
第二条，中间值不要进上下文。

166
00:14:50,452 --> 00:14:55,392
让执行环境替你拿着中间结果，只把结论送回来。

167
00:14:55,392 --> 00:14:58,746
上下文不是日志，别什么都往里塞。

168
00:14:58,746 --> 00:15:02,484
第三条，新开干净环境，用完即弃。

169
00:15:02,484 --> 00:15:05,032
不复用就不积累攻击面。

170
00:15:05,032 --> 00:15:10,260
资源从启动参数那一层就开始焊死，别指望代码自觉。

171
00:15:10,260 --> 00:15:15,296
第四条，通信走窄接口，先验形状再逐字段重建。

172
00:15:15,296 --> 00:15:19,335
窄接口本身就是防御，重建比验证更可靠。

173
00:15:19,335 --> 00:15:23,145
第五条，两头记账，自报的数字不作数。

174
00:15:23,145 --> 00:15:27,832
凡是让不可信的一方上报消耗，都要在可信侧再记一遍。

175
00:15:27,832 --> 00:15:33,373
而且权限不因进沙箱而松动，子调用照走同一条审批管线。

176
00:15:33,373 --> 00:15:34,791
留一道练习。

177
00:15:34,791 --> 00:15:40,561
程序 A 是一个同步热循环，程序 B 是 await 一个永远不会兑现的承诺。

178
00:15:40,561 --> 00:15:49,443
对照第三道防线推演一下，A 和 B 分别被哪一本账终止，为什么 A 不能靠挂一个待完成的工具调用躲过计费。

179
00:15:49,443 --> 00:15:54,058
想明白这一题，双预算的设计就刻进脑子里了。

