Claude Code 上週發表 dynamic workflows (官方文件),大家第一反應大多是「終於能一次跑幾百個 sub-agent 了」。這當然很猛,但小編覺得更值得講的,是它背後那條技術脈絡: 從 2024 年初的 Code Act 一路長出來的。

把這條線拉開來看,dynamic workflows 不是憑空冒出來的新玩意,而是「用程式碼當 agent 的行動」這個老想法,走到成熟、產品化的一步。技術上小編覺得設計得非常漂亮,這篇就來把這條路從頭走一遍。

1. Code Act 的核心洞見: 用程式碼當行動空間

故事從 “Executable Code Actions Elicit Better LLM Agents” (2024/2) 這篇講起。傳統 agent 的做法是 JSON function calling: 模型吐一個 JSON 物件,描述要呼叫哪個工具、帶哪些參數,再交給後端的處理常式執行,把結果塞回去。

CodeAct 提出的轉向是: 不要用預先定義好的 JSON 格式當行動空間,而是直接讓 LLM 寫一段「可執行的 Python」當作它的行動,把規劃跟工具呼叫合併進同一份程式碼裡一起跑。

為什麼程式碼會比 JSON 好? 兩個關鍵理由:

🔹 模型本來就很會寫程式碼。 LLM 在訓練時看過幾百萬個開源專案的真實程式碼,但工具呼叫用的那些特殊 token,是靠合成資料硬訓出來的,模型在真實世界裡根本沒見過。

🔹 程式碼天生支援組合。 迴圈、條件判斷、變數傳遞、把好幾個工具的輸出串起來,這些在 JSON schema 裡很難表達,在 Python 裡卻幾乎不費力。

論文 Figure 1 那個例子舉得很漂亮。任務是在美國、日本、德國、印度裡,找出買「CodeAct」這支手機最划算的國家。每個國家都得查匯率、稅率、當地售價、運費,再算出最終美元價格。用到的工具有 lookup_ratelookup_phone_priceconvert_and_taxlookup_shipping_costestimate_final_price

兩種做法的差異:

JSON Function Calling
→ {"tool":"lookup_rate","country":"Germany"}
← exchange_rate=1.1, tax_rate=0.19
→ {"tool":"lookup_phone_price","country":"Germany"}
← 700
→ {"tool":"convert_and_tax","price":700,...}
← 833
→ {"tool":"lookup_shipping_cost","country":"Germany"}
← ...
→ {"tool":"estimate_final_price",...}
← ...
⬆ 以上才跑完「德國」一國,四國要重複四遍。
每個 → ← 都是一次完整的模型來回,10+ 次起跳
CodeAct
countries = ["USA","Japan","Germany","India"]
final_prices = {}
for country in countries:
    rate, tax = lookup_rate(country)
    price = lookup_phone_price("CodeAct", country)
    converted = convert_and_tax(price, rate, tax)
    shipping = lookup_shipping_cost(country)
    final_prices[country] = estimate_final_price(
        converted, shipping)
cheapest = min(final_prices, key=final_prices.get)
迴圈跑完四國,工具輸出直接傳給下一個工具。
1 段程式碼、1 個 action

左邊每呼叫一個工具,結果都得塞回模型,模型再決定下一步,一路來回。右邊一個 for 迴圈跑完四國,lookup_rate 的輸出直接傳給 convert_and_tax (資料流),最後用 Python 內建的 min() 挑出最便宜的。模型只需要產生這一段程式碼、看一次最終答案。

CodeAct 的一個具體實作是 Hugging Face 的 smolagents,裡面的 CodeAgent 就是讓模型寫 Python 來呼叫工具,而不是走 JSON function calling。

這裡有個容易混淆的點: CodeAct 跟 Code Interpreter 不一樣。Code Interpreter 只是把「跑程式碼」當成眾多工具裡的「一個工具」來用,程式碼本身碰不到 agent 的其他工具。

CodeAct 則是把整個決策過程,連同函式呼叫,都寫進同一份程式碼裡一起執行。少了中間來回,效率自然好。

2. 同一招用到 MCP 上: Cloudflare 自家的 Code Mode

時間快轉到 MCP 生態爆炸之後,出現一個很現實的問題: 工具太多了。每個操作都註冊成一個工具,幾千個工具定義直接塞爆上下文。

Cloudflare 的 Code Mode 參考了 CodeAct 的思路: 與其把每個操作描述成獨立的工具,不如把 MCP 工具轉成一份有型別的 TypeScript SDK,叫模型寫程式碼去呼叫。他們新的 Cloudflare MCP server 涵蓋整個 Cloudflare API (數千個端點),只定義兩個 agent 工具: search()execute()

這樣大約只吃 1000 個 token,同樣的東西若用傳統「每個端點一個工具」的做法,要 117 萬 token,直接超過大多數模型的 context window。search() 讓模型去查 OpenAPI 規格、把幾千個端點收斂到需要的那幾個,execute() 在沙箱裡跑實際呼叫。

這裡有個概念叫「composition tax」(組合稅): 每一次工具呼叫的結果,都得先塞回模型的神經網路,再被原封不動抄到下一個呼叫的輸入,白白浪費 token 跟延遲,還多出一次推理。動作越多,這個稅越重。寫成程式碼就能在執行環境裡直接串接,跳過這道稅。

3. 程式碼可呼叫 agent tools: PTC

