Codex 的 Agent Loop 是怎样构建的

Agent Loop 是 Codex CLI 中的核心交互逻辑,负责协调用户、模型以及工具之间的交互,Codex 将其称为「harness」。

Agent Loop

每个 AI Agent 的核心都有一个被称为 Agent Loop 的机制。Agent Loop 的简化示意图如下所示: 20260126151103 首先,Agent 从用户那里获取输入,将其纳入为模型准备的文本指令集合中,这被称为提示词(prompt)。

下一步是向模型发送指令并请求其生成响应来查询它,这个过程称为推理(inference)。在推理过程中,文本提示首先被转化为一系列输入令牌(input tokens)——即用于索引模型词汇表的整数。这些令牌随后被用于对模型进行采样,从而生成新的输出令牌(output tokens)。

输出令牌(output tokens)被转换回文本,成为模型的响应(response)。由于令牌是逐步生成的,这种转换可以在模型运行时同步进行,这也是许多基于 LLM 的应用程序能显示流式输出的原因。实际上,推理过程通常被封装在一个基于文本操作的 API 之后,从而抽象了令牌化的具体细节。

推理步骤的结果有两种情况:

  1. 生成对用户原始输入的最终回复
  2. 请求执行一次工具调用(例如,“运行 ls 并报告输出结果”)

在情况 2 中,Agent 会执行该工具调用,并将输出结果追加到原始提示中。此输出用于生成一个新的输入,进而重新查询模型;Agent 随后可考虑这一新信息并再次尝试。

这个过程会重复进行,直到模型停止生成工具调用,转而生成一条面向用户的消息(在 OpenAI 的模型中称之为“助手消息”(assistant message))。

在许多情况下,这条消息会直接回应用户的原始请求,但也可能是向用户提出的后续问题。

由于 Agent 能够执行修改本地环境的工具调用,其“输出”并不限于助手消息。在许多情况下,Agent 的主要输出是在机器上编写或编辑的代码。

尽管如此,每个轮次总是以一条助手消息(例如“我已添加了您请求的 architecture.md ”)结束,这标志着 Agent Loop 的终止状态。从 Agent 的角度来看,其工作已完成,控制权将返回给用户。

从用户输入到 Agent 响应的过程,如示意图所示,称为对话的一个轮次(在 Codex 中称为一个线程)。

这个对话轮次可能涉及模型推断与工具调用之间的多次迭代。每当您向现有对话发送新消息时,对话历史会作为新轮次提示的一部分被包含,其中包括先前轮次的消息和工具调用。

20260126153549

这意味着随着对话的增多,用于模型采样的提示长度也会增加。这一长度至关重要,因为每个模型都有一个上下文窗口(context window),即一次推理调用所能使用的最大令牌数。需要注意的是,这个窗口既包括输入令牌也包括输出令牌。可以想象为,一个 Agent 可能决定在一轮对话中调用数百个工具,从而耗尽上下文窗口。因此,管理上下文窗口是 Agent 的众多职责之一

接下来,开始深入了解 Codex 是如何运行 Agent Loop 的。

模型推理

Codex CLI 通过向 Responses API⁠ 发送 HTTP 请求来运行模型推理(Model inference),它能与任何实现 Responses API⁠ 的端点协同工作:

  • 使用 ChatGPT 登录
  • 使用 API 密钥
  • 本地 oss 模型
  • 云端提供商(如 Azure)

构建初始化提示

作为终端用户,在使用 Responses API 时无需一字不差地指定用于采样模型的提示词。相反,可以将各种输入类型指定为查询的一部分,而 Responses API 服务器则负责决定如何将这些信息组织成模型设计所适用的提示。

可以视提示词为一个“项目列表”,本节将解释查询如何被转化为这个列表。

在初始化提示中,列表中的每一项都与一个角色(role)相关联。角色指明了关联内容的重要程度,其值按优先级递减顺序如下:系统、开发者、用户、助手。

Responses API 接收一个包含众多参数的 JSON 载荷。重点关注以下三个参数:

  • instructions ⁠: 插入模型上下文中的系统(或开发者)消息
  • tools :模型生成响应时可调用的一系列工具列表
  • input ⁠:模型接收的文本、图像或文件输入列表

