WEBVTT

1
00:00:00.000 --> 00:00:03.623
这是一个 AI 智能体修 bug 的完整记录。

2
00:00:03.623 --> 00:00:07.404
它读了一个文件，写了一个文件，然后回复：完成了，

3
00:00:07.404 --> 00:00:09.137
并且不再请求任何调用。

4
00:00:09.483 --> 00:00:11.132
那么，bug 修好了吗？

5
00:00:11.467 --> 00:00:13.957
这份记录里没有任何东西能告诉我们。

6
00:00:13.957 --> 00:00:17.179
看完这个视频，你会知道缺的是哪一条观察结果，

7
00:00:17.179 --> 00:00:20.987
以及为什么智能体说“完成”只是一个说法，而不是结论。

8
00:00:22.033 --> 00:00:25.945
首先，把两件我们常常混为一谈的事分开。

9
00:00:26.283 --> 00:00:29.791
语言模型产出文字，它做的就只有这个。

10
00:00:29.791 --> 00:00:35.249
有时这段文字长得像一个请求：调用这个工具，带上这些参数。

11
00:00:35.249 --> 00:00:37.393
我们把它叫作“提议”。

12
00:00:37.733 --> 00:00:42.304
包在模型外面的程序，也就是“控制器”，决定接下来发生什么。

13
00:00:42.304 --> 00:00:43.565
它检查这次调用：

14
00:00:43.565 --> 00:00:49.869
工具是否认识、参数名是否是预期的那几个、路径是否允许、内容是否是它能写入的文本。

15
00:00:49.869 --> 00:00:52.234
只有这样，提议才变成“动作”。

16
00:00:52.583 --> 00:00:56.041
如果调用没通过检查，什么都不会执行。

17
00:00:56.041 --> 00:01:02.189
控制器把一条拒绝作为这次调用的结果送回去，模型在下一轮就能看到。

18
00:01:02.189 --> 00:01:06.608
模型可以请求任何东西；允许什么，由控制器决定。

19
00:01:07.648 --> 00:01:11.265
我们用能想到的最小任务来看这个循环。

20
00:01:11.265 --> 00:01:17.494
一个文件 result.txt，里面是数字 5，它应该是 4。

21
00:01:17.832 --> 00:01:20.519
第一轮，模型调用 read。

22
00:01:20.519 --> 00:01:24.359
控制器执行它，并把真实的结果送回去：5。

23
00:01:24.359 --> 00:01:28.966
之后的每一次请求都带着它：是文件本身，不是猜测。

24
00:01:29.315 --> 00:01:31.956
第二轮，调用是：写入 4。

25
00:01:31.956 --> 00:01:35.004
控制器写入，工具报告：已写入。

26
00:01:35.348 --> 00:01:36.622
现在该停了吗？

27
00:01:36.622 --> 00:01:37.714
确实很想停。

28
00:01:37.714 --> 00:01:39.897
文件大概已经是 4 了。

29
00:01:39.897 --> 00:01:45.356
但“已写入”只说明发生了一个副作用，并不能说明结果符合规格。

30
00:01:45.698 --> 00:01:48.416
第三轮，调用是：verify。

31
00:01:48.416 --> 00:01:52.582
一个固定的检查把文件和规格比对，报告：已验证。

32
00:01:52.582 --> 00:01:57.111
这个结果，挂在这次调用自己的 ID 下，就是证据。

33
00:01:57.448 --> 00:02:03.365
第四轮，模型不再请求任何调用，于是循环以 final 停下。

34
00:02:03.365 --> 00:02:09.479
模型提议，控制器决定，工具执行，观察结果再送回去供下一次选择。

35
00:02:09.479 --> 00:02:12.437
这个循环就是这个智能体的核心。

36
00:02:13.480 --> 00:02:15.902
现在倒回去，让模型犯个错。

37
00:02:15.902 --> 00:02:19.442
第二轮里，它写入的是 5 而不是 4。

38
00:02:19.442 --> 00:02:22.795
写入照样成功，工具照样说“已写入”。

39
00:02:22.795 --> 00:02:25.590
只有检查发现了问题：它失败了。

40
00:02:25.930 --> 00:02:29.502
接着模型不带任何调用地回复：“已验证。”

41
00:02:29.502 --> 00:02:32.717
按照约定，循环以 final 停下。

