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

2
00:00:06,382 --> 00:00:10,276
这是 DeepSeek Harness 系列的又一集，讲两件事。

3
00:00:10,276 --> 00:00:16,406
第一件是多步编排为什么被拆成四个原语，而不是一个统一的任务系统。

4
00:00:16,406 --> 00:00:22,319
第二件是发布文里那四种模式，为什么在源码里只是四份配置文件。

5
00:00:22,319 --> 00:00:26,286
先给结论，因为这一集的结论比过程重要。

6
00:00:26,286 --> 00:00:31,923
四个原语没有共享一个任务引擎，它们连持久化形态都不一样。

7
00:00:31,923 --> 00:00:37,716
workflow 管执行，schedule 管时间，plan 管协作姿态，todo 管进度展示。

8
00:00:37,716 --> 00:00:39,531
谁也不冒充谁。

9
00:00:39,531 --> 00:00:41,358
为什么值得关心。

10
00:00:41,358 --> 00:00:47,427
因为「要不要做一个统一的任务系统」是几乎每个智能体产品都会撞上的岔路口。

11
00:00:47,427 --> 00:00:51,514
统一了，进度界面好看，管理入口只有一个。

12
00:00:51,514 --> 00:00:55,360
拆开了，每个原语能把自己的语义说到底。

13
00:00:55,360 --> 00:01:00,625
这一集讲的就是这条岔路口上，选择和代价分别是什么。

14
00:01:00,625 --> 00:01:02,331
这一集分五块。

15
00:01:02,331 --> 00:01:18,850
先讲四个原语各自的边界，再讲三种容易踩错的误读，然后讲错过合并和待定切换这两处最有态度的实现，接着讲四种模式为什么只是四份配置，最后讲自我修改的三条纪律，收五条能带走的原则。

16
00:01:18,850 --> 00:01:21,926
先给一句能贯穿两块的线索。

17
00:01:21,926 --> 00:01:30,496
无论是拆四个原语，还是把模式做成配置，背后都是同一个判断，能被声明的东西就不要写进代码。

18
00:01:30,496 --> 00:01:35,520
执行逻辑千变万化，声明不完，所以交给模型写脚本。

19
00:01:35,520 --> 00:01:41,866
时间、姿态、展示需要确定性，声明得完，所以做成事件和快照。

20
00:01:41,866 --> 00:01:47,311
模式是插件组合，组合天然可以声明，所以只是四份文件。

21
00:01:47,311 --> 00:01:53,657
顺带提醒，本集的行号都是二零二六年八月十三日核对源码的结果。

22
00:01:53,812 --> 00:01:55,348
先看 workflow。

23
00:01:55,298 --> 00:01:56,957
它是一次性的。

24
00:01:56,957 --> 00:02:09,974
模型写一段脚本，引擎在一个隔离的运行环境里执行，脚本里想起子智能体就打回宿主去起，跑完只留下结果和展示记录，逻辑本身不落成持久状态机。

25
00:02:09,974 --> 00:02:17,209
这个选择回答了一个大纲级别的问题，多步编排应该是模型写脚本，还是框架状态机。

26
00:02:17,209 --> 00:02:21,055
这里的答案是两个都要，但分工明确。

27
00:02:21,055 --> 00:02:27,462
执行编排交给模型写脚本，因为编排逻辑千变万化，框架预设不完。

28
00:02:27,462 --> 00:02:36,572
时间、姿态、展示交给框架状态机，因为这三样需要跨轮次甚至跨重启的确定性，模型的脚本给不了。

29
00:02:36,572 --> 00:02:39,108
还有一条纪律特别值得记。

30
00:02:39,108 --> 00:02:46,873
脚本里拼错一个选项，抛的是致命错误，并行组合器对它直接重抛、终止整个脚本。

31
00:02:46,873 --> 00:02:52,329
只有子智能体真实的运行失败，才映射成逐项的结果为空。

32
00:02:52,329 --> 00:02:54,252
为什么要分这么清。