instructions

在 Codex 中, instructions 字段若指定则从 ~/.codex/config.toml 中的 model_instructions_file ⁠读取;否则就使用与模型⁠关联的 base_instructions 。base_instructions 存放于 Codex 代码库中,并已打包进了 CLI(如 gpt-5.2-codex_prompt.md ⁠)中。

tools

tools 字段是符合 Responses API 定义架构的工具定义列表。对于 Codex 而言,这包括由 Codex CLI 提供的工具、由 Responses API 提供并供 Codex 使用的工具,以及通过 MCP 服务器由用户提供的工具:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
[
  // Codex's default shell tool or spawning new processes locally.
  {
    "type": "function",
    "name": "shell",
    "description": "Runs a shell command and returns its output...",
    "strict": false,
    "parameters": {
      "type": "object",
      "properties": {
        "command": {"type": "array", "description": "The command to execute", ...},
        "workdir": {"description": "The working directory...", ...},
        "timeout_ms": {"description": "The timeout for the command...", ...},
        ...
      },
      "required": ["command"],
    }
  }

  // Codex's built-in plan tool
  {
    "type": "function",
    "name": "update_plan",
    "description": "Updates the task plan...",
    "strict": false,
    "parameters": {
      "type": "object",
      "properties": {"plan":..., "explanation":...},
      "required": ["plan"]
    }
  },

  // Web search tool provided by the Responses API.
  {
    "type": "web_search",
    "external_web_access": false
  },

  // MCP server for getting weather as configured in the
  // user's ~/.codex/config.toml.
  {
    "type": "function",
    "name": "mcp__weather__get-forecast",
    "description": "Get weather alerts for a US state",
    "strict": false,
    "parameters": {
      "type": "object",
      "properties": {"latitude": {...}, "longitude": {...}},
      "required": ["latitude", "longitude"]
    }
  }
]

input

最后的 input 字段是一个项目列表。在添加用户消息之前,会先将以下项目⁠插入到 input 中:

  1. 一条包含 role=developer 的消息,描述仅适用于 Codex 提供的在 tools 部分定义的 shell 工具的沙箱环境。也就是说,其他工具(例如来自 MCP 服务器提供的工具)不由 Codex 进行沙箱处理,需要自行实施防护措施。

    该信息是基于一个模板构建的,其中的关键内容片段来自捆绑到 Codex CLI 中的 Markdown 片段,例如 workspace_write.md ⁠ 和 on_request.md ⁠:

    <permissions instructions>
      - description of the sandbox explaining file permissions and network access
      - instructions for when to ask the user for permissions to run a shell command
      - list of folders writable by Codex, if any
    </permissions instructions>
    
  2. (可选)一条包含 role=developer 的消息,其内容是从用户 config.toml 文件中读取的 developer_instructions 值。

  3. (可选)一条包含 role=user 的消息,其内容为“用户指令”,这些指令并非源自单一文件,而是整合自多个来源⁠。通常,更具体的指令会出现在后面:

    • AGENTS.override.md 、 AGENTS.md 和 $CODEX_HOME 中的内容
    • 在默认限制(32 KiB)下,从项目的 cwd 根目录(如果存在)开始,逐级向上查找至 cwd 所在目录:添加其中任何 AGENTS.override.md 、 AGENTS.md 的内容,或由 project_doc_fallback_filenames in config.toml 指定的任何文件名
    • 如果配置有技能(Skill):
      • 一份关于技能的简短说明
      • 每个技能的元数据
      • 关于如何使用技能的部分
  4. 一条包含 role=user 的消息,用于描述当前运行 Agent 的本地环境。它指定了当前工作目录和用户使用的 shell⁠:

    <environment_context>
      <cwd>/Users/mbolin/code/codex5</cwd>
      <shell>zsh</shell>
    </environment_context>
    

在 Codex 完成上述所有计算以初始化 input 后,会附加用户消息以启动对话。

