Skip to content
Published on

本番エージェント設計パターン 2026 — Supervisor・CodeAct・Plan-Execute・Self-RAG・Handoff・Subagent を実際に組み合わせる方法

Authors

プロローグ — 入門書のパターン vs 本番のパターン

2023〜2024 年に出回った「LLM エージェント設計パターン」記事のカタログはどれも似ている。ReAct、Chain-of-Thought、Reflection、Plan-and-Solve、Tree of Thoughts、そして最後の章に「マルチエージェント」。きれいに 5 つの箱。デモはちゃんと動く。

問題は、それらの多くが おもちゃのベンチマーク環境で検証された ことだ。HotpotQA の 2〜3 ホップ、GSM8K の算数、ALFWorld のテキストゲーム。そこで意味があったものは、本番 — 5,000 行のコードベースを修正し、社内データを 30 のシステムから引き、決済 API を呼び、30 分のタスクの途中で OOM が起きる環境 — では、別の壊れ方をする。

2026 年の本番エージェントの真実、一行にまとめるなら:単一のパターンで動いているシステムはほとんどない。 Supervisor + CodeAct + Subagent isolation + Hooks を一緒に出荷するか、Plan-Execute + Self-RAG + Reflexion + Handoff を一緒に出荷する。責任は分担されており、各持ち場で別のパターンが仕事をする。

本稿では 2024〜2026 年の本番ワークロードで生き残った 9 つのパターン を整理する。各パターンについて、(1) 何か、(2) いつ光るか、(3) どう崩れるか、(4) 30〜50 行の最小スケッチ。そして最後に — 実際にどう組み合わせるか。


1. Supervisor / Orchestrator

一行定義: 上位(supervisor)エージェントが 1 つ、タスクを受け、複数の専門(specialist)エージェントに分配し、結果を集めて次のステップを決める。

LangGraph の公式「supervisor」ガイドが推奨する構成で、実際に最もよく見るマルチエージェントトポロジ。自由気ままな swarm と違い、誰が次に働くか を 1 つの supervisor が決めるため、全体を制御可能な状態に保てる。

トポロジ

                    ┌────────────┐
   user task ────►  │ Supervisor │
                    └─────┬──────┘
                          │ route by intent / state
              ┌───────────┼───────────┐
              ▼           ▼           ▼
       ┌──────────┐ ┌──────────┐ ┌──────────┐
       │ Research │ │ Coder    │ │ Reviewer │
       │ agent    │ │ agent    │ │ agent    │
       └────┬─────┘ └────┬─────┘ └────┬─────┘
            └──────┬─────┴──────┬─────┘
                   ▼            ▼
              return to Supervisor (loop)

いつ光るか

  • ワークフローの各段階が 異質 なとき。検索は長く高価、コーディングはツールが違い、レビューは短く決定論的。1 つのエージェントに詰めるとシステムプロンプトがパッチワークになる。
  • 状態を見たルーティング が必要なとき。supervisor は毎ステップで全状態を見て「次は誰」を決める。
  • コスト非対称 なモデル混合を使いたいとき(supervisor に Opus、specialist に Haiku)。

どう崩れるか

  • supervisor がコンテキストのブラックホールになる。 全 specialist の出力が supervisor の context に積み上がり、5 ラウンドで context window が飽和する。対策:specialist の結果を返す際に強制要約、もしくは「ツール返り値」として扱い raw chain は捨てる。
  • ルーティングの無限ループ。 supervisor が「Research → Coder → Research → Coder」を 6 回繰り返して同じ結論に着く。対策:決定論的な hop budget(LangGraph の recursion_limit 相当)。
  • specialist 出力の形ズレ。 coder が raw diff を返すのに supervisor は「PR 説明文」を期待。対策:各 specialist の return type を schema で固定する。

最小スケッチ

# supervisor.py — LangGraph スタイルの最小 supervisor
from typing import Literal, TypedDict
from langgraph.graph import StateGraph, END

class State(TypedDict):
    task: str
    messages: list
    next: Literal["research", "code", "review", "done"]
    hop: int

def supervisor(state: State) -> dict:
    if state["hop"] >= 6:
        return {"next": "done"}
    prompt = f"Task: {state['task']}\nHistory: {state['messages'][-3:]}\nWho works next? research/code/review/done"
    decision = llm(prompt)  # 強制的に enum のひとつだけを返す
    return {"next": decision, "hop": state["hop"] + 1}

def specialist(name):
    def run(state: State) -> dict:
        result = run_agent(name, state["task"], state["messages"])
        # 重要:raw output を丸ごと入れず、要約だけ
        return {"messages": state["messages"] + [{"agent": name, "summary": summarize(result)}]}
    return run

