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

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

3
00:00:09,254 --> 00:00:15,925
这一集要解决的工程问题很具体，一条工具输出有两兆字节，它接下来该去哪。

4
00:00:15,925 --> 00:00:17,752
为什么值得关心。

5
00:00:17,752 --> 00:00:23,533
这是每一个智能体系统都会撞上的问题，而且越到后期越频繁。

6
00:00:23,533 --> 00:00:29,254
搜索命中几万行，抓回一整页文档，读一个超大的日志文件。

7
00:00:29,254 --> 00:00:34,663
这些结果如果处理不好，要么把上下文撑爆，要么把关键信息弄丢。

8
00:00:34,663 --> 00:00:37,043
先说只有三个候选答案。

9
00:00:37,043 --> 00:00:44,987
选项一，整个塞进上下文，下一次模型请求直接被它占满，钱包和上下文窗口一起遭殃。

10
00:00:44,987 --> 00:00:54,038
选项二，砍掉超出的部分，省是省了，可万一模型后面要找的正是被砍掉的那行报错，任务就卡死了。

11
00:00:54,038 --> 00:01:06,718
选项三是这套系统的做法，全文落盘存档，上下文里只留首尾预览，外加一句提示，告诉模型全文存在哪个路径、用读文件或者搜索就能捞。

12
00:01:06,718 --> 00:01:09,519
丢出去的信息随时找得回来。

13
00:01:09,519 --> 00:01:13,485
一句话概括，截断丢信息，溢写找得回。

14
00:01:13,485 --> 00:01:18,990
这一集就讲这件事怎么落地，以及它背后那套很讲究的守门逻辑。

15
00:01:18,990 --> 00:01:22,548
再补一句为什么要专门做成一个插件。

16
00:01:22,548 --> 00:01:32,692
因为大输出这件事没有统一答案，多大算大、预览留多少、凭证怎么写，不同团队、不同模型、不同任务的答案都不一样。

17
00:01:32,692 --> 00:01:37,067
做成可配置的策略插件，比写死在内核里合适。

18
00:01:37,067 --> 00:01:43,125
而且它挂在后处理事件上，意味着换一套策略不用动内核一行代码。

19
00:01:43,125 --> 00:01:45,324
这一集的内容分三块。

20
00:01:45,324 --> 00:01:54,675
先讲五步决策和三个容易混淆的点，再讲存储失败为什么不算失败，最后讲凭证长什么样、以及三家怎么对比。

21
00:01:54,675 --> 00:01:57,908
末尾收五条可以带走的原则。

22
00:01:58,060 --> 00:02:05,954
干这件事的插件挂在工具执行流水线的后处理事件上，等一条工具结果彻底定稿后才出手。

23
00:02:05,904 --> 00:02:07,972
整个决策只有五步。

24
00:02:07,972 --> 00:02:09,991
第一步，先委托。

25
00:02:09,991 --> 00:02:14,762
先让下游监听器把结果结算好，自己再对定稿动手。

26
00:02:14,762 --> 00:02:21,866
这个顺序很关键，意味着哪怕别的插件换过内容，换上来的内容照样被管住。

27
00:02:21,866 --> 00:02:24,197
第二步，纯文本检查。

28
00:02:24,197 --> 00:02:30,472
结果里混进任何一个非文本块，比如一张截图，整个不碰，原样保留。

29
00:02:30,472 --> 00:02:36,409
策略只认识最终格式化文本，不懂工具内部结构，所以宁可不碰。

30
00:02:36,409 --> 00:02:40,183
出处是插件源码的第八十到八十七行。

31
00:02:40,183 --> 00:02:42,298
第三步，字节阈值。

32
00:02:42,298 --> 00:02:47,154
按 UTF-8 大小量，超过上限才动手，没超就放行。

33
00:02:47,154 --> 00:02:49,041
第四步，落盘。

34
00:02:49,041 --> 00:02:51,685
全文原样写进会话存档。

35
00:02:51,685 --> 00:02:53,464
第五步，替换。

36
00:02:53,464 --> 00:02:57,851
首尾预览加取回凭证进上下文，原文不再占地方。

37
00:02:57,851 --> 00:03:00,856
为什么留首尾而不是只留开头。

