雨天小六

读懂 Codex(1.7):Coding Agent 相比普通聊天 Agent 增加了什么

· 更新于 2026-08-01 · 专栏:读懂 Codex

#Codex#Coding Agent#Agent Runtime#软件架构#开发工具

“能够调用函数的聊天模型”与“能在真实仓库里安全工作的 Coding Agent”之间,差的不是一个 shell 按钮。开放任意命令执行只解决了“可以产生副作用”,没有解决“在哪个工作区”、 “按什么项目规则”、“有什么权限”、“怎样观察真实结果”、“中断后如何恢复”和“如何证明改了 什么”。

本节的“普通聊天 Agent”是一个概念对比基线:它能维护对话,也可以有通用 Tool Call/Result 闭环,但不假设它拥有项目工作区和编码生命周期。Codex 源码里没有一个名为 ChatAgent 的 对照类;下面的“增加”是系统能力差异,不是虚构两个 Rust 类做代码 diff。

先给出结论:Coding Agent 的关键增量是一个拥有项目状态、可控副作用、真实环境反馈、 持久化历史与终态的 Runtime。模型仍负责提出下一个行动和解释观察,Runtime 则决定这个行动 能否、在哪里、用什么权限执行,并保存执行后真正发生的事。

先建立三层能力栈

不应把“聊天模型”和“Coding Agent”看成两个完全无关的系统。后者是在前者的生成能力上分层 增加 Runtime 合同:

对话生成层

输入 system/developer/user 层次的指令与历史,模型产生 assistant Message、Reasoning 或结构化 Tool Call 意图。这一层不直接持有本地文件句柄、子进程 PID 或沙箱权限。

通用 Agent 层

增加 typed Tool Call/Result、call_id、ToolRouter/Registry、并发与取消,以及可重复多次采样的 Agent Loop。模型可以从环境取得新观察,不再只用已有 Prompt 生成一段话。

Coding Runtime 层

增加项目与环境快照、命令/进程/补丁等副作用工具、PermissionProfile、审批与沙箱、 stdout/stderr/exit code、Turn Diff、World State、Rollout、压缩、中断与恢复。这些能力才把 “调用一个函数”改造成“在持续变化的真实仓库中完成可审计任务”。

从对话生成基线、通用 Agent Runtime 到 Codex Coding Runtime 的能力分层图,展示项目上下文、编码工具、权限沙箱、代码观测和可恢复状态
图 1.7-1:Coding Agent 不是用 Runtime 替代模型,而是把模型的行动意图放进可验证的执行、权限、观测与恢复边界。对话基线是概念模型,Codex 层才是本文追踪的源码实现。

增量一:“我在哪里工作”成为显式状态

聊天对话可以让用户粘贴一段代码,但粘贴的文本没有完整项目环境:它不告诉模型当前 cwd、 工作区根、Git 状态、项目规则、shell 类型、选定远程环境、MCP 连接和此时真正可用的工具。

Codex 将这些事实放进两个不同时间尺度的对象。

TurnContext:整个用户回合的合同

TurnContext 保存整个 Turn 不应在每次采样中随意漂移的语义:

  • Turn ID、trace、Session source、parent thread 和 originator;
  • model/provider/reasoning/personality/mode;
  • config、developer instructions、history mode 和 extension data;
  • approval policy、PermissionProfile、network proxy 与 platform sandbox 配置;
  • dynamic tools、Skills context、final output schema;
  • timing、metadata 和 terminal error。

这些字段让工具调用不必从 Prompt 文本里猜“当前是什么权限模式”或“应该使用哪个模型”。

StepContext:一次采样的可变环境快照

StepContext 每次后续采样前可重新捕获,当前明确绑定:

  • TurnEnvironmentSnapshot 和 ready environment capability roots;
  • executor 产生的 capability discovery snapshot;
  • 该步精确 MCP binding 与固定 MCP tool list;
  • 既用于广告又用于执行的定稿 ToolRouter;
  • 该环境观察到的 canonical AGENTS.md。

这一层解决了 Coding Agent 特有的“世界会在行动后改变”。模型通过 Apply Patch 改了文件,或用户 在运行中追加了一个 MCP 引用,下一次采样不应无脑复用旧快照。但在同一次 sampling 内, 模型看到的 tools 和真正执行 Call 的 Router 又必须是同一份定稿视图。

