Skip to content

【实现】 Prompt 缓存 Phase 2:显式缓存接入与缓存用量口径统一 #733

Open

Description

@Cai-Tang-www

关联

目标问题(Why)

Phase 1(#606)已经完成 provider-neutral 的请求结构优化:Context Builder 输出 StableSystemPrompt / DynamicSystemPrompt,工具 schema 按名稳定排序,运行态信息全部后置。这让自动前缀缓存型 provider(DeepSeek、OpenAI-compatible)能吃到更长的稳定前缀。

但 Phase 1 按设计没有接入任何显式缓存能力,所以目前:

  1. Anthropic 侧完全没有 cache_control 断点,稳定前缀每轮按全价计费。按当前模板估算,system prompt 核心部分 18611 字符(其中 core/tool_usage.md 独占 9200),加 19 个工具 schema,单轮输入轻易过 2 万 token;30 轮会话里固定前缀重复发送约 60 万 token。命中价是未命中的 10%,这块是当前最大的单项成本缺口。
  2. OpenAI 侧虽能靠自动缓存命中,但没有 prompt_cache_key,请求被负载均衡打散,命中率不稳定。
  3. 没有任何缓存命中量的观测字段,命中率无法验证,也无法进评测基线。

另外发现 Phase 1 遗留的一个隐患:memo 索引被放在 stable 段里。memo 是 agent 自己写入的记忆,memo_remember 记一条就变一次,等于每写一条记忆就摧毁整个缓存前缀。Phase 1 只关心"前缀是否稳定",没有把它当作易变源处理。

与 #606 的范围关系

#606 第 5 节的标题是「非目标」,首句为:

本 RFC 第一阶段不处理:

这是范围限定,不是永久排除。其中第 8 条:

不扩展 token usage、runtime event、TUI 展示或 ledger 来统计缓存命中率。

而 6.2 节同时写明了后续路径:

本 RFC 第一阶段推荐最小实现。等后续接入 Anthropic cache_control、Gemini cachedContent 或 OpenAI cache key 时,再引入 provider-neutral PromptCacheHint 作为正式 provider 请求契约。

本 issue 就是这句话预告的后续阶段,SystemStablePrefix 即此处的 PromptCacheHint 最小形态。因此越过第 8 条不是否定 Phase 1 的判断,而是 Phase 2 的必要条件:接入显式缓存后若不扩展 usage 与 ledger,会出现静默错误。

Anthropic 的 input_tokens 不含缓存命中与缓存写入,OpenAI 的 prompt_tokens 含。provider_stream.go 原先直接取 Usage.InputTokens 写账本。一旦打上缓存标记,Anthropic 返回 input_tokens=500, cache_read=18000 时账本只累计 500 —— session.TokenInputTotal 严重低估,compact 永不触发、预算门禁形同虚设,且全链路没有任何报错。

Phase 1 没接显式缓存,所以这个问题不存在;Phase 2 接了,它就成了必须先修的配套。所以本次同时做三件事:统一 InputTokens 口径、账本改用含缓存总量、事件透出缓存明细。

核心设计(How)

公共层只表达一个语义:哪一段系统提示是稳定前缀。不引入厂商字段。

type GenerateRequest struct {
    SystemPrompt       string  // 渲染完成的完整文本,单一真源
    SystemStablePrefix string  // 必须是 SystemPrompt 的真实前缀
}

func (r GenerateRequest) ResolvedStablePrefix() string  // HasPrefix 校验,不成立返回空
func (r GenerateRequest) VolatileSystemSuffix() string  // 与前缀拼接严格等于原文

SystemStablePrefix 是 #606 提到的 "provider-neutral PromptCacheHint" 的最小实现形式:只表达前缀边界,不表达 TTL、断点数量、保留策略等厂商细节。等真正需要 TTL 配置时再升级为结构体,避免提前抽象。

各 adapter 映射:

driver 映射方式
anthropic 系统提示拆两个 text block,稳定块末尾打 cache_control: ephemeral;最后一个 tool 再打一个(工具集不变时独立命中);稳定前缀估算不足 1024 token 时不打标
openaicompat chat completions 与 responses 两种模式均发送 prompt_cache_key(模型 + 稳定前缀 + 工具名集合的 sha256 前 32 hex);默认关闭,需显式 prompt_cache: auto 开启
gemini 依赖隐式缓存,不发送额外参数
deepseek / qwen / glm / mimo / minimax 一律不发送路由键,见下

分段策略:默认值按 driver 区分,自研厂商为硬约束

分两层,解决两个不同问题。

第一层:默认值按 driver 区分(避免回归)。 cache_control 是 Anthropic 官方协议字段,端点不会拒绝,默认开启。prompt_cache_key 是 OpenAI 的可选扩展参数,而 openaicompat 服务任意兼容网关 —— 未确认支持前默认发送会让原本可用的网关返回 400,且现有 stream fallback 只处理流式解析失败、不处理 HTTP 参数拒绝。因此该驱动默认关闭,需在 provider 配置中显式写 prompt_cache: auto 声明端点支持。

第二层:五个自研 driver 是硬约束。 chatcompletions.Request 是 DeepSeek / Qwen / GLM / MiMo / MiniMax 共用的中间结构,会被直接 json.Marshal 进请求体。它们有各自的上下文缓存机制(DeepSeek 按前缀全局命中),发路由键要么被忽略、要么被严格网关拒绝,因此即使显式 auto 也不发送。

不做基于错误文本的 400 识别重试:识别脆弱,且会把真实错误掩盖成重试。用能力声明 + 保守默认更可控。

prompt_cache 支持写入 provider.yaml 并随配置读写往返保留;非法值在加载阶段报错,不会静默降级。

边界与非目标

  • 不做 Gemini 显式缓存(caches.create() 需管理生命周期与 token-hour 计费,收益待隐式缓存实测后再评估)
  • 不做多段分层([]PromptBlock 支持 role / workspace / workflow / node / delta 五层)。那是 【架构】 重构 NeoCode Agent Loop:Workflow Orchestrator、统一 AgentLoop 与上下文隔离 #721 Phase 5 的 PromptBundle,属编排层改造,与本 issue 的前缀打标不是同一件事
  • 不改动 provider adapter 的请求内容语义,只增加可选标记字段
  • 不引入本地响应缓存

落地清单(What)

  • GenerateRequest 增加 SystemStablePrefix 与前缀校验方法
  • runtime 组装请求时计算并校验稳定前缀,无法证明前缀关系时降级为空
  • Anthropic:拆块 + cache_control + 1024 token 门槛 + 工具断点
  • OpenAI-compatible:prompt_cache_key 计算与下发,chat completions 与 responses 两种模式均覆盖
  • 默认值按 driver 区分:anthropic 默认开启,其余默认关闭,需显式声明
  • prompt_cache 支持 provider.yaml 读写往返,非法值加载即失败
  • Usage 增加 CacheReadInputTokens / CacheWriteInputTokens、TotalInputTokens()、CacheHitRate()
  • 统一 InputTokens 口径:OpenAI 侧减去 cached_tokens 并收敛负值
  • 预算链路与账本改用 TotalInputTokens()
  • token_usage / ledger_reconciled 事件透出缓存明细
  • memo 索引移出 stable 段(fix(context) 独立提交)
  • 新增 provider 级配置 prompt_cache: auto|off,非法值启动即失败
  • 文档同步:docs/guides/configuration.md、docs/runtime-provider-event-flow.md
  • TTL 配置(prompt_cache_ttl: 5m|1h,现用 Anthropic 默认 5m)
  • TUI / Web 渲染命中率
  • 评测基线纳入 cache hit rate 与单位任务成本(依赖 【实现】 Agent Loop 重构 Phase 0:建立任务质量评测基线与迁移护栏 #722)

验收标准(Done)

  • 稳定前缀与易变尾部拼接严格等于原始 SystemPrompt(按字节)
  • 前缀关系不成立时不打标,行为退化为旧链路
  • 稳定前缀不足最小可缓存长度时不打标,不产生无效写入溢价
  • prompt_cache: off 时 Anthropic 不发送 cache_control、OpenAI 不发送路由键,请求内容其余部分不变
  • 五家自研 driver 的请求体不含 prompt_cache_key,显式 auto 也不含
  • openaicompat 未显式开启时请求体与旧链路完全一致,不引入 400 回归
  • cached_tokens 超过 prompt_tokens 时总输入收敛为上报量,不虚高
  • 缓存 usage 穿过生成与对账链路后,账本累计值与两个事件字段一致
  • OpenAI cached_tokens 大于 prompt_tokens 时 InputTokens 收敛为 0,不出现负数
  • 真实调用两轮后,第二轮 cache_read_input_tokens 接近稳定前缀 token 数
  • 评测集上的单位任务输入成本下降幅度记录(依赖 【实现】 Agent Loop 重构 Phase 0:建立任务质量评测基线与迁移护栏 #722)

风险与回滚

风险 处理
Usage.InputTokens 语义变化影响下游消费方 在 PR 中标注为行为变更;事件同时提供缓存明细,消费方可改用 TotalInputTokens
缓存写入溢价(Anthropic 1.25x)在短会话中不划算 1024 token 门槛 + prompt_cache: off 开关
某厂商对未知字段严格校验导致 400 五家自研 driver 已排除;openaicompat 用户可关闭
缓存键碰撞把不相关请求挤到同一后端 键包含模型、完整稳定前缀、工具名集合三要素
回滚 全部改动是可选字段与开关,prompt_cache: off 即回到 Phase 1 行为;代码层可按 commit 整体回滚,无 schema 迁移

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions