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

2
00:00:06,382 --> 00:00:14,507
这一集讲一件能直接省下真金白银的事，而很多团队正在为它白付钱，而且完全不知道自己在付。

3
00:00:14,507 --> 00:00:29,571
它不像慢查询那样会报警，也不像报错那样会被人看见，它只是安静地躺在每一条请求的账单里，直到某天有人把缓存命中率拉出来看一眼，才发现百分之九十以上的前缀本来是可以免费的。

4
00:00:29,571 --> 00:00:36,494
你的 agent 每一条请求，开头都带着同一段 system prompt 加工具 schema，动辄几千 token。

5
00:00:36,494 --> 00:00:40,340
你以为这些 token 是便宜的，因为它们是重复的。

6
00:00:40,340 --> 00:00:45,075
但重复本身不构成便宜，逐字重复才构成便宜。

7
00:00:45,075 --> 00:00:48,946
改了开头一个字，后面全部按原价重算。

8
00:00:48,946 --> 00:01:01,157
所以 DeepSeek Harness 得出一个很硬的结论，prompt 前缀不是文案，它是模型这个 API 的接口，它的稳定性是一种兼容性承诺，要像维护公开接口一样去维护。

9
00:01:01,157 --> 00:01:03,249
今天我们拆三件事。

10
00:01:03,249 --> 00:01:08,164
为什么改前缀里的一个字会让后面所有 token 的缓存作废。

11
00:01:08,164 --> 00:01:12,083
DSH 靠哪三件事把这个承诺落到工程上。

12
00:01:12,083 --> 00:01:17,960
以及 Claude Code 用一次 10.2% 的真金白银教训，验证了同一个道理。

13
00:01:17,960 --> 00:01:26,374
这三件事听着分散，其实是一条因果链，先理解机制，再看治理办法，最后看别人付过的学费。

14
00:01:26,524 --> 00:01:29,022
先把 KV Cache 说成人话。

15
00:01:28,972 --> 00:01:36,411
模型处理请求的时候，会为每个 token 算出一堆中间结果，也就是键和值，缓存起来。

16
00:01:36,411 --> 00:01:46,087
下一条请求进来，只要开头的 token 序列跟上一条逐字相同，这段前缀的计算就能直接复用，provider 按命中给你打折。

17
00:01:46,087 --> 00:01:54,380
DeepSeek 的官方定价里，命中缓存的输入 token 比未命中便宜一个数量级，具体倍率以官方价目页为准。

18
00:01:54,380 --> 00:01:57,313
关键在于逐字相同这四个字。

19
00:01:57,313 --> 00:02:03,022
缓存是按前缀匹配的，从第一个不同的 token 起，后面全部作废。

20
00:02:03,022 --> 00:02:08,551
注意不是从那个位置往后的一小段作废，是后面整条线全部作废。

21
00:02:08,551 --> 00:02:11,171
那 agent 请求的开头是什么。

22
00:02:11,171 --> 00:02:17,289
是 system prompt 加工具 schema，动辄几千 token，而且每条请求都带着。

23
00:02:17,289 --> 00:02:24,440
于是这几千 token 的稳定性，会直接乘以你的请求量，变成账单上一笔看得见的钱。

24
00:02:24,440 --> 00:02:33,058
一个每天跑一万次请求的系统，和一个每天跑十次的系统，面对同一个前缀抖动，损失差三个数量级。

25
00:02:33,058 --> 00:02:35,678
这里有个很容易搞错的地方。

26
00:02:35,678 --> 00:02:41,387
很多人以为缓存是按片段匹配的，改哪一段就只重算哪一段。

27
00:02:41,387 --> 00:02:42,385
不是。

28
00:02:42,385 --> 00:02:47,589
它是从前往后的一条线，前面断了，后面整条线都得重来。

29
00:02:47,589 --> 00:02:56,123
这个误解非常普遍，也是很多团队优化半天不见效的原因，他们优化了后面的内容，而断点在前面。

30
00:02:56,123 --> 00:03:04,332
所以在动手优化之前，先做一件事，把你的 system prompt 从头到尾扫一遍，找出第一个会变的位置。

