KV Cache 是接口
prompt 前缀稳定性当成兼容性承诺来维护。
先玩再讲。下面的色带是一条 system prompt 加工具 schema 的 token 序列(数值为教学化抽象)。点「播放」,演示会依次做几个常见操作:原样重发、改 persona 一个字、加一个工具、追加对话、插件加载顺序抖动。每一步色带上会标出缓存从第几个 token 起失效,右边的计价器累计你多付的重算 token。右上角可以切换 DSH 模式和对照模式,抖动那一步的结果完全不同。
先把 KV Cache 说成人话。模型处理请求时,会为每个 token 算出一堆中间结果(键和值)缓存起来。下一条请求进来,只要开头的 token 序列和上一条逐字相同,这段前缀的计算就能直接复用,provider 按命中给你打折。DeepSeek 的官方定价里,命中缓存的输入 token 比未命中便宜一个数量级(具体倍率以官方价目页为准)。
关键在逐字相同这四个字。缓存按前缀匹配:从第一个不同的 token 起,后面全部作废。而 agent 请求的开头是什么?system prompt 加工具 schema,动辄几千 token,每条请求都带。改 persona 里一个词、换一下工具顺序、在开头塞个当前时间,缓存就从那个位置断掉,之后的每条请求都全价重算。
所以 DSH 得出一个结论:prompt 前缀是模型这个 API 的接口,它的稳定性是一种兼容性承诺,要像维护公开 API 一样维护。落到工程上是三件事。
第一件,写进文档纪律。本地快照里 packages 下 268 个包 README,有 215 个带一个固定的 #### KV Cache effect 小节。任何会出现在模型请求里的东西,文档必须按三段式交代:模型看到什么(What the model sees)、token 成本多少(Token effect)、对缓存有什么影响(KV Cache effect)。以 packages/core/tools/README.md 的工具 schema 一节为例,第 145 行原文:
(只要可见的工具定义及其顺序不变,前缀就稳定。注册、卸载或按作用域限制工具,都可能从第一个变化的 schema token 起使缓存复用失效。) 出处:deepseek-harness-master 仓库
packages/core/tools/README.md 第 145 行,核对日期 2026-08-13
同一个文件第 186 到 188 行还有一句反向陈述:工具调用的历史和结果是 append-only 的,新内容跟在可复用前缀后面,不会打翻已有的缓存。什么伤缓存、什么不伤,全部写成可查的文档条目。
第二件,工具顺序由中心列表规范化。工具 schema 是前缀的大头,它的顺序原本跟着插件注册顺序走。插件是并发加载的,注册顺序随环境抖动,DSH 在 CI 里实际观察到了不同的请求头(Agent Note 2026-07-06-explicit-tool-order 的问题一节)。顺序影响请求字节,请求字节影响缓存,于是它成了必须显式治理的对象:配置里的 toolOrder 列表统一定序,列表里必须恰好有一个 <unlisted-tools> 其余项标记,没配列表就按字典序兜底。规范化发生在 assemble() 内部、waterfall 之前,注册顺序在任何可观测的位置都不再出现。
第三件,严格插值,宁可抛异常不交付坏 prompt。persona 是模板,{{model}} 这样的变量组严格按注册表解释:引用了未注册的变量、变量本次没有值、花括号组格式错误,一律抛异常,轮次直接失败,一个请求都不会发出去。理由很直接:静默容错等于把一个悄悄变形的前缀发给模型,缓存悄悄失效,坏 prompt 还可能悄悄改变行为。大声失败反而便宜。
顺序也是接口两组内容完全相同、只是顺序不同的工具 schema,对缓存来说是两个不同的前缀。所以工具顺序不能交给加载时序这种环境噪声决定。
失效从第一个变化 token 开始缓存按前缀匹配,越靠前的内容越碰不得。把易变的东西(时间、动态状态)往后放,把万年不变的身份和 schema 往前放。
追加不伤缓存对话历史 append-only 地增长,旧前缀原样保留,只为新增部分付全价。这也是会话日志只追加设计在账单上的红利。
下面这段是工具排序的核心逻辑。函数开头(第 165 至 168 行)先做一道保留名检查:工具提供方敢用 <unlisted-tools> 这个保留名直接抛异常。往下就是排序本体,看两个失败分支:没配列表走字典序兜底,toolOrder 里写了没注册的工具名直接抛。抛异常的时机在组装阶段,请求发出之前。
if (toolOrder === undefined) return tools.sort(compareToolNames)
const unknown = toolOrder.filter(name => name !== TOOL_ORDER_REST && !knownNames.has(name))
if (unknown.length > 0) {
throw new Error(`toolOrder lists unregistered tool${unknown.length > 1 ? 's' : ''} ${unknown.map(name => `"${name}"`).join(', ')}; known tools: ${[...knownNames].sort().join(', ') || '(none)'}`)
}
const listed = new Set(toolOrder)
const rest = tools.filter(tool => !listed.has(tool.name)).sort(compareToolNames)
return toolOrder.flatMap(name =>
name === TOOL_ORDER_REST ? rest : tools.filter(tool => tool.name === name))
}
packages/core/system-prompt/src/index.ts,核对日期 2026-08-13。代码块保留源码原文。两个边界条件值得记住,出处都在 Agent Note 2026-07-06-explicit-tool-order。插件热重载后注册顺序变了,工具顺序会变吗?不会,中心列表在 waterfall 之前规范化,注册顺序无处可观测。toolOrder 里写了个拼错的工具名会怎样?该 Note 后果一节写得很细:轮次在组装时失败,不开步骤、不记请求头、不向适配器发请求,每个轮次都同样失败直到配置修好,进程本身保持运行。
严格插值这边是三个连着的抛异常分支,一个都不放过。第一个管格式:变量名不匹配命名正则就抛「malformed prompt variable reference」,连 {{}} 这种空名都被注释点名,走的就是这条格式错误路径。第二个管注册:名字不在注册表里就抛「unknown prompt variable」,报错顺带列出全部已注册变量名。第三个管取值:注册了但这次组装没给值,同样抛异常。三个分支拦下的都是同一件事,一个悄悄变形的前缀。
出处:packages/core/system-prompt/src/index.ts 第 277 至 290 行的插值分支,核对日期 2026-08-13。
第 283 行的 Object.hasOwn 有个小心思:用普通属性访问查 {{constructor}} 会顺着原型链摸到 Object 的内建方法,被误认为已注册变量。查自有属性,原型链上的名字一律算未注册。防的就是这种悄悄放行的坏 prompt。
Claude Code:还原源码 restored-src/src/tools/AgentTool/prompt.ts 第 57 到 64 行的注释记录了一次真实事故。子代理列表原本嵌在工具描述里,MCP 异步连接、插件重载、权限模式切换都会改变这个列表,工具描述一变,整块工具 schema 的缓存全部作废。这一个问题占了全球机群 cache_creation token 的 10.2%。修法和 DSH 的思路殊途同归:把易变的列表从静态前缀里挪出去,改成单独的 attachment 消息注入,工具描述保持可缓存(材料出处:claude-code-sourcemap-main/study/chapters/05-multi-agent.md 第 106 至 121 行)。区别在时序:Claude Code 是账单上看到 10.2% 之后修的,DSH 在 CI 抖动阶段就把顺序治理掉了,还把纪律铺到了 215 份文档里。
Grok Build:走静态模板路线。system prompt 从预生成的模板解密渲染(crates/codegen/xai-grok-agent/src/prompt/template.rs),再拼上 AGENTS.md 和 skills 内容(同目录 mod.rs 的模块划分)。模板是编译期固定的,前缀天然比动态组装稳定,这是静态路线的先天优势。代价是灵活性:DSH 那种插件随时贡献段、变量、工具的组装模型,在这条路线上不存在。至于 Grok 是否有等价的逐包缓存影响文档,已核对的本地快照里未见,这一条基于已公开证据保留未知。
找出你的 prompt 里的缓存杀手
假设你的 agent 在 system prompt 第二行写了「当前时间:2026-08-13 22:04:35」,每秒都在变。推演:每条请求的缓存从第几段开始失效?一天 1000 条请求多付多少重算 token?给出两个修复方案并比较:把时间挪到 prompt 末尾的动态上下文里,或者把精度降到天。提示:想想哪个方案在跨天的边界上仍然会断一次缓存。
给你的团队写一份 KV Cache effect 模板
模仿 DSH 的三段式(模型看到什么 / token 成本 / 缓存影响),为你项目里「会进入模型请求的东西」列一张清单:system prompt、工具 schema、动态注入的上下文、RAG 检索结果。逐项写出它的缓存影响,标出哪几项放错了位置。写完你大概率会发现至少一个和 Claude Code 那 10.2% 同款的问题。