专题篇章 · 拆开 OpenAI Codex

对模型说随便并行,底下用锁管住

模型看见的、实际执行的、历史记录的,是三套顺序。一面旗允许一次发多个调用,一把读写锁决定谁能叠着进。

本页解决的问题

先给结论

「对模型说随便并行,底下用锁管住」要解决的关键问题是什么?

模型看见的、实际执行的、历史记录的,是三套顺序。一面旗允许一次发多个调用,一把读写锁决定谁能叠着进。

判断标准

跟着交接处走,不要只看 Demo。 系统是否可靠,往往取决于模型、工具、状态、权限和人的交接处。把每次交接都当成可以观察、测试和恢复的地方。

下一步

为一个自动化步骤写清输入、负责人、审批和恢复动作。

常见误区

一次运行成功了,却说不清发生了什么,也无法安全重放。

课程目标读完能说清三件事。发给模型的并行旗,只表示一次响应里可以出现多个工具调用。每个工具默认走写锁,要并行必须自己改。结果写进会话时,仍按模型当初发出调用的顺序排列。
先玩一遍 · 几个工具同时起跑
同一把读写锁:谁进看菜单,谁在门外等后厨
场景
点窗口可改读牌或写牌。后到的读用来看 fair 锁:写者已经排队,后来的读者不能插队。
看菜单 · 读锁
能并行的可以多人同时在
后厨 · 写锁
独占,只准一人
门外排队
模型发出的顺序
实际执行的顺序
历史入账的顺序
等待开始。点播放,看锁怎么分流。
逻辑轨迹 · 动画每一步对应源码里的哪一段
  1. build_prompt 把并行旗写成 trueturn.rs L1321
  2. 编请求时和 Lite 标记做与client.rs L952
  3. 路由查注册表,没有就当 falserouter.rs L137
  4. Hidden 即使自称并行也当串行registry.rs L472
  5. 先 spawn,再等就绪,最后拿锁parallel.rs L144
  6. 能并行就 read,否则 writeparallel.rs L152
  7. 结果按 FuturesOrdered 插入顺序入账turn.rs L2135
点播放,看几个工具同时起跑之后,谁能叠着进,谁必须等。
谁能并行亮读牌的进看菜单,可以叠着跑。亮写牌的要独占后厨,门外有写者之后,后来的读也只能排在它后面。
三套顺序模型发出的顺序是 1、2、3、4。执行顺序可以重叠。历史入账仍按发出顺序,先发出的先入账,哪怕它更晚跑完。
你能改的边界把补丁改成读牌,四个会一起进。把读牌改成写牌,它们会互相挡住。后到的读用来看 fair 锁不让插队。
教学示意:窗口与耗时为课程化设定,用来展示读写锁分流和三条顺序可以不一致。逻辑轨迹右侧行号对应 openai/codex 仓库 commit 4f39251a01。
思路一 · 对模型说可以并行,底下再自己分流
它解决什么问题

你让模型同时读 src/main.rs、读 src/lib.rs,再打一处补丁。屏幕上两份读文件几乎同时出进度,打补丁那一下却停了一拍。

如果运行时把可以并行理解成这些调用叠着跑,两个 apply_patch 会一起改共享的 diff 记账,账会乱。如果这些调用首尾相接,两个读文件也要排队,多耗一轮墙钟。请求侧那面旗只回答模型愿不愿意一次发多个盒子,回答不了盒子落地时谁能重叠。

思路是什么

发给模型的旗写在 Prompt 上。字段默认是 false,采样主路径走 build_prompt,把旗写成 true。编成 Responses 请求体时,再和模型是不是 Responses Lite 做一次与。Lite 上这面旗关掉。压缩那两条组包路径也写死 true,为的是和主采样请求的形状对齐,websocket 复用会逐字段对比,其中就包括这面旗。

出处:codex-rs/core/src/session/turn.rs 第 1312 至 1328 行 · codex-rs/core/src/client.rs 第 946 至 953 行

