跳至主要內容
模組 5:Fable 5 時代 3 / 8
進階 Session 27 Workflow Orchestration Multi-Agent

Workflow 工具:腳本化的多代理編排

認識 Claude Code 的 Workflow 工具 — 以 agent()、pipeline()、parallel()、schema 與 resume,用確定性 JavaScript 編排子代理。

2026年7月8日 20 分鐘閱讀

你將學到什麼

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 內建功能。有兩樣東西刻意缺席:

  1. 腳本本身沒有檔案系統或 Node.js API。 有工具的是 agent;腳本沒有。任何會觸及外界的事都要透過 agent()
  2. 沒有非確定性。 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?) 生成一個子代理並回傳它的最終文字。選項有:labelphaseschemamodeleffortisolation: 'worktree'agentType。如果使用者跳過該 agent,或它因終端性 API 錯誤而死亡,它會回傳 null — 一定要處理這種情況。modelsonnet | opus | haiku | fable)與 effort(推理強度,lowmax)覆寫讓你混用層級:機械式階段用便宜模型配低 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: [...] },不是需要解析的散文。機械式的列表工作交給 haikulow 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 可覆寫 modeleffort

子代理與 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 的結構性屬性,而不只是模型的屬性。