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

2
00:00:06,382 --> 00:00:09,146
这是 DeepSeek Harness 系列的第二集。

3
00:00:09,146 --> 00:00:17,932
这一集要解决的工程问题很实际，一个产品能被用户改到什么程度，以及改完之后还能不能说清哪一行是谁写的。

4
00:00:17,932 --> 00:00:19,759
为什么值得关心。

5
00:00:19,759 --> 00:00:23,401
大多数产品的可配置性停在两个极端。

6
00:00:23,401 --> 00:00:31,129
要么给一个大配置文件，所有人的改动搅在一起；要么只给几个开关，深层能力根本碰不到。

7
00:00:31,129 --> 00:00:36,069
前者升级变成手工比对，后者用户只能等官方发版。

8
00:00:36,069 --> 00:00:40,408
中间那条路有没有，取决于配置系统怎么分层。

9
00:00:40,408 --> 00:00:42,620
再补一个判断标准。

10
00:00:42,620 --> 00:00:45,685
你可以用三个问题给自己产品打分。

11
00:00:45,685 --> 00:00:53,377
第一个，用户想换掉一个深层能力，比如把压缩策略换成自己写的实现，需要读源码吗。

12
00:00:53,377 --> 00:00:59,158
第二个，升级发行版的时候，用户能一眼看出哪些改动会被覆盖吗。

13
00:00:59,158 --> 00:01:06,045
第三个，配置出问题的时候，用户能在不启动的情况下看到真正生效的那一份吗。

14
00:01:06,045 --> 00:01:10,913
三个问题都答不上来，说明可配置性只是表面上存在。

15
00:01:10,913 --> 00:01:15,901
这一集讲的这套系统，恰好对三个问题都给了明确答案。

16
00:01:15,901 --> 00:01:25,120
不用读源码，一条插入就能换实现；升级不会覆盖用户层，因为层是分开的；离线转储能看到真正生效的那一份。

17
00:01:25,120 --> 00:01:27,788
所以这一集的主线有两句。

18
00:01:27,788 --> 00:01:33,713
第一句是，配置由四层补丁对着一个空数组叠出来，晚应用的赢。

19
00:01:33,713 --> 00:01:38,930
第二句是，命中同一个条目时整条替换，不做字段合并。

20
00:01:38,930 --> 00:01:40,504
内容分三块。

21
00:01:40,504 --> 00:01:52,367
先讲三个名词和四层归属方，再讲替换语义为什么会造成一个反直觉结果，最后讲同一个算法既管挂载也管配置转储，带来的可预测性。

22
00:01:52,367 --> 00:01:55,600
末尾收五条可以带走的原则。

23
00:01:55,730 --> 00:02:00,571
先给结论，这套系统里没有一个大而全的配置文件。

24
00:02:00,521 --> 00:02:07,144
它的产品形态是由一摞一摞的补丁列表叠出来的，每一摞都有明确的归属方。

25
00:02:07,144 --> 00:02:10,545
第一个名词是捆绑包，英文叫 bundle。

26
00:02:10,545 --> 00:02:15,798
它是发行版默认，实体就是一个 npm 包里带的那份补丁列表。

27
00:02:15,798 --> 00:02:23,502
内置的有三个，基础包是共享核心，网页应用包是浏览器表层，无头包是一次性任务模式。

28
00:02:23,502 --> 00:02:26,855
第二个名词是组装档，英文叫 profile。

29
00:02:26,855 --> 00:02:32,120
它是用户的一套组装，是主目录下以名字命名的一个目录。

30
00:02:32,120 --> 00:02:38,706
清单里排好要用哪些捆绑包、按什么顺序，旁边放一份用户自己的补丁文件。

31
00:02:38,706 --> 00:02:43,899
首次使用网页或者无头这两个名字，会自动初始化模板。

32
00:02:43,899 --> 00:02:45,906
第三个名词是补丁。

33
00:02:45,906 --> 00:02:48,238
它是改动的最小单位。

34
00:02:48,238 --> 00:02:55,726
一条补丁按编号找到目标条目，可以做三件事，改配置、禁用、或者插入新条目。

35
00:02:55,726 --> 00:02:58,598
树外插件也是从这里进来的。

