Z4 / コマ1SEMINAR MATERIALS

座学 ③ / 11:05 – 11:45 / このコマの中心

マルチエージェント設計論

分けること自体は目的ではない。分けるとコストが増える(LLM要求数が増え、情報が伝言ゲームで劣化し、デバッグが難しくなる)。 それでも分けるのは、分けないと解けない問題があるから。

この座学では「なぜ分けるのか」を定量的な理由として押さえ、「どう分けるか」の設計判断を4つ扱う。 ここで得た語彙が、以降の改善サイクルと成果報告書の「設計」章の土台になる。

図と例について

このページの図や例に出てくる「偵察役/侵入役/昇格役/調査役」というサブエージェントの分け方は、説明のための一例 (委譲の起点となる Orchestrator だけは、テンプレート実装に実在する名前)。 実際にどう分けるか(そもそも分けるか、何体にするか、何と呼ぶか)は、テンプレート実装を読んだうえで各自が決める。 ここで身につけてほしいのは特定の構成ではなく、構成を決めるときの判断軸のほう。

1単一エージェントに全部やらせない理由

理由は3つ。コンテキスト長・責務分離・ツール選択精度。 どれも「なんとなく良さそう」ではなく、放置すると計測値として悪化する具体的な問題として出る。

理由 1 — コンテキスト長

座学②で見たとおり、履歴は毎ターン伸びる。 単一エージェントは偵察の生ログも、失敗した exploit のエラーも、内部偵察の一覧も、全部同じ1本の履歴に積む。 分割すると、各サブエージェントの履歴は仕事が終わった時点で捨てられる。親に返るのは結論だけ。

同じ標的を攻略したとき、コンテキストがどうなるか 0 / 16
単一エージェント
1本の履歴に全部を積む
判断の質 良好
マルチエージェント
Orchestrator が委譲し、サブの履歴は破棄される
判断の質 良好
再生すると、同じ作業を2つの構成でやったときのコンテキストの伸び方が並んで出る。
本質

マルチエージェントの正体は「コンテキストの使い捨て」。 サブエージェントは大量の生情報を読み込んで、結論だけを返し、途中の詳細は親に渡さない。 親のコンテキストには結論だけが積まれるので、長時間の作業でも必要な情報を保持しやすい。

裏を返せば:捨てた情報は二度と戻らない。何を返り値の型に含めるかが設計の全てになる。 ここが甘いと「サブは見つけていたのに親が知らない」という失敗が起きる。

理由 2 — 責務分離

プロンプトは長くなるほど、個々の指示が効かなくなる。 偵察・侵入・昇格の全てを1つのシステムプロンプトに書くと、数千トークンの「指示の塊」になり、どれも中途半端に効く状態になる。

単一の場合

「あなたはペンテスターです。まず偵察し、次に侵入し、それから昇格し、ただし〜のときは〜し、…」

指示が競合する。「網羅的に列挙せよ」と「早く侵入に移れ」は矛盾するが、 1つのプロンプトに両方書かれていると、LLM はその場の気分でどちらかを選ぶ。再現性が落ちる。

分けた場合

偵察役:「攻撃面の確定だけが仕事。侵入はしない。打ち切り理由を必ず書け」

各エージェントのプロンプトが短く、目的が1つになる。 矛盾する指示は、エージェント間の境界として表現される。 「いつ偵察をやめて侵入に移るか」は Orchestrator の判断になり、明示的に書ける。

本ゼミの役割分担がこの形になっている理由

担当A=偵察/外部調査、担当B=侵入、担当C=内部偵察/権限昇格。 責務が分離されているから、3人が同じリポジトリを同時に触ってもコンフリクトしない。 プロンプト・ツール・状態スキーマのオーナーが一意に決まる。設計上の分離が、そのまま作業分担になっている。

理由 3 — ツール選択精度

