当 OpenAI 发布 Codex(他们基于云的编码代理)时,他们必须解决的最难问题与 AI 模型本身几乎没有关系。
模型 codex-1 是 OpenAI 的 o3 针对软件工程微调的版本。它很重要,但也只是更大系统中的一个组件。真正的工程投入在模型周围的一切。
如何从五个不同来源组装正确的提示?当你的对话历史增长到威胁超出模型内存时会发生什么?如何让同一个代理在终端、Web 浏览器和三个不同 IDE 中工作而无需每次都重写它?
当 Codex 团队需要他们的代理在 VS Code 内部工作时,他们首先尝试了明显的方法并通过 MCP(连接 AI 模型到工具的新兴标准)暴露它。它不起作用。真正代理需要的丰富交互模式(如流式进度、在任务中间暂停等待用户批准,以及发出代码 diff)不能干净地映射到 MCP 提供的东西。所以团队从头构建了新协议。
在本文中,我们将看看 OpenAI 如何在模型周围构建正确的编排层。
免责声明:这篇文章基于 OpenAI 工程团队公开分享的细节。如果你发现任何不准确之处,请评论。
Codex 是什么
Codex 是可以编写功能、修复 bug、回答关于代码库的问题并提出拉取请求的编码代理。
每个任务在它自己的隔离云沙箱中运行,预加载你的仓库。你可以并行分配多个任务并实时监控进度。
Codex 幕后如何工作也很有趣。系统有三层值得理解:代理循环、提示和上下文管理,以及让一个代理服务于许多不同界面的多表面架构。
代理循环
在 Codex 的核心是称为代理循环的东西。代理接受用户输入,构建提示,将其发送到模型进行推理,并取回响应。
然而,那个响应不总是最终答案。通常,模型用工具调用响应,比如”运行这个 shell 命令并告诉我发生了什么”。当那发生时,代理执行工具调用,将输出附加到提示,并用这个新信息再次查询模型。这个循环重复,有时数十次,直到模型最终为用户产生最终消息。
使这不仅仅是简单循环的是 harness 在沿途管理的一切。
Codex 可以读取和编辑文件、运行 shell 命令、执行测试套件、调用 linter 和运行类型检查器。单个用户请求如”修复认证模块中的 bug”可能触发代理读取几个文件、运行现有测试看看什么失败、编辑代码、再次运行测试、修复 linting 错误,并在产生最终提交前再次运行测试。
模型在每个步骤进行推理,但 harness 处理其他一切,如执行命令、收集输出、管理权限,以及决定循环何时完成。
模型和 harness 之间的区别很重要,因为它塑造开发者实际如何使用 Codex。OpenAI 自己的工程团队使用它来卸载重复、范围明确的工作,如重构、重命名、编写测试和分类 on-call 问题。
代理循环也有外层。每个推理和工具调用周期构成 OpenAI 称为”turn”的东西。然而,对话不在一个 turn 后结束。当用户发送后续消息时,之前所有 turn 的历史(包括所有工具调用和它们的输出)都包含在下一个提示中。这是变得昂贵的地方,也是下一层复杂性开始的地方。
提示和上下文管理
当你输入请求到 Codex 时,你的消息成为更大提示的底层。在它上面,系统堆叠环境上下文(如你的当前工作目录和 shell)、仓库中任何 AGENTS.md 文件的内容(这些是代理的项目特定指令,涵盖编码约定和运行哪些测试命令等事情)、沙箱权限规则、配置文件中的开发者指令、模型特定指令、工具定义和系统消息。
每层都有角色(系统、开发者或用户),向模型表示其优先级。服务器控制顶层的顺序。客户端控制其余。这个分层构造意味着模型总是有关于它操作环境的丰富上下文。但这也意味着在用户说一个字之前提示已经很大。而且它只会从那里增长。
模型进行的每个工具调用产生输出并附加到提示。每个新对话 turn 包含之前所有 turn 的完整历史,包括工具调用。
这意味着在对话过程中发送到 API 的总 JSON 呈二次方增长。如果第一个 turn 发送 X 量数据,第二个 turn 重新发送所有 X 加上新数据,第三个 turn 重新发送所有那个加更多,依此类推。
OpenAI 故意接受这个成本。他们可以使用服务器端参数让 API 记住之前的对话状态,避免需要重新发送一切。他们选择不这样做,因为这样做会破坏每个请求的无状态性,并阻止支持需要零数据保留的客户。因此,每个请求都是自包含的并携带完整对话。
关键缓解是提示缓存。因为 Codex 总是将新内容附加到现有提示的末尾,旧提示总是新提示的精确前缀。这个前缀属性让 OpenAI 重用之前推理调用的计算,所以即使数据传输是二次方的,实际模型计算保持接近线性。
然而,前缀属性是脆弱的。任何改变提示开始或中间的东西(如切换模型、更改工具或改变沙箱配置)破坏缓存。当 OpenAI 添加对 MCP 工具的支持时,他们意外引入一个 bug,工具没有以一致顺序列出。那个不一致性本身就足以破坏缓存命中。
最终,即使有缓存,对话也命中上下文窗口限制(模型可以在单个推理调用中处理的最大令牌数)。当那发生时,Codex 压缩对话。它用更小的、代表性的版本替换完整历史,通过携带模型潜在状态的加密负载保留模型对发生什么的理解。实际上,压缩机制比简单摘要涉及更多细微差别,但核心思想成立:管理上下文窗口是一级工程问题,不是事后想法。
AGENTS.md 文件值得在这里简要提及,因为它们代表上下文应该生活在哪里的设计决策。OpenAI 不是将项目特定知识硬编码到系统中,而是让开发者将 AGENTS.md 文件放在他们的仓库中,与他们的代码一起。这些文件告诉 Codex 如何导航代码库、运行哪些测试命令,以及如何遵循项目的约定。模型有它们时表现更好,但没有它们也能工作。
多表面架构
Codex 作为 CLI 工具开始生命。你在终端运行它,它在你的本地代码库上操作。
然后 OpenAI 需要在 VS Code 中使用它,然后在 Web 应用中使用它。此外,它也需要在 macOS 桌面应用中使用。最后,像 JetBrains 和 Xcode 这样的第三方 IDE 也想集成它。为每个表面重写代理逻辑不是一个选项。
如前所述,第一次尝试是将 Codex 暴露为 MCP 服务器。然而,团队发现 MCP 的语义不能承载实际代理对话的完整重量。Codex 需要流式增量进度当模型推理时。它需要在任务中间暂停并询问用户在运行某些命令之前批准。它需要发出结构化 diff。这些交互模式对当时 MCP 提供的太丰富。
所以他们构建了 App Server。所有核心代理逻辑(代理循环、线程管理、工具执行、配置和认证)生活在一个 OpenAI 称为”Codex core”的单一代码库中。App Server 用任何客户端可以通过标准输入/输出讲的 JSON-RPC 协议包裹这个核心。
协议是完全双向的:
- 客户端可以发送请求到服务器(启动线程、提交任务)
- 服务器也可以发送请求回客户端,例如在执行 shell 命令之前请求批准
代理的 turn 暂停直到用户响应”允许”或”拒绝”。这让代理在自主性和人类监督之间平衡,而无需将该策略硬编码到代理循环本身。
不同地方以不同方式使用这个架构:
- VS Code 扩展和桌面应用捆绑 App Server 二进制文件,将其作为子进程启动,并保持双向 stdio 通道打开
- Web 应用在云容器中运行 App Server。worker 用签出的仓库配置容器,启动二进制文件,并通过 HTTP 流式传输事件到浏览器。状态生活在服务器上,所以即使用户关闭标签页工作也继续
- 像 Xcode 这样的合作伙伴通过保持客户端稳定并在新 App Server 二进制文件可用时指向它们来解耦它们的发布周期与 OpenAI 的。协议设计为向后兼容,所以旧客户端可以安全地与新服务器对话
这个架构不是从一开始就计划的。它从 CLI 进化,通过失败的 MCP 尝试,到现在支撑每个 Codex 表面的 App Server 协议。那个轨迹本身就是关于系统设计的有用教训:正确的抽象通常在你尝试错误的之前不存在。
OpenAI 的经验证明模型是组件,代理是系统。大部分工程在系统中。
如果你使用像 Codex 这样的工具,理解这些机制帮助你更有效地使用它们。编写清晰的 AGENTS.md 文件给代理提供有意义改进其输出的项目特定上下文。紧密范围的任务比模糊、开放式的请求工作得更好,因为代理循环在每次循环有清晰的下一步时最有效。知道长对话由于上下文窗口限制和压缩而退化解释了为什么为新任务开始新线程通常给出更好结果。
Codex 仍然有真实约束。它不能接受图像输入用于前端工作。你不能在任务中间纠正代理。委托给远程代理比交互式编辑花费更长时间,那个工作流转变需要习惯。OpenAI 正在朝着与 Codex 交互感觉更像与同事异步协作的未来工作,但那个愿景和当前产品之间的差距仍然显著。
参考
- Introducing Codex
- Unrolling the Codex agent loop
- Unrolling the Codex harness: how we built the App Server
本文为学习目的的个人翻译,译文仅供参考。
原文链接:How OpenAI Codex Works。
版权归原作者或原刊登方所有。本文为非官方译本;如有不妥,请联系删除。