36
00:02:58,598 --> 00:03:06,555
用命令行装进组装档之后，它就是普通的一层补丁，和系统自带的能力走完全相同的通道。

37
00:03:06,555 --> 00:03:13,117
出处是 bundle 包说明文档，以及命令行里 profile 的第一百一十四到一百一十七行。

38
00:03:13,117 --> 00:03:15,581
三个名词的关系是这样。

39
00:03:15,581 --> 00:03:22,072
捆绑包负责发行版给什么，组装档负责你选什么，补丁负责你改哪一个字段。

40
00:03:22,072 --> 00:03:26,747
把它们分开，是为了让每一层都能回答归属问题。

41
00:03:26,880 --> 00:03:28,548
为什么说四层。

42
00:03:28,498 --> 00:03:34,664
因为改配置的人其实有四种角色，发行方、团队、个人、单次命令。

43
00:03:34,664 --> 00:03:40,205
四种角色的生命周期完全不同，写在同一个文件里必然打架。

44
00:03:40,205 --> 00:03:42,080
想象反面做法。

45
00:03:42,080 --> 00:03:49,772
一个大配置文件，发行版默认、这套组装的定制、个人偏好、临时实验全写在里面。

46
00:03:49,772 --> 00:03:59,640
三个月后升级发行版，新默认和你的旧改动搅在一起，你说不清哪一行是谁写的、哪一行能动，升级变成一场手工比对。

47
00:03:59,640 --> 00:04:01,623
还有更日常的翻车。

48
00:04:01,623 --> 00:04:09,412
想临时试一个实验模型，顺手改了配置忘了改回来，第二天整套环境跑的都是实验配置。

49
00:04:09,412 --> 00:04:17,020
根源是改动没有归属，谁写的、跟着什么走、什么时候该消失，一个文件说不清这三件事。

50
00:04:17,020 --> 00:04:19,243
所以四层是这样分的。

51
00:04:19,243 --> 00:04:28,811
捆绑包层跟着发行版走，组装档层跟着这套组装走，主目录层跟着这台机器走，命令行参数层跟着这一次命令走。

52
00:04:28,811 --> 00:04:35,650
启动时对着一个空数组，按固定顺序把四层补丁依次刷上去，晚应用的赢。

53
00:04:35,650 --> 00:04:37,789
起点真的是空数组。

54
00:04:37,789 --> 00:04:45,481
根配置文件的内容就是一对方括号，模板注释直接写着别改这个文件，去改补丁文件。

55
00:04:45,481 --> 00:04:54,003
所有实际内容都由补丁插入进来，所以最终插件树里的每一行配置，都能回答自己是从哪一层来的。

56
00:04:54,003 --> 00:04:58,054
出处是 profile-boot.ts 的第六十到六十四行。

57
00:04:58,054 --> 00:05:01,082
两层用户补丁的分工也讲究。

58
00:05:01,082 --> 00:05:08,534
主目录层是机器本地偏好，对每个组装档都生效，所以排在组装档层之后、压过它。

59
00:05:08,534 --> 00:05:13,042
两层改同一个编号时，赢家是主目录层那条。

60
00:05:13,042 --> 00:05:21,395
命令行参数层排最后，用来做不想写进文件的一次性实验，可以重复传多份，按命令行顺序应用。

61
00:05:21,395 --> 00:05:30,337
长会话里两个用户层的文件还被监听着，改一下保存，运行中的树就按同样的层序重组一次。

62
00:05:30,480 --> 00:05:35,550
这一段值得单独讲，因为它是整个设计里最漂亮的一处。

63
00:05:35,500 --> 00:05:39,045
层序在源码里就是一个数组字面量。

64
00:05:39,045 --> 00:05:45,740
启动器把组合好的组装档摊平成一个补丁数组，数组顺序就是应用顺序。

65
00:05:45,740 --> 00:05:53,745
这段函数短到能当金句看，它证明四层的先后被写死在一个数组里，没有任何条件分支。

66
00:05:53,745 --> 00:05:58,336
出处是 profile-boot.ts 的第一百二十一至一百二十九行。

67
00:05:58,336 --> 00:06:00,380
为什么说这处漂亮。

68
00:06:00,380 --> 00:06:04,238
因为优先级是配置系统里最容易长歪的地方。

