1
00:00:00,100 --> 00:00:07,634
本内容改编自 Datawhale《Hello-Agents》，采用 CC BY-NC-SA 4.0 授权，为二次演绎配音版。

2
00:00:07,634 --> 00:00:11,358
这套教材讲的是从零开始构建智能体。

3
00:00:11,358 --> 00:00:14,963
今天这一集，我们不讲原理，我们讲事故。

4
00:00:14,963 --> 00:00:20,348
前面几集聊的都是怎么把智能体搭起来，听起来每一步都挺顺。

5
00:00:20,348 --> 00:00:32,596
但真到自己动手写一个能干活的智能体，你会撞上另一类问题，它跟模型聪不聪明没关系，来自你的系统设计，而且表现形式特别气人，叫做看不见。

6
00:00:32,596 --> 00:00:35,973
这一集改编自教材的实践踩坑章节。

7
00:00:35,973 --> 00:00:42,500
作者把自己踩过的坑完整复盘了一遍，我挑出最有代表性的三段讲给你听。

8
00:00:42,500 --> 00:00:49,747
第一段，他照着大厂公开的最佳实践抄作业，结果第一版烂透了，只能删库重来。

9
00:00:49,747 --> 00:00:58,125
第二段，一条看起来非常专业的管道命令，让智能体连跑三轮、烧掉一堆额度，最后给了一个错误结论。

10
00:00:58,125 --> 00:01:04,122
第三段，工具到底该拆多细，拆粗了查不出问题，拆细了模型不会选。

11
00:01:04,122 --> 00:01:09,302
这三件事方向看起来完全相反，失败方式却一模一样。

12
00:01:09,302 --> 00:01:12,115
这个道理我们到最后再点破。

13
00:01:12,115 --> 00:01:14,423
先从第一个坑说起。

14
00:01:14,572 --> 00:01:24,990
动手之前，作者查了大量业界的设计实践，比如某团队分享的上下文工程经验教训，还有某家模型厂商官方写的开发指南。

15
00:01:24,990 --> 00:01:33,161
他的想法很自然，既然有代码智能体工具，让人工智能帮我把这些高级概念全实现一遍不就行了。

16
00:01:33,161 --> 00:01:43,161
于是他不假思索地堆砌了一堆看起来很优雅的设计，多层记忆系统，复杂的上下文工程，多智能体系统，一个都没落下。

17
00:01:43,161 --> 00:01:49,928
然后他满怀期待地跑了第一版测试，现实狠狠给了他一巴掌，整个系统烂透了。

18
00:01:49,928 --> 00:01:58,366
面对一个极其简单的修改需求，智能体像发疯一样调用了七八种工具，进行了好几轮的左右脑互搏。

19
00:01:58,366 --> 00:02:05,145
最终他只收获了一段根本跑不通的残缺代码，以及一张严重超支的额度账单。

20
00:02:05,145 --> 00:02:10,673
看着满屏报错他才反应过来，智能体开发和传统软件开发不一样。

21
00:02:10,673 --> 00:02:18,955
以前做后端，习惯先画好架构图再写代码，图纸够优雅系统就稳固，这是程序员的本能。

22
00:02:18,955 --> 00:02:28,594
但智能体开发不是这样，你是在跟一个大模型打交道，它本身是概率性的，同样的输入每次都可能给你完全不同的输出。

23
00:02:28,594 --> 00:02:34,604
他在一个不确定的地基上，强行叠加了一套自己都没验证过的复杂架构。

24
00:02:34,604 --> 00:02:41,094
多智能体、先规划再执行，这些设计彼此交叉，让不确定性成倍放大。

25
00:02:41,094 --> 00:02:48,967
复杂架构不但没兜住底，反而因为状态流转太多、工具交叉太复杂，让模型错得更离谱。

26
00:02:48,967 --> 00:02:53,606
错误在各个组件之间来回传，他连排查都无从下手。

27
00:02:53,606 --> 00:03:06,226
那些大厂的最佳实践当然是好东西，但他忽略了一点，那些复杂架构是他们踩了无数坑、耗费了海量额度之后演进出来的结果，不是新手上路的起点。

28
00:03:06,388 --> 00:03:14,162
看着这堆连简单读取文件都会陷入死循环的代码，他做了一个决定，删库，推倒重来。

29
00:03:14,162 --> 00:03:21,083
奉行少即是多的原则，他直接复用了项目最基础的主干，把最短的链路先跑通。

