AI エージェント開発実践 #5 サブエージェントで仕事を分ける

読了 5分

第4回では、コンテキストを減らし、空け、要約しました。しかし、もう一つ根本的な解決策があります。そもそも一つのコンテキストにすべてを詰め込まないことです。仕事の一部を別のエージェントに切り出して結果だけを受け取れば、その仕事の途中経過はメインエージェントのコンテキストにまったく入りません。今回はサブエージェントを扱います。

なぜ分けるのか #

サブエージェントの利点は三つに整理できます。

  • コンテキストの分離 — 調査作業には、検索結果を数十件あさる過程がついてきます。サブエージェントがその過程を自分のコンテキストで済ませ、結論だけ報告すれば、メインエージェントは結論だけを受け取ります。第4回の技法が「積もったものを減らす」方法だとすれば、これは「積もらせない」方法です。
  • 役割の分離 — サブエージェントごとに別のシステムプロンプトと別のツール一覧を与えられます。調査担当には検索ツールだけ、執筆担当にはファイルツールだけ、という具合です。第2回で見た「ツール一覧は狭いほどよい」という原則を、役割の単位で実現するわけです。
  • 並列実行 — 互いに独立した作業なら、サブエージェントを複数同時に走らせられます。

delegate ツールを作る #

実装の核心は意外なほど単純です。サブエージェントは結局、別の messages 配列で回るもう一つのループです。第1回の run_agent をほぼそのまま使い、役割とツールをパラメータで受け取るようにします。

run_subagent.py
def run_subagent(system: str, tools: list, task: str, max_steps: int = 15) -> str:
    """独立したコンテキストで作業を行い、最終報告だけを返す。"""
    messages = [{"role": "user", "content": task}]
    for _ in range(max_steps):
        response = client.messages.create(
            model="claude-opus-4-8",
            max_tokens=16000,
            system=system,
            tools=tools,
            messages=messages,
        )
        if response.stop_reason == "end_turn":
            return next(b.text for b in response.content if b.type == "text")
        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": run_tools(response)})
    return "(サブエージェントがステップ上限内に作業を終えられませんでした)"

そしてこれをメインエージェントのツールとして公開します。

delegate_tool.py
{
    "name": "delegate_research",
    "description": (
        "調査専門のサブエージェントに調査作業を任せる。"
        "複数の文書を検索して読まないと答えが出ない質問なら、直接検索せずこのツールを使う。"
        "task には何を調べるかと報告形式を具体的に書く。"
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "task": {"type": "string", "description": "調査する内容と期待する報告形式"},
        },
        "required": ["task"],
    },
}

def delegate_research(task: str) -> str:
    return run_subagent(
        system="あなたは調査専門のエージェントです。依頼された内容を調査し、根拠とともに簡潔に報告してください。",
        tools=research_tools,   # 検索・読み取りツールのみ
        task=task,
    )

メインエージェントから見れば、サブエージェントはただのツール一つです。ループの構造は何も変わりません。

報告形式を契約にする #

サブエージェントでよくある失敗は、委任の段階ではなく報告の段階で起きます。メインエージェントはサブエージェントの最終テキストだけを受け取るので、そのテキストに必要な情報が抜けていれば、委任全体が無駄骨になります。だから task に報告形式を明記することが重要です。

task_with_contract.txt
2026年5月の返金ポリシー変更の内容を調査せよ。
報告形式:
- 変更の要点 (3行以内)
- 根拠となる文書のタイトルと場所
- 確認できなかったことがあれば明記

「確認できなかったことを明記せよ」という行が特に有用です。これがないと、サブエージェントは手ぶらのとき、もっともらしい推測で報告書を埋める傾向があります。

オーケストレーター・ワーカーと並列実行 #

サブエージェントが複数になると、メインエージェントの役割が変わります。自分で働く代わりに、仕事を分けて結果を統合するオーケストレーターになります。互いに独立した委任なら同時に実行できます。Claude が一つの応答で delegate_research を三回呼んだら、その三つの呼び出しをスレッドで並列処理する、という具合です。

parallel_delegation.py
from concurrent.futures import ThreadPoolExecutor

def run_tools(response) -> list:
    blocks = [b for b in response.content if b.type == "tool_use"]
    with ThreadPoolExecutor(max_workers=4) as pool:
        results = list(pool.map(execute_tool, blocks))
    return results

第1回で「並列呼び出しの結果はすべて返さなければならない」と言ったことが、ここでそのまま適用されます。サブエージェント三つがそれぞれ数分かかる調査を同時に行えば、全体の時間は最も遅い一つ分に縮みます。

委任が過剰にならないように #

サブエージェントはただではありません。呼び出しのたびに別のループが丸ごと回るので、費用と時間がかかります。二つの基準をおすすめします。

  • 一回で終わる仕事は直接やる。一度の照会で答えが出る仕事にサブエージェントを立ち上げるのは無駄です。delegate ツールの description に「複数の文書を検索して読む必要がある場合のみ」のように条件を書くと、過剰な委任が減ります。
  • 委任の深さは一段に制限する。サブエージェントがさらにサブエージェントを呼び始めると、費用と動作の追跡が難しくなります。サブエージェントには delegate ツールを与えないことで、構造的に防げます。

よくつまずくところ #

  • サブエージェントが文脈を共有していると仮定する — サブエージェントはメインの会話をまったく知りません。task に書かれたことが知っていることのすべてです。必要な背景は task にすべて入れる必要があります。
  • 報告形式なしで委任する — 形式のない報告は、要点が抜けるか冗長になります。期待する出力を task に明記します。
  • すべてを委任する — 委任そのものが目的になると、単純な作業にもループを一つずつ回すことになります。直接やったほうが速い仕事の基準を description に書いておきます。

まとめ #

今回は、仕事を分けて任せるサブエージェントを扱いました。

  • サブエージェントは別の messages で回るもう一つのループであり、メインエージェントにはツール一つに見えます。
  • 価値の核心はコンテキストの分離です。途中経過はサブエージェントが済ませ、結論だけが戻ってきます。
  • task に報告形式を契約のように明記し、委任の条件と深さを制限して費用を管理します。

ここまで、ツールはすべて私たちのコード内の Python 関数でした。次回の「AI エージェント開発実践 #6 MCP サーバーを自作する」では、ツールを標準プロトコルのサーバーに分離し、どのエージェントからでも再利用する方法を扱います。

X