OpenAI Codex · 工具排程

對模型說隨便並行,底下用鎖管住

模型看見的、實際執行的、歷史記錄的,是三套順序。一面旗允許一次發多個呼叫,一把讀寫鎖決定誰能疊著進。

課程目標讀完能說清三件事。發給模型的並行旗,只表示一次響應裡可以出現多個工具呼叫。每個工具預設走寫鎖,要並行必須自己改。結果寫進會話時,仍按模型當初發出呼叫的順序排列。
先玩一遍 · 幾個工具同時起跑
同一把讀寫鎖:誰進看選單,誰在門外等後廚
場景
點視窗可改讀牌或寫牌。後到的讀用來看 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 和未知名字走獨佔。跑完之後,歷史按發出順序入帳。三套順序可以不一致。