拆解 Codex:一个桌面 Agent 到底由哪些核心部分组成?

拆解 Codex:一个桌面 Agent 到底由哪些核心部分组成?

写在前面

大家好,我是三雒。

最近一段时间在做一个桌面的Agent项目,一直在研究 Codex 的实现,也尝试把它的本地工具能力复用到自己的项目里。刚开始看代码时,我脑子里有一个很朴素的理解:Codex 不就是一个会调用 工具的大模型吗?

但真正顺着源码往下追,很快就会发现这个理解太薄了。

模型确实负责推理,也会生成 Tool Call,但它并不天然拥有当前工作目录、长进程和文件权限这些操作系统状态。真正把工作区信息提供给模型,并保证命令在正确目录执行的,是模型外面的 Agent Harness。一次持续几分钟甚至几小时的任务,也不是靠模型单次回答完成的。

这一篇是"拆解 Codex"系列的第一篇,我们先不钻进某个 Tool 的参数,也不急着分析每个 Rust 模块,而是先回答一个最基础的问题:

当用户输入一句话时,Codex 为什么能够连续读代码、运行命令、修改文件、执行测试,最后再给出结论?

本文分析基于本地 codex 仓库的 f3465047d1ee 版本。

先从一个真实任务开始

假设我们给 Codex 一个很常见的任务:

请修复 APP-1427:登录成功后偶发跳回登录页。Bug 单中附有复现步骤和客户端日志,请定位根因、完成修复并验证。

如果只是普通聊天模型,它最多根据我们贴进去的 Bug 描述和代码给出修改建议。但 Coding Agent 的关键,是能把这句话转换成一个持续执行的工作流。

  1. 模型识别出这是一个 Bug 修复任务,根据当前可见的 Skill 描述选择并加载对应 Skill;
  2. Skill 的操作规则进入上下文,告诉模型这类任务通常需要读取 Bug 单、获取日志、定位代码并完成验证;
  3. 模型先生成读取 Bug 单和获取日志的 Tool Call,ToolRouter 找到对应 Handler,由工具真正执行这些动作;
  4. 工具返回 Tool Result,Agent Loop 将结果写入 Conversation History,并带着更新后的上下文再次请求模型;
  5. 模型分析日志内容,形成新的排查线索,再调用搜索工具定位相关代码;工具只负责执行搜索,并把代码片段交还给模型;
  6. 模型判断证据足够后生成修改方案,调用 Patch 工具落盘,再调用命令工具运行测试;
  7. 测试结果再次回到 Agent Loop。失败时,模型根据新输出继续调整;通过后,模型才返回最终结论。

这条链路里,模型负责理解任务、分析结果和决定下一步;工具负责读取、搜索、修改和执行;Agent Loop 则把两者连接起来,让一次 Tool Result 成为下一次模型判断的输入。

在第一轮模型判断之前,对当前工作目录生效的 AGENTS.md 已经在会话或 Turn 的上下文构建阶段被发现并加入指令。只有工作目录或规则作用域发生变化时,系统才需要刷新适用的规则。

说白了,这不是一次问答,而是模型在项目规则约束下,借助 Skill 和工具不断观察、行动、再观察的闭环。

Codex 的整体结构

从职责上看,可以把 Codex 分成三层:

  • 最上面是 CLI、IDE、桌面应用等客户端;
  • 中间是负责推理、状态和工具编排的 Agent Harness;
  • 最下面是本地操作系统、工作区以及通过 MCP 接入的外部服务。

如果继续拆开中间的 Harness,最核心的是五部分:

  1. Agent Loop:驱动模型和工具持续循环;
  2. 上下文与状态:保存任务进度,并构造每次模型真正看到的输入;
  3. Tool 系统:把模型给出的工具意图路由到正确处理器;
  4. 本地执行 Runtime:真正运行命令、修改文件并管理进程;
  5. 权限与安全:在动作落地前约束文件、网络和提权边界。

AGENTS.md、Skills、Plugins 和 MCP 并不是另一套 Agent Loop。它们分别从"工作规则"和"外部能力"两个方向扩展这套 Harness。

下面逐个看。

1. Agent Loop:真正让 Agent 动起来的发动机

