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

2
00:00:06,382 --> 00:00:10,300
这是 DeepSeek Harness 系列的又一集，讲子 Agent。

3
00:00:10,300 --> 00:00:17,740
但讲的不是子 Agent 怎么用，而是一个更前置的问题，子 Agent 这个概念，应该在哪一层被定义。

4
00:00:17,740 --> 00:00:23,858
先说结论，这里没有做一个单独的子 Agent 功能，它做的是一张注册表。

5
00:00:23,858 --> 00:00:30,576
任何实现了约定接口的传输层，都可以按名字注册进来，成为子 Agent 的一种实现。

6
00:00:30,576 --> 00:00:33,016
官方发行版注册了六个。

7
00:00:33,016 --> 00:00:35,108
为什么这个区别重要。

8
00:00:35,108 --> 00:00:47,632
因为一旦把它做成功能，每一种新形态都要求你改功能本身，进程内新开是一种，带上下文复制是一种，委派给别家产品又是一种，功能会越做越臃肿。

9
00:00:47,632 --> 00:00:53,750
做成接缝之后，差异全部沉在接缝之下，接缝之上只有一个词汇表。

10
00:00:53,750 --> 00:00:55,528
这一集分四块。

11
00:00:55,528 --> 00:01:11,866
先讲这张注册表和它注册的六个实现，再讲输入和输出各是什么，然后讲带上下文这件事为什么被做成独立实现，接着讲能力门闩为什么选择在启动前报错，最后横向对比三家，收五条能带走的原则。

12
00:01:11,866 --> 00:01:14,955
先给一句能贯穿全篇的线索。

13
00:01:14,955 --> 00:01:19,378
这一集真正的主角不是子 Agent，而是「接缝」这个词。

14
00:01:19,378 --> 00:01:32,756
你会发现后面讲的每一处设计，能力门闩、能力表、统一的输入输出，全都是在为同一件事服务，让接缝之上只有一个词汇表，把差异全部压到接缝之下。

15
00:01:32,756 --> 00:01:40,460
顺带提醒，本集的行号都是二零二六年八月十三日核对源码的结果，别把行号当结论背。

16
00:01:40,612 --> 00:01:43,194
先看清这张表上都有谁。

17
00:01:43,144 --> 00:01:47,002
第一个叫 spawn，进程内新开一个子智能体。

18
00:01:47,002 --> 00:01:51,461
第二个叫 fork，进程内新开，但带上父级的上下文。

19
00:01:51,461 --> 00:01:53,925
第三个叫 acp，协议桥。

20
00:01:53,925 --> 00:02:01,076
第四个和第五个分别是 codex 和 claude-code，这两个会各起一个真实的产品命令行进程。

21
00:02:01,076 --> 00:02:04,490
第六个叫 sdk，对接一个远程的实例。

22
00:02:04,490 --> 00:02:07,795
出处是子系统文档的第五到七行。

23
00:02:07,795 --> 00:02:12,639
请把这张表读两遍，因为它本身就把这一集的核心讲完了。

24
00:02:12,639 --> 00:02:22,723
进程内新开、带上下文复制、委派给别家产品、连远程实例，四种看起来完全不同的东西，在这张表上是平级的六行。

25
00:02:22,723 --> 00:02:26,185
父智能体不需要知道它面对的是哪一种。

26
00:02:26,185 --> 00:02:35,055
有个特别值得玩味的地方，委派给 Claude Code 这件事，在这套系统里不是什么集成项目，它只是注册表里的两行。

27
00:02:35,055 --> 00:02:38,733
这就是为什么标题说子 Agent 是一个接缝。

28
00:02:38,733 --> 00:02:43,468
再把「委派给别家产品」这件事的成本单独拎出来看一看。

29
00:02:43,468 --> 00:02:54,310
在很多系统里，要让自家的智能体把任务交给另一个产品，是一场集成项目，要写适配器、要对齐协议、要处理两边的生命周期差异。

30
00:02:54,310 --> 00:03:09,446
而在这里，它是注册表里的一行，实现只有几十行，做的事无非是解析对方的可执行文件，在父会话的工作目录里通过官方接口起一个真实进程，然后把它挂到共享的子进程管理者之下。

31
00:03:09,446 --> 00:03:12,378
当然，成本低不等于没有代价。