69
00:06:04,238 --> 00:06:15,476
一旦允许按条件调整顺序，比如某个环境下主目录层提前，那么同一份配置在不同机器上的结果就会不同，而用户完全看不出原因。

70
00:06:15,476 --> 00:06:20,247
写死一个数组，等于放弃了灵活性，换来了确定性。

71
00:06:20,247 --> 00:06:26,581
同一摞补丁、同一个算法，挂载的结果和配置转储的结果不可能漂移。

72
00:06:26,581 --> 00:06:28,709
这一句后面还会展开。

73
00:06:28,709 --> 00:06:32,170
再补一句为什么这个结构长期成立。

74
00:06:32,170 --> 00:06:35,283
分层覆盖是配置系统的通则。

75
00:06:35,283 --> 00:06:42,855
样式表的层叠、系统服务的 drop-in 目录、编辑器里用户设置压过默认设置，全是同一个结构。

76
00:06:42,855 --> 00:06:53,108
只要一份产品同时被发行方、团队、个人、单次命令四种角色修改，层就必须分开，否则升级和回滚都无从下手。

77
00:06:53,108 --> 00:06:58,144
换个语言重写整个框架，这四层还是这四层。

78
00:06:58,300 --> 00:07:03,766
现在讲这集最容易踩的一处，也是和直觉冲突最厉害的一处。

79
00:07:03,716 --> 00:07:08,873
叠层还剩一个关键决定，两层补丁改到同一个条目时怎么办。

80
00:07:08,873 --> 00:07:17,154
直觉答案是深度合并，字段级合并，上层只写想改的字段，其余字段自动保留，写起来省事。

81
00:07:17,154 --> 00:07:19,341
省事的代价有两个。

82
00:07:19,341 --> 00:07:22,106
第一，合并表达不了删除。

83
00:07:22,106 --> 00:07:29,305
下层设了一个字段，上层想把它去掉，合并语义下没有这个动作，只能覆盖成别的值。

84
00:07:29,305 --> 00:07:40,507
第二，最终结果和任何一份文件的字面内容都对不上，排查配置时你得在脑子里把所有层的合并算法跑一遍，才知道现在生效的到底是什么。

85
00:07:40,507 --> 00:07:43,163
这套系统选了整条替换。

86
00:07:43,163 --> 00:07:50,050
补丁按编号命中目标条目后，把补丁里除编号之外的每个顶层键直接赋值过去。

87
00:07:50,050 --> 00:07:57,022
配置是一个顶层键，所以旧的配置对象被整个换掉，里面的字段一个都不保留。

88
00:07:57,022 --> 00:08:02,010
想只改一个字段，也得把要保留的字段重新写一遍。

89
00:08:02,010 --> 00:08:09,510
这是官方文档明说的已知限制，原话是组装档覆盖必须重述需要保留的组合包字段。

90
00:08:09,510 --> 00:08:14,437
出处是 vendor 下 index.ts 的第一百一十到一百二十四行。

91
00:08:14,437 --> 00:08:17,298
由此产生最反直觉的那一幕。

92
00:08:17,298 --> 00:08:26,661
主目录层只写了模型一个字段，下面两层设好的供应商和温度就被刷没了，而且没有任何警告，只有结果不对。

93
00:08:26,661 --> 00:08:28,512
这一条要反复讲。

94
00:08:28,512 --> 00:08:33,356
用户会以为自己只改了一个字段，实际上删掉了两个。

95
00:08:33,356 --> 00:08:40,111
排查的时候日志里什么都没有，因为从系统角度看这是一次完全合法的替换。

96
00:08:40,111 --> 00:08:41,986
为什么还要选它。

97
00:08:41,986 --> 00:08:46,877
因为反过来选合并，代价落在排查阶段而且会持续发生。

98
00:08:46,877 --> 00:08:55,567
合并语义下，每一次配置不符合预期，用户都要在脑子里跑一遍合并算法，这个成本是每次排查都付一次的。

99
00:08:55,567 --> 00:09:01,937
替换语义下，成本只发生在写的时候，而且是有形的，重述一遍字段而已。

100
00:09:01,937 --> 00:09:08,236
两类成本的性质不一样，一个是持续的无形成本，一个是一次性的有形成本。

101
00:09:08,236 --> 00:09:12,863
官方文档把这条明说是已知限制，态度也很值得学。