接著是 Claude API 這邊的 Programmatic Tool Calling (PTC),它建在 code_execution 工具之上 (這就是 Claude 的 Code Interpreter)。作法很單純: 你照常定義自訂工具,只要在工具定義裡加一個 allowed_callers: ["code_execution_20260120"],Claude 就不會一個一個直接呼叫它們,而是寫一段 Python,把這些工具當成函式,在程式碼執行容器裡組合、執行。

一句話抓住定位: PTC 就是「一段在容器裡執行的程式碼,而且這段程式碼能呼叫你原本要給 agent 的那些工具」。對照前面: Code Interpreter 的程式碼只能在沙箱裡自己算,碰不到 agent 的工具; CodeAct 在研究層面做到了讓程式碼呼叫 agent 工具。PTC 的升級是把這件事變成正式上線的 API,附帶的好處是: 工具的執行留在你那邊,工具結果不進 Claude 的上下文

機制拆開來看是這樣:

  1. Claude 在 Anthropic 的容器裡寫一段程式碼,裡面可能有迴圈、條件判斷、好幾個工具呼叫。
  2. 程式碼跑到 result = await query_db(sql) 時,容器暫停,API 把這個呼叫當成一個 tool_use 事件回傳給「你」。
  3. 工具還是跑在你那邊: 你的伺服器執行它、把結果回傳,Anthropic 的容器只負責跑那段編排程式碼。
  4. 結果回到「正在執行的那段程式碼」繼續往下,這些中間結果完全不進 Claude 的 context window (也不計入 token)。
  5. 整段程式碼跑完,Claude 只收到最後印出來的 stdout。

編按: 這正好補上 composition tax 的另一半。Code Mode 解決了「工具定義」塞爆上下文,PTC 解決的是「工具結果」塞爆上下文。例如一次搜尋回傳 50 筆原始結果,程式碼可以在容器裡直接解析、過濾、交叉比對,只留下相關的幾筆,而不是把 50 筆全倒進 Claude 腦袋裡讓它自己讀。

所以 PTC 做到的是: Claude 用程式碼組合多個工具呼叫 (迴圈、條件、串接輸出),但工具還是跑在你那邊,你可以檢查、拒絕、記 log、排進人工審核佇列。

但官方也講得很白,PTC 不是萬靈丹。嚴格循序、每一步都要 Claude 看完上一步才能決定下一步的工作,PTC 幫不上忙 (那種情況本來就省不掉來回),一次只呼叫一兩個工具的場景甚至會小貴一點。它真正發威的是「大量平行分流」或「結果很大、可以先在程式碼裡過濾」的任務。

(另外有個常見誤會: MCP connector 提供的工具,目前不能被 PTC 程式化呼叫。)

4. 開源實作: LangChain Deep Agents 的 Interpreter 與 Interpreter Skills

LangChain 的 Deep Agents 做的 interpreter,跟剛剛的 PTC 骨子裡是同一套做法,只是搬到了不同的層。PTC 是 Anthropic 在 API 供應商這一層做的 (Anthropic 管容器,工具在你那邊執行); Deep Agents Interpreter 則是 LangChain 在 harness 層做的中介層 (middleware),跟模型無關,連開源模型都能用。LangChain 自己也說 PTC、Code Mode、interpreter、RLM 殊途同歸。

具體來說,它給 agent 一個內嵌的小型執行環境,agent 可以在裡面寫程式碼、存變數、定義輔助函式、跨呼叫保留狀態,像有了一個 Python 或 Node REPL。實作上它是掛一個 eval 工具,但別被「工具」兩個字誤導: 它背後是一個有狀態、會跨呼叫存活的執行環境,不是那種呼叫一次就結束的普通工具。

小編初看時覺得這跟 sandbox 功能有什麼差別? 其實差在兩個地方:

🔸 預設能力的方向相反。 沙箱是「先給一台電腦,再往下收」: agent 一開始就拿到完整的作業系統、檔案系統、網路、shell,你再去限制。interpreter 剛好倒過來,「先什麼都沒有,要什麼再明確給」: 預設只有一個語言執行環境,沒有檔案系統、沒有網路、沒有 shell,連讀檔、抓網頁、開子代理人(spawn sub-agent) 都得一個一個透過 allowlist 橋接進來。

🔸 隔離在不同的層。 這也回答一個常見疑問: 「interpreter 不也是在沙箱裡跑程式碼嗎?」其實不一定。interpreter 底層是像 QuickJS 這種內嵌的小引擎,跟 harness 跑在同一個行程裡,它的「隔離」是語言層的: 執行環境預設沒綁任何主機 API,所以裡面的程式碼根本沒有東西可以濫用。真正的沙箱 (gVisor、microVM 那種) 則是作業系統或硬體層的隔離,目的是擋住 agent 自己生成、可能亂來的程式碼「逃逸」。一句話: 能力控管 (capability scoping) 防的是「能碰到什麼」,VM 隔離防的是「會不會逃出去」,是兩回事。跑不可信的程式碼時,你還是可以把整個 interpreter 再包進一個沙箱,那是多加的一層防禦,不是 interpreter 自帶的機制。

PTC 跟 Deep Agents Interpreter 共同做到的關鍵動作是: 一旦把 Task 或 agent 工具也橋接進來,程式碼就能開子代理人。程式碼從「呼叫工具」升級成「呼叫 agent」,正式參與了 agent loop。

