学 AI 产品 · 专业 AI 产品经理播客第 4 章 · T4 解剖 DeepSeek Harness:一切皆插件的 Agent 底座 · EP 21
第 4 章 · EP 21

KV Cache 是接口

时长 14:32音色 云健 · 男声

同步字幕

章节导航(点击跳转)

0:00开场1:26
1:26KV Cache 说成人话1:44
3:10一个字怎么废掉整条缓存1:39
4:50第一件:写进文档纪律1:20
6:10第二件:工具顺序由中心列表治理1:35
7:46第三件:严格插值宁可抛异常1:37
9:23一个 10.2% 的教训1:48
11:12找出你的缓存杀手1:50
13:03可带走的设计原则1:28
解读全文

KV Cache 是接口

本集对应课程:DeepSeek Harness · 工程方法论 ·《KV Cache 是接口》
音频与本文配套,本文为文字版深度解读,可独立阅读。

一句话速览

prompt 前缀稳定性当成兼容性承诺来维护。KV Cache 按前缀逐字匹配,从第一个变化的 token 起全部作废——所以前缀不是文案,是接口。


一、先把 KV Cache 说成人话

模型处理请求时,会为每个 token 算出一堆中间结果(键和值)并缓存。下一条请求进来,只要开头的 token 序列与上一条逐字相同,这段前缀的计算就能直接复用,provider 按命中打折。

DeepSeek 官方定价中,命中缓存的输入 token 比未命中便宜一个数量级(具体倍率以官方价目页为准)。

关键点:逐字相同

缓存按前缀匹配:从第一个不同的 token 起,后面全部作废。

不是「从那个位置往后的一小段作废」,是后面整条线全部作废。

常见误解

误解:缓存按片段匹配,改哪一段就只重算哪一段。

事实:它是从前往后的一条线,前面断了,后面整条线都得重来。

这是很多团队「优化半天不见效」的原因——他们优化了后面的内容,而断点在前面。


二、能力地图:什么伤缓存,什么不伤

操作是否伤缓存失效起点
改 persona 里一个词✗ 伤那个词的位置
换工具顺序(内容相同、顺序不同)✗ 伤第一个顺序不同的位置
在开头塞当前时间✗ 伤时间所在位置(每秒都在变)
追加对话历史✓ 不伤无——新内容跟在可复用前缀后面
追加工具调用历史与结果✓ 不伤无——append-only

提示1:追加不伤缓存,是只追加设计的账单红利

对话历史 append-only 地增长,旧前缀原样保留,只为新增部分付全价。

前面讲持久化时讨论过它对一致性的好处,这里补上另一半:它对钱包同样有好处。

推论:越靠前的内容越碰不得。把易变的东西(时间、动态状态)往后放,把万年不变的身份和 schema 往前放。


三、DSH 的三件套

3.1 第一件:写进文档纪律

本地快照中 packages 下 268 个包的 README,有 215 个带固定的 #### KV Cache effect 小节。

任何会出现在模型请求里的东西,文档必须按三段式交代:

  1. What the model sees — 模型看到什么
  2. Token effect — token 成本多少
  3. KV Cache effect — 对缓存有什么影响

正向陈述(packages/core/tools/README.md 第 145 行):

Prefix-stable while visible definitions and their order are unchanged. Registration, disposal, or scoped restriction may invalidate reuse from the first changed schema token.

反向陈述(同文件第 186–188 行):工具调用的历史和结果是 append-only 的,新内容跟在可复用前缀后面,不会打翻已有缓存。

提示2:隐性成本不写下来就进不了 review

这一步看着最笨,却解决了一个真问题:缓存影响是隐性的,写代码的人根本不知道自己改的那行字会让别人的账单翻倍。

只有把影响写进文档,它才能进入 review 的视野,才能被评审时问一句——这行改动会不会动到前缀。

3.2 第二件:工具顺序由中心列表规范化

问题:工具 schema 是前缀的大头,其顺序原本跟着插件注册顺序走。插件并发加载,注册顺序随环境抖动——DSH 在 CI 里实际观察到了不同的请求头(Agent Note 2026-07-06-explicit-tool-order)。

顺序影响请求字节 → 请求字节影响缓存 → 顺序成了必须显式治理的对象。