30
00:03:21,083 --> 00:03:28,559
核心组件被精简到只剩几块，没有搞任何花哨的设计模式，就是粗暴地把它们攒在一起。

31
00:03:28,559 --> 00:03:34,340
当这个简陋的第一版在终端里跑通了第一个极简任务，他长舒了一口气。

32
00:03:34,340 --> 00:03:40,747
只要能完成最基础的对话和工具调用，就能在真实任务环境里验证和迭代。

33
00:03:40,747 --> 00:03:43,559
先跑起来，比一步更重要。

34
00:03:43,559 --> 00:03:50,158
架构再优雅，地基是概率性的，你也没法靠图纸证明它对，只能靠跑。

35
00:03:50,308 --> 00:03:54,056
第一版跑通之后，他开始放松对工具的约束。

36
00:03:54,056 --> 00:03:59,006
既然架构已经精简了，让模型自由组合命令应该没问题吧。

37
00:03:59,006 --> 00:04:07,647
于是他给终端工具开了绿灯，模型不只能跑单条命令，还能写管道、重定向、子命令，就像人一样。

38
00:04:07,647 --> 00:04:09,546
然后事故就来了。

39
00:04:09,546 --> 00:04:13,885
那天他提了个简单需求，帮我搜一下某个函数的定义。

40
00:04:13,885 --> 00:04:23,933
模型很快给出了一条看起来挺专业的命令，先搜函数定义，过滤掉测试文件，再取前五十行，老工程师常用的组合拳。

41
00:04:23,933 --> 00:04:26,085
但执行结果是空的。

42
00:04:26,085 --> 00:04:30,652
智能体看到这个空结果愣了一下，然后开始补救。

43
00:04:30,652 --> 00:04:33,008
第一轮重试，还是空。

44
00:04:33,008 --> 00:04:35,376
第二轮重试，还是空。

45
00:04:35,376 --> 00:04:38,357
第三轮重试，结果依然是空。

46
00:04:38,357 --> 00:04:47,647
三轮之后它放弃了，告诉他说，我在仓库里没有找到这个函数的定义，可能函数名有误，或者它不在派森文件里。

47
00:04:47,647 --> 00:04:54,282
但他手动去仓库里看了，那个函数明明就在某个工具模块的第四十二行。

48
00:04:54,282 --> 00:04:59,703
他复制那条命令到自己终端里跑，才发现搜索工具报错了。

49
00:04:59,703 --> 00:05:12,095
原来他启动智能体时的工作目录不是项目根目录，而是项目下的一个子目录，那个源码目录相对当前路径根本不存在，搜索工具直接报错退出。

50
00:05:12,095 --> 00:05:16,061
但在智能体那边，错误信息被管道吞掉了。

51
00:05:16,061 --> 00:05:29,198
命令里用了管道符号，搜索工具的错误输出没有传到标准输出，而是被导向下一个命令的输入，后面的过滤命令收到空输入输出也是空，再后面的命令还是空。

52
00:05:29,198 --> 00:05:31,554
错误在链路中被压扁了。

53
00:05:31,554 --> 00:05:37,527
智能体看到的只是一个空字符串，它根本不知道上游已经失败了。

54
00:05:37,527 --> 00:05:43,068
最坑的是，模型基于这个被压扁的信息做出了完全错误的判断。

55
00:05:43,068 --> 00:05:51,974
它以为确实没找到，于是开始各种补救，换搜索词，换目录，甚至怀疑是不是用户自己记错了函数名。

56
00:05:51,974 --> 00:05:58,357
这些动作全都建立在一个错误的判断之上，白白消耗了大量额度。

57
00:05:58,492 --> 00:06:03,153
他的第一反应是，终端工具太危险了，得加限制。

58
00:06:03,153 --> 00:06:06,408
于是他写了一大堆安全检查代码。

59
00:06:06,408 --> 00:06:09,678
但很快他发现，命令行太灵活了。

60
00:06:09,678 --> 00:06:19,654
你禁了管道，它可以用子命令替换；你禁了重定向，它可以用 tee 命令；你禁了删除命令，它可以用重定向把文件清空。

61
00:06:19,654 --> 00:06:26,793
补丁越打越多，代码越写越长，但那个根本问题依然存在，到底是哪一步失败了。

62
00:06:26,793 --> 00:06:36,733
如果返回空，你还是不知道是仓库里真的没有，还是搜索工具因为路径错误压根没执行，模型依然无法针对性纠错。

