Notes
深入解析 Codex 智能体循环
Unrolling the Codex agent loop
作者: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 智能体来说,其核心都是一套称为“智能体循环”的运行机制。其简化示意如下图所示:
首先,智能体接收用户 输入,并将其整合为一系列为可供模型使用的文本指令,这一系列指令称为 提示。
接着,智能体将指令发送给模型,并要求模型生成响应,这个过程称为 推理。在推理过程中,文本提示首先被转换为一串输入 Token(在新窗口中打开) — 这些 Token 是用于索引模型词汇表的整数值。随后,这些 Token 被用于对模型进行采样,从而生成一串新的输出 Token。
输出 Token 会转换回文本作为模型响应。由于 Token 是逐次生成的,转换过程可以与模型运行同步进行,因此许多大语言模型应用能实现流式输出。实际应用中,推理过程通常封装在一个处理文本的 API 内部,从而对使用者隐藏了 Token 化的细节。
推理步骤完成后,模型可能产生两种结果:(1) 生成对用户原始输入的最终响应;或 (2) 请求智能体执行一次 工具调用(例如,“运行 ls 命令并返回结果”)。若是情况 (2),智能体则执行所请求的工具调用,并将工具执行的结果追加到原始提示中。工具的输出结果将用于生成新的输入,并再次提交给模型;智能体基于这个更新后的上下文,开始新一轮推理。
此过程循环往复,直至模型不再发起工具调用,转而生成一条面向用户的消息(在 OpenAI 模型中称为 助手消息)。多数情况下,此消息会直接回应用户的初始请求,但也可能是一个向用户提出的追问。
由于智能体能够执行会修改本地环境的工具调用,因此其“输出”并不局限于助手消息本身。在许多场景下,软件智能体最主要的“输出”,其实是它在你计算机上直接编写或修改的代码。然而,每一轮交互最终都会由一条助手消息来结束,例如“你要求的 architecture.md 文件已添加”,这标志着智能体循环达到终止状态。从智能体的视角来看,其任务已告完成,控制权也随之交还给用户。
图中所示的从 用户输入 到 智能体响应 的完整过程,被称为一次对话 轮次(在 Codex 中也称为一个 对话线程)。需要注意的是,一次 对话轮次 可能包含模型推理与工具调用之间的多次循环迭代。每当你向既有对话发送新消息时,包括之前所有轮次的消息与工具调用在内的完整对话历史,都会被纳入新轮次的提示中:
这意味着,随着对话的推进,用于模型采样的提示也会越来越长。提示长度至关重要,因为每个模型都有一个 上下文窗口,即其单次推理调用所能处理的最大 Token 数。需要注意,此窗口的容量同时涵盖了输入 与 输出 Token。可以想象,智能体在单轮交互中可能发起数百次工具调用,因此存在耗尽上下文窗口的风险。正因如此,上下文窗口管理 便成了智能体的核心职责之一。现在,让我们深入 Codex 内部,看其如何运行智能体循环。
模型推理
Codex CLI 通过向 Responses API(在新窗口中打开) 发送 HTTP 请求来执行模型推理。接下来,我们将详细分析信息如何流经 Codex,以及它如何利用 Responses API 来驱动智能体循环。
Codex CLI 所使用的 Responses API 端点是可配置的(在新窗口中打开),这意味着它可以与任何实现 Responses API(在新窗口中打开) 的端点一同工作,例如:
- 通过 ChatGPT 登录(在新窗口中打开)使用 Codex CLI 时,会使用
https://chatgpt.com/backend-api/codex/responses作为端点 - 使用 API 密钥对 OpenAI 托管模型进行身份验证时,会使用 (在新窗口中打开)
https://api.openai.com/v1/responses作为端点 - 使用
--oss参数运行 Codex CLI 以配合 gpt-oss(并与 ollama 0.13.4+(在新窗口中打开) 或 LM Studio 0.3.39+(在新窗口中打开) 一同使用)时,默认端点为本地运行的http://localhost:11434/v1/responses - Codex CLI 也可与 Azure 等云服务商托管的 Responses API 协同运行
接下来,我们探讨 Codex 如何为对话中的第一次推理调用创建提示。
构建初始提示
作为最终用户,你在查询 Responses API 时,并不需要逐字输入用于对模型进行采样的提示。取而代之的是,你只需在查询中指定各类输入,Responses API 服务器会负责将这些信息组织成模型可接受的提示结构。你可以将提示理解为一个“项目列表”;本节将阐述你的查询如何被转换为此列表。
在初始提示中,列表内的每个项目都关联一个角色。角色决定了其关联内容的权重优先级,取值如下(按优先级降序排列):system、developer、user、assistant。
Responses API(在新窗口中打开) 接收一个包含多个参数的 JSON 负载。我们重点关注以下三个参数:
instructions(在新窗口中打开):插入到模型上下文中的系统(或开发者)消息tools(在新窗口中打开):模型在生成响应时可能调用的工具列表input(在新窗口中打开):提供给模型的文本、图像或文件输入列表
在 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>
- (可选)一条
role=developer的消息,其内容是从用户config.toml文件中读取的developer_instructions值。
3.(可选)一条 role=user 的消息,其内容为“用户指令”,这些指令并非源自单一文件,而是从多个来源聚合而来(在新窗口中打开)。通常,更具体的指令会出现在更靠后的位置:
$CODEX_HOME目录中AGENTS.override.md和AGENTS.md的内容- 受大小限制(默认 32 KiB),从
cwd的 Git/项目根目录(如果存在)开始,向上逐级检查每个目录,直至cwd本身:添加AGENTS.override.md、AGENTS.md、或 config.toml 中由project_doc_fallback_filenames指定的任何文件的内容 - 如果已配置任何技能(在新窗口中打开):
- 一段关于技能的简短前言
- 每个技能的技能元数据(在新窗口中打开)
- 一个关于如何使用技能(在新窗口中打开)的章节
- 一条
role=user的消息,描述智能体当前运行的本地环境。该消息会指定当前工作目录和用户的 shell(在新窗口中打开):
纯文本
1<environment_context>2 <cwd>/Users/mbolin/code/codex5</cwd>3 <shell>zsh</shell>4</environment_context>
当 Codex 根据前述计算对 input 进行初始化后,就会将用户消息追加进去,从而开始一轮新的对话。
前面的示例侧重于每条消息的内容,但请注意,input 中的每个元素都是一个 JSON 对象,包含 type、role(在新窗口中打开) 和 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 的自定义实现可能做出不同选择):
如你所见,提示中前三个项目的顺序由服务器决定,而非客户端。也就是说,在这三个项目中,只有 system message 的内容也由服务器控制,因为 tools 和 instructions 由客户端决定。之后,来自 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]
那么,用于在后续查询中对模型进行采样的提示将如下所示:
需要特别注意的是,旧提示是新提示的 精确前缀。这是有意为之的,因为这样可以显著提升后续请求的效率,让我们得以充分利用 提示缓存(详见下一节“性能”部分)。
回顾智能体循环的第一张示意图,我们可以看到推理和工具调用之间可能发生多次迭代。提示可能会持续增长,直到我们最终收到一条助手消息,标志该轮对话结束:
纯文本
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 长度会不断增加:
接下来,我们分析这个不断增长的提示会对性能产生什么样的影响。
性能考量
不少读者这时可能会想到:“如果把整段对话都算上,发送到 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 的整个团队。