Codex 中智能体的执行机制分析和对比

读者对象:了解大模型的调用和工具执行、ReAct等知识,但想近一步学习了解多智能体等更复杂实现机制的技术人员。

备注说明 : 所分析的是Codex CLI 命令行执行方式下智能体代码的执行机制,Codex App应用的代码是没有开源的,但底层执行逻辑是一致的。代码位置 :github.com/openai/code... · Rust 工作区 codex-rs · commit 8c07694(2026-10-18 克隆) Codex 代码中智能体执行机制同时提供了两个版本,通过如下的参数进行控制,后续称为 V1、V2。 通过研究同一场景的实现机制的对比和版本的演进,能对自己的产品研发具有借鉴意义。

枚举 取值 含义
MultiAgentVersion V1 / V2 / Disabled 两个进程内实现(V1 第一代 / V2 增强版)或整个关闭

针对Codex中多智能体的使用/触发,可以看我写的前一篇文章 : 《从使用者角度看看Codex如何使用多智能体》


1. 基线:普通 ReAct 是怎么执行的

1.1 它要解决什么问题

给模型一个需要多步才能完成的任务------例如"把配置文件里的超时改成 30 秒"------模型不可能一步到位:它要先想清楚改哪个文件、再真的去改、看到结果后确认改成功了,才能回复用户。

ReAct 就是把这类过程标准化成一个闭环:想一步、做一步、看一步结果,再想下一步,直到模型认为可以给出最终答案。它是今天绝大多数智能体(Codex 也在其列)的底座。不理解这个闭环,就看不懂 V1/V2 到底"多"出了什么。

1.2 机制:一个闭环,四个动作

  1. 模型读上下文。 上下文是一个消息列表(messages),里面放着任务描述,以及此前每一轮的执行结果。
  2. 模型做一次推理。 输出两种可能之一:一个结构化的工具调用请求 (用哪个工具、参数是什么),或者一条最终答案。
  3. 运行时(Runtime)执行工具。 若是工具调用,由外层运行时真正执行(读写文件、执行命令、查询数据),把返回结果作为一条新消息追加进上下文。
  4. 回到第 1 步。 模型基于更新后的上下文继续推理;若第 2 步输出的是最终答案,循环结束。

由此得到三个关键性质:

  • 单线程:动作顺序执行,不存在并行的执行体;
  • 单上下文:全程共享同一份记忆,无隔离、无分叉;
  • 同步内联:工具结果直接回填同一上下文,调用方原地等待。

另有一个工程细节不容忽略:循环必须设最大步数保护。否则模型可能反复调用同一个工具,永远停不下来。

1.3 代码样例

下面是一份最小实现(教学示意,省略了错误处理与重试;call_model 为伪函数,真实项目里它是一次大模型 API 调用,返回结构化结果):

python 复制代码
from typing import Any

# ---------- 工具层:由外层运行时真正执行 ----------
def edit_file(path: str, old: str, new: str) -> str:
    """把文件中一处 old 替换为 new。"""
    return f"已替换 1 处:{path}"

TOOLS = {"edit_file": edit_file}

# ---------- ReAct 主循环 ----------
def react_loop(task: str, max_steps: int = 10) -> str:
    # 上下文:一个消息列表,所有回合共享同一份
    messages: list[dict[str, Any]] = [{"role": "user", "content": task}]

    for _ in range(max_steps):
        # 模型只负责"决定":读上下文,输出思考 + 工具调用请求或最终答案
        reply = call_model(messages, tools=TOOLS)

        if reply.tool_calls:
            # 把"思考 + 工具调用请求"写进上下文
            messages.append(reply.to_message())
            for call in reply.tool_calls:
                # 真正执行工具的是运行时,不是模型
                result = TOOLS[call.name](**call.args)
                # 观察结果作为新消息追加,驱动下一轮推理
                messages.append({
                    "role": "tool",
                    "tool_call_id": call.id,
                    "content": result,
                })
            continue  # 回到循环顶部

        # 没有工具调用:模型给出最终答案,循环结束
        return reply.content

    return "已达到最大步数,任务未完成"

请注意其中两行的分工:call_model 只产出"用哪个工具、参数是什么"的请求;TOOLScall.name 才真正执行工具。模型负责决定,运行时负责执行------这条分界线在 V1/V2 里依然成立,只是组织的形态变了。

1.4 一张图看懂普通的ReAct机制

普通 ReAct 的执行闭环:模型读上下文后二选一------输出工具调用请求(由运行时执行,观察结果写回上下文,进入下一轮),或直接给出最终答案(循环结束)。左侧是随回合增长的消息列表,底部是单线程、单上下文、同步内联三个关键性质。

1.5 再进一步:普通 subagent 长什么样

许多框架在 ReAct 之上加了"子智能体":父在某一轮决定"这块活交给一个专职的子去干",于是同步调用一个子------父原地等待,子自己跑完一整个 ReAct 循环,把最终答案作为这次调用的返回值交还父;父把答案内联回填进自己的上下文,继续下一轮。子用完即弃:没有名字、没有历史、不留记录。

python 复制代码
# 普通 subagent:同步内联,跑完即弃
def run_subagent(task: str) -> str:
    sub_messages = [{"role": "user", "content": task}]  # 子只拿到任务描述
    return react_loop_with(sub_messages)                # 子自己跑完整个循环

# 父智能体中:调用即阻塞,拿到最终答案才继续
answer = run_subagent("把 config.py 的超时改成 30 秒")
messages.append({"role": "tool", "content": answer})   # 结果内联回填父上下文

请记住这个"同步内联"的形态。Codex V1 的 spawn_agent 看起来在做同样的事,底层却是另一套路------下一章开始对比。


2. 三种执行模式对比

2.1 三者定位

  • 普通 ReAct:一个智能体、一条循环、单线程单上下文,全程同步。
  • Codex V1:父派生独立线程的子智能体,父子各自跑循环,子完成后异步通知父。
  • Codex V2:核心与 V1 相同,额外支持子智能体的驻留、换出与重载,并挂了一个树级共享信箱。

V1 与 V2 的主要差异在驻留层、完成信号和通信设施,不在进程边界。

2.2 共同底座

三者的内核都是 ReAct:都是"模型决定下一步、运行时执行、结果喂回模型"这个循环;真正的执行者都是外层运行时,模型只输出结构化的工具调用请求。区别只在于这个循环有几个、怎么组织。

2.3 对比总览

三种执行模式对比。该图表达普通 ReAct 的"单循环、同步内联",V1 的"异步派生 + 独立线程 + 单向状态通知",V2 的"异步派生 + 驻留换出 + 信箱寻址"三者在执行结构上的差异。

为便于窄屏阅读,拆成两段:表 A 看 V1 相对普通 ReAct 改了什么,表 B 看 V2 相对 V1 又补了什么。

表 A:普通 ReAct ↔ Codex V1(跨越点:并行 + 异步)

维度 普通 ReAct Codex V1
循环 单条 父子各一条独立
执行单元 单体、单上下文 父子各自线程 + 上下文
并发 仅同回合多工具 真并发、父不阻塞
控制流 同步内联 异步派生、只返回 id
上下文 单一共享 父子隔离、可 fork 父全量历史
结果回流 工具值直接回填 异步通知(仅状态、无内容)
完成后 出答案即结束 通知父;子仍在册、可再派活
通信 无 无信箱

表 B:Codex V1 ↔ Codex V2(跨越点:驻留 + 信箱)

维度 Codex V1 Codex V2
生命周期 常驻、无换出 可换出 / 重载(LRU)
资源 静态占用、有上限限制 空闲换出内存腾空槽位
上下文继承 fork 全量 / 不给 fork_turns 精细(默认只给快照)
完成消息 仅引用 + 状态 带实际内容
通信 仅父子完成通知 树级信箱、可寻址收发
唤醒 无信箱,靠父主动派活 发信即唤醒续跑

一个最易混淆的点 :普通 ReAct 也常支持"一个回合里并行调多个工具",就是一次大模型调用可以返回多个要执行的工具信息,应用程序运行时可以并行执行这些工具,但这和 Codex 的并行不是一回事------前者是同一条循环、同一个上下文里的并行步骤 ,结果仍全部回填同一处;后者是多个独立执行体、多个独立上下文的并行 ,各自有各自的循环和记忆,靠异步消息或通知汇合。区分点就一句:并行的是"步骤"还是"执行体"。

2.4 两次跃迁的直白解释

把三种模式排成一条线,从普通 ReAct 到 V2 是两次递进。

第一次跃迁(普通 ReAct → V1):从"一个循环"到"多个并行循环",从"同步"到"异步"。 普通 ReAct 是"一个人从头干到尾",所有事在一条上下文、一条线程里顺序完成。V1 允许父在干活的同时派出好几个子Agent并行干别的,父子各有各的上下文和线程。代价是控制流变了:父派完不傻等,而是先返回一个 id 继续干自己的活,子Agent干完了再回头异步通知父。V1 换来并行能力,付出的代价是结果不再内联回填、要异步收。