63
00:06:36,733 --> 00:06:41,589
后来他想明白了，根因不是命令太危险，而是不可诊断。

64
00:06:41,589 --> 00:06:43,728
具体来说是三个问题。

65
00:06:43,728 --> 00:06:46,865
第一，多步骤被塞进了一个动作里。

66
00:06:46,865 --> 00:06:55,074
管道把好几步逻辑打包在一起，中间状态全丢了，智能体只能看到最终结果，看不到执行过程。

67
00:06:55,074 --> 00:06:58,007
第二，观察信号只有一个终态。

68
00:06:58,007 --> 00:07:06,048
成功、失败、空结果全都混在一起，模型分不清是真的没找到，还是查找过程中出错了。

69
00:07:06,048 --> 00:07:09,065
第三，模型无法针对性纠错。

70
00:07:09,065 --> 00:07:15,267
它不知道搜索、过滤、截取这三步里谁出了问题，下一步只能瞎猜。

71
00:07:15,267 --> 00:07:19,461
它的重试不是基于修正错误，而是基于赌运气。

72
00:07:19,461 --> 00:07:25,134
给模型更高的自由度，不是在提升能力上限，而是在放大不确定性。

73
00:07:25,134 --> 00:07:33,440
它确实能写出更聪明的命令，但一旦出错，连你自己都排查不了它在哪一步聪明反被聪明误了。

74
00:07:33,580 --> 00:07:36,630
后来的做法是把终端工具降级。

75
00:07:36,630 --> 00:07:44,213
不是删掉，而是明确它的定位，只处理那些原子工具覆盖不到的边角需求，不走主链路。

76
00:07:44,213 --> 00:07:49,561
高频操作全部拆成原子工具，每个工具都有明确的状态码。

77
00:07:49,561 --> 00:07:59,802
成功，任务完成，结果放在数据字段里；部分成功，任务完成但内容被截断；失败，错误字段里有具体的错误码。

78
00:07:59,802 --> 00:08:10,775
比如查找工具搜不到文件，返回空数组，意思是确实没有这个文件；路径本身不存在，返回路径未找到；搜索超时，返回超时。

79
00:08:10,775 --> 00:08:19,657
模型能清晰区分确实没有和出错了，路径错了就换路径，超时了就缩小范围，真没找到就如实告诉用户。

80
00:08:19,657 --> 00:08:31,905
终端工具的硬约束也明确了，禁止读取搜索列目录这类已有专门工具的操作，禁止交互式程序，默认禁止网络访问，还有一批命令直接进黑名单。

81
00:08:31,905 --> 00:08:37,410
这样做之后调试简单很多，出了问题看日志就知道是哪一步。

82
00:08:37,410 --> 00:08:44,177
这一章的结论只有一句话，可诊断性是可恢复性的前提，不知道哪坏了就修不好。

83
00:08:44,177 --> 00:08:51,641
在智能体开发里，给模型自由组合命令的能力听起来很美好，实际上是在制造黑盒。

84
00:08:51,641 --> 00:09:00,487
看似高效的管道命令，把错误信息压扁成一个个无法区分的空结果，让模型在错误的道路上越跑越远。

85
00:09:00,487 --> 00:09:09,633
原子工具虽然步骤繁琐，但每一步都有明确的输入输出和状态，出了问题你能定位，模型错了你能纠正。

86
00:09:09,796 --> 00:09:17,041
终端工具那种什么都管的万能模式确实有问题，拆成原子工具之后调试清晰多了。

87
00:09:17,041 --> 00:09:20,705
但他很快又踩了一个新坑，拆得太碎了。

88
00:09:20,705 --> 00:09:27,664
意识到万能工具有问题之后，他走向了另一个极端，把每个功能点都拆成独立工具。

89
00:09:27,664 --> 00:09:42,003
列目录，递归列目录，按文件名查找，按通配符查找，精确匹配搜索，正则匹配搜索，模糊匹配搜索，按行范围读取，按字节偏移读取，读取完整文件，后面还有很多。

90
00:09:42,003 --> 00:09:43,734
问题很快来了。

91
00:09:43,734 --> 00:09:46,631
第一，模型开始选工具困难。

92
00:09:46,631 --> 00:09:52,760
都是找文件，按名查找、按通配符查找、全局匹配，到底用哪个。

93
00:09:52,760 --> 00:09:58,157
模型经常在第一步就卡住，要花好几轮才能确定该用哪一个。

