AI エージェント開発実践 #5 サブエージェントで仕事を分ける
第4回では、コンテキストを減らし、空け、要約しました。しかし、もう一つ根本的な解決策があります。そもそも一つのコンテキストにすべてを詰め込まないことです。仕事の一部を別のエージェントに切り出して結果だけを受け取れば、その仕事の途中経過はメインエージェントのコンテキストにまったく入りません。今回はサブエージェントを扱います。
なぜ分けるのか #
サブエージェントの利点は三つに整理できます。
- コンテキストの分離 — 調査作業には、検索結果を数十件あさる過程がついてきます。サブエージェントがその過程を自分のコンテキストで済ませ、結論だけ報告すれば、メインエージェントは結論だけを受け取ります。第4回の技法が「積もったものを減らす」方法だとすれば、これは「積もらせない」方法です。
- 役割の分離 — サブエージェントごとに別のシステムプロンプトと別のツール一覧を与えられます。調査担当には検索ツールだけ、執筆担当にはファイルツールだけ、という具合です。第2回で見た「ツール一覧は狭いほどよい」という原則を、役割の単位で実現するわけです。
- 並列実行 — 互いに独立した作業なら、サブエージェントを複数同時に走らせられます。
delegate ツールを作る #
実装の核心は意外なほど単純です。サブエージェントは結局、別の messages 配列で回るもう一つのループです。第1回の run_agent をほぼそのまま使い、役割とツールをパラメータで受け取るようにします。
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 "(サブエージェントがステップ上限内に作業を終えられませんでした)"そしてこれをメインエージェントのツールとして公開します。
{
"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 に報告形式を明記することが重要です。
2026年5月の返金ポリシー変更の内容を調査せよ。
報告形式:
- 変更の要点 (3行以内)
- 根拠となる文書のタイトルと場所
- 確認できなかったことがあれば明記「確認できなかったことを明記せよ」という行が特に有用です。これがないと、サブエージェントは手ぶらのとき、もっともらしい推測で報告書を埋める傾向があります。
オーケストレーター・ワーカーと並列実行 #
サブエージェントが複数になると、メインエージェントの役割が変わります。自分で働く代わりに、仕事を分けて結果を統合するオーケストレーターになります。互いに独立した委任なら同時に実行できます。Claude が一つの応答で delegate_research を三回呼んだら、その三つの呼び出しをスレッドで並列処理する、という具合です。
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 サーバーを自作する」では、ツールを標準プロトコルのサーバーに分離し、どのエージェントからでも再利用する方法を扱います。