async def capture_coding_step(turn, pending_input):
    required_mcp = await discover_mcp_mentions(pending_input)
    environments = await select_ready_environments(turn)
    agents_rules = await agents_md_manager.refresh_if_selection_changed(
        environments
    )
    mcp_binding = await bind_mcp(required_mcp, environments)
    router = await build_tool_router(
        turn_context=turn,
        environments=environments,
        mcp=mcp_binding,
        dynamic_tools=turn.dynamic_tools,
    )
    return StepContext(
        turn=turn,
        environments=environments,
        loaded_agents_md=agents_rules,
        mcp=mcp_binding,
        tool_router=router,
    )

AGENTS.md 也因此不是一段在 Session 开始时永久粘住的 prompt 文本。AgentsMdManager 按环境选择 缓存;选择变化时重新加载项目指令。具体搜索路径、层级作用域与大小限制会在 4.3—4.4 单独拆开。

增量二:工具不只是通用 API,而是编码副作用面

通用 Agent 可以调天气、查订单、发邮件。Coding Agent 的不同之处不是这些 API 比较“技术”,而是 它的工具直接作用于一个长寿命、可变、可恢复的项目环境。

Codex 的 ToolRouter 将多种能力收敛进同一分派边界:

  • Shell/Unified Exec:启动命令、收集 stdout/stderr/exit code,并管理持续进程;
  • Apply Patch:用受限语法表达 add/update/delete/move 变更,产生精确 delta;
  • View Image/媒体:将工作区中不只是文本的产物变成模型可观察内容;
  • MCP、Dynamic 和 Extension tools:把项目外服务与扩展纳入同一 call_id/历史合同;
  • Request User Input:在不能安全假设时暂停并获得新信息;
  • Multi-Agent tools:把独立子任务、通信和终态也放入受控工具路径。

本节只建立能力地图,不假装已经解释每种工具的实现。后续 6.x 会逐项拆 Tool Spec、Handler、 Runtime、子进程、截断、审批和沙箱;7.x 再处理 Multi-Agent 和扩展。

这些工具的共同基础不是“名称都放进一个 dict”,而是:

  • 模型可见 ToolSpec 与真正 Registry 之间有明确构建关系;
  • namespace/name 和 payload kind 决定分派;
  • call_id 关联行动与观察;
  • Handler 声明并发、取消和 diff consumer 能力;
  • Tool lifecycle 经过 Hook、telemetry 与类型化 Output;
  • 可恢复错误返模型,合同错误向 Runtime 传播。

增量三:权限从 Prompt 外部进入确定性控制面

聊天对话中,“请不要执行危险命令”可以是 developer instruction。它会影响模型选择,但不能作为 唯一安全边界。模型输出是非确定性的,Prompt 文本也不能创建或撤销操作系统权限。

Codex 在 TurnContext 里显式持有:

  • approval_policy:什么情况需要请求用户或审查者;
  • PermissionProfile:当前文件系统、网络与执行能力的结构化配置;
  • network proxy/managed network 状态;
  • Windows/Linux 等平台沙箱选择所需配置。

Tool Orchestrator 把这些对象与工具请求结合,决定初始 SandboxAttempt,必要时请求审批,并仅在 策略允许时对沙箱拒绝做受控升级。这个判定不读 assistant Message 里的自我声明。

async def authorize_and_execute(invocation):
    turn = invocation.step_context.turn
    policy = derive_execution_policy(
        permission_profile=turn.permission_profile,
        approval_policy=turn.approval_policy,
        network=turn.network,
        platform_sandbox=turn.windows_sandbox_level,
    )

    attempt = policy.initial_attempt(invocation)
    if attempt.needs_approval:
        await approval_service.require(attempt.request)

    result = await execute_in_selected_environment(invocation, attempt)
    if result.is_sandbox_denied and policy.permits_escalation(result):
        await approval_service.require(policy.retry_request(result))
        result = await execute_in_selected_environment(
            invocation,
            policy.retry_attempt(result),
        )
    return result

这一层是 Coding Agent 与“模型直接拿到 shell”的安全分界线。Tool Call 表达意图,PermissionProfile 限定能力,Approval 处理动态授权,Sandbox 在实际执行时实施边界。四者不能合并成一句 prompt。

增量四:真实观测不依赖 Agent 自己宣称

一个普通聊天回答可以说“代码已经修好”,但如果没有连接工作区,这只是一句生成文本。Coding Agent 需要两类 Runtime 观测:

工具结果观测

Shell/Exec 输出包含 stdout、stderr、exit code、duration、timeout 状态等。Apply Patch 结果包含哪些变更 已提交,拒绝/部分失败时仍尽量保留已知前缀。这些结果通过 call_id 与原 Call 配对,进入下一次 Prompt。

Turn 净差异观测

