AIエージェントを作り始めると、最初にぶつかる壁は「プロンプトの書き方」ではありません。「どこまでを一つのエージェントに任せるか」という設計の問題です。
私が最初に作ったエージェントは、Web調査・要約・レポート生成の三つの責務を一つに詰め込んでいました。動くには動くのですが、途中でコンテキストが詰まり、後半になるほど応答の質が落ちる。リトライしても同じ問題が繰り返される。この体験が、オーケストレーション設計を真剣に考えるきっかけになりました。
タスク分解の粒度をどう決めるか
エージェントを分割するかどうかの判断は、コンテキストウィンドウの消費量と処理の独立性の二軸で考えると整理しやすいです。
コンテキストが重い処理は分ける。大量のドキュメントを読み込んで要約する、コードベース全体を解析してバグを探す — こうした処理を他の作業と同一のエージェントでやらせると、コンテキストが汚染されて後続の判断がずれてきます。「この処理が終わった後、コンテキストはどう見えるか」を想像するのが有効です。
独立して実行できる処理は並列化する。三つのサービスへのAPIコールが結果を互いに参照しないなら、順番に実行する理由はありません。Anthropic の Claude Agent SDK では asyncio.gather() でサブエージェントを並列実行できます。
import asyncio
from anthropic import Anthropic
client = Anthropic()
async def run_agent(task: str, context: str) -> str:
"""単一タスクを実行するサブエージェント"""
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=2048,
system="あなたは与えられたタスクを完結させる専門エージェントです。",
messages=[{"role": "user", "content": f"コンテキスト: {context}
タスク: {task}"}]
)
return response.content[0].text
async def parallel_research(topics: list[str], shared_context: str) -> list[str]:
"""複数トピックを並列調査"""
tasks = [run_agent(f"{topic}について調査してまとめて", shared_context) for topic in topics]
return await asyncio.gather(*tasks)公式ドキュメントには書かれていませんが、実際に並列実行すると API レートリミットに頻繁にぶつかります。asyncio.Semaphore で同時実行数を 3〜5 に制限するのが現実的な落としどころです。
サブエージェントへのハンドオフ設計
オーケストレーターがサブエージェントに仕事を渡すとき、何を渡して何を渡さないかが重要です。
渡すべき情報: 目的(Why)、現在の状態(What is known)、完了条件(Definition of Done)、失敗時の対応方針。
渡してはいけない情報: オーケストレーター自身の思考過程、他のサブエージェントへの指示内容(干渉の原因になる)、過去の失敗ログの全文(必要なエッセンスだけ渡す)。
def create_handoff_context(
goal: str,
current_state: dict,
success_criteria: str,
failure_policy: str = "report and stop"
) -> str:
"""サブエージェントへの引き渡しコンテキストを構造化する"""
return f"""
## あなたのミッション
{goal}
## 現在わかっていること
{json.dumps(current_state, ensure_ascii=False, indent=2)}
## 完了条件
{success_criteria}
## 失敗時の対応
{failure_policy}
"""この構造を使い始めてから、サブエージェントの応答が「指示の解釈で悩む」ではなく「作業に集中する」ものに変わりました。
ループ制御と終了条件の設計
エージェントループで最も危険なのは「無限ループ」ではなく「終わっていないのに終了したと判断するループ」です。
ループの終了条件は、エージェント自身の判断に完全に委ねない設計にするべきです。次の三層で管理するのが効果的です。
class AgentLoop:
def __init__(self, max_iterations: int = 10, timeout_seconds: int = 300):
self.max_iterations = max_iterations
self.timeout_seconds = timeout_seconds
self.iteration = 0
self.start_time = None
def should_continue(self, agent_says_done: bool, result: dict) -> bool:
"""三層の終了判定"""
# Layer 1: ハードリミット(最優先)
if self.iteration >= self.max_iterations:
return False
import time
if time.time() - self.start_time > self.timeout_seconds:
return False
# Layer 2: エージェントの自己申告
if agent_says_done:
# Layer 3: 結果の検証(自己申告を鵜呑みにしない)
return not self._validate_completion(result)
return True
def _validate_completion(self, result: dict) -> bool:
"""完了条件を独立して検証する"""
required_keys = ["summary", "confidence", "next_actions"]
return all(k in result and result[k] for k in required_keys)実際の経験から言うと、max_iterations=10 でほとんどのタスクは十分です。それ以上かかるタスクは、タスク分解が不十分なサインであることが多いです。
エラーハンドリングのパターン
エージェントのエラーは大きく三種類に分かれます。
一時的エラー(APIタイムアウト、レートリミット): 指数バックオフでリトライ。最大3回、それ以上は上位エージェントに報告。
ツール実行エラー(ファイルが存在しない、権限不足): ツールの呼び出し方を変えてリトライ。同じエラーが2回続いたら上位に委譲。
論理エラー(タスクが完了条件を満たせない): リトライしても意味がありません。エラーの内容を構造化して上位エージェントが判断できる形で返す。
from enum import Enum
class ErrorType(Enum):
TRANSIENT = "transient" # リトライ可能
TOOL = "tool" # ツール変更でリトライ
LOGIC = "logic" # エスカレーション
def classify_error(error: Exception) -> ErrorType:
error_str = str(error).lower()
if "timeout" in error_str or "rate limit" in error_str:
return ErrorType.TRANSIENT
if "not found" in error_str or "permission" in error_str:
return ErrorType.TOOL
return ErrorType.LOGIC実際に動かした構成
私が個人開発で使っている「コンテンツ生成エージェント」の構成を簡略化して紹介します。
オーケストレーター → リサーチエージェント(並列3件)→ 構成提案エージェント → 執筆エージェント → 品質チェックエージェント
この連鎖で重要なのは、各エージェントが「自分のアウトプットを次のエージェントが使いやすい形で返す」意識を持たせることです。そのために、各エージェントのシステムプロンプトに出力フォーマットを JSON スキーマで明示しています。
最初の一歩としては、二段階構成(リサーチャー + ライター)から始めることをお勧めします。シンプルな構成でも、一つのエージェントに全部やらせるより明確に品質が上がります。その体験を積んでから、段階的に複雑さを追加していくのが遠回りに見えて実は一番の近道です。