状态: Draft
组件: Context Builder(Stable Prefix / Dynamic Suffix)、Runtime Provider Request、OpenAI-compatible / Anthropic / Gemini Provider Adapter、Token Usage 账本与事件
日期: 2026-05-10
相关技术: Prompt Caching, Context Caching, ReAct Loop, Provider Adapter, Token Usage Ledger, Stable Prefix, Tool Schema Canonicalization
1. 摘要
本 RFC 针对 NeoCode 当前消息发送链路的 prompt cache 命中率低、命中情况不可观测、provider 缓存能力未充分接入的问题提出重设计方案。当前设计的核心矛盾是:系统每轮把稳定规则、运行态状态、仓库检索结果、Todo、Git 摘要与临时提醒拼成一个 system prompt,导致 provider 侧用于缓存的前缀频繁变化。
主流 provider 到 2026-05-10 的方案已经很清晰:
- OpenAI-compatible:自动缓存长输入前缀,建议把静态内容放在 prompt 最前面,并通过
cached_tokens 观测命中
- Anthropic:显式在 system/tools/messages 上标记
cache_control 断点,并通过 cache_read_input_tokens / cache_creation_input_tokens 观测命中
- Gemini:支持 implicit caching,并可通过
cachedContent 显式创建和复用上下文缓存
本方案不是简单增加一个“缓存开关”,而是从以下五个层面重新设计:
- 上下文分层:Context Builder 输出
StableSystemPrompt 与 DynamicSystemPrompt
- 请求抽象:Provider 请求携带 provider-neutral
PromptCache hint
- Provider 适配:OpenAI 解析命中指标,Anthropic 接入
cache_control,Gemini 接入 cachedContent
- 工具稳定化:Tool schema 稳定排序,skill hints 不再重排工具数组
- 可观测性:Usage、Runtime Event 与账本记录 cache read/create/cached token
2. 背景与问题
当前系统已经具备:
internal/context 的统一 prompt 构建能力
internal/runtime 的 provider request 冻结与 token usage 调和能力
- OpenAI-compatible、Anthropic、Gemini 等 provider adapter
- compact / micro compact / repository context / todo / skills 等上下文投影能力
但当前仍有几个关键问题:
2.1 System Prompt 混合了稳定内容和高频变化内容
newPromptSources 当前把 core prompt、capabilities、rules、task state、plan mode、todos、skills、repository context、system state 按顺序合成同一个 system prompt。前半部分相对稳定,后半部分每轮都可能变化。
这意味着 provider 看到的是一个整体字符串:Todo revision 变化、git dirty 变化、repository retrieval query 变化、hook reminder 注入,都会改变 system prompt 的 token 序列。
缓存前缀不是“有很多重复文本”就能命中,而是要求前缀稳定。
2.2 动态段落出现在 provider 缓存敏感区域内
当前 Task State、Todo State、Repository Context、System State 都在 system prompt 中。它们并非不能发送给模型,而是不应混入稳定缓存前缀:
Todo State 的 rev、status、created order 会随执行变化
Repository Context 的 targeted retrieval 与 changed files 会随用户意图和工作区变化
System State 的 git dirty/ahead/behind 会随工具执行变化
- hook notification / progress reminder 是临时状态,只应该进入动态尾部
这些内容放在稳定前缀中,会让 provider 每轮创建新缓存,而不是读取旧缓存。
2.3 Provider 缓存能力没有统一抽象
GenerateRequest 当前只有 SystemPrompt、Messages、Tools、ThinkingConfig。这使 runtime 无法表达“这段是稳定前缀,那段是动态尾部”,provider adapter 也无法基于通用语义做差异化映射。
结果是:
- OpenAI-compatible 只能依赖自动缓存,且无法传稳定 cache key
- Anthropic 无法对稳定 block 加
cache_control
- Gemini 无法管理
cachedContent
2.4 即使命中,NeoCode 也可能统计不到
provider/types.Usage 当前只记录:
InputTokens
OutputTokens
TotalTokens
InputObserved
OutputObserved
但主流 provider 的缓存指标不在这些字段里:
- OpenAI:
prompt_tokens_details.cached_tokens 或 input_tokens_details.cached_tokens
- Anthropic:
cache_read_input_tokens、cache_creation_input_tokens
- Gemini:
cached_content_token_count
如果不扩展 usage 结构,TUI / runtime / ledger 都无法回答“本轮命中率是多少”。
2.5 Tool schema 顺序会被 skill hints 扰动
Runtime 会调用 prioritizeToolSpecsBySkillHints 调整工具顺序。这对模型提示优先级有帮助,但对缓存不友好:工具数组顺序变化会改变 provider 请求前缀。
工具 schema 是最适合缓存的内容之一,应该稳定排序。技能对工具的偏好应进入 dynamic prompt,而不是改变工具数组。
3. 典型用户场景
场景 1:同一会话连续发送消息,但缓存命中率很低
旧行为:
用户在同一项目中连续让 NeoCode 修复问题。AGENTS.md、核心 prompt 和工具 schema 大部分相同,但每轮 Todo State、System State、Repository Context 都变。provider 看到的 system prompt 前缀不断变化,缓存创建多、读取少。用户感受到首 token 慢、输入 token 成本高。
新行为:
AGENTS.md、核心 prompt、固定能力说明和工具 schema 进入稳定前缀;Todo、Git、retrieval、hook reminder 进入动态尾部。连续请求复用同一稳定前缀,OpenAI/Anthropic/Gemini 都能读到缓存。
场景 2:切换 skill 后工具 schema 顺序变化,缓存被打断
旧行为:
激活某个 skill 后,prioritizeToolSpecsBySkillHints 把相关工具排到前面。工具集合没变,但数组顺序变了,provider 请求结构变化,缓存命中下降。
新行为:
工具 schema 始终按工具名稳定排序。skill hints 被渲染到 dynamic prompt,例如“本轮优先考虑这些工具”。模型仍能看到偏好,provider 缓存前缀保持稳定。
场景 3:Anthropic 用户以为开启了缓存,但实际没有 cache_control
旧行为:
NeoCode 只是把 system prompt 放到 params.System,没有标记 cache_control。Anthropic 不会按预期建立显式缓存断点,usage 中也没有被统一展示的 cache read/create 指标。
新行为:
Anthropic adapter 把 stable system prompt 作为带 cache_control 的 text block,dynamic system prompt 作为普通 block。返回 usage 后,NeoCode 把 cache_read_input_tokens 和 cache_creation_input_tokens 写入统一 usage。
场景 4:Gemini 长规则/长工具说明每轮重复发送
旧行为:
Gemini 每轮都收到完整 system instruction 与工具声明。即使 provider 有 implicit cache,NeoCode 也没有显式管理 cachedContent,无法稳定复用长上下文缓存。
新行为:
NeoCode 为 Gemini 建立进程内 cachedContent manager。稳定前缀 hash 命中时直接复用 cached content name;创建失败时降级为普通请求,不影响主链路。
场景 5:用户问“缓存命中率为什么低”,系统没有证据
旧行为:
Runtime 只上报 input/output/total token。即使 provider 返回缓存字段,也被 adapter 忽略。排查只能靠猜。
新行为:
每轮 token_usage event 带 cached_input_tokens、cache_creation_input_tokens、cache_hit_ratio。排查时能明确看到是前缀没稳定、provider 未返回缓存字段,还是缓存 TTL 过期。
4. 设计目标
本方案要求同时满足:
- 稳定 prompt 前缀必须与运行态动态信息分离。
- Provider 缓存能力通过统一抽象接入,差异收敛在
internal/provider。
- 未支持缓存的 provider 必须保持现有行为,不影响主链路。
- 缓存命中率必须可观测,可从 usage/event/ledger 中直接计算。
- Tool schema 必须按稳定规则排序,不能被 skill hints 扰动。
- 缓存失败必须降级为普通请求,不得导致用户请求失败。
5. 非目标
本 RFC 不处理:
- 不改变 TUI / Gateway / Runtime / Provider / Tools 主链路。
- 不引入本地响应缓存,不缓存模型输出。
- 不缓存用户消息、工具结果或敏感附件内容。
- 不默认启用 OpenAI 24h extended retention。
- 不要求所有 provider 都实现显式缓存;不支持的 provider 直接忽略 cache hint。
- 不把 provider 特定字段泄漏到
runtime 或 context。
6. 核心设计
6.1 Context Builder 输出 Stable / Dynamic 两段
扩展 BuildResult:
type BuildResult struct {
SystemPrompt string
StableSystemPrompt string
DynamicSystemPrompt string
Messages []providertypes.Message
}
SystemPrompt 继续保留,值为 stable + dynamic 拼接,保证旧调用路径兼容。
稳定段只包含长期稳定内容:
- Core prompt sections
- Capabilities 的稳定部分
- Project / Global Rules
- 固定安全边界、工具使用原则、上下文管理原则
动态段包含运行态内容:
- Task State
- Current Plan / Plan Mode
- Todo State
- Active Skills
- Repository Context
- System State
- Progress reminder / hook notification / pending system reminder
这样设计的原因是:provider 缓存吃的是稳定前缀,不是系统 prompt 这个字段名本身。把动态信息从稳定段里剥离,才能让连续请求共享同一段 token 前缀。
6.2 增加 provider-neutral PromptCache hint
扩展 provider/types.GenerateRequest:
type GenerateRequest struct {
Model string
SystemPrompt string
Messages []Message
Tools []ToolSpec
ThinkingConfig *ThinkingConfig
SessionAssetReader SessionAssetReader
PromptCache PromptCacheHint
}
type PromptCacheHint struct {
Enabled bool
StableSystemPrompt string
DynamicSystemPrompt string
StablePrefixKey string
}
StablePrefixKey 由 runtime 计算:
sha256(provider_identity + model + stable_system_prompt + canonical_tool_schema)
这样设计的原因是:runtime 只知道“哪段稳定”,不知道 provider 要怎么缓存;provider 只知道“怎么映射到厂商协议”,不应重新猜哪些内容稳定。PromptCacheHint 是二者之间的最小契约。
6.3 Runtime 组装请求时冻结稳定前缀
prepareTurnBudgetSnapshot 中:
- 调用 context builder 获取 stable/dynamic prompt
- 调用 tool manager 获取工具 schema
- 对 tool schema 做稳定排序与 canonical hash
- 构造
PromptCacheHint
SystemPrompt 仍使用 stable + dynamic + reminder 的最终拼接结果
临时提醒处理规则:
withProgressReminder
pendingSystemReminder
hookNotificationsForTurn
这些内容全部进入 dynamic system prompt 末尾,不得混入 stable prompt。
这样设计的原因是:提醒是最典型的单轮动态信息。把它拼进 stable prompt 会让缓存每轮失效。
6.4 Tool schema 稳定排序,skill hints 改为动态提示
删除或停止在主请求路径使用 prioritizeToolSpecsBySkillHints 对工具数组重排。
新规则:
- provider 请求中的
Tools 按 Name 字典序稳定排序
- 同名工具保留原始相对顺序作为兜底
- skill hints 渲染为 dynamic prompt 中的“preferred tools”段落
示例:
## Skill Tool Hints
- Prefer these tools when relevant: filesystem_read_file, filesystem_grep
这样设计的原因是:工具 schema 通常很长且稳定,是最值得缓存的请求部分。为了提示优先级改变 schema 顺序,收益小于缓存损失。
6.5 OpenAI-compatible:自动缓存 + 指标解析
OpenAI-compatible adapter 分两条路径:
- Chat Completions:解析
usage.prompt_tokens_details.cached_tokens
- Responses:解析
usage.input_tokens_details.cached_tokens
如果 SDK / compatible gateway 支持请求参数:
- 设置
prompt_cache_key = StablePrefixKey
- 默认
prompt_cache_retention = "in_memory"
如果不支持这些字段,则只做稳定前缀排序和 cached tokens 解析,不阻断请求。
这样设计的原因是:OpenAI 的缓存主要是自动缓存。NeoCode 最重要的工作是保证静态内容靠前、解析命中指标,并在可用时提供稳定 cache key。
6.6 Anthropic:stable block 加 cache_control
Anthropic request 构建改为:
StableSystemPrompt 非空时,作为第一个 system text block
- 该 block 带
cache_control: {type: "ephemeral", ttl: configuredTTL}
DynamicSystemPrompt 非空时,作为第二个普通 system text block
- 若 SDK 支持 tool-level cache control,则最后一个稳定 tool definition 也加同样 cache control
TTL 默认:
context:
prompt_cache:
anthropic_ttl: "5m"
可选值仅允许 "5m" 与 "1h"。
这样设计的原因是:Anthropic 的显式缓存断点不是自动从字符串里推断出来的,必须在 request block 上标记。稳定和动态拆成两个 block 后,动态状态不会污染稳定缓存。
6.7 Gemini:implicit cache + cachedContent manager
Gemini v1 分两步:
- 先保证 stable prompt 在 system instruction 最前,并解析
cached_content_token_count
- 再增加进程内 cachedContent manager
cachedContent key:
provider_identity + model + stable_system_prompt_hash + tool_schema_hash
cachedContent value:
type GeminiCachedContentEntry struct {
Name string
ExpiresAt time.Time
}
行为:
- key 命中且未过期:
GenerateContentConfig.CachedContent = entry.Name
- key 未命中:创建 cached content,成功后写入 manager,再发送请求
- 创建失败:记录 warning,降级普通请求
- 请求返回 cached content 不存在或过期:删除 entry,重试一次普通请求
TTL 默认:
context:
prompt_cache:
gemini_ttl_sec: 600
这样设计的原因是:Gemini 明确支持显式 context cache。进程内管理足以覆盖 NeoCode 当前 TUI/daemon 的常见连续请求场景,同时避免持久化 provider cache name 带来的过期、权限和数据生命周期复杂度。
6.8 Usage 与事件补齐缓存字段
扩展 providertypes.Usage:
type Usage struct {
InputTokens int
OutputTokens int
TotalTokens int
CachedInputTokens int
CacheCreationInputTokens int
InputObserved bool
OutputObserved bool
CacheObserved bool
}
扩展 TokenUsagePayload:
CachedInputTokens int
CacheCreationInputTokens int
CacheHitRatio float64
CacheObserved bool
命中率:
cache_hit_ratio = cached_input_tokens / max(input_tokens, 1)
Ledger 调和规则:
- provider 返回 cache 字段 → 记录 observed cache usage
- provider 不返回 cache 字段 →
CacheObserved=false
- cache 字段不参与上下文预算扣减,只用于成本/性能观测
这样设计的原因是:缓存优化必须先可观测。没有 cached/read/create token,就无法判断是前缀不稳定、TTL 过期、provider 不支持,还是 adapter 没解析。
6.9 配置开关与降级策略
新增配置:
context:
prompt_cache:
enabled: true
anthropic_ttl: "5m"
gemini_ttl_sec: 600
默认行为:
enabled=true,但 provider 可按能力降级
- 配置非法时启动阶段失败
- 单次 cache create/read 失败不得让用户请求失败
- provider 不支持 cache hint 时忽略,不报错
这样设计的原因是:缓存是性能与成本优化,不应成为主链路可用性的单点风险。
7. 与现有模块的关系
context
BuildResult 增加 StableSystemPrompt 与 DynamicSystemPrompt
newPromptSources 拆成 stable sources 与 dynamic sources
composeSystemPrompt 保持兼容,用于生成旧字段 SystemPrompt
- 新增测试确保 dynamic 内容不会进入 stable prompt
runtime
prepareTurnBudgetSnapshot 填充 PromptCacheHint
- tool specs 在进入 request 前稳定排序
- skill hints 改为追加到 dynamic prompt,不再重排 tool schema
newTurnBudgetUsageObservation、ledger reconcile、emitTokenUsage 支持 cache usage
provider/types
GenerateRequest 增加 PromptCache
Usage 增加缓存字段
- 保持 JSON tag 使用 snake_case
provider/openaicompat
- Chat Completions adapter 解析
prompt_tokens_details.cached_tokens
- Responses adapter 解析
input_tokens_details.cached_tokens
- 支持时透传
prompt_cache_key 与 prompt_cache_retention
- 不支持时保持现有请求结构
provider/anthropic
BuildRequest 使用 stable/dynamic system block
- stable block 添加
cache_control
- usage 解析补齐 cache read/create 字段
- TTL 从 runtime config 注入 provider runtime config
provider/gemini
BuildRequest 支持 cached content name
- Provider 持有进程内 cachedContent manager
- usage 解析补齐 cached content token 字段
- 创建/读取失败降级普通请求
config
- 新增
PromptCacheConfig
- 默认启用
- 校验
anthropic_ttl 只允许 "5m" / "1h"
- 校验
gemini_ttl_sec > 0
docs
- 新增
docs/prompt-cache-hit-rate-redesign.md
- 后续实现时同步更新
docs/guides/configuration.md
- 实现完成后可新增
docs/prompt-cache.md 作为用户排查指南
8. 测试场景
- 同一 BuildInput 只改变 Todo revision,
StableSystemPrompt 不变,DynamicSystemPrompt 变化。
- 同一 BuildInput 只改变 git dirty,
StableSystemPrompt 不变。
- 同一 BuildInput 只改变 repository retrieval query,
StableSystemPrompt 不变。
- Project Rules 内容变化时,
StablePrefixKey 变化。
- Tool schema 顺序输入不同但集合相同,canonical tool hash 相同。
- Active skill hints 变化时,tool schema 顺序不变,dynamic prompt 变化。
- OpenAI Chat Completions usage 带
prompt_tokens_details.cached_tokens 时,Usage.CachedInputTokens 正确。
- OpenAI Responses usage 带
input_tokens_details.cached_tokens 时,Usage.CachedInputTokens 正确。
- Anthropic request 的 stable system block 带
cache_control,dynamic block 不带。
- Anthropic usage 带
cache_read_input_tokens 和 cache_creation_input_tokens 时,统一 usage 正确。
- Gemini cachedContent manager 命中时,request 使用 cached content name。
- Gemini cachedContent 创建失败时,请求降级为普通 generate,不返回错误。
- Gemini cached content 过期时,删除 entry 并普通请求重试一次。
context.prompt_cache.enabled=false 时,各 provider 不发送显式 cache 控制,但仍可解析 provider 返回的 cache usage。
- Provider 不返回 cache 字段时,
CacheObserved=false,input/output usage 仍正常调和。
9. 假设与默认决策
- OpenAI-compatible 的 v1 优先做命中指标解析与稳定前缀;
prompt_cache_key / retention 参数按 SDK 与网关能力 best effort 接入。
- OpenAI 24h extended retention 不默认开启,避免改变数据保留语义。
- Anthropic 默认 TTL 为
"5m";"1h" 仅通过配置显式启用。
- Gemini cachedContent 只做进程内复用,不把 cache name 持久化到 session 或配置文件。
- 缓存字段不参与 context budget 计算,只用于成本、性能和命中率观测。
- Prompt cache 失败不影响主链路;任何显式缓存失败都降级为普通请求。
- Tool schema 稳定排序优先级高于 skill hints 的工具顺序偏好。
- 动态 prompt 仍完整发送给模型,本方案不牺牲模型可见运行态上下文。
10. 一句话结论
NeoCode 当前缓存命中率低的问题不是“provider 没有缓存”,而是“NeoCode 没有给 provider 一个稳定可缓存的前缀”。新架构通过 StableSystemPrompt / DynamicSystemPrompt 分层、PromptCacheHint 统一请求抽象、OpenAI / Anthropic / Gemini provider 适配 和 cache usage 可观测性,把缓存从碰运气变成可设计、可验证、可排查的主链路能力。
状态: Draft
组件: Context Builder(Stable Prefix / Dynamic Suffix)、Runtime Provider Request、OpenAI-compatible / Anthropic / Gemini Provider Adapter、Token Usage 账本与事件
日期: 2026-05-10
相关技术: Prompt Caching, Context Caching, ReAct Loop, Provider Adapter, Token Usage Ledger, Stable Prefix, Tool Schema Canonicalization
1. 摘要
本 RFC 针对 NeoCode 当前消息发送链路的 prompt cache 命中率低、命中情况不可观测、provider 缓存能力未充分接入的问题提出重设计方案。当前设计的核心矛盾是:系统每轮把稳定规则、运行态状态、仓库检索结果、Todo、Git 摘要与临时提醒拼成一个 system prompt,导致 provider 侧用于缓存的前缀频繁变化。
主流 provider 到 2026-05-10 的方案已经很清晰:
cached_tokens观测命中cache_control断点,并通过cache_read_input_tokens/cache_creation_input_tokens观测命中cachedContent显式创建和复用上下文缓存本方案不是简单增加一个“缓存开关”,而是从以下五个层面重新设计:
StableSystemPrompt与DynamicSystemPromptPromptCachehintcache_control,Gemini 接入cachedContent2. 背景与问题
当前系统已经具备:
internal/context的统一 prompt 构建能力internal/runtime的 provider request 冻结与 token usage 调和能力但当前仍有几个关键问题:
2.1 System Prompt 混合了稳定内容和高频变化内容
newPromptSources当前把 core prompt、capabilities、rules、task state、plan mode、todos、skills、repository context、system state 按顺序合成同一个 system prompt。前半部分相对稳定,后半部分每轮都可能变化。这意味着 provider 看到的是一个整体字符串:Todo revision 变化、git dirty 变化、repository retrieval query 变化、hook reminder 注入,都会改变 system prompt 的 token 序列。
缓存前缀不是“有很多重复文本”就能命中,而是要求前缀稳定。
2.2 动态段落出现在 provider 缓存敏感区域内
当前
Task State、Todo State、Repository Context、System State都在 system prompt 中。它们并非不能发送给模型,而是不应混入稳定缓存前缀:Todo State的rev、status、created order 会随执行变化Repository Context的 targeted retrieval 与 changed files 会随用户意图和工作区变化System State的 git dirty/ahead/behind 会随工具执行变化这些内容放在稳定前缀中,会让 provider 每轮创建新缓存,而不是读取旧缓存。
2.3 Provider 缓存能力没有统一抽象
GenerateRequest当前只有SystemPrompt、Messages、Tools、ThinkingConfig。这使 runtime 无法表达“这段是稳定前缀,那段是动态尾部”,provider adapter 也无法基于通用语义做差异化映射。结果是:
cache_controlcachedContent2.4 即使命中,NeoCode 也可能统计不到
provider/types.Usage当前只记录:但主流 provider 的缓存指标不在这些字段里:
prompt_tokens_details.cached_tokens或input_tokens_details.cached_tokenscache_read_input_tokens、cache_creation_input_tokenscached_content_token_count如果不扩展 usage 结构,TUI / runtime / ledger 都无法回答“本轮命中率是多少”。
2.5 Tool schema 顺序会被 skill hints 扰动
Runtime 会调用
prioritizeToolSpecsBySkillHints调整工具顺序。这对模型提示优先级有帮助,但对缓存不友好:工具数组顺序变化会改变 provider 请求前缀。工具 schema 是最适合缓存的内容之一,应该稳定排序。技能对工具的偏好应进入 dynamic prompt,而不是改变工具数组。
3. 典型用户场景
场景 1:同一会话连续发送消息,但缓存命中率很低
旧行为:
用户在同一项目中连续让 NeoCode 修复问题。AGENTS.md、核心 prompt 和工具 schema 大部分相同,但每轮
Todo State、System State、Repository Context都变。provider 看到的 system prompt 前缀不断变化,缓存创建多、读取少。用户感受到首 token 慢、输入 token 成本高。新行为:
AGENTS.md、核心 prompt、固定能力说明和工具 schema 进入稳定前缀;Todo、Git、retrieval、hook reminder 进入动态尾部。连续请求复用同一稳定前缀,OpenAI/Anthropic/Gemini 都能读到缓存。
场景 2:切换 skill 后工具 schema 顺序变化,缓存被打断
旧行为:
激活某个 skill 后,
prioritizeToolSpecsBySkillHints把相关工具排到前面。工具集合没变,但数组顺序变了,provider 请求结构变化,缓存命中下降。新行为:
工具 schema 始终按工具名稳定排序。skill hints 被渲染到 dynamic prompt,例如“本轮优先考虑这些工具”。模型仍能看到偏好,provider 缓存前缀保持稳定。
场景 3:Anthropic 用户以为开启了缓存,但实际没有 cache_control
旧行为:
NeoCode 只是把 system prompt 放到
params.System,没有标记cache_control。Anthropic 不会按预期建立显式缓存断点,usage 中也没有被统一展示的 cache read/create 指标。新行为:
Anthropic adapter 把 stable system prompt 作为带
cache_control的 text block,dynamic system prompt 作为普通 block。返回 usage 后,NeoCode 把cache_read_input_tokens和cache_creation_input_tokens写入统一 usage。场景 4:Gemini 长规则/长工具说明每轮重复发送
旧行为:
Gemini 每轮都收到完整 system instruction 与工具声明。即使 provider 有 implicit cache,NeoCode 也没有显式管理
cachedContent,无法稳定复用长上下文缓存。新行为:
NeoCode 为 Gemini 建立进程内 cachedContent manager。稳定前缀 hash 命中时直接复用 cached content name;创建失败时降级为普通请求,不影响主链路。
场景 5:用户问“缓存命中率为什么低”,系统没有证据
旧行为:
Runtime 只上报 input/output/total token。即使 provider 返回缓存字段,也被 adapter 忽略。排查只能靠猜。
新行为:
每轮
token_usageevent 带cached_input_tokens、cache_creation_input_tokens、cache_hit_ratio。排查时能明确看到是前缀没稳定、provider 未返回缓存字段,还是缓存 TTL 过期。4. 设计目标
本方案要求同时满足:
internal/provider。5. 非目标
本 RFC 不处理:
runtime或context。6. 核心设计
6.1 Context Builder 输出 Stable / Dynamic 两段
扩展
BuildResult:SystemPrompt继续保留,值为 stable + dynamic 拼接,保证旧调用路径兼容。稳定段只包含长期稳定内容:
动态段包含运行态内容:
这样设计的原因是:provider 缓存吃的是稳定前缀,不是系统 prompt 这个字段名本身。把动态信息从稳定段里剥离,才能让连续请求共享同一段 token 前缀。
6.2 增加 provider-neutral PromptCache hint
扩展
provider/types.GenerateRequest:StablePrefixKey由 runtime 计算:这样设计的原因是:runtime 只知道“哪段稳定”,不知道 provider 要怎么缓存;provider 只知道“怎么映射到厂商协议”,不应重新猜哪些内容稳定。
PromptCacheHint是二者之间的最小契约。6.3 Runtime 组装请求时冻结稳定前缀
prepareTurnBudgetSnapshot中:PromptCacheHintSystemPrompt仍使用 stable + dynamic + reminder 的最终拼接结果临时提醒处理规则:
withProgressReminderpendingSystemReminderhookNotificationsForTurn这些内容全部进入 dynamic system prompt 末尾,不得混入 stable prompt。
这样设计的原因是:提醒是最典型的单轮动态信息。把它拼进 stable prompt 会让缓存每轮失效。
6.4 Tool schema 稳定排序,skill hints 改为动态提示
删除或停止在主请求路径使用
prioritizeToolSpecsBySkillHints对工具数组重排。新规则:
Tools按Name字典序稳定排序示例:
这样设计的原因是:工具 schema 通常很长且稳定,是最值得缓存的请求部分。为了提示优先级改变 schema 顺序,收益小于缓存损失。
6.5 OpenAI-compatible:自动缓存 + 指标解析
OpenAI-compatible adapter 分两条路径:
usage.prompt_tokens_details.cached_tokensusage.input_tokens_details.cached_tokens如果 SDK / compatible gateway 支持请求参数:
prompt_cache_key = StablePrefixKeyprompt_cache_retention = "in_memory"如果不支持这些字段,则只做稳定前缀排序和 cached tokens 解析,不阻断请求。
这样设计的原因是:OpenAI 的缓存主要是自动缓存。NeoCode 最重要的工作是保证静态内容靠前、解析命中指标,并在可用时提供稳定 cache key。
6.6 Anthropic:stable block 加 cache_control
Anthropic request 构建改为:
StableSystemPrompt非空时,作为第一个 system text blockcache_control: {type: "ephemeral", ttl: configuredTTL}DynamicSystemPrompt非空时,作为第二个普通 system text blockTTL 默认:
可选值仅允许
"5m"与"1h"。这样设计的原因是:Anthropic 的显式缓存断点不是自动从字符串里推断出来的,必须在 request block 上标记。稳定和动态拆成两个 block 后,动态状态不会污染稳定缓存。
6.7 Gemini:implicit cache + cachedContent manager
Gemini v1 分两步:
cached_content_token_countcachedContent key:
cachedContent value:
行为:
GenerateContentConfig.CachedContent = entry.NameTTL 默认:
这样设计的原因是:Gemini 明确支持显式 context cache。进程内管理足以覆盖 NeoCode 当前 TUI/daemon 的常见连续请求场景,同时避免持久化 provider cache name 带来的过期、权限和数据生命周期复杂度。
6.8 Usage 与事件补齐缓存字段
扩展
providertypes.Usage:扩展
TokenUsagePayload:命中率:
Ledger 调和规则:
CacheObserved=false这样设计的原因是:缓存优化必须先可观测。没有 cached/read/create token,就无法判断是前缀不稳定、TTL 过期、provider 不支持,还是 adapter 没解析。
6.9 配置开关与降级策略
新增配置:
默认行为:
enabled=true,但 provider 可按能力降级这样设计的原因是:缓存是性能与成本优化,不应成为主链路可用性的单点风险。
7. 与现有模块的关系
context
BuildResult增加StableSystemPrompt与DynamicSystemPromptnewPromptSources拆成 stable sources 与 dynamic sourcescomposeSystemPrompt保持兼容,用于生成旧字段SystemPromptruntime
prepareTurnBudgetSnapshot填充PromptCacheHintnewTurnBudgetUsageObservation、ledger reconcile、emitTokenUsage支持 cache usageprovider/types
GenerateRequest增加PromptCacheUsage增加缓存字段provider/openaicompat
prompt_tokens_details.cached_tokensinput_tokens_details.cached_tokensprompt_cache_key与prompt_cache_retentionprovider/anthropic
BuildRequest使用 stable/dynamic system blockcache_controlprovider/gemini
BuildRequest支持 cached content nameconfig
PromptCacheConfiganthropic_ttl只允许"5m"/"1h"gemini_ttl_sec > 0docs
docs/prompt-cache-hit-rate-redesign.mddocs/guides/configuration.mddocs/prompt-cache.md作为用户排查指南8. 测试场景
StableSystemPrompt不变,DynamicSystemPrompt变化。StableSystemPrompt不变。StableSystemPrompt不变。StablePrefixKey变化。prompt_tokens_details.cached_tokens时,Usage.CachedInputTokens正确。input_tokens_details.cached_tokens时,Usage.CachedInputTokens正确。cache_control,dynamic block 不带。cache_read_input_tokens和cache_creation_input_tokens时,统一 usage 正确。context.prompt_cache.enabled=false时,各 provider 不发送显式 cache 控制,但仍可解析 provider 返回的 cache usage。CacheObserved=false,input/output usage 仍正常调和。9. 假设与默认决策
prompt_cache_key/ retention 参数按 SDK 与网关能力 best effort 接入。"5m";"1h"仅通过配置显式启用。10. 一句话结论
NeoCode 当前缓存命中率低的问题不是“provider 没有缓存”,而是“NeoCode 没有给 provider 一个稳定可缓存的前缀”。新架构通过 StableSystemPrompt / DynamicSystemPrompt 分层、PromptCacheHint 统一请求抽象、OpenAI / Anthropic / Gemini provider 适配 和 cache usage 可观测性,把缓存从碰运气变成可设计、可验证、可排查的主链路能力。