42
00:02:32.717 --> 00:02:36.289
但这条回复改变不了挂在检查调用下的结果，

43
00:02:36.289 --> 00:02:38.968
不管它的解释听起来多有说服力。

44
00:02:39.314 --> 00:02:42.502
消息可以描述结果，只有检查才真正观察到结果。

45
00:02:42.502 --> 00:02:45.980
所以，只有在最后一次写入之后出现一次通过的检查，

46
00:02:45.980 --> 00:02:47.429
才能报告“已验证”。

47
00:02:48.462 --> 00:02:50.481
那么这个循环怎样结束？

48
00:02:50.481 --> 00:02:54.151
停止原因只有两种，而且哪一种都不是检查。

49
00:02:54.496 --> 00:02:57.763
final：一条回复里没有工具调用。

50
00:02:57.763 --> 00:03:02.301
step_limit：轮次预算用完了，默认是四轮。

51
00:03:02.301 --> 00:03:05.750
约定里没有重复检测，所以如果没有预算，

52
00:03:05.750 --> 00:03:09.380
一个一直请求的模型可能会无休止地转下去。

53
00:03:09.729 --> 00:03:14.263
检查结果要从历史里单独读出：最新一次 verify 的结果；

54
00:03:14.263 --> 00:03:17.889
如果之后又有写入让它过时，就是 not_run。

55
00:03:17.889 --> 00:03:21.516
这里，检查失败了，模型又写了一次，然后预算用完。

56
00:03:21.516 --> 00:03:23.934
停止原因：step_limit。

57
00:03:23.934 --> 00:03:25.597
检查：not_run。

58
00:03:25.597 --> 00:03:28.317
而那次失败，在任何摘要里都必须保留。

59
00:03:28.662 --> 00:03:30.756
final 意味着对话停止了，

60
00:03:30.756 --> 00:03:33.547
verified 意味着一次检查通过了。

61
00:03:33.547 --> 00:03:35.919
两者互相独立：在讲义的预算实验里，

62
00:03:35.919 --> 00:03:38.850
一个三轮的运行停在 step_limit，

63
00:03:38.850 --> 00:03:41.083
而它的检查是 verified。

64
00:03:41.083 --> 00:03:42.618
别把这两个词混在一起。

65
00:03:43.662 --> 00:03:44.639
再看看工具。

66
00:03:44.639 --> 00:03:47.244
每个工具都有一份很窄的调用契约。

67
00:03:47.244 --> 00:03:50.500
read 只接受 result.txt。

68
00:03:50.500 --> 00:03:55.384
write 最多 32 个字符，只写 result.txt。

69
00:03:55.384 --> 00:03:58.314
bash 只运行那条固定的验证命令。

70
00:03:58.662 --> 00:04:03.588
其他任何工具、参数名、路径或命令，都会在产生任何效果之前被拒绝，

71
00:04:03.588 --> 00:04:06.975
拒绝结果挂在这次调用自己的 ID 下送回去。

72
00:04:06.975 --> 00:04:10.669
在我们对这份练习材料中 tool 函数的运行里，

73
00:04:10.669 --> 00:04:15.288
这六个调用都返回了拒绝，result.txt 里仍然是 5。

74
00:04:15.629 --> 00:04:19.895
所以，名字叫 bash，并不等于给了一个 shell。

75
00:04:19.895 --> 00:04:22.582
适配器运行的是那个写死的验证程序，

76
00:04:22.582 --> 00:04:25.742
从不把模型的文字放进 shell 命令。

77
00:04:25.742 --> 00:04:30.640
而这个分发器只能限制经过它的调用：外层的隔离由安全启动器负责。

78
00:04:31.678 --> 00:04:38.486
当一个项目公布它的智能体说明时，要像工程师读配置文件那样去读，

79
00:04:38.486 --> 00:04:41.341
而不是把它当成行为的保证。

80
00:04:41.341 --> 00:04:48.148
把每一句话归进四类：角色或目标、工具调用契约、停止或上报规则，

81
00:04:48.148 --> 00:04:49.686
以及谨慎提醒。

82
00:04:50.028 --> 00:04:53.243
然后对每一句都问：是什么在强制执行它？

83
00:04:53.243 --> 00:04:58.319
schema、工具自己的代码，或者模型之外的沙箱或权限检查，

