マルチエージェント・オーケストレーションのパターン:キュー、状態、ハンドオフ
信頼できるマルチエージェントシステムは、巧妙なプロンプトで決まるものではなく、退屈な分散システムの規律で決まる。エージェント間に永続的なキューを置き、状態をモデルの外で保持し、リトライしても二重実行されない冪等なハンドオフを作る。モデルはワーカー、キューはバックボーンだ。
毎週水曜。28,400人以上の読者。無駄なし。
✓ メールをご確認ください — 確認リンクをクリックして登録を完了してください。
✓ 登録が完了しました!
✓ すでに登録済みです。
目次
2026年6月更新。
TL;DR: 信頼できるマルチエージェントシステムは、巧妙なプロンプトで勝ち取るものではなく、退屈な分散システムの規律で勝ち取るものだ。エージェント間に永続的なキューを置き、状態をモデルの外で保持し、リトライが二重に動作しないようすべてのハンドオフを冪等にする。モデルはワーカー、キューはバックボーンだ。この3つを正しく押さえれば、オーケストレーションは怖いものではなくなる。
オペレーターの視点: 私の100以上あるエージェントのほとんどは単一ステップだ。そうでないもの — 分類し、次にエンリッチし、次にアクションするパイプライン — が信頼できるようになったのは、「プロンプトチェーン」と考えるのをやめ、「LLMワーカーを持つジョブキュー」と考え始めてからだった。これはアーキテクチャであって、プロンプトエンジニアリングではない。
「マルチエージェント」というと、エージェント同士が会話するように聞こえる。実際には、信頼できる版はその逆だ。エージェントは直接やり取りなど一切しない。キューにメッセージを置き、キューから仕事を拾い、オーケストレーションはその間の配管に宿る。本番で持ちこたえるパターンを紹介する。
パターン1:すべてのエージェントの間に永続的なキューを置く
最初の本能は、エージェントAの中からエージェントBを直接呼び出すことだ。やめておこう。直接呼び出しは両者を結合する。Bが遅ければAはブロックされ、Bが失敗すればAの仕事は失われ、BをスケールしたくてもAに手を入れずにはできない。
代わりに、Aは自分の仕事を終えてBのためにメッセージをキューに入れる。Bは独立したワーカーで、自分のペースでキューを排出する。
// エージェントAが終了し、キュー経由でハンドオフする — Bへの直接呼び出しはない
await env.ENRICH_QUEUE.send({
traceId,
type: "enrich",
payload: classifierResult,
});
// Aのジョブは完了。Bが独立してこれを拾う。Cloudflareでは、まさにこのためにWorkers Queuesを使っている — 私が使うエージェントスタックの背後にあるのと同じプリミティブだ。キューは4つのものを無料で与えてくれる。バッファリング(Bがダウンしていても仕事を失わない)、リトライ(失敗したメッセージは再配信される)、バックプレッシャー(急増はクラッシュせずキューに溜まる)、疎結合(Aに手を入れずにBをスケールまたは再デプロイできる)。これらはどれも、さもなければ自前で作って間違える羽目になるものだ。
パターン2:状態は常にモデルの外で保持する
最も一般的なマルチエージェントのバグは、モデルがステップ間で何かを覚えていると思い込むことだ。覚えていない。モデルの各呼び出しはステートレスで、唯一の記憶はプロンプトに入れたものだけだ。だから「このジョブがパイプラインのどこにいるか」の信頼できる情報源は、会話ではなくデータベースに宿らなければならない。
私は、すべてのエージェントが読み書きする単一のジョブレコードを保持する。
interface JobState {
traceId: string;
stage: "classified" | "enriched" | "acted" | "done" | "failed";
data: Record<string, unknown>;
attempts: number;
updatedAt: number;
}各エージェントは同じループを回す。ジョブの状態を読み、自分の仕事をし、新しい状態を書き、次のステージをキューに入れる。モデルは状態を保持せず、関連する切片を入力として受け取り、結果を返す。これがシステムを再起動可能にする。ワーカーがジョブの途中で死んでも、状態レコードは物事がどこにあったかを正確に示し続け、再配信されたキューメッセージがそこから引き継ぐ。デバッグも扱いやすくなる。状態テーブルがすべてのジョブの旅路をクエリ可能な記録として残すからだ — エージェントが本当に機能しているかをどう測るかと同じ計測のマインドセットだ。
パターン3:すべてのハンドオフを冪等にする
キューは少なくとも1回の配信を保証するのであって、ちょうど1回ではない。つまりメッセージは2回配信されうる — ネットワークの乱れ、リトライ、再デプロイ。エージェントのアクションが冪等でなければ、二重配信は二重実行する。確認メールが2通、予約が2件、課金が2回。これはオーケストレーションのバグの中で最もたちが悪い種類で、チームが本番で発見するものだ。
修正方法は、キーを使ってアクションを冪等にすることだ。
async function handleEnrich(msg: QueueMessage, env: Env) {
const job = await getJob(env, msg.traceId);
if (job.stage !== "classified") {
// このステージをすでに通過済み — これは重複配信。スキップする。
return;
}
const result = await enrich(job.data);
await advanceJob(env, msg.traceId, "enriched", result);
await env.ACT_QUEUE.send({ traceId: msg.traceId, type: "act" });
}ステージチェックによって、操作は2回実行しても安全になる。2回目の配信はジョブがすでに進んでいるのを見て何もしない。外部の副作用(メール送信、カード課金)については、下流のAPIに冪等性キーを渡し、そちらでも重複排除させる。すべてのメッセージは2回配信されると想定し、それが無害になるよう設計しよう — いずれ必ずそうなるからだ。
パターン4:オーケストレーター対コレオグラフィー — 意図的に選ぶ
フローを配線する方法は2つあり、正しい選択は複雑さによる。
コレオグラフィー(私のデフォルト):各エージェントは次のステップだけを知り、それをキューに入れる。フローはチェーンから創発する。シンプルで分散的、拡張しやすい — キューを挿入するだけでステージを追加できる。欠点は、フロー全体を記述する単一の場所がないため、複雑なパイプラインは見通しが悪くなりうることだ。
オーケストレーション(中央コーディネーター):1つのオーケストレーターがフローを所有し、各エージェントを順に呼び出し、結果に基づいて次を決める。フロー全体が読みやすい1か所に宿り、分岐ロジックは明示的だ。代償は、それ自体が永続的でなければならない中央コンポーネントだ — オーケストレーター自身の状態が外部化されていなければ(パターン2)、それが単一障害点になる。
私のルール:分岐が複雑になるまではコレオグラフィー、その後は永続的なオーケストレーター。 線形の3ステージのパイプラインはコレオグラフィーだ。条件付きルーティング、並列のファンアウト、ジョインを持つフローには、クラッシュ後に再開できるよう状態がデータベースに宿るオーケストレーターが必要だ。
パターン5:断片を失わずにファンアウト・ファンイン
1つのジョブがN個の並列サブタスクを生み(50レコードをエンリッチ、20文書を要約)、続行前にそのすべてを待つ必要があるとき、ジョインが必要だ。コツはジョブ状態のカウンターだ。
- 親はN個の子メッセージをキューに入れ、ジョブレコードに
expected: N, completed: 0を書く。 - 各子は自分の仕事をし、
completedをアトミックにインクリメントする。 completedをexpectedと等しくまで押し上げた子が、次のステージをキューに入れる。
このアトミックなインクリメントが要だ — これがないと、同時に終わる2つの子が両方とも自分は最後ではないと思い込み、ジョインは決して発火しない。データストアがアトミックにインクリメントできるカウンター、またはトランザクションを使う。このパターンにより、パイプラインの高コストな中間部分を並列化しながら(多くの場合Haikuで安く済む仕事 — HaikuとSonnetのコスト計算を参照)、最後にクリーンなジョインを保てる。
私が省くもの
これらのどれをやるにも、重量級のエージェントフレームワークは要らない。キュー、状態テーブル、冪等性キーは、どのプラットフォームにもすでにあるプリミティブだ。キューが無料でくれる機能を得るために手の込んだマルチエージェントフレームワークに手を伸ばし、置き換えた配管よりデバッグの難しいブラックボックスを抱え込むチームを見てきた。退屈なプリミティブから始めよう。フレームワークに手を伸ばすのは、それが解決する具体的な痛みを感じたときだけにしよう。
まとめ:エージェントはステートレスなワーカー、キューは永続的なバックボーン、状態はデータベースに宿り、すべてのハンドオフは2回実行しても安全。これがすべてだ。
よくある質問
エージェントは互いを直接呼び出すべきか、それともキューを経由すべきか?
キューを経由する。直接呼び出しはエージェントを結合する — 一方の失敗や遅延が他方に伝播し、独立してスケールや再デプロイができない。永続的なキューは、バッファリング、リトライ、バックプレッシャー、疎結合を無料で与えてくれる。
マルチエージェントの状態はどこに宿るべきか?
モデルの外、データベースの中に、各エージェントが読み書きするジョブレコードとして。モデルの呼び出しはステートレスなので、パイプラインの進捗の信頼できる情報源は外部でなければならない — それがクラッシュ後にシステムを再起動可能にする。
同じジョブに対してエージェントが二重に動作するのをどう防ぐか?
ハンドオフを冪等にする。アクションの前にジョブのステージをチェックし、すでに進んでいれば何もせず、外部APIには冪等性キーを渡す。キューは少なくとも1回配信するので、すべてのメッセージが2回到着しうると想定し、重複が無害になるよう設計しよう。
マルチエージェントフレームワークは必要か?
たいていは不要だ。永続的なキュー、状態テーブル、冪等性キーがあれば、プラットフォームがすでに提供するプリミティブで本番のニーズの大半をカバーできる。フレームワークを採用するのは、それが固有に解決する具体的な問題にぶつかったときだけにし、デフォルトでは採用しない。
毎週水曜。28,400人以上の読者。無駄なし。
✓ メールをご確認ください — 確認リンクをクリックして登録を完了してください。
✓ 登録が完了しました!
✓ すでに登録済みです。
AIプレイブックをメールでお届け
毎週水曜。28,400人以上の読者。無駄なし。
メールをご確認ください。
確認メールをお送りしました — リンクをクリックして登録を完了してください。1分以内に届かない場合は迷惑メールをご確認ください。
登録が完了しました。
ようこそ — 次号がまもなくお手元に届きます。
すでに登録済みです — 毎週水曜日にお届けします。