Codex 还维护 TurnDiffTracker。它不是在每次改动后粗暴地对整个工作区重跑 Git diff,而是基于 已提交 Apply Patch mutation 的精确 delta,在内存里维护每个 path 的 baseline/current content、revision 和 rename origin,最后生成整个 Turn 的净 unified diff。

class TurnDiffTracker:
    def track_delta(self, environment_id, patch_delta):
        if not self.valid:
            return
        if not patch_delta.is_exact:
            self.invalidate()
            return

        for change in patch_delta.changes:
            self.apply_change(environment_id, change)
        self.unified_diff = self.render_net_diff()

如果 delta 不精确,tracker 选择 invalidate,不伪造一份“看起来很精确”的 diff。这是 Coding Agent 可审计性里很重要的诚实边界。

Sampling request 在工具收束与 cancellation 检查后,若 tracker 有净 diff,向客户端发 TurnDiff Event。 所以客户端不需要从 assistant 最终文本里用正则抽取“修改了哪些文件”。

增量五:“运行验证”是能力闭环,不是 Runtime 硬编码脚本

可靠 Coding Agent 应该在可能时运行与变更匹配的测试、类型检查、lint 或 build。但这句设计目标 不等于 Codex 的 run_turn 里有一个写死的:

inspect()
edit()
always_run("pytest")
final()

这种硬编码无法覆盖 Rust、Go、JavaScript、移动端、单仓多包、远程构建和用户自定义工作流。

Codex 提供的是可组合验证能力:

  • shell/exec 可执行项目本身的测试命令;
  • AGENTS.md 和 developer instructions 可以指定项目应运行什么;
  • Tool Output 把 exit code 和完整诊断回灌给模型;
  • Agent Loop 让模型根据失败继续修正;
  • Stop Hook 可在模型拟完成时检查条件,并用明确 continuation prompt 要求继续;
  • 最终回答可说明已运行的验证和未能验证的限制。

因此,“Codex 支持验证闭环”是源码可支撑的说法;“Codex 永远强制每次修改后跑全量测试” 不是。具体步骤由模型在项目规则、工具观察、权限与 Hook 约束下选择。

用户任务经 Agent Loop、StepContext、ToolRouter、策略审批沙箱进入工作区,再以工具结果、Turn Diff 和 Rollout 回到模型的 Coding Agent 时序闭环图
图 1.7-2:Coding Agent 的真实闭环是“快照—意图—授权—执行—观察—新快照”。Runtime 没有硬编码唯一的先读后改再测步骤,但为模型和 Hook 提供了可验证它的能力与边界。

增量六:历史不再只是用户和 assistant 的文本

普通聊天历史通常只需回放消息。Coding Agent 要恢复一份工作,还需要回答:

  • 当时使用的 Base/Developer/Project rules 是什么?
  • 模型发出过哪些 Tool Call,每个 Output 是什么?
  • 哪一次 Turn 正常完成,哪一次中断?
  • World State 怎样从全量快照经 patch 变到后续状态?
  • 历史是否做过 compaction,哪个 window 是当前窗口?
  • 用户是否 rollback,之后哪些项不应再生效?

Codex 的 Context History 保存 ResponseItems,并在生成 Prompt 时做 Call/Output 配对修复、孤儿 Output 删除、 媒体能力适配与超长工具输出截断。Rollout 还记录 SessionMeta、TurnContext、World State、Compaction、 Rollback、TurnStarted/Complete/Aborted 和其他事件。

@dataclass
class RecoverableCodingHistory:
    session_meta: SessionMeta
    response_items: list[ResponseItem]
    turn_context_records: list[TurnContextRecord]
    world_state_events: list[WorldStateFull | WorldStatePatch]
    compaction_events: list[CompactionRecord]
    rollback_events: list[RollbackRecord]
    terminal_events: list[TurnComplete | TurnAborted]

rollout_reconstruction 并不把文件每一行都盲目追加到当前历史,而是理解 compaction、rollback、 World State 与终态的语义来重建。后续 8.x 会把持久化和恢复算法拆到足够细;本节只需建立一个认识: Coding Agent 的历史是行动与环境状态的事件日志,不是纯聊天文本。

增量七:中断、进程与终态是一等责任

普通文本生成被中断,最直接后果是回答没输完。Coding Agent 被中断时,可能还有:

  • 正在运行的 shell 子进程与孙进程;
  • 正在等待的审批或 request-user-input;
  • 正在写文件的 Apply Patch;
  • 正在运行的 MCP 请求或子 Agent;
  • 已经记录的 Call,但尚未记录 Output;
  • 需要保存到 Rollout 的部分改动与中断标记。

