Workflow 工具:腳本化的多代理編排
認識 Claude Code 的 Workflow 工具 — 以 agent()、pipeline()、parallel()、schema 與 resume,用確定性 JavaScript 編排子代理。
你將學到什麼
Fable 5 時代最大的架構新增是 Workflow 工具(工作流程工具):Claude Code 撰寫一份純 JavaScript 編排(orchestration)腳本,由 harness(執行框架)負責執行 — 透過迴圈、條件判斷與扇出(fan-out)協調大量子代理(subagent),而這些控制流是程式碼,不是模型的即興發揮。
完成本堂課後,你將知道:
- 為什麼在大規模下,腳本化編排勝過即興式委派
meta區塊的規則與各個基本元件:agent()、pipeline()、parallel()、phase()、log()、args- JSON Schema 如何強制子代理產出經過驗證的結構化輸出
- 決定該用
pipeline()還是parallel()的 barrier(屏障)紀律 - 硬性限制、被禁止的非確定性行為,以及 resume(恢復執行)如何運作
問題所在
你已經認識兩種委派模型。在第 4 堂課,主 agent 一次一個決定地生成子代理 — 讀一個結果、思考、生成下一個。在第 9 堂課,agent 之間透過訊息協調。兩者都是即興的:控制流存在於模型的工作記憶裡。
即興在大規模下會崩潰。要求一個 agent「審查全部 40 個變更的檔案、驗證每一個發現、不要提早停」,你就是在信任一個語言模型能一邊在上下文(context)裡雜耍所有結果,一邊執行一個 40 次迭代的迴圈。它可能漂移、去重不一致,或在第 28 個檔案悄悄停下。模型極度擅長判斷 — 這是 bug 嗎?這個證據站得住腳嗎? — 卻不擅長簿記。
程式碼的特性恰好相反。for 迴圈永遠不會在第 28 次迭代時累了。所以 Fable 5 時代把工作拆開:
Improvised delegation Scripted orchestration
───────────────────── ──────────────────────
Model decides each step Script decides each step
Loop = model discipline Loop = JavaScript
Fan-out = model memory Fan-out = pipeline(items, ...)
Hard to reproduce Deterministic, resumable
如何運作
核心模型
主 agent 撰寫一份純 JavaScript 腳本(不是 TypeScript)。harness 在背景執行它並回傳一個任務 ID;完成時會收到通知。/workflows 顯示即時進度。
腳本主體在 async 環境中執行,具備標準的 JS 內建功能。有兩樣東西刻意缺席:
- 腳本本身沒有檔案系統或 Node.js API。 有工具的是 agent;腳本沒有。任何會觸及外界的事都要透過
agent()。 - 沒有非確定性。
Date.now()、Math.random()和不帶參數的new Date()全都會拋出例外。我們會在「確定性與 Resume」一節說明原因。
meta 區塊
每份腳本都必須以此開頭:
export const meta = {
name: 'review-changes',
description: 'Review changed files, verify findings, summarize',
phases: [
{ title: 'Collect' },
{ title: 'Review' },
{ title: 'Report' },
],
};
meta 區塊必須是純字面值(pure literal) — 不能有變數、函式呼叫或展開運算。而且 phases 裡的標題必須與腳本主體中的 phase() 呼叫一致,harness 才能呈現準確的進度。
基本元件
agent(prompt, opts?) 生成一個子代理並回傳它的最終文字。選項有:label、phase、schema、model、effort、isolation: 'worktree'、agentType。如果使用者跳過該 agent,或它因終端性 API 錯誤而死亡,它會回傳 null — 一定要處理這種情況。model(sonnet | opus | haiku | fable)與 effort(推理強度,low 到 max)覆寫讓你混用層級:機械式階段用便宜模型配低 effort,驗證與綜合用最高層級配高 effort(見第 26 堂課)。
isolation: 'worktree' 給 agent 一個全新的 git worktree — 成本不低(每個 agent 大約 200–500 ms 的設置時間加上磁碟空間),所以只在多個 agent 平行修改檔案時才使用。沒有變更的 worktree 會自動移除。
pipeline(items, stage1, stage2, ...) 讓每個項目獨立地依序通過每個階段,階段之間沒有 barrier。項目 A 可以已經在第 3 階段,而項目 B 還在第 1 階段。這是多階段工作的預設選擇;總耗時等於最慢的單一項目鏈,而不是各階段全體等待的總和。
parallel(thunks) 以 barrier 並行執行 thunk — 它會等待全部完成。拋出例外的 thunk 會解析為 null(這個呼叫本身永遠不會 reject),所以要用 .filter(Boolean) 過濾結果:
const results = (await parallel([
() => agent('Check the API layer for the bug'),
() => agent('Check the data layer for the bug'),
])).filter(Boolean);
phase(title) 與 log(message) 用來分組進度並向使用者敘述過程。args 攜帶呼叫 Workflow 時傳入的值,讓腳本可以參數化。workflow(nameOrRef, args?) 以行內方式執行另一個 workflow — 可用已儲存的名稱或腳本路徑 — 但巢狀只能一層深,且子 workflow 共用父層的並行上限、中止訊號與預算。
透過 Schema 取得結構化輸出
傳入 opts.schema(一份 JSON Schema),agent() 就會回傳經過驗證的物件而非自由文字:子代理會被強制透過 StructuredOutput 工具輸出,不符 schema 時在工具呼叫層自動重試。這正是讓腳本化控制流變得實用的關鍵 — 腳本可以依 result.files.length 分支,因為 files 保證存在且是陣列。
Pipeline 與 Parallel:Barrier 紀律
最常見的編排錯誤,是插入不必要的 barrier。規則是:
只有當第 N 階段需要來自第 N−1 階段全部項目的跨項目脈絡時,barrier 才有正當性。
正當的 barrier:對完整集合去重或合併、總數為零時提早結束,或「把這個和其他發現互相比較」這類提示。
嗅覺測試:「我需要先 flatten/map/filter」或「這些階段在概念上是分開的」不構成 barrier 的理由 — 把轉換放進 pipeline 的階段裡做。每一個不必要的 barrier,都會讓整批工作等待其中最慢的成員。
確定性與 Resume
為什麼禁止 Date.now() 和 Math.random()?因為每次執行都會保存它的腳本,而用 resumeFromRunId 重新啟動時,會從快取重播 agent() 呼叫中最長的未變更前綴 — 只有被編輯過或新增的呼叫才會真的執行。transcript 目錄中的 journal.jsonl 記錄每個 agent 實際的回傳值。如果腳本在重播時產生不同的值,快取的前綴就會與現實脫節。確定性讓修復大型 workflow 變得便宜:編輯壞掉的階段、resume,它之前的一切都來自快取。
限制
- 每個 workflow 的並行 agent 數:min(16, CPU 核心數 − 2);超額的呼叫會排隊。
- 生命週期上限:每個 workflow 1,000 個 agent — 防止迴圈失控的保險。
- 單一
pipeline()或parallel()呼叫:最多 4,096 個項目,以明確的錯誤強制執行,而不是無聲截斷。
已儲存的 Workflow 與選擇加入
具名的 workflow 存放在 .claude/workflows/,可以用名稱呼叫。而且 workflow 是**選擇加入(opt-in)**的:沒有明確訊號 — 提示裡的「ultracode」關鍵字、「use a workflow」的要求,或指示使用它的 skill — Claude Code 會使用個別子代理或獨自作業。
實作範例
以下是一個典型的 review-changes workflow,逐行講解:
export const meta = {
name: 'review-changes',
description: 'Review changed files, verify findings, summarize',
phases: [
{ title: 'Collect' },
{ title: 'Review' },
{ title: 'Report' },
],
};
phase('Collect');
const changes = await agent(
'List the source files changed on this branch relative to main ' +
'(git diff --name-only). Return only files that exist.',
{
label: 'collect-changes',
phase: 'Collect',
model: 'haiku',
effort: 'low',
schema: {
type: 'object',
properties: {
files: { type: 'array', items: { type: 'string' } },
},
required: ['files'],
},
}
);
if (!changes || changes.files.length === 0) {
log('No changed files — nothing to review.');
} else {
phase('Review');
log(`Reviewing ${changes.files.length} changed files`);
const reviews = await pipeline(
changes.files,
(file) => agent(
`Review ${file} for correctness bugs introduced on this branch. ` +
'For each suspected bug, give the location and a concrete failure scenario.',
{ label: `review:${file}`, phase: 'Review', model: 'sonnet' }
),
(review) => review && agent(
`A reviewer reported:\n${review}\n\nTry to REFUTE each finding by ` +
'reading the actual code. Return only findings that survive.',
{ label: 'verify', phase: 'Review', effort: 'high' }
)
);
phase('Report');
const confirmed = reviews.filter(Boolean);
const report = await agent(
'Merge these verified review results into one report, deduplicating ' +
`overlapping findings:\n${confirmed.join('\n---\n')}`,
{ label: 'summarize', phase: 'Report', model: 'fable', effort: 'high' }
);
log(report || 'Summary agent returned nothing.');
}
meta 區塊是純字面值,它的三個 phase 標題與三次 phase() 呼叫完全一致。
Collect 使用 schema,所以腳本拿到的是 { files: [...] },不是需要解析的散文。機械式的列表工作交給 haiku 配 low effort。!changes 這道防護很重要:agent 被跳過或死亡時,agent() 會回傳 null。
Review 是一個沒有 barrier 的兩階段 pipeline() — 某個檔案的審查一完成,它的驗證就立刻開始,即使其他檔案還在審查中。驗證階段把每個發現交給一個被提示去反駁它的懷疑者,並使用 high effort,因為判斷才是值得花 token 的地方。review && 這道防護讓 null 沿著鏈傳遞下去,而不是讓程式崩潰。
Report 是唯一一個 barrier 有正當性的地方:合併與去重需要全部的驗證結果 — 而 await 整個 pipeline 恰好提供了這個 barrier。綜合交給最高層級的 fable 模型。而且由於這裡沒有任何非確定性,如果之後 summarize 的提示需要微調,resume 會從快取重播 Collect 和 Review,只重新執行最後一個 agent。
這份腳本每個發現只用一個懷疑者。第 28 堂課會把驗證正式地放大。
前後對比
| 即興式委派(第 4/第 9 堂課) | 腳本化 workflow(Fable 5 時代) |
|---|---|
| 控制流在模型的上下文裡 | 控制流在確定性的 JavaScript 裡 |
| 扇出受限於模型的簿記能力 | pipeline() 可處理多達 4,096 個項目 |
| 漏掉一次迭代無人察覺 | 迴圈不可能跳過項目 |
| 執行失敗 = 從頭再來 | Resume 重播快取前綴,只重跑其餘部分 |
| 自由文字結果,靠運氣解析 | Schema 驗證的結構化輸出 |
| 所有工作用同一個模型層級 | 每個 agent 可覆寫 model 與 effort |
子代理與 agent team 並沒有消失 — 對探索性或小規模的工作,它們仍是正確的工具。當工作的形狀已知、規模又大時,workflow 就接手。
關鍵洞見
控制流交給程式碼,判斷交給模型。 這一句話就是整個設計。
模型在大規模下做不好的一切 — 計數、迴圈、不提早停、記住全部 40 個結果 — 都交給 JavaScript 處理,而它把這些事做得完美。程式碼做不到的一切 — 判斷一個 diff 是不是 bug、反駁一個看似合理的發現、綜合一份報告 — 都委派給 agent() 呼叫,每個呼叫都配上該階段應得的模型層級與 effort。
那些一開始感覺很受限的規則(meta 必須是純字面值、不能用 Math.random()、不能存取檔案系統)換來的是保證:可重現的執行、以快取為後盾的 resume,以及不會失控的平行性。
下一堂課
第 28 堂課涵蓋編排品質模式 — 建立在這些基本元件之上的典型結構:對抗式驗證(adversarial verify)、評審團(judge panel)、loop-until-dry、多模式掃描(multi-modal sweep)與完整性批評者(completeness critic)。事實證明,答案品質是 harness 的結構性屬性,而不只是模型的屬性。