WEBVTT

1
00:00:00.000 --> 00:00:02.137
一位开发者打开构建仪表盘。

2
00:00:02.137 --> 00:00:05.753
构建服务报告的状态是“OK”，前后带着空格，

3
00:00:05.753 --> 00:00:08.382
仪表盘却显示“unknown”。

4
00:00:08.717 --> 00:00:12.771
有人提了一个补丁，它已有的两个测试都通过了。

5
00:00:12.771 --> 00:00:14.615
那么，这个补丁对吗？

6
00:00:14.950 --> 00:00:20.690
看完这个视频，你会从约定出发、而不是从补丁出发，多推出一个测试，

7
00:00:20.690 --> 00:00:23.739
并看清为什么两个绿色的测试还不够。

8
00:00:24.783 --> 00:00:27.124
先看约定，不看代码。

9
00:00:27.124 --> 00:00:29.230
状态是一个字符串。

10
00:00:29.567 --> 00:00:31.567
忽略前后的空白和大小写。

11
00:00:31.567 --> 00:00:36.233
“OK”表示通过，“fail”表示失败，其他任何字符串，

12
00:00:36.233 --> 00:00:39.900
包括空字符串，都表示未知（unknown）。

13
00:00:40.233 --> 00:00:45.260
每一条规则都可以测试，在看补丁之前就可以。

14
00:00:46.299 --> 00:00:47.604
这是提出的补丁。

15
00:00:47.604 --> 00:00:52.333
它先把状态转成小写，再去查表：不带空格的“OK”变成通过，

16
00:00:52.333 --> 00:00:56.737
“fail”变成失败，其他都是未知（unknown）。

17
00:00:57.083 --> 00:01:01.088
两个已有的测试检查“ok”和“fail”，都通过了。

18
00:01:01.088 --> 00:01:03.399
这是真实跑过、真实通过的检查。

19
00:01:03.733 --> 00:01:05.826
但它们检查了哪几条规则？

20
00:01:05.826 --> 00:01:09.139
两个输入里既没有空格，也没有大写字母。

21
00:01:09.139 --> 00:01:11.581
空白这条规则从来没被测试过。

22
00:01:12.616 --> 00:01:15.404
所以，从约定推出一个测试。

23
00:01:15.404 --> 00:01:18.408
输入是前后带空格的“OK”。

24
00:01:18.408 --> 00:01:22.055
不管补丁怎么写，约定说答案是通过。

25
00:01:22.399 --> 00:01:23.097
跑一下。

26
00:01:23.097 --> 00:01:27.112
补丁把输入转成小写，空格还在，查不到对应的键，

27
00:01:27.112 --> 00:01:29.381
于是返回 unknown。

28
00:01:29.381 --> 00:01:30.253
测试失败。

29
00:01:30.253 --> 00:01:32.697
这个看起来合理的补丁是错的。

30
00:01:33.032 --> 00:01:34.717
不是每个新输入都有用。

31
00:01:34.717 --> 00:01:40.230
“cancelled”在错误的补丁和正确的实现里都返回 unknown。

32
00:01:40.230 --> 00:01:42.680
它检查了兜底规则，却分不出两者。

33
00:01:43.714 --> 00:01:48.916
修复只多一个调用：先去掉前后的空白，再转小写。

34
00:01:49.264 --> 00:01:53.237
现在用从约定里取出的六个用例，比较两个版本。

35
00:01:53.237 --> 00:01:57.751
原来的补丁通过六个里的四个，修复后的版本六个全过。

36
00:01:58.098 --> 00:02:02.915
保留原来的两个测试，也把新的用例留作回归测试。

37
00:02:02.915 --> 00:02:08.361
六个用例是对这些行为的观察，不是对所有字符串的证明。

38
00:02:09.397 --> 00:02:11.805
这对智能体为什么重要？

39
00:02:11.805 --> 00:02:15.527
假设模型写了补丁，也写了它的测试。

40
00:02:15.864 --> 00:02:19.838
如果两者都默认输入没有空格，它们就会彼此一致，

41
00:02:19.838 --> 00:02:21.566
同时漏掉同一条规则。

42
00:02:21.566 --> 00:02:26.231
这是一种可能出现的共同遗漏，不是在说模型这样做的频率。

43
00:02:26.580 --> 00:02:33.941
独立的期望结果要从别处来：产品约定，以及你刻意挑选的用例。

44
00:02:34.980 --> 00:02:39.498
假设这个补丁是由模型提出的，它会怎样产生？

45
00:02:39.498 --> 00:02:45.092
语言模型处理的是分词单元：词的片段、标点和代码字符。

46
00:02:45.092 --> 00:02:51.331
每一步，它根据上下文算出下一个分词单元的分布，再选出一个。

47
00:02:51.331 --> 00:02:55.634
这个分词单元加入上下文，然后重复这一步。

48
00:02:55.980 --> 00:03:00.871
这样可以生成一个有用的多步算法，但这并不意味着模型执行过它。

49
00:03:00.871 --> 00:03:05.274
合法的 Python 代码，实现的仍可能是另一条规则，

50
00:03:05.274 --> 00:03:07.230
而不是用户需要的那一条。

51
00:03:07.580 --> 00:03:13.015
而且，一个分词单元出现的概率，并不是程序满足规格的概率。

52
00:03:13.015 --> 00:03:18.644
在一项合适的检查真正运行之前，补丁的行为始终没有被观察到。

53
00:03:19.680 --> 00:03:24.695
在普通的推理过程中，工作流提供请求和其他上下文。