32
00:03:12,378 --> 00:03:19,902
把任务交给别家产品，意味着你要接受对方的能力边界、对方的权限模型、对方的输出格式。

33
00:03:19,902 --> 00:03:24,145
这些差异不会消失，只是被推到了接缝的另一侧。

34
00:03:24,145 --> 00:03:30,828
接缝能做的是让你不必为每个差异改一次主干，而不是让差异本身归零。

35
00:03:30,964 --> 00:03:33,161
看清楚接口的两头。

36
00:03:33,111 --> 00:03:42,775
输入是工具层把模型的委派请求组装成一个启动请求，带上提示词、父智能体、取消信号，外加四个可选项。

37
00:03:42,775 --> 00:03:49,169
四个可选项分别是结构化输出 schema、深度上限、工具过滤、换人设。

38
00:03:49,169 --> 00:03:50,491
发生什么。

39
00:03:50,491 --> 00:03:59,638
服务先查所选实现的静态能力表，四个可选项每一项都要有对应的能力标记，缺一项就在启动之前抛错。

40
00:03:59,638 --> 00:04:01,152
输出是什么。

41
00:04:01,152 --> 00:04:07,390
一个运行句柄，父智能体等它的结果，最后落成一条普通的工具结果。

42
00:04:07,390 --> 00:04:09,770
请注意最后半句的分量。

43
00:04:09,770 --> 00:04:15,635
对父智能体来说，几种实现回来的都是同一种东西，一条工具结果。

44
00:04:15,635 --> 00:04:20,275
正因为出口统一，上游才完全不需要关心下游是谁。

45
00:04:20,275 --> 00:04:24,854
这里能提炼出一个判断接缝好坏的标准，看两端。

46
00:04:24,854 --> 00:04:29,145
入口统一，出口统一，中间的实现随便换。

47
00:04:29,145 --> 00:04:35,659
凡是做不到两端统一的抽象，都只是把复杂度挪了个位置，没有真正消除它。

48
00:04:35,659 --> 00:04:40,010
再看那四个可选项，它们本身也藏着设计取舍。

49
00:04:40,010 --> 00:04:44,121
为什么是四个，不是把所有配置都塞进请求里。

50
00:04:44,121 --> 00:04:48,712
因为这四个恰好是「不同实现之间真正会有差别」的维度。

51
00:04:48,712 --> 00:04:55,731
结构化输出不是谁都给得了，深度限制不是谁都做得到，工具过滤和换人设同理。

52
00:04:55,731 --> 00:05:04,181
把差异维度收敛到四个，再为每个维度配一个能力标记，能力表就变成了一张可以逐项校验的清单。

53
00:05:04,181 --> 00:05:14,205
反过来说，如果一个请求对象里有二十个字段，其中十八个在某些实现上会被静默忽略，那这张表就失去了意义。

54
00:05:14,205 --> 00:05:19,409
字段少而每一项都可校验，是能力表能成立的前提。

55
00:05:19,564 --> 00:05:23,456
接下来是这一集我最想让你记住的一个细节。

56
00:05:23,406 --> 00:05:27,805
很多框架把「带不带父上下文」做成一个布尔参数。

57
00:05:27,805 --> 00:05:31,495
这里没有，它把 fork 做成了一个独立实现。

58
00:05:31,495 --> 00:05:32,637
为什么。

59
00:05:32,637 --> 00:05:35,389
因为它俩差的不只是一个开关。

60
00:05:35,389 --> 00:05:43,418
fork 要从父日志里切出一段「已完成轮次的平衡前缀」当种子，切到最后一个轮次结束事件为止。

61
00:05:43,418 --> 00:05:47,877
进行中的轮次是不平衡的，回放不了，必须排除。

62
00:05:47,877 --> 00:05:53,153
这是会话日志层面的约定，塞进一个布尔参数里说不清楚。

63
00:05:53,153 --> 00:05:57,024
那段种子函数只有七行，值得整段看。

64
00:05:57,024 --> 00:06:08,754
它先找出日志里最后一个轮次结束事件，找不到就返回空；找到就从头切到那一条为止，因为序号等于数组下标，这是仅追加契约的直接推论。

65
00:06:08,754 --> 00:06:11,771
顺带点破一个容易误读的字段。