33
00:02:54,252 --> 00:03:00,166
因为写错代码和运行失败是两类错误，混在一起脚本就没法调了。

34
00:03:00,166 --> 00:03:05,526
一类是你的问题，一类是环境的问题，报错方式必须一眼可分。

35
00:03:05,526 --> 00:03:07,702
把这个原则推广一下。

36
00:03:07,702 --> 00:03:12,786
凡是混合了多种职责的执行体，错误分类都要提前设计好。

37
00:03:12,786 --> 00:03:19,000
因为调用方拿到错误之后要做的第一件事就是判断「我该重试还是该改代码」。

38
00:03:19,000 --> 00:03:28,195
如果这两类错误长得一样，调用方就只能盲目重试，把一次本该立刻修复的拼写错误变成一轮毫无意义的重跑。

39
00:03:28,195 --> 00:03:35,514
这里的选择也值得一说，为什么拼错选项要终止整个脚本，而不是跳过那一项继续跑。

40
00:03:35,514 --> 00:03:48,050
因为脚本里的每一步可能依赖上一步的结果，跳过一步之后，后面的步骤很可能在一个错误的前提上继续跑，最终产出一个看起来完成了、实际全错的结果。

41
00:03:48,050 --> 00:03:52,101
宁可整个终止，也不给一个被污染的结果。

42
00:03:52,252 --> 00:03:55,062
第二个原语是 schedule，管时间。

43
00:03:55,012 --> 00:04:03,029
它是持久的，创建、派发、删除都是会话事件，回放日志就能重建全部提醒状态。

44
00:04:03,029 --> 00:04:12,740
这里有一段特别值得整段看的代码，讲的是一个很常见的边界，会话离线错过了好几个到期时点，恢复之后怎么办。

45
00:04:12,740 --> 00:04:15,481
朴素的做法是逐个补发。

46
00:04:15,481 --> 00:04:22,825
这里的做法不是，它用一次除法直接算出最新一次到期时点，然后把记录推进到未来。

47
00:04:22,825 --> 00:04:25,625
不枚举、不回放、不积压。

48
00:04:25,625 --> 00:04:28,570
那段代码的核心就是三步。

49
00:04:28,570 --> 00:04:34,976
先算出跳过了几步，用当前时间减去目标时间再除以间隔，向下取整。

50
00:04:34,976 --> 00:04:40,589
然后用目标时间加上步数乘以间隔，得到这一次应该触发的时点。

51
00:04:40,589 --> 00:04:42,680
最后再算出下一次。

52
00:04:42,680 --> 00:04:47,164
出处是调度域源码第五百三十六到五百四十三行。

53
00:04:47,164 --> 00:04:50,084
顺带说一句它对自己的严格。

54
00:04:50,084 --> 00:04:59,243
算完之后还要检查这个数字是不是安全整数、有没有小于目标、有没有超过当前时间，任何一条不满足就抛错。

55
00:04:59,243 --> 00:05:00,998
为什么这么小心。

56
00:05:00,998 --> 00:05:09,087
因为这是一段纯算术，一旦溢出或者算错，提醒就会漂到很远的未来或者过去，而且很难被发现。

57
00:05:09,087 --> 00:05:12,825
在算术上加护栏，比事后调试便宜得多。

58
00:05:12,825 --> 00:05:15,865
再看「不补发」这个决定背后的取舍。

59
00:05:15,865 --> 00:05:20,745
逐个补发听起来更负责，实际上会制造一个更糟的问题。

60
00:05:20,745 --> 00:05:29,976
设想一个每小时提醒一次的任务，会话离线了五天，恢复瞬间如果逐个补发，会一次性涌进来一百二十条提醒。

61
00:05:29,976 --> 00:05:35,144
用户真正需要的不是这一百二十条历史，而是「现在该做什么」。

62
00:05:35,144 --> 00:05:40,349
所以合并成最新一次，不是偷懒，是对用户注意力的保护。