LangChain 後來進一步做了 interpreter skills: 開發者事先把一段「已知有效的固定流程」寫成 TypeScript 模組,註冊成一個 skill。模型在對話中判斷「現在該用哪個 skill、傳什麼輸入」,但 skill 內部的步驟是寫死的程式碼,不是模型即時決定的。

拿他們的 GitHub repo triage 範例來看,一個 interpreter skill 由 SKILL.md (告訴模型什麼時候該用) 和 index.ts (實際的流程程式碼) 組成。當使用者說「幫我整理這個 repo 的 issue」,模型判斷該用這個 skill,在 interpreter 裡呼叫它:

// skills/github-triage/SKILL.md 告訴模型: "Use this skill when a user asks for repository triage."
// skills/github-triage/index.ts 匯出 triage() 函式,內部流程是寫死的:
//   抓所有 open items → 開子代理人做摘要 → 逐一分群歸類

const { triage } = await import("@/skills/github-triage");
const result = await triage("langchain-ai/deepagents", {
  issues: true, prs: true, discussions: true,
});
result.toMarkdown();

模型決定的是「要不要用、傳什麼參數、結果怎麼處理」; 但 triage() 內部怎麼抓資料、怎麼開子代理人、怎麼分群,全是寫死在 index.ts 裡的確定性程式碼。

這跟 Claude Code dynamic workflows 已經非常接近了,差別是 interpreter skills 由開發者事先寫好,而 dynamic workflows 是模型在接到任務時才即時生成那份編排腳本。

這套設計背後的理論支點: RLM

dynamic workflows 不只是工程上的巧思,它底下有一條清楚的研究脈絡撐著,叫 RLM (Recursive Language Models)。

這是 MIT 的 Alex Zhang、Tim Kraska、Omar Khattab 提出的框架 (2025 年 10 月先以部落格形式提出,年底發表論文)。核心想法是: 把整段 prompt 當成一個放在 REPL 裡的「外部物件」。主模型不把上下文一口氣吞進腦袋,而是寫程式碼去窺看它、拆解它,對其中的片段遞迴呼叫自己或子模型,中間結果留在 REPL 的變數裡,只有精簡過的結果才回到主模型。這樣就繞過了固定 context window 的天花板。

關鍵在於 RLM 長期主張、而 coding agent 一直缺的那個能力: 以程式化的方式呼叫 sub-agent,把它們的輸出在程式碼裡傳來傳去,而不經過主模型的上下文。 這正是 dynamic workflows 在做的事。

也因此,RLM 作者群直接認領了這個發表: Omar Khattab 說「Claude Code 終於是一個 RLM 了」; Alex Zhang 則認為 Opus 4.8 加上 dynamic workflows,大概是第一個被認真訓練成 RLM 的前沿模型實例。

Dynamic workflows 的設計跟 RLM 的主張高度吻合: 模型寫的那段 JS 裡,agent() 就是在遞迴開子代理人,而編排這層是決定性的。

5. Claude Code Dynamic Workflows

終於來講到本篇的主角 Claude Code dynamic workflows 了,一樣是讓模型寫程式碼去編排 sub-agent。模型即時寫出程式碼這件事,跟 PTC、Deep Agents Interpreter 並沒有不同。真正不一樣的,是把這套編排磨硬、做成產品: 編排腳本可以中斷後原地 resume,而且是決定性的 (第 8 節會講); 規模一次拉到上千個 sub-agent; 在背景獨立跑完才把結果交回對話; 還收斂成六種可互相組合的模式、能存檔重用。前面那幾種都沒有這層「可重播、可規模化」的保證。

實際操作時,你在 prompt 裡帶一個 workflow (或 ultracode),Claude 就會即時寫出一份 JavaScript 編排腳本,用 agent()parallel()pipeline()phase() 這些函式 (官方文件),平行啟動一大批 sub-agent (同時最多 16 個,單次最多 1000 個),跑完再把結果交回來。

借用 Voxyz 那篇文章的講法: 計畫從對話的上下文搬進了可執行腳本。 以前用 Claude Code 跑大任務,所有步驟都擠在同一個對話上下文裡: 做完一步、等結果、決定下一步、再等結果。任務越複雜,上下文本身反而變成瓶頸,卡住的從來不是「誰來做」,而是「誰記得整個計畫」。dynamic workflows 把計畫從對話裡抽出來: 迴圈、分支、中間狀態、審查鏈、重試,全都在腳本裡跑,主對話只收到最後的摘要。

官方那篇 “A harness for every task” 非常推薦一讀,把「為什麼需要這個」講得很清楚。當 Claude 在單一 context window 裡又要規劃又要執行,跑久了會出現三種失敗模式:

1️⃣ Agentic laziness (偷懶): 複雜的多步任務做到一半就宣告完成,例如 50 項的安全檢查只做了 20 項就說好了。

2️⃣ Self-preferential bias (偏袒自己): Claude 傾向偏好自己產出的結果,叫它對照評分準則自我審查時,這個偏差特別明顯。

3️⃣ Goal drift (目標漂移): 多輪之後,尤其每次 compaction 都是有損壓縮,「不要做 X」這種邊界約束會慢慢被弄丟。

把任務拆給各自擁有獨立、乾淨上下文的多個 Claude,從結構上就避開這三個坑。

