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

2
00:00:06,382 --> 00:00:10,528
这是 DeepSeek Harness 系列的第六集，一次两节合辑。

3
00:00:10,528 --> 00:00:13,545
素材八千多字，我会做取舍。

4
00:00:13,545 --> 00:00:15,444
两节分别讲什么。

5
00:00:15,444 --> 00:00:26,418
第一节讲上下文压缩，为什么把自动收拾拆成主动和被动两个触发器，以及溢出之后允许重试的凭证为什么是一个只增不减的世代号。

6
00:00:26,418 --> 00:00:32,920
第二节讲 token 计量，为什么给压缩决策用的数和给界面显示的数必须是两套。

7
00:00:32,920 --> 00:00:41,766
把它们放在一起的理由是，两节都在讲同一件事，不同消费方要用不同的数，别让一个口径伺候所有人。

8
00:00:41,766 --> 00:00:47,475
第一节里，重试决策不信插件的返回值，只信账本上的计数器。

9
00:00:47,475 --> 00:00:53,677
第二节里，压缩决策不信界面上的百分比，只信自己在边界上现算的数。

10
00:00:53,677 --> 00:00:55,961
开场先说第一节的动机。

11
00:00:55,961 --> 00:01:02,776
上下文窗口是有上限的，token 数是估出来的，估算和真实计数总有出入。

12
00:01:02,776 --> 00:01:08,473
一次算错就直接撞墙，没有第二道防线，这就是单触发器的问题。

13
00:01:08,473 --> 00:01:10,973
再补一个更日常的翻车。

14
00:01:10,973 --> 00:01:18,846
你跑一个长任务，前二十轮都好好的，第二十一轮突然报一个上下文超限，整个任务断在这里。

15
00:01:18,846 --> 00:01:22,391
用户不知道发生了什么，只看到任务失败。

16
00:01:22,391 --> 00:01:28,449
更糟的是，如果这时候有自动重试，还可能进入一个越试越贵的循环。

17
00:01:28,449 --> 00:01:37,620
这一类问题表面上是一次请求失败，背后其实是三件事没分开，什么时候该预防、什么时候该兜底、以及重试凭什么。

18
00:01:37,780 --> 00:01:41,480
先看只做主动阈值这一条路会怎样。

19
00:01:41,430 --> 00:01:44,939
每次请求前量一下，超过八成就收拾。

20
00:01:44,939 --> 00:01:47,343
听起来够了，实际不够。

21
00:01:47,343 --> 00:01:49,242
因为 token 是估的。

22
00:01:49,242 --> 00:01:56,622
一条超大的工具结果突然塞进来，测量还没到阈值，请求已经超限，模型服务直接拒绝。

23
00:01:56,622 --> 00:02:04,470
这时候没有任何补救逻辑接手，这一轮就地报错终止，用户看到的是一次莫名其妙的失败。

24
00:02:04,470 --> 00:02:08,557
所以做法是把这件事拆成两个独立的触发器。

25
00:02:08,557 --> 00:02:16,514
快满了主动收，撞墙了被动救，两条路挂不同的事件、用不同的条件、有不同的失败语义。

26
00:02:16,514 --> 00:02:20,612
第一条叫压力路径，挂在每个步骤开始之前。

27
00:02:20,612 --> 00:02:28,665
先量总 token，超过容量的零点八就动手收拾，收拾时给最近的对话留百分之十六的原文尾巴。

28
00:02:28,665 --> 00:02:32,163
为什么是零点八而不是等到零点九五。

29
00:02:32,163 --> 00:02:36,826
因为留出的那部分余量，是给这一次请求本身准备的。

30
00:02:36,826 --> 00:02:43,040
请求发出去之后还会增长，如果在临界点才动手，收拾完可能马上又满了。

31
00:02:43,040 --> 00:02:47,559
留两成余量，等于给收拾之后的工作留了呼吸空间。

32
00:02:47,559 --> 00:02:50,660
为什么留百分之十六的原文尾巴。

33
00:02:50,660 --> 00:02:57,391
因为最近的对话往往是模型最需要的上下文，全压缩掉会让它忘了刚才在做什么。

34
00:02:57,391 --> 00:03:04,098
这个数字和阈值一样，都是可以按服务商加模型的组合逐一覆盖的，不是写死的。