63
00:05:40,349 --> 00:05:49,267
这里能提炼出一条通用经验，凡是批量补发的场景，先问一句，积压的这些里面，有多少是用户现在还需要的。

64
00:05:49,267 --> 00:05:52,704
大多数时候答案是只有最后一条。

65
00:05:52,704 --> 00:05:54,531
再补两条边界。

66
00:05:54,531 --> 00:06:02,764
提醒只以追加轮次的形式回到原会话，没有推送，没有外部通知通道，冷会话不干活。

67
00:06:02,764 --> 00:06:09,399
交付语义是至少一次，如果在落派发之前崩溃，恢复后会重复一次提醒。

68
00:06:09,532 --> 00:06:12,655
第三个原语是 plan，管协作姿态。

69
00:06:12,605 --> 00:06:16,583
它最轻，就是一个布尔事件的日志折叠。

70
00:06:16,583 --> 00:06:19,900
这里必须先掐掉一个最常见的误读。

71
00:06:19,900 --> 00:06:21,823
plan mode 不是权限。

72
00:06:21,823 --> 00:06:28,975
它是软性指引，激活时往系统提示词里加一段策略说明，工具目录一个字不变。

73
00:06:28,975 --> 00:06:32,677
这个不变是有意的，为了请求缓存稳定。

74
00:06:32,677 --> 00:06:38,987
真正拦住写操作的是沙箱和审批，这两者都不读 plan 状态，要分别配。

75
00:06:38,987 --> 00:06:41,992
所以别以为开了 plan mode 就安全了。

76
00:06:41,992 --> 00:06:46,018
再看它的生效时机，这里有个很讲究的设计。

77
00:06:46,018 --> 00:06:55,621
用户在模型流式输出时点了切换，插件不立刻写日志，而是先挂在进程内存的待定里，等下一个轮内边界才动手。

78
00:06:55,621 --> 00:06:57,532
顺序讲究得很。

79
00:06:57,532 --> 00:07:05,249
监听器先问下游这一步收不收，下游拒绝、信号已取消、或者没有待定，都原样放行。

80
00:07:05,249 --> 00:07:08,698
三关都过了，才把选择追加进日志。

81
00:07:08,698 --> 00:07:16,944
追加万一失败，只记一条警告然后放行这一步，绝不因为一次姿态切换失败就阻塞整个轮次。

82
00:07:16,944 --> 00:07:19,203
这也回答了崩溃语义。

83
00:07:19,203 --> 00:07:26,270
待定只活在进程内存，切换还没落日志时崩溃，重启后维持切换前的状态。

84
00:07:26,270 --> 00:07:30,934
出处是 plan 模式源码第二百零五到二百一十八行。

85
00:07:31,084 --> 00:07:33,149
第四个原语是 todo。

86
00:07:33,099 --> 00:07:38,520
它是快照，每次写入整表替换，界面靠投影渲染最新一份。

87
00:07:38,520 --> 00:07:43,171
再说一遍那个最常见的误读，todo 不驱动执行。

88
00:07:43,171 --> 00:07:48,375
它是纯展示状态，整表替换、落日志、投影给界面。

89
00:07:48,375 --> 00:07:52,450
没有部分更新，没有回读工具，没有稳定 id。

90
00:07:52,450 --> 00:07:56,861
把它当任务引擎用，是对这个原语最常见的误读。

91
00:07:56,861 --> 00:08:00,130
这上面有个设计特别能看出脾气。

92
00:08:00,130 --> 00:08:06,500
有个配置项叫允许并行进行中，它是必填的，schema 里没有任何默认值。

93
00:08:06,500 --> 00:08:08,003
为什么强制。

94
00:08:08,003 --> 00:08:15,875
因为允不允许多个任务同时进行中，取决于这个部署跑不跑并发子智能体，工具自己观测不到。

95
00:08:15,875 --> 00:08:18,423
所以它强制部署方表态。