因此 Codex 用 Turn CancellationToken 穿过 sampling、tools 和 compaction,ToolCallRuntime 区分优雅清理与 task abort,并尽量生成 aborted Tool Output。Task wrapper 在宽限期后强制收尾,Interrupted 还要先写模型可见 标记,flush 后再发 TurnAborted。

正常 TurnComplete 也需要两道持久化屏障:Task body 返回后先 flush 普通项,终态事件追加后 再 flush。这些细节看起来与“写代码”无关,却决定客户端在收到完成/中断后重读状态时, 看到的是否是同一个世界。

增量八:开放式能力必须有扩展与约束接口

编码任务不可能由内置工具覆盖所有场景。Codex 因此不只有一个固定 tools 数组,还提供:

  • MCP bindings 与发现工具;
  • dynamic tools 和 extension tool executors;
  • Skills 与 Plugins 的提示/能力注入;
  • PreToolUse/PostToolUse/Stop/Compact/Session 等 Hooks;
  • Multi-Agent 子任务和通信工具;
  • Code Mode 与不同产品表面的适配。

扩展性又必须受现有协议约束,否则每个插件都会自己定义身份、权限、取消、错误和持久化。 所以扩展 Tool 仍经 ToolRouter/Registry、ToolInvocation、call_id、ToolOutput 与 Agent Loop;Prompt fragment 仍经 Developer/Contextual User 槽位和顺序管理;Hook 仍返结构化 Outcome。

def install_extension(extension, runtime):
    for prompt_fragment in extension.prompt_fragments():
        runtime.prompt_compiler.add_to_declared_slot(prompt_fragment)

    for tool in extension.tools():
        runtime.tool_router.register(
            name=tool.namespaced_name,
            spec=tool.model_visible_spec,
            handler=tool.handler,
        )

    for hook in extension.hooks():
        runtime.hooks.register_typed(hook)

扩展性是 Coding Agent 适应不同仓库和组织的必要条件,协议化则是它不因扩展而失去安全与可恢复性的条件。

一张表看清差异不在“会不会写代码”

系统问题对话生成基线通用 Agent RuntimeCodex Coding Runtime
输出是什么Assistant MessageMessage + typed Call/Result同左,配合编码工具与环境事件
世界从哪里来Prompt 文本Tool OutputStepContext + Environment + AGENTS.md + World State + Tool Output
如何改变世界没有 Runtime 副作用通用 HandlerShell/Exec/Apply Patch/MCP/扩展/子 Agent
权限由谁决定无或产品外层Tool hostPermissionProfile + policy + approval + sandbox
失败怎样表达回答或客户端 ErrorTool Result保留 exit/output/diff 的观察 + Fatal/Turn 边界
任务怎样继续下一条对话Agent Loop多 sampling Turn + pending input + compaction + StopGate
怎样知道改了什么依赖文本陈述视工具而定Apply Patch delta + TurnDiffTracker + lifecycle
怎样恢复对话消息视实现而定Rollout + History + World State + compaction/rollback/terminal replay
怎样验证给出建议可调验证 API运行项目命令、消费真实结果、Hook 可阻止无证据完成

这张表也提醒一件事:Coding Agent 能否生成高质量代码仍然与模型能力密切相关,但一个强模型 若缺少上述 Runtime 边界,仍只能给出高质量建议,无法将“仓库已被正确修改”变成可审计事实。

一个 Mini Coding Agent 至少要保留什么

如果在第九章用 Python 复刻最小 Coding Agent,不需要一开始就实现 Codex 所有 Provider、MCP、多 Agent 和持久化数据库,但下列边界不应省略:

@dataclass
class MiniCodingTurn:
    turn_id: str
    workspace: WorkspaceSnapshot
    project_rules: list[ScopedRule]
    permission_profile: PermissionProfile
    history: list[ResponseItem]
    cancellation: CancellationToken
    diff_tracker: TurnDiffTracker


async def run_mini_coding_agent(user_task, runtime):
    turn = await runtime.start_turn(user_task)

    while True:
        step = await runtime.capture_step(
            workspace=turn.workspace,
            project_rules=turn.project_rules,
        )
        prompt = runtime.build_prompt(turn.history, step)
        response = await runtime.model.sample(
            prompt,
            tools=step.router.model_visible_specs,
        )

        calls = []
        for item in response.completed_items:
            await runtime.record(item)
            if call := step.router.decode_call(item):
                calls.append(call)

        if calls:
            results = await runtime.execute_all(
                calls,
                permission_profile=turn.permission_profile,
                cancellation=turn.cancellation,
                diff_tracker=turn.diff_tracker,
            )
            for result in results.in_call_order():
                await runtime.record(result)
            continue

        if await runtime.stop_gate_requires_continuation(turn):
            await runtime.record(runtime.stop_gate_prompt())
            continue

        await runtime.flush_before_terminal_event()
        return await runtime.complete_turn(
            last_message=response.last_message,
            diff=turn.diff_tracker.unified_diff,
        )