g = StateGraph(State)
g.add_node("supervisor", supervisor)
g.add_node("research", specialist("research"))
g.add_node("code", specialist("code"))
g.add_node("review", specialist("review"))
g.set_entry_point("supervisor")
g.add_conditional_edges("supervisor", lambda s: s["next"],
    {"research": "research", "code": "code", "review": "review", "done": END})
for s in ["research", "code", "review"]:
    g.add_edge(s, "supervisor")
graph = g.compile()

ポイントは supervisor ノードが 決定関数 であること — 仕事自体はせず、誰が働くかだけを決める。


2. CodeAct — アクション言語として Python を使う

一行定義: エージェントの「Action」出力を tool_name(arg) の JSON オブジェクトではなく、実行可能な Python ブロック にする。1 ステップで変数を割り当て、分岐し、ループし、複数ツールを一度に呼べる。

出発点は Yang ら "Executable Code Actions Elicit Better LLM Agents"(CodeAct、2024 年 6 月)。2025 年に Manus チームが自社エージェントのアクション層として採用していることを公開記事で明かした。HuggingFace の smolagents.CodeAgent も同じ思想で作られている。

比較

[古典 ReAct + JSON tool calls]
Action: {"tool": "search", "args": {"q": "kubernetes pod oom"}}
Observation: [50 件]
Action: {"tool": "search", "args": {"q": "kubernetes pod oom limits"}}
Observation: [30 件]
Action: {"tool": "summarize", "args": {"text": "..."}}
→ LLM 呼び出し 3 回、ラウンドトリップ 3 回

[CodeAct]
Action:
```python
results_a = search("kubernetes pod oom")
results_b = search("kubernetes pod oom limits")
merged = list(set(results_a + results_b))[:20]
print(summarize("\n".join(r.snippet for r in merged)))

Observation: [要約 1 つ] → LLM 呼び出し 1 回、ラウンドトリップ 1 回


### いつ光るか

- **複数ツールを組み合わせる** タスク。fetch → 変換 → 集計 → グラフ化が 1 ブロックに収まる。
- **反復が自然な** タスク。100 ファイルの処理を JSON tool call でやると 100 回のラウンドトリップが必要。CodeAct なら `for f in files:` 一行。
- **算術が混ざる** タスク。LLM 自身が計算せず、`numpy` 一行で終わる。
- **状態が積み上がる** タスク。変数を介して前ステップの結果が次ステップに自然に流れる。

### どう崩れるか

- **サンドボックスが漏れた瞬間、セキュリティは終わる。** Python を実行するということは任意コード実行ということ。対策:Docker、Firecracker、E2B、gVisor、最低でも Python`restricted exec` + syscall フィルタ。
- **`import os; os.system('rm -rf /')` の一行で全てが終わる。** LLM がそれを書かない保証はない。対策:import の allowlist + ネットワーク隔離 + ephemeral filesystem。
- **無限ループ。** `while True: search("x")` 一発でトークン請求書が爆発する。対策:実行タイムアウト + 出力トークン上限。
- **デバッグが難しい。** 1 ブロックが 7 ツールを呼び、5 番目で失敗すると、モデルは「何が起きたか」を再構築する必要がある。対策:実行トレースをそのままモデルに返す(行ごとの stdout)。

### 最小スケッチ

```python
# codeact_executor.py — 隔離環境でモデル生成コードを実行
import subprocess, json, textwrap

ALLOWED_IMPORTS = {"json", "re", "math", "datetime", "tools"}

def execute_codeact(code: str, tools: dict, timeout_s: int = 30) -> str:
    # tools はモデルが呼べる関数の dict — コンテナに事前注入
    wrapper = textwrap.dedent(f"""
        import json, sys
        from tools import {", ".join(tools.keys())}
        try:
        {textwrap.indent(code, "            ")}
        except Exception as e:
            print(f"__ERROR__: {{type(e).__name__}}: {{e}}", file=sys.stderr)
    """)
    result = subprocess.run(
        ["docker", "run", "--rm", "-i", "--network=none",
         "--memory=512m", "--cpus=1.0", "codeact-sandbox:latest", "python", "-"],
        input=wrapper.encode(),
        capture_output=True,
        timeout=timeout_s,
    )
    stdout = result.stdout.decode()[-4000:]  # 出力上限
    stderr = result.stderr.decode()[-1000:]
    return f"STDOUT:\n{stdout}\n\nSTDERR:\n{stderr}"

# モデルループ
while not done:
    code = llm_extract_codeact(messages)  # ```python ... ``` ブロックを抽出
    obs = execute_codeact(code, tools)
    messages.append({"role": "user", "content": f"Execution result:\n{obs}"})