Codex 当前的主循环入口位于:

text 复制代码
codex-rs/core/src/session/turn.rs

其中 run_turn 的源码注释已经把基础逻辑说得很清楚:模型会返回 Assistant Message 或 Function Call;如果返回 Function Call,Codex 执行工具,并在下一次请求中把结果交还给模型;如果模型只返回最终消息,这个 Turn 就可以结束。

抽掉错误处理、事件上报和上下文压缩后,它的骨架大致可以理解成:

rust 复制代码
loop {
    // 1. 构造这一次模型看到的上下文
    let step_context = session.capture_step_context(turn_context).await;
    let history = session.clone_history().await;

    // 2. 请求模型,并持续消费流式响应
    let mut stream = request_model(
        step_context,
        history.for_prompt(...),
    ).await?;

    let mut in_flight = FuturesOrdered::new();
    let mut needs_follow_up = false;

    while let Some(event) = stream.next().await {
        if let ResponseEvent::OutputItemDone(item) = event? {
            match ToolRouter::build_tool_call(item.clone())? {
                // 3. 模型返回 Tool Call:交给 Runtime 执行
                Some(call) => {
                    in_flight.push_back(Box::pin(
                        tool_runtime
                            .clone()
                            .handle_tool_call(call, cancellation_token.child_token())
                    ));
                    needs_follow_up = true;
                }

                // 普通 Message 或 Reasoning:记录为会话 Item
                None => session
                    .record_conversation_items(turn_context, &[item])
                    .await,
            }
        }
    }

    // 4. 等待工具完成,并把 Tool Result 写回 History
    while let Some(tool_result) = in_flight.next().await {
        session
            .record_conversation_items(turn_context, &[tool_result?.into()])
            .await;
    }

    // 5. 有 Tool Result 时进入下一轮,让模型继续判断
    if !needs_follow_up {
        break;
    }
}

真实源码没有把这些动作全部写在 run_turn 同一层:run_sampling_request 构造 ToolRouterToolCallRuntimetry_run_sampling_request 消费模型流,handle_output_item_done 识别并调度 Tool Call,最后由 drain_in_flight 等待工具完成并把 Tool Result 写回 Conversation History。

除此之外,真实实现还要处理用户中途追加的消息、重试、取消、Hooks、Context Window、自动 Compaction,以及可能并行返回的多个 Tool Call。

但最稳定的核心没有变:

text 复制代码
模型判断 → 执行动作 → 观察结果 → 再次判断

所以,模型负责产生"下一步应该做什么",Agent Loop 负责让这一步真的发生,并把结果重新送回模型。两者组合起来,才会出现我们看到的持续工作能力。

2. 上下文与状态:模型每一步到底看见了什么

问题来了,第二次请求模型时,Codex 应该发送什么?

显然不能只发送最初那句"修复 APP-1427"。模型还需要知道:

  • 前面已经搜索过哪些文件;
  • 工具返回了什么结果;
  • 当前工作目录和可用环境;
  • 项目中的 AGENTS.md 规则;
  • 当前 Turn 启用了哪些 Skills、Plugins 和 Connectors;
  • 文件和权限状态是否发生变化;
  • 用户有没有在运行过程中追加新要求。

在当前实现里,这部分不是一个简单的 Vec<Message>

运行层至少可以看到三个不同生命周期的对象:

  • Session:承载整个线程级的服务、历史和活动任务;
  • TurnContext:固定一轮任务使用的模型、权限、模式和配置;
  • StepContext:固定某一次 Sampling 看到的环境、MCP Tool 快照和 AGENTS.md 状态。

这里 StepContext 很关键。一次 Turn 可能包含很多次模型请求,而工作区、可用环境、MCP 工具列表在期间可能变化。Codex 会为一次具体的 Sampling 捕获一致的请求视图,保证"展示给模型的工具"和"随后真正执行的工具"来自同一份上下文。

然后,Conversation History 会经过 for_prompt(...) 转换成模型输入。随着任务持续运行,Tool Output、Assistant Message 和用户追加信息不断进入历史。

上下文快到窗口上限时,run_turn 还会触发自动 Compaction,把冗长历史压缩成后续仍可继续工作的状态。