第二次跃迁(V1 → V2):从"静态常驻、单向协作"到"可换出重载、可寻址、可唤醒"。 V1 的子Agent跑完后线程并不销毁,但也没有"请出内存"一说------不显式关闭就一直占着线程,并发一多容易达到系统的线程控制上限。父若想再让它干活,得自己主动调 send_input 给它开一个新回合(被关闭过的先 resume_agent 拉起来);拿结果则靠 wait 或读子线程。总之协作全靠父单向发起:完成通知只带"状态"不带内容,兄弟之间无法直接说话。V2 补了三块:换出与重载 (空闲的子Agent释放,需要时唤醒续跑,资源不再静态占用)、信箱 (任意智能体可按标识互相发消息,支持"先排队、对方上线再投递"和"发信即唤醒对方开工"两种模式)、更干净的上下文继承(默认子只拿指令快照,不再把父整段历史复制过去)。


3. V1 相对"普通 subagent"的差异

第 1.5 节的同步 subagent 是最容易和 V1 混淆的对象。相同点是:都由父模型在一次决策里触发、子都跑自己的推理循环、都能再往下派生子。但底层是两回事。

维度 普通 subagent Codex V1
执行方式 同步内联:父阻塞等子返回 异步派生:spawn_agent 立即返回 id,父继续
身份 匿名、一次性,跑完即弃 有 thread_id、有历史文件、记进 Agent 图谱,可恢复
结果回流 子的最终回答就是工具返回值,直接回填 事后异步通知(仅状态),父再自行 wait / 读子线程取产物
上下文 一般只拿到父手写的任务描述 可 fork 父的全量历史作为 inherited context

用代码对照更直观:

ini 复制代码
# 普通 subagent:父阻塞,结果内联回填
answer = run_subagent(task)        # 等子跑完才继续
messages.append({"role": "tool", "content": answer})

# Codex V1:异步派生,父不阻塞
agent_id = spawn_agent(task)       # 立即返回子的 ID
# 父继续做别的事;子在独立线程里并行跑自己的 ReAct 循环
# 子跑完后,一条"仅状态"的通知被写进父的历史

一句话:普通 subagent 是"函数调用",V1 是"派生线程 + 异步消息"。 前者不并行(父在等),后者并行,代价是结果不再内联回填、要异步收。


4. V1 的异步完成通知:结果到底怎么回到父

这是 V1 最容易被误解的一处。父智能体并不是靠一次"同步返回值"拿到子智能体的结果的。

V1 异步完成通知流程。该图表达从父调用 spawn_agent、经独立监工等到子终态、再把"状态"写回父历史、最后由父自行取结果的八步过程;左侧三个标注点出"异步"具体落在哪里,右侧点出"通知不含实际输出"这一关键边界。

完整过程分八步:

  1. 父调用 spawn_agent。 运行时创建子线程后,spawn_agent 工具立刻返回子的 agent_id,父这一回合的工具调用就此结束,父不阻塞、继续做别的事。
  2. 运行时挂上监工。 派生时若不走 V2 驻留通道,运行时会起一个独立的监工后台任务,并把父子线程标识、父回合标识、子的引用名交给它。监工和父的回合解耦,只负责盯子的状态。
  3. 子并行运行。 子智能体在自己的独立线程里跑自己的 ReAct 循环,父子并行推进。
  4. 子到达终态。 监工持续观察子的状态,直到它进入终态(已完成、出错、关闭等都算;仍在运行或初始化中的不算)。
  5. 监工过滤误报。 如果终态是关闭、且整棵树正在关停,监工直接返回、不通知------这是"整树生命周期交接",不是子的执行结果,不能误报。
  6. 组装完成通知。 监工生成一条完成通知,内容只有两项:子的引用名和终态状态。
  7. 写入父历史。 监工把这条通知追加进父的对话历史------但只记录、不驱动新一轮推理。它渲染成一段带标记的结构化文本,角色相当于 user,让父在之后的某一轮里能读到。
  8. 父自取结果。 因为通知里没有子的实际输出,父要在后续某一轮读到这条通知后,再由模型决定下一步------通常是调等待工具阻塞等子并取其最终消息,或直接读子的线程历史。

"异步"具体落在三处:① spawn_agent 立即返回、父不阻塞;② 监工是独立后台任务、和父子并行;③ 通知只写进历史、不抢占父当前回合。三处都不占用父的执行流。

一个必须讲准的边界:由于通知是"只写历史、不驱动新回合",它并不会自动把父叫醒去开新一轮大模型调用;父何时处理这条通知,由上层(终端界面、应用服务后端、调用方)按各自策略决定。这正是 V2 要改进的地方------V2 的完成消息带实际内容、可寻址、还能触发新回合(见第 6 章)。


5. V1 的短板与 V2 的改进

把 V1 放到真实的大规模、长期协作场景里,会暴露三个软肋;V2 的增强基本是一一对症。