96
00:08:18,423 --> 00:08:25,707
设成否之后，模型多标一个进行中就吃一条错误，最多只能有一个任务处于进行中。

97
00:08:25,707 --> 00:08:31,957
出处是 todo 工具源码第四十一到四十三行和第一百零七到一百零九行。

98
00:08:31,957 --> 00:08:34,024
对比一下别家的做法。

99
00:08:34,024 --> 00:08:41,248
Claude Code 把同一条纪律写进了提示词，硬编码一句「任意时刻有且只有一个任务处于进行中」。

100
00:08:41,248 --> 00:08:48,411
这里是把它做成必填的部署配置，选了否就由代码拒绝，而不是靠提示词劝告。

101
00:08:48,411 --> 00:08:54,445
一个用提示词约束模型，一个用 schema 约束部署然后让代码执行。

102
00:08:54,445 --> 00:08:58,520
这两种路线没有绝对优劣，但代价不同。

103
00:08:58,520 --> 00:09:08,712
提示词路线的好处是改起来快，改一句话就能调整纪律，代价是模型可能不听，而且不听的时候你没有任何证据去追究。

104
00:09:08,712 --> 00:09:17,859
schema 路线的好处是确定性强，选了否就一定会被拒绝，代价是部署方必须先想清楚，没法含糊过去。

105
00:09:17,859 --> 00:09:22,522
顺带说一句为什么这里要强制必填、不给默认值。

106
00:09:22,522 --> 00:09:28,459
因为默认值就是替部署方做了一个决定，而这个决定是工具观测不到的。

107
00:09:28,459 --> 00:09:36,548
给了默认值，大多数部署方会一路回车用默认值，然后在某个场景下被这个默认决定坑到。

108
00:09:36,548 --> 00:09:41,260
强制表态，是把一个隐藏的决策推到明面上。

109
00:09:41,404 --> 00:09:44,094
把这个选择放到同行里看。

110
00:09:44,044 --> 00:09:51,340
Claude Code 走的是聚合路线，七种异步工作统一挂在一个任务框架下，共享一套生命周期。

111
00:09:51,340 --> 00:09:55,041
聚合换来统一的进度界面和管理入口。

112
00:09:55,041 --> 00:09:59,933
这里是相反的路线，四个原语各有各的持久化形态。

113
00:09:59,933 --> 00:10:04,032
拆分换来的是每个原语能把自己的语义说到底。

114
00:10:04,032 --> 00:10:12,950
前面讲的两处最能说明问题，schedule 的错过合并、plan 的待定切换，这两件事塞进统一框架里都得妥协。

115
00:10:12,950 --> 00:10:20,017
因为统一框架要求所有工作共享一套状态模型，而这两处语义偏偏都是特例。

116
00:10:20,017 --> 00:10:28,743
所以这道选择题的实质不是统一好还是拆分好，而是你要不要为统一的进度界面，牺牲每个原语的语义深度。

117
00:10:28,743 --> 00:10:32,445
顺带说一句 workflow 和别家思路的关系。

118
00:10:32,445 --> 00:10:39,477
它的元数据字段词汇和 Claude Code 的动态工作流是对齐的，思路同源，落点不同。

119
00:10:39,477 --> 00:10:45,823
对齐词汇表意味着两边写出来的东西能互相理解，这是很务实的选择。

120
00:10:45,988 --> 00:10:50,853
现在换到第二件事，发布文里那四种模式到底是什么。

121
00:10:50,803 --> 00:10:52,606
先解那个悬念。

122
00:10:52,606 --> 00:10:59,469
标准、代码、极简、创造，这四种模式在源码里找不到一行模式分支。

123
00:10:59,469 --> 00:11:08,892
配置目录底下就是四个子目录，每个目录一份配置文件，一份文件描述一种插件组合，给一个会话挂载。

124
00:11:08,892 --> 00:11:11,693
极简模式全文只有六十二行。

125
00:11:11,693 --> 00:11:19,505
人设一句话写死，加一个拒绝后续拼装的标记，工具只有持久终端和编辑器，连压缩都没有。