肝心なディテール:出力上限(最後の 4 KB のみ)、ネットワーク隔離--network=none の上で必要なドメインのみ追加)、メモリ/CPU 制限タイムアウト


3. Plan-Execute — 計画を先に、その後実行

一行定義: 大きな LLM 呼び出し 1 回で N ステップの plan を作り、その plan に沿って決定論的(または小型モデル)でステップごとに実行する。ReAct が「1 思考、1 行動」を交互に行うのに対し、Plan-Execute は「N ステップを一度に思考、N 行動を実行」。

LangGraph の公式チュートリアルに "plan-and-execute" の例があり、Wang ら "Plan-and-Solve Prompting"(2023)以来、最も安定的な構造として定着している。

トポロジ

[Plan phase]                       [Execute phase]
┌──────────────┐                   ┌──────────────┐
│  Planner LLM │  →  step list  →  │ Executor LLM │  →  results
│  (one shot)  │                   │  (per step)  │
└──────────────┘                   └──────┬───────┘
                  optional re-plan  ◄─────┘
                  (if step fails)

いつ光るか

  • ステップ数が事前に予測可能 なタスク。「レポート作成」は (調査 → アウトライン → 草案 → レビュー → 仕上げ) でほぼ常に分解できる。
  • 独立したステップ で、より安いモデルで実行できる。コストが半分以下になることが多い。
  • 再現性が重要な ワークフロー。plan を保存しておけば同じ入力で同じ plan が出る(temperature=0)。

どう崩れるか

  • plan の段階で幻覚。 Planner が「Step 3: customers テーブルにクエリ」と言うが、そんなテーブルはない。実行段階で初めて気付く。対策:planner に利用可能なツール/リソースの schema を先に与えるか、planner の前に "discover" フェーズを通す。
  • 世界は plan どおりに動かない。 3 番目のステップで外部 API が 401 を返したら、4 番目は無意味。対策:re-planning トリガーを定義する(失敗、または予期しない観測)。
  • 過剰分解。 4 ステップで足りる仕事を 12 ステップにする。対策:planner プロンプトに「絶対必要でない限り N ステップ以下」と明記する。

最小スケッチ

# plan_execute.py
def plan(task: str, tools_schema: dict) -> list[dict]:
    prompt = f"""You will be given a task. Decompose it into 3-7 ordered steps.
Each step: {{"id": int, "goal": str, "tool": str | null, "depends_on": [int]}}
Available tools: {list(tools_schema.keys())}
Task: {task}
Return a JSON array."""
    return json.loads(llm(prompt, temperature=0))

def execute_step(step: dict, prior_results: dict) -> dict:
    context = "\n".join(f"step{i}: {prior_results[i]}" for i in step["depends_on"])
    return run_executor_agent(step["goal"], context, tool=step["tool"])

def plan_execute(task: str, tools_schema: dict, max_replans: int = 2):
    steps = plan(task, tools_schema)
    results, replans = {}, 0
    i = 0
    while i < len(steps):
        try:
            results[steps[i]["id"]] = execute_step(steps[i], results)
            i += 1
        except UnexpectedObservation as e:
            if replans >= max_replans: raise
            steps = plan(f"{task}\nSo far: {results}\nProblem: {e}", tools_schema)
            replans += 1
            i = 0  # 新 plan で最初から
    return results

ReAct と比べて LLM 呼び出し回数が 1/3 〜 1/5 になるのが普通。だからコストが最大の利点になる。


4. Self-RAG / Corrective RAG — そもそも検索するかを判断する

一行定義: 全クエリに検索を付ける素の RAG と違い、エージェント自身が 「検索すべきか」「結果は十分か」「もう一度検索すべきか」 を判断する。

源流は Asai ら "Self-RAG"(2023)。その後 Yan ら "Corrective Retrieval Augmented Generation"(CRAG、2024)が「検索結果が悪ければウェブ検索で補完」する形に拡張した。2025〜2026 年には LangGraph チュートリアルの "Adaptive RAG" が事実上の標準リファレンスになった。

グラフ

                        ┌─────────────┐
   query  ───────────►  │ Route LLM   │
                        └──────┬──────┘
                  retrieve?    │     direct?
              ┌────────────────┴────────────────┐
              ▼                                 ▼
        ┌──────────┐                      ┌──────────┐
        │ Retrieve │                      │ Answer   │
        └────┬─────┘                      │ directly │
             ▼                            └──────────┘
        ┌──────────┐
        │ Grade    │  ──→  irrelevant  ──→  rewrite query, retry
        │ docs LLM │  ──→  partial     ──→  web search supplement
        └────┬─────┘  ──→  relevant    ──→  answer
        ┌──────────┐
        │ Answer   │  ──→  hallucinated?  ──→  retry
        │ + check  │  ──→  grounded      ──→  return
        └──────────┘