編按: 這三個失敗模式雖然是 Anthropic 自己歸納的,但跟學界的觀察高度吻合。Berkeley 那篇 《Why Do Multi-Agent LLM Systems Fail?》 (MAST) 分析了 1600 多份多 agent 系統的失敗軌跡,歸納出 14 種失敗模式,其中很大一塊正是「驗證不足」跟「提早收工」: 沒有獨立的把關者、任務沒做完就被宣告完成。dynamic workflows 用獨立上下文的 agent 配上對抗式驗證,剛好是針對這類失敗的結構性解法。

最有名的案例是 Bun 從 Zig 改寫成 Rust: 約 75 萬行 Rust、99.8% 既有測試通過、從第一個 commit 到 merge 只花 11 天,靠的就是把任務切成「map → generate → review → verify → fix loop → cleanup」的流水線。

從 Code Interpreter 到 Dynamic Workflows,雖然都是「讓 LLM 輸出程式碼來執行」,但真正分出差異的是: 那段生成的程式碼,被准許呼叫什麼、又被限制成什麼。

階段
生成的程式碼呼叫什麼
能開子代理人?
限制 → 換到的穩定性
Code Interpreter
只有 Python 套件、檔案 (碰不到 agent 工具)
運算不設限,但站在 agent loop 外
CodeAct
agent 的工具 (固定函式)
幾乎不設限: 表達力最大,但最難控管
Code Mode
包好的一整套 SDK (= 那套 API)
框在 SDK 內: 用熟悉的程式碼駕馭龐大 API、省上下文
PTC・Deep Agents Interpreter
agent tools,甚至開子代理人
靠 allowlist 開放: 結果留在程式碼、保住控制面
Dynamic Workflows
只有編排函式 agent()/parallel()/pipeline()
只能編排、禁時間亂數: 換到 resume、可重播、決定性

✏️ 小編製圖

Code Interpreter 什麼都能寫,但程式碼碰不到 agent 的工具,等於站在 agent loop 外面。CodeAct 的關鍵一步,是讓程式碼呼叫 agent 的工具,把程式碼拉進 loop 裡。之後每個階段的設計取捨不同: Code Mode 框在一套 SDK 裡; PTC 跟 Deep Agents Interpreter 用 allowlist 決定能碰哪些工具跟 agent; Dynamic Workflows 則把程式碼限制在「編排」這一件事,不能直接碰檔案、shell、網路 (那些都丟給 sub-agent),連時間跟亂數都被禁掉,換來的是 resume 跟決定性。

表格裡另一個值得注意的差異: 程式碼呼叫的是「死的函式」,還是「會自己推理的 sub-agent」。 原始 CodeAct 論文裡,程式碼呼叫的工具都是 lookup_rate 這種固定函式: 你可以用迴圈、條件去串它們的輸出,但函式本身不會思考。

不過嚴格講,CodeAct 這條線本身就能開子代理人,只要塞進去的那個「工具」其實是一個 agent 就行。Hugging Face 的 smolagents 早就做到了: 你給一個 agent 設好 namedescription,當成 managed_agents 交給一個 manager CodeAgent,manager 生成的 Python 就能把它當函式呼叫。官方範例裡,一個 manager 指揮底下的 web_search_agent 查資料,自己負責規劃跟計算。所以表格裡 CodeAct 那格的 ✗,與其說是「技術上做不到」,不如說是「原論文沒往這個方向想,生態也還沒把 sub-agent 編排當成主軸」。

PTC 跟 Deep Agents Interpreter 帶出的重點是「程式碼能呼叫 agent tools」,雖然 tool 裡面也可以包一個 sub-agent,但這不是它們強調的用法。程式碼可以用迴圈、條件去組合多個工具呼叫,中間結果留在程式碼裡不必繞回主模型的上下文,後一個工具的輸入還可以拿前一個的結果在程式碼裡算出來 (a = await tool(...); b = await tool(transform(a))),不必一開始就把所有參數寫死。

真正把 sub-agent 編排當成核心的,是 RLM 的研究和 Dynamic Workflows。RLM 一路主張的就是「以程式化的方式呼叫 sub-agent,把輸出在程式碼裡傳遞,而不經過主模型的上下文」。到了 Dynamic Workflows,開子代理人變成程式碼唯一在做的事,還補上了 resume、可重播、決定性這些硬保證。

表格裡那條 ✗✗✗✓✓ 的分界,標的不是「技術上能不能開子代理人」(CodeAct 透過 smolagents 也做得到),而是「有沒有把 sub-agent 編排當成一等公民,再用產品級保證撐住」。

PTC / Deep Agents Interpreter 跟 Dynamic Workflows 都能 spawn、也都能分階段,差別不在「能不能」,而在編排層被磨得多硬。Dynamic Workflows 多了 parallel/pipeline/phase 這些原語,可中斷後 resume,還有禁時間亂數換來的決定性; PTC / Deep Agents Interpreter 則更通用、也更鬆,少了這些硬保證。

Dynamic Workflows 把程式碼的角色限制在編排,再用六種固定模式把編排本身也標準化。限制換來的是能 resume、能重播、結果可預期。

六種可以互相組合的編排模式

“A harness for every task” 這篇把常見的編排手法整理成六種,實際用時常常會疊在一起:

1️⃣ 分類並執行 (classify-and-act): 先用一個分類 agent 判斷任務屬於哪一型,再依結果路由到不同的 agent 或行為。也可以反過來放在最後,用分類器決定要輸出什麼。

輸入分類器行為 A行為 B行為 C

