Prompt Caching via Exact Prefix Preservation

Nikola Balic (@nibzard)· emerging

问题

包含大量工具调用的长时Agent对话会遭遇二次方级性能衰减问题:

  • JSON载荷持续膨胀:每次迭代都会将完整的对话历史发送至API
  • 高昂的重复计算开销:若未启用缓存,模型会反复处理相同的静态内容
  • ZDR限制:零数据保留(Zero Data Retention,ZDR)策略禁止留存服务端状态,因此无法采用previous_response_id优化方案
  • 配置变更影响:对话中途的配置修改(如沙箱、工具、工作目录变更)会破坏缓存效率

如果缺乏合适的缓存策略,随着对话时长增加,推理成本与延迟会呈二次方级增长。

方案

通过精确前缀保留提升提示词缓存(prompt cache)效率——始终追加新消息而非修改已有消息,并合理排序消息以最大化缓存命中。

核心要点:提示词缓存仅基于精确前缀匹配生效。若某请求的前N个token与过往请求匹配,则可复用已缓存的计算结果。

消息排序策略

  1. 静态内容前置(位于prompt开头,所有请求共享缓存):

    • 系统消息(若由服务器控制)
    • 工具定义(需保持固定顺序)
    • 开发者指令
    • 用户/项目专属指令
  2. 可变内容后置(位于prompt末尾,随请求动态变化):

    • 用户消息
    • 助手消息
    • 工具调用结果(迭代追加)

通过插入实现配置变更: 当对话中途需调整配置时,插入新消息而非修改已有消息:

[静态前缀...]
<sandbox_config_v1>     // 初始配置消息
[对话内容...]

<sandbox changed>
<sandbox_config_v2>     // 新增的插入消息
[对话继续...]

这种方式可保留所有历史消息的精确前缀,维持缓存命中效果。

导致缓存命中失效的操作

  • 修改可用工具列表(位置敏感)
  • 调整消息顺序
  • 修改已有消息内容
  • 切换模型(会影响服务器端系统消息)

面向ZDR(零数据留存)的无状态设计: 避免使用previous_response_id以支持零数据留存(ZDR)。转而依靠提示词缓存实现线性性能:

不使用previous_response_id:
- 网络流量呈二次方增长(每次发送完整JSON)
- 采样成本呈线性增长(得益于提示词缓存)

使用previous_response_id:
- 网络流量呈线性增长
- 但违反ZDR要求(服务器必须存储对话状态)

如何使用

Prompt构建检查清单

  1. 按稳定性排序消息:静态内容 → 可变内容
  2. 绝不修改现有消息:始终追加新消息
  3. 保持工具顺序一致:以确定性顺序枚举工具
  4. 插入而非更新:配置变更时添加新消息

配置变更处理

| 变更类型 | 禁止操作 | 正确操作 | |------------------|------------------------|--------------------------------------| | 沙箱/审批模式 | 修改权限消息 | 插入新的role=developer消息 | | 工作目录 | 修改环境消息 | 插入新的role=user消息 | | 工具列表 | 对话中途变更 | 尽可能避免;否则会破坏缓存 |


MCP服务器注意事项

MCP服务器会推送notifications/tools/list_changed事件,用于标识工具列表发生变更。请勿在对话中途响应此事件,否则会破坏缓存命中效果。建议采用以下替代方案:

  • 将工具刷新操作延迟至对话边界节点执行
  • 或接受缓存未命中作为必要的权衡代价

实现示例

function buildPrompt(state: ConversationState): Prompt {
  const items: PromptItem[] = [];

  // 静态前缀(可缓存)
  items.push({ role: 'system', content: state.systemMessage });
  items.push({ type: 'tools', tools: state.tools });  // 务必保持顺序一致!
  items.push({ role: 'developer', content: state.instructions });

  // 可变内容(追加式添加)
  items.push(...state.history);

  return { items };
}

function handleConfigChange(
  state: ConversationState,
  newConfig: SandboxConfig
): ConversationState {
  // 禁止:修改现有权限消息
  // 正确:插入新消息
  return {
    ...state,
    history: [
      ...state.history,
      {
        role: 'developer',
        content: formatSandboxConfig(newConfig),
      },
    ],
  };
}

权衡

优点:

  • 线性采样成本:Prompt caching 让重复推理的复杂度从二次方降至线性
  • 兼容ZDR:无状态设计支持零数据保留(Zero Data Retention,ZDR)政策
  • 无服务器状态:规避previous_response_id带来的复杂度
  • 简洁的概念模型:精确前缀匹配的逻辑易于理解和推理

缺点:

  • 二次方网络流量:JSON负载大小仍呈二次方增长(仅采样过程被缓存)
  • 缓存脆弱性:对话中途的变更(如工具、模型切换)会破坏前缀匹配,导致缓存失效
  • 需严格控制内容顺序:所有静态内容必须置于可变内容之前
  • 工具枚举复杂度高:必须维持工具顺序的一致性
  • MCP服务器限制:工具的动态变更会引发缓存未命中

参考文献

关键词

包含OpenAI Codex相关技术资源,涵盖Codex智能体循环解析博客、提示缓存官方文档、Codex命令行工具GitHub仓库,以及关联的上下文窗口自动压缩技术模式。

来源摘要

正在获取来源并生成中文摘要…

来源: https://openai.com/index/unrolling-the-codex-agent-loop/

← 返回社区