いつ光るか

  • 質問が多様な システム。検索不要なもの(「2 + 2 は?」)と、複数検索が必要なもの(「当社の返金ポリシーと 2025 年 EU 消費者法の比較」)が混在する。
  • 検索品質がまばらな 環境。wiki + Slack + Notion + GitHub issue を横断する場合。一度では全部出ない。
  • 幻覚の代償が大きい ドメイン — 法務・医療・金融。「検索結果が不十分」と言えることが重要。

どう崩れるか

  • ループが長くなる。 「再検索」「再検索」「再検索」→ トークン請求書が爆発。対策:retrieval hop の上限(通常 3)。
  • grader が自分自身を幻覚する。 ドキュメントは答えを含んでいるのに、grader が「無関係」と判断する。対策:grader を小型分類器に分離し、confidence 閾値を設ける。
  • rewrite が元の意図をぼかす。 「返金ポリシー」→ rewrite → 「返金手続きのステップ別ガイド」になり、例外条項を見落とす。対策:rewrite 時に元クエリも一緒に渡す。

最小スケッチ

# self_rag.py
def adaptive_rag(query: str, max_hops: int = 3):
    route = llm_classify(query, choices=["direct", "retrieve"])
    if route == "direct":
        return llm_answer(query)

    for hop in range(max_hops):
        docs = retrieve(query, k=5)
        grade = llm_grade(query, docs)  # relevant / partial / irrelevant のいずれか
        if grade == "relevant":
            answer = llm_answer_with_context(query, docs)
            if llm_check_grounding(answer, docs) == "grounded":
                return answer
            # 幻覚なら再試行
            continue
        if grade == "partial":
            web_docs = web_search(query, k=3)
            return llm_answer_with_context(query, docs + web_docs)
        # irrelevant → rewrite
        query = llm_rewrite(query, hint=f"prior docs were off-topic: {summarize(docs)}")
    return llm_answer_with_context(query, docs)  # 最後の試行

5. Reflexion / Self-Critique — verbal-RL の再試行ループ

一行定義: タスクを試み、失敗したら 「なぜ失敗したか」の自然言語 reflection を生成してメモリに保存し、次の試行のシステムプロンプトに注入する。重みは変わらない — "verbal reinforcement learning" だ。

源流は Shinn ら "Reflexion: Language Agents with Verbal Reinforcement Learning"(NeurIPS 2023)。HumanEval で GPT-4 のコーディング成功率を 80% → 91% に押し上げた。

サイクル

     ┌──────────────────────────────────────┐
     │                                      │
     ▼                                      │
  Actor  ──► Action ──► Env ──► Reward      │
  (LLM)                                     │
     ▲                                      │
     │                                      │
  Evaluator (success/fail)                  │
     │                                      │
     ▼                                      │
  Self-Reflection LLM ──► "why I failed" ───┘
                          (memory buffer)

いつ光るか

  • 明確な成功/失敗シグナル があるタスク。コードのテスト合否、数値の正誤、ユーザの拒否。シグナルがあれば reflection は噛み付く対象を持てる。
  • 試行が安く速い 環境 — 一行直して unit test を回す。1 時間 100 ドルの外部 API なら合わない。
  • 同種タスクを繰り返す とき。Reflection メモリが積もり、学習効果が出る。

どう崩れるか

  • 誤った reflection が次の試行を害する。 「前回は X が原因で失敗」が誤っていれば、次の試行は誤った方向へ進む。対策:reflection に confidence を付け、同じ reflection が 2 回連続したら別方向を強制する。
  • 無限試行。 「また失敗、reflection、また試行...」対策:ハードな retry 上限(3〜5)。
  • memory bloat。 Reflection が積もり、context window を食い潰す。対策:直近 N 個のみ保持、または意味的類似度で重複除去。

最小スケッチ

# reflexion.py
def reflexion_loop(task: str, max_trials: int = 5):
    reflections = []
    for trial in range(max_trials):
        sys_prompt = base_prompt + "\n\nPast reflections:\n" + "\n".join(reflections[-3:])
        result = actor_llm(task, system=sys_prompt)
        evaluation = evaluator(result, task)  # status: success|fail, plus reason
        if evaluation["status"] == "success":
            return result
        reflection = reflect_llm(task=task, attempt=result, why_failed=evaluation["reason"])
        # 重複除去
        if reflection not in reflections:
            reflections.append(reflection)
        else:
            # 同じ気づきの繰り返し → 角度を変える強制
            reflection = reflect_llm(task=task, attempt=result, hint="try a completely different approach")
            reflections.append(reflection)
    return result  # 最後の試行を返す