解法:

  • 配置里的 toolOrder 列表统一定序
  • 列表中必须恰好有一个 <unlisted-tools> 其余项标记
  • 没配列表 → 按字典序兜底
  • 规范化发生在 assemble() 内部、waterfall 之前 → 注册顺序在任何可观测位置都不再出现

源码(packages/core/system-prompt/src/index.ts 第 169–178 行):开头先做保留名检查——工具提供方敢用 <unlisted-tools> 这个保留名直接抛异常;往下是排序本体,两个失败分支:没配列表走字典序兜底,toolOrder 里写了未注册的工具名直接抛。抛异常时机在组装阶段、请求发出之前。

两个边界条件:

  1. 插件热重载后注册顺序变了,工具顺序会变吗?不会——中心列表在 waterfall 之前规范化,注册顺序无处可观测。
  2. toolOrder 里写了个拼错的工具名会怎样?轮次在组装时失败,不开步骤、不记请求头、不向适配器发请求,每个轮次都同样失败直到配置修好,进程本身保持运行。

3.3 第三件:严格插值,宁可抛异常

persona 是模板,变量组严格按注册表解释。三个连着的抛异常分支,一个都不放过:

分支管什么触发条件
1格式变量名不匹配命名正则 → malformed prompt variable reference(连空名也走这条)
2注册名字不在注册表 → unknown prompt variable,报错顺带列出全部已注册变量名
3取值注册了但本次组装没给值 → 抛异常

为什么宁可让整个轮次失败:静默容错 = 把一个悄悄变形的前缀发给模型,缓存悄悄失效,坏 prompt 还可能悄悄改变行为。大声失败反而便宜——立刻可见、立刻可修。

源码小心思(第 283 行):用 Object.hasOwn 而非普通属性访问查变量是否已注册。因为普通属性访问查 {{constructor}} 会顺着原型链摸到 Object 的内建方法,被误认为已注册变量。查自有属性 → 原型链上的名字一律算未注册。

提示3:宁可报错,绝不蒙混

保留名硬边界 + 严格插值 + Object.hasOwn,是同一种价值观的三种表现:防的就是悄悄放行的坏 prompt。


四、横向对比:一个 10.2% 的教训与一条静态路线

Claude Code:账单上的 10.2%

还原源码 restored-src/src/tools/AgentTool/prompt.ts 第 57–64 行注释记录了一次真实事故:

  • 子代理列表原本嵌在工具描述里
  • MCP 异步连接、插件重载、权限模式切换都会改变这个列表
  • 工具描述一变 → 整块工具 schema 缓存全部作废
  • 这一个问题占了全球机群 cache_creation token 的 10.2%

修法:把易变列表从静态前缀里挪出去,改成单独的 attachment 消息注入,工具描述保持可缓存。

与 DSH 的差别在时序:Claude Code 是账单上看到 10.2% 之后修的;DSH 在 CI 抖动阶段就把顺序治理掉了,还把纪律铺到了 215 份文档里。

Grok Build:静态模板路线

system prompt 从预生成的模板解密渲染(crates/codegen/xai-grok-agent/src/prompt/template.rs),再拼上 AGENTS.md 和 skills 内容。

  • 优势:模板编译期固定 → 前缀天然比动态组装稳定
  • 代价:灵活性。DSH 那种「插件随时贡献段、变量、工具」的组装模型在这条路线上不存在
  • 保留未知:Grok 是否有等价的逐包缓存影响文档,已核对的本地快照里未见,此条基于已公开证据保留未知

五、审查清单

  • 你的 system prompt 开头有没有时间、随机 id、版本号这类每次都变的东西?
  • 工具 schema 的顺序是显式治理的,还是跟着注册/加载顺序走?
  • 同一份代码在不同机器上,发出的请求字节是否一致?(在 CI 里比对过请求头吗?)
  • 模板插值缺值时是抛异常还是静默填空字符串?
  • 变量注册检查用的是自有属性还是普通属性访问?(原型链污染风险)
  • 每个会进入模型请求的东西,文档里有没有写清它的缓存影响?
  • 对话历史、工具结果是不是 append-only 的?
  • 易变列表有没有被塞进静态的工具描述里?(Claude Code 那 10.2% 的同款问题)
  • 你的团队有没有一份可复用的「缓存影响」文档模板?
  • 你衡量的是「断了几次」还是「断在哪」?——后者才是决定成本的变量

