返回 CodeWhale
CACHE.md
根目录 / docs / zh_hans / CACHE.md
1 # 提示缓存稳定性(固定前缀)
2
3 > 英文原文:[CACHE.md](../CACHE.md)。
4 > 最后与英文同步日期(last synced with English revision):2026-09-29。
5
6 提供商(provider)的提示缓存(DeepSeek KV cache、Anthropic `cache_control`)
7 只有在请求的**字节前缀**与上一次完全一致时才有收益:先是系统提示,
8 再是工具目录,最后是 `messages[0..n-1]`。这些字节只要变了一处,
9 第一处差异之后的所有 token 缓存都会失效。
10
11 ## 不变式
12
13 **会话开始之后,系统提示和工具目录就是冻结的字节。历史只增不减。
14 只有说得出原因,才允许出现缓存未命中。**
15
16 具体来说:
17
18 - **头部**(系统提示 + 工具)在会话开始时组合一次,之后**只有**在显式的、
19 有日志记录的头部变更操作中才会重新组合。工具循环**不会**在循环中途
20 刷新系统提示。所以智能体(agent)写文件(这会改变项目上下文包、目录列表、技能扫描结果)
21 时,也不会在回合中途让固定前缀发生变化。
22 - **历史只增不减。** 模型必须看到的易变事实(LSP 诊断、中途干预输入、
23 子智能体(sub-agent)完成结果)一律追加到消息列表,绝不拼进冻结前缀。工作区漂移也是这么处理:
24 在每个**新的用户回合**开始时(绝不在工具循环中途),引擎会重新组合那些易变内容。
25 只要有内容和模型上次看到的不一样,就在用户消息*之前*追加**一条**
26 `<context_update>` 用户角色消息,里面是有界的 `+`/`-` 行级差异
27 (项目包里的新文件、改动过的 AGENTS.md 行、新增技能、记忆条目、目标文本)。
28 头部字节保持固定,这次更新只是一次普通追加,所以前缀仍在延长。
29 固定住的系统提示会告诉模型一次:更新是以这种方式送达的。
30 每条差异只送一次(`/cache stats` 会显示 `Context updates: N`)。
31 - 每一次未命中都**说得清来由**。`PrefixStabilityManager`(`prefix_cache.rs`)
32 会记录每次变更和原因,并通过 `/cache stats` 报告出来。
33
34 ## 哪些算已声明的头部变更
35
36 下面这些操作会带着有日志的 `change:<what>` 原因重新固定前缀
37 (这是预期内、只影响一次请求的未命中):
38
39 | 操作 | 原因 |
40 | --- | --- |
41 | `/model`(SetModel) | `change:model` |
42 | 模式切换(agent/plan/operate/yolo) | `change:mode` |
43 | 目标设置 / 暂停 / 恢复 / 清除 / 状态 | `change:goal` |
44 | 回合中途的工具表面变更(延迟工具的加入/移除、工具搜索激活、运行时 MCP 工具抵达) | `change:tool_surface` |
45 | 会话同步 / 恢复(SyncSession) | `resume` |
46 | 会话构建 | `initial` |
47
48 历史重置会合法地让尾部(而不是头部)失效,记录为 `reset:<what>`——
49 `reset:compaction`、`reset:clear`。
50
51 除此之外,任何在**没有**声明原因的情况下改动头部字节的行为都算**漂移**:
52 它会记为 `drift:<component>`,原来的固定点被**保留**下来。同一个未声明的前缀
53 会继续算作未命中,而不是悄悄变成新基线;`/cache stats` 里也会出现一条 `WARNING`。
54 去掉循环中途刷新之后,正常运行时漂移应保持为零;漂移计数不为零,
55 就是需要排查的真实缺陷。
56
57 ## 归因与旧做法对比
58
59 下面两种早先的做法已被否决,这与 DeepSeek Harness 的设计一致:
60
61 - **检测并上报 + 漂移时重新固定。** 以前,管理器每次遇到变更都会重新固定到新前缀。
62 于是一个糟糕的步骤之后,提供商缓存其实已经失效,`/cache stats`
63 却还显得“稳定”。现在遇到未声明的漂移,它会保留原来的固定点。
64 - **每个工具步骤都从磁盘重新组合系统提示。** 以前回合循环在每次模型请求之前
65 都会调用 `refresh_system_prompt()`,包括在工具循环中途。这个做法已经删除,
66 头部刷新只发生在上文声明的几个边界上。
67
68 没有配置密钥时(常见情况),工具结果的脱敏(`prepare_model_bound_request`)
69 不改动内容,所以不会挪动前缀。一旦工具结果里出现已配置的密钥,就必须脱敏——
70 这是安全要求。**这条消息里**,脱敏优先于缓存稳定性。
71
72 ## 验证这项修复
73
74 `/cache stats` 会报告前缀稳定性、固定原因、最近一次未命中的原因、
75 未声明漂移次数,以及提供商缓存的总体命中率。在一次编码会话里,
76 预期第一个回合是写入,之后的每一步——包括智能体写完文件之后的步骤——都能命中。
77
78 ### 实时端到端检查(手动,需要密钥)
79
80 用真实的 `DEEPSEEK_API_KEY` 跑一次会话,让智能体在一个回合内至少执行三个
81 工具步骤,然后打开 `/cache inspect`。除第一个请求之外,每个请求都应报告
82 `prompt_cache_hit_tokens > 0`;基础静态前缀哈希和工具目录哈希在各步骤之间
83 不能变。如果命中在回合中途掉下来,固定原因和漂移计数会指出问题所在。
84
85 ## KV 缓存影响说明(面向贡献者)
86
87 任何要加入会话上下文的新贡献项,都必须说明它的 **KV 缓存影响**:
88 它该放进冻结前缀(系统 + 工具),还是放进只追加的历史?绝不要把易变事实
89 (时间、一次指令修改、技能目录变更、项目文件变更)拼接进前缀——
90 要作为用户角色消息追加。后续请求必须是 `previous ⊕ suffix`,
91 除非有带日志的头部变更或一次历史重置能解释这个差异。
92
93 ## 暂缓:完全可重建(Layer 3)
94
95 DeepSeek Harness 通过一个纯函数投影 `deriveMessages()`,从只追加的会话日志
96 推导出每一次请求,所以前缀是自然延长的,不需要管理器去维持。
97 Codewhale 现在固定头部,遇到漂移就追加一条 `<context_update>` 送出去;
98 剩下的一步是让会话日志成为唯一事实来源,并配上纯投影
99 (同时把上下文更新基线一并持久化)。那是后续的独立工作线,不属于本次变更。
100
100 lines MARKDOWN