38
00:03:00,856 --> 00:03:10,988
因为工具输出的信息分布常常是两头重，开头是命令回显和摘要，结尾是最终的报错或者结论，中间才是大段重复内容。

39
00:03:10,988 --> 00:03:16,541
只留开头会丢掉最有价值的报错；只留结尾会丢掉上下文。

40
00:03:16,541 --> 00:03:22,286
首尾都留，用一行省略说明连起来，模型能判断要不要去捞全文。

41
00:03:22,286 --> 00:03:31,673
这也是预览和截断的本质差别，截断是单向的，一旦截掉就没了；预览是有指向的，它告诉模型剩下的在哪。

42
00:03:31,673 --> 00:03:34,041
守门的顺序也值得记。

43
00:03:34,041 --> 00:03:40,820
入口是一串放行判断，四条不碰的理由挨个查，任何一条命中就原样放行。

44
00:03:40,820 --> 00:03:43,188
都没命中，再量字节。

45
00:03:43,188 --> 00:03:44,943
全过了才动手。

46
00:03:44,943 --> 00:03:51,121
出处是 spill 策略包下 index.ts 的第一百九十四到二百零九行。

47
00:03:51,270 --> 00:03:53,828
第一点，只处理纯文本。

48
00:03:53,778 --> 00:03:59,355
这一条刚说过，但值得再强调一次，因为它的失败方向是保守的。

49
00:03:59,355 --> 00:04:06,566
混了非文本块就整条不碰，宁可让上下文胖一点，也不冒险去处理自己看不懂的结构。

50
00:04:06,566 --> 00:04:09,475
第二点，读文件工具被跳过。

51
00:04:09,475 --> 00:04:18,742
模型面向的那一臂明确跳过读文件，防止读文件的输出被溢写成文件、模型再读、再溢写的死循环。

52
00:04:18,742 --> 00:04:21,795
这一条特别能体现设计者的细心。

53
00:04:21,795 --> 00:04:27,672
日志那一臂不跳，因为日志副本进不了模型上下文，循环不成立。

54
00:04:27,672 --> 00:04:35,052
同一个机制在两条臂上有不同处理，依据是循环能不能成立，不是图省事统一。

55
00:04:35,052 --> 00:04:41,831
出处是同文件第一百九十五到一百九十七行、第二百一十九到二百二十二行注释。

56
00:04:41,831 --> 00:04:44,523
第三点，凭证不是路径。

57
00:04:44,523 --> 00:04:52,828
取回凭证是一个不透明句柄，本地后端给的是文件路径，远程后端可以给统一资源标识或者键。

58
00:04:52,828 --> 00:05:01,133
消费方不解析它，按后端附带的取回提示渲染话术，不假定读文件永远是正确的取回方式。

59
00:05:01,133 --> 00:05:04,571
出处是溢出子系统文档的第七十行。

60
00:05:04,571 --> 00:05:12,023
这三个点放在一起，能看出一个共同的态度，凡是自己判断不了的地方，一律保守处理。

61
00:05:12,023 --> 00:05:14,090
为什么保守是对的。

62
00:05:14,090 --> 00:05:19,246
因为溢写这个动作本身是有风险的，它把内容搬出了上下文。

63
00:05:19,246 --> 00:05:25,593
搬出去容易，万一搬错了、或者搬出去之后取不回来，损失比不搬大得多。

64
00:05:25,593 --> 00:05:32,047
所以每一个不确定的环节都选择不搬，只在处理得了的那一类内容上动手。

65
00:05:32,047 --> 00:05:36,446
这种保守不是保守，是风险不对称下的正确选择。

66
00:05:36,446 --> 00:05:38,694
再补一条实操提醒。

67
00:05:38,694 --> 00:05:49,980
如果你在写一个自定义工具，想让自己的大输出被这套机制管住，就要保证输出是纯文本，并且不要在结果里混入图片或者其他二进制块。

68
00:05:49,980 --> 00:05:54,619
混了就整条不碰，这一点很多工具作者会踩。

69
00:05:54,770 --> 00:06:00,116
现在讲这一集最反直觉的一处，也是最能看出价值观的一处。

70
00:06:00,066 --> 00:06:04,730
溢写存储写失败了，这次工具调用算成功还是失败。

71
00:06:04,730 --> 00:06:09,405
答案在异常捕获分支里，算成功，一个字都不藏。