66
00:06:11,771 --> 00:06:22,156
fork 的说明里有个「继承父上下文」的描述性字段，它只是个标签，供工具层生成不骗人的措辞，真正的差别在种子怎么切。

67
00:06:22,156 --> 00:06:24,115
别把描述当实现。

68
00:06:24,115 --> 00:06:32,480
这给我们的教训是，当一个布尔参数开始需要长篇注释来解释的时候，它多半该被拆成两个东西了。

69
00:06:32,480 --> 00:06:36,675
再补一层为什么「平衡前缀」这个概念这么关键。

70
00:06:36,675 --> 00:06:43,141
会话日志是仅追加的，一轮对话从开始到结束构成一个平衡的单元。

71
00:06:43,141 --> 00:06:51,795
如果你在半路切一刀，切出来的种子是一段进行中的对话，将来回放的时候会卡在一个没有出口的状态里。

72
00:06:51,795 --> 00:06:57,925
所以种子必须切在轮次边界上，这不是洁癖，是回放能不能成立的前提。

73
00:06:57,925 --> 00:07:03,081
这也解释了为什么 fork 必须是一个独立实现而不是一个参数。

74
00:07:03,081 --> 00:07:10,076
切种子这件事涉及对日志结构的理解，它属于会话层，不属于「要不要」的开关层。

75
00:07:10,076 --> 00:07:15,473
把跨层的语义塞进参数，短期省事，长期一定说不清。

76
00:07:15,604 --> 00:07:19,760
现在讲能力门闩，这是这一集最实用的一块。

77
00:07:19,710 --> 00:07:21,261
逻辑不复杂。

78
00:07:21,261 --> 00:07:34,710
请求里的四个可选项被排成一张需求清单，请求带了结构化输出，就要求实现的能力表里这一项为真；带了深度上限就要求有深度限制能力；工具过滤和换人设同理。

79
00:07:34,710 --> 00:07:38,388
然后逐项对照，第一个对不上的当场抛错。

80
00:07:38,388 --> 00:07:45,287
错误文案直说是哪个实现不支持哪个能力，错误码是 UNSUPPORTED_CAPABILITY。

81
00:07:45,287 --> 00:07:55,299
出处是子智能体主源码第四百八十一到四百九十五行，演示里那句报错文案是逐字复刻自第四百九十到四百九十三行。

82
00:07:55,299 --> 00:07:58,304
关键在于报错的时机，启动之前。

83
00:07:58,304 --> 00:08:04,013
没有降级，没有警告后继续，子进程在这之前一个都不会启动。

84
00:08:04,013 --> 00:08:05,840
举个具体的例子。

85
00:08:05,840 --> 00:08:12,258
部署启用了委派给 Claude Code 的工具，模型发起委派时带上了结构化输出的要求。

86
00:08:12,258 --> 00:08:21,681
因为 Claude Code 那一档的能力表是空的，四个能力一项都不支持，所以请求当场被拒，它的命令行进程压根没有被启动过。

87
00:08:21,681 --> 00:08:26,104
这里的设计取向有个名字，叫 fail loud，响亮地失败。

88
00:08:26,104 --> 00:08:30,071
它的对立面是接受请求然后静默忽略。

89
00:08:30,220 --> 00:08:37,898
值得专门花一段讲，为什么这里选择当场报错，而不是接受请求、忽略掉不支持的那一项。

90
00:08:37,848 --> 00:08:39,807
想象另一种做法。

91
00:08:39,807 --> 00:08:47,127
请求带结构化输出，实现不支持，那就忽略这个 schema，照常跑，返回一段普通文本。

92
00:08:47,127 --> 00:08:49,531
父智能体会拿到什么。

93
00:08:49,531 --> 00:08:54,254
它以为自己会拿到一个可以解析的结构，结果拿到一段 prose。

94
00:08:54,254 --> 00:08:55,949
后果有两层。

95
00:08:55,949 --> 00:09:00,516
第一层是直接的，解析失败或者解析出错误的结果。

96
00:09:00,516 --> 00:09:12,415
第二层更麻烦，这个失败发生在很后面，等父智能体发现的时候，子智能体已经跑完了，时间和 token 都花掉了，而你要从一段文本反推为什么结构不对。

