Notes

深入解析 Codex 智能体循环

Unrolling the Codex agent loop

OpenAI 2026年01月23日 查看原文
中文翻译 / 转载笔记,保留原始文章结构,便于站内阅读与检索。

作者:Michael Bolin,技术团队成员

Codex CLI⁠(在新窗口中打开) 是我们打造的跨平台本地软件智能体,其设计宗旨是在你的计算机上安全、高效地运行,并交付高质量、可靠的软件变更。自四月首次发布该 CLI 以来⁠,我们在构建先进软件智能体方面积累了丰富经验。为分享这些收获与思考,我们推出了本系列文章。作为开篇之作,本文将深入解析 Codex 的运作机制与核心原理,并分享我们在实践中积累的经验。(如需详细了解 Codex CLI 的底层实现细节,请查看我们的开源仓库:https://github.com/openai/codex⁠(在新窗口中打开)。我们的许多设计决策细节都记录在 GitHub 的议题和 Pull Request 中,可供深入查阅。)

首先,我们将聚焦于 智能体循环 — 这是 Codex CLI 的核心逻辑,负责协调用户、模型以及模型为执行任务所调用的工具三者之间的交互。希望本文能帮助你全面理解我们的智能体(或称“运行框架”)在运用大语言模型(LLM)时所发挥的关键作用。

在深入之前,先简单说明一下术语:在 OpenAI,“Codex”是指一系列软件智能体产品,包括 Codex CLI、Codex Cloud 和 Codex VS Code 扩展。本文重点探讨 Codex 的 运行框架。它提供了核心的智能体循环与执行逻辑,是所有 Codex 体验的基石,并通过 Codex CLI 对外提供。为行文简洁,下文将交替使用“Codex”与“Codex CLI”。

智能体循环

对任何一个 AI 智能体来说,其核心都是一套称为“智能体循环”的运行机制。其简化示意如下图所示:

Image 1: 标题为“Agent loop”的示意图,展示了 AI 系统如何处理用户请求、调用工具、观察结果、更新计划并返回输出。箭头连接用户输入、模型推理、工具操作和最终响应等步骤。

首先,智能体接收用户 输入,并将其整合为一系列为可供模型使用的文本指令,这一系列指令称为 提示

接着,智能体将指令发送给模型,并要求模型生成响应,这个过程称为 推理。在推理过程中,文本提示首先被转换为一串输入 Token⁠(在新窗口中打开) — 这些 Token 是用于索引模型词汇表的整数值。随后,这些 Token 被用于对模型进行采样,从而生成一串新的输出 Token。

输出 Token 会转换回文本作为模型响应。由于 Token 是逐次生成的,转换过程可以与模型运行同步进行,因此许多大语言模型应用能实现流式输出。实际应用中,推理过程通常封装在一个处理文本的 API 内部,从而对使用者隐藏了 Token 化的细节。

推理步骤完成后,模型可能产生两种结果:(1) 生成对用户原始输入的最终响应;或 (2) 请求智能体执行一次 工具调用(例如,“运行 ls 命令并返回结果”)。若是情况 (2),智能体则执行所请求的工具调用,并将工具执行的结果追加到原始提示中。工具的输出结果将用于生成新的输入,并再次提交给模型;智能体基于这个更新后的上下文,开始新一轮推理。

此过程循环往复,直至模型不再发起工具调用,转而生成一条面向用户的消息(在 OpenAI 模型中称为 助手消息)。多数情况下,此消息会直接回应用户的初始请求,但也可能是一个向用户提出的追问。

由于智能体能够执行会修改本地环境的工具调用,因此其“输出”并不局限于助手消息本身。在许多场景下,软件智能体最主要的“输出”,其实是它在你计算机上直接编写或修改的代码。然而,每一轮交互最终都会由一条助手消息来结束,例如“你要求的 architecture.md 文件已添加”,这标志着智能体循环达到终止状态。从智能体的视角来看,其任务已告完成,控制权也随之交还给用户。

图中所示的从 用户输入智能体响应 的完整过程,被称为一次对话 轮次(在 Codex 中也称为一个 对话线程)。需要注意的是,一次 对话轮次 可能包含模型推理工具调用之间的多次循环迭代。每当你向既有对话发送新消息时,包括之前所有轮次的消息与工具调用在内的完整对话历史,都会被纳入新轮次的提示中:

Image 2: 标题为“Multi-turn agent loop”的图示,展示 AI 智能体如何迭代地接收用户输入、生成行动、咨询工具、更新状态并返回结果。图示包含带标签的步骤、箭头以及展示智能体推理循环的示例工具输出。

这意味着,随着对话的推进,用于模型采样的提示也会越来越长。提示长度至关重要,因为每个模型都有一个 上下文窗口,即其单次推理调用所能处理的最大 Token 数。需要注意,此窗口的容量同时涵盖了输入 输出 Token。可以想象,智能体在单轮交互中可能发起数百次工具调用,因此存在耗尽上下文窗口的风险。正因如此,上下文窗口管理 便成了智能体的核心职责之一。现在,让我们深入 Codex 内部,看其如何运行智能体循环。

模型推理

Codex CLI 通过向 Responses API⁠(在新窗口中打开) 发送 HTTP 请求来执行模型推理。接下来,我们将详细分析信息如何流经 Codex,以及它如何利用 Responses API 来驱动智能体循环。

Codex CLI 所使用的 Responses API 端点是可配置的⁠(在新窗口中打开),这意味着它可以与任何实现 Responses API⁠(在新窗口中打开) 的端点一同工作,例如:

接下来,我们探讨 Codex 如何为对话中的第一次推理调用创建提示。

构建初始提示

作为最终用户,你在查询 Responses API 时,并不需要逐字输入用于对模型进行采样的提示。取而代之的是,你只需在查询中指定各类输入,Responses API 服务器会负责将这些信息组织成模型可接受的提示结构。你可以将提示理解为一个“项目列表”;本节将阐述你的查询如何被转换为此列表。

在初始提示中,列表内的每个项目都关联一个角色。角色决定了其关联内容的权重优先级,取值如下(按优先级降序排列):systemdeveloperuserassistant

Responses API⁠(在新窗口中打开) 接收一个包含多个参数的 JSON 负载。我们重点关注以下三个参数:

在 Codex 中,instructions 字段可从 model_instructions_file⁠(在新窗口中打开)(路径为~/.codex/config.toml)中读取(如已指定);否则, 使用与模型关联的 base_instructions⁠(在新窗口中打开)。这些模型特定的指令存放在 Codex 代码仓库中,并随 CLI 打包发布(例如 gpt-5.2-codex_prompt.md⁠(在新窗口中打开))。

tools 字段是一个工具定义列表,需符合 Responses API 定义的 schema。对 Codex 而言,这一列表通常包含:Codex CLI 内置工具、通过 Responses API 向 Codex 开放的工具,以及用户(通常通过 MCP 服务器)提供的工具:

JavaScript

1[2 // Codex's default shell tool for spawning new processes locally.3 {4 "type": "function",5 "name": "shell",6 "description": "Runs a shell command and returns its output...",7 "strict": false,8 "parameters": {9 "type": "object",10 "properties": {11 "command": {"type": "array", "description": "The command to execute", ...},12 "workdir": {"description": "The working directory...", ...},13 "timeout_ms": {"description": "The timeout for the command...", ...},14 ...15 },16 "required": ["command"],17 }18 }1920 // Codex's built-in plan tool.21 {22 "type": "function",23 "name": "update_plan",24 "description": "Updates the task plan...",25 "strict": false,26 "parameters": {27 "type": "object",28 "properties": {"plan":..., "explanation":...},29 "required": ["plan"]30 }31 },3233 // Web search tool provided by the Responses API.34 {35 "type": "web_search",36 "external_web_access": false37 },3839 // MCP server for getting weather as configured in the40 // user's ~/.codex/config.toml.41 {42 "type": "function",43 "name": "mcp__weather__get-forecast",44 "description": "Get weather alerts for a US state",45 "strict": false,46 "parameters": {47 "type": "object",48 "properties": {"latitude": {...}, "longitude": {...}},49 "required": ["latitude", "longitude"]50 }51 }52]

最后,JSON 负载中的 input 字段本身也是一个项目列表。在添加用户消息之前,Codex 会先将以下项目插入⁠(在新窗口中打开)input

1.一条 role=developer 的消息,用于描述沙盒权限。此沙盒 仅适用于在tools部分定义的、由 Codex 提供的 shell 工具。也就是说,其他工具(例如来自 MCP 服务器的工具)不受 Codex 沙盒限制,需自行负责执行其防护措施。

该消息基于一个模板构建,其关键内容来自打包到 Codex CLI 中的 Markdown 代码片段,例如 workspace_write.md⁠(在新窗口中打开)on_request.md⁠(在新窗口中打开)

纯文本

1<permissions instructions>2 - description of the sandbox explaining file permissions and network access3 - instructions for when to ask the user for permissions to run a shell command4 - list of folders writable by Codex, if any5</permissions instructions>

  1. (可选)一条 role=developer 的消息,其内容是从用户 config.toml 文件中读取的 developer_instructions 值。

3.(可选)一条 role=user 的消息,其内容为“用户指令”,这些指令并非源自单一文件,而是从多个来源聚合而来⁠(在新窗口中打开)。通常,更具体的指令会出现在更靠后的位置:

  1. 一条 role=user 的消息,描述智能体当前运行的本地环境。该消息会指定当前工作目录和用户的 shell⁠(在新窗口中打开)

纯文本

1<environment_context>2 <cwd>/Users/mbolin/code/codex5</cwd>3 <shell>zsh</shell>4</environment_context>

当 Codex 根据前述计算对 input 进行初始化后,就会将用户消息追加进去,从而开始一轮新的对话。

前面的示例侧重于每条消息的内容,但请注意,input 中的每个元素都是一个 JSON 对象,包含 typerole⁠(在新窗口中打开)content 字段,如下所示:

JSON

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

当 Codex 构建好要发送给 Responses API 的完整 JSON 负载后,它会根据 ~/.codex/config.toml 中 Responses API 端点的配置,发起一个带有 Authorization 标头的 HTTP POST 请求(如果指定了额外的 HTTP 头和查询参数,也会一并添加)。

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

Image 3: 展示 AI 智能体循环中单一步骤的快照图。用户请求输入模型,模型生成一个思考、一个带工具名称的行动和一个工具输入。该图突出了工具被调用之前的这个中间推理步骤。

如你所见,提示中前三个项目的顺序由服务器决定,而非客户端。也就是说,在这三个项目中,只有 system message 的内容也由服务器控制,因为 toolsinstructions 由客户端决定。之后,来自 JSON 负载的 input 会紧随其后,从而完成整个提示的构建。

现在,我们已准备好提示,可以对模型进行采样。

第一轮对话

这次对 Responses API 的 HTTP 请求开启了 Codex 中的第一轮对话。服务器回复一个服务器发送事件 (SSE⁠(在新窗口中打开)) 流。每个事件的 data 是一个 JSON 负载,其”type”字段以”response”开头,可能类似以下示例(完整事件列表请参阅我们的 API 文档⁠(在新窗口中打开)):

纯文本

1data: {"type":"response.reasoning_summary_text.delta","delta":"ah ", ...}2data: {"type":"response.reasoning_summary_text.delta","delta":"ha!", ...}3data: {"type":"response.reasoning_summary_text.done", "item_id":...}4data: {"type":"response.output_item.added", "item":{...}}5data: {"type":"response.output_text.delta", "delta":"forty-", ...}6data: {"type":"response.output_text.delta", "delta":"two!", ...}7data: {"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 字段中:

JavaScript

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

那么,用于在后续查询中对模型进行采样的提示将如下所示:

Image 4: 标题为“Snapshot 2”的示意图,展示了一次工具调用后的 AI 智能体。模型接收工具观测结果,并产生新的思考和行动。箭头连接输入、观测和输出,以展示智能体如何迭代其推理循环。

需要特别注意的是,旧提示是新提示的 精确前缀。这是有意为之的,因为这样可以显著提升后续请求的效率,让我们得以充分利用 提示缓存(详见下一节“性能”部分)。

回顾智能体循环的第一张示意图,我们可以看到推理和工具调用之间可能发生多次迭代。提示可能会持续增长,直到我们最终收到一条助手消息,标志该轮对话结束:

纯文本

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

在 Codex CLI 中,我们会向用户展示助手消息,并自动聚焦输入框,提示用户该轮到自己继续输入、推进对话了。如果用户做出回应,则上一轮的助手消息和用户的新消息都必须追加到 Responses API 请求的 input 中,以开始新一轮对话:

JavaScript

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

再次强调,随着对话不断推进,我们发送给 Responses API 的 input 长度会不断增加:

Image 5: 标题为“Snapshot 3”的示意图,展示了 AI 智能体循环的最终阶段。收到工具结果后,模型生成一个总结性思考,并向用户返回最终答案。箭头说明了从工具输出到完成响应的过渡。

接下来,我们分析这个不断增长的提示会对性能产生什么样的影响。

性能考量

不少读者这时可能会想到:“如果把整段对话都算上,发送到 Responses API 的 JSON 总量是不是呈 二次增长?”这种直觉是正确的。尽管 Responses API 确实支持一个可选的 previous_response_id⁠(在新窗口中打开) 参数来缓解此问题,但 Codex 目前并未使用它,主要是为了保持请求完全无状态,并支持零数据保留 (ZDR) 配置。

选择不使用 previous_response_id,可以让 Responses API 提供方的实现更简单,因为每个请求都将保持 无状态。对选择启用零数据保留(ZDR)⁠(在新窗口中打开)的客户而言,这也让支持流程更简单明晰,因为只要依赖 previous_response_id,就需要存储相应数据,而这与 ZDR 的原则相违背。请注意,ZDR 客户不会牺牲从先前轮次专有推理消息中获益的能力,因为相关的 encrypted_content 可以在服务器上解密。(OpenAI 会保存 ZDR 客户的解密密钥,但不会保存他们的数据。)有关支持 ZDR 的 Codex 相关更改,请参见 PR #642⁠(在新窗口中打开)#1641⁠(在新窗口中打开)

通常,模型采样的成本远高于网络流量成本,因此采样成为我们提升效率的主要目标。正因如此,提示缓存才至关重要,它使我们能够重用先前推理调用中的计算。一旦命中缓存,模型采样的开销就会从二次复杂度降为线性复杂度。我们的提示缓存⁠(在新窗口中打开)文档有更详细的说明:

缓存命中仅在提示内部存在精确的前缀匹配时才可能发生。为实现缓存优势,请将指令和示例等静态内容置于提示开头,而将用户特定信息等可变内容置于提示末尾。这也适用于图像和工具,它们在各次请求间必须完全相同。

考虑到这一点,让我们看看哪些类型的操作可能导致 Codex 发生“缓存未命中”:

  • 在对话中途更改模型可用的 tools
  • 更改作为 Responses API 请求目标的模型(实际上,这会更改原始提示中的第三项,因为该项包含模型特定的指令)。
  • 更改沙盒配置、审批模式或当前工作目录。

Codex 团队在 Codex CLI 中引入可能损害提示缓存的新功能时,必须非常谨慎。例如,我们最初对 MCP 工具的支持曾引入一个未能以固定顺序枚举工具的 bug⁠(在新窗口中打开),导致缓存未命中。请注意,MCP 工具可能尤其棘手,因为 MCP 服务器可以通过 notifications/tools/list_changed⁠(在新窗口中打开) 通知动态更改其提供的工具列表。在长对话中途响应此通知,可能导致代价高昂的缓存未命中。

在可能的情况下,我们通过向 input 追加一条消息(而非修改之前的消息)来处理对话中发生的配置更改:

  • 如果沙盒配置或审批模式发生变化,我们插入⁠(在新窗口中打开)一条新的 role=developer 消息,其格式与原始的 <permissions instructions> 项目相同。
  • 如果当前工作目录发生变化,我们插入⁠(在新窗口中打开)一条新的 role=user 消息,其格式与原始的 <environment_context> 相同。

我们竭尽全力确保缓存命中以提升性能。我们还需管理另一个关键资源:上下文窗口。

为避免耗尽上下文窗口,我们采用的通用策略是:当 Token 总数超过设定阈值时,就对当前对话进行 压缩。具体来说,我们用一个新的、更小的项目列表替换 input,该列表代表了对话内容,使智能体能够在理解已发生情况的基础上继续工作。早期的压缩实现⁠(在新窗口中打开)要求用户手动调用 /compact 命令,该命令会使用现有对话加上用于摘要⁠(在新窗口中打开)的自定义指令来查询 Responses API。Codex 将包含摘要的助手消息结果用作新的 input⁠(在新窗口中打开),用于后续对话轮次。

在后续版本中,Responses API 引入了专门的 /responses/compact端点⁠(在新窗口中打开),用于更高效地执行对话压缩。该端点会返回一个项目列表⁠(在新窗口中打开),可用来替代先前的 input 以继续对话,同时释放上下文窗口。此列表包含一个特殊的 type=compaction 项目,附带一个不透明的 encrypted_content 项目,用于保存模型对原始对话的潜在理解。现在,当超过 auto_compact_limit⁠(在新窗口中打开) 时,Codex 会自动使用此端点压缩对话。

下期预告

我们已经介绍了 Codex 智能体循环,并逐步讲解了 Codex 在查询模型时如何构建和管理其上下文。在此过程中,我们强调了在 Responses API 之上构建智能体循环时的实用考量和最佳实践。

虽然智能体循环为 Codex 奠定了基础,但这仅仅是个开始。在接下来的文章中,我们将深入探讨 CLI 的架构、研究工具使用的实现方式,并仔细审视 Codex 的沙盒模型。

作者

Michael Bolin

致谢

特别感谢构建 Codex CLI 的整个团队。

继续阅读

查看全部