126
00:11:19,505 --> 00:11:22,162
这是拿来跑基准测试的配置。

127
00:11:22,162 --> 00:11:32,330
创造模式则是标准模式原封不动，多挂三样，自指工具集、一个教写组合的教学包、一段教模型分清两个平面的人设。

128
00:11:32,330 --> 00:11:36,585
两个平面是这套体系的坐标系，值得单独记。

129
00:11:36,585 --> 00:11:44,674
宿主平面放跨会话共享的东西，持久化、沙箱与审批、模型路由、子智能体注册表。

130
00:11:44,674 --> 00:11:51,801
预设平面放一个会话贡献给这些注册表的东西，它的工具、人设、提示词段落。

131
00:11:51,801 --> 00:11:54,926
有个细节能看出边界画得多细。

132
00:11:54,926 --> 00:11:59,722
预设里发布服务的行，要么归宿主，要么包进隔离领域。

133
00:11:59,722 --> 00:12:09,217
极简模式想用不带沙箱的本地文件系统，就把它包在自己的领域里，只遮蔽自己这个会话，别的会话照旧走沙箱。

134
00:12:09,217 --> 00:12:12,558
再看 skill 的分层，它也是同一个思路。

135
00:12:12,558 --> 00:12:16,933
全局层放部署级注册的，预设层放随预设走的。

136
00:12:16,933 --> 00:12:22,630
读取的时候，近层同名直接赢，排序权重只在同一层内起作用。

137
00:12:22,630 --> 00:12:24,938
实现上有个细节很巧。

138
00:12:24,938 --> 00:12:37,318
它把所有层排成一列，全局层排最前，预设作用域链按远祖先在前、本层最后依次跟上，然后按顺序把每一层的条目灌进同一个映射表，键是名字。

139
00:12:37,318 --> 00:12:47,883
映射表的天性就是后写的覆盖先写的，所以近层同名条目自动替换远层，遮蔽就是一次写入，没有任何跨层的权重比较。

140
00:12:47,883 --> 00:12:52,270
出处是 skill 源码第五百五十二到五百六十六行。

141
00:12:52,270 --> 00:12:56,212
为什么坚持遮蔽要干脆，不做跨层合并排序。

142
00:12:56,212 --> 00:13:00,731
因为一旦模型看到两个同名 skill，它就无所适从了。

143
00:13:00,731 --> 00:13:06,224
宁可让近层直接赢，也不要搞出一套谁也记不住的权重规则。

144
00:13:06,364 --> 00:13:12,299
最后是这一集最激进的一块，让智能体在运行时改写自己的运行时。

145
00:13:12,249 --> 00:13:13,956
流程是这样的。

146
00:13:13,956 --> 00:13:26,251
模型先用一个查询工具读清楚运行时的精确签名，然后调用定义工具，返回插件和包的标识，再调用运行工具，新版本激活，新工具进目录。

147
00:13:26,251 --> 00:13:29,941
用着不满意，就追加一个新版本再切过去。

148
00:13:29,941 --> 00:13:32,970
三条纪律让这件事有得后悔。

149
00:13:32,970 --> 00:13:35,314
第一条，定义不执行。

150
00:13:35,314 --> 00:13:43,607
定义工具只校验参数和语法，把源码记成一个不可变的包，不申请审批、不执行、不动当前指针。

151
00:13:43,607 --> 00:13:46,636
要让它跑起来，得再调一次运行。

152
00:13:46,636 --> 00:13:50,542
定义与激活分开，改坏了才有得回滚。

153
00:13:50,542 --> 00:13:53,163
第二条，失败不动指针。

154
00:13:53,163 --> 00:13:59,845
运行工具只在完全成功后才切换当前版本，启动失败时旧版本原地不动。

155
00:13:59,845 --> 00:14:02,706
第三条，旧版本永远保留。

156
00:14:02,706 --> 00:14:05,807
改版本是追加新包，不是覆盖旧的。

