編排品質模式
七個 workflow 品質模式 — 對抗式驗證、評審團、loop-until-dry — 讓多代理的答案從貌似合理變成詳盡可靠。
你將學到什麼
第 27 堂課教了你 Workflow(工作流程)工具的機制:agent()、pipeline()、parallel()、schema、resume。這堂課講的是你用這些機制建構什麼。生成二十個 agent 不會讓答案變正確 — 讓答案正確的是 workflow 的結構。
完成本堂課後,你將知道:
- 七個經典品質模式:adversarial verify(對抗式驗證)、perspective-diverse verify(多視角驗證)、judge panel(評審團)、loop-until-dry(循環至枯竭)、multi-modal sweep(多模式掃描)、completeness critic(完整性批評者)與 no-silent-caps(不容無聲上限)
- 為什麼 loop-until-dry 的去重必須針對所有看過的發現,而不只是已確認的
- 如何讓 workflow 的結構與需求的規模相稱,並讓 token 預算塑造執行過程而不是讓它崩潰
- 如何把這些模式組合成一支詳盡審查腳本
問題所在
一個天真的 workflow 扇出 N 個 agent、收集它們的答案,然後合併。感覺很嚴謹。但它有三種失敗模式,再多的扇出也修不了:
- 貌似合理但錯誤的結果。 被問「這是 bug 嗎?」的單一 agent 傾向於言之鑿鑿地說是。合併十二份這樣的報告,你得到的是十二個信心滿滿的「也許」。
- 規模未知的探索。 如果你不知道總共有多少 bug,跑一輪並不能告訴你任何關於完整性的資訊。「agent 都回來了」不等於「搜尋完成了」。
- 無聲截斷。 一個只保留「前 10 個發現」或抽樣子集的階段,會在不告訴任何人的情況下丟掉覆蓋範圍。報告看起來很完整,因為裡面沒有任何東西說並非如此。
解法是結構性的。你沒辦法靠 prompt 脫身 — 你要安排 agent,讓錯誤的發現被殺掉、讓探索能證明自己已經枯竭、讓每個上限都看得見。
如何運作
Adversarial Verify(對抗式驗證)
針對每個候選發現,生成 N 個獨立的懷疑者,它們拿到的 prompt 是反駁它 — 不是審查、不是評分,而是證明它是錯的。如果多數反駁成立,這個發現就死了。
prompt 的方向很重要。被要求「驗證這個」的 agent 會錨定在同意上;被要求「這大概是錯的 — 說明為什麼」的 agent 才會真正重新推導這個主張。獨立的懷疑者(各自在自己的上下文(context)裡、彼此不知道對方存在)無法在同一個錯誤上串通。這是對「貌似合理但錯誤」結果的直接對策。
Perspective-Diverse Verify(多視角驗證)
一個變體:不用 N 個一模一樣的反駁者,而是給每個驗證者一個不同的視角(lens) — 正確性、安全性、效能、能否重現。你用一些冗餘換取覆蓋範圍:三個相同的懷疑者可能全都漏掉一個競態條件(race condition),而一個明確被指派並行議題視角的驗證者會抓到它。當問題是「這個主張是真的嗎?」用相同的反駁者;當問題是「這個主張是真的嗎,而且它是完整的全貌嗎?」就用視角。
Judge Panel(評審團)
用於生成型任務(設計、修復、解釋)而非驗證型任務:從不同角度執行 N 次獨立嘗試,用平行的評審打分,然後從優勝者加上落選者的最佳想法進行綜合。綜合這一步是它與「三選一挑最好」的分水嶺 — 落敗的嘗試常常包含優勝者漏掉的一個洞見。
Loop-Until-Dry(循環至枯竭)
用於規模未知的探索:持續生成一輪輪的搜尋者(finder),直到連續 K 輪沒有任何新發現。這個終止條件是唯一誠實的枯竭訊號 — 固定的輪數只是一個套著迴圈外衣的猜測。
微妙的規則是:去重要針對所有看過的(seen),而不只是已確認的(confirmed)。 假設第 1 輪找到候選 X,而你的評審否決了它。如果你的去重清單只存已確認的發現,第 2 輪的搜尋者會再次「發現」X,評審會再次否決它,迴圈永遠不會枯竭 — 被評審否決的發現會永遠重現。seen 集合必須記錄每一個曾被回報的發現,無論它的命運如何。
Multi-Modal Sweep(多模式掃描)
平行的 agent 各自用不同的方式搜尋:按容器、按內容、按實體、按時間。十個跑同一套搜尋策略的 agent 共享同樣的盲點;四個策略彼此正交的 agent 能互補盲點。在程式碼審查中,這表示一個搜尋者逐檔掃描、一個跨模組邊界追蹤資料流、一個順著被改動的函式找到呼叫端、一個檢查改動觸及的錯誤路徑。
Completeness Critic(完整性批評者)
主要流程跑完後,最後一個 agent 只拿到一個問題:「還缺什麼?」 它的工作不是找出更多實例,而是找出未被涵蓋的類別 — 「沒有人檢查 migration 腳本」、「沒有搜尋者看並行問題」。它的發現會作為下一輪探索的種子。這是第 20 堂課錯誤恢復反射的 workflow 層級版本:先假設流程某處失敗了,然後去找出來。
No Silent Caps(不容無聲上限)
如果覆蓋範圍在任何地方被限制 — top-N 挑選、抽樣、預算逼出的提前退出 — 就記錄被丟掉的東西。一行 log() 不花什麼成本;一個看不見的上限賠上的是整份報告的信任。這和你在第 24 堂課套用在生產環境配置上的可觀測性紀律相同,只是這次指向 workflow 本身。
讓結構與需求相稱
不是每個請求都值得動用全部火力。「找出這個 diff 裡的 bug」是有界的需求:一次 multi-modal sweep 加上 adversarial verify 就成比例了。「徹底稽核這個」是講求詳盡的需求:loop-until-dry、judge panel 和 completeness critic 正是重點所在,而不是殺雞用牛刀。Ultracode 式的執行明確為「最詳盡的正確答案」而非「最便宜的答案」做了優化 — 但那種規模是自行選擇加入(opt-in)的,而讓結構匹配意圖,是編排者(orchestrator)的第一個判斷。
預算驅動的規模調整
當使用者給了 token 目標(例如 prompt 裡的 +500k),腳本會看到一個 budget 物件:budget.total(一個數字,未給目標時為 null)、budget.spent() 和 budget.remaining()。這個池由主迴圈與該回合內所有 workflow 共享,而目標是一個硬上限 — 一旦花費達到總額,後續的 agent() 呼叫會拋出例外。所以別把預算當裝飾:在迴圈邊界檢查 remaining(),優雅地停止探索,並記錄你放棄的輪次。一個在半途因例外而死掉的 workflow 什麼都回報不了;一個提前退出並記錄上限的 workflow,仍能回報它確認過的一切。
實作範例
以下是組合後的腳本:多模式搜尋者、針對 seen 去重、三視角評審團、循環直到連續兩輪枯竭 — 附帶預算防護,而且沒有任何無聲上限。
export const meta = {
name: 'exhaustive-review',
description: 'Multi-modal bug hunt that loops until dry, judged by a three-lens panel',
phases: [{ title: 'Hunt' }, { title: 'Report' }],
};
const findingSchema = {
type: 'object',
properties: {
findings: {
type: 'array',
items: {
type: 'object',
properties: {
file: { type: 'string' },
summary: { type: 'string' },
},
required: ['file', 'summary'],
},
},
},
required: ['findings'],
};
// Multi-modal sweep: four DIFFERENT search strategies, not four clones.
const modes = [
'Sweep the changed files one by one for logic errors',
'Trace data flow across module boundaries for contract mismatches',
'Follow each changed function to its call sites and check the callers',
'Inspect error paths and edge cases the changes touch',
];
const lenses = ['correctness', 'security', 'performance'];
const seen = new Set(); // every finding ever reported — confirmed or not
const confirmed = [];
let dryRounds = 0;
let round = 0;
phase('Hunt');
while (dryRounds < 2) {
round += 1;
// Budget-driven scaling: exit before the hard ceiling makes agent() throw.
if (budget.total !== null && budget.remaining() < budget.total * 0.15) {
log(`Budget cap: stopped discovery after round ${round - 1}; later rounds dropped.`);
break; // no silent caps — the report will say discovery was bounded
}
const known = [...seen].join('\n');
const batches = await parallel(modes.map((mode, i) => () =>
agent(
`${mode}. Report bugs in the current diff of this repository.\n` +
`Do NOT repeat findings already known:\n${known}`,
{
label: `finder-r${round}-m${i + 1}`,
phase: 'Hunt',
model: 'haiku', // mechanical fan-out: cheap tier,
effort: 'low', // low effort
schema: findingSchema,
},
),
));
// Dedup against SEEN, not confirmed — otherwise judge-rejected
// findings get "rediscovered" every round and the loop never dries.
const fresh = batches
.filter(Boolean)
.flatMap((b) => b.findings)
.filter((f) => {
const key = `${f.file} :: ${f.summary}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
if (fresh.length === 0) {
dryRounds += 1;
log(`Round ${round}: dry (${dryRounds} consecutive).`);
continue;
}
dryRounds = 0;
log(`Round ${round}: ${fresh.length} new candidates.`);
// Perspective-diverse judge panel: three lenses per finding, each
// prompted to REFUTE. Majority refutation kills the finding.
const verdicts = await parallel(fresh.map((f) => async () => {
const votes = await parallel(lenses.map((lens) => () =>
agent(
`You are a skeptical reviewer. Lens: ${lens}.\n` +
`Assume this finding is WRONG and try to refute it:\n` +
`${f.file}: ${f.summary}`,
{
label: `judge-${lens}`,
phase: 'Hunt',
model: 'fable', // judgment stage: top tier,
effort: 'high', // high effort
schema: {
type: 'object',
properties: {
refuted: { type: 'boolean' },
reason: { type: 'string' },
},
required: ['refuted', 'reason'],
},
},
),
));
const refutals = votes.filter(Boolean).filter((v) => v.refuted).length;
return refutals >= 2 ? null : f;
}));
for (const f of verdicts.filter(Boolean)) confirmed.push(f);
// A completeness critic would slot in here: ask "what's missing?"
// and feed uncovered categories into the next round's finder prompts.
}
phase('Report');
log(`Confirmed ${confirmed.length} of ${seen.size} candidates seen.`);
await agent(
'Write the final review report, ordered by severity, for these ' +
'confirmed findings:\n' +
confirmed.map((f) => `- ${f.file}: ${f.summary}`).join('\n'),
{ label: 'synthesizer', phase: 'Report', effort: 'high' },
);
逐一檢視這些承重的設計選擇:
seen與confirmed是分開的集合。 去重鍵在發現一出現時就進入seen;confirmed只在評審團投票之後才會增長。把兩者合併,迴圈就永遠無法誠實地終止。- 終止條件是
dryRounds < 2— 連續兩輪零新發現。一輪枯竭可能是運氣;兩輪就是證據。 - 搜尋者跑在
haiku上、effortlow;評審跑在fable上、efforthigh。 機械式扇出用便宜的模型,驗證用最高層級 — 也就是第 25 堂課的層級混用模式。綜合者完全省略model,直接繼承 session 模型,這是你不確定其他層級是否合適時的正確預設。 parallel()裡拋出例外的 thunk 會解析為null,而被跳過或最終失敗的agent()也回傳null— 所以每個收集步驟在使用前都跑.filter(Boolean)。一個崩潰的評審變成一張缺席的票,而不是一個崩潰的 workflow。- 兩個上限都有記錄。 預算退出與最後那行
confirmed ... of ... seen,意味著這份報告的覆蓋範圍光靠執行日誌就能稽核。 - 並行數已經替你處理好。 每輪啟動 4 個搜尋者,接著最多
3 × fresh個評審;超過每個 workflow 上限 min(16, CPU 核心數 − 2) 的部分會直接排隊,而 1,000 個 agent 的生涯上限則為拒絕枯竭的迴圈提供最後防線。
前後對比
| 天真的扇出 | 品質模式組合 |
|---|---|
| N 個 agent、跑一輪、全部合併 | 一輪接一輪,直到搜尋證明枯竭 |
| 發現照單全收 | 發現得在反駁團前存活,否則出局 |
| 同一種搜尋的 N 個複製體 | 正交的搜尋模式互補盲點 |
| 完整性靠假設 | 完整性由批評者拷問 |
| 上限與抽樣不可見 | 每個邊界都有記錄;覆蓋範圍可稽核 |
| 預算超支讓執行半途崩潰 | 在迴圈邊界檢查預算;優雅、有記錄地退出 |
| 每種需求都用同一種結構 | 結構隨「找 bug」與「徹底稽核」調整規模 |
關鍵洞見
答案品質是 harness(執行框架)的結構性屬性,而不只是模型的屬性。
同一個模型,嵌在不同的結構裡,就產出不同品質的答案。一個被問「找出 bug」的 agent 產出貌似合理的輸出。把同一個模型拆成用四種不同方式搜尋的搜尋者、被迫反駁的懷疑者、必須證明枯竭的迴圈,以及承認每個上限的日誌,產出的就是你能辯護的結果 — 因為每個主張都挺過了一次攻擊,而每個缺口都記錄在案。
這正是這些模式擁有正式名稱的原因。它們不是 prompt 技巧;它們是架構。當需求配得上這個規模時,你伸手取用這些模式,就像後端工程師伸手取用 retry-with-backoff(帶退避的重試)一樣 — 不是每次事故都重新發明,而是依需要組合。
下一堂課
第 29 堂課涵蓋 Persistent Memory(持久記憶) — 讓 Claude Code 跨 session 攜帶精選知識的檔案式記憶目錄:一個事實一個檔案、一份 MEMORY.md 索引、四種記憶類型,以及讓回憶保持可信的清潔規則。