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

2
00:00:06,382 --> 00:00:12,271
这一集解两个工程问题，它们都出自同一个真实仓库，OpenAI 的 Codex。

3
00:00:12,271 --> 00:00:20,673
第一个是，一个三十多万行、一百三十五个成员的大仓库，你要加一个小功能，这个文件应该落在哪。

4
00:00:20,673 --> 00:00:26,911
第二个是，界面上看到的一轮对话，内部到底转了几圈，谁有资格说停。

5
00:00:26,911 --> 00:00:29,543
先说第一个为什么值得关心。

6
00:00:29,543 --> 00:00:33,040
因为几乎所有团队的默认答案都是错的。

7
00:00:33,040 --> 00:00:43,413
打开仓库，看到核心目录里有会话、有上下文、有工具、有守卫，新文件放进去最省事，依赖表不用改，调用链不用跨包。

8
00:00:43,413 --> 00:00:46,814
这个省事会在半年后变成高利贷。

9
00:00:46,814 --> 00:00:54,663
你会听到一个具体数字，往核心加一行，二十五个直接下游要重编，界面层还会被间接带上。

10
00:00:54,663 --> 00:00:59,254
第二个问题值得关心，是因为它决定插话能不能被接住。

11
00:00:59,254 --> 00:01:07,896
你让 Agent 改一个函数，它中途调了工具，你补一句测试用 pytest，它写完答案之后停止钩子说还没跑 linter。

12
00:01:07,896 --> 00:01:17,283
这几件事如果在同一个循环里抢出口，就变成几个布尔互相打架，谁先检查、谁能打断谁，全靠口头约定。

13
00:01:17,283 --> 00:01:20,781
上半场讲落点，下半场讲循环。

14
00:01:20,781 --> 00:01:24,831
两件事的共同点是一句话，结构决定成本。

15
00:01:24,831 --> 00:01:28,461
你把代码放在哪，决定以后谁为它付账。

16
00:01:28,461 --> 00:01:32,716
你把循环切成几层，决定插话有没有干净的插口。

17
00:01:32,716 --> 00:01:36,526
还有一点先交代清楚，这一集不是 Rust 语言课。

18
00:01:36,526 --> 00:01:46,382
所有这些做法换个语言照样成立，因为底层是编译单元之间的依赖边，是循环之间的控制权归属，跟语法没关系。

19
00:01:46,516 --> 00:01:48,629
设想一个真实需求。

20
00:01:48,579 --> 00:01:56,428
每轮对话开头，把当前 git 分支名写进模型上下文，让模型知道自己在哪个分支上干活。

21
00:01:56,428 --> 00:01:58,471
你的第一反应是什么。

22
00:01:58,471 --> 00:02:03,903
打开核心目录，加一个文件，在会话初始化时调用一下。

23
00:02:03,903 --> 00:02:08,855
分支名看起来就该跟会话待在一起，而会话在核心里。

24
00:02:08,855 --> 00:02:12,858
依赖表不用动，编译一次就过，十分钟搞定。

25
00:02:12,858 --> 00:02:16,391
一周之后，同样的理由又进来三条。

26
00:02:16,391 --> 00:02:24,048
一个截断工具输出的辅助函数，一个模型供应商的适配层，一个会话恢复的边角逻辑。

27
00:02:24,048 --> 00:02:28,098
核心的入口文件顶部又多三个模块声明。

28
00:02:28,098 --> 00:02:31,752
每一次都很省事，每一次都有充分理由。

29
00:02:31,752 --> 00:02:33,843
麻烦在半年后兑现。

30
00:02:33,843 --> 00:02:39,012
有人想单独复用那个上下文片段的类型，发现它焊在核心里。

31
00:02:39,012 --> 00:02:45,514
要用它，就得把整个核心拉进来，包括沙箱、远程工具协议、安全守卫。

32
00:02:45,514 --> 00:02:50,190
一个只想拿到一小段文本的人，被迫拖走一台收割机。

33
00:02:50,190 --> 00:02:53,206
这就是核心包的典型失败模式。

34
00:02:53,206 --> 00:02:57,930
不是某一次设计错了，而是每一次都很小、都先放核心。

35
00:02:57,930 --> 00:03:03,062
单个决定全都合理，加总起来就变成谁都不敢碰的上帝包。

36
00:03:03,062 --> 00:03:08,266
所以真正要防的不是那一次加代码，而是那个默认落点。

37
00:03:08,266 --> 00:03:10,382
再补一句更狠的。

38
00:03:10,382 --> 00:03:16,584
上帝包一旦形成，你就失去了一张很重要的牌，那就是单独演进的可能。

39
00:03:16,584 --> 00:03:21,488
想给某个能力换实现，得先说服整个核心跟你一起动。

40
00:03:21,488 --> 00:03:25,598
而这笔账，当初加那三行的人并没有付。

41
00:03:25,756 --> 00:03:33,013
Codex 的做法很朴素，把禁令写进仓库的 AGENTS 文件，加粗英文，抵制往核心加代码。

42
00:03:32,963 --> 00:03:34,670
旁边跟着三问。

43
00:03:34,670 --> 00:03:38,877
第一问，现有的非核心包能不能住下这个新概念。

44
00:03:38,877 --> 00:03:41,088
能住就住，别新建。

45
00:03:41,088 --> 00:03:44,910
第二问，住不下，该不该新建一个工作区包。

46
00:03:44,910 --> 00:03:48,564
该建就建，并且允许为此重构旧代码。

47
00:03:48,564 --> 00:03:56,954
这个允许很关键，很多团队不敢新建包，是怕背上重构的债，这里明确说，重构是被许可的代价。

48
00:03:56,954 --> 00:04:01,341
第三问，非要进核心，你得说清为什么拆不出去。

49
00:04:01,341 --> 00:04:04,165
这是最后一档，不是默认档。

50
00:04:04,165 --> 00:04:06,317
配套的是评审纪律。

51
00:04:06,317 --> 00:04:11,293
评审遇到往核心堆功能的合并请求，被要求主动挡回去。

52
00:04:11,293 --> 00:04:17,831
这条禁令写进文件的日期是二零二六年三月二十六日，那时核心已经是最大的包。

53
00:04:17,831 --> 00:04:24,970
立完之后，工作区成员从七十五个长到一百三十五个，核心的生产代码却还在涨。

54
00:04:24,970 --> 00:04:29,261
建议你把这句话记下来，它标出了禁令的真实边界。

55
00:04:29,261 --> 00:04:33,288
它挡得住默认往核心扔，挡不住核心继续长。

56
00:04:33,288 --> 00:04:38,684
禁令不是止疼药，它只改默认动作，不改业务本身的复杂度。

57
00:04:38,684 --> 00:04:47,098
真正值钱的地方在于，它把一条本来靠老员工口口相传的默契，写成新人第一天就能读到的文本。

58
00:04:47,098 --> 00:04:53,492
明文可评审、可争论、可修改，默契不行，默契会随人员流动蒸发。

59
00:04:53,492 --> 00:04:59,502
另外注意第三问的措辞，它不是问能不能拆，而是问为什么拆不出去。

60
00:04:59,502 --> 00:05:03,456
这个措辞把举证责任放在了提交者身上。

61
00:05:03,456 --> 00:05:07,338
默认是不进核心，想进核心你得给理由。

62
00:05:07,338 --> 00:05:12,627
举证责任放在哪一边，决定了这条规则最后是硬还是软。

63
00:05:12,772 --> 00:05:16,399
禁令听着像洁癖，直到你把数字摊开。

64
00:05:16,349 --> 00:05:23,525
按 Rust 源码行数算，核心三十三万行，界面层二十七万，应用服务端十四万八千。

65
00:05:23,525 --> 00:05:28,765
大于等于一万行的有二十四个，小于两千的有七十七个。

66
00:05:28,765 --> 00:05:32,912
少数几个吃掉大半体积，典型的幂律分布。

67
00:05:32,912 --> 00:05:38,188
核心去掉测试之后剩大约十万三千行，有八十六个模块。

68
00:05:38,188 --> 00:05:41,650
直接依赖它的工作区成员，二十五个。

69
00:05:41,650 --> 00:05:50,003
界面层比较特殊，它的依赖清单里没有核心这一行，它依赖的是应用服务端客户端，再由客户端把核心拉进来。

70
00:05:50,003 --> 00:05:56,602
所以往核心加一行，二十五个直接下游要重编，界面层仍然被间接带上。

71
00:05:56,602 --> 00:06:01,265
你以为只改了一个包，实际按下了半个仓库的重新编译。

72
00:06:01,265 --> 00:06:03,669
再回到分支名那个需求。

73
00:06:03,669 --> 00:06:09,883
落在核心里，它的类型就跟沙箱、守卫、远程工具协议焊在一起。