因此,上下文工程并不是"把整个仓库全部塞进 Prompt"。更准确地说,它是在每个决策点构造一个有边界、可复现、对当前动作足够的信息快照。

3. Tool 系统:模型意图如何找到真正的执行器

模型决定运行命令时,返回的不是一段直接交给操作系统的代码,而是结构化 Tool Call,例如:

json 复制代码
{
  "name": "exec_command",
  "arguments": {
    "cmd": "npm test",
    "workdir": "/workspace/project"
  }
}

Tool 系统需要完成两件不同的事情:

  1. 告诉模型当前有哪些工具、每个工具接受什么参数;
  2. 模型真的调用后,找到对应 Runtime 执行。

当前源码中的 ToolRouter 很直接地体现了这两个世界:

rust 复制代码
pub struct ToolRouter {
    registry: ToolRegistry,
    model_visible_specs: Vec<ToolSpec>,
}

model_visible_specs 是模型能看见的契约,registry 则保存真正可以执行这些契约的 Runtime。

在每次 Sampling 前,built_tools(...) 会根据当前 StepContext 重新构建工具视图。这里合并的不只是 Codex 内置工具,还可能包括:

  • MCP Tools;
  • Plugins 和 Connectors;
  • Dynamic Tools;
  • Extension Tool Executors;
  • Tool Search 动态加载的能力。

模型返回结果后,ToolRouter 把 Response Item 转成内部 ToolCall,再构造 ToolInvocation。其中不仅有参数,还带着 SessionStepContext、取消信号、Call ID 和 Diff Tracker。

这也是为什么 Tool 不能简单理解成一个函数。

模型看到的是能力契约;Router 负责识别意图;Registry 负责找到 Runtime;Handler 才负责进入具体执行流程。

4. 本地 Runtime:Tool Call 最终怎样落到操作系统

到了这一层,模型的意图才真正变成系统动作。

以最常见的命令执行为例,exec_command 需要处理:

  • 命令和 Shell 解析;
  • 工作目录和环境选择;
  • stdout、stderr 聚合;
  • PTY 与非 PTY 模式;
  • 短命令直接返回;
  • 长命令保留进程会话;
  • 超时、取消和进程树回收。

如果命令没有在当前等待窗口内结束,Codex 会返回一个可继续操作的 session_id。后续 write_stdin 并不会启动第二个 Shell,而是继续向原来的进程或 PTY 写入内容,并读取增量输出。

修改文件也是类似的。

apply_patch 不是让模型随便覆盖一个文件。它需要解析 Patch、核对上下文、验证目标路径,再把文件变化转换成可追踪的 Diff 和 Tool Result。失败时,错误会回到 Agent Loop,让模型重新读取文件或调整 Patch。

所以 Tool 和 Runtime 也不是一回事:

  • Tool 解决"模型想做什么";
  • Runtime 解决"这件事在本机怎么可靠完成"。

5. 权限与安全:能执行不代表应该执行

一个可以运行任意命令、读写本地文件的 Agent,如果只依靠 Prompt 约束,风险非常高。

Codex 把安全边界继续下沉到了执行层。当前 TurnContext 中就包含:

  • Approval Policy;
  • Permission Profile;
  • 文件系统 Sandbox Policy;
  • Network Sandbox Policy;
  • 当前工作目录与可用环境。

命令执行前,系统会结合 Tool 请求、配置和当前权限判断:

  • 是否可以直接执行;
  • 是否只能在 Sandbox 中执行;
  • 是否需要向用户申请额外权限;
  • 是否应该直接拒绝。

不同平台的底层实现并不相同。macOS 使用 Seatbelt;Linux 当前主要通过 Bubblewrap 构造文件系统沙箱,并配合 Seccomp 和 PR_SET_NO_NEW_PRIVS 限制进程与网络能力,Landlock 只保留为兼容旧版本的 Legacy 路径;Windows 则使用基于 Restricted Token 等机制的专用沙箱后端。但它们服务的是同一个目标:让模型无法仅凭自己的一句话越过系统授权。

安全规则不能只写在 System Prompt 里。Prompt 是给模型看的,Sandbox 才是模型绕不过去的执行边界。

一次完整任务是怎样跑完的