97
00:09:12,415 --> 00:09:15,168
当场报错的排查成本是多少。

98
00:09:15,168 --> 00:09:20,144
一行错误信息，写着哪个实现不支持哪个能力，完事。

99
00:09:20,144 --> 00:09:26,610
这里能提炼出一条通用的取舍原则，能力不支持这件事，越早暴露越便宜。

100
00:09:26,610 --> 00:09:32,439
把它推迟到运行之后，你就把一个明确的错误换成了一个模糊的结果。

101
00:09:32,439 --> 00:09:37,812
模糊的结果是最难排查的一类 bug，因为它看起来像是成功了。

102
00:09:37,812 --> 00:09:44,531
再补一句，不接受后降级还有个隐藏好处，它让能力表变成了一份可信的合同。

103
00:09:44,531 --> 00:09:51,357
如果请求会被静默降级，那能力表写了什么就不重要了，反正不支持也能跑。

104
00:09:51,357 --> 00:09:56,874
只有当不支持就真的跑不了，这张表才值得被调用方信任。

105
00:09:57,028 --> 00:10:00,175
再看生命周期，这里有两条路。

106
00:10:00,125 --> 00:10:02,384
第一条叫一次性运行。

107
00:10:02,384 --> 00:10:06,158
等结果、释放、结束，一锤子买卖。

108
00:10:06,158 --> 00:10:10,149
模型发起一次委派，拿到一条结果，完事。

109
00:10:10,149 --> 00:10:12,601
第二条叫可继续的激活。

110
00:10:12,601 --> 00:10:18,706
它没有运行这个概念，而是一份持久会话，加上至多一个驻留的激活。

111
00:10:18,706 --> 00:10:23,899
父级用发消息来追加轮次，用打断来中断，然后收汇报。

112
00:10:23,899 --> 00:10:31,074
后者的好处是能在多轮里持续推进一个子任务，代价是状态管理复杂得多。

113
00:10:31,074 --> 00:10:32,973
还有个细节值得记。

114
00:10:32,973 --> 00:10:40,341
可继续子智能体结算的时候，管理器会无条件给父级投一条结算通知，带上最终输出。

115
00:10:40,341 --> 00:10:49,728
这条通知和子智能体主动汇报用的是不同的消息来源类型，这样日志不会把运行时的记账算成子智能体说的话。

116
00:10:49,728 --> 00:10:51,339
为什么要区分。

117
00:10:51,339 --> 00:10:59,872
因为一旦混在一起，回放和展示就会把系统的记账当成对话内容，模型看到的历史就不再干净。

118
00:10:59,872 --> 00:11:03,610
又一次看到那个老原则，账本各记各的。

119
00:11:03,610 --> 00:11:07,120
再把两种生命周期的选择标准说清楚。

120
00:11:07,120 --> 00:11:10,293
什么时候用一次性，什么时候用可继续。

121
00:11:10,293 --> 00:11:15,125
判断依据只有一条，这个子任务能不能在一轮里说完。

122
00:11:15,125 --> 00:11:20,341
能说完的用一次性，父智能体拿到结果就走，没有状态要管。

123
00:11:20,341 --> 00:11:26,110
需要多轮推进、中途还想插话或者纠偏的，才值得上可继续。

124
00:11:26,110 --> 00:11:28,803
可继续的代价要心里有数。

125
00:11:28,803 --> 00:11:36,375
它要维护一份持久会话，要处理驻留激活的生命周期，要区分主动汇报和结算通知。

126
00:11:36,375 --> 00:11:41,723
这些复杂度是实打实的，不要因为听起来更灵活就默认选它。

127
00:11:41,723 --> 00:11:45,425
能用一次性解决的，别上可继续。

128
00:11:45,580 --> 00:11:49,929
放到同行里看，这三家的抽象层次完全不同。

129
00:11:49,879 --> 00:11:52,583
Claude Code 把委派做在产品层。

130
00:11:52,583 --> 00:12:00,191
一个入口，参数里塞进各种形态，挑角色、后台运行、开隔离副本、换模型。

131
00:12:00,191 --> 00:12:03,977
往上还有协调者模式和团队模式两套。

132
00:12:03,977 --> 00:12:08,869
表达力很强，但每种形态都是这一个产品里的功能分支。