94
00:09:58,157 --> 00:10:12,860
有一次他让模型找一下所有测试文件，模型先递归列了所有文件，然后想用正则搜索来过滤，发现正则是搜内容不是搜文件名，于是又调回递归列举去拿更多上下文，最后才选对。

95
00:10:12,860 --> 00:10:15,901
本来一步搞定的事，用了四步。

96
00:10:15,901 --> 00:10:19,315
第二，结构说明噪声淹没上下文。

97
00:10:19,315 --> 00:10:26,646
每个工具都有参数描述、类型定义、约束条件，十几个工具加起来几千个令牌就出去了。

98
00:10:26,646 --> 00:10:31,827
模型还没开始解决任务，就先消耗大量注意力在读说明书上。

99
00:10:31,827 --> 00:10:39,243
更糟的是长说明容易让模型选择性失明，可能只注意到部分工具，或者把参数搞混。

100
00:10:39,243 --> 00:10:41,767
第三，维护成本爆炸。

101
00:10:41,767 --> 00:10:49,699
按名查找和按通配符查找有八成逻辑是重复的，但因为是两个独立工具，得维护两份代码。

102
00:10:49,699 --> 00:10:53,870
这时候他才意识到，工具系统不是颗粒越细越好。

103
00:10:53,870 --> 00:11:05,000
过度封装和过度拆分本质上都会把系统推向不稳定，只是一个坏在执行期，就是万能工具，一个坏在决策期，就是过度原子化。

104
00:11:05,140 --> 00:11:09,477
后来他给自己定了一个判断框架，频率乘以确定性。

105
00:11:09,477 --> 00:11:25,536
高频强确定的动作必须原子化，一步完成不可再分；中频带副作用的动作必须受控，关键操作加保险；低频弱确定的动作保留弹性，但放到兜底层，而且明确禁止什么，而不是允许什么。

106
00:11:25,536 --> 00:11:29,659
按这个框架，工具体系被重新设计成三层。

107
00:11:29,659 --> 00:11:39,575
这套分层不是架构美学，是被真实故障逼出来的，最大价值是降低模型的决策负担，让高频路径更短更清晰。

108
00:11:39,575 --> 00:11:45,728
最上层是高频原子层，主力武器，使用频率最高，必须极致稳定。

109
00:11:45,728 --> 00:11:57,543
比如查找文件，他一开始也想按匹配方式拆成好几个工具，后来发现这就是过度原子化，模型会纠结到底用精确匹配还是通配符、要不要递归。

110
00:11:57,543 --> 00:12:03,204
最后合并成一个，只做一件事，给定一个模式返回候选路径。

111
00:12:03,204 --> 00:12:13,793
内部实现可以很复杂，支持递归、自动处理大小写、结果排序，但对模型暴露的接口必须简单，它只需要说找所有派森文件。

112
00:12:13,793 --> 00:12:16,161
搜索工具是另一个例子。

113
00:12:16,161 --> 00:12:25,356
内部优先用快速的搜索程序，不可用时自动回退到派森实现，结果按修改时间排序，最近修改的排前面。

114
00:12:25,356 --> 00:12:31,041
但对模型来说，它看到的就是一个稳定入口，返回格式固定。

115
00:12:31,041 --> 00:12:36,414
中间一层是中频受控层，涉及文件修改，是高危操作。

116
00:12:36,414 --> 00:12:39,623
他定了一条硬规则，不读就不能改。

117
00:12:39,623 --> 00:12:46,606
工具注册表会维护一个读缓存，文件没被读过，写入和修改直接返回错误。

118
00:12:46,606 --> 00:12:53,192
这防止了模型凭记忆改文件，它必须先把内容拿到上下文里确认过才能动手。

119
00:12:53,192 --> 00:12:54,779
还有乐观锁。

120
00:12:54,779 --> 00:13:01,269
就算读过了，文件也可能在读之后被外部程序改动，比如编辑器自动保存。

121
00:13:01,269 --> 00:13:09,334
写入和修改会比对文件的修改时间和大小，不匹配就返回冲突错误，模型必须重新读再改。

122
00:13:09,334 --> 00:13:17,808
多点修改工具则保证一次性提交的多个改动要么全成功要么全失败，避免文件停在改了一半的状态。

123
00:13:17,808 --> 00:13:22,279
最下面一层是低频兜底层，就是那个终端工具。

124
00:13:22,279 --> 00:13:29,863
他没删，因为总有原子工具覆盖不到的场景，比如跑测试、装依赖、检查版本管理状态。