54
00:03:24.695 --> 00:03:30.129
粘贴一个文件或纠正一个回答，通常不会更新模型的参数。

55
00:03:30.129 --> 00:03:36.816
训练改变的是学到的参数；提示词改变的是当前这次回答能用到的东西。

56
00:03:37.163 --> 00:03:38.873
想象两个上下文包。

57
00:03:38.873 --> 00:03:42.294
一个只有“修好这个标签”和实现代码。

58
00:03:42.294 --> 00:03:46.665
另一个还多了一条来自约定的“忽略前后的空白”。

59
00:03:46.665 --> 00:03:49.516
第二个包提供了一条明确的约束，

60
00:03:49.516 --> 00:03:53.127
但这并不意味着模型拿着它就一定会成功。

61
00:03:53.463 --> 00:03:55.325
训练是另一回事。

62
00:03:55.325 --> 00:04:02.541
预训练、监督微调和强化学习会改变参数，也可能提高写代码的能力。

63
00:04:02.541 --> 00:04:07.662
但需求、版本和执行条件，仍然要在使用时检查。

64
00:04:08.696 --> 00:04:12.933
一个编码智能体，是模型加上一套执行工作流。

65
00:04:12.933 --> 00:04:14.749
模型提出一个动作。

66
00:04:14.749 --> 00:04:18.381
控制器检查工具名、参数、权限和预算。

67
00:04:18.381 --> 00:04:23.627
然后，被允许的工具执行这个操作，并返回一个观察结果。

68
00:04:23.963 --> 00:04:26.391
每个事件确立的东西各不相同。

69
00:04:26.391 --> 00:04:30.208
一次被允许的读取，说明它通过了一次权限判断。

70
00:04:30.208 --> 00:04:32.462
一次写入，说明发生了写入。

71
00:04:32.462 --> 00:04:35.585
而模型说工作完成了，是一个完成声明。

72
00:04:35.930 --> 00:04:40.550
权限判断不能确立正确性，写入结果也不是测试结果。

73
00:04:40.550 --> 00:04:44.593
如果一次测试请求被拒绝，或者一直没有结束，

74
00:04:44.593 --> 00:04:49.021
后来一句“所有测试都通过了”补不上缺失的观察。

75
00:04:49.021 --> 00:04:53.449
一个稳妥的下一步，是查看那次拒绝或缺失的结果，

76
00:04:53.449 --> 00:04:55.760
再争取一次被允许的检查。

77
00:04:56.096 --> 00:05:00.928
今天的循环只有一个模型、一次控制器判断和一个工具结果。

78
00:05:00.928 --> 00:05:05.939
生产环境里的编码智能体，会在同一个循环外面包上更多机制：

79
00:05:05.939 --> 00:05:12.381
工具定义和预算、一份压缩过的早期步骤记录、上下文文件和技能、钩子和沙箱，

80
00:05:12.381 --> 00:05:15.244
以及一份规格说明和分阶段的计划。

81
00:05:15.244 --> 00:05:18.286
第二到第四周会一层一层地研究它们。

82
00:05:18.286 --> 00:05:23.297
每一层改变的，是模型能看到什么，或者一个动作可以做什么。

83
00:05:23.297 --> 00:05:25.623
没有哪一层会替你检查结果。

84
00:05:25.623 --> 00:05:30.276
外围机制再丰富，也仍然需要在当前文件上完成一次检查。

85
00:05:31.312 --> 00:05:32.778
现在往上看一层。

86
00:05:32.778 --> 00:05:36.625
开发者还需要一个和各行一致的失败构建计数。

87
00:05:36.625 --> 00:05:39.556
调用方用辅助函数给每一行打标签，

88
00:05:39.556 --> 00:05:43.770
可计数时却是把原始状态直接和“fail”比较。

89
00:05:44.112 --> 00:05:46.832
所以，只修辅助函数，还会留下不一致。

90
00:05:46.832 --> 00:05:50.911
遇到一个带空白、大小写混杂的失败状态，这一行显示失败，

91
00:05:50.911 --> 00:05:51.969
计数却还是零。

92
00:05:51.969 --> 00:05:55.142
就算辅助函数的六个测试全加上，也会漏掉它。

93
00:05:55.479 --> 00:05:58.364
提出的修复，改为统计已经算好的标签。

94
00:05:58.364 --> 00:06:01.730
再用一个检查完整输出的回归测试，检查计数，

95
00:06:01.730 --> 00:06:04.294
以及没有变化的编号和它们的顺序。

96
00:06:04.294 --> 00:06:08.141
这些都是手工推出的期望，不是记录下来的运行结果。

97
00:06:08.141 --> 00:06:11.186
还要注意任务本身：单说“让测试通过”，

98
00:06:11.186 --> 00:06:13.591
并没有说明要保护哪种产品行为。

99
00:06:14.629 --> 00:06:15.920
回到仪表盘。

100
00:06:15.920 --> 00:06:19.579
两个绿色的测试没能说明补丁是对的；

101
00:06:19.579 --> 00:06:23.452
一个从约定推出的测试说明了它是错的。

102
00:06:23.795 --> 00:06:24.688
试一个问题。

103
00:06:24.688 --> 00:06:27.514
一个新服务发来“completed”，

104
00:06:27.514 --> 00:06:30.488
而我们的约定把它映射为 unknown。

105
00:06:30.488 --> 00:06:31.827
它应该表示通过吗？

106
00:06:31.827 --> 00:06:35.545
在改动任何东西之前，先写下你要问产品负责人的问题。