ツールは座学②で見たとおり、説明文込みで毎回 LLM に渡される。 選択肢が増えるほど、似たツールの取り違えと、そもそも呼ばない失敗が増える。

1体に持たせるツール数と、選択の失敗 ツール数 6
この図は概念図

曲線の形はモデル・ツールの説明文の質・タスクによって大きく変わる。数値は当てにしないこと。 重要なのは傾向 — ツールは足せば足すほど強くなるわけではないという点。 実際の数値は、コマ2以降で各自が測る。 「ツールを1つ足したら、他のツールの呼ばれ方がどう変わったか」を見るのが、コマ3以降の改善の基本動作になる。

2委譲の粒度

分けると決めたあと、どのくらいの大きさで切るか。細かすぎても粗すぎても壊れる。 3つの粒度を並べて、それぞれの壊れ方を見る。

粒度を選ぶ

粒度を決める判断基準

問いYES なら分けるNO なら分けない
大量の生情報を読む必要があるか 読み取り専用の一時的なコンテキストを別に持たせる価値がある。これが最も有力な分割理由 親が直接ツールを呼べばよい。委譲のオーバーヘッドが無駄
指示が親と矛盾するか 「網羅的に」vs「早く進め」のような競合は、境界にすると解決する 同じ方針で動くなら、同じプロンプトの中にいてよい
返り値を型で定義できるか 定義できる=責務が明確。ReconResult のように書ける 型にできない=そもそも仕事の定義が曖昧。分ける前に責務を考え直す
専用ツールが3つ以上あるか 親のツール一覧から隔離できる。選択精度が上がる 1〜2個なら親に持たせたほうが速い
失敗しても親が続行できるか 失敗を局所化できる。「このサブが死んでも別ベクタへ」が書ける 失敗が即座に全体の失敗なら、分けても回復性は上がらない
実務的な目安

「1回の委譲で 5〜30 ターン程度の作業をさせる」あたりが扱いやすい。 5ターン未満なら親が直接ツールを呼べばよく、30ターンを超えるとサブ自身のコンテキストが上限に近づき始め、 かつ途中経過が親から見えないブラックボックスの時間が長すぎる。 テンプレート実装がどの粒度で切っているかは座学④で確認する。

3サブエージェント間の直接相談を許すか

侵入役が「この製品の PoC を探したい」と思ったとき、調査役に直接聞きにいってよいか、Orchestrator を経由すべきか。 これは好みの問題ではなく、制御可能性と柔軟性のトレードオフ

トポロジの比較
テンプレート実装の立ち位置

テンプレート実装は 調査役への相談を軸に組まれている。 つまり「他のエージェントが調査役を呼べる」形の部分メッシュになっている。

ここには設計上の弱点が埋まっている。検索で有用な情報が見つからないときのフォールバックが設計されていないと、そこで停止する検索で出てこない対象に当たったとき何をするかは、どこかで必ず問われる。 構成を見直すタイミングで、ここをどうするかが議論の中心になるはず。

4失敗回復とループ検知

マルチエージェントでは、失敗が2つの層で起きる。サブエージェント内部の失敗(座学②の4層)と、 「サブに任せたが成果が返ってこない」という委譲レベルの失敗。後者の代表がループ。

ループ検知 — 実行トレースを流して、どの検知器が引っかかるか見る
EXECUTION TRACE
DETECTORS
実際のログでよく見る形。予算を使い切るまで止まらない。

検知したあと、何をするか

検知だけでは意味がない。検知後の行動を設計しないと、検知しても同じ場所で止まるだけになる。

ESCALATION 1

制約を追加して再委譲

tried にある手は禁止。別のアプローチで」と明示して同じサブを呼び直す。最も低コスト。まずこれ。

ESCALATION 2

別のサブに依頼する

侵入役に有効な候補が残っていなければ調査役に別角度で調べさせる、内部偵察に戻る。視点を変える。

ESCALATION 3

フェーズを戻す