6. Handoff — swarm スタイルのマルチエージェント

一行定義: supervisor はいない。各エージェントが自分の仕事が終わったら、次のエージェントに直接「ハンドオフ」する。 2024 年 10 月に公開された OpenAI Swarm ライブラリの核となるパターンで、Anthropic の computer-use デモも結果として同じ形を取る。

Supervisor vs Handoff

[Supervisor]                       [Handoff]
   ┌─────────┐                        A
   │   Sup   │                        │ handoff_to(B)
   └──┬──┬──┬┘                        ▼
      ▼  ▼  ▼                         B
      A  B  C                         │ handoff_to(C)
中央集権の決定                         C
"次は誰?"                              分散決定
                                       "次に渡すのは私が知っている"

OpenAI Swarm では、handoff は「関数のように見えるツール呼び出し」だ。transfer_to_billing() を呼ぶと会話コンテキストが billing エージェントに移る。

いつ光るか

  • 段階遷移が明確な ワークフロー — 「triage が受ける → 返金なら billing、技術なら tech-support → そこで終了」。
  • 各エージェントの責任が狭く、よく定義 されている。狭いスコープ=集中したシステムプロンプト。
  • 累積状態が小さい 会話。handoff はコンテキストを引きずるので、量が大きいとコストが膨らむ。

どう崩れるか

  • handoff のループ。 A → B → A → B。対策:handoff hop カウンタ + 同じペアが 2 回往復したらエスカレーション。
  • コンテキストがどこへでも付いてくるためコスト爆発。 長い会話だと、handoff のたびに新エージェントが同じコンテキストを再処理する。対策:handoff 時点で要約 + 小さい変数パケットだけ渡す。
  • システムプロンプトの衝突。 Tech-support は「共感的に」、billing は「数字だけ」。handoff 直後にトーンが急変する。対策:共通ベースプロンプト + 役割別オーバーレイ。

最小スケッチ

# swarm.py — OpenAI Swarm 風
def make_agent(name, instructions, tools, handoffs):
    return {
        "name": name,
        "instructions": instructions,
        "tools": tools + [{"name": f"transfer_to_{h.name}", "fn": lambda h=h: h} for h in handoffs],
    }

triage = make_agent("triage",
    "Route user to billing or tech_support based on intent.",
    tools=[], handoffs=[billing, tech])

def run(agent, user_msg, history):
    while True:
        history.append({"role": "user", "content": user_msg})
        out = llm(agent["instructions"], history)
        if out.tool_call and out.tool_call.name.startswith("transfer_to_"):
            agent = out.tool_call.fn()  # handoff!
            continue
        return out, history

7. Specialist routing — intent 分類器でディスパッチ

一行定義: 入力が来ると、まず 安く速い分類器 が intent を捉え、対応する specialist エージェントへルーティングする。Supervisor に似ているが、違いは 分類器が LLM ではなく、小型の分類モデルか決定論的ルール であること。

ほとんどの本番チャットボットがすでにこの形だ。Anthropic の顧客対応エージェント、OpenAI ChatGPT の GPTs ルーティング、Slack/Discord ボットのコマンドルーティング。

トポロジ

user msg
┌─────────────┐
│ Intent      │   ← 小型 BERT 分類器、正規表現、もしくは Haiku のような安い LLM
│ classifier  │
└──┬─────┬────┘
   │     │
   ▼     ▼
agent_a  agent_b  ... agent_n
(各自が自分のシステムプロンプトとツールを持つ)

いつ光るか

  • intent が列挙可能 なとき(「返金 / 配送 / アカウント / その他」)。
  • 各 intent の応答が大きく異なる とき。1 つのプロンプトでは対応できない。
  • レイテンシが critical なとき。BERT-base 分類器なら 50ms 以内に終わる。

どう崩れるか

  • 誤分類された入力は永遠に間違ったエージェントへ行く。 「決済が失敗して返金もないなんてふざけるな」は billing? complaint? 対策:低 confidence なら generalist にフォールバック。分類器の出力には常に「不確実」オプションを置く。
  • 新 intent が生まれると分類器の再学習が必要。 対策:小型 LLM に「既存 N クラス + その他」で zero-shot 分類させ、「その他」率を監視する。
  • multi-intent。 「注文をキャンセルして新規注文したい」= cancel + create。対策:multi-label 分類、または generalist が直接処理する。

最小スケッチ