2️⃣ 分流並整合 (fan-out-and-synthesize): 把任務拆成很多小步,每步開一個 agent,最後再把結果整合起來。適合步驟很多、或每步都需要自己乾淨上下文以免互相干擾的情況。整合那一步是個同步點 (barrier): 它會等所有分流的 agent 都跑完,再把它們的結構化輸出併成一份結果。

任務步驟 1步驟 2步驟 3整合同步點 (barrier)結果

3️⃣ 對抗式驗證 (adversarial verification): 每開一個 agent 產出結果,就另外開一個 agent,專門拿評分準則或標準去「反駁」它的輸出。

產出 agent結果反駁 agent挑戰✓ 通過✗ 淘汰

4️⃣ 生成並篩選 (generate-and-filter): 針對一個主題先生成一堆點子,再用評分準則或驗證去篩,去掉重複的,只留下品質最高、通過檢驗的那幾個。

生成篩選評分準則 / 去重只留精華

5️⃣ 錦標賽 (tournament): 不是分工,而是讓 agent 互相競爭。開 N 個 agent 用不同方法各做同一件事,再用一個評審 agent 兩兩比較、淘汰,直到選出贏家。

方案 A方案 B方案 C方案 D勝者 1勝者 2冠軍

6️⃣ 直到完成為止持續迴圈 (loop until done): 對「工作量未知」的任務,不要設固定回合數,而是一直開 agent 直到觸發停止條件 (沒有新發現、或 log 裡不再有錯誤) 為止。

開一批 agent達成停止條件?否: 再開一輪完成

6. 更多精彩用法: 它不只拿來寫程式

官方那篇文章裡,小編覺得最有啟發的反而是作者一句話: workflows 用在「非寫程式」的任務上,往往更有威力。挑幾個他給的實際 prompt 範例 (已翻成中文),順手標出背後用到的模式:

🔹 抓 1/50 機率的 flaky test: 「這個測試大概每 50 次會失敗 1 次。開一個 workflow 重現它,提出幾個假設、在各自的 worktree 裡對抗式地驗證,/goal 設定: 不到有一個假設成立不准停。」(loop-until-done + 對抗式驗證 + 根本原因調查)

🔹 從自己的歷史紀錄煉出規則: 「用 workflow 翻過我最近 50 個 session,挖出我一再重複糾正的地方,把常出現的那幾類變成 CLAUDE.md 規則。」(分流並整合 + 記憶與規則遵循)

🔹 挖出 Slack 裡沒人追的問題: 「用 workflow 翻過去半年的 #incidents 頻道,找出反覆出現、卻從來沒人開單追蹤的根本原因。」(大規模分流)

🔹 多視角壓力測試商業計畫: 「拿我的商業計畫,跑一個 workflow,讓不同 agent 分別站在投資人、客戶、競爭對手的角度,逐點挑毛病。」(對抗式驗證用在非技術任務)

🔹 80 份履歷排序: 「這裡有 80 份履歷,用 workflow 依後端職缺排序,再仔細複查前十名。先用 AskUserQuestion 工具問我評分標準。」(排序 + 生成並篩選)

🔹 命名錦標賽: 「我要幫這個 CLI 工具取名。用 workflow 腦力激盪一堆選項,再跑一場錦標賽選出前三名。」(tournament)

🔹 驗證部落格草稿的每個技術宣稱: 「翻過我這篇部落格草稿,用 workflow 對照 codebase 驗證裡面每一個技術宣稱,我不想出包。」(深度驗證)

這些任務的關鍵不是「大」,而是「有很多獨立的小判斷、需要互相把關、而且最好別全擠在同一個上下文裡」。官方還把適用情境整理成一份清單: 遷移重構、深度研究、深度驗證、上千筆排序、記憶與規則遵循、根本原因調查、大規模分流、探索與品味、評估、模型與智慧路由。反過來作者也提醒: 一般的寫程式任務通常不需要五個審查者的陣仗,別為了用而用。

7. 看一份真的 workflow: deep-research 長怎樣

上面那些模式講起來還是抽象,剛好 Claude Code 內建的 /deep-research 就是用 dynamic workflows 寫的。以下都是直接讀它實際生成的那份 JS 腳本 (數字、門檻都照腳本,不是憑空猜的)。

它的 meta 先宣告整條流水線分成五個 phase:

phases: [
  { title: "Scope",      detail: "把問題拆成 5 個搜尋角度" },
  { title: "Search",     detail: "5 個平行 WebSearch agent,一個角度一個" },
  { title: "Fetch",      detail: "URL 去重,抓前 15 個來源,抽出可查證的主張" },
  { title: "Verify",     detail: "每個主張 3 票對抗式驗證,2/3 票反駁就淘汰" },
  { title: "Synthesize", detail: "合併語意重複、依信心排序、附上出處" },
]

這就是 Claude 為「深度研究」這個任務即時寫出來的 harness,整條流水線長這樣:

Scope
1 個 agent
把問題拆成 5 個搜尋角度
↓  pipeline,階段間不等齊
Search
5 個 agent · 平行
一個角度一個 WebSearch
↓  純 JS: URL 正規化 + 去重 + 抓取額度控制
Fetch
≤15 個 agent
每個來源抽出可查證的主張
↓  同步點 (barrier): 等所有主張到齊,排序取前 25
Verify
25 × 3 個 agent
每個主張三個找碴 agent,2/3 反駁就淘汰
↓  純 JS: 計票 + 篩出存活的主張
Synthesize
1 個 agent
合併、排序、附出處,輸出報告
藍底方塊 = 模型判斷 (agent()),中間的連接線 = 寫死的 JavaScript 編排。判斷交給模型,串接交給程式。