31
00:03:04,332 --> 00:03:10,666
优化永远应该从那个位置开始，而不是从你觉得最啰嗦的那一段开始。

32
00:03:10,804 --> 00:03:15,850
顺着这个机制往下推，就能看出哪些操作是缓存杀手。

33
00:03:15,800 --> 00:03:17,554
改 persona 里一个词。

34
00:03:17,554 --> 00:03:19,994
从那个词的位置起全废。

35
00:03:19,994 --> 00:03:22,098
换一下工具的顺序。

36
00:03:22,098 --> 00:03:32,002
两组内容完全相同、只是顺序不同的工具 schema，对缓存来说是两个完全不同的前缀，从第一个顺序不同的位置起全废。

37
00:03:32,002 --> 00:03:34,562
在开头塞一个当前时间。

38
00:03:34,562 --> 00:03:37,591
这一条最隐蔽，也最常见。

39
00:03:37,591 --> 00:03:46,028
时间每秒都在变，等于你每一条请求都在向 provider 宣告，我的前缀是全新的，请全部按原价计费。

40
00:03:46,028 --> 00:03:52,134
但有一类操作不伤缓存，而且它正好解释了前面几讲讲过的一个设计。

41
00:03:52,134 --> 00:03:59,345
对话历史是只追加地增长的，新内容跟在可复用前缀后面，不会打翻已有的缓存。

42
00:03:59,345 --> 00:04:03,756
旧前缀原样保留，你只为新增的那部分付全价。

43
00:04:03,756 --> 00:04:08,384
这就是会话日志只追加这个设计在账单上的红利。

44
00:04:08,384 --> 00:04:16,377
前面讲持久化的时候我们只讲了它对一致性的好处，这里补上另一半，它对钱包同样有好处。

45
00:04:16,377 --> 00:04:20,054
把这两类放在一起，一条原则就出来了。

46
00:04:20,054 --> 00:04:26,737
越靠前的内容越碰不得，把易变的东西往后放，把万年不变的身份和 schema 往前放。

47
00:04:26,737 --> 00:04:29,297
这里可以顺手记一个估算式。

48
00:04:29,297 --> 00:04:38,528
一次抖动的损失，约等于前缀总 token 减去仍然能复用的那一段，乘以请求次数，再乘以命中与未命中的差价。

49
00:04:38,528 --> 00:04:50,571
三个变量里，团队通常只盯着请求次数，但真正好改、而且改动成本最低的是第一个，把易变内容往后挪，就是在扩大仍然能复用的那一段。

50
00:04:50,716 --> 00:04:57,456
所以 DSH 把前缀稳定性当成兼容性承诺来维护，落到工程上是三件事。

51
00:04:57,406 --> 00:05:00,039
第一件，写进文档纪律。

52
00:05:00,039 --> 00:05:07,947
本地快照里 packages 下 268 个包的 README，有 215 个都带一个固定的缓存影响小节。

53
00:05:07,947 --> 00:05:13,560
任何会出现在模型请求里的东西，文档必须按三段式交代清楚。

54
00:05:13,560 --> 00:05:18,152
模型看到什么，token 成本多少，对缓存有什么影响。

55
00:05:18,152 --> 00:05:20,243
举一个真实的例子。

56
00:05:20,243 --> 00:05:34,041
工具 schema 那节的原文是这样写的，只要可见的工具定义及其顺序不变，前缀就稳定；注册、卸载或者按作用域限制工具，都可能从第一个变化的 schema token 起使缓存复用失效。

57
00:05:34,041 --> 00:05:37,791
同一个文件的另一处还有一句反向陈述。

58
00:05:37,791 --> 00:05:45,363
工具调用的历史和结果是只追加的，新内容跟在可复用前缀后面，不会打翻已有的缓存。

59
00:05:45,363 --> 00:05:50,111
什么伤缓存，什么不伤，全部写成可查的文档条目。

60
00:05:50,111 --> 00:05:54,197
这一步看着最笨，但它解决的是一个真问题。

61
00:05:54,197 --> 00:06:01,493
缓存影响是隐性的，写代码的人根本不知道自己改的那行字会让别人的账单翻倍。

62
00:06:01,493 --> 00:06:10,808
只有把影响写进文档，它才能进入 review 的视野，才能被别人在评审时问一句，这行改动会不会动到前缀。

63
00:06:10,948 --> 00:06:14,539
第二件，工具顺序由中心列表规范化。

64
00:06:14,489 --> 00:06:20,379
工具 schema 是前缀的大头，而它的顺序原本跟着插件注册顺序走。

65
00:06:20,379 --> 00:06:24,730
插件是并发加载的，注册顺序会随环境抖动。

66
00:06:24,730 --> 00:06:33,780
DSH 在 CI 里真的观察到了不同的请求头，也就是说同一个代码版本在不同的机器上发出了字节不一样的请求。

67
00:06:33,780 --> 00:06:42,963
顺序影响请求字节，请求字节影响缓存，于是顺序成了必须显式治理的对象，绝不能交给环境噪声决定。

68
00:06:42,963 --> 00:06:45,715
治理的办法是一张中心列表。

69
00:06:45,715 --> 00:06:54,381
配置里的定序列表统一定序，列表里必须恰好有一个代表其余工具的占位标记，没配列表就按字典序兜底。

70
00:06:54,381 --> 00:07:01,473
规范化发生在组装阶段、waterfall 之前，注册顺序在任何可观测的位置都不再出现。

71
00:07:01,473 --> 00:07:04,045
两个边界条件值得记住。

72
00:07:04,045 --> 00:07:09,333
第一，插件热重载之后注册顺序变了，工具顺序会变吗。

73
00:07:09,333 --> 00:07:16,268
不会，因为中心列表在 waterfall 之前就已经规范化，注册顺序根本无处可观测。

74
00:07:16,268 --> 00:07:20,114
第二，列表里写了个拼错的工具名会怎样。

75
00:07:20,114 --> 00:07:31,376
轮次在组装时直接失败，不开步骤、不记请求头、不向适配器发请求，每个轮次都同样失败直到配置修好，而进程本身保持运行。

76
00:07:31,376 --> 00:07:34,585
源码里还有一个细节很能说明态度。

77
00:07:34,585 --> 00:07:39,057
工具提供方如果敢占用那个保留名，直接抛异常。

78
00:07:39,057 --> 00:07:45,980
保留名这种东西，不允许被业务拿来用，这是一条硬边界，不是可以商量的约定。

79
00:07:46,132 --> 00:07:48,545
第三件，严格插值。

80
00:07:48,495 --> 00:07:54,229
persona 是模板，变量组严格按注册表解释，三个分支一个都不放过。

81
00:07:54,229 --> 00:07:55,803
第一个管格式。

82
00:07:55,803 --> 00:08:01,572
变量名不匹配命名正则就抛异常，连空名都走这条格式错误路径。

83
00:08:01,572 --> 00:08:03,219
第二个管注册。

84
00:08:03,219 --> 00:08:11,825
名字不在注册表里就抛异常，报错的时候顺带把全部已注册的变量名列出来，让你一眼看出该用哪个。

85
00:08:11,825 --> 00:08:13,459
第三个管取值。

86
00:08:13,459 --> 00:08:17,474
注册了但这次组装没给值，同样抛异常。

87
00:08:17,474 --> 00:08:22,666
三个分支拦下的其实是同一件事，一个悄悄变形的前缀。

88
00:08:22,666 --> 00:08:25,827
那为什么宁可让整个轮次失败。

89
00:08:25,827 --> 00:08:27,318
理由很直接。

90
00:08:27,318 --> 00:08:37,017
静默容错等于把一个悄悄变形的前缀发给模型，缓存悄悄失效，更糟的是那个坏 prompt 还可能悄悄改变模型的行为。

91
00:08:37,017 --> 00:08:44,794
大声失败反而便宜，因为它立刻可见，立刻可修，不会变成一个三个月后才被发现的账单异常。

92
00:08:44,794 --> 00:08:47,642
源码里还有一个小心思值得学。