# routing.py
INTENT_AGENTS = {
    "billing": billing_agent,
    "shipping": shipping_agent,
    "account": account_agent,
    "general": general_agent,
}

def classify(text: str) -> tuple[str, float]:
    # 小型分類器 — bert-base finetune か Haiku zero-shot
    logits = small_classifier(text)
    intent, conf = top1(logits)
    return intent, conf

def route(user_msg: str):
    intent, conf = classify(user_msg)
    if conf < 0.7 or intent == "uncertain":
        return general_agent(user_msg)  # フォールバック
    return INTENT_AGENTS[intent](user_msg)

8. Subagent isolation — Claude Code のコンテキスト隔離パターン

一行定義: メインエージェントが大きなタスクの途中で、完全に新しいコンテキストウィンドウ を持つ子エージェント(subagent)を立ち上げ、狭いサブタスクを任せ、結果の要約だけを受け取る。子のコンテキストは親に蓄積されない。

Claude Code を象徴するパターンのひとつで、Anthropic の公式ドキュメントが「subagent / Task tool」として扱っている。OpenAI Agents SDK の subagent や Cursor の「バックグラウンドエージェント」にも同じ概念がある。

[main agent context]
  ┌────────────────────────────────────────────────┐
  │ user task, system prompt, history (240k tok)   │
  │                                                │
  │   ── spawn(subagent, "summarize 18 PRs") ──►   │
  │                                                │
  │            [subagent context]                  │
  │            ┌───────────────────┐               │
  │            │ fresh window      │               │
  │            │ subtask + tools   │               │
  │            │ (returns: 400 tok)│               │
  │            └─────────┬─────────┘               │
  │                      │                         │
  │   ◄────── result summary (400 tok) ──          │
  │                                                │
  │ continues main task (context grew by 400 tok)  │
  └────────────────────────────────────────────────┘

いつ光るか

  • 大規模な検索/探索 が必要だが、親は raw 結果を知る必要がないとき。「この 18 個の PR を全部読んで stale なものを見つけて」は subagent に出し、親は結果リストだけ受ける。
  • 長いセッションで context rot 防止 が重要なとき。親コンテキストが 80% を超える前に委任する。
  • 並列化可能な独立サブタスク があるとき。3 つの subagent を同時に走らせる。

どう崩れるか

  • 子が親の意図を知らない。 「stale PR を見つけて」だけだと、子は「なぜ?」を知らない。対策:親がプロンプト作成時に充分なコンテキストを明示的にパックする。
  • 子が仕事を無限に拡げる。 「PR 18 個」→ 子が PR ごとにまた subagent を立てる → 再帰。対策:subagent の深さ制限 + トークン予算制限。
  • 結果の統合が難しい。 3 人の子が異なるフォーマットの結果を返す。対策:subagent プロンプトに出力 schema を明記する。

最小スケッチ

# subagent.py
def spawn_subagent(parent_state, subtask: str, tools: list,
                   max_tokens: int = 8000, depth: int = 0):
    if depth >= 2:
        raise RuntimeError("subagent depth limit reached")
    sub_context = {
        "system": SUB_AGENT_SYSTEM,  # 親とは別のシステムプロンプト
        "messages": [{"role": "user", "content": subtask}],
    }
    while not done(sub_context) and tokens(sub_context) < max_tokens:
        sub_context = step(sub_context, tools, depth=depth + 1)
    summary = final_summary(sub_context)  # 子が自分の結果を短く要約する
    return summary  # 親はこれだけを受ける

# 親ループ内
if parent_decides_to_delegate():
    summary = spawn_subagent(state, "summarize the 18 open PRs in the repo", tools=[git_tool, gh_tool])
    state["messages"].append({"role": "tool", "name": "subagent", "content": summary})

肝は 子の raw chain が親の messages に入らないこと。8,000 トークンの子作業が 400 トークンの要約に畳まれる。


9. Hooks + 権限ゲート — LLM ステップ間の決定論的ガードレール

一行定義: エージェントループの各ステップの前/後に 決定論的なコード(hook) が入り、検査/変換/ブロックを行う。LLM は速く柔軟だが信用ならない。hook は遅いが 100% 決定論的。両者が交互に走る。

Claude Code の hook system(PreToolUse, PostToolUse, UserPromptSubmit)が代表例で、LangGraph の interrupt_before / interrupt_after、OpenAI Agents SDK の guardrails、Anthropic Agent Skills の permission gate も同じファミリ。