✏️ 小編製圖

逐段拆:

Scope (1 個 agent): 把使用者的問題交給一個 agent,要它拆成 5 個互補的搜尋角度 (廣度、學術、近期新聞、反方、實作面),並用 schema 強制回傳結構化 JSON,不准自由發揮。

Search → Fetch (用 pipeline,不等齊): pipeline()parallel() 的差別是「要不要等所有人都跑完才往下」(後者是同步點/barrier)。pipeline 不等,所以角度 A 搜完、去重、開始抓網頁的同時,角度 B 可能還在搜,不必等齊。每個角度: 一個 search agent 找 4-6 個結果 → 一段「純 JavaScript」做 URL 正規化、去重、抓取額度控制 → 再對每個沒看過的 URL 平行開一個 fetch agent 抽出主張。

去重、配額這些事完全不是 agent 在做,是普通的 JS:

const novel = sorted.filter(r => {
  const key = normURL(r.url)
  if (seen.has(key)) { dupes.push(...); return false }    // 看過了,丟掉
  if (fetchSlots <= 0 && relRank[r.relevance] >= 1) {      // 額度用完,低相關的丟掉
    budgetDropped.push(...); return false
  }
  seen.set(key, ...); fetchSlots--; return true
})

Verify (用 parallel,這裡刻意要同步等齊): 對排序後的前 25 個主張,每個都平行開「3 個」對抗式驗證 agent。它們的 prompt 開門見山就是「要懷疑、想辦法反駁這個主張」,而且「不確定就預設 refuted=true」。3 票有 2 票反駁,這個主張就淘汰。這正是前面講的對抗式驗證加上生成並篩選,而 self-preferential bias 在這裡被結構性擋掉了: 驗證的不是寫出主張的那個 agent,是另外三個被叫來找碴的 agent。

parallel(Array.from({ length: 3 }, (_, v) => () =>
  agent(VERIFY_PROMPT(claim, v), { schema: VERDICT_SCHEMA })
)).then(verdicts => {
  const refuted = verdicts.filter(v => v.refuted).length
  return { ...claim, survives: refuted < 2 }   // 2 票反駁就淘汰
})

Synthesize (1 個 agent): 把存活下來的主張交給一個 agent,合併語意重複、分組成一條條 finding、依信心高低排序、附上出處,產出一份有引用的報告。

看完這份腳本就會發現,所有「需要判斷」的事 (拆角度、搜尋、抽主張、反駁、整合) 都包在 agent() 裡; 而所有「不該讓模型亂猜」的事 (串接、去重、計票、排序、淘汰) 全是寫死的 JavaScript。一個 agent 偷懶或看走眼,只會壞掉它自己那一格,不會污染整條流水線。

8. 模型生成腳本,但腳本本身是決定性的

這裡有個觀念要分清楚,也是整套設計最巧妙的地方: 寫 workflow 的是模型,但 workflow 一旦寫好,執行就是決定性的。

模型的判斷只發生在兩個地方: 一是「寫出這份腳本」的當下,二是每個 agent() 呼叫的內部。中間的迴圈、分支、去重、計票、合併,全是寫死的 JavaScript,跑幾次結果都一樣。所謂的「dynamic」,動的是「腳本被生出來」這一刻; 一旦生出來,它就是一份規規矩矩、可重播的程式。

Yi Ding 的觀察蠻精準的: 這東西本質上就是一個 (相當詳細的) prompt,教模型用 JavaScript 寫出一段像圖結構的編排描述。那段「教模型寫 workflow」的 prompt 大致在教這些 (示意,非逐字):

你可以寫一支 JavaScript 編排腳本,開頭必須是:
  export const meta = { name, description, phases }   // 純字面值,不能放變數或運算

腳本裡可以用這些函式:
  agent(prompt, { schema, label })  // 開一個 sub-agent;給 schema 就強制它回傳結構化 JSON
  pipeline(items, stage1, stage2)   // 每個項目獨立流過各階段,不等齊 ← 預設首選
  parallel(thunks)                  // 一次跑多個、等全部完成才往下 (barrier) ← 真的需要才用
  phase(title) / log(msg)           // 分組與進度回報

規則:
  - 預設用 pipeline,只有「下一階段真的需要上一階段全部結果」時才用 parallel
  - 想更可信就用對抗式驗證: 每個發現另外開 N 個 agent 試著反駁它
  - 不准用 Date.now()、Math.random()、無參數的 new Date()

最後那條規則小編覺得最特別。一個正常的 JavaScript 環境怎麼可能不給你取時間跟亂數? 但 workflow 偏偏把這幾個函式做成會直接報錯。原因藏在 resume 機制裡: workflow 會把每一個 agent() 呼叫的結果寫進一份執行日誌,這樣中斷的執行才能原地接續 (已經跑完的 agent 直接從日誌裡回傳快取結果,只有沒跑完的才重跑)。一旦腳本裡摻了時間或亂數這種非決定性的東西,重跑時就會跟上一次對不起來,日誌裡的快取立刻失效。所以要時間戳記,就從 args 傳進來; 要讓每個 agent 長得不一樣,就用索引 (index) 去變化它的 prompt 或 label。