72
00:06:09,405 --> 00:06:17,819
磁盘满了、权限不对、后端没挂载，都只换来一条警告日志，然后原始结果原样内联进上下文。

73
00:06:17,819 --> 00:06:26,905
设计文档里的原话说得很直白，溢写失败绝不会把成功的工具调用变成错误结果，也不会隐藏内联结果。

74
00:06:26,905 --> 00:06:28,480
逻辑很朴素。

75
00:06:28,480 --> 00:06:37,951
溢写是省钱的优化，优化失败最多让上下文胖一点，绝不能把一次成功的调用弄成失败，更不能弄丢信息。

76
00:06:37,951 --> 00:06:44,285
出处是 index.ts 的第一百五十三到一百六十一行，以及设计笔记第九十一行。

77
00:06:44,285 --> 00:06:51,496
这里有一条通用的判断标准可以带走，优化路径上的失败，不能改变主路径的结果。

78
00:06:51,496 --> 00:06:58,239
如果一个省钱的优化失败了，就让系统回到不省钱的那种状态，而不是报错。

79
00:06:58,239 --> 00:07:03,696
把优化失败升级成主流程失败，是最常见的过度设计。

80
00:07:03,696 --> 00:07:06,244
再想想反面会发生什么。

81
00:07:06,244 --> 00:07:17,049
如果溢写失败就把这次工具调用标记成错误，模型会看到一条它无法理解的错误，然后可能换一种方式重试，比如再跑一次同样的搜索。

82
00:07:17,049 --> 00:07:24,189
这一次重试既浪费了时间，又拿不到更好的结果，纯粹是为一次磁盘写入失败买单。

83
00:07:24,189 --> 00:07:30,871
更糟的是，失败信息里可能还会把原始内容藏掉，那就真的是把信息弄丢了。

84
00:07:30,871 --> 00:07:36,965
所以这条规则的实质是，不要把基础设施的问题转嫁给模型去处理。

85
00:07:36,965 --> 00:07:45,871
模型能处理的是任务层面的失败，比如文件不存在、参数不对；处理不了的是磁盘满、权限不足。

86
00:07:45,871 --> 00:07:50,186
后者应该在系统层消化掉，最多留一条日志。

87
00:07:50,186 --> 00:07:53,131
还有一个反方向的坑也被堵了。

88
00:07:53,131 --> 00:07:57,494
配置校验放在插件加载时，不放在每次调用时。

89
00:07:57,494 --> 00:08:06,977
一个负数或者小数的上限配置会直接让部署启动失败，因为坏配置该炸的是部署，轮不到某次工具调用背锅。

90
00:08:06,977 --> 00:08:10,920
出处是第一百一十四到一百一十九行。

91
00:08:11,070 --> 00:08:19,553
替换后的模型可见文本是三段式，保留的头部预览、省略说明加凭证、保留的尾部预览。

92
00:08:19,503 --> 00:08:32,953
凭证那一行由提示函数拼出，示例措辞是，省略了若干字节，完整格式化结果存放在某个路径，可以用带偏移和长度参数的读文件，或者对这个路径做搜索。

93
00:08:32,953 --> 00:08:38,878
措辞刻意通用，因为策略只知道最终文本，不了解工具内部资源。

94
00:08:38,878 --> 00:08:48,385
这一句很重要，它意味着凭证里不会出现读某个文件的第几行到第几行这种精确指示，因为策略没有这个信息。

95
00:08:48,385 --> 00:08:52,015
一个细节能看出这套代码的较真程度。

96
00:08:52,015 --> 00:08:57,712
凭证本身的字节数会先从上限预算里扣掉，再算预览能留多少。

97
00:08:57,712 --> 00:09:04,311
不然预览花满预算、凭证再往后一贴，替换文本反而可能比上限还大。

98
00:09:04,311 --> 00:09:12,688
要是凭证一行就超过整个上限，策略干脆放弃溢写、保留内联，绝不违反自己宣称的上限。

99
00:09:12,688 --> 00:09:18,482
出处是第一百七十一到一百七十二行、第一百八十三到一百八十五行。

100
00:09:18,482 --> 00:09:20,909
存档文件本身也讲究。