どこに挟まるか

       ┌──────────────────────────────────────┐
       │              Agent loop              │
       │                                      │
 user  │  ┌──── UserPromptSubmit hook ───┐    │
 msg ──┼─►│ check policy, strip secrets   │   │
       │  └──┬────────────────────────────┘   │
       │     ▼                                │
       │  ┌──── LLM step ──────────┐          │
       │  │ propose tool call      │          │
       │  └──┬─────────────────────┘          │
       │     ▼                                │
       │  ┌── PreToolUse hook ────┐           │
       │  │ allow? deny? mutate?  │           │
       │  └──┬────────────────────┘           │
       │     ▼                                │
       │  ┌── Tool execution ─────┐           │
       │  └──┬────────────────────┘           │
       │     ▼                                │
       │  ┌── PostToolUse hook ───┐           │
       │  │ redact, audit         │           │
       │  └──┬────────────────────┘           │
       │     ▼                                │
       │  loop                                │
       └──────────────────────────────────────┘

いつ光るか

  • 取り返しのつかないアクション に対する人間の承認。rm -rf、決済、外部メール送信。LLM が「大丈夫」と言おうが、hook がブロックする。
  • 秘密情報の漏洩防止 — LLM 入力に API キー、トークン、PII が入っていないか毎回正規表現で検査する。
  • ポリシー強制 — 「このディレクトリ外のファイルは触らない」「このドメインリスト外には fetch しない」。
  • 監査ログ — 全ての tool call を PostHook で永続化。インシデント発生時に追跡可能。

どう崩れるか

  • 厳しすぎるとエージェントが止まる。 「全てのファイル書き込み拒否」→ エージェントは何もできない。対策:deny-by-default の代わりに allowlist + 境界ケースは「ユーザに尋ねる」。
  • 見えない hook → 同じ試行の繰り返し。 拒否されたがモデルは理由を知らずまた試す。対策:hook の拒否メッセージを次の LLM 入力に流す。
  • hook 内で LLM 呼び出し = 非決定論。 「これが危険か別の LLM に聞く」は hook の意味を壊す。対策:hook は静的ルールに徹し、曖昧なものは人間に投げる。

最小スケッチ

# hooks.py
def pre_tool_hook(tool: str, args: dict, state) -> tuple[bool, str | None]:
    if tool == "bash":
        cmd = args.get("command", "")
        if "rm -rf" in cmd or "curl http" in cmd:
            return False, "blocked: dangerous command pattern"
        if "$SECRET" in cmd or re.search(r"sk-[a-zA-Z0-9]{20,}", cmd):
            return False, "blocked: secret leak detected"
    if tool == "write_file":
        if not args["path"].startswith(state["allowed_root"]):
            return False, f"blocked: outside allowed root {state['allowed_root']}"
    return True, None

def agent_step(state):
    proposal = llm_propose_action(state)
    allowed, reason = pre_tool_hook(proposal.tool, proposal.args, state)
    if not allowed:
        state["messages"].append({"role": "tool", "content": f"BLOCKED: {reason}"})
        return state
    result = execute(proposal)
    audit_log(proposal, result)  # post hook
    state["messages"].append({"role": "tool", "content": result})
    return state

10. 実システムはこれらをどう組み合わせるか

ここからが本題だ。単一パターンで動く本番システムはほぼない。2026 年のよく出来たエージェントは 3〜4 のパターンを合成 する。

ケース 1 · コーディングエージェント(Claude Code、Cursor、Devin 系)

[user task]
┌────────────────────────────────────────────────┐
│  Main agent (loop) — Plan-Execute              │
│   - Reads task, builds a 3-7 step plan         │
│   - Per step:                                  │
│      ┌─── PreToolUse hook ────┐                │
│      │ permission + policy    │                │
│      └─────────┬──────────────┘                │
│                ▼                               │
│      ┌─── Tool / CodeAct exec ────┐            │
│      └─────────┬──────────────────┘            │
│                ▼                               │
│      ┌─── PostToolUse hook ───┐                │
│      │ audit, redact          │                │
│      └─────────┬──────────────┘                │
│                ▼                               │
│      need deep exploration? ──► spawn subagent │
│                                  (isolated ctx)│
│                ▼                               │
│   - On step failure: Reflexion-style retry     │
└────────────────────────────────────────────────┘

組み合わせ:Plan-Execute + Subagent isolation + Hooks + Reflexion(4 個)。

ケース 2 · 顧客サポートエージェント(Anthropic、Intercom Fin、Linear Asks)

[user msg]
┌──── Specialist routing (intent classifier) ────┐
│       ▼          ▼         ▼          ▼        │
│   billing    shipping   account    general     │
│      │          │         │           │        │
└──────┼──────────┼─────────┼───────────┼────────┘
       ▼          ▼         ▼           ▼
     [each specialist: Self-RAG over internal KB]
     can resolve? ──── yes ──► reply
       no
     Handoff to human OR another specialist