這個特別的限制也說明了整套設計的核心: 編排層必須是純粹、可重播的,模型的不確定性被嚴格關在 agent() 這個盒子裡。外面是一份能 resume、能重跑、能存檔重用的程式; 裡面才是會思考、會判斷、偶爾也會犯錯的模型。

也有幾個現實邊界值得先知道:

  • 成本: 一次跑幾十上百個 agent,token 成本明顯比一般對話高,官方建議先拿小範圍試水溫
  • 不能中途插手: 執行中不能插入使用者輸入,需要人工核准的地方,得拆成兩個 workflow、中間留一個核准空檔
  • 檔案修改自動核准: sub-agent 預設跑在 acceptEdits 模式,所以真正該把關的是腳本本身與 workflow 邊界
  • resume 有限制: 只保證在同一個 Claude Code session 內有效,關掉重開就是從頭跑

收束: 這一切其實很符合 Bitter Lesson

ihower 之前在這個部落格寫過一篇 Bitter Lesson 與 agent harness,裡面點名一個「workflow trap」: 視覺化的 workflow builder 把任務拆解鎖死在一張固定的圖上,模型之後變強了,這張圖也不會自動受益,反而變成包袱。照 Bitter Lesson 的邏輯,把可靠性押在人工手刻的鷹架程式碼 (scaffold) 上,是把複雜度搬錯方向。

dynamic workflows 巧妙地閃過了這個陷阱。差別在於: 拆解 workflow 的不是人,是模型,而且每個任務都重新寫一份客製的。 官方那句話講得最到位: Claude 現在可以「為手上的任務即時寫出自己的 harness」。你拿到了確定性編排帶來的可靠性 (避開偷懶、偏袒、漂移),卻不必付出人工設計鷹架的代價,而那種鷹架正是模型一變強就第一個被淘汰的東西。所以這個設計是順著 Bitter Lesson 的: 連「編排」這件事本身,也交還給模型。

ihower 那篇的結論是: 原本覺得需要人工編排的 workflow 任務,幾乎都能丟給模型即時生成的 harness,不必再事先設計流程。dynamic workflows 基本上就是這個方向的具體實現。

小編看法

最後小編補幾個比較主觀的想法。

需要 GUI 支援

如果它就停在 CLI 這個形態,小編覺得採用門檻會偏高。原因是這套概念其實偏抽象: 計畫從上下文搬進腳本、決定性編排、對抗式驗證,這些好處都要想一層才懂,不容易一眼看懂。對多數使用者來說,體感上最直接的反而是「token 怎麼燒這麼兇」,那個爽點 (省下的上下文、避開的偷懶與漂移) 卻是隱形的,藏在你看不到的地方。

但如果把它做成 GUI,讓那份編排腳本變成一張動態的節點與連線圖,agent 怎麼分流、在哪裡匯合、哪一條審查鏈正在跑、哪個節點卡住了,全部即時長在眼前,採用率會高很多。因為「看見流程怎麼跑」這件事太有感了,它把隱形的價值變成你盯著看的畫面。

希望同時支援事先生成和動態生成

目前 Claude Code 是靠 ultracode 自動判斷這個任務要不要動用 workflow,而且每次都是當場重新編排一份。你雖然可以把喜歡的那份存下來、下次重用,但小編實測後發現,存下來的腳本常常把這一次執行的具體場景 (檔名、數量、路徑那些) 寫死進去,重用性其實沒那麼完美。更可惜的是,你沒辦法「先讓它把 workflow 的 JavaScript 生出來、自己檢查過、確認沒問題,再真正執行」,生成跟執行是綁在一起的。

其實這就是前面講的 LangChain interpreter skills 在做的事: 事先寫好一份確定性的編排腳本,存起來重用。小編希望 Claude Code 能兩種都支援: 對重複性高的任務,可以事先生成、檢視、微調一份 workflow 腳本再拿去跑; 對一次性的任務,維持現在每次動態生成的做法。

不過還是得說一下這兩種做法的優缺點:

  • 事先生成: 可檢視、可測試、可微調,對重複性任務更可控。但缺點是一旦把編排凍結下來,模型下一版變強了,這份腳本也不會自動受益,反而可能變包袱,這正是前面講的 Bitter Lesson 的風險。
  • 動態生成: 每次針對當下任務量身定做,模型變強就自動受益,不會被舊腳本綁住。但缺點是你沒辦法事先檢查,生成跟執行綁在一起,而且存下來的腳本容易把場景寫死。

或是有一種中間解: 執行前能把生成的腳本叫出來瞄一眼、改幾筆 (要不要看是你的自由,不強制),存檔時多一步「幫我把這次的檔名、數量、路徑抽成參數」,讓重用的是「編排骨架」而不是「那一次的場景」。這樣既保住動態生成的彈性,又補掉現在重用會寫死的痛點。

OpenAI 的缺席

小編覺得比較可惜的是,整條脈絡最起點的 Code Interpreter,當年其實是 OpenAI 在 ChatGPT 上帶火的 (一度叫 Advanced Data Analysis),是它先讓大家看到「讓模型寫程式碼、跑程式碼」有多好用。但後來把「程式碼當行動空間」一路往 agent 編排推的,反而是 Cloudflare、Anthropic、LangChain 跟學界。OpenAI 在 Codex 產品層有做 subagents (平行 spawn 多個 agent),但在 API 層、SDK、產品上一直沒有走「程式碼當行動空間」這個方向,沒有 PTC 等級的東西,Code Interpreter 裡的程式碼到現在還是碰不到 agent 的工具。