157
00:14:05,807 --> 00:14:08,679
回滚就是运行一个旧标识。

158
00:14:08,679 --> 00:14:11,360
然后是那条最重要的边界。

159
00:14:11,360 --> 00:14:18,331
创造模式的文件头原话是，把一个跑在这个预设上的会话当作 shell 权限来对待。

160
00:14:18,331 --> 00:14:27,045
模型写的脚本贴着活运行时跑，没有沙箱兜底，防线画在谁能用这个预设上，代码本身不设围栏。

161
00:14:27,045 --> 00:14:30,614
所以给谁开创造模式，等于给谁开 shell。

162
00:14:30,614 --> 00:14:34,028
这句话是这一集最该记住的一句。

163
00:14:34,028 --> 00:14:36,648
再补两条容易忽略的规则。

164
00:14:36,648 --> 00:14:48,126
其一，动态插件让工具集中途变了形状时，会话日志会记录变更后的完整请求头，维持「模型看到的就等于日志里的」这条不变量。

165
00:14:48,126 --> 00:14:54,761
这一点很关键，否则回放的时候工具目录和当时不一致，回放就不是回放了。

166
00:14:54,761 --> 00:14:59,977
其二，人设里明令智能体绝不许编辑发行版的预设目录。

167
00:14:59,977 --> 00:15:08,114
原因有两个，升级会整个覆盖它，而且改坏创造模式的预设，等于亲手关掉自己所在的模式。

168
00:15:08,114 --> 00:15:10,494
要改就复制出去改副本。

169
00:15:10,494 --> 00:15:17,213
这条规则的幽默之处在于，它是在教智能体不要锯断自己坐着的那根树枝。

170
00:15:17,356 --> 00:15:19,108
收五条原则。

171
00:15:19,058 --> 00:15:22,039
第一条，先问状态要活多久。

172
00:15:22,039 --> 00:15:29,876
活一次运行的用脚本式编排，活到会话重启之后的用持久事件，只是给人看的用快照。

173
00:15:29,876 --> 00:15:34,082
状态需要活多久，决定了它该用什么形态存。

174
00:15:34,082 --> 00:15:38,121
第二条，写错代码和运行失败要分开报。

175
00:15:38,121 --> 00:15:43,289
一类是你的问题，一类是环境的问题，混在一起就没法调。

176
00:15:43,289 --> 00:15:46,823
第三条，软性指引不要冒充权限。

177
00:15:46,823 --> 00:15:52,304
姿态只是姿态，真正拦住动作的是沙箱和审批，两者要分别配。

178
00:15:52,304 --> 00:15:55,080
别以为开了某个模式就安全了。

179
00:15:55,080 --> 00:15:59,527
第四条，能用 schema 约束的纪律，别写成提示词。

180
00:15:59,527 --> 00:16:03,962
提示词靠模型自觉，schema 加代码执行靠机制。

181
00:16:03,962 --> 00:16:08,998
强制必填、不给默认值，是让部署方表态的最好方式。

182
00:16:08,998 --> 00:16:12,003
第五条，自我修改要有得后悔。

183
00:16:12,003 --> 00:16:21,030
定义不执行、失败不动指针、旧版本永远保留，这三条缺一条，让智能体改写自己就是一场赌博。

184
00:16:21,030 --> 00:16:22,448
留一道练习。

185
00:16:22,448 --> 00:16:27,580
模型正在流式输出一大段方案，用户此刻点了进入 plan mode。

186
00:16:27,580 --> 00:16:32,676
这个选择什么时候真正写进日志，什么时候开始影响模型请求。

187
00:16:32,676 --> 00:16:37,268
如果这一轮结束前进程崩了，重启后是开还是关。

188
00:16:37,268 --> 00:16:40,873
提示是，待定只存在于进程内存。

189
00:16:40,873 --> 00:16:46,618
想清楚这一题，软性指引和硬状态的区别就刻进脑子里了。

