Claude Code 最近多了一個功能叫動態工作流(dynamic workflows):讓主代理在執行時,當場寫一支 JavaScript,生成並協調一群子代理——每個子代理有自己獨立的 context window 和一個聚焦的小目標。

我前幾天用它做了件很實際的雜活:評估四個候選部落格選題,看哪個跟我既有文章庫重複、哪個值得寫。這篇把那支 script 整個攤開,講三件事——怎麼寫、parallelpipeline 怎麼選、跑一次燒多少 token。

為什麼不是「開更多分頁」那麼簡單

你可能會想,並行做事,開幾個對話視窗不就好了?

差別在 context。Claude Code 過去是「一個對話、一條 context」,所有東西擠在同一個上下文視窗。長任務這個模式有三個老毛病,官方發布時直接點名:智能惰性(做到一半宣布完工)、自我偏好偏差(驗證自己的產出時護短)、目標漂移(對話太長、尤其壓縮過後忘了最初目標)。

動態工作流的解法不是把單一 context 養得更肥,而是把活切開:每個子代理拿一塊乾淨的上下文,做一件聚焦的事,彼此不互相汙染。並行只是順帶的好處,真正的價值是這個隔離。

核心就五個函數

整個 API 你先記五個就能動:

  • agent(prompt, opts):派一個子代理,回傳它的最終輸出。opts 裡給 schema,它就被強制用結構化格式回傳。
  • parallel(thunks):一批任務同時跑,全部跑完才往下走。這是一道柵欄(barrier)。
  • pipeline(items, ...stages):每個項目各自流過所有階段,項目之間不設柵欄——A 還在第一階段,B 可能已經到第三階段。
  • phase(title):把進度分組顯示,純粹方便你看跑到哪。
  • schema:不是函數,是你丟給 agent 的一個 JSON Schema,決定它回什麼結構。

兩個一定要知道的限制,都寫在官方文件裡:meta 區塊必須是純字面值(不能放變數或函數呼叫);腳本裡不能用 Date.now()Math.random()(它們會破壞工作流的可重播性,直接呼叫會丟錯)。

把 script 攤開

先是宣告區。meta 在最前面,名字、描述、階段,全是死的字面值:

1
2
3
4
5
6
7
8
export const meta = {
name: 'ai-topic-gap-scan',
description: '評估 4 個候選 AI 開發工具選題對文章庫的缺口與讀者價值',
phases: [
{ title: 'Evaluate', detail: '每個候選主題派一個 agent:grep 文章庫查重複 + 評估讀者價值' },
{ title: 'Synthesize', detail: '綜合 4 份評估,排序出最該寫的選題缺口' },
],
}

接著定 schema。這是整支 script 我覺得最關鍵的一塊——它決定子代理回給我的是一個能直接用的物件,而不是一段要我自己 parse 的中文:

1
2
3
4
5
6
7
8
9
10
11
12
const EVAL_SCHEMA = {
type: 'object',
properties: {
topic: { type: 'string' },
coveredInVault: { type: 'boolean', description: '文章庫是否已有實質重複的文章' },
closestExistingPost: { type: 'string', description: '庫裡最接近的既有文章檔名,沒有填 "無"' },
valueForReaders: { type: 'integer', minimum: 1, maximum: 10 },
angle: { type: 'string', description: '若要寫,最佳切入角度(一句話)' },
reason: { type: 'string', description: '評分理由,2-3 句' },
},
required: ['topic', 'coveredInVault', 'closestExistingPost', 'valueForReaders', 'angle', 'reason'],
}

然後是主體——散出去四個子代理並行評估:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
phase('Evaluate')

const evals = await parallel(
CANDIDATES.map((c) => () =>
agent(
`你是技術部落格的選題編輯。評估「${c.topic}」值不值得寫成新文章。\n` +
`步驟:用 Glob 列出文章庫檔名、用 Grep 查關鍵字「${c.keywords}」、` +
`判斷是否實質重複、給 1-10 的讀者價值分、給一句切入角度。`,
{ label: `eval:${c.topic.slice(0, 16)}`, phase: 'Evaluate', schema: EVAL_SCHEMA }
)
)
)

const valid = evals.filter(Boolean)

注意 CANDIDATES.map(... () => agent(...))——parallel 收的是一陣列「拿來就能呼叫的函數」(thunk),不是已經啟動的 Promise。這個細節錯了會變成全部序列跑,並行就沒了。

最後收齊四份評估,丟給一個綜合代理排序:

1
2
3
4
5
6
7
8
9
phase('Synthesize')