101
00:09:20,909 --> 00:09:32,099
本地后端把文件写到会话专属目录下面，根目录私有，写入用排他方式且仅所有者可读，预先植入的符号链接没法重定向写入。

102
00:09:32,099 --> 00:09:35,609
出处是溢出子系统文档第八十五行。

103
00:09:35,609 --> 00:09:38,217
这一处为什么值得单独说。

104
00:09:38,217 --> 00:09:44,912
因为溢写把内容写到了磁盘上，而磁盘是共享的、可预测的、容易被攻击的。

105
00:09:44,912 --> 00:09:56,294
如果文件权限放得太开，一个溢写出来的工具输出就可能被同机器的其他用户读到，而工具输出里往往有代码、日志、甚至密钥。

106
00:09:56,294 --> 00:10:03,830
排他写入则挡住了另一类攻击，别人预先放一个符号链接在那里，想把写入重定向到别处。

107
00:10:03,830 --> 00:10:08,169
两处细节都不起眼，但都是在堵真实的漏洞。

108
00:10:08,169 --> 00:10:16,727
同族设计还有附件系统，引用进日志、字节放外部存储，思路一样，正文只留轻量引用。

109
00:10:16,880 --> 00:10:22,250
把守门逻辑和异常分支合起来手推一次，这是本集最实用的一段。

110
00:10:22,200 --> 00:10:25,169
部署配置的上限是五万字节。

111
00:10:25,169 --> 00:10:27,765
三条工具结果先后进来。

112
00:10:27,765 --> 00:10:30,626
第一条，六万字节的纯文本。

113
00:10:30,626 --> 00:10:36,323
守门全过，超过上限，落盘，上下文里留首尾预览加凭证。

114
00:10:36,323 --> 00:10:38,402
存档柜多一个文件。

115
00:10:38,402 --> 00:10:43,907
第二条，二十万字节，但混了一个图片块的浏览器截图结果。

116
00:10:43,907 --> 00:10:49,676
第二步纯文本检查就没过，原样保留，二十万字节全进上下文。

117
00:10:49,676 --> 00:10:51,647
存档柜没有新增。

118
00:10:51,647 --> 00:10:57,573
这一条经常被人误判，以为大就一定会被溢写，其实非文本直接不碰。

119
00:10:57,573 --> 00:11:02,573
第三条，八万字节的纯文本，但落盘时磁盘满了。

120
00:11:02,573 --> 00:11:08,823
守门全过，写入抛异常，捕获后返回不替换，原始结果原样内联。

121
00:11:08,823 --> 00:11:12,729
存档柜没有新增，这次调用仍然算成功。

122
00:11:12,729 --> 00:11:21,419
三条结果三种命运，正好覆盖了这套机制的三条边界，非文本不碰、超限才动、写入失败不改判。

123
00:11:21,419 --> 00:11:24,808
第二条最容易被误判，值得多说一句。

124
00:11:24,808 --> 00:11:31,972
很多人以为只要大就会被溢写，其实字节阈值是第三步，前面还有一道纯文本检查。

125
00:11:31,972 --> 00:11:39,508
一个二十万字节的截图结果，因为混了图片块，第二步就被放行了，二十万字节原样进上下文。

126
00:11:39,508 --> 00:11:45,962
这看起来像是机制的疏漏，实际是刻意的，策略宁可不碰自己看不懂的东西。

127
00:11:45,962 --> 00:11:48,438
第三条也值得多想一层。

128
00:11:48,438 --> 00:11:56,082
磁盘满的情况下保留内联，意味着这一次上下文会比预期大，可能触发后续的压缩。

129
00:11:56,082 --> 00:12:01,647
但这是可接受的连锁反应，因为压缩是另一套机制，它有自己的兜底。

130
00:12:01,647 --> 00:12:06,840
把一个机制的问题交给另一个机制处理，比就地报错好。

131
00:12:06,990 --> 00:12:10,101
横向对比一下，焦点在通用性。

132
00:12:10,051 --> 00:12:18,332
第二家的官方口径是默认把工具响应限制在两万五千个 token，源码对应一个最大结果字符数配置。

133
00:12:18,332 --> 00:12:22,767
超限落盘后会附带说明路径，方向和这里一致。

134
00:12:22,767 --> 00:12:29,558
官方博客还补了一条原则，截断时要告诉智能体为什么截了、怎么拿到完整内容。

135
00:12:29,558 --> 00:12:33,007
这一条和这里的凭证是同一个思路。

136
00:12:33,007 --> 00:12:39,906
第三家工具输出默认上限两万字节，超限截断并随结果返回一个已截断标志。

137
00:12:39,906 --> 00:12:48,308
它的命令行工具是特例，全量输出先写进会话目录的日志文件，截断的部分可以从文件里找回。

138
00:12:48,308 --> 00:12:54,305
只是这份落盘是命令行专属的，别的工具输出被截就是被截了。

139
00:12:54,305 --> 00:12:56,421
差别就在覆盖范围。

140
00:12:56,421 --> 00:13:01,625
第三家只给命令行工具落盘，第二家和这里做成了通用机制。

141
00:13:01,625 --> 00:13:04,630
再补一处第二家值得一提的地方。

142
00:13:04,630 --> 00:13:12,022
它的官方博客还写了一条原则，截断时要告诉智能体为什么截了、怎么拿到完整内容。

143
00:13:12,022 --> 00:13:21,697
这一条和这里的凭证是同一个思路，说明两家在这一点上判断一致，光告诉模型截过了不够，还要告诉它去哪找。

144
00:13:21,697 --> 00:13:26,805
这是个很实用的标准，自己实现截断的时候也该照着做。

145
00:13:26,805 --> 00:13:37,070
这里的版本切得最碎，预览机制归一个库，存储归一个只有单方法抽象服务的接缝，策略插件只决定什么时候溢写、怎么拼凭证。

146
00:13:37,070 --> 00:13:42,815
三个包各管一段，换一个远程存储后端不用动策略一行代码。

147
00:13:42,815 --> 00:13:51,841
设计笔记的替代方案一节还点名了参照对象，做通用默认行为，就是冲着类似通用工具结果持久化去的。

148
00:13:51,990 --> 00:13:53,622
最后收五条。

149
00:13:53,572 --> 00:13:55,976
第一，大输出不要二选一。

150
00:13:55,976 --> 00:14:04,606
塞进去撑爆上下文，砍掉丢失信息，中间还有第三条路，全文落盘、上下文里只留预览加凭证。

151
00:14:04,606 --> 00:14:08,765
判断标准是，丢出去的信息还能不能找回来。

152
00:14:08,765 --> 00:14:15,724
而预览要留首尾，因为工具输出常常两头重，开头是回显、结尾是报错。

153
00:14:15,724 --> 00:14:18,332
第二，守门顺序要保守。

154
00:14:18,332 --> 00:14:23,945
凡是自己判断不了的情况一律不碰，混了非文本块就整条保留。

155
00:14:23,945 --> 00:14:28,500
宁可让上下文胖一点，也不要冒险处理看不懂的结构。

156
00:14:28,500 --> 00:14:31,877
第三，防死循环要按臂分别处理。

157
00:14:31,877 --> 00:14:40,760
跳过读文件工具是为了防止读了又溢写、溢写又读的循环；日志那一臂不跳，是因为循环在那里不成立。

158
00:14:40,760 --> 00:14:44,678
依据是循环能不能成立，不是统一省事。

159
00:14:44,678 --> 00:14:48,560
第四，优化失败不能改变主路径结果。

160
00:14:48,560 --> 00:14:53,007
溢写写失败照样算成功，原始内容原样内联。

161
00:14:53,007 --> 00:14:57,058
坏配置该炸在部署，不该炸在某次调用。

162
00:14:57,058 --> 00:15:02,046
第五，凭证要是不透明句柄，预算要先扣掉自己的开销。

163
00:15:02,046 --> 00:15:09,414
消费方不解析它，按后端给的提示渲染；凭证比上限还大时，干脆放弃这次优化。

164
00:15:09,414 --> 00:15:19,534
还有一条别忘了，存档文件要排他写入、仅所有者可读，落盘这件事把内容移出了受保护的上下文，权限不能放得太开。

165
00:15:19,534 --> 00:15:23,392
一句话收尾，截断丢信息，溢写找得回。

166
00:15:23,392 --> 00:15:30,002
这是两种上下文观，差别不在技术，在你认不认为丢出去的东西将来还要用。

167
00:15:30,002 --> 00:15:33,716
认这个前提，就得为取回留一条路。