93
00:08:47,642 --> 00:08:53,820
查变量有没有注册的时候，用的是自有属性检查，而不是普通属性访问。

94
00:08:53,820 --> 00:09:01,452
因为用普通属性访问去查某些名字，会顺着原型链摸到语言内建的方法，被误认为已经注册。

95
00:09:01,452 --> 00:09:05,707
查自有属性，原型链上的名字一律算未注册。

96
00:09:05,707 --> 00:09:09,108
防的就是这种悄悄放行的坏 prompt。

97
00:09:09,108 --> 00:09:14,193
这两个细节是同一种价值观，宁可报错，绝不蒙混。

98
00:09:14,193 --> 00:09:23,820
把这三件事连起来看，你会发现它们防的都是同一类风险，不是性能问题，而是悄悄发生的、没人会立刻察觉的变形。

99
00:09:23,956 --> 00:09:31,357
横向看 Claude Code，它用一次真实事故验证了同一个道理，而且代价是记在账单上的。

100
00:09:31,307 --> 00:09:34,529
还原源码的注释记录得很清楚。

101
00:09:34,529 --> 00:09:43,435
子代理列表原本嵌在工具描述里，而 MCP 异步连接、插件重载、权限模式切换都会改变这个列表。

102
00:09:43,435 --> 00:09:48,206
工具描述一变，整块工具 schema 的缓存全部作废。

103
00:09:48,206 --> 00:09:53,459
就这一个问题，占了全球机群缓存创建 token 的百分之十点二。

104
00:09:53,459 --> 00:09:56,608
修法跟 DSH 的思路殊途同归。

105
00:09:56,608 --> 00:10:04,613
把易变的列表从静态前缀里挪出去，改成单独的附件消息注入，让工具描述保持可缓存。

106
00:10:04,613 --> 00:10:06,139
区别在时序。

107
00:10:06,139 --> 00:10:16,115
Claude Code 是账单上看到那个数字之后才修的，DSH 在 CI 抖动阶段就把顺序治理掉了，还把纪律铺到了两百多份文档里。

108
00:10:16,115 --> 00:10:24,312
这里没有谁更高明，只是所处阶段不同，一个是事后被数据教育，一个是事前把隐患当成设计约束。

109
00:10:24,312 --> 00:10:28,795
但结论完全一样，易变的东西不能待在静态前缀里。

110
00:10:28,795 --> 00:10:41,572
我个人更欣赏事前那条路，不是因为它更聪明，而是因为它便宜，同样的隐患，在 CI 阶段发现只需要加一张列表，在生产环境发现要等到账单出来才被看见。

111
00:10:41,572 --> 00:10:45,021
Grok Build 走的是另一条路线，静态模板。

112
00:10:45,021 --> 00:10:50,803
system prompt 从预生成的模板解密渲染，再拼上项目说明和技能内容。

113
00:10:50,803 --> 00:10:57,365
模板是编译期固定的，前缀天然比动态组装稳定，这是静态路线的先天优势。

114
00:10:57,365 --> 00:11:05,634
代价是灵活性，DSH 那种插件随时贡献段、变量、工具的组装模型，在这条路线上就不存在了。

115
00:11:05,634 --> 00:11:12,365
静态与动态之争在这里有了一个很具体的裁判标准，谁的前缀更可预测。

116
00:11:12,508 --> 00:11:16,664
我们用课程里的练习把这件事落到你自己身上。

117
00:11:16,614 --> 00:11:23,045
假设你的 agent 在 system prompt 第二行写了当前时间，精确到秒，每秒都在变。

118
00:11:23,045 --> 00:11:24,739
请你推演一遍。

119
00:11:24,739 --> 00:11:28,321
每条请求的缓存从第几段开始失效。

120
00:11:28,321 --> 00:11:32,636
第二行就变了，所以从第二行起，后面全部作废。

121
00:11:32,636 --> 00:11:36,638
你真正能复用的只剩第一行那点身份声明。

122
00:11:36,638 --> 00:11:40,184
一天一千条请求多付多少重算 token。