const synthesis = await agent(
`以下是 4 份評估 JSON:\n${JSON.stringify(valid, null, 2)}\n` +
`請從「最該寫」到「最不該寫」排序,並指出文章庫最大的缺口。`,
{ label: 'synthesize', phase: 'Synthesize', schema: SYNTH_SCHEMA }
)

return { evals: valid, synthesis }

SYNTH_SCHEMA 我就不整段貼了,重點是它要求一個 ranking 陣列加一個 vaultGap 字串——同樣靠 schema 把「主編的判斷」框成結構化資料。

設計決策一:parallel 還是 pipeline

這是寫 workflow 最先要決定的事,我一開始也猶豫。

官方文件的預設建議是 pipeline,因為它不設柵欄、整體更快——項目 A 走完第一階段就能直接進第二階段,不用等 B。大多數多階段的活都該用它。

但我這個案子用了 parallel,因為綜合那一步必須等四份評估全部到齊才能排序。少一份,排出來的東西就是殘缺的。這正是少數真的需要 barrier 的場景:下游要的是「全部結果的總和」,而不是「單一項目流過管線」。

一句話判準:如果你的下一步只需要單一項目的前一步結果,用 pipeline;如果它需要前一階段所有項目的結果,才用 parallel 這道柵欄。

設計決策二:schema 把散文變物件

沒有 schema,子代理回你一段話,你還得自己從裡面挖出「價值幾分、重不重複」。給了 schema,驗證在工具呼叫那一層就做掉——格式不對它自己重試,回到你手上已經是乾淨的物件,可以直接 valid.filter(e => e.valueForReaders >= 7) 這樣用。

舉個實際的——這是其中一個子代理回來的東西,原封不動(reason 我截短了):

1
2
3
4
5
6
7
8
{
"topic": "Claude Code Dynamic Workflows(動態工作流)",
"coveredInVault": false,
"closestExistingPost": "多個AI-agent並行改程式碼互相覆蓋的隔離原則.md",
"valueForReaders": 7,
"angle": "用『靜態 plan-then-execute vs 動態 runtime 即興派發』的對照切入",
"reason": "文章庫在多代理這塊已經很厚,但沒有一篇正面講 runtime 即興編排,最接近那篇談的是並行派發的失敗模式,屬於沾到邊而非實質重複。"
}

你看到的不是一段「我覺得這題還行因為⋯⋯」的散文,而是欄位齊全、型別正確的物件——valueForReaders 真的是個 7 不是字串,coveredInVault 真的是個布林。把判斷框成 schema,下游的 filtersort 才接得住。

那個 evals.filter(Boolean) 也不是裝飾。依 runtime 規格,子代理中途被略過、或它自己出錯時會回傳 null,過濾掉才不會把 null 餵進下一步。我這次五個全活,但生產環境你得假設它會掉。

跑一次多少錢

講完怎麼寫,講代價。這趟的真實數字:

指標 數字
子代理數 5(4 評估 + 1 綜合)
工具呼叫 31 次
耗時 133 秒
token 375,328

兩分鐘換一份選題排序,代價 37 萬 token。這不便宜——而且要講清楚,37 萬是 token 量,不是帳單金額;你實際付多少,看你的訂閱方案、以及子代理跑在哪個模型上。官方自己也講白了:「大多數傳統 coding 任務不需要五個審稿員。」

所以我的判準是這樣:

  • 該用:深度研究、跨檔案大遷移、根因調查、大規模去重——這些單一 context 撐不住、會惰性或漂移的活。
  • 不該用:單檔編輯、你心裡已經有答案、或一個子代理就能搞定的事。

補一個跟隔離有關的點:如果你的子代理會並行改同一批檔案,記得讓它們各跑一個 worktree(agent(prompt, { isolation: 'worktree' })),否則它們會互相蓋掉對方的修改。並行很爽,但前提是任務之間真的不共享狀態。

寫在最後

動態工作流不難寫,難的是判斷什麼時候值得動用它。五個函數、一個 schema,半小時就能跑起來;但 37 萬 token 的帳單會逼你誠實面對「這活到底要不要五個腦袋」。

最後說個題外話。我用這支 workflow 取材時,還順手踩了一個關於我自己的坑——我一度咬定其中一個子代理在幻覺,寫成文章、還過了兩輪 AI 審稿,最後翻開 transcript 才發現它老老實實做了查證,幻覺的是我。那是另一篇文章了。

工具值得學。只是學會之後,別像我一樣,只看它最終吐了什麼就下結論。