执行侧另有一张表。ToolExecutor 默认 supports_parallel_tool_calls 返回 false。漏写覆盖就走写锁。exec_commandview_imagetool_search 覆盖成 trueapply_patch 不覆盖。路由先查注册表,查不到当 false。曝光是 Hidden 的,handler 自己返回 true 也没用。MCP 还要服务器开关或只读提示,缺省仍是串行。

出处:codex-rs/tools/src/tool_executor.rs 第 122 至 124 行 · codex-rs/core/src/tools/registry.rs 第 470 至 473 行

模型只看见 parallel_tool_callstrue 或 Lite 下的 false。它看不见谁能并行。工具清单也不会因为某个工具其实要拿写锁而少掉一项。

请求侧 build_prompt 写成 true Lite 再与成 false 模型可以一次发多个 不保证落地会重叠 分类表 默认 false,要并行自己改 Hidden、未知名字当串行 MCP 要 opt-in 或只读提示 模型看不见这张表 闸门 并行走读锁 串行走写锁 一把 RwLock 管全场 先 spawn,再拿锁
教学化结构图:一面旗、一张表、一把锁,各自管一段。
为什么长期成立

对模型的许可以和宿主调度拆开,换语言重写也用得上。请求侧只回答愿不愿意一次发多个调用。执行侧只回答这一次能不能和别人重叠。Lite 测试把旗关掉,说明作者接受某些模型路径上失去这层提示。闸门始终在。模型如果仍在一条响应里发出两个调用,两个任务照样 spawn,照样按本地表拿锁。

思路二 · 一把全场读写锁当闸门
它解决什么问题

分类对了,还要有人看门。两个读可以共存,一个写必须独占。若先握住锁再等 MCP 服务器连上,一次冷启动会让旁边已经就绪的 exec_command 也卡住。

还有公平性。若后来的读者能从写者头顶上翻进去,补丁可能一直拿不到锁。换成标准库那把 RwLock,优先级依赖操作系统,写者有机会被饿死。

思路是什么

一次采样共用一把 tokio::sync::RwLock<()>。锁保护的值是单元类型,里面没有业务数据,只当闸门。任务先 spawn,可选地等就绪,再拿锁。能并行就 read,否则 write。就绪等在锁外面。一个还没连上的 MCP 服务器,不会占着写锁让旁边的 shell 也卡住。

出处:codex-rs/core/src/tools/parallel.rs 第 144 至 156 行

这把锁的优先级是 fair,也叫 write-preferring。已经排队的写请求没拿到、没释放之前,后面的读锁不会发下去。一个 view_image 还在跑,apply_patch 已经在门外,再来的 exec_command 明明可以和 view_image 重叠,却必须排在补丁后面。fair 换来的是写者不会饿死,代价是后来的读者被写者隔开。

分类粒度是工具实例,看不到这一次的参数。exec_command 无论跑 ls 还是 rm,都走读锁。apply_patch 无论补丁多大,都走写锁。两个无关的串行工具也会互相挡住。检索业务代码,没有容量上限。十个 shell 可以一起进。容量交给进程、沙箱和操作系统。

看菜单 · 已拿读锁 view_image exec_command 已经进门的继续跑 两人可以同时看菜单 门外 · 写锁排队 apply_patch 等读者放锁 写者排进 FIFO 后到的读 exec_command 不能插队 排在写者后面
教学化示意:已经进门的读者继续跑,后来的读者看见写者排队,自己排到后面。
为什么长期成立

问的是这一刻有没有独占者。餐厅可以多人同时看菜单,只准一人进后厨。公平策略写在锁的实现里。业务代码只问并行还是独占。换一把会让新读者插队的锁,写者就有机会被饿死。把就绪等待放在锁外面,也是同一类判断:还没准备好的人,不要占着门口。

思路三 · 跑完的顺序不决定入账的顺序
它解决什么问题

两个 exec_command 叠着跑,后发出的可能先跑完。若按完成顺序写历史,模型下一轮看到的结果顺序会和它发出的调用对不上。流内每到一条 OutputItemDone 就建 future,那一套管的是何时开工。这里管的是开工之后谁能重叠,以及结果按什么顺序入账。