这份最小实现仍然需要指定:

  • workspace snapshot 如何捕获和更新;
  • project rules 如何按目录作用域加载;
  • ToolSpec 与 Registry 如何保持合同;
  • 哪些工具可并发,哪些必须串行;
  • 审批和沙箱怎样实际限制副作用;
  • Tool Result 如何保留 call_id、exit code 和截断信息;
  • 中断时如何终止子进程并修复历史形状;
  • 哪些事件要持久化,终态前后的 flush 屏障是什么。

少了任何一项,仍可能做出一个令人印象深刻的 Demo,但它还没有建立生产 Coding Agent 所需的可靠边界。

应该如何验证“这是一个 Coding Agent Runtime”

验收不能只给它一个知识题,看最后回答似不似程序员。应该给出可观测的仓库任务和失败注入:

async def test_project_rule_changes_tool_behavior():
    workspace.write("AGENTS.md", "do not modify generated files")
    workspace.write("generated.py", "old")
    await agent.run("change generated.py")
    assert workspace.read("generated.py") == "old"


async def test_patch_is_reported_by_runtime_diff():
    await agent.run("rename function")
    assert terminal.turn_diff == expected_unified_diff
    assert terminal.turn_diff != parse_diff_from_assistant_text()


async def test_permission_denial_is_not_bypassed_by_prompt_claim():
    model.force_call("shell", {"cmd": "forbidden"})
    model.force_message("I have permission")
    await agent.run("act")
    assert sandbox.execution_count == 0
    assert model.next_request.has_failed_tool_output


async def test_resume_preserves_call_output_and_abort_state():
    rollout = await run_until_interrupted_after_tool_call()
    restored = await runtime.resume(rollout)
    assert restored.history.has_paired_or_aborted_output()
    assert restored.previous_turn.status == "aborted"


async def test_tool_changes_refresh_next_step_snapshot():
    await agent.run("create config then inspect it")
    assert captured_steps[1].world_state != captured_steps[0].world_state

Codex 当前测试体系正是按这种类型拆开。TurnDiffTracker 有 add/update/delete/rename、多环境和 invalidation 测试;Apply Patch、Shell 和 Unified Exec 有独立 Handler/Runtime 测试;Context History 有 Call/Output 配对和截断测试;Rollout Reconstruction 用大量序列验证 Compaction、Rollback、World State 和 Turn 终态。这些证据比“跑了一个 Demo,回答看起来很专业”更能保护架构。

第一章结论:从预测下一 Token 到管理可恢复副作用

第一章从模型最基础的下一 Token 预测出发,到这里已经建立了一条完整链条:

  1. Token 与上下文窗口解释了模型如何在有限输入上产生下一段输出;
  2. System/Developer/User/Assistant 和 typed ResponseItem 将权威来源、消息与机器控制项分开;
  3. ToolSpec、FunctionCall、call_id、ToolRouter 和 Registry 将模型输出变成可校验行动意图;
  4. Tool Result 与第二次采样把行动后的真实观察放回模型,形成闭环;
  5. Agent Loop 用采样屏障、工具收束、pending input、compaction、Stop Hook 和 Turn 终态管理多次闭环;
  6. 错误边界决定什么可重试、什么应返模型、什么必须终止,防止虚假观察与重复副作用;
  7. Coding Runtime 再加上项目快照、文件/进程工具、权限沙箱、diff、验证能力、持久化与恢复。

因此,Agent 不是“一个更会思考的聊天模型”。它是模型之外的控制系统。Coding Agent 又不只是 “Agent + shell”,而是一个能够把项目状态编译成 Prompt,把模型意图转成受权限约束的副作用,再把真实结果 变成新观察并可持久恢复的 Runtime。

第一章到此完成的是“从语言模型到 Coding Agent 的基础因果链”,不是把 Codex 所有实现细节 一次性讲完。后续章节将沿这张地图向下钻取:进程入口与配置、Thread/Turn/Step 内核、Prompt 与 History、 模型运输、每一种工具和安全机制、多 Agent 与扩展,以及 Rollout 和恢复。每一项都还会拆成源码级子章节。

评论


← 返回文章列表