123
00:11:40,184 --> 00:11:46,963
把你 system prompt 的总 token 数减去第一行，乘上一千，再乘上命中与未命中的差价。

124
00:11:46,963 --> 00:11:49,835
这个数字通常会让人坐直。

125
00:11:49,835 --> 00:11:51,843
修复方案有两个。

126
00:11:51,843 --> 00:11:55,953
第一个，把时间挪到 prompt 末尾的动态上下文里。

127
00:11:55,953 --> 00:11:58,477
第二个，把精度降到天。

128
00:11:58,477 --> 00:11:59,871
哪个更好。

129
00:11:59,871 --> 00:12:04,030
第一个更好，但要在跨天的边界上多想一层。

130
00:12:04,030 --> 00:12:14,078
精度降到天，看起来一天只断一次，但那个断点是不可避免的，而且它断在每天的第一条请求上，之后一整天才能复用。

131
00:12:14,078 --> 00:12:21,674
挪到末尾则不同，它让时间彻底离开前缀的敏感区，无论它怎么变都不影响前面的复用。

132
00:12:21,674 --> 00:12:27,972
这道题真正的价值在于，它逼你区分两件事，断了几次，和断在哪。

133
00:12:27,972 --> 00:12:31,037
后者才是决定成本的那个变量。

134
00:12:31,037 --> 00:12:44,078
给你的团队留一个动作，照着 DSH 的三段式，把会进入模型请求的东西列一张清单，system prompt、工具 schema、动态注入的上下文、检索结果，逐项写出缓存影响。

135
00:12:44,078 --> 00:12:49,499
写完你大概率会发现至少一个跟那百分之十点二同款的问题。

136
00:12:49,499 --> 00:12:58,826
我建议你把这张清单贴到团队的 wiki 上，并且在每次改动 system prompt 的评审里问一句，这次改动会不会动到前缀。

137
00:12:58,826 --> 00:13:03,141
这个问题只要问出口，一半的事故就不会发生。

138
00:13:03,292 --> 00:13:06,342
最后给你几条能直接拿走的东西。

139
00:13:06,292 --> 00:13:10,175
第一，把 prompt 前缀当成公开接口来维护。

140
00:13:10,175 --> 00:13:14,682
它的稳定性是一种兼容性承诺，不是文案偏好。

141
00:13:14,682 --> 00:13:22,230
改前缀里的一个字，跟改一个公开接口的字段名，是同一个量级的变更，需要同样的谨慎。

142
00:13:22,230 --> 00:13:24,598
第二，顺序也是接口。

143
00:13:24,598 --> 00:13:29,802
内容完全相同、顺序不同的两组 schema，是两个不同的前缀。

144
00:13:29,802 --> 00:13:38,973
顺序绝不能交给加载时序这种环境噪声决定，要有一张中心列表显式治理，并且在请求发出之前完成规范化。

145
00:13:38,973 --> 00:13:43,011
第三，宁可大声失败，不要静默容错。

146
00:13:43,011 --> 00:13:48,396
插值缺值、格式错误、引用了未注册的变量，一律抛异常。

147
00:13:48,396 --> 00:13:54,766
悄悄变形的前缀会同时伤害账单和模型行为，而且两处都极难排查。

148
00:13:54,766 --> 00:13:57,542
第四，把易变的东西往后放。

149
00:13:57,542 --> 00:14:04,658
时间、动态状态、易变的列表，一律挪出静态前缀，实在挪不走就挪到末尾。

150
00:14:04,658 --> 00:14:07,663
这是所有 harness 通用的省钱动作。

151
00:14:07,663 --> 00:14:10,932
第五，把隐性成本写进文档。

152
00:14:10,932 --> 00:14:14,658
缓存影响不写下来，就进不了代码评审。

153
00:14:14,658 --> 00:14:21,869
DSH 那份三段式模板可以直接抄，模型看到什么、token 成本多少、对缓存有什么影响。

154
00:14:21,869 --> 00:14:23,684
这一集就到这里。

155
00:14:23,684 --> 00:14:31,677
最后留一个问题给你，你的 system prompt 里第一个会变的位置在哪，找到它，今天就能省下一笔钱。