短板一:子智能体只能常驻,不能换出。 V1 每个子派生后就是一条常驻线程,没有"先请出内存、腾地方"的机制。并发一多容易超过线程配额上限,要么新派生的子被拒,要么内存和线程被占满。V2 引入驻留层 + LRU 换出 :空闲子智能体按策略换出(保留状态,不是杀死),需要时由发信或派活重载唤醒,续上之前的进度。于是一棵很大的智能体树也能在固定槽位里运转。

短板二:上下文继承太粗。 V1 的继承开关配置只有"全给或不给"上下文:选全给就把父的整段历史复制给子------既浪费上下文窗口,也把父的全部中间过程泄露给子。V2 把它做成可调的 fork_turns(不继承 / 全继承 / 继承最近 N 条),默认只给一份指令快照,上下文干净、省窗口空间占用、不泄露上下文;需要时才全继承或继承最近 N 条。

短板三:协作单向,且没有信箱。 V1 的完成通知只带"谁 + 状态",没有内容、不可寻址、父子不可双向通信;父事后想再叫子Agent干活,只能自己主动调 send_input(被关闭过的先 resume_agent),兄弟之间想互通消息则完全没有渠道------总之没有标准信箱。V2 在树级挂了共享信箱:任意智能体可按目标寻址发消息,支持"先排队、对方上线再投递"和"发信即唤醒对方开工"两种模式;完成消息也从"空通知"升级成带实际内容的消息。


6. 两代实现差异总表

维度 V1 V2(默认)
子智能体载体 同进程独立线程(独立会话 / 上下文 / 循环) 同进程独立线程(同上)
工具集 派生子 / 发送输入 / 等待 / 关闭 / 恢复 派生子 / 发消息 / 派后续任务 / 等待 / 列出 / 中断
寻址方式 线程标识ID 规范路径(如 /root/a/b)
输入形态 富输入(文本 / 图片 / 技能引用) 文本消息(支持加密载荷)
嵌套深度 默认 1 层 不限制(靠并发治理)
并发上限 线程配额(默认 6) 驻留槽位(默认 4,可派 3)+ 执行容量
内存策略 全部常驻 懒驻留 + LRU 驱逐 + 按需重载
上下文继承 全量 / 不给 不继承 / 全继承 / 最近 N 条(默认不继承)
完成信号 状态通知:仅子路径 + 状态,写进父历史 带实际内容的消息,可寻址、可触发新回合
关闭方式 显式关闭,可恢复 无显式关闭,靠空闲驱逐;中断工具中止当前回合
治理 各自的资源视图 共享树级运行时:注册表、预算、关停统一

演进的主线 :是"第一代进程内多线程 → 加上资源弹性与真正的多智能体通信"------把常驻变成可换出/重载,把单向一次性的状态通知变成可寻址、带内容、可唤醒的信箱,把粗粒度上下文继承变精细,把线程标识寻址变成规范路径寻址。这也是许多智能体框架从"能跑"走向"可治理"时都要补的一课。


7. 小结

一条主线收束全篇:

  • 普通 ReAct 是基线:单循环、单线程、单上下文、同步内联------模型决定,运行时执行。
  • Codex V1 完成第一次跃迁:把"派活"从同步函数调用变成异步派生------子有独立线程、独立上下文、持久身份和可恢复的历史;代价是结果异步回流、只带状态、不带内容。
  • Codex V2 完成第二次跃迁:在异步派生之上补资源弹性(驻留/换出/重载)与真正的多智能体通信(树级信箱、可寻址、可唤醒、可带内容),并把上下文继承从"全给或不给"细化到按需裁剪。

分水岭始终是那一句:并行的是"步骤"(普通 ReAct)还是"执行体"(V1 / V2)。

相关推荐
goehou1 小时前
Agent 把用户地址发给了模型,数据脱敏该在哪一层做
llm·agent·数据脱敏·隐私合规·工程实践
frcoder2 小时前
TackHarness打造属于自己的编程工作流
agent·ai编程·1024程序员节·研发·harness
墨心@3 小时前
PI分层设计的经验
agent·pi·coding agent·harness
羞儿3 小时前
【DSH】创造模式与内核边界:一切皆插件的递归与自证
agent·1024程序员节·harness
编程的一拳超人3 小时前
【元宝搜索开发者征文大赛】四语实战:Java/Python/Go/Node.js 构建 AI 搜索应用全攻略
java·python·golang·node.js·agent
lucas_AI4 小时前
把网线拔了,OpenAI 的 AI 智能体还是聊上了:CPU 缓存成了密道
agent
sbjdhjd5 小时前
从黑板事实到长链路剪枝:AI 渗透测试 Agent 的实验、失效与改造
人工智能·网络安全·数据挖掘·php·agent·剪枝·1024程序员节