133
00:12:08,869 --> 00:12:16,766
委派对象永远是另一个 Claude Code 实例，把任务交给另一个产品这件事，在这个结构里没有位置。

134
00:12:16,766 --> 00:12:26,814
Grok Build 走的是第三条路，它把子 Agent 的配置解析抽成一个纯逻辑库，按优先级解析出生效配置，执行仍在自家进程内。

135
00:12:26,814 --> 00:12:29,602
它抽象的是子 Agent 长什么样。

136
00:12:29,602 --> 00:12:32,703
而这里抽象的是子 Agent 跑在哪。

137
00:12:32,703 --> 00:12:35,828
这两句话值得放在一起念三遍。

138
00:12:35,828 --> 00:12:40,455
抽象「长什么样」，你能换角色、换人设、换深度。

139
00:12:40,455 --> 00:12:45,059
抽象「跑在哪」，你能换进程、换产品、换机器。

140
00:12:45,059 --> 00:12:49,879
两者不冲突，但抽象层次不同，能替换的东西就不同。

141
00:12:49,879 --> 00:12:54,746
顺带说一句委派别家产品在这套系统里的实际成本。

142
00:12:54,746 --> 00:13:07,571
发行版的四个预设里，委派给 codex 和 claude-code 的工具行都带着 disabled 出厂，注释写明，复制一份预设、删掉这个标记，就能只对复制版的会话开放这个产品后端。

143
00:13:07,571 --> 00:13:09,482
一个配置开关的事。

144
00:13:09,482 --> 00:13:12,306
最后补一句这套设计的边界。

145
00:13:12,306 --> 00:13:20,251
它让委派别家产品变得很便宜，但它没有解决另一个更难的问题，不同产品之间的行为一致性。

146
00:13:20,251 --> 00:13:29,037
你在自家系统里定义的权限、审计、回放，跨到别家产品那一侧之后还成不成立，取决于对方提供什么。

147
00:13:29,037 --> 00:13:34,109
接缝能统一的是调用方式，统一不了的是对面的实现质量。

148
00:13:34,109 --> 00:13:41,742
所以委派别家产品这件事，成本从来不在接线，而在你愿不愿意接受对面的那套语义。

149
00:13:41,884 --> 00:13:43,636
收五条原则。

150
00:13:43,586 --> 00:13:46,928
第一条，先问接缝定义在哪一层。

151
00:13:46,928 --> 00:13:53,442
凡是你要支持多种实现的场景，先把接口的两端钉死，再让实现去自由发挥。

152
00:13:53,442 --> 00:13:56,387
定义错层，后面全是补丁。

153
00:13:56,387 --> 00:13:59,572
第二条，入口统一，出口统一。

154
00:13:59,572 --> 00:14:03,995
判断一个抽象好不好，看它的两端是否都收敛。

155
00:14:03,995 --> 00:14:08,286
只有一端收敛的抽象，只是把复杂度挪了个位置。

156
00:14:08,286 --> 00:14:13,358
第三条，能力不支持就当场报错，绝不接受后降级。

157
00:14:13,358 --> 00:14:20,858
越早暴露越便宜，而且只有当不支持就真的跑不了，你的能力表才是一份可信的合同。

158
00:14:20,858 --> 00:14:27,276
第四种，当一个布尔参数需要长篇注释来解释，就该拆成两个东西了。

159
00:14:27,276 --> 00:14:31,796
fork 和 spawn 的差别在种子怎么切，不在开关打不打。

160
00:14:31,796 --> 00:14:37,036
第五条，一次性与可继续是两种生命周期，别混着设计。

161
00:14:37,036 --> 00:14:44,308
可继续的那条路要额外处理结算通知与消息来源，让账本和对话各走各的通道。

162
00:14:44,308 --> 00:14:45,726
留一道练习。

163
00:14:45,726 --> 00:14:52,144
部署启用了委派给 Claude Code 的工具，模型发起委派时带上了结构化输出的要求。

164
00:14:52,144 --> 00:14:59,872
请按能力门闩推演，错误在哪一层抛出、错误码是什么、Claude Code 的命令行进程有没有被启动过。

165
00:14:59,872 --> 00:15:09,933
再想一想，如果选择接受请求但忽略 schema，父智能体拿到的工具结果会出什么问题，为什么那比当场报错更难排查。

