一个小问题
几个月前,当我们打算把Codex CLI的功能集成到Pywen中时,我第一次系统阅读了 Codex CLI 源码,
在研究了OpenAI新退出的Responses API时,对一个细节长期“卡壳”,
既然 Responses API 提供了 previous_response_id 这种“引用上一次响应、减少重复传输”的能力,
为什么 Codex 依然选择使用传统的历史记录的方式重新发送给模型?
从代码表面看,这像是故意走一条更“重”的路:对话越长,请求里的历史记录越多,
似乎还会带来二次增长的开销。只靠阅读源代码,无法解释这种反直觉。
当时我做过一个工程猜测: Codex 需要兼容多种模型/多种后端,
因为并不是所有模型后端都完整支持这套 Responses API,
当时(大约2025年10月份)只有gpt-5以及gpt-5-codex模型使用这套Responses API,
其他老模型压根没有previous_response_id这个参数,所以猜测Codex为了跨端点通用,
干脆直接全都不用。这个猜测虽然合理,但是不够全面。
源码里只体现“做了什么”,无法告诉我们“为什么这么做”。
直到读到 OpenAI 官方开发人员 Michael Bolin 发布的那篇博客Unrolling the Codex agent loop,
整个问题才真正闭环,Codex 不使用 previous_response_id,
虽然有兼容性的考量,但最重要的目的,恰恰是为了保持无状态。
一、为什么有状态更方便
如果用一句话概括:Codex 的 agent loop 在同一个对话里反复迭代(推理→工具→推理→工具…), 每次再次请求模型时,它并不引用上一次 response 来“增量续写”, 而是把已有历史 items 作为前缀完整携带,再把本次新增的 items 追加到末尾, 重新发起一次 Responses API 调用。

主流的Agent都是这么做的,而且为了应对越来越多的上下文累积,
客户端不得不去做一些诸如上下文压缩的这种工作。
这是因为使用历史记录作为输入会带来一个显而易见的后果:对话越长,payload 越大;
turn 内工具调用越多,重复携带的内容越多。
从工程直觉出发,很自然会想到:previous_response_id 看起来正是为解决这种“对话增长带来的重复传输”问题而生。
当然这并不意味着模型本身是有状态的,只不过OpenAI在后端层面帮我实现了状态。
二、为什么不使用有状态API?
“是否使用 previous_response_id”本质上不是一个局部优化点,而是一个架构立场:状态究竟放在哪里?
- 用
previous_response_id的思路:把对话状态更多交给服务端保存/索引,客户端用一个 ID 去引用它。 - Codex 当前的思路:请求尽量自包含(stateless),客户端把状态作为 items 带过去,服务端不需要为会话存储负责。
这两种立场的差异,牵扯到合规(数据留存)、部署形态(第三方云/本地实现)、运维复杂度(会话存储)、以及性能策略(缓存命中条件)。 这些约束多数来自产品与平台层面的决策,而不是某个函数里的 if/else 能解释清楚的。
这次官方博客把 Codex 不使用 previous_response_id 的理由讲得很直白,核心可以归成三条主线。
1)保持请求无状态,降低对 Responses API 提供方的要求
Codex CLI 的端点是可配置的,可以对接任何实现 Responses API 的服务(OpenAI、云厂商、甚至本地)。
如果 Codex 强依赖 previous_response_id,
等于要求每个提供方都实现稳定的“会话状态存储与引用”机制:
保存什么、保存多久、如何回收、如何跨节点一致、如何容灾……这对接入方是额外负担,
也会降低“协议兼容即可接入”的可移植性。
这一点正是我之前阅读源码的猜测。
Codex 选择不使用它,直接把每次请求做成自包含,能显著简化服务端实现:只要实现一个标准的 Responses API,不需要额外扛会话状态管理。
2)支持 Zero Data Retention(ZDR)等合规配置
这点是很多人只看代码永远猜不到的“硬约束”。ZDR 的目标是尽量不在服务端保留用户数据。
如果使用 previous_response_id,服务端为了让这个 ID 可用,通常需要存储能重建上下文的东西(哪怕是压缩形式),这与“零数据留存”的精神冲突。
Codex 现在的设计是:服务端可以保留解密所需的密钥,但不需要保留对话数据本身;
对话的关键信息由客户端在每次请求时带上,敏感推理态可以通过 encrypted_content 之类的机制在不持久化明文的前提下继续发挥作用。
这条链路的存在,直接解释了“为什么明明有省事的 ID 引用机制却不用”:因为它会把系统推向“服务端需要记住你的对话”的方向,
而这恰恰是某些客户配置下不允许的。
3)性能优化的主战场不在网络重复,而在推理复用
Codex 用“严格前缀 + prompt caching”对冲二次增长, 从表面看,重复携带 JSON 是浪费;但博客强调, 真正昂贵的是模型采样(推理),网络流量那点开销通常不值一提。 于是 Codex 把设计重心放在“让推理可复用”上: 保证新一轮请求的 prompt 是旧 prompt 的严格前缀, 只在末尾追加新 items, 从而最大化 prompt caching 的命中机会。缓存命中后,推理成本接近线性增长,而不是每轮从头算起。
这也解释了 Codex 为什么对工具列表顺序要求一致性, 历史记录中途不要改前面消息等规则,甚至 检测到有改动直接报错,因为这涉及到缓存能否命中。
三、如何解决这种上下文疯涨?
尽管上面介绍到的通过缓存命中能减少Token消耗,但实际上依然无法消除上下文增长, 当上下文窗口快被撑爆时,使用 compact 把历史重写成更短但可用的 items 列表,继续跑 agent loop。
关于如何把压缩历史对话,是Agent设计中另一个核心的模块,会在后续的内容中再做介绍。
为什么比是什么更重要
其实这也非常符合我自己的代码注释准则,注释或者文档中去记录为什么,要比注释做了什么重要的多。 代码做了什么,只要我们仔细阅读代码总能看出来,特别是现在有了大模型的帮助,解释代码做了什么更加方便高效。 但是为什么这样做,却只有作者本人清楚。一定程度上,为什么这么做,才是一份代码的核心价值。