74
00:06:09,883 --> 00:06:17,263
落在一个只碰协议和字符串工具的小包里，核心可以当调用方去用它，而它不依赖核心。

75
00:06:17,263 --> 00:06:20,893
依赖方向反过来的那一刻，边界就没了。

76
00:06:20,893 --> 00:06:26,361
叶子依赖中心，中心再依赖叶子，编译图会从两边一起胀。

77
00:06:26,361 --> 00:06:32,359
这是编译期依赖的物理性，不是风格建议，是编译器每天替你执行一遍的约束。

78
00:06:32,359 --> 00:06:35,652
顺带说一句那个幂律分布的含义。

79
00:06:35,652 --> 00:06:40,604
二十四个大包吃掉大半体积，七十七个小包加起来没多少。

80
00:06:40,604 --> 00:06:49,102
这说明真正需要治理的只有少数几个位置，你不需要给每个包都立规矩，盯住头部那几个就够了。

81
00:06:49,102 --> 00:06:53,561
这是做架构治理时很实用的一条省力原则。

82
00:06:53,716 --> 00:06:55,817
正确的落点长什么样。

83
00:06:55,767 --> 00:07:03,411
核心自己已经依赖六十一个带项目前缀的包，其中包括被拆出去的上下文片段包和功能开关包。

84
00:07:03,411 --> 00:07:07,522
这个方向说明一件事，核心是可以当调用方的。

85
00:07:07,522 --> 00:07:17,462
看那个上下文片段包的清单，几乎没有业务依赖，只碰协议和一段字符串工具，对外重新导出两个片段类型和一个特征。

86
00:07:17,462 --> 00:07:20,154
它小到可以被任何一层复用。

87
00:07:20,154 --> 00:07:23,003
核心依赖它，它不依赖核心。

88
00:07:23,003 --> 00:07:29,228
分支名这种片段就该走这条路，类型落在片段包，核心当调用方。

89
00:07:29,228 --> 00:07:33,051
模型供应商的适配也已经抽到独立的包里。

90
00:07:33,051 --> 00:07:41,055
只有必须摸到会话内部状态的东西，才走到最后一档，而且评审仍要问一句，为什么拆不出去。

91
00:07:41,055 --> 00:07:44,541
旁边还有三把尺子，量的是同一件事。

92
00:07:44,541 --> 00:07:49,144
文件目标五百行，大约超过八百行就开新模块。

93
00:07:49,144 --> 00:07:54,457
非机械改动一次不超过八百行，复杂逻辑压到五百行以内。

94
00:07:54,457 --> 00:08:03,772
加上前面落点三问，三层一起看，针对的是同一个毛病，人和 AI 都倾向于把改动写大，并且写进已经很大的文件里。

95
00:08:03,772 --> 00:08:12,786
这三把尺子你可以直接抄进自己的仓库，它们不需要任何工具支持，一条合并请求模板里的检查项就够了。

96
00:08:12,786 --> 00:08:16,885
再多说一句为什么尺子要按行数量这么土。

97
00:08:16,885 --> 00:08:21,945
因为人和 AI 都倾向于把改动写大，写进已经很大的文件里。

98
00:08:21,945 --> 00:08:30,695
行数是唯一一个不需要理解业务就能读出来的信号，它粗糙，但它能在评审的第一分钟就给出提示。

99
00:08:30,844 --> 00:08:34,171
必须说清一个让人不舒服的事实。

100
00:08:34,121 --> 00:08:38,881
这条禁令本身没有静态检查，没有持续集成任务。

101
00:08:38,881 --> 00:08:44,481
你在整个仓库里检索那句英文，只命中 AGENTS 文件这一处。

102
00:08:44,481 --> 00:08:50,070
它挡得住习惯，挡不住有理由的例外，也挡不住有人根本没读。

103
00:08:50,070 --> 00:08:52,198
那它靠什么还在运转。

104
00:08:52,198 --> 00:08:55,058
靠旁边那几条有红灯的规则。

105
00:08:55,058 --> 00:09:00,407
依赖清单改了但没刷新 Bazel 锁文件，持续集成立刻变红。

106
00:09:00,407 --> 00:09:06,080
用宏把文件嵌进二进制却没在构建文件里登记，构建也红。

107
00:09:06,080 --> 00:09:11,068
真正在机器层面兜底的是构建一致性，不是架构整洁度。

108
00:09:11,068 --> 00:09:13,075
这个对照很有价值。

109
00:09:13,075 --> 00:09:16,597
架构约束和构建约束是两种东西。

110
00:09:16,597 --> 00:09:20,515
构建约束可以自动化，因为它有唯一正确答案。

111
00:09:20,515 --> 00:09:25,395
架构约束大部分时候只能靠评审，因为它需要理由。

112
00:09:25,395 --> 00:09:28,989
横向看两个同类项目，能看清取舍。

113
00:09:28,989 --> 00:09:48,380
一个把原则写成一切皆插件，运行时只收服务、类型化事件和可逆副作用，模型适配器、工具注册表、会话日志、智能体循环全是插件，没有需要打补丁的特权内核，新包落进现有分组时根目录清单都不用改，通配符会发现它。

114
00:09:48,380 --> 00:10:04,129
另一个把根清单标明是自动生成的，人只改各包自己的清单，成员按同一口径数是七十九个，分层写在根说明里，终端界面是界面层，运行时是壳，工具和空间是领域能力，公共和构建是叶子。

115
00:10:04,129 --> 00:10:16,184
Codex 用编译期包换掉了插件式装卸器，代价是新能力必须改成员数组、写构建文件、重新编译，能下手的位置只剩评审和持续集成。

116
00:10:16,184 --> 00:10:21,701
前者赢在灵活，后者赢在类型安全和编译期可见的依赖图。

117
00:10:21,701 --> 00:10:25,691
没有哪个更好，只有你愿意付哪种成本。

118
00:10:25,828 --> 00:10:28,614
下半场换话题，讲循环。

119
00:10:28,564 --> 00:10:34,874
界面上你看到的是一轮对话，你说了一句，它忙了一阵，最后回一句改完了。

120
00:10:34,874 --> 00:10:39,550
内部叠了三层循环，每层只回答自己那一个问题。

121
00:10:39,550 --> 00:10:43,564
最外层叫任务壳，问这一趟任务还活着吗。

122
00:10:43,564 --> 00:10:47,807
中间层叫轮次，问这一轮还要不要再采样一次。

123
00:10:47,807 --> 00:10:51,533
最内层叫采样层，问这一条流结束了吗。

124
00:10:51,533 --> 00:10:53,865
控制权每次只在一层。

125
00:10:53,865 --> 00:11:04,730
对外入口是开始或转向一轮那个函数，它自己不看会话空不空闲，返回值只表示核心接没接住这条输入，不等钩子，也不等采样。

126
00:11:04,730 --> 00:11:11,064
空闲就起一个常规任务，忙碌就把你的话写成转向事件放进待处理队列。

127
00:11:11,064 --> 00:11:14,213
三层循环从任务壳才开始转。

128
00:11:14,213 --> 00:11:20,884
任务壳发出一次开始事件，然后只要队列里还有待处理输入，就再调一次轮次函数。

129
00:11:20,884 --> 00:11:30,090
第二次进去时入口参数是空的，新消息从队列里取，轮次编号钉死不变，界面不会再闪一次新的一轮开始了。

130
00:11:30,090 --> 00:11:35,595
这个细节看着小，它决定回放日志里是一个桶还是两个桶。

131
00:11:35,595 --> 00:11:42,134
轮次层把采样回来的两件事合成一个布尔，模型需要追问或者有待处理输入。

132
00:11:42,134 --> 00:11:46,461
为真就自己继续循环，为假才去跑停止钩子。

133
00:11:46,461 --> 00:11:54,393
钩子如果带回一句提示，就拦住收工，这一层自己再转一圈，此时任务壳和采样层都还没退。

134
00:11:54,393 --> 00:11:56,773
采样层自己还有两圈。

135
00:11:56,773 --> 00:12:01,256
外圈处理可重试的错误，内圈消费一条流式响应。

136
00:12:01,256 --> 00:12:07,675
工具调用不等整条流结束，输出项完成的那一刻就把异步任务挂上去。

137
00:12:07,675 --> 00:12:13,540
流收到完成信号，先把在飞的工具结果收干净，再交回轮次层。

138
00:12:13,684 --> 00:12:17,420
任务壳和轮次层都看同一个待处理队列。

139
00:12:17,370 --> 00:12:20,326
你可能会问，这不是重复吗。

140
00:12:20,326 --> 00:12:23,884
不是，两处问的是两个时刻的两个问题。

141
00:12:23,884 --> 00:12:33,163
轮次层在采样刚刚结束时问，问的是这一轮还要不要再采一次，为真就把插话和工具结果留在同一个轮次编号里。