之前的例子侧重于每条消息的内容,但请注意, input 的每个元素都是一个 JSON 对象,包含 type 、 role ⁠和 content ,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "type": "message",
  "role": "user",
  "content": [
    {
      "type": "input_text",
      "text": "Add an architecture diagram to the README.md"
    }
  ]
}

一旦 Codex 构建好要发送给 Responses API 的完整 JSON 载荷,它就会根据 Responses API 端点在 ~/.codex/config.toml 中的配置情况,使用对应的 Authorization 头部信息发起 HTTP POST 请求(如果指定了额外的 HTTP 头部和查询参数,也会一并添加)。

当 OpenAI Responses API 服务器收到请求时,它会使用 JSON 数据来推导出模型的提示,如下所示(请注意,自定义实现的 Responses API 可能会做出不同的选择):

20260126162544

可以看到,提示中前三个项目的顺序是由服务器决定的,而非客户端。但是在这三个项目中,只有系统消息的内容由服务器控制,因为 tools 和 instructions 是由客户端决定的。接着是来自 JSON 的有效载荷 input ,最终完成提示。

现在已经有了提示,准备对模型进行采样。

第一轮

这个向 Responses API 发出的 HTTP 请求启动了 Codex 中的第一轮“对话”。服务器会以 Server-Sent Events(SSE)流作为回复。

每个事件的 data 都是一个 JSON 负载,包含一个 "type" ,以 "response" 开始,类似于这样:

data: {"type":"response.reasoning_summary_text.delta","delta":"ah ", ...}
data: {"type":"response.reasoning_summary_text.delta","delta":"ha!", ...}
data: {"type":"response.reasoning_summary_text.done", "item_id":...}
data: {"type":"response.output_item.added", "item":{...}}
data: {"type":"response.output_text.delta", "delta":"forty-", ...}
data: {"type":"response.output_text.delta", "delta":"two!", ...}
data: {"type":"response.completed","response":{...}}

Codex 接收事件流并将其作为内部事件对象重新发布,供客户端使用。

像 response.output_text.delta 这样的事件用于支持 UI 中的流式传输。

而像 response.output_item.added 这样的其他事件则被转换为对象并追加到 input 中,供后续的 Responses API 调用使用。

假设对 Responses API 的首次请求包含两个 response.output_item.done 事件:一个带有 type=reasoning ,另一个带有 type=function_call 。当使用工具调用的响应再次查询模型时,这些事件必须体现在 JSON 的 input 字段中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
[
  /* ... original 5 items from the input array ... */
  {
    "type": "reasoning",
    "summary": [
      "type": "summary_text",
      "text": "**Adding an architecture diagram for README.md**\n\nI need to..."
    ],
    "encrypted_content": "gAAAAABpaDWNMxMeLw..."
  },
  {
    "type": "function_call",
    "name": "shell",
    "arguments": "{\"command\":\"cat README.md\",\"workdir\":\"/Users/mbolin/code/codex5\"}",
    "call_id": "call_8675309..."
  },
  {
    "type": "function_call_output",
    "call_id": "call_8675309...",
    "output": "<p align=\"center\"><code>npm i -g @openai/codex</code>..."
  }
]

用于采样模型作为后续查询一部分的提示词最终形式如下:

20260126162954

特别是要注意,旧提示词是新提示词的精确前缀。这是有意为之的,能充分利用提示缓存,从而大幅提高后续的请求效率。

回顾最初绘制的 Agent Loop 图,可以看到推理与工具调用之间可能存在多次迭代过程。提示信息会持续累积,直到最终接收到助手消息——这标志着当前回合的结束。

data: {"type":"response.output_text.done","text": "I added a diagram to explain...", ...}
data: {"type":"response.completed","response":{...}}

Codex CLI 会向用户展示助手消息并聚焦到输入框,向用户示意轮到他们"继续对话"。如果用户作出回应,前一轮的助手消息和用户的新消息都必须追加到 Responses API 请求的 input 中,以开启新一轮对话:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[
  /* ... all items from the last Responses API request ... */
  {
    "type": "message",
    "role": "assistant",
    "content": [
      {
        "type": "output_text",
        "text": "I added a diagram to explain the client/server architecture."
      }
    ]
  },
  {
    "type": "message",
    "role": "user",
    "content": [
      {
        "type": "input_text",
        "text": "That's not bad, but the diagram is missing the bike shed."
      }
    ]
  }
]