補充比較: 那 agent teams 呢? 小編沒那麼看好

跟 dynamic workflows 幾乎同期,Claude Code 還推了另一個平行化功能 agent teams (目前還是 experimental、預設關閉)。

一句話講清楚差別: dynamic workflows 是「模型寫一份決定性腳本,編排一群各自獨立的 sub-agent」; agent teams 則是「開一個 team lead,底下生出好幾個完整的 Claude Code 實例當隊友,隊友各有自己的上下文、能互相點對點傳訊、搶同一張 task list 的工作」。subagent 只會把結果回報給主 agent,彼此不講話; agent teams 的隊友則能互相討論、互相挑戰,你甚至可以分別對每個隊友下指令。

Agent Teams vs Dynamic Workflows 圖片來源: Cat Wu (@_catwu)

小編在 《Multi-Agent 反模式與推薦模式》 那篇整理過一個核心判斷: multi-agent 的價值是「並行覆蓋」,不是「分工」。 而 agent teams 的賣點剛好踩在這條線上,看你怎麼用,很容易滑到錯的那邊:

1️⃣ 「角色分工」很容易滑進三省六部的幻覺。 官方範例那種「一個管 UX、一個管架構、一個當魔鬼代言人」「一個查安全、一個查效能、一個查測試」,如果是唯讀的審查或研究,那是健康的並行覆蓋,沒問題。但只要任務變成「每個隊友各認領一個模組去寫 code」,就掉進那篇點名的反模式: LLM 沒有人類的注意力上限,貼上「架構師」「QA」的角色標籤不會讓它更強,只會多出人為的推諉跟一層層的傳話漂移。

2️⃣ 並行寫入的老問題它沒解,只是叫你自己閃。 官方文件自己就寫: 「兩個隊友改同一個檔案會互相覆蓋,請把工作切開、讓每個隊友各自負責不同的檔案。」避免衝突的責任,被原封不動丟回給你手動切。Devin、Cognition 的工程經驗早就講白了: 平行寫入到現在還是行不通,可靠的做法是「單線寫入、其他 agent 只負責判斷」。

3️⃣ 點對點通訊 = 通訊開銷 + 傳話遊戲。 隊友彼此直接傳訊 (官方範例還鼓勵它們「像科學辯論一樣互相反駁」),agent 一多,訊息就多、漂移就多、也更難觀測。對照之下,orchestrator-worker (subagent、dynamic workflows 走的就是這條) 把溝通收斂到中心,反而更穩、也更好整合。那篇也提過,大廠 (Anthropic、OpenAI、Google) 實際用的都是 orchestrator-worker,不是角色扮演的流水線。

4️⃣ 成本跟可靠性都還沒到位。 每個隊友是一個完整的 Claude Code 實例 (不是輕量 subagent),token 隨人數線性往上疊。multi-agent 普遍燒 3-10 倍,Anthropic 自家 research 系統甚至約 15 倍。而且三個 90% 正確率的 agent 串起來,整體正確率掉到 72.9%。目前它還掛著一排 experimental 限制: 不能 resume、task 狀態會 lag、關機很慢、一次只能帶一個 team、不能巢狀、lead 還不能換人。學界也才剛出一篇 CooperBench,直接以「為什麼 coding agent 還當不了你的隊友」為題,點出它們缺的正是找共識、協調這種社會性能力。

另外一個觀察: agent teams 到現在還是 experimental、預設關閉,你得自己去設一個 CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS 環境變數才打得開。相比之下 dynamic workflows 直接上線、ultracode 隨叫隨用,兩者的成熟度差距從預設值就看得出來。

小編認為 agent teams 相對 dynamic workflows 的優勢並不明確。官方推薦的入門場景是唯讀的並行探索 (PR 審查、競爭假設除錯、查資料),但 dynamic workflows 用決定性編排加上對抗式驗證,一樣能做這些事,而且更便宜、更可重播。agent teams 真正不同的地方是「你可以在執行中途跟個別隊友對話、調整方向」,這是 workflow (背景跑完才回來) 做不到的,但這個互動性是否值得額外的成本和複雜度,目前沒有明確的證據。一旦想拿它來「分工寫 code」,就撞進那篇反模式的失敗區。

維度
Dynamic Workflows
Agent Teams
編排者
模型寫的決定性腳本
team lead 即時協調,隊友自己搶工作
寫入模型
收斂到腳本控管,偏單線
多個完整實例可同時寫,衝突要你自己切檔案
agent 間通訊
不互聊,結果回腳本
點對點互相傳訊
可重播 / resume
有 (執行日誌、禁時間亂數)
還不行 (experimental)
成本
高,但編排層零推理
更高,每個隊友一個完整實例
最適合
大規模、可決定性編排的覆蓋型任務
唯讀的並行探索 (審查 / 研究 / 競爭假設)

✏️ 小編製圖

這兩者真正的差異在前面講的那些: 決定性與可重播、成本結構、通訊模型 (中心化 vs. 點對點)、以及並行寫入的可靠性。從這些維度看,dynamic workflows 目前成熟度明顯領先,而 agent teams 還在找它真正不可替代的場景。

參考連結