142
00:12:33,163 --> 00:12:44,317
任务壳在轮次已经跳出之后问，问的是这一趟任务还要不要再进一次轮次函数，处理的是界面已经可以收工、队列里又来了必须处理的输入。

143
00:12:44,317 --> 00:12:47,430
去掉任何一问，价值就显出来。

144
00:12:47,430 --> 00:13:04,417
去掉轮次那一问，模型写出最终答案后轮次会去跑停止钩子然后跳出，你中途那句测试用 pytest 只能等任务壳再进一次轮次函数，功能上还能补上，但停止钩子会在插话进模型之前先跑一轮，顺序错了。

145
00:13:04,417 --> 00:13:20,526
去掉任务壳那一问，轮次返回后任务直接结束，队列里后到的消息要么消失，要么等会话变空闲，由另一个待处理函数换一个新的轮次编号重新开任务，开始事件会再闪一次，回放里变成两个桶。

146
00:13:20,526 --> 00:13:23,976
停止钩子的语义也靠层次才清楚。

147
00:13:23,976 --> 00:13:32,353
钩子返回阻断并且带提示，控制权留在轮次层，采样层早已返回，任务壳还在等这次轮次结束。

148
00:13:32,353 --> 00:13:37,810
阻断是轮次层内部续跑，放行才是把控制权交回任务壳。

149
00:13:37,810 --> 00:13:42,497
再看两个同类切法，你就知道分层的维度不止一种。

150
00:13:42,497 --> 00:13:57,341
一个按语义切，外层不断调用轮次，轮次内部再套一层，每一圈先做前置步骤再做一步，第三层不是第三条循环而是一对队列，调用方在入队时选择追问、转向还是直接注入。

151
00:13:57,341 --> 00:14:10,046
另一个是单层循环加一个状态袋，可变状态都放进一个对象，循环体顶部解构，继续的时候写回整袋，继续与否只由助手消息里的工具使用块点亮。

152
00:14:10,046 --> 00:14:12,942
差别不在层数，在切分维度。

153
00:14:12,942 --> 00:14:18,928
按生命周期切，你能把流重试、取消、结束各自关在采样层。

154
00:14:18,928 --> 00:14:23,146
按语义切，你能从会话事件重放两条队列。

155
00:14:23,146 --> 00:14:29,048
状态袋的代价是，改一处继续，要同时核对好几个状态字段。

156
00:14:29,188 --> 00:14:32,479
把这一集压成五条能带走的东西。

157
00:14:32,429 --> 00:14:36,179
第一条，防默认落点，不防单次提交。

158
00:14:36,179 --> 00:14:44,700
新概念先问现有的非核心包能不能住，住不下就新建包并允许重构，进核心必须回答为什么拆不出去。

159
00:14:44,700 --> 00:14:49,725
明天能做的动作是，把这三问写进你的合并请求模板。

160
00:14:49,725 --> 00:14:52,645
第二条，叶子类型待在叶子包。

161
00:14:52,645 --> 00:14:58,775
判断标准只有一句，这个类型被复用的时候，需要把多少东西一起拖走。

162
00:14:58,775 --> 00:15:02,657
拖走沙箱和守卫的类型，位置就放错了。

163
00:15:02,657 --> 00:15:06,071
第三条，给代码体量配三把尺子。

164
00:15:06,071 --> 00:15:13,835
文件目标五百行，超过八百开新模块，非机械改动一次不超过八百，复杂逻辑压到五百。

165
00:15:13,835 --> 00:15:16,588
不需要工具，检查项就够。

166
00:15:16,588 --> 00:15:20,277
第四条，循环按谁有资格决定继续来切。

167
00:15:20,277 --> 00:15:25,590
一条流结束了吗，这一轮还要再采吗，这一趟任务还活着吗。

168
00:15:25,590 --> 00:15:30,097
三个问题三个层次，不要收成一个循环加三个布尔。

169
00:15:30,097 --> 00:15:33,859
第五条，区分构建约束和架构约束。

170
00:15:33,859 --> 00:15:37,609
前者能自动化，因为没有第二种正确答案。

171
00:15:37,609 --> 00:15:40,975
后者只能靠评审，因为它需要理由。

172
00:15:40,975 --> 00:15:44,797
别指望给架构整洁度装一个红灯，装不上。

173
00:15:44,797 --> 00:15:48,186
串起来看，五条都在做同一个动作。

174
00:15:48,186 --> 00:15:52,585
承认单点扛不住，然后用结构把压力分摊掉。