再次强调,由于是在延续对话,发送给 Responses API 的 input 长度会持续增加:

20260126163311

接下来探讨一下这个不断增长的提示对性能意味着什么。

性能考量

你可能会问:"等等,在整个对话过程中发送给 Responses API 的 JSON 数据量,难道不是按照 Agent Loop 的平方来增长的吗?"

完全没错。虽然 Responses API 确实支持可选的 previous_response_id 参数来缓解这个问题,但 Codex 目前并未使用它,主要是为了保持请求完全无状态,并支持零数据保留(ZDR)配置。

避免使用 previous_response_id 简化了 Responses API 提供方的工作,因为它确保每个请求都是无状态的。这也使得支持选择零数据保留(ZDR)的客户变得简单,因为存储支持 previous_response_id 所需的数据与 ZDR 原则相悖。

请注意,ZDR 客户不会牺牲从前几轮中获得专有推理信息的能力,因为相关的 encrypted_content 可以在服务器端解密。OpenAI 会保留 ZDR 客户的解密密钥,但不会保留数据。

一般而言,模型采样成本远高于网络传输成本,因此采样环节是效率优化的重点目标。这正是提示缓存如此重要的原因——它能复用先前推理调用中的计算结果。当缓存命中时,模型采样复杂度会从二次方降至线性。

关于提示缓存的文档对此有更详细的说明:

缓存命中仅当提示内容出现完全匹配的前缀时才可能实现。为充分发挥缓存优势,请将静态内容(如指令说明和示例)置于提示开头,而将动态内容(如用户特定信息)放在末尾。这一原则同样适用于图像和工具调用——两次请求间必须保持完全相同的内容构成。

哪些类型的操作可能导致 Codex 中发生“缓存未命中的情况”?

  • 在对话中途改变模型可用的 tools
  • 更改作为 Responses API 请求目标的 model 
  • 更改沙盒配置、审批模式或当前工作目录

Codex 团队在引入可能影响提示缓存的新功能时必须格外谨慎。例如,最初对 MCP 工具的支持引入了一个错误,即未能以一致的顺序枚举工具⁠,从而导致缓存未命中。请注意,MCP 工具可能尤其棘手,因为 MCP 服务器可以通过 notifications/tools/list_changed ⁠ 动态更改它们提供的工具列表。在长对话中途遵循此通知可能导致昂贵的缓存未命中。

在可能的情况下,通过向 input 追加一条新消息来反映配置变化,而不是修改之前的消息:

  • 如果沙盒配置或审批模式发生变化,就插入一条格式与原 <permissions instructions> 项相同的 role=developer 新消息
  • 如果当前工作目录发生变化,就插入一条格式与原 <environment_context> 相同的 role=user 新消息

Codex 为了确保性能缓存命中而不遗余力。但还有一个必须管理的关键资源:上下文窗口

Codex 避免耗尽上下文窗口的一般策略是,一旦令牌数量超过某个阈值,就压缩对话内容。具体来说,Codex 将 input 替换为一个新的、更小的项目列表,这个列表代表了对话内容,使得 Agent 能够继续对话,同时了解迄今为止发生的情况。

早期的压缩实现需要用户手动调用 /compact 命令,该命令会利用现有对话加上用于摘要的自定义指令来查询 Responses API。Codex 使用生成的结果助手消息(包含摘要)作为后续对话回合的新 input 。

自那时起,Responses API 已演变为支持一个特殊的 /responses/compact 端点⁠,该端点能更高效地执行压缩。它会返回一个列表⁠,可用于替代先前的 input 以继续对话,同时释放上下文窗口的空间。此列表包含一个特殊的 type=compaction 物品,附带一个不透明的 encrypted_content 物品,用以保留模型对原始对话的潜在理解。现在,当 auto_compact_limit ⁠超过限制时,Codex 会自动使用该端点来压缩对话。

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