84
00:04:58.319 --> 00:04:59.504
可以强制执行。

85
00:04:59.504 --> 00:05:02.549
一句以“绝不”开头的话，只是在请求。

86
00:05:02.549 --> 00:05:07.964
我们的循环也做了同样的区分：在产生任何效果之前的检查在强制执行，

87
00:05:07.964 --> 00:05:10.671
而提示词里的契约文字只是在提议。

88
00:05:11.012 --> 00:05:12.649
有两个习惯值得带走。

89
00:05:12.649 --> 00:05:15.268
工具定义本身就是提示词的一部分。

90
00:05:15.268 --> 00:05:20.342
另外，说明写得长，并不是安全的证据：要数的是被强制执行的规则，

91
00:05:20.342 --> 00:05:21.815
而不是写下的句子。

92
00:05:21.815 --> 00:05:25.416
还有一个区别：你的四轮循环从不压缩它的历史，

93
00:05:25.416 --> 00:05:27.544
而生产级智能体必须这样做。

94
00:05:28.577 --> 00:05:34.206
到目前为止，每一次请求都带着完整的历史：任务、每一条回复，

95
00:05:34.206 --> 00:05:36.147
以及每一个工具结果。

96
00:05:36.494 --> 00:05:39.921
上下文窗口限制了一次请求能带多少内容，

97
00:05:39.921 --> 00:05:43.348
而每一轮都要把之前的每个字节再发一遍。

98
00:05:43.348 --> 00:05:48.940
一个只有一行的文件，四轮完全装得下；修一个代码仓库就装不下了。

99
00:05:49.277 --> 00:05:51.421
占地方最多的是三类内容。

100
00:05:51.421 --> 00:05:56.424
工具输出，比如读文件的内容和测试日志，通常是最大的一块，

101
00:05:56.424 --> 00:05:57.497
也最先过时。

102
00:05:57.497 --> 00:06:00.713
失败的尝试会留下来，连同它们的报错。

103
00:06:00.713 --> 00:06:03.572
反复读取，每读一次就多一份拷贝。

104
00:06:03.572 --> 00:06:08.039
另外，系统说明和工具定义也会随每一次请求一起发送。

105
00:06:09.077 --> 00:06:13.553
窗口溢出是最明显的失败，但另外三种会更早出现。

106
00:06:13.553 --> 00:06:17.250
这是一段更长的历史，来自本周的自测题。

107
00:06:17.593 --> 00:06:18.602
过时的观察。

108
00:06:18.602 --> 00:06:21.796
对 status.py 的第一次读取，

109
00:06:21.796 --> 00:06:25.159
在智能体改过这个文件之后仍然留在历史里。

110
00:06:25.159 --> 00:06:30.202
没有任何东西把它标为过时，所以后面的步骤可能依据旧文本行事。

111
00:06:30.202 --> 00:06:34.741
自相矛盾也类似：一次失败的尝试和取代它的决定都还留着。

112
00:06:35.077 --> 00:06:36.173
被埋没的指令。

113
00:06:36.173 --> 00:06:39.461
“不要修改 view.py”只出现过一次，

114
00:06:39.461 --> 00:06:41.497
在智能体读到的一个文件里。

115
00:06:41.497 --> 00:06:43.846
它现在压在之后的所有内容下面，

116
00:06:43.846 --> 00:06:47.604
循环里没有任何机制让模型在许多轮之后仍然重视它。

117
00:06:47.943 --> 00:06:51.431
现在把所有较早的工具结果清理成简短的占位，

118
00:06:51.431 --> 00:06:53.424
只保留智能体自己的消息。

119
00:06:53.424 --> 00:06:56.911
失败还在，但只留在智能体自己说的那句话里。

120
00:06:56.911 --> 00:06:59.735
关于 view.py 的约束没了，

121
00:06:59.735 --> 00:07:03.222
而且剩下的内容里没有任何迹象表明它缺失了。

122
00:07:04.260 --> 00:07:07.470
摘要是用一段文字替换较早的轮次。

123
00:07:07.470 --> 00:07:12.285
一份有用的摘要保留六样东西：任务和它的验收检查；

124
00:07:12.285 --> 00:07:18.102
每一条约束和决定，以及它们的出处；尝试过什么、怎样失败的；

