理解 Codex Harness
理解 Codex Harness
最近在看 Codex 的实现时,主要想弄清楚几件事:API Key 和 ChatGPT OAuth 最后到底怎么发请求,AGENTS.md、Plan Mode、Skill、/goal 这些东西最终被放到了哪里,一个 thread 跑久以后 context 是每轮重新拼还是不断追加,以及为什么 Codex 在明确的软件工程任务上很好用,但拿来长时间聊天或者讨论 idea 时总有比较强的收敛倾向。把这些问题串起来以后,核心其实就是 Codex Harness 如何维护一次长期运行的模型上下文。
Provider
Codex 对不同 provider 的处理并不复杂。以 OpenAI 自己的 API Key 和 ChatGPT OAuth 为例,二者在客户端就会选择不同的 base URL。API Key 默认走:
1 | |
ChatGPT OAuth 则走:
1 | |
Responses client 再统一请求 {base_url}/responses,因此最终分别变成:
1 | |
和:
1 | |
认证也对应不同。API Key 一般就是:
1 | |
ChatGPT OAuth 使用 OAuth access token,并且还会附带 ChatGPT account 相关信息,例如:
1 | |
Codex backend 路径还可能携带 x-codex-installation-id、x-codex-routing-hint、thread/session 信息等 metadata。也就是说,provider 的差别首先体现在 endpoint、credential 和额外 header 上。
代码层面可以近似理解成:
1 | |
之后仍然使用同一套 Responses request 结构:
1 | |
因此 provider 这一层更像 transport 和 auth 的选择,模型请求本身仍然沿用统一的数据结构。
Request 的组成
正常的 Responses 请求可以先简化成:
1 | |
看 Codex context 时,最好把 instructions、input[] 和 tools[] 分开,因为三者的生命周期并不一样。
instructions 是模型级 base instructions。仓库里能看到默认 fallback prompt,例如:
1 | |
但实际运行时不一定直接使用这个文件。Model catalog 可以给具体 model 下发 instructions_template,另外 config 也允许覆盖,所以实际来源大致是:
1 | |
最终这一部分进入 request.instructions。
input[] 则是长期上下文的主体。除了普通的 user、assistant、tool call 和 tool result,Codex 自己也会不断生成 synthetic message,例如 AGENTS.md、environment、permissions、collaboration mode、skills、plugin instructions、goal continuation、model switch 等。它们最终都会以 ResponseItem 的形式和普通 conversation history 混在一起。
tools[] 不属于 history。每次请求前,Codex 会根据当前 StepContext 从 ToolRouter 重新导出:
1 | |
因此 input[] 更接近一个长期增长的 history,而 tools[] 更接近当前执行环境的 snapshot。
构建 Turn
忽略 hooks、telemetry、retry 等外围逻辑,一个普通 turn 可以简化成:
1 | |
capture_step_context() 会先固定本轮相关的 model、cwd、environment、permissions、MCP、tool router、loaded AGENTS.md 等状态。之后这些状态才会被转换成模型可见的 context。
首轮没有之前的 reference context,所以 Codex 会完整注入一次当前状态。内部会先把 fragment 分成几类:
1 | |
比较常见的对应关系是:
1 | |
其中 AGENTS.md 当前实现是 synthetic user message,而不是 developer message,这一点比较反直觉。
假设当前 repo 是 /workspace/codex。如果用户在 Plan Mode 下输入:
1 | |
第一次真正发给模型之前,history 大概会长成:
1 | |
然后是:
1 | |
再然后才是用户真正输入:
1 | |
由于指定了 $system-design,完整 SKILL.md 会被读取并继续追加:
1 | |
因此 Skill 是分两阶段加载的。首轮只告诉模型有哪些 skill 以及它们的描述,真正使用某个 skill 时再把完整内容放进 history,这样可以避免一开始就把所有 SKILL.md 全部塞进 context window。
WorldState 与 History
如果每一轮都重新加入 AGENTS.md、permissions、Plan Mode、environment、skills 等内容,context 会很快膨胀。Codex 为此维护了一个 WorldState,核心逻辑可以简化成:
1 | |
第一次建立 context 时完整注入,之后只追加发生变化的部分。
例如从 Plan Mode 切到 Default Mode,不会重新发送完整的 AGENTS.md、environment、permissions 和 skills,而只会继续在 history 后面增加新的 collaboration mode:
1 | |
旧的 Plan Mode message 仍然留在前面的 history 中,只是后面的新状态会覆盖之后的行为。AGENTS.md 改变时也是类似处理。假设 cwd 从 /workspace/codex 进入 /workspace/agent,而新的目录下有自己的 AGENTS.md,Codex 会继续 append 一条新的 synthetic user message,并显式写明:
1 | |
所以从整体上看,Codex 的 model-visible context 很像 append-only history:旧状态不直接修改,而是通过新的 message 描述状态变化。
Plan Mode
Plan Mode 本身并不是一个独立的模型或者独立 session,而是当前 collaboration_mode 的一部分。它对应一段 developer instruction,大致会要求先理解环境、不要直接修改 repo、先澄清目标,并给出 implementation-ready plan。
切换到其他 mode 后,新的 collaboration mode 会继续追加进 history。因此 Plan Mode 更接近一段会随运行状态变化的 Harness policy。
/goal
/goal 也没有直接修改 system prompt。Goal extension 会自己维护 objective、status、token budget、progress 等状态,需要自动 continuation 时,会生成新的 synthetic user message,例如:
1 | |
之后这条 message 和普通 user input 一样进入 history。对模型来说,Goal 更像 Harness 自动插入的一条 steering message。
Model Switch 与 Compaction
切换 model 时,request.model 会改变,对应的 request.instructions 也可能换成新 model 的 base instructions,history 里还可能追加 <model_switch> 一类 transition message。但原来的 conversation 并不会因为换 model 就全部清空。
真正会重建 model-visible history 的是 compaction。正常情况下 history 持续增长:
1 | |
到 context window 边界以后,旧 history 会先被压缩,然后 Harness 会把当前有效的 AGENTS.md、permissions、mode、environment 等状态重新 materialize 到新的 context window 中,再继续 append 后续内容。
所以可以认为普通情况下 Codex 一直在 history 后面追加内容,compaction 才会替换当前 model-visible window,然后从新的 window 继续增长。
示例
下面这个例子把一次具体 context 从空 thread 到第一次 request,再到 /goal 和 Plan Mode 切换的过程拆开,可以逐步看每次到底增加了什么。
理解 context 的组成以后,Codex 在使用上的一些特点也比较容易解释。它长期携带 repo、AGENTS.md、environment、permissions、tools、Plan / Default mode、skills、goal、verification 等信息,这些 instruction 本身就一直把当前对话和“工程任务”绑定在一起。
因此即使一开始只是讨论一个 idea,它也很容易往查 repo、确定约束、形成 plan、开始实现和验证这个方向走。Plan Mode 可以推迟实现,但它的目标仍然是最后得到一个 implementation-ready plan,并不是为了长期保留多个开放方向。
所以这种差别并不一定来自模型本身。相同或者接近的模型放在不同 harness 里,本来就会表现得很不一样。Codex 更偏 execution-oriented harness;如果目标只是聊天、反复改变问题定义或者长时间保留多个未确定方向,大量 repo、permission、tool 和 completion 相关 context 反而会成为约束。