レッスン3: 並列化: セクショニングと投票
学習目標:
- 並列化の2つの変種——セクショニングと投票——を区別し、それぞれが何を解決するかを理解し、「出力はプログラム的に集約される」が引く境界を尊重する
Promise.allと自前の同時実行プールでセクショニングを実装し、集約がペイロードではなく参照を渡すようにする
- ファンアウトの3つのコスト(結果がコンテキストを溢れさせること、実プロダクトの同時実行の上限、投票のN倍のトークン)を計算し、それを使って、ある案を並列化すべきかを判断する
前提: レッスン1と2を修了し、レッスン2のrunAgent()ラッパーを持っていること | 前: << レッスン2: つなぐ、振り分ける: チェイニングとルーティング | 次: レッスン4 >>
12本のドキュメント、1本のチェーン、1時間の行列
レッスン2のチェーンはもう動きます: アウトライン → ゲート → 草稿 → ゲート → 用語点検。レビュー向けのプロンプトに差し替えても——要点を抽出し、修正を提案し、用語を確かめる——チェーンの形は変わりません。1本のドキュメントで走らせると6〜7分です。
そこへプロダクトチームがディレクトリを1つ机に置いていきます: ドキュメント12本、それぞれに同じレビューを1周かける必要があります。
あなたはforループを書き、走らせて、コーヒーを淹れに行きます。1時間後に戻ります。ログはドキュメント9で止まっています。ドキュメント10が要点を抽出中です。
その1時間、機械はほとんどの時間を待つことに使いました。ドキュメント2のリクエストを送る前に、ドキュメント1のAPI応答を待つ。ドキュメント3の番が来る前に、ドキュメント2が4ステージ全部を終えるのを待つ。実際的な問いを1つ: ドキュメント3のレビュー結論は、ドキュメント2の結果の一語に依存していますか?
していません。12本は独立したドキュメントです。そのレビューレポートは、誰が先に終わるかを気にしません。チェイニングでは、待つことに理由がありました——次のステップの入力が前のステップの出力だからです。ここにはそんな理由はありません。この12回の実行が行列に並んでいるのは、forループが並ばせたからにすぎません。
このシリーズのコース6は、ファンアウト・集約の協調パターンと、多視点の投票がどう働くかをすでに扱いました1。このレッスンはそれをコードに変え、帳尻を合わせます: ファンアウトはタダではありません。速さは本物ですし、コストも本物です。
定義: 同時に走らせられて、出力はプログラム的に集約される
原文の言い回しから始めましょう。LLMは、ときにタスクに対して同時に働き、その出力をプログラム的に集約させることができます。このワークフローが並列化であり、2つの主要な変種を持ちます1:
- セクショニング: タスクを、並列に走る独立したサブタスクへ分けること1。12本のドキュメントのレビューはセクショニングです。
- 投票: 同じタスクを複数回走らせて、多様な出力を得ること1。3つの視点にそれぞれ同じコピーを評価させるのが投票です。
使いどころ: 分けられたサブタスクを速度のために並列化できるとき、あるいはより高い確信度の結果を得るために複数の視点や試行が必要なとき1。飛ばされやすいけれど価値のある補足が1つあります——複数の考慮事項を持つ複雑なタスクでは、それぞれの考慮事項を別々のLLM呼び出しが扱い、個々の側面に注意を集中させたほうが、LLMは一般により良い性能を出します1。言い換えると、セクショニングと投票は単なる時短ではありません。「法務、セキュリティ、ブランド」を1つのプロンプトに詰め込むのと、3回の呼び出しがそれぞれ1つを見るのとでは、品質が違います。
もう半文、釘を打っておきます: 出力はプログラム的に集約される。ファンアウトした結果が戻ってきたあと、判定、絞り込み、要約を行うのはあなたのコードです——12本のレポートを全部読んでサマリーを書く、もう1回のモデル呼び出しではありません。モデルに集約させるのは別のパターンです。レッスン4のオーケストレーターがまさにその仕事をします。ここで線をはっきり引いておいてください。現行のClaudeプラットフォームのドキュメントは、マルチエージェントオーケストレーションの下に並列化を挙げています: 独立したサブタスクを同時にファンアウトし(複数のソースを検索する、別々のファイルを解析する)、コーディネーターに結果を統合させる2——あちらのバージョンでは集約をコーディネーターが行い、このレッスンはプログラム的な集約のバージョンを書いていることに注意してください1。同じ言葉でも、誰が集約するかは別のことです。
セクショニング: forループをPromise.allへ取り替える
直列版はこうなり、合計時間は12本の総和です:
セクショニング版は1行変えるだけで、合計時間はいちばん遅い1本に近づきます:
runAgent()はレッスン2と同じラッパーの位置に相変わらず座っています——1つの完全なハーネスループで、内部でstop_reasonによって分岐します。このレッスンのスタブ応答はどれも1ターンで完結する(end_turn)ので、最後のスクリプトのバージョンでは簡略化としてツールの分岐を省いています。本物のclientにつなぐときやツールが要るときは、レッスン2のツールを捌くバージョンをそのまま戻してください。並列化は、このループ自体の行を1つも変えません。ただ、これらのループを行列に並ばせるのをやめるだけです。
集約は次の行で、このコードが行います:
この3行に2回目のモデル呼び出しは含まれていません。filter、reduce、しきい値の判定——すべて決定的なコードです。同じ12本のレポートが入れば、毎回同じ1行の結論が出ます。それが集約をコードに置いておくことの利益です: ファンアウトした12回の呼び出しは非決定的で、マージのステップは決定的。何かが壊れたとき、どちら側を疑えばよいか分かります。
Promise.allには先に知っておくべき気性があります: どれか1つのpromiseがrejectするとawait全体がrejectし、他の11本が終わっていてもその結果を受け取れません。12本のドキュメントをレビューして、7本目が500を踏むとバッチ全体が無駄になります——他の11本は空回りです。そのコストは理不尽です。Promise.allSettledに切り替えるか、このレッスンの演習のように、各ワーカーをtry/catchで包んで失敗を記録として集めてください——ファンアウトの各経路は、それぞれ独立に失敗できるべきです。
並列化が速度だけの話ではない理由
並列化が「同じことがより早く終わる」だけなら、それはパフォーマンスの小技であって、1レッスンを割く値打ちはありません。本当の理由はコンテキストの側にあります。
Anthropicのマルチエージェント・リサーチシステムの振り返りは率直です: 検索の本質は圧縮であり、膨大なコーパスから洞察を蒸留することだ。サブエージェントは自分自身のコンテキストウィンドウを持って並列に動作することで圧縮を助け、問いの異なる側面を同時に探索してから、最も重要なトークンをリードのリサーチエージェントのために凝縮する。各サブエージェントはまた関心の分離をもたらす——別々のツール、プロンプト、探索の軌跡——それが経路依存性を減らし、徹底的で独立した調査を可能にする3。
この2文を分解しましょう。ファンアウトが買っているものは、少なくとも3つあります:
- ウィンドウの容量。彼らはこのアーキテクチャ上の判断を結論として書いています: 別々のコンテキストウィンドウを持つエージェントへ作業を分配することが、並列の推論のための容量を足す3。レッスン1で扱ったとおり、本当に天井にぶつかっているのはウィンドウのサイズではなく、「1つのループ」という形です。ファンアウトは、ウィンドウを引き伸ばすのではなく、いくつも開くことで単一ウィンドウの限界を回避します。
- 関心の分離。違うツールとプロンプトを携えた3つのサブエージェントは、自然と互いを汚染しません。
- 経路依存性の低下。1つのループの中では、ステップ3の判断がステップ2の言い回しに引っ張られます。独立した3本の軌跡は、同じ偏りを共有しません。
現行のプラットフォームのドキュメントも同じ方向を示します: 複数のエージェントが自分の分離されたコンテキストを持って並列に動けることは、出力の品質の向上を助け、完了までの時間の改善にもつながりうる2。品質が先に来ていることに注目してください。
速度の側では数字が示されていますが、一緒に写し取らなければならない文脈がついています: 彼らの初期のエージェントは逐次的な検索を実行しており、それは痛いほど遅かった。速度のために、2種類の並列化を導入した: (1) リードエージェントが3〜5のサブエージェントを直列ではなく並列に立ち上げる、(2) サブエージェントが3つ以上のツールを並列に使う。これらの変更は複雑なクエリでリサーチの時間を最大90%削減した3。
この数字は、3つの限定条件と一緒に使わなければなりません: これはレイテンシの数字であって品質の数字ではない。複雑なクエリに限られる(単純なクエリには並列化する余地があまりない)。彼ら自身のシステムから来ている。forをPromise.allへ取り替えてどれだけ短縮できるかは、あなたのサブタスクのどれだけが本当に独立か、各経路がどれだけ遅いか、同時実行がどこでボトルネックになるかに依存します——このレッスンの残りはその話です。
1つ目のコスト: 集約が、節約したコンテキストを食い返す
ファンアウトするとき、誰もが「一度に何本走るか」を見ます。事故はたいてい帰り道で起きます。
Claude Codeのサブエージェントのドキュメントはこのコストをテーブルに載せています: サブエージェントが完了すると、その結果はメインの会話へ返る。多くのサブエージェントがそれぞれ詳細な結果を返すと、大量のコンテキストを消費しうる4。サブエージェントが存在するのはメインの会話のコンテキストを守るためです——探索と実装をメインの会話の外に置くこと4——しかし返ってくるものが重すぎた瞬間、保護は裏返ります。
同じ振り返りが処方も与えており、しかも具体的です: サブエージェントにすべてをリードエージェント経由で伝えさせるのではなく、専門化されたエージェントが独立に永続する出力を作れるアーティファクトのシステムを実装すること。サブエージェントはツールを呼んで自分の作業を外部のシステムへ保存し、それから軽量な参照をコーディネーターへ返す3。
これをコードを書くときの合言葉に翻訳すると: ペイロードではなく参照を渡す。
差はどれくらいでしょうか。このレッスンの演習のスクリプトが実際の数字を出してくれます: 8本の産出がディスク上で合計1,951バイト、集約へ返るのは731バイトだけで、しかもこの比はドキュメントが長くなるほど急速に開いていきます——レポートが10倍長くなっても、返るものは一行のサマリーとパス1つのままです。全文が必要な人は、パスをたどって読めばよいのです。
この道はついでに別のものも買ってくれます。ワークフローのドキュメントは、ランタイムが実行の進行につれて各エージェントの結果を追跡し、それが同一セッション内での再開を可能にすると述べています。多くの小さなエージェントへ作業をファンアウトするワークフローは、それゆえ1つの長いエージェントよりも多くの進捗を保全します5。出力が外側に着地し、1行ずつ記録が残る——記録を残す部分は、このシリーズのコース11の可観測性と同じ原理です。「途中で中断されてもゼロからやり直しにならない」部分は、コース9の「長時間タスクを中断に耐えさせる」の領分です。
2つ目のコスト: 同時実行は決して無制限ではない
Promise.all(docs.map(...))と書いた瞬間、あなたは実質「同時実行数 = 配列の長さ」と言っています。配列が12なら問題ありません。配列が200なら、話は別です。
実プロダクト3つの上限を見てください:
- Claude Code: デフォルトでは、セッション内で20のサブエージェントが走っているときにAgentツールでもう1つ立ち上げようとすると
Concurrent subagent limit reachedで失敗し、エラーメッセージはリトライしないようClaudeへ明示的に伝えます4。
- Claude Codeのワークフローランタイム: 最大16の同時実行エージェント、Claude Codeが使えるCPUが少なければそれ以下(CPUが制限されたコンテナの内側を含む)5。1回の実行あたり合計1,000エージェント5。
- Managed Agents: 最大25の同時スレッドがサポートされます。コーディネーターは名簿にある1つのエージェントの複数のコピーを呼び出すことができ、1つのエージェントに紐づく複数のスレッドを作れます2。
3つの異なるチーム、3つの異なる実装が、いずれも上限を設けており、しかもその数字は大きくありません。この事実そのものが教材です: 上限なしのファンアウトは最適化ではなく事故です。(レッスン4では実際の事故の事例を扱います——初期のエージェントは単純なクエリに50のサブエージェントを立ち上げていました3。そのときあなたは、「上限はいくつか」より「いくつ立ち上げるかを誰が決めるか」のほうが厄介だと気づくでしょう。)
いちばん安上がりな絞り方はバッチ処理です:
動きますが、バケツ効果があります: 各バッチは、いちばん遅い1本が終わるのを待ってから次を始めます。3本のうち1本が特に長ければ、他の2経路はただ待ちます。
同時実行プールにはこの問題がありません——N本の「レーン」を固定し、各レーンは今の作業を終えるとすぐ共有カーソルから次の項目を取り、常にN本が飛んでいる状態にします:
10行ほど、依存なし。cursor++はシングルスレッドのJavaScriptでは安全です——2つのawaitのあいだの同期的なコードは中断されません。2本のレーンが同じインデックスを取る危険はありません。演習ではtry/catchを足して、1経路の失敗がバッチ全体を巻き添えにしないようにします。
limitはいくつにすべきでしょうか。普遍的な答えはありません。あなたのAPIのクォータ、下流のサービスの容量、1経路の所要時間の交点です。しかし、具体的な数字を書き込むことと書き込まないことは、2種類の別のエンジニアリングです。
3つ目のコスト: 投票はN倍のトークンを払う
投票の定義は一文です: 同じタスクを複数回走らせて、多様な出力を得ること1。コードは短いです:
ここでの集約も、やはりコードが行います——filterとしきい値です。何票でしきい値とするかはプロダクトの判断であり、ハードコードされ、いつでも変えられ、監査できます。モデルの即興に委ねるべきではありません。高リスクのシナリオはしきい値を「1票で拒否権」に寄せられますし、低リスクのシナリオは3票そろわないと止めない、にできます。
会計は素直です: N回投票すれば、N倍のトークンを払う。このお金をレッスン1の倍率の隣に置いてください——彼らのデータでは、エージェントはチャットのやり取りの約4×のトークンを使い、マルチエージェントシステムは約15×です。だからマルチエージェントシステムは、この性能の向上の対価を賄えるほど価値の高いタスクを必要とします3。3視点の投票の値札は、単一エージェントのあの4×を、さらに3倍したものです。15×を3倍しないでください——あの15×にはすでにファンアウトぶんの会計が含まれています。
だから投票は「安心のためにもう何回か走らせる」ではありません。具体的な何かを買わなければなりません。ワークフローのドキュメントはもっとはっきりしています: プランをコードへ移すことは、ワークフローに、ただエージェントを増やすのではなく、再現可能な品質のパターンを適用させます——独立したエージェント同士に、報告される前に互いの発見を敵対的にレビューさせたり、いくつもの角度からプランを起草してそれらを互いに突き合わせて評価させたりできるので、1回きりのパスより信頼できる結果が得られます5。
「独立したエージェントが互いをレビューする」は、このシリーズのコース10と同じ規則です: 作業者は自分の仕事を判定しない。同じコンテキストの中でモデルに自己点検させれば、たいていは自分がたった今出力したものを擁護します。独立したコンテキストの経路に切り替え、プロンプトを切り替えて、はじめて本当に問題を捕まえられるかもしれません。投票と相互レビューが買っているのは「多数決」ではなく、独立性です。
境界を1つ書き留めておきます: 1つのモデルへの3回の呼び出しは、3人の独立した審判ではありません。同じ訓練由来の偏りを共有しています。投票はサンプリングのノイズと、1回きりのパスで注意が抜ける穴をふるい落とせます。系統的な偏りはふるい落とせません。投票さえすれば真理が出てくる仕組みとして扱わないでください。
物差し: 独立性は前提条件であって、オプションではない
このレッスンの利益はすべて、繰り返し現れてきた1つの前提の上に載っており、取り出しておく値打ちがあります: サブタスクが本当に互いに依存していないこと。
Claude Codeのドキュメントは、複数のサブエージェントが同時に調査する話をするとき、一文をわざわざ足しています: 各サブエージェントは自分の領域を独立に探索し、それからClaudeが発見を統合する。これは調査の経路が互いに依存していないときに最もうまく働く4。逆向きの条件はマルチエージェントの振り返りに書かれています: すべてのエージェントが同じコンテキストを共有する必要がある領域や、エージェント間に多くの依存関係がある領域があり、それらの領域は今日のマルチエージェントシステムには向いていない。たとえば、ほとんどのコーディングタスクはリサーチよりも本当に並列化できるタスクが少なく、LLMエージェントは他のエージェントへのリアルタイムの調整と委任がまだ得意ではない3。
ある案を並列化すべきかを判断するには、1つ問いかけてください: 経路2は、何をすべきかを知るために経路1の結論を待つ必要がありますか?
- 待つ必要がある → これは並列化の形ではありません。前のステップの出力が次のステップの入力——それはレッスン2のチェイニングです。
- 待つ必要がない → セクショニング。
- 同じことについて、独立した判断をいくつか欲しい → 投票。
1つ目のケースがいちばん曖昧になりがちです: 依存があるのに「たぶんうまくいくだろう」で無理にファンアウトしてしまう。結果として、いくつものエージェントの経路が互いを知らないまま、それぞれの結論を書きます。集約のところであなたが手作業で矛盾をならすことになり、節約した時間はまるごとならすのに使われ、おまけに余分なトークンも払っています。
境界: このレッスンの分解はあらかじめ定義されている
言葉を1つ釘で留めておきます——それがレッスン4の入口です。
このレッスンのすべての例で、サブタスクを定義したのは誰か。あなたです。12本のドキュメントは、あなたがディレクトリから読み出しました。3つの視点は、あなたが配列にハードコードしました。コードが走る前に、何本の経路がファンアウトし、それぞれが何をするかはすべて決まっています。これをあらかじめ定義された分解と呼びます。
その反対は、何個に分けるか、それぞれが何をするかをモデルが決めることです。原典はこの違いを2つのパターンの決定的な分水嶺として扱っています——オーケストレーター・ワーカーのワークフローでは、中央のLLMが動的にタスクを分解し、ワーカーLLMへ委任し、その結果を統合します1。並列化と地勢的には似ていますが、決定的な違いはその柔軟性です——サブタスクはあらかじめ定義されておらず、具体的な入力に基づいてオーケストレーターが決めます1。
つまり2つの境界は「一度に何本走るか」ではなく、「あの配列を誰が書いたか」です。配列があなたのものなら、それはこのレッスン。配列がモデルによってその場で生成されるなら、それは次のレッスン。前のレッスンのルーティングは、すでに1つの決定(どの分岐か)をモデルへ渡しました。次のレッスンはもっと大きな塊を渡します: 分解そのものです。
💻 演習
まとめ
- 並列化の定義は「LLMは、ときにタスクに対して同時に働き、その出力をプログラム的に集約させることができる」であり、2つの変種はセクショニング(タスクを並列に走る独立したサブタスクへ分ける)と投票(同じタスクを複数回走らせて多様な出力を得る)です1
- その条件は、サブタスクを速度のために並列化できること、あるいはより高い確信度のために複数の視点と試行が必要なことです。複数の考慮事項を持つ複雑なタスクでは、それぞれを別々の呼び出しが注意を集中して扱ったほうが一般により良い性能が出ます1
- ファンアウトが買うのは速度だけではありません: 自分自身のコンテキストウィンドウを持って並列に動作するサブエージェントが探索し、最も重要なトークンを凝縮して返すことに加えて、関心の分離(別々のツール、プロンプト、探索の軌跡)と、より少ない経路依存性ももたらします3。別々のコンテキストウィンドウを持つエージェントへ作業を分配することが、並列の推論のための容量を足します3
- 速度向上の数字には文脈がついてきます: 彼らのResearchシステムは2階層の並列化を導入し(リードが3〜5のサブエージェントを並列に立ち上げ、サブエージェントが3つ以上のツールを並列に使う)、複雑なクエリでリサーチの時間を最大90%削減しました3——これは彼ら自身のシステムから出たレイテンシの数字であって、品質の数字ではありません
- 集約が1つ目のコストです: サブエージェントの結果はメインの会話へ返り、多くのサブエージェントがそれぞれ詳細な結果を返すと大量のコンテキストを消費します4。解決策はアーティファクトのシステムです——サブエージェントは出力を外部のシステムへ保存し、軽量な参照だけをコーディネーターへ返します3
- 同時実行は決して無制限ではありません: Claude Codeはデフォルトで20の同時サブエージェントで、超えると「リトライするな」と明示して失敗します4。ワークフローランタイムは最大16の同時実行エージェント(CPUが制限されていればそれ以下)をサポートし、1回の実行あたり1,000が上限です5。Managed Agentsは最大25の同時スレッドです2
- 投票のコストはN倍のトークンであり、あの倍率と一緒に置かなければなりません——彼らのデータでは、エージェントはチャットの約4×、マルチエージェントシステムはチャットの約15×です。タスクの価値がそれを賄えるほど高くなければなりません3。それが買うべきなのは再現可能な品質のパターンです。独立したエージェントが敵対的にレビューすること、あるいはいくつもの角度から起草してから突き合わせて評価することのような5
- 独立性が前提条件です: 複数のサブエージェントが同時に調査するのは、調査の経路が互いに依存していないときに最もうまく働きます4。共有コンテキストや多くの依存を必要とする領域は今日は向いていません3——依存があるなら、チェイニングの形へ戻ってください
- このレッスンの分解はすべてあらかじめ定義されたものです(配列を書いたのはあなたです)。サブタスクがあらかじめ定義されておらず、具体的な入力に基づいてオーケストレーターが決めることが、オーケストレーター・ワーカーと並列化の決定的な違いであり1、次のレッスンの主題でもあります
>> レッスン4: オーケストレーター・ワーカー: 分解そのものを動的にする