六、怎么量化一次前缀抖动

一个可以直接套用的估算式:


单次抖动损失 ≈(前缀总 token − 仍可复用的前缀 token)× 请求次数 ×(未命中单价 − 命中单价)

三个变量里,团队通常只盯着第二个(请求次数),而真正能改、且改动成本最低的是第一个——把易变内容往后挪,就是在扩大「仍可复用的前缀」那一段。

这也解释了为什么 DSH 把「越靠前越碰不得」当成一条纪律而不是一句提醒:它前面那一小段的长度,直接乘在每一次请求上。


七、约束说明

  1. 取材范围:packages/core/tools/README.md(第 145、186–188 行)、packages/core/system-prompt/src/index.ts(第 169–178、277–290 行)、Agent Note 2026-07-06-explicit-tool-order、Claude Code 还原源码 restored-src/src/tools/AgentTool/prompt.ts(第 57–64 行,材料出处 claude-code-sourcemap-main/study/chapters/05-multi-agent.md 第 106–121 行)、Grok Build crates/codegen/xai-grok-agent/src/prompt/template.rs。核对日期 2026-08-13,依据本地仓库 deepseek-harness-master。
  2. 价格口径:命中缓存比未命中「便宜一个数量级」出自 DeepSeek 官方定价的公开表述,具体倍率以官方价目页为准,文中不给出精确倍率。
  3. 演示数据性质:课程「前缀稳定性显微镜」中的色带数值为教学化抽象,非真实 token 计数。本集结论均来自上述文档与源码,不依赖演示数值。
  4. 10.2% 的口径:指全球机群 cache_creation token 的占比,为 Claude Code 侧的自述数据,非本集实测。
  5. Grok 侧的保留未知:未见等价的逐包缓存影响文档,此项明确标注为未知,不作推断。
  6. 本集不含题库内容:课程两道练习已改写为正文讲解与团队动作建议,不在集页另行出题。

八、练习推演:找出你的缓存杀手

场景:agent 在 system prompt 第二行写了「当前时间:精确到秒」,每秒都在变。

Q1:每条请求的缓存从第几段开始失效?

第二行就变了 → 从第二行起后面全部作废 → 真正能复用的只剩第一行。

Q2:一天 1000 条请求多付多少重算 token?

(system prompt 总 token 数 − 第一行)× 1000 ×(命中与未命中的差价)。这个数字通常会让人坐直。

Q3:两个修复方案,哪个更好?

方案做法跨天边界表现
A把时间挪到 prompt 末尾的动态上下文时间彻底离开前缀敏感区,无论怎么变都不影响前面复用
B把精度降到天看起来一天只断一次,但断点不可避免,且断在每天第一条请求上,之后一整天才能复用

A 更好。 关键区别在于:B 只是减少了断的次数,A 是改变了断的位置。

核心洞察:要区分「断了几次」和「断在哪」——后者才是决定成本的变量。

给团队的动作:照着三段式(模型看到什么 / token 成本 / 缓存影响),为会进入模型请求的东西列清单:system prompt、工具 schema、动态注入上下文、RAG 检索结果。标出哪几项放错了位置。写完大概率会发现至少一个与那 10.2% 同款的问题。


九、可带走的设计原则

  1. 把 prompt 前缀当成公开接口来维护。 它的稳定性是兼容性承诺,不是文案偏好。改前缀里的一个字 ≈ 改一个公开接口的字段名。
  2. 顺序也是接口。 内容相同、顺序不同的两组 schema 是两个不同前缀。顺序必须由中心列表显式治理,并在请求发出前完成规范化。
  3. 宁可大声失败,不要静默容错。 插值缺值、格式错误、未注册变量一律抛异常。悄悄变形的前缀同时伤害账单和模型行为,且都极难排查。
  4. 把易变的东西往后放。 时间、动态状态、易变列表一律挪出静态前缀;实在挪不走就挪到末尾。这是所有 harness 通用的省钱动作。
  5. 把隐性成本写进文档。 缓存影响不写下来就进不了 code review。三段式模板可以直接抄:模型看到什么 / token 成本多少 / 对缓存有什么影响。

*来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)*