組み合わせ:Specialist routing + Self-RAG + Handoff(3 個)。

ケース 3 · データ分析エージェント(Manus、OpenAI Code Interpreter)

[user question over CSV / DB]
Supervisor (Manus-style)
    ├─► Researcher subagent  (subagent isolation)
    │     └─ Self-RAG over schemas/docs
    ├─► Analyst subagent
    │     └─ CodeAct (writes pandas / SQL in Python blocks)
    │     └─ executes in sandbox with Hooks (network=none, fs scoped)
    └─► Reporter subagent
          └─ writes markdown summary

組み合わせ:Supervisor + Subagent isolation + CodeAct + Self-RAG + Hooks(5 個)。

ケース 4 · OS 自動化エージェント(Anthropic computer use)

[user goal: "book a flight"]
Plan phase: high-level plan (find site → search → fill form → confirm)
Execute phase: action loop
   - screenshot
   - decide click/type/key
   - PreActionHook: dangerous zone? (banking, settings) → halt + ask user
   - execute
   - PostActionHook: screenshot diff
   - on unexpected screen → Reflexion-style replan

組み合わせ:Plan-Execute + Hooks + Reflexion

組み合わせマトリックス

                        単純   | 中程度        | 非常に複雑
入門書                  ReAct  | ReAct+Refl    | (ベンチマークここで止まる)
実本番推奨              Routing| Plan-Exec+    | Supervisor+Subagent+
                       +Self-  | Hooks+        | CodeAct+Hooks+Reflexion
                        RAG    | Subagent      |

11. アンチパターン

  1. 全パターンの寄せ集め。 「supervisor の中にまた supervisor、subagent の中に plan-execute...」デバッグ不可能になる。追加する前に削れ が黄金律。
  2. Hooks なしの CodeAct。 数日のうちに誰かが rm -rf を書くか、秘密情報を漏らす。CodeAct はサンドボックス + hooks が前提条件。
  3. 全 specialist の raw 出力を supervisor に累積。 5 ラウンドで 240k トークン使い切る。
  4. ハードキャップなしの Reflexion。 12 回の再試行が 3,000 ドルの請求書として返ってくる。
  5. Plan-Execute の plan を絶対視。 plan は仮説に過ぎない。Execute で plan と異なる観測が出たら replan を許可せねばならない。
  6. Handoff 時にコンテキストをそのまま渡す。 毎回新エージェントが同じコンテキストを再読するため、コストが N 倍になる。要約 + 変数パケット のみ渡す。
  7. Self-RAG の grader を本体と同じモデルにする。 Grader は安い別モデルの方がよい。
  8. routing の confidence を無視。 confidence 0.51 の入力をそのまま specialist に流す。fallback トラックは必須。

12. パターン選択チェックリスト

新しいエージェントを作るとき、自問すべき質問:

  • タスクの ステップ数が事前予測可能 か? → 可能なら Plan-Execute、不可能なら ReAct + Reflexion。
  • 異質なツール/知識領域 が混ざるか? → Specialist routing か Supervisor。
  • 計算/構成の多い アクションがあるか? → CodeAct(ただしサンドボックス + hooks が前提)。
  • 外部知識が答えに必須 か? → 全 RAG ではなく Self-RAG(必要なときだけ)。
  • 明確な成功/失敗シグナル があるか? → Reflexion 検討。
  • 取り返しのつかないアクション があるか? → Hooks + 権限ゲート、必須。
  • 長セッション / 大規模探索 があるか? → Subagent isolation。
  • エージェント間の自然な handoff がワークフローにあるか? → Handoff(hop cap 必須)。

自分の組み合わせマトリックスを描け。パターンは道具に過ぎない。

エピローグ — パターンカタログではなくシステム思考

2024 年の記事が「ReAct を使え」と言ったなら、2026 年の答えはもっと静かだ — 「あなたのワークロードではどの失敗が多いか?」 から始まる。コンテキストが暴走するなら Subagent。検索がまばらなら Self-RAG。段階が散らかっているなら Plan-Execute。取り返しのつかないアクションがあるなら Hooks。責任が広すぎるなら Specialist routing か Supervisor。

パターンはカードデッキで、あなたが手札を組む。手札はシステム次第。そして一度に一枚ずつ足せ — それが最大の教訓。

次回は、これらの組み合わせの 評価 を扱う。Inspect AI、Phoenix、LangSmith でマルチパターンエージェントの回帰をどう捕まえるか、そして swarm / handoff のような非決定的トポロジで「正解」をどう定義するか。

参考 / References