35
00:03:04,098 --> 00:03:06,201
它的失败语义很松。

36
00:03:06,201 --> 00:03:12,655
收拾中途出了错，日志里记一句就继续走，提前收拾失败了天塌不下来。

37
00:03:12,655 --> 00:03:21,490
第二条叫上下文溢出路径，挂在请求报错之后，只认适配器规范化过的那个超限错误码，其他错误一律放行。

38
00:03:21,490 --> 00:03:26,922
它不看阈值，保留预算直接清零，强制做一次真实的缩减。

39
00:03:26,922 --> 00:03:29,014
它的失败语义很严。

40
00:03:29,014 --> 00:03:33,605
必须给出决定，要么重试，要么保留原始错误上报。

41
00:03:33,605 --> 00:03:41,922
重试上限默认一次，每收到一条成功的模型回复就清零计数，正常干活的会话不会被卡住。

42
00:03:41,922 --> 00:03:58,368
出处是压缩基础包下 index.ts 的第一百四十七到一百六十五行、第一百七十九到二百二十三行，两个默认值在同包 config 的第二十和二十三相，重试上限在第九十三行，都可按服务商加模型的组合逐一覆盖。

43
00:03:58,368 --> 00:04:02,311
一句话概括，上排是预防，下排是兜底。

44
00:04:02,311 --> 00:04:06,806
两条路各自独立，一条失效了另一条照常工作。

45
00:04:06,806 --> 00:04:08,705
为什么长期成立。

46
00:04:08,705 --> 00:04:12,563
把预防和兜底分开，是可靠性工程的通则。

47
00:04:12,563 --> 00:04:18,405
备份和恢复是两套系统，限流和熔断是两道闸门，道理相同。

48
00:04:18,405 --> 00:04:26,325
预防路径追求便宜、常跑、失败无所谓；兜底路径追求可靠、少跑、失败必须有交代。

49
00:04:26,325 --> 00:04:30,700
这两种诉求塞进同一段逻辑里必然互相迁就。

50
00:04:30,840 --> 00:04:36,655
现在讲这一集最值钱的一处，溢出之后怎么确认收拾真的起效了。

51
00:04:36,605 --> 00:04:38,504
先说难点在哪。

52
00:04:38,504 --> 00:04:42,795
压缩是一个开放接缝，第三方可以接自定义后端。

53
00:04:42,795 --> 00:04:48,828
假设某个后端每次都报告成功，但从来没真正改过模型可见的内容。

54
00:04:48,828 --> 00:04:58,504
如果只看返回值就重试，请求原样超限，再报错，再压缩，再重试，每一圈都是白花的接口钱，死循环烧到天亮。

55
00:04:58,504 --> 00:05:00,487
答案是一个世代号。

56
00:05:00,487 --> 00:05:08,588
先说背景，表面层是会话日志里模型可见事件的实时投影，可以理解成模型眼里的那份对话。

57
00:05:08,588 --> 00:05:14,718
世代号是它身上的一个只读计数器，记的是这份对话被替换过几次。

58
00:05:14,718 --> 00:05:21,953
整个代码库只有一处会让它加一，一段旧消息真的被摘要替换、真的落盘的那一刻。

59
00:05:21,953 --> 00:05:25,475
没有任何接口能把它改小或重置。

60
00:05:25,475 --> 00:05:32,458
所以世代号前进了，在数学上等价于至少发生过一次真实的、已落盘的替换。

61
00:05:32,458 --> 00:05:34,177
这就是硬证据。

62
00:05:34,177 --> 00:05:36,713
溢出恢复的用法就三步。

63
00:05:36,713 --> 00:05:43,035
动手收拾之前先拍快照记下当前值；收拾；收拾完拿新值和快照比。

64
00:05:43,035 --> 00:05:48,288
严格变大才允许重试，否则放行，原始错误原样上报。

65
00:05:48,288 --> 00:05:52,578
插件说什么不重要，账本上的数字变没变才重要。

66
00:05:52,578 --> 00:06:07,666
出处是 index.ts 的第一百九十一行、第二百一十八到二百二十二行，世代号的定义在核心包 surface.ts 的第一百三十六到一百四十二行，全库唯一的加一处在第三百六十一到三百七十一行。

