對模型説隨便並行,底下用鎖管住
模型看見的、實際執行的、歷史記錄的,是三套順序。一面旗允許一次發多個調用,一把讀寫鎖決定誰能疊着進。
- build_prompt 把並行旗寫成 trueturn.rs L1321
- 編請求時和 Lite 標記做與client.rs L952
- 路由查註冊表,沒有就當 falserouter.rs L137
- Hidden 即使自稱並行也當串行registry.rs L472
- 先 spawn,再等就緒,最後拿鎖parallel.rs L144
- 能並行就 read,否則 writeparallel.rs L152
- 結果按 FuturesOrdered 插入順序入帳turn.rs L2135
你讓模型同時讀 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_command、view_image、tool_search 覆蓋成 true。apply_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_calls 為 true 或 Lite 下的 false。它看不見誰能並行。工具清單也不會因為某個工具其實要拿寫鎖而少掉一項。
對模型的許可以和宿主調度拆開,換語言重寫也用得上。請求側只回答願不願意一次發多個調用。執行側只回答這一次能不能和別人重疊。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 可以一起進。容量交給進程、沙箱和操作系統。
問的是這一刻有沒有獨佔者。餐廳可以多人同時看菜單,只准一人進後廚。公平策略寫在鎖的實現裏。業務程式碼只問並行還是獨佔。換一把會讓新讀者插隊的鎖,寫者就有機會被餓死。把就緒等待放在鎖外面,也是同一類判斷:還沒準備好的人,不要佔着門口。
兩個 exec_command 疊着跑,後發出的可能先跑完。若按完成順序寫歷史,模型下一輪看到的結果順序會和它發出的調用對不上。流內每到一條 OutputItemDone 就建 future,那一套管的是何時開工。這裏管的是開工之後誰能重疊,以及結果按什麼順序入帳。
採樣循環把 tool_future 按到達順序推進 FuturesOrdered。drain 按插入順序出隊,再寫入會話。讀鎖讓兩個 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 也可以疊着跑。
Claude Code:按參數分批,只讀 bash 才並行
Claude 的 isConcurrencySafe 預設返回 false。BashTool 把並行交給 isReadOnly:命令通過只讀約束才返回 true。ls 可以進並行批,帶寫副作用的命令進串行批。連續的安全調用收成一批走併發,不安全的每個自成一批,一批裏也是一個接一個。上限來自環境變數,解析失敗時是 10。
Codex 的 exec_command 省掉命令解析,寫命令也進讀鎖。三邊都把調度元數據藏在宿主,閉合的位置不同。Codex 閉合在預設值和 Hidden,對 shell 最放開。DSH 閉合在分類器,bash 全串行。Claude 夾在中間。
後到的讀能不能插隊
view_image 還在跑,apply_patch 已經在門外排隊。這時模型又發出一個 exec_command。演示裏切到後到的讀,單步走完,對照下面三問。
exec_command 能不能和還在跑的 view_image 疊着跑。apply_patch 的窗口改成讀牌,閘門還會不會把它單獨攔住。