把上面的部分重新串起来,"修复 APP-1427 的登录跳转问题"会经过下面这条链路:

实际过程可以简化成这样:

  1. 客户端提交包含 Jira 链接和验收目标的 User Turn;
  2. Session 创建这一轮的 TurnContext,把适用的 AGENTS.md 规则和可用能力加入上下文;
  3. Codex 捕获 StepContext,向模型提供历史、环境、工具以及可用 Skill 的描述;
  4. 模型识别 Bug 修复意图,选择对应 Skill,并按 Skill 给出的流程发起 Tool Call;
  5. ToolRouter 将调用交给对应 Handler,由工具完成读取 Bug 单、获取日志或搜索代码等动作;
  6. Tool Result 被写回 Conversation History,Agent Loop 带着新结果再次请求模型;
  7. 模型分析返回的日志和代码,决定下一次 Tool Call;步骤 5~7 会持续循环;
  8. 证据足够后,模型调用 Patch 和命令工具完成修改与测试;
  9. 权限系统和 Sandbox 约束每一次真实操作;
  10. 测试结果同样返回 Agent Loop:失败则继续循环,通过后模型返回最终 Assistant Message;
  11. Turn 完成,客户端收到最终状态、Diff 和验证结果。

这里没有哪个单独模块能够称为"Codex",模型、Loop、Context、Tools、Runtime 和 Safety 缺一块,体验都会完全不同。

三个最容易混淆的概念

模型不等于 Agent

模型只负责根据当前输入生成下一步动作,Agent Loop 负责不断调用工具执行、观察和继续。模型本身无状态并不维护上下文,Agent 维护上下文并在每次请求时候把上下文发送给模型。

Tool 不等于执行器

Tool Spec 是模型可见的契约,ToolRouter 和 ToolRegistry 负责路由,真正操作系统的是对应 Handler 和 Runtime。

对话历史不等于上下文

模型输入除了对话,还包括项目规则、工作区状态、权限、工具定义、Skills、环境信息和经过压缩的历史。

写到最后

这一篇只是先建立对Agent的全局认识,相当于一张地图,沿着这张地图继续往下追,还有几个更值得弄清楚的问题:

  • 模型每次只完成一次推理,Agent 为什么能够持续工作,直到任务真正结束?
  • 一个持续几十分钟甚至更久的任务,Codex 怎样记住前面发生过什么,又如何决定每次该给模型看哪些信息?
  • 当模型决定搜索代码、运行命令或修改文件时,这些意图究竟怎样变成操作系统中的真实动作?
  • Agent 已经能够读写文件和执行命令,系统又如何判断哪些动作可以直接执行,哪些需要询问用户,哪些必须拒绝?
  • 同一套 Agent 能力,又是怎样延伸到 MCP、桌面应用和真实浏览器中的?

这些问题,才是理解 Codex 从"会调用工具的模型"走向完整桌面 Agent 的关键。

相关推荐
武子康1 小时前
全双工语音 Agent 如何评测:从首音延迟到事件级验收
人工智能·llm·agent
孙启超1 小时前
Token太贵自己写了一个mac版开源AI编程工具
人工智能·macos·开源·llm·agent·ai编程·ai应用开发
@Mr_LiuYang2 小时前
《深入理解 AI Agent:设计原理与工程实践 》实验2-2 2-7 大模型注意力权重分布可视化
人工智能·大模型·agent·注意力机制
weixin_471383032 小时前
07 LangGraph 集成 RAG
python·langchain·agent·langgraph
七夜zippoe2 小时前
OpenClaw 测试策略实战:AI Agent 自动化测试体系搭建与落地
人工智能·ai·自动化·agent·测试体系·openclaw
苏灿烤鱼3 小时前
GitHub #2 拆解|仅 +59,代码知识图谱为什么仍值得关注?
python·agent·mcp
山间小僧13 小时前
「AI学习笔记」Loop Engineering 和 Graph Engineering
langchain·agent·ai编程
大侠Luffy13 小时前
我开源了一个 Agent Skill:一键把播客生成小红书帖子
agent·ai编程·vibecoding
Jackson__13 小时前
从 LLM 到 Agent:一篇文章搞懂 AI 圈热词!
前端·agent·ai编程