125
00:13:29,863 --> 00:13:33,277
但它的定位必须是兜底，不是默认。

126
00:13:33,277 --> 00:13:37,676
约束的核心只有一条，禁止它做高频动作能做的事。

127
00:13:37,676 --> 00:13:44,502
模型试图用它列目录，就会收到提示让它改用专门的列表工具，不让它抄近道。

128
00:13:44,502 --> 00:13:58,036
为什么不干脆删掉，因为完美原子化不现实，总有些长尾需求，跑个自定义脚本，检查系统环境变量，执行项目特定的构建命令，频率太低不值得专门做工具。

129
00:13:58,036 --> 00:14:05,212
关键是它的存在不能影响主链路的稳定性，它必须是最后手段，不是默认入口。

130
00:14:05,212 --> 00:14:07,075
最后还有两件事。

131
00:14:07,075 --> 00:14:19,923
一是统一响应协议，所有工具不论哪一层都返回统一格式，模型不需要学不同工具的返回格式，错误处理只要看状态和错误码，调试时输出结构一致。

132
00:14:19,923 --> 00:14:33,661
二是工具注册表，它负责汇总参数定义，自动为写入类操作注入乐观锁字段，还有一个熔断机制，工具连续失败会被临时禁用，防止模型在坏工具上死循环。

133
00:14:33,796 --> 00:14:35,933
现在把三个坑串起来。

134
00:14:35,933 --> 00:14:42,650
第一个坑是照抄复杂架构，第二个是放开命令组合，第三个是工具拆得过细。

135
00:14:42,650 --> 00:14:52,530
三件事方向完全相反，一个是太复杂，一个是太自由，一个是太琐碎，失败方式却一模一样，都是让系统变得不可观测。

136
00:14:52,530 --> 00:15:07,870
复杂架构让错误在组件之间传来传去，你不知道是哪一层出的；管道命令把中间状态压扁，模型只看到空；过度原子化则把不确定性搬到了决策期，模型在选工具这一步就迷路了。

137
00:15:07,870 --> 00:15:17,053
所以那句话可以这么说，给模型自由不是在提升能力上限，而是在放大不确定性，除非你同时把可观测性做上去。

138
00:15:17,053 --> 00:15:19,541
给你带走五条判断依据。

139
00:15:19,541 --> 00:15:22,185
第一，先问能不能看见。

140
00:15:22,185 --> 00:15:27,822
设计任何一步之前先问，如果它失败了，我和模型分别能看到什么。

141
00:15:27,822 --> 00:15:33,471
如果答案只是看到一个空结果，那这一步设计得再优雅也要重来。

142
00:15:33,471 --> 00:15:35,671
第二，把状态分开。

143
00:15:35,671 --> 00:15:43,531
成功、部分成功、失败、路径不存在、超时，必须是不同的信号，不能合并成一个空。

144
00:15:43,531 --> 00:15:49,036
模型分不清真的没有和出错了，它后面所有重试都是在赌运气。

145
00:15:49,036 --> 00:15:52,137
第三，一个动作只做一件事。

146
00:15:52,137 --> 00:16:00,478
凡是会把多步逻辑打包成一个动作的机制都会丢掉中间状态，管道是典型，但绝不只是管道。

147
00:16:00,478 --> 00:16:04,228
第四，用频率乘以确定性给工具分层。

148
00:16:04,228 --> 00:16:16,464
高频强确定的动作原子化一步到位，中频带副作用的动作先读后写、加乐观锁、保证原子性，低频弱确定的动作留个口子但明确写禁止什么。

149
00:16:16,464 --> 00:16:18,916
第五，控制工具总量。

150
00:16:18,916 --> 00:16:28,531
每加一个工具都要算一算它的结构说明要吃掉多少上下文，能合并的就合并，内部实现可以复杂，对外的口子一定要少。

151
00:16:28,531 --> 00:16:33,002
最后补一句，这可能是这一集最反直觉的一句。

152
00:16:33,002 --> 00:16:36,271
可控性比一次性完成任务重要得多。

153
00:16:36,271 --> 00:16:45,923
一个笨一点但每一步都能看见的系统，会慢慢变好；一个聪明但会制造黑盒的系统，你连它什么时候错都不知道。

154
00:16:45,923 --> 00:16:54,156
下一集接着讲踩坑实录的下半部分，换到提示词、上下文、可观测性和通用方法论。