思路是什么

采样循环把 tool_future 按到达顺序推进 FuturesOrdereddrain 按插入顺序出队,再写入会话。读锁让两个 shell 叠着跑,入账仍按发出顺序。先发出的先入账,哪怕它其实更晚跑完。

出处:codex-rs/core/src/session/turn.rs 第 2130 至 2140 行

模型以为的顺序、锁上实际发生的顺序、历史里写下的顺序,可以不一致。
为什么长期成立

观测顺序和执行重叠从结构上分开。并行只改墙钟,不改账本。自己做 Agent 时,至少把这两条队列分开写。对模型说可以并行,跑工具时再看本地表,结果仍按调用列表的原顺序收下。

横向对比 · 同一道题的另一种答法

DeepSeek Harness:按参数分类,独占当屏障

DSH 让每个工具提供 isConcurrencySafe(args)。只有精确的 true 才加入并行。缺声明、参数不合法、分类器抛错,都是独占。bash 没有分类器,整条独占。调度器等完整消息到齐,连续的并行调用编成一组,每个独占调用单独成组当屏障。组内滚动池,上限默认 10。

Codex 可以没有这套分组,因为它把判断压成工具级布尔,再用一把锁模拟屏障。DSH 能让只读的 bash 仍然串行,少掉一部分并发。Codex 能让两个 ls 叠着跑,两个 rm 也可以叠着跑。

两侧均已核对源码 · 2026-08-22

Claude Code:按参数分批,只读 bash 才并行

Claude 的 isConcurrencySafe 默认返回 falseBashTool 把并行交给 isReadOnly:命令通过只读约束才返回 truels 可以进并行批,带写副作用的命令进串行批。连续的安全调用收成一批走并发,不安全的每个自成一批,一批里也是一个接一个。上限来自环境变量,解析失败时是 10。

Codex 的 exec_command 省掉命令解析,写命令也进读锁。三边都把调度元数据藏在宿主,闭合的位置不同。Codex 闭合在默认值和 Hidden,对 shell 最放开。DSH 闭合在分类器,bash 全串行。Claude 夹在中间。

两侧均已核对源码 · 2026-08-22
课堂练习
01

后到的读能不能插队

view_image 还在跑,apply_patch 已经在门外排队。这时模型又发出一个 exec_command。演示里切到后到的读,单步走完,对照下面三问。

这个 exec_command 能不能和还在跑的 view_image 叠着跑。
三条结果在历史上按什么顺序入账。
如果把 apply_patch 的窗口改成读牌,闸门还会不会把它单独拦住。
Takeaway:对模型说可以并行,是请求侧的旗。谁能叠着跑,看工具有没有改默认值,Hidden 和未知名字走独占。跑完之后,历史按发出顺序入账。三套顺序可以不一致。

「先玩一遍 · 几个工具同时起跑」的能力藏在每次交接里

「你让模型同时读 src/main.rs 、读 src/lib.rs ,再打一处补丁。屏幕上两份读文件几乎同时出进度,打补丁那一下却停了一拍」说明,Agent 的表现不只由模型决定。模型、上下文、工具、状态、权限和人之间的每次交接,都会改变任务能否继续以及出了问题能否恢复。

先写清状态,再增加能力

从「如果运行时把可以并行理解成这些调用叠着跑,两个 apply_patch 会一起改共享的 diff 记账,账会乱。如果这些调用首尾相接,两个读文件也要排队,多耗一轮墙钟。请求侧那面旗只回答模型愿不愿意一次发多个盒子,回答不了盒子落地时谁能重叠」出发,可以把流程拆成起始状态、下一步动作、工具返回、状态更新和停止条件。这样调试时找的是第一处丢失信息或权限的位置,而不是笼统地说“模型变笨了”。

  • build_prompt 把并行旗写成 true turn.rs L1321
  • 编请求时和 Lite 标记做与 client.rs L952
  • 路由查注册表,没有就当 false router.rs L137

