写 coding agent 时,慢通常不只来自模型本身。真正让人感到拖沓的,是每一轮都要把一大段工作现场重新交给模型: base instructions、工具 schema、项目规则、权限状态、历史消息、工具结果、最新用户输入,最后还要等模型吐出第一个 token。 长任务跑到中后段,这个“重新交现场”的成本会越来越明显。
OpenAI 的
prompt caching 文档
把合同讲得很清楚:缓存依赖 exact prefix match;静态内容应该放在请求开头,动态内容放在尾部;
请求可以通过 prompt_cache_key 影响路由;响应里的
usage.prompt_tokens_details.cached_tokens 用来观察命中结果。
对 Codex 来说,源码里真正值得看的点就变成:它怎样让请求开头足够稳定,又怎样把会变化的东西推到合适的位置。
这篇的中心判断是:Codex 的 prompt cache 纪律不是一个 cache 参数,而是一套模型视图纪律。
prompt_cache_key 只负责把相似请求带进同一个缓存域;能不能命中,还要看
instructions、tools、input、context update、tool output 和
compaction 共同形成的前缀是否稳定。
证据边界。
OpenAI 官方文档说明 prompt cache 的 provider contract:exact prefix、缓存门槛、路由、retention、
prompt_cache_key 和 cached_tokens。Codex 公开源码只能证明客户端如何构造请求、
保存历史、触发 compaction、记录 token usage。服务端如何序列化内部 token、如何放置 KV cache、
如何调度机器,本文不把它写成 Codex 源码事实。
第九篇追六个问题:
- prompt cache 到底缓存的是哪一层视图?
prompt_cache_key在 Codex 里从哪里来,为什么默认跟 thread 绑定?- Codex 请求里的
instructions、tools、input怎样形成稳定前缀? - thread settings、环境上下文和工具结果为什么要作为动态尾部处理?
- compaction 为什么既能减压,也会重写未来的缓存形状?
- Codex 用哪些指标把 cache 效果和体感速度记录下来?
一、先定坐标:缓存命中的是模型视图,不是聊天窗口
很多性能讨论会直接问“有没有命中缓存”。这个问法太早了。先要问: 哪份内容进入了这次 API request?顺序是什么?哪些字段在 provider contract 里会参与 prefix? UI 里显示的历史、磁盘里的 rollout、runtime 临时构造的 request input,这三者不总是同一份东西。
| 层 | owner | 作用 | 和 prompt cache 的关系 |
|---|---|---|---|
| 可见历史 | 客户端投影 | 让用户看到 turn、工具、hook、事件和最终回答。 | 只提供观察,不等于下一轮完整发给模型。 |
| 持久记录 | rollout / thread store | 恢复、回放、fork 和审计。 | 恢复时用来重建 runtime history,但不是 provider cache 本身。 |
| 模型视图 | Prompt / Responses request |
本轮实际交给模型的 instructions、tools、input 和控制字段。 |
provider 只能在这份有序输入上做 prefix 复用。 |
因此,Codex 的性能文章不能只看网络请求耗时。它要先看模型视图怎么构造。
Prompt 结构就把这件事拆开了:conversation input、可用 tools、
parallel_tool_calls、base_instructions、personality、output schema。
到 ModelClient::build_responses_request 时,它们会变成 Responses API 的请求字段:
instructions、input、tools、reasoning、text
和 prompt_cache_key。
形状示意:
Responses request
instructions: stable base instructions
tools: model-visible tool schemas
input: prior context + dynamic turn tail
text: output schema / verbosity
prompt_cache_key: thread-scoped cache domain
这段示意省略了很多字段,但足够说明本文的读法:先观察 request shape,再解释速度。
cached_tokens 只是结果指标;稳定前缀才是 runtime 能主动维护的工程纪律。
二、缓存域:prompt_cache_key 默认跟 thread 走
Codex 的 ModelClient 是 session-scoped。源码注释说它持有跨 turn 共享的配置和状态:
auth、provider selection、thread id、transport fallback state。这个作用域正好也解释了
prompt_cache_key 的默认值。
ModelClient::prompt_cache_key() 先看 override;没有 override 时,返回 thread_id。
build_responses_request 再把这个值放进每次 Responses 请求。换句话说,Codex 默认把一个
thread 的连续 turns 放在同一个 cache domain 附近,让相似前缀有机会落到同类路由上。
prompt_cache_key 的位置很容易被高估。它不会声明“缓存到哪一条 message 为止”,
也不会修复被动态内容打碎的前缀。它的价值是稳定请求的缓存域:
同一个 thread 或同一类审查会话,用一致的 key 帮助 provider 找到可能已有的前缀。
源码里也有一个例外:Guardian review session 可以通过
with_prompt_cache_key_override 设置专门的 key。这说明 key 是路由/域的控制点,
不属于某个单 turn 的临时状态。它应该跟“哪些请求应共享前缀”这个语义一起变化,而不是跟每条用户输入一起变化。
三、稳定前缀:instructions、tools、context base
官方文档建议把静态内容放在开头。Codex 源码里对应三类材料:base instructions、model-visible tool schemas、以及初始上下文或上下文 diff 建立出的 context base。
3.1 instructions 要稳定
build_responses_request 直接从 prompt.base_instructions.text 取
instructions。prompt caching 测试里,连续两轮请求会断言
instructions 保持一致;当某些模型没有把 apply_patch 暴露成 tool 时,
源码会追加稳定的 patch 指令,测试也会断言两轮的 instructions 仍然一致。
这里的关键不是“指令永远不变”。模型、personality、collaboration mode 或配置切换时,它当然会变。 关键是普通连续 turn 不应该把临时信息塞进 base instructions。临时信息应该进入后面的 input tail, 这样 stable prefix 才有机会复用。
3.2 tools 是前缀的一部分
OpenAI 文档明确说 tool definitions 可以被缓存,并计入 1024 token 门槛。
Codex 的请求构造也把 prompt.tools 通过 create_tools_json_for_responses_api
转成 tools 字段。第 4 篇和第 7 篇讲过:工具表来自 core tools、MCP、dynamic tools、
tool_search 暴露的延迟工具,以及 plugin / skill 带来的能力入口。
这解释了一个常见的体感问题:工具越多,单轮请求越重;但如果工具 schema 足够稳定, 它们也更适合成为缓存前缀的一部分。反过来,如果每轮都无谓改变工具列表或工具 schema 顺序, prompt cache 会先在 request shape 上受伤。
3.3 context base:一次注入,后续用 diff
第 2 篇讲过 reference_context_item。它是 context diff 的基线:当 runtime 能确认上下文基线还可信时,
下一轮只需要发送 settings update;基线被 rollback 或 compaction 打断时,再回到 full reinjection。
prompt caching 测试里的 prefixes_context_and_instructions_once_and_consistently_across_requests
正好验证了这条纪律:第一轮包含 permissions、cached contextual user prefix 和 user message;
第二轮会把第一轮 input 作为前缀保留下来,再追加新的 user message。
这不是为了让测试好看。它直接保护 prompt cache:把稳定规则和环境上下文作为已确认的前缀, 后续变化用新消息追加,而不是每轮重新把同一批上下文改写成另一种形状。
四、动态尾部:改变可以发生,但要晚一点发生
动态信息不可避免。用户会改变 cwd、权限、model、reasoning effort;工具会返回新的 stdout/stderr; hooks 可能补 additional context;MCP 工具列表可能刷新;compact 可能替换 history。 Codex 的策略不是消灭变化,而是尽量让变化出现在稳定前缀之后。
prompt caching suite 里有两个直接证据。一个测试把 thread settings 改掉后,断言
prompt_cache_key 不变,并且第二次请求以前一次 input 为前缀,再追加新的 permissions
message、environment message 和用户消息。另一个测试在 per-turn overrides 下做同样断言:
key 仍然稳定,model switch 和环境变化作为后续消息进入 input。
| 变化来源 | Codex 的处理 | 保护的性能属性 |
|---|---|---|
| 用户新输入 | 作为新的 user message 追加。 | 旧 prefix 不被重新排版。 |
| thread settings override | 生成 settings / environment update,接在已有 input 后。 | 保留前面已经稳定的上下文。 |
| tool output | 记录到 history,再由 for_prompt() 规范化后进入下一次 request。 |
保证 call/output 成对,不让 malformed history 破坏模型视图。 |
| hook additional context | 变成 contextual fragment 写入 model context。 | 把策略补充变成可观察的输入变化。 |
| compaction | 用 summary / replacement history 改写后续基线。 | 降低上下文压力,同时建立新的可恢复前缀。 |
这也是第 8 篇 hooks 铺垫的原因。hook 如果在 prompt gate 或 post-tool gate 补上下文, 它不是“免费附加信息”;它会改变后续模型视图。一个小 hook 是否影响性能,要看它把内容插到哪里、 是否每轮变化、是否破坏早期 prefix。
五、compaction:减压之后,缓存形状也换了
prompt cache 不能替代 context window。历史越来越长时,Codex 仍然要 compact。
ContextManager 会维护 token usage、估算 token count,也会在 history rewrite 时 bump
history_version。get_context_remaining 工具则把当前总 token usage
和 model context window 相减,让模型能查询剩余窗口。
到 remote compact v2,源码会先 clone history,拿 base instructions,
再调用 trim_function_call_history_to_fit_context_window 对工具输出做本地减压。
随后它构造 compact request,并复用普通 Responses 请求里的共享字段。
测试 remote_manual_compact_*_reuses_prompt_cache_key 明确断言:
compact request 的 prompt_cache_key 和 normal request 一致,并且 compact 请求保留同一套共享请求字段。
compact 的性能含义有两面:它减少未来请求需要携带的历史,但也会建立新的前缀形状。 旧前缀不再是后续模型视图的主线;summary、保留的工具结果、重新注入的 initial context, 会一起成为新的 cache 候选。
所以不要把 compact 理解成“触发一次总结就更快”。如果 summary 太粗,语义会丢; 如果保留太多,窗口压力还在;如果 compact 后没有稳定的 replacement history,恢复和后续 cache 都会漂。 Codex 把 compact 放在 history / rollout / token usage 这一整套机制里,就是为了让减压之后仍然能继续重建同一条工作主线。
六、指标:cached、non-cached 和 first token
provider cache 是否命中,最终要靠 usage 观察。Codex 的 TokenUsage
里有 input_tokens、cached_input_tokens、output_tokens、
reasoning_output_tokens 和 total_tokens。
turn 结束时,on_task_finished 会拿本轮开始前的 token usage snapshot,
和当前 total token usage 做差,得到这次 turn 的输入、cached input、non-cached input、输出和总量。
这些值会写进 tracing span、session telemetry histogram 和 analytics event。
同一个收尾路径还记录 time_to_first_token_ms 和 turn duration。
这使得 Codex 可以把两个层面的性能分开看:一层是 provider 侧有多少输入 tokens 来自 cache;
另一层是用户体感的首 token 和整体 turn 时长。
| 指标 | 说明 | 不要误读成 |
|---|---|---|
cached_input_tokens |
provider 报告本轮有多少输入 token 命中 cache。 | 模型“少看了”这些内容。 |
non_cached_input() |
输入 token 中没有命中的部分。 | 所有非缓存 token 都是浪费。 |
time_to_first_token_ms |
用户等待首个可见输出的时间。 | 只由 prompt cache 决定。 |
total_tokens |
本轮 token 总账。 | 上下文窗口中仍完整保留的语义质量。 |
这套指标也解释了为什么性能文章要排在 hooks 后面。一个 post-tool hook 可以让模型看见不同反馈; 一个 permission hook 可以减少无效审批等待;一个 stop hook 可以让 turn 多跑一次; 它们都会影响体感速度,但不一定直接体现在 cached tokens 上。源码里的性能观测必须把 cache、 tool loop、事件投影和 turn lifecycle 放在一起看。
七、常见误读:把 cache 当成单点开关
到这里可以把几个误读收掉。它们很常见,因为 prompt cache 的 API 字段看起来很简单, 但 runtime 真正维护的是一条长请求链。
| 误读 | 更准确的读法 | 源码里的边界 |
|---|---|---|
prompt_cache_key 决定命中。 |
key 影响缓存域和路由,命中还要 exact prefix。 | ModelClient::prompt_cache_key() 与 request shape 分开。 |
| 聊天历史在,模型就会完整看到。 | 模型看到的是 for_prompt() 之后的 input。 |
ContextManager::for_prompt() 会规范化、补 call/output、不支持图片时去掉图片。 |
| 工具越少一定越快。 | 工具 schema 是成本,也是可缓存前缀;关键是稳定和按需暴露。 | tools 字段由 prompt.tools 构造,测试断言常规工具表稳定。 |
| compact 之后缓存自然更好。 | compact 会换掉未来前缀,需要稳定 replacement history 承接。 | remote compact 复用 prompt_cache_key,同时改写 history。 |
| 缓存命中等于用户一定觉得快。 | 首 token 还受工具循环、审批、网络、compaction 和输出长度影响。 | turn 收尾同时记录 token usage、TTFT 和 duration。 |
八、下一篇:为什么要回到 rollout 和恢复
prompt cache 把性能问题压到模型视图;但模型视图从哪里来?它来自 history、context updates、 compaction replacement、tool results 和 rollout reconstruction。只要这些记录不能稳定恢复, 下一轮 request shape 就会漂,cache 分析也会失去参照。
所以下一篇回到持久化和恢复:Codex 怎样把一次 turn 的过程写进 rollout,怎样从
RolloutItem 重建 history,怎样处理 rollback、fork、compact boundary 和 token usage。
读完那一篇,性能、恢复和客户端投影才会合成同一条线:用户看到的事实、磁盘保存的事实、
模型下一轮看到的事实,必须能互相对齐。
参考源码与文档
- OpenAI Prompt caching guide
- OpenAI Prompt Caching 201
- OpenAI latest model guide:reasoning models 与 prompt caching 建议
- openai/codex 固定源码快照
Prompt结构与get_formatted_input_for_request()ModelClient与 turn-scopedModelClientSession注释with_prompt_cache_key_override()与默认 thread id keybuild_responses_request()构造 instructions、input、tools 与prompt_cache_keyResponsesApiRequest与 websocket request 保留prompt_cache_key- session 初始化
ModelClient与 Guardian review override run_turn构造 sampling request input 的顺序ContextManager、reference_context_item与for_prompt()- token usage 更新与本地估算
TokenUsageInfo与TokenCountEvent- turn start 记录 token usage 起点与 tracing 字段
- turn stop 计算 cached / non-cached token usage
- turn 完成事件记录 duration 与 time-to-first-token
get_context_remaining计算剩余 context window- remote compact v2 clone history、trim tool outputs、构造 prompt
- compact request 复用
prompt_cache_key - 测试:tools 与 instructions 在连续请求中保持稳定
- 测试:cached contextual prefix 跨请求复用
- 测试:thread settings override 保持 cached prefix 和 key
- 测试:per-turn overrides 保持 cached prefix 和 key
- 测试:remote compact request 复用 normal request 的
prompt_cache_key