102
00:09:12,863 --> 00:09:17,755
不藏着，不包装成特性，直接写成一句必须重述。

103
00:09:17,755 --> 00:09:22,514
这比给一个看起来聪明、实际难排查的语义要诚实。

104
00:09:22,660 --> 00:09:26,456
顺着替换语义，有三条行为边界必须记。

105
00:09:26,406 --> 00:09:33,545
第一条，补丁指向不存在的编号只警告不报错，标准错误上打一行提示就跳过。

106
00:09:33,545 --> 00:09:37,824
所以编号写错了不会炸，只会静默没生效。

107
00:09:37,824 --> 00:09:43,521
这一条和替换语义凑在一起特别危险，你以为覆盖了，其实打空了。

108
00:09:43,521 --> 00:09:50,648
第二条，补丁文件内容为空或者只有注释会直接抛异常，因为解析结果不是列表。

109
00:09:50,648 --> 00:09:53,641
这条反而友好，至少会告诉你。

110
00:09:53,641 --> 00:09:59,759
第三条，想让某一层什么都不做，写一个空数组，而不是留空文件。

111
00:09:59,759 --> 00:10:03,028
再往下看替换语义的回报在哪。

112
00:10:03,028 --> 00:10:04,939
回报是可预测性。

113
00:10:04,939 --> 00:10:14,579
这个算法全库只有一份，挂载用它，离线合成配置转储也用它，转储出来的结果和真正启动的内容不可能漂移。

114
00:10:14,579 --> 00:10:25,648
算法的输入永远不被改动，结果永远是深拷贝，这样配置热重载时撤掉一条补丁才能真的还原，早先的值不会被烤进缓存。

115
00:10:25,648 --> 00:10:29,951
出处是同一个文件的第四十三到五十二行。

116
00:10:29,951 --> 00:10:39,014
配套还有一份所见即可改的地图，一份三千一百五十二行的生成文档，把每个可加载包的配置类型原样列出来。

117
00:10:39,014 --> 00:10:43,593
想知道某条补丁能写哪些键，查这份目录就行。

118
00:10:43,593 --> 00:10:48,870
一句话记住，转储里看到什么，改配置就能得到什么。

119
00:10:49,010 --> 00:10:52,433
把三家摆在一起看，差别很清楚。

120
00:10:52,383 --> 00:10:58,032
第一家的分层单位是插件条目，一条补丁换掉一整条配置。

121
00:10:58,032 --> 00:11:05,436
四层从捆绑包到命令行参数顺序固定，根配置是空数组，一切内容可溯源到某一层。

122
00:11:05,436 --> 00:11:13,224
换掉深层能力等于换掉一行条目，把压缩插件的配置整条替换，甚至插入一个第三方实现。

123
00:11:13,224 --> 00:11:23,729
第二家的分层对象是设置字段，排了五层来源，用户设置、项目设置、本地设置、标记设置、策略设置，越靠后越大。

124
00:11:23,729 --> 00:11:29,114
管理员的策略层永远压顶，还有一批字段只在受管来源生效。

125
00:11:29,114 --> 00:11:33,693
它改的是行为参数，插件树本身没有摆上配置台面。

126
00:11:33,693 --> 00:11:39,703
第三家走的是深度合并，递归合并表、替换数组、上层字段赢。

127
00:11:39,703 --> 00:11:45,977
层序是系统受管、受管、用户，依赖与设备管控层最后压顶。

128
00:11:45,977 --> 00:11:53,200
它选的就是刚才那个深度合并对照，改一个字段不用重述，但上层没法删掉下层的字段。

129
00:11:53,200 --> 00:11:55,063
对比焦点在两处。

130
00:11:55,063 --> 00:12:05,688
第一是合并语义，后两家都是字段级合并，写起来省事；第一家是条目级替换，写起来啰嗦，换来转储与文件字面一致的可预测性。

131
00:12:05,688 --> 00:12:20,127
第二是分层对象，那两家分的是设置字段，第一家分的是插件树本身，所以用户能改到的深度不一样，一条插入就能把第三方实现接进主循环，这在前两家的配置系统里没有对应物。

132
00:12:20,127 --> 00:12:23,649
也要说一句人家有的、这边没有的。

133
00:12:23,649 --> 00:12:31,449
第二家有管理员策略层和只在受管来源生效的锁定字段，这套系统目前没有等价机制。

134
00:12:31,449 --> 00:12:37,615
企业场景下这一条很重要，团队想锁死某个能力的时候，缺的就是这个。

135
00:12:37,615 --> 00:12:40,644
这一条缺口的形状值得说清楚。

136
00:12:40,644 --> 00:12:46,570
四层覆盖解决的是谁能最后说话，它没有解决谁能禁止别人说话。

137
00:12:46,570 --> 00:12:52,315
覆盖和锁定是两种不同的权力，前者是叠加的，后者是截断的。

138
00:12:52,315 --> 00:13:00,716
一个只支持覆盖的系统，天然做不到强制合规，因为最上层的命令行参数永远能盖掉下面所有层。

139
00:13:00,716 --> 00:13:04,887
要做锁定，得引入一个独立于层序的维度。

140
00:13:05,040 --> 00:13:09,641
把两条语义合起来手推一次，这是本集最实用的一段。

141
00:13:09,591 --> 00:13:18,052
组装档层写编号是对话模型，配置里三个字段，供应商是深度求索、模型是三点二、温度是零点二。

142
00:13:18,052 --> 00:13:23,113
主目录层写同一个编号，配置里只有一个字段，温度是零。

143
00:13:23,113 --> 00:13:27,716
按整条替换，最终条目的配置只剩温度是零。

144
00:13:27,716 --> 00:13:31,634
供应商和模型消失了，而且没有任何警告。

145
00:13:31,634 --> 00:13:35,240
这就是刚才那个反直觉结果的具体版本。

146
00:13:35,240 --> 00:13:38,665
问题二，把这两条补丁对调所在层。

147
00:13:38,665 --> 00:13:49,399
结果变成主目录层那条被组装档层覆盖，最终配置是供应商、模型、温度三点齐全，而用户想改的温度零反而没生效。

148
00:13:49,399 --> 00:13:53,894
对调之后失败方向变了，从丢字段变成改不动。

149
00:13:53,894 --> 00:13:57,343
问题三，怎么在不启动的情况下验证。

150
00:13:57,343 --> 00:13:59,423
答案是配置转储。

151
00:13:59,423 --> 00:14:07,860
因为同一个算法既管挂载也管离线合成，转储出来的就是真正生效的那一份，不用真的跑一遍。

152
00:14:08,020 --> 00:14:09,652
最后收五条。

153
00:14:09,602 --> 00:14:12,823
第一，先分归属再分优先级。

154
00:14:12,823 --> 00:14:21,790
改配置的人有发行方、团队、个人、单次命令四种角色，生命周期不同，写在同一个文件里必然打架。

155
00:14:21,790 --> 00:14:25,660
四层各自归位，升级和回滚才有着手点。

156
00:14:25,660 --> 00:14:29,686
第二，优先级顺序要写死，不要留条件分支。

157
00:14:29,686 --> 00:14:38,364
一个数组字面量胜过一堆环境判断，因为同一份配置在不同机器上结果不同，是最难排查的一类问题。

158
00:14:38,364 --> 00:14:42,295
第三，起点用空数组，一切内容可溯源。

159
00:14:42,295 --> 00:14:47,006
每一行配置都能回答自己从哪层来，排查时不用猜。

160
00:14:47,006 --> 00:14:51,429
第四，替换语义的代价是重述，回报是可预测。

161
00:14:51,429 --> 00:14:57,295
命中同一编号整条替换，想保留的字段必须重写，而且没有警告。

162
00:14:57,295 --> 00:15:03,737
写补丁的时候养成习惯，复制下面那层的完整内容再改，不要只写差量。

163
00:15:03,737 --> 00:15:06,838
第五，转储是唯一的验金石。

164
00:15:06,838 --> 00:15:13,941
同一个算法既管挂载也管配置转储，所以离线能验证的东西不要靠启动来验证。

165
00:15:13,941 --> 00:15:17,727
改完补丁先转储一次，看字面结果对不对。

166
00:15:17,727 --> 00:15:28,364
一句话收尾，可配置性的上限不是开关有多少，而是用户能不能在不读源码的前提下，说清现在生效的是哪一行、它从哪来。