成功路径不能代表系统可靠

用「view_image 还在跑, apply_patch 已经在门外排队。这时模型又发出一个 exec_command 。演示里切到后到的读,单步走完,对照下面三问」重放一次成功和一次失败,记录每一轮真正传入的上下文、工具结果和负责人;只要第二个人能复述这条链,系统才有可维护性。

从「先玩一遍 · 几个工具同时起跑」走到「思路一 · 对模型说可以并行,底下再自己分流」

「先玩一遍 · 几个工具同时起跑」先把问题落在「同一把读写锁:谁进看菜单,谁在门外等后厨 播放 单步 重置 场景 默认四件套 后到的读 两个独占 点窗口可改读牌或写牌。后到的读用来看 fair 锁:写者已经排队,后来的读者不能插队。 看菜单 · 读锁 能并行的可以多人同时在 后厨 · 写锁 独占,只准一人 门外排队 模型发出的顺序 实际执行的顺序 历史入账的顺序 等待开始。点播放,看锁怎么分流。 逻辑轨迹 · 动画每一步对应源码里的哪一段 build_prompt…」上;到了「思路一 · 对模型说可以并行,底下再自己分流」,讨论继续推进到「你让模型同时读 src/main.rs 、读 src/lib.rs ,再打一处补丁。屏幕上两份读文件几乎同时出进度,打补丁那一下却停了一拍」。两段连起来,重点就不只是记住一个结论,而是看清它成立所依赖的条件。

把这条判断带到下一个场景

分析 Agent 时,沿着状态、动作、工具结果和下一步的顺序走一遍;每次交接都要能说明信息从哪里来、由谁确认、失败时停在哪里。

  • 「先玩一遍 · 几个工具同时起跑」:同一把读写锁:谁进看菜单,谁在门外等后厨 播放 单步 重置 场景 默认四件套 后到的读 两个独占 点窗口可改读牌或写牌。后到的读用来看 fair 锁:写者已经排队,后来的读者不能插队。 看菜单 · 读锁 能并行的可以多人同时在 后厨 · 写锁 独占,只准一人 门外排队 模型发出的顺序 实际执行的顺序 历史入账的顺序 等待开始。点播放,看锁怎么分流。 逻辑轨迹 · 动画每一步对应源码里的哪一段 build_prompt…
  • 「思路一 · 对模型说可以并行,底下再自己分流」:你让模型同时读 src/main.rs 、读 src/lib.rs ,再打一处补丁。屏幕上两份读文件几乎同时出进度,打补丁那一下却停了一拍
  • 「最后的要点」:先 spawn,再等就绪,最后拿锁 parallel.rs L144

最后的「最后的要点」把讨论落到「先 spawn,再等就绪,最后拿锁 parallel.rs L144」。回看这条线索时,最值得保留的是:当输入、规模或风险改变,哪些判断需要重新做一遍。

标记为已学完 阅读进度会自动记录
← 上一篇下一篇 →

继续阅读

同一条线上的下一篇。

文章讨论

读到这里,留下一个判断。

把刚想明白的地方、还没想通的问题,留给下一位一起学习的人。

正在讨论 对模型说随便并行,底下用锁管住 拆开 OpenAI Codex
3条讨论文章讨论 · 与共学社区同步
在共学社区查看
AM
Asha Morgan内容编辑
观点实践记录

我把这篇文章里的一个判断改写成了今天可以验证的小实验。比记住结论更有用的是,知道下一步要观察什么。

文章讨论7 有帮助
LH
Lin Harper独立开发者
观点观点

读完以后我先回头找它成立的条件,而不是直接把方法搬进项目。这个顺序让后面的取舍清楚很多。

文章讨论5 有帮助
KM
Kiki Moore产品运营
问题问题

如果把这个判断放到真实工作里,最先需要补的约束是什么?我想知道从阅读到第一次实践之间,哪一步最值得先做。

文章讨论4 有帮助