视频来源:Bilibili 原视频
多开几个 agent 并行干活,应该更快——这几乎是所有人的默认假设。
我拿两组控制变量实验测了一下,结果是反的:小任务开子代理,耗时慢 2.7 倍,token 多烧 13.2 倍,而两组的测试通过数完全一样。把任务规模翻到三倍再测,差距收敛到 1.57 倍 / 4.2 倍,并行依然没赢。
这篇把数据、机制、以及「什么时候才真的该开」的判断标准写完整。所有数字来自我本机 codex-cli 0.145.0 上跑出来的一手实验,方法和复现提示词都在文末。
先说一个前提:Codex 默认不会自己开子代理
很多人以为活多了 Codex 会自己分派出去。今天的默认配置下不会。
开机指令里写着:
Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask
同一段还留着一句 "Any earlier instruction enabling proactive multi-agent delegation no longer applies"——官方放开过主动委派,后来收回了。默认组实测 0 次 spawn,和这句话对得上。
想确认自己环境的实际状态,跑这个:
codex debug prompt-input | less
# 搜 /multi_agent 定位到 multi_agent_mode 段
看完后面的成本数据,你大概能猜到官方为什么要收回。
实验一:三个独立小模块,20 个测试
控制变量:同一个基线仓库复制两份(3 个完全独立的 Python 模块 stub + 20 个预写测试,测试即验收标准,全程不改)。两组父线程都是同一档模型、同一推理档位,两组都临时摘除全局 AGENTS.md、清空 MCP。唯一变量:开不开子代理。
- 默认组:出厂配置,无任何子代理设置
- 指挥组:项目级
.codex/agents/配三个角色(implementer 快模型 medium、reviewer 只读 high、scout 只读 low),项目 AGENTS.md 写死编排规则——每模块派一个并行 implementer、一律fork_turns="none"、完工后 reviewer 审查
| 指标 | 默认组(单代理) | 指挥组(1 父 + 4 子) | 倍数 |
|---|---|---|---|
| 测试结果 | 20/20 通过 | 20/20 通过 + 评审"无高危" | 打平 |
| 耗时 | 144s | 387s | 慢 2.7× |
| 父线程模型请求数 | 7 | 46 | 6.6× |
| 父线程 input | 173,544(非缓存 25,832) | 1,200,799(非缓存 47,775) | 6.9× |
| 子线程 input(×4) | — | 1,084,760(非缓存 77,912) | — |
| 总 input | 173,544 | 2,285,559 | 13.2× |
| 总 output | 2,412 | 12,322 | 5.1× |
| 非缓存 input 合计 | 25,832 | 125,687 | 4.9× |
质量打平,时间和钱全输。这个结果我反复核了三遍。
需要强调的是:两组测试都是 20/20 全过。多代理组并没有产出更差的代码,它只是更慢、更贵。
为什么会反转:三个机制
一、fork_turns 默认是 all,且和模型覆盖锁死绑定
子代理启动时,默认把父线程整段对话原样复制一份带走。你聊了三小时的会话,开一个子代理就是再买一份三小时的上下文。
更关键的是第二层:全量 fork 的子代理强制继承父线程的模型和推理档位,配置里写了也不生效。想给子代理换便宜模型,必须先把 fork_turns 设成 none 或具体轮数,砍掉上下文继承,模型覆盖才生效。要么全带、要么砍掉,没有中间态。
这个参数不在配置文件里——config.toml 没有这个键,.codex/agents/*.toml 也没有这个字段。它是 spawn_agent 工具的调用参数,每次派生时由模型现场决定传什么(binary 里的校验串是 fork_turns must be 'none', 'all', or a positive integer string)。
所以唯一的控制方式是把规则写成话,放进 AGENTS.md 或当次提示词:
派子代理一律 fork_turns="none"
我的实验就是在项目 AGENTS.md 里写死这条,模型每次派生都会带上。
另外,V2 下旧参数 fork_context 已废弃,报错会提示改用 fork_turns。
二、每个子代理出生就带约 2 万 token 的启动开销
系统提示加工具说明,子代理一启动就先吃掉这些。做过一个极端测试:让子代理只回一个 "ok",两个字母。
结果:input 20,735,output 5。输入是输出的四千倍。
这笔钱它干不干活都要烧。实验一开了 4 个子代理,光启动就是八万起步。
三、父线程 wait 期间的轮询是重发灾难
父线程等子代理干完时不是干等——它要不断确认子代理干完没有、干到哪一步了。每轮询一次,就把全部上下文重发一遍,一次两三万 token。
实验一里父线程发了 46 次请求,自己就烧掉 120 万 input。纯等待,也在烧钱。
这个问题官方在 issue #13733 里自己称为"架构缺口"。
再加上一点:子代理各建各的缓存,缓存写入不共享。
三笔叠加,子任务本身不够大时,编排开销吃掉全部并行收益。
实验二:任务翻三倍,差距收敛但仍然输
看到实验一,很自然的反驳是:任务太小了,前后端这种真能并行的活,多代理总该赢。
所以加测了第二组。57 个测试:一个订单业务模块(≈后端)、一个报表渲染模块(≈前端)、外加一个小工具。并行组按最优实践编排——2 个 implementer 真并行,父代理不闲着、同时自己实现小工具,不派评审。
| 指标 | 串行组 | 并行组(父 + 2 子) | 倍数 |
|---|---|---|---|
| 测试结果 | 57/57 通过 | 57/57 通过 | 打平 |
| 耗时 | 135s | 212s | 慢 1.57× |
| 父线程模型请求数 | 9 | 24 | 2.7× |
| 总 input | 258,504 | 1,083,523(父 596,876 + 子 486,647) | 4.2× |
| 非缓存 input 合计 | 24,264 | 67,971 | 2.8× |
编排本身是完美执行的,这点要说清楚:两个子代理的时间线真实重叠了 61 秒;父代理真的同时干完了自己那个模块(git 记录显示父线程只 patch 了小工具那个文件);等待轮询从实验一的 46 次请求降到 24 次。
但墙钟还是输。原因是串行组的吞吐太猛——同一档模型串行 135 秒就吃掉了 57 个测试,而子代理跑单个模块要 87~119 秒,再加上派工收工的约 90 秒开销。
真正值钱的是趋势:任务规模扩大约三倍,时间惩罚从 2.7× 收敛到 1.57×,token 惩罚从 13.2× 收敛到 4.2×。
交叉点是存在的,但比直觉远得多。
三条判断标准
从上面两笔账推出来的:
时间:并行能省的有上限——串行总时长减去最长的那个子任务,最多就这么多,而且还要先扣掉派工、收工、等待的时间。
token:是固定多烧的——启动开销乘以子代理个数,加上等待期间反复的上下文重发,躲不掉。
于是:
- 子任务单体要够大,分钟级起步。太小的活,省下的时间抵不过启动和派工的开销。
- 父代理必须自己有活干。派完工自己也领一摊,否则它就在那干等、一直轮询——两组实验里请求数 46 → 24 的差别就是这么来的。
- 给子代理配快模型省的是 token,不是时间。想在墙钟上赢过串行,子代理也得上旗舰模型。这两件事经常被混为一谈。
第三条要补一句:如果子代理领的活本身比较重要,那还是该给它配好模型。按活的性质配,不要一刀切。
一个例外:任务再小也值得开的场景
讲了半天"别乱开",有一种场景例外——无偏见评审。
不开子代理时,评审是同一个 agent 自己做的。它既当运动员又当裁判员,自己写完自己审,结果可想而知。
而且你跟主线程聊了一路,它已经被对话带偏了——你想听什么,它清楚。
这时候用 fork_turns="none" 派一个 reviewer 出来:它没读过你们的对话,没有预设立场,只看代码和测试说话。跑测试、读 diff、按高中低报问题、不许改文件。它的结论你不认同,可以回推给它辩,辩到收敛。
同一个省钱用的参数,换个用法就是质量工具。
什么没测(边界)
实测只覆盖了"小"(20 测试)和"中"(57 测试)两档。
以下三条属于按趋势外推,我没有实测,不下结论:
- 子任务达到 10 分钟级时并行是否反超
- 子代理也配旗舰模型时的表现
- 改走 worktree + 多实例 exec(没有 wait 轮询开销)这条路
测过的给数据,没测过的说没测过。
另外两个容易踩的点
并发默认是 4 个槽,含主线程——也就是最多 3 个 worker 同时干活,多的排队。这个数能改:config.toml 的 [agents] 段 max_concurrent_threads_per_session。实测设成 10,开机指令会变成 11 个槽(因为这个配置数不含主线程)。但官方自己在提示文案里建议设 8 以下,因为消耗涨得很快。
子代理的消耗不出现在 codex exec --json 的事件流里——父线程只报自己的 usage。你盯着屏幕,永远不知道总共烧了多少(这也是 issue #28058 里"不可审计"痛点的来源)。
审计路径是 rollout 文件。对号方法:事件流开头 thread.started 那个 id,就是父线程 rollout 的文件名;同一时间段 ~/.codex/sessions 里的其他 rollout,就是子代理的。少数一个子代理的账,整个测评就作废了。
自己测:复现提示词
这套方法比结论本身更有用。把下面这段发给你自己的 Codex,任务它自己生成、两组它自己跑、消耗它自己查,最后吐一张对比表:
帮我测评一下 Codex 的子代理:同一个开发任务,多代理并行和单代理到底差多少——时间、token 消耗、产出质量,全部要有实验数据。
要求:
1. 任务你自己生成:建一个小仓库,3 个互相独立的模块 stub,每个模块配预写好的测试(测试就是验收标准,全程不许改测试)。想测更大的任务,把模块做大就行,方法不变。
2. 对照必须干净:仓库复制两份,一份单代理跑,一份多代理跑;父线程同一个模型、同一个推理档位,唯一变量就是开不开子代理。
3. 多代理组认真配,别拿默认配置糊弄:.codex/agents/ 里建角色 toml(至少 implementer,可加只读的 scout / reviewer),项目 AGENTS.md 写清编排规则——每个模块派一个并行 implementer,一律 fork_turns="none"。
4. 消耗必须查全:父线程的消耗在 codex exec --json 事件流的 turn.completed 里;子代理的消耗不在事件流里,要去 ~/.codex/sessions 当天的 rollout 文件里数 token_count 事件——条数就是请求数,最后一条的累计值就是总消耗。少算子代理的消耗,整个测评作废。
5. 两组测试都全过之后,给我一张对比表:耗时 / 测试通过数 / 模型请求数 / 总 input / 非缓存 input / 总 output,每项算出倍数,最后用一句话回答:这个任务规模下,该不该开子代理。
三点说明:
这套骨架是通用的。 把第一句的测评目标换成你自己的问题,后面五条一个字都不用动。目标说清楚、对照设干净、消耗查全——方法可以迁移到任何同类问题上。
你的数字不会和我的一样。 任务是模型自己生成的,每次都不同,所以只有方向可比、绝对值不可比。反过来说这才是好事:任务跟我不一样,结论还成立,这个结论才算数。
别少算子代理的账。 事件流里看不到它们,必须去翻 rollout。
附:三个角色的 toml(实验原件)
上面提示词第 3 条说的角色配置,这是我实验里真实在用的三份,一个字没改。放在项目的 .codex/agents/ 目录下,一个 toml 一个角色。
必填三项:name、description(什么时候用它)、developer_instructions(行为守则)。可选的才是重头:model 换模型、model_reasoning_effort 调档位、sandbox_mode 限权限——这三个字段在 0.145.0 上逐个实测有效。
scout — 只读侦察,快模型低档位,翻代码回来汇报:
# .codex/agents/scout.toml
name = "scout"
description = "Fast read-only explorer that maps relevant files and reports concise findings without editing anything."
developer_instructions = """
You are a read-only scout. Map the files relevant to the task, summarize what
each one does and how they connect, and report back concisely (under 200
words). Never modify any file.
"""
model = "gpt-5.6-luna"
model_reasoning_effort = "low"
sandbox_mode = "read-only"
implementer — 领一个模块干到它自己的测试通过,可写沙箱:
# .codex/agents/implementer.toml
name = "implementer"
description = "领一个模块,实现到它自己的测试通过为止"
developer_instructions = """只做父线程分配给你的那一个模块。
读模块文件和测试文件,实现 stub,跑该测试文件直到通过。
不许碰其他模块。汇报:改了什么 + 测试结果。"""
model = "gpt-5.6-luna"
model_reasoning_effort = "medium"
sandbox_mode = "workspace-write"
reviewer — 无偏见评审,只读、高档位,description 里就写明要用 fork_turns="none" 派:
# .codex/agents/reviewer.toml
name = "reviewer"
description = "Unbiased read-only reviewer: runs the full test suite and reviews the diff for correctness issues. Spawn with fork_turns=\"none\" so it starts without the parent's context."
developer_instructions = """
You are an adversarial code reviewer with no prior context on purpose. Run the
full test suite (python3 -m unittest discover -s tests), then read `git diff`
and hunt for correctness problems, edge cases, and sloppy implementations.
Never modify files. Report: suite result, plus findings ranked
HIGH/MEDIUM/LOW, or 'clean' if none.
"""
model = "gpt-5.6-luna"
model_reasoning_effort = "high"
sandbox_mode = "read-only"
拿去把名字和模型换成你自己的就能用。注意 implementer 和 scout 的 model 覆盖要生效,派它们的时候必须带 fork_turns="none"——就是前面说的那个绑定。
结论
子代理不是开了就快。
小任务下它明确更慢更贵;任务变大差距收敛,但在我测过的两档规模里并行始终没有反超。真正决定要不要开的是三件事:子任务够不够大、父代理自己有没有活干、你想省的是钱还是时间。
以及那个例外——需要一个没被你的对话带偏的评审时,任务再小也值得开。
配套视频(含现场演示):为什么 Codex 默认不让它自己开子代理
Codex 系列合集:space.bilibili.com/152686439/lists/8461939
同系列相关:
- 神级提示词:一份可复用的全局 AGENTS.md 工作协议——本文开头提到的「Subagent 使用授权」条款就出自那一期
- Goal 目标模式:把模糊需求写成可验证目标——Goal 管"做到什么算完",子代理管"由谁去做",长任务里这两件事是连着的
xhyapi