125
00:07:18.102 --> 00:07:22.717
尚未解决的失败，以及那次检查所针对的文件版本；

126
00:07:22.717 --> 00:07:25.926
涉及的文件及其状态；还有下一步。

127
00:07:25.926 --> 00:07:31.544
它丢掉原始的工具输出，因为智能体可以重新读取或重新运行。

128
00:07:31.893 --> 00:07:33.321
现在来生成一份。

129
00:07:33.321 --> 00:07:35.818
这是根据那段历史写出的摘要。

130
00:07:35.818 --> 00:07:38.315
读起来很顺，但它是一个说法。

131
00:07:38.315 --> 00:07:42.775
它保留了任务、被修改的文件，以及下一步：修好查找。

132
00:07:42.775 --> 00:07:47.056
它丢掉了那条约束，也丢掉了已经失败过的那次尝试。

133
00:07:47.393 --> 00:07:50.106
于是下一步又做了一遍只去空白的修改。

134
00:07:50.106 --> 00:07:52.518
我们在临时文件上运行了这次修改，

135
00:07:52.518 --> 00:07:55.532
同一个检查又一次在 “OK” 上失败了。

136
00:07:55.877 --> 00:07:58.650
像检查任何生成的成果一样检查摘要。

137
00:07:58.650 --> 00:08:03.054
对它应当保留的每一项，在完整记录里找到它出自哪条消息。

138
00:08:03.054 --> 00:08:06.479
有两项找不到对应：来自说明文件的那条约束，

139
00:08:06.479 --> 00:08:09.415
以及那次失败的尝试和它所针对的版本。

140
00:08:09.415 --> 00:08:12.351
把它们补回去，每一条都连到它的出处。

141
00:08:12.693 --> 00:08:15.209
然后对下一步重新运行同一个检查。

142
00:08:15.209 --> 00:08:20.082
我们的下一次修改既去空白又转小写，而且没有碰 view.py。

143
00:08:20.082 --> 00:08:21.026
检查通过了。

144
00:08:21.026 --> 00:08:26.057
但单靠这个检查，发现不了丢失的约束；发现它的是和完整记录的比对。

145
00:08:27.093 --> 00:08:30.130
什么时候该压缩，什么时候该重新开始？

146
00:08:30.130 --> 00:08:34.517
如果任务还在继续，而最新的原始结果仍然重要，就压缩。

147
00:08:34.517 --> 00:08:39.409
如果大部分历史已经过时或自相矛盾、一个阶段的工作已经结束，

148
00:08:39.409 --> 00:08:42.109
或者智能体总是以同样的方式失败，

149
00:08:42.109 --> 00:08:45.820
就用全新的上下文和一份书面交接说明重新开始。

150
00:08:46.160 --> 00:08:48.611
交接说明包含的条目和摘要一样，

151
00:08:48.611 --> 00:08:51.553
但要写到一个新会话单凭它就能接着干。

152
00:08:51.553 --> 00:08:53.024
无论压缩还是重启，

153
00:08:53.024 --> 00:08:56.946
在修改之前都要重新读一遍下一次修改会涉及的文件。

154
00:08:57.993 --> 00:09:02.254
回到开头：读、写、完成。

155
00:09:02.593 --> 00:09:06.434
缺的那条观察结果，是最后一次写入之后一次通过的检查。

156
00:09:06.434 --> 00:09:08.797
有了它，你才可以报告“已验证”。

157
00:09:08.797 --> 00:09:13.228
没有它，“完成”只是一个说法；一份没人核对过的摘要也是如此。

158
00:09:13.577 --> 00:09:16.292
和同伴对答案之前，先试一个问题。

159
00:09:16.292 --> 00:09:20.534
一个智能体写了 status.py，验收测试失败。

160
00:09:20.534 --> 00:09:23.418
它又写了一遍，然后轮次预算用完了。

161
00:09:23.418 --> 00:09:25.115
新文件看起来是对的。

162
00:09:25.115 --> 00:09:28.508
这份记录显示的停止原因和检查状态是什么？

163
00:09:28.508 --> 00:09:30.036
摘要必须保留什么？

164
00:09:30.036 --> 00:09:33.259
哪一条观察结果能让你报告它“已验证”？

165
00:09:33.259 --> 00:09:36.483
写下你的答案，以及你接下来会跑的检查。