侵入で有効なベクタが残っていなければ偵察に戻る。座学①で見た後戻りの経路がここで効く。

ESCALATION 4

打ち切って記録する

「どこで止まったか、なぜか」を構造化して残す。これが書けていれば、評価では十分な成果になる。

評価との関係

到達目標は全標的の攻略ではなく、「どこで止まったか、なぜか」を説明できること。 エスカレーション4がきちんと実装されていれば、止まったこと自体が測定可能な結果になる。 黙って予算を使い切って終わるのが最悪 — 何も分からないまま4時間が消える。

5予算配分

LLM 要求数には上限がある(コストと時間の両方で)。マルチエージェントでは 親が使った分+全サブが使った分が合計になるので、放っておくと1つのサブが全部使い切る。

1標的あたり 120 リクエストを、どう配るか
予算を分けて渡す — Pydantic AI では usage を引き継ぐ
from pydantic_ai.usage import UsageLimits

@orchestrator.tool
async def delegate_recon(ctx: RunContext[Deps], instruction: str) -> ReconResult:
    """偵察エージェントに委譲する。攻撃面の確定だけを依頼すること。"""
    budget = ctx.deps.budget.take("recon")        # 配分表から取り出す
    r = await recon_agent.run(
        instruction,
        deps=ctx.deps,
        usage=ctx.usage,                             # ← 親子で使用量を合算して数える
        usage_limits=UsageLimits(request_limit=budget),
    )
    ctx.deps.state.merge_recon(r.output)             # 共有状態へ反映
    return r.output                                 # 親の履歴に載るのは、この型の分だけ
3つの押さえどころ

usage=ctx.usage を渡す:渡し忘れると、サブの消費が親の上限に数えられず、合計が上限を大きく超える。
② 返り値は型で縛る:親の履歴に積まれるのは ReconResult の中身だけ。ここに何を入れるかが、サブの捨てた情報のうち何が生き残るかを決める。
③ 状態への反映と返り値は別物:詳細は共有状態に書き、親の履歴には要点だけ返す。これで「情報は残るが、コンテキストは伸びない」が両立する。

6設計判断は記録する

ここまでの5つは全てトレードオフのある判断で、正解は標的とモデルに依存する。 「何をどう決めたか」と「なぜそう決めたか」を残すのが、成果報告書の「システム構成」章と「考察」章の中身になる。

判断選択肢記録すべきこと
分割するか単一 / 分割分割した理由(どの制約に当たったか)。実測値があると説得力が増す
粒度粗い / 中 / 細かい1委譲あたりの平均ターン数。粒度を変えたときの成功率の変化
トポロジスター / 部分メッシュ / フルメッシュ直接相談を許した相手と、その理由。許さなかった相手と、その理由
ループ検知完全一致 / 正規化 / 進捗ベース検知した回数と、エスカレーション後に前進できた割合
予算配分固定 / 動的配分と、実際の消費分布。「どこで予算を消費したか」
改善サイクルにつながる

以降は「個別エージェントの改善」と「チーム構成そのものの設計」を行き来する時間。 節目でこの座学に立ち返り、動かして見えた実態と突き合わせることになっている。 「委譲の粒度は適切だったか」「責務が滲み出していないか」を3人で点検するので、 今日決めたことを、そのとき参照できる形で書いておくこと。


この座学の要点

1. 分割の正体はコンテキストの使い分け

サブは途中の詳細を親に渡さず、結論だけ返す。だから返り値の型が設計の全て。

2. 分割にはコストがある

要求数増・伝言ゲーム・デバッグ困難。理由なく分けない。

3. 判断は5つ

分けるか/粒度/トポロジ/ループ検知/予算配分。全部トレードオフ。

次の 座学④ では、ここで扱った設計判断が テンプレート実装で実際にどう実装されているかを突き合わせる。 コードを読むのではなく、設計判断の実例として読む。「なぜこの設計なのか」を議論しながら進める。