Agent 工程对照
← 返回框架对照

上下文 · 第 4 章

提示词缓存

前缀缓存怎么保住?什么操作会让它失效?

四家管的缓存不在同一层:调度层、请求层、计量层、声明层各占一家。

场景建议

编辑判断

按你的需求,倾向哪种做法

借鉴的是那一家的设计,不等于选用那个框架。

场景

改请求结构时不想悄悄打破缓存

它逐字段比对两次请求,并要求解构保持穷尽——缓存失效变成编译期问题,不靠人记。

场景

要算清缓存省了多少钱

DeepSeek 把命中与写入记成不相交的三份;Pi 在 cost 里单列 cacheRead / cacheWrite 费率。

场景

自己部署推理后端

它用 sticky 子代理加 prefetch / finish / evict 钩子,让同一个子会话尽量落到同一个后端,复用 KV 缓存。

容易踩的坑

  • 谈缓存优化前先说清在哪一层谈:调度、请求、计量、声明四层的手段互不替代。

四家怎么做

差异与源码

openJiuwen

不管前缀,管子代理的 KV 缓存亲和性

四家里唯一不在「前缀复用」这一层解决问题的。它的 kv_cache 模块处理的是路由亲和:特定类型的子代理(browser_agent、verification_agent)被标为 sticky,配合 prefetch / finish / evict 三个生命周期钩子,让同一个子会话尽量落到同一个推理后端上,从而复用已有的 KV 缓存。子会话 id 由「父会话 id + 任务 id」推导,保证跨轮次稳定。这是把缓存当作调度问题而不是拼装问题。

适合
自建推理集群、能控制请求路由到哪台后端的部署。
代价
解决的是调度层问题,对调用第三方 API 的场景帮助有限。
Codex

请求字段逐个比对,新字段强制显式表态

请求带 prompt_cache_key,但真正的工程做法在这个比较函数:判断两次请求能否复用连接与缓存前缀时,它把 ResponsesApiRequest 完整解构,逐字段比对——除 input 与 client_metadata 外全部必须相等。注释写明了为什么不实现 PartialEq,以及关键约束:保持解构穷尽,这样将来新增任何请求字段都必须显式决定它是否影响复用。缓存失效在这里是编译期问题,不是靠人记住。

适合
请求结构经常演进、又想稳住前缀缓存命中率的产品。
代价
请求结构的比较函数要保持穷尽解构,每加一个字段都要显式表态。

此后的变化

  • 2026-W40 →

    新增默认关闭的 incremental_tools:Responses Lite 首次发送完整工具目录,之后只追加新增或变化的定义,删除工具用开发者通知表示,已发送的请求前缀不变。

DeepSeek Harness

不放缓存断点,但把缓存 token 记成不相交三份

缓存位置交给厂商,Harness 负责把账算准:TokenUsage 规定 inputTokens 只计未命中缓存的输入,命中与写入分别记为 cacheReadTokens / cacheWriteTokens,三者不相交、相加才是计费输入。注释还点名了一个真实坑——有些厂商(包括 DeepSeek 自己的 prompt_tokens)把缓存命中折进总数里,适配器必须减回去。这份账直接喂给 ctx.tokenMeter,而压缩阈值又读 tokenMeter,所以缓存计量算错会连带压缩时机出错。

适合
按 token 计费敏感、并且压缩阈值依赖准确计量的系统。
代价
每个厂商适配器都要把缓存命中从总数里减回去,写错会连带压缩时机出错。
Pi

按线路协议声明缓存能力,能力差异写进配置

缓存被当作模型能力来建模:每种 wire protocol 各有一张 compat schema,分别声明支持什么。三张 schema 都有 supportsLongCacheRetention,只有 Anthropic 那张多出 supportsCacheControlOnTools——因为只有它支持把缓存断点打在工具定义上。顶层还有 cacheControlFormat: "anthropic" 指明断点写法,以及 cost 里独立的 cacheRead / cacheWrite 费率(含分档)。新增一个厂商因此是改配置,不是改代码。

适合
要接多家厂商、而各家缓存能力不同的场景。
代价
能力差异写进配置,新增厂商时要先弄清它支持什么缓存特性。

此后的变化

  • 2026-W37 →

    本格描述的「按线路 compat schema 声明缓存能力」新增一例:Fireworks Messages 线路打开 supportsToolReferences 以启用延迟工具加载。文档同时点明该收益的脆弱处——加载器名必须是 ToolSearch 或 tool_search,否则 Fireworks 会把加载的 schema 并回初始工具前缀,缓存收益消失。

横向读这一行

同一个「缓存」,四家管的根本不是同一层:openJiuwen 管调度层(让子代理粘在同一个后端上,复用 KV),Codex 管请求层(逐字段比对,决定这次请求能不能接着上次的前缀),DeepSeek 管计量层(把命中与写入记成不相交的三份 token,喂给压缩阈值),Pi 管声明层(各家线路协议支持什么缓存能力写进配置)。要谈缓存优化,得先说清楚在哪一层谈。

设计空间分析

「提示词缓存」在四家那里是四个不同层次的问题:调度、请求、计量、声明——没有一家同时处理全部四层。

openJiuwen 在调度层:它不管前缀怎么拼,而是把 browser_agent、verification_agent 等子代理标成 sticky,配合 prefetch、finish、evict 三个钩子,让同一个子会话尽量落到同一个推理后端,复用已经算好的 KV 缓存;子会话 id 由父会话 id 加任务 id 推导,跨轮次稳定。这只在能控制请求路由时才有意义。

Codex 在请求层:判断两次请求能否复用前缀时,把整个请求结构穷尽解构、逐字段比对,除 input 与 client_metadata 外必须全部相等,并在注释里要求解构保持穷尽——以后谁新增字段,编译器都会逼他表态这个字段影不影响缓存。

DeepSeek 在计量层:缓存断点交给厂商,但 token 必须记成不相交的三份——未命中输入、缓存读、缓存写,相加才是计费输入;这份账喂给 tokenMeter,而压缩阈值又读 tokenMeter,所以缓存算错会连带压缩时机出错。Pi 在声明层:每种线路协议一张兼容性 schema,只有 Anthropic 那张多出「工具定义上可打缓存断点」一项,费率里单列缓存读写,新增厂商是改配置而不是改代码。

编辑判断

四层之间是互补而非替代关系:请求层决定能不能命中,声明层决定能用哪些手段,计量层决定你能否知道自己省了多少,调度层只在自建推理时才轮得到。对多数调用第三方 API 的团队,Codex 的「请求结构穷尽比对」与 DeepSeek 的「三份不相交计量」是最值得先借的两招。

这个问题为什么存在

第 4 章

上下文工程

上下文是稀缺资源:腐化、焦虑、压缩与缓存