67
00:06:07,666 --> 00:06:17,378
项目的设计笔记明确否决过只看返回值的写法，理由是自定义后端可能报告成功却没有改变模型可见状态。

68
00:06:17,378 --> 00:06:22,162
一句话记住，世代号没前进，一次重试都不放行。

69
00:06:22,300 --> 00:06:27,237
这里有个反方向的细节，特别能说明这套标准的纯粹。

70
00:06:27,187 --> 00:06:36,346
就算收拾中途抛了异常，只要前面的免费剪枝已经落盘、世代号已经前进，这份进展照样够格授权重试。

71
00:06:36,346 --> 00:06:37,993
注意这个判断。

72
00:06:37,993 --> 00:06:42,644
它不看过程是否顺利，只看账本上有没有真实进展。

73
00:06:42,644 --> 00:06:45,180
异常也算，只要证据在。

74
00:06:45,180 --> 00:06:49,423
反过来，过程顺利但账本没动，也不放行。

75
00:06:49,423 --> 00:06:52,644
两边用的是同一条标准，凭证据。

76
00:06:52,644 --> 00:06:55,805
现在手推一个谎报成功的后端。

77
00:06:55,805 --> 00:07:04,495
设重试上限为一，装一个自定义压缩后端，每次都报告收拾成功，但从不真正替换模型可见的内容。

78
00:07:04,495 --> 00:07:06,779
第一次溢出报错发生了。

79
00:07:06,779 --> 00:07:15,649
推演结果，世代号不变，对账失败，原始错误直接上报，这一轮就结束了，重试计数根本没机会增加。

80
00:07:15,649 --> 00:07:23,594
如果改成只看返回值，同一场景下每圈耗时五秒，第一分钟会发出十二次注定失败的请求。

81
00:07:23,594 --> 00:07:26,046
为什么这个套路长期成立。

82
00:07:26,046 --> 00:07:36,442
用单调递增的版本号证明状态确实变了，数据库乐观锁用了几十年，版本提交的链条、分布式系统里的纪元，都是它的变体。

83
00:07:36,442 --> 00:07:42,464
好处是把信任问题变成算术问题，执行者可以撒谎，账本不会。

84
00:07:42,464 --> 00:07:50,829
只要系统里存在不受信任的扩展点，重试之前核对一个改不了的计数器，永远是最便宜的防线。

85
00:07:50,980 --> 00:07:56,182
横向对比一下，焦点就一个，压缩失败会不会循环烧钱。

86
00:07:56,132 --> 00:08:04,485
第一家主动阈值路径和这里的压力路径同构，默认百分之八十五触发，另有默认关闭的两段式预摘要。

87
00:08:04,485 --> 00:08:13,331
请求报错后的被动恢复加世代号对账这条路，在已核对的材料里没有见到等价机制，这一条保留未知项。

88
00:08:13,331 --> 00:08:21,024
第二家主动方向做得最厚，每次调接口前要过裁剪、微压缩、折叠、全量摘要四道工序。

89
00:08:21,024 --> 00:08:26,721
它防烧钱的答案是计数熔断，自动压缩连续失败三次就停手。

90
00:08:26,721 --> 00:08:29,017
这个三来自真实事故。

91
00:08:29,017 --> 00:08:37,394
源码注释记载曾有一千二百七十九个会话连续失败五十次以上，全球每天浪费约二十五万次接口调用。

92
00:08:37,394 --> 00:08:39,894
两家的差别值得说清楚。

93
00:08:39,894 --> 00:08:50,807
第二家数失败次数，数到三就熔断，止损线是拿事故数据校准出来的，属于计数器止损，先允许问题发生几次，再靠上限兜住。

94
00:08:50,807 --> 00:09:02,130
这里不数次数，要求每次重试都出示世代号前进的证据，一次无效重试都不放行，属于结构性证明，让无效重试从机制上发不出去。

95
00:09:02,130 --> 00:09:07,057
前者的三需要事故喂出来，后者的零是推导出来的。

96
00:09:07,057 --> 00:09:08,980
这就是差别。

97
00:09:09,120 --> 00:09:10,896
现在讲第二节。

98
00:09:10,846 --> 00:09:14,176
先给结论，两个数回答的问题不一样。

99
00:09:14,176 --> 00:09:19,212
压缩决策要回答，此刻这个会话如果发请求会有多大。

100
00:09:19,212 --> 00:09:21,423
答案必须准，可以贵。

101
00:09:21,423 --> 00:09:30,438
界面状态行只要给用户看个占用率，答案必须便宜、持久、重连后立刻能显示，准到小数点没有意义。

102
00:09:30,438 --> 00:09:33,995
所以干脆各修一条路，谁也不迁就谁。

103
00:09:33,995 --> 00:09:36,628
决策用的数要准，可以贵。

104
00:09:36,628 --> 00:09:39,392
展示用的数要便宜，可以糙。

105
00:09:39,392 --> 00:09:46,147
这一句听着像常识，实际做起来最容易犯的错是让两个消费方共用一个数。

106
00:09:46,147 --> 00:09:52,902
共用之后必然有一方不满意，然后开始在另一方的要求上打补丁，最后两边都别扭。

107
00:09:52,902 --> 00:09:57,649
彻底分开反而简单，因为每一方只需要满足自己的约束。

108
00:09:57,649 --> 00:10:00,065
决策这条路叫重放。

109
00:10:00,065 --> 00:10:05,690
每次被调用，都把持久日志的当前尾部折叠成一份不可变快照。

110
00:10:05,690 --> 00:10:21,764
最近一次成功请求的服务商用量如果能匹配当前请求信封、且总量不低于它的完整启发式锚点，就拿来当锚；表面层此后的增减用有符号增量重新定价；没有可复用的锚就整体按固定启发式定价。

111
00:10:21,764 --> 00:10:30,826
代价也明明白白，每次调用是随表面层大小线性增长的，所以只有压缩这样的决策方在自己的请求边界调它。

112
00:10:30,826 --> 00:10:33,086
展示这条路叫投影。

113
00:10:33,086 --> 00:10:38,723
它是普通的持久会话投影状态，只有两个各自后者胜的字段。

114
00:10:38,723 --> 00:10:46,571
一个是压力 token，最近一次请求报告的提示词侧规模，口径是输入加缓存读写、不含输出。

115
00:10:46,571 --> 00:10:52,052
另一个是上下文窗口，来自最新一条请求上下文日志记录。

116
00:10:52,052 --> 00:10:56,223
分子分母各写各的，从不凑成一次原子观测。

117
00:10:56,223 --> 00:11:02,797
出处是计量器文档的对应小节，以及 usage-projection.ts 的第七十到七十二行。

118
00:11:02,930 --> 00:11:10,548
光有压力 token 有个尴尬，它只在请求报用量时更新，流式期间一动不动，更看不见压缩。

119
00:11:10,498 --> 00:11:17,337
压缩替换了一大段内容，状态行上的数却纹丝不动，用户会以为压缩没干活。

120
00:11:17,337 --> 00:11:25,269
所以折叠函数顺手带一份表面层的运行总量，公布的是样本加上此后表面层的有符号变动。

121
00:11:25,269 --> 00:11:32,457
源码注释把意图写得很直白，占用率回答的是下一次请求的大小，而不是上一次。

122
00:11:32,457 --> 00:11:38,370
效果就是，压缩刚落盘、一个新请求都没发，投影已经掉下来了。

123
00:11:38,370 --> 00:11:43,394
出处是 usage-projection.ts 的第一百五十到一百六十一行注释。

124
00:11:43,394 --> 00:11:45,342
还有一个时序细节。

125
00:11:45,342 --> 00:11:55,125
用量样本在同一事件加入表面层之前盖章，所以助手消息锚定的是它自己那次请求看到的表面层，增量的起点不会错位。

126
00:11:55,125 --> 00:12:09,428
整个投影的对外视图只有七行，第三个展开项是全课的题眼，压力 token 加表面层 token 减去取样时的表面层 token，样本加上取样之后的净变动，再用取最大值兜住下界。

127
00:12:09,428 --> 00:12:13,659
两个来源字段缺一个，对应的输出干脆不出现。

128
00:12:13,659 --> 00:12:18,406
出处是同文件第一百九十八到二百零四行。

129
00:12:18,550 --> 00:12:22,514
再讲两处边界，都能顺着那段代码推出来。

130
00:12:22,464 --> 00:12:24,952
第一处，服务商不回用量。

131
00:12:24,952 --> 00:12:32,103
投影侧的压力 token 一直缺席，第二、三个展开项都不出现，界面干脆不显示占用率。

132
00:12:32,103 --> 00:12:37,560
设计笔记明确，只有压力与容量都已知时才显示占用率。

133
00:12:37,560 --> 00:12:42,392
决策侧不受影响，重放退化为启发式锚，照常给数。

134
00:12:42,392 --> 00:12:45,517
第二处，占用率的非原子分叉。

135
00:12:45,517 --> 00:12:51,899
换模型时，新上下文窗口立刻生效，压力 token 还是上一个路由的旧样本。

136
00:12:51,899 --> 00:12:56,202
占用率此刻是近似值，直到下一个请求报用量。

137
00:12:56,202 --> 00:12:59,663
文档明说这是取舍，不当缺陷处理。

138
00:12:59,663 --> 00:13:11,947
原文那句辩护词值得记住，确实需要同一边界精确数字的消费方，应在自己的请求边界调用重放方法，那里两个值同时可得，而不是读取该投影。

139
00:13:11,947 --> 00:13:13,954
还有一条口径提醒。

140
00:13:13,954 --> 00:13:17,596
压力 token 只算提示词侧，不含输出。

141
00:13:17,596 --> 00:13:23,918
它描述的是发出去的请求有多大，和计费总量是两个投影单元，别混。

142
00:13:23,918 --> 00:13:32,488
最要紧的一句是，整个框架中没有任何环节依据占用率百分比做决策，压缩直接读重放方法。

143
00:13:32,488 --> 00:13:37,067
界面上那个数再漂亮，也进不了决策函数的参数表。

144
00:13:37,067 --> 00:13:39,964
这一条为什么值得单独强调。

145
00:13:39,964 --> 00:13:45,337
因为展示用的数有一个天然诱惑，它便宜、随时可得、还好看。

146
00:13:45,337 --> 00:13:55,469
一旦有人在决策里图省事读它，系统的正确性就绑在了一个允许近似的值上，而这个允许是写进文档的，不算缺陷。

147
00:13:55,469 --> 00:14:00,817
所以必须有一条明确的纪律挡住这条路，不能靠自觉。

148
00:14:00,980 --> 00:14:02,612
最后收五条。

149
00:14:02,562 --> 00:14:06,024
第一，预防和兜底要分成两个触发器。

150
00:14:06,024 --> 00:14:13,476
预防路径便宜、常跑、失败无所谓；兜底路径可靠、少跑、失败必须有交代。

151
00:14:13,476 --> 00:14:17,093
两种诉求塞进一段逻辑必然互相迁就。

152
00:14:17,093 --> 00:14:20,928
第二，重试要出示证据，不要信口头汇报。

153
00:14:20,928 --> 00:14:26,312
用一个只增不减、只有真实落盘才会前进的计数器做对账。

154
00:14:26,312 --> 00:14:29,185
插件可以撒谎，账本不会。

155
00:14:29,185 --> 00:14:37,562
第三，止损有两种，一种靠事故数据校准出上限，一种靠结构性证明让无效动作发不出去。

156
00:14:37,562 --> 00:14:42,694
后者不需要等事故发生，代价是你要先想清楚什么是硬证据。

157
00:14:42,694 --> 00:14:45,940
第四，不同消费方要用不同的数。

158
00:14:45,940 --> 00:14:54,245
决策的数准而贵，在自己的边界现算；展示的数糙而便宜，要持久、要重连后立即可用。

159
00:14:54,245 --> 00:14:56,877
别让一个口径伺候所有人。

160
00:14:56,877 --> 00:15:01,300
第五，承认近似值，并把辩护词写进文档。

161
00:15:01,300 --> 00:15:05,471
占用率的非原子性是设计决策，不是缺陷。

162
00:15:05,471 --> 00:15:10,387
关键是明确告诉需要精确值的消费方该走哪条路。

163
00:15:10,387 --> 00:15:15,627
一句话收尾，看到界面上的百分比，记住没有任何决策读它。

164
00:15:15,627 --> 00:15:20,916
真正做决定的那一个数，永远在决策方自己的边界上现算。

