メインコンテンツへスキップ
モジュール 5:Fable 5 時代 4 / 8
上級 Session 28 Workflow Quality Verification Patterns

オーケストレーション品質パターン

敵対的検証、ジャッジパネル、loop-until-dry など、マルチエージェントの回答を「もっともらしい」から「網羅的」へ変える7つのワークフロー品質パターンを解説。

2026年7月8日 18 分で読む

学ぶこと

セッション 27では、Workflow ツールの仕組み — agent()pipeline()parallel()、スキーマ、resume — を学びました。このセッションで扱うのは、その仕組みを使って何を組み立てるかです。エージェントを20体起動しても回答は正しくなりません — 正しさを生むのは、ワークフローの構造です。

このセッションを終えると、次のことがわかります:

  • 7つの正典的な品質パターン:adversarial verify(敵対的検証)、perspective-diverse verify(視点多様化検証)、judge panel(ジャッジパネル)、loop-until-dry、multi-modal sweep(マルチモーダルスイープ)、completeness critic(網羅性クリティック)、no-silent-caps
  • loop-until-dry の重複排除を、確認済みの発見だけでなく見たものすべてに対して行わなければならない理由
  • ワークフローの構造を依頼の規模に合わせる方法と、トークンバジェットに実行をクラッシュさせるのではなく形作らせる方法
  • これらのパターンを1本の網羅的レビュースクリプトへ合成する方法

課題

素朴なワークフローは、N 体のエージェントをファンアウトし、回答を集めてマージします。一見、厳密そうです。しかしこれには、ファンアウトをいくら増やしても直らない3つの故障モードがあります:

  1. もっともらしいが間違った結果。 「これはバグですか?」と聞かれた単一のエージェントは、説得力たっぷりに「はい」と答えがちです。そんなレポートを12件マージしても、得られるのは自信満々の「かもしれない」が12件だけです。
  2. サイズ不明の探索。 バグが何個あるか分からないなら、1回のパスは網羅性について何も教えてくれません。「エージェントが戻ってきた」は「探索が終わった」と同義ではないのです。
  3. サイレントな切り捨て。 「上位10件の発見」だけを残したり、サブセットをサンプリングしたりするステージは、誰にも告げずにカバレッジを捨てています。欠落を示すものがレポートのどこにもないため、完全なレポートに見えてしまいます。

対策は構造的なものです。プロンプトの工夫で逃げ切ることはできません — 間違った発見が殺され、探索が自らの枯渇を証明し、すべての上限が可視化されるように、エージェントを配置するのです。

仕組み

Adversarial Verify(敵対的検証)

候補となる各発見に対して、N 体の独立した懐疑者(スケプティック)を起動します。そのプロンプトは、発見を反証すること — レビューでもスコアリングでもなく、間違いだと証明することです。過半数が反証すれば、その発見は死にます。

プロンプトの方向性が重要です。「これを検証して」と頼まれたエージェントは同意にアンカーしますが、「これはおそらく間違っている — 理由を示せ」と頼まれたエージェントは、実際に主張を再導出します。独立した懐疑者(それぞれ自分のコンテキスト内にいて、互いの存在を知らない)は、同じ間違いで結託できません。これが「もっともらしいが間違った結果」への直接の対抗策です。

Perspective-Diverse Verify(視点多様化検証)

その変種です。N 体の同一の反証者の代わりに、各検証者に異なるレンズ — 正しさ、セキュリティ、パフォーマンス、再現するか — を与えます。冗長性の一部をカバレッジと引き換えにするわけです。3体の同一の懐疑者が全員レースコンディションを見逃しても、並行性を明示的に割り当てられたレンズならそれを捕まえるかもしれません。問いが「この主張は真か?」なら同一の反証者を、「この主張は真か、しかもそれで全部か?」ならレンズを使いましょう。

Judge Panel(ジャッジパネル)

検証タスクではなく生成タスク(設計、修正、説明)向けのパターンです。異なる角度から N 回の独立した試行を実行し、並列のジャッジでスコアリングし、勝者に次点勢の最良のアイデアを加えたものから統合します。この統合ステップこそが「3つのうち一番良いものを選ぶ」との違いです — 敗れた試行には、勝者が見落とした洞察が1つ含まれていることがよくあります。

Loop-Until-Dry(枯れるまでループ)

サイズ不明の探索では、K 回連続のラウンドが何も新しいものを返さなくなるまで、ファインダーのラウンドを起動し続けます。この終了条件だけが、枯渇を示す正直なシグナルです — 固定のラウンド数は、ループの皮をかぶった当てずっぽうにすぎません。

微妙だが重要なルール:重複排除は、確認済みのものだけでなく、見たものすべてに対して行うこと。 ラウンド1で候補 X が見つかり、ジャッジがそれを却下したとしましょう。重複排除リストが確認済みの発見しか保持していなければ、ラウンド2のファインダーは X を再び「発見」し、ジャッジは再び却下し、ループは永遠に枯れません — ジャッジに却下された発見が延々と再登場するのです。seen セットは、その後の運命がどうであれ、報告されたすべての発見を記録しなければなりません。

Multi-Modal Sweep(マルチモーダルスイープ)

並列エージェントがそれぞれ異なる方法で探索します:コンテナ別、内容別、エンティティ別、時間別。同じ検索戦略を走らせる10体のエージェントは同じ盲点を共有しますが、直交する戦略を持つ4体のエージェントは互いの盲点をカバーし合います。コードレビューなら、1体のファインダーはファイルを1つずつ走査し、1体はモジュール境界をまたいでデータフローを追跡し、1体は変更された関数を呼び出し元まで追い、1体はエラーパスを検査する、といった具合です。

Completeness Critic(網羅性クリティック)

メインパスの後、最後のエージェントに1つの質問を与えます:「何が抜けている?」 その仕事は、さらなるインスタンスを見つけることではなく、カバーされていないカテゴリを見つけることです — 「誰もマイグレーションスクリプトをチェックしていない」「並行性を見たファインダーがいない」。その発見は、次の探索ラウンドの種になります。これはセッション 20のエラーリカバリの反射をワークフローレベルに引き上げたものです:プロセスはどこかで失敗したはずだと仮定して、確かめに行くのです。

No Silent Caps(サイレントな上限の禁止)

カバレッジがどこかで制限されているなら — 上位 N 件の選択、サンプリング、バジェットに強いられた早期終了 — 何が落とされたかをログに残すことlog() の1行はタダ同然ですが、見えない上限はレポート全体への信頼を代償にします。これはセッション 24でプロダクション構成に適用したのと同じ可観測性の規律を、ワークフロー自体に向けたものです。

依頼の規模に合わせたスケーリング

すべてのリクエストがフル装備に値するわけではありません。「この diff のバグを見つけて」は範囲の限られた依頼です:マルチモーダルスイープ1回と adversarial verify で釣り合いが取れます。「これを徹底的に監査して」は網羅性を求める依頼です:loop-until-dry、ジャッジパネル、網羅性クリティックこそが本題であって、やりすぎではありません。Ultracode(ウルトラコード)スタイルの実行は、最も安い答えではなく最も網羅的で正しい答えのために明示的に最適化されています — ただしその規模はオプトインであり、構造を意図に合わせることが、オーケストレーターの最初の判断なのです。

バジェット駆動スケーリング

ユーザーがトークンターゲット(プロンプト内の +500k など)を与えると、スクリプトからは budget オブジェクトが見えます:budget.total(数値。ターゲットが与えられなかった場合は null)、budget.spent()budget.remaining()。このプールはメインループとそのターン内のすべてのワークフローで共有され、ターゲットは**固い上限(ハードシーリング)**です — 消費が total に達すると、それ以降の agent() 呼び出しは例外を投げます。だからバジェットを飾りとして扱わないこと:ループの境界で remaining() をチェックし、落としたラウンドをログに残しながら、探索を穏やかに停止しましょう。ラウンドの途中で例外で死ぬワークフローは何も報告しません。ログ付きの上限とともに早期終了するワークフローは、確認できたものすべてをきちんと報告します。

ハンズオン

合成したスクリプトがこちらです:マルチモーダルなファインダー、seen に対する重複排除、3レンズのジャッジパネル、ドライラウンドが2回連続するまでのループ — バジェットガード付き、サイレントな上限なし。

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' },
);

要となる選択を順に見ていきましょう:

  • seenconfirmed は別々のコレクションです。 重複排除のキーは発見が現れた瞬間に seen に入ります。confirmed はパネルの投票を経て初めて増えます。この2つを1つに潰すと、ループは正直に終了できなくなります。
  • 終了条件は dryRounds < 2 — 新しい発見がゼロのラウンドが2回連続すること。1回のドライラウンドは運かもしれませんが、2回は証拠です。
  • ファインダーは haikulow effort で、ジャッジは fablehigh で動きます。 機械的なファンアウトには安価なモデルを、検証には最上位ティアを — セッション 25のティア混合パターンです。シンセサイザーは model を完全に省略してセッションモデルを継承します。別のティアが合うと確信できないときの、正しいデフォルトです。
  • 例外を投げた parallel() のサンク(thunk)は null に解決され、スキップされた、あるいは最終的に失敗した agent()null を返します — だからすべての収集ステップは、使用前に .filter(Boolean) を実行しています。クラッシュしたジャッジは、クラッシュしたワークフローではなく、欠けた1票になるのです。
  • 両方の上限がログに残ります。 バジェットによる離脱と最後の confirmed ... of ... seen の行によって、レポートのカバレッジは実行ログだけから監査可能になります。
  • 並行性は面倒を見てもらえます。 各ラウンドは4体のファインダーを起動し、その後最大 3 × fresh 体のジャッジを起動します。ワークフローごとの上限 min(16, CPU コア数 − 2) を超えた分は単にキューに入り、枯れることを拒むループに対しては、生涯上限の1,000エージェントが最後の防波堤になります。

何が変わったか

素朴なファンアウト品質パターンの合成
N 体のエージェント、1パス、すべてマージ探索が枯渇を証明するまでラウンドを継続
発見は報告されたまま受理発見は反証パネルを生き延びるか、死ぬか
同じ検索のクローンが N 体直交する検索モードが互いの盲点をカバー
網羅性は仮定するもの網羅性はクリティックに尋問されるもの
上限とサンプリングは不可視すべての制限をログに記録;カバレッジは監査可能
バジェット超過で実行中にクラッシュループ境界でバジェットをチェック;ログ付きの穏やかな離脱
どの依頼にも同じ構造「バグを見つけて」と「徹底的に監査して」で構造をスケール

キーインサイト

回答の品質は、モデルだけの性質ではなく、ハーネスの構造的な性質です。

同じモデルでも、埋め込まれた構造しだいで異なる品質の回答を生みます。「バグを見つけて」と頼まれた1体のエージェントは、もっともらしい出力を生みます。同じモデルを、4通りの方法で探索するファインダー、反証を強いられる懐疑者、枯渇を証明しなければならないループ、そしてすべての上限を白状するログへと分割すれば、擁護できる出力が得られます — すべての主張が攻撃を生き延び、すべてのギャップが記録に残っているからです。

これらのパターンに正典的な名前が付いているのは、そのためです。プロンプトの小技ではなく、アーキテクチャなのです。依頼がその規模を正当化するとき、バックエンドエンジニアがリトライ・ウィズ・バックオフに手を伸ばすのと同じように、パターンに手を伸ばします — インシデントごとに発明するのではなく、必要に応じて合成するのです。

次のセッション

セッション 29 では Persistent Memory(永続メモリ) — Claude Code がキュレートされた知識をセッションをまたいで持ち運べるようにする、ファイルベースのメモリディレクトリ — を扱います。1ファクト1ファイルの原則、MEMORY.md インデックス、4つのメモリタイプ、そして想起を信頼できるものに保つ衛生ルールを学びます。