第9章 エージェントアーキテクチャ(Agent Architectures)
9.1 概要
Section titled “9.1 概要”第5章でエージェントループという「心臓部」を、第7章でツールという「手足」を、第8章でメモリという「記憶」を見た。本章では、それらをどう組み上げるかという構成の型を扱う。
アーキテクチャの選択は、第4章4.3.3節で述べた「最も単純な構成から始める」という原則の延長にある。本章で紹介する構成は、後にいくほど強力だが高価で複雑になる。上から順に検討し、足りなければ次に進むという読み方をしてほしい。
章の後半では MCP(Model Context Protocol) を扱う。これはアーキテクチャというより「ツールをどう接続するかの標準規格」だが、エージェントの構成に大きく影響するため本章に含める。
重要な注意: MCPの仕様は2026年7月28日版で大きく変わった。従来の
initializeハンドシェイクとセッションIDが廃止され、ステートレスな設計になっている。インターネット上の解説記事やAIが生成するコードの多くは旧仕様に基づいているため、9.6節以降を読む際はこの点に注意してほしい。
9.2 主要なアーキテクチャ
Section titled “9.2 主要なアーキテクチャ”9.2.0 第4章のワークフローパターンとの関係
Section titled “9.2.0 第4章のワークフローパターンとの関係”先に、第4章4.3.2節で挙げた5つのワークフローパターンとの関係を整理しておく。両者は別系統の分類ではなく、同じ形を「誰が制御するか」で分けたものである(第4章4.3.1節)。
| 第4章のワークフローパターン | 本章の対応する構成 | 違い |
|---|---|---|
| プロンプトチェイニング | (対応なし。固定手順) | 経路が固定 |
| ルーティング | (対応なし。分岐1回) | ループがない |
| 並列化(セクショニング) | DAGエージェント(9.2.4) | 分割の仕方をLLMが決める |
| オーケストレータ・ワーカー | Planner-Executor(9.2.3) | 計画を明示的な成果物として外に出す |
| 評価者・最適化者 | ループ内の振り返り(第5章5.6節) | 反復の継続をLLMが判断する |
つまり本章の構成は、ワークフローパターンの制御をLLM側に寄せたものと見るとよい。オーケストレータ・ワーカーと Planner-Executor がよく似ているのはこのためである。
9.2.1 ReAct(Reason + Act)
Section titled “9.2.1 ReAct(Reason + Act)”最も基本的で、最も広く使われている構成である。 実のところ、第5章で組み立てたエージェントループはReActそのものである。
ReActは「推論(Reasoning)」と「行動(Acting)」を交互に繰り返す。モデルは、次に何をすべきかを考え → ツールを実行し → 結果を観察し → また考える。
特徴
| 観点 | 評価 |
|---|---|
| 適応性 | ◎ 予期しない結果に即座に対応できる |
| 実装の容易さ | ◎ ループ1つで書ける |
| レイテンシ | △ 逐次的。ステップ数に比例して遅い |
| コスト | △ 毎ステップ全履歴を再送する |
| 予測可能性 | △ 経路が毎回変わる |
| 長期タスクへの適性 | ✗ 途中で目標を見失いやすい |
**最大の弱点は「計画性の欠如」**である。ReActは常に「次の一手」だけを考えており、全体の見通しを持たない。10手先まで必要なタスクでは、途中で迷走しやすい。第5章5.4.2節の停滞は、この性質から生じる。
使うべき場面: ステップ数が5〜15程度で、ツール結果に応じた柔軟な対応が必要なタスク。多くの実務的なエージェントはこれで足りる。
9.2.2 RAGエージェント
Section titled “9.2.2 RAGエージェント”第3章3.6.4節で「エージェント型RAG」として触れた構成である。ReActの特殊形と見てよい。検索をツールとして与え、いつ・何回検索するかをモデルに委ねる。
従来型RAG(検索1回 → 回答1回)との違いは第3章で述べたとおりである。ここで補足すべきは、RAGエージェント固有の設計上の注意点である。
① 検索の打ち切り条件を設ける。 「情報が足りない」と判断し続けると検索が止まらない。検索ツールの呼び出し回数に上限を設け、上限に達したら「見つかった範囲で答える」よう促す。
② 出典の追跡を強制する。 複数回の検索を経ると、どの情報がどこから来たかが曖昧になる。ツールの返り値に必ず出典IDを含め、回答時に引用させる(第7章7.4.1節)。
③ 検索結果の蓄積がコンテキストを圧迫する。 5回検索すれば5回分の結果が積み上がる。第8章8.3節の context editing が特に有効な構成である。
9.2.3 Planner-Executor(計画・実行分離)
Section titled “9.2.3 Planner-Executor(計画・実行分離)”先に全体の計画を立て、それから実行する構成である。ReActの「計画性の欠如」を補う。
利点
- 見通しが立つ。実行前に計画をユーザーに提示して承認を得られる(第4章4.5節の高リスク操作への対策)
- コストを最適化できる。第2章2.6節・第3章3.3.3節で見たモデルとeffortの使い分けが、そのまま適用できる。計画には上位モデルを高いeffortで、実行には軽量モデルを低いeffortで使う
- 並列化できる。計画時点で依存関係が分かれば、独立したステップを同時に実行できる
- 監査しやすい。計画が記録として残る
欠点
- 計画が外れると弱い。前提が崩れたときの再計画(replanning)が必須になり、実装が複雑になる
- 初手のコストが高い。計画立案に上位モデル+高effortを使うため、単純なタスクでは割に合わない
- 柔軟性が下がる。計画に縛られて、途中で見つかった良い方法に切り替えられない場合がある
再計画の設計が肝である。 計画を絶対視すると失敗する。実行中に前提が崩れたことを検知したら、計画を捨てて立て直す経路を必ず用意する。
@dataclassclass Plan: steps: list[str] created_at_step: int
def should_replan(plan: Plan, observation: ToolOutcome, current_step: int) -> bool: """再計画が必要かを判定する。判定自体をLLMに任せる方式もある。""" return any([ observation.is_error and current_step > 0, # 想定外の失敗 current_step >= len(plan.steps), # 計画を使い切った # 実務では「観察が計画の前提と矛盾するか」をLLMに判定させることが多い ])interface Plan { steps: string[] createdAtStep: number}
/** 再計画が必要かを判定する。判定自体をLLMに任せる方式もある。 */function shouldReplan( plan: Plan, observation: ToolOutcome, currentStep: number): boolean { return [ observation.isError && currentStep > 0, // 想定外の失敗 currentStep >= plan.steps.length, // 計画を使い切った // 実務では「観察が計画の前提と矛盾するか」をLLMに判定させることが多い ].some(Boolean)}使うべき場面: ステップ数が15以上、手順に明確な段階がある、実行前に計画の承認が必要、コスト最適化が重要 — これらのいずれかに当てはまる場合。
9.2.4 DAGエージェント(有向非巡回グラフ)
Section titled “9.2.4 DAGエージェント(有向非巡回グラフ)”タスクを依存関係を持つノードのグラフとして表現し、依存が解決したノードから実行していく構成である。
②③④は互いに独立しているため並列実行できる。⑤は②③④すべての完了を待つ。
Planner-Executor が計画を「順序つきリスト」で持つのに対し、DAGは「依存グラフ」で持つ。この違いが並列性を生む。
import asynciofrom dataclasses import dataclass, field
@dataclassclass Node: id: str task: str depends_on: list[str] = field(default_factory=list)
async def run_dag(nodes: list[Node], execute) -> dict[str, str]: """依存が解決したノードから順に、可能な限り並列で実行する。""" results: dict[str, str] = {} pending = {n.id: n for n in nodes}
while pending: # 依存がすべて解決済みのノードを集める ready = [n for n in pending.values() if all(d in results for d in n.depends_on)] if not ready: raise RuntimeError( f"依存を解決できないノードが残っています: {list(pending)}。" "循環依存がないか確認してください。" )
# ready をまとめて並列実行する(ここが DAG の利点) outputs = await asyncio.gather( *(execute(n, {d: results[d] for d in n.depends_on}) for n in ready) ) for node, out in zip(ready, outputs): results[node.id] = out del pending[node.id]
return resultsinterface Node { id: string task: string dependsOn: string[]}
/** 依存が解決したノードから順に、可能な限り並列で実行する。 */async function runDag( nodes: Node[], execute: (node: Node, deps: Record<string, string>) => Promise<string>): Promise<Map<string, string>> { // Python の dict は、キーの型が明確になる Map に対応させる const results = new Map<string, string>() const pending = new Map<string, Node>(nodes.map((n) => [n.id, n]))
while (pending.size > 0) { // 依存がすべて解決済みのノードを集める const ready = [...pending.values()].filter((n) => n.dependsOn.every((d) => results.has(d)) ) if (ready.length === 0) { throw new Error( `依存を解決できないノードが残っています: ${[...pending.keys()].join(', ')}。` + '循環依存がないか確認してください。' ) }
// ready をまとめて並列実行する(ここが DAG の利点) // Python は gather の戻り値を zip でノードに対応づけるが、 // 添字アクセスを避けるためノードと出力を組にして返す const outputs = await Promise.all( ready.map(async (n) => { const deps: Record<string, string> = {} for (const d of n.dependsOn) { const value = results.get(d) if (value !== undefined) deps[d] = value } return [n, await execute(n, deps)] as const }) ) for (const [node, output] of outputs) { results.set(node.id, output) pending.delete(node.id) } }
return results}利点: 並列性による大幅な高速化。依存関係が明示されるため、デバッグと再実行が容易。失敗したノードだけを再実行できる。
欠点: グラフを事前に構築する必要がある。動的に構造が変わるタスクには向かない。実装が最も複雑。
使うべき場面: 独立した調査・取得が多数あり、それらを統合するタイプのタスク。「複数のデータ源から集めて統合する」パターンは典型例である。
なお、グラフをLLMに生成させる方式もある。この場合は Planner-Executor の計画部分がDAGを出力する形になり、両者は組み合わせて使われる。
9.2.5 アーキテクチャの比較と選択
Section titled “9.2.5 アーキテクチャの比較と選択”| ReAct | RAGエージェント | Planner-Executor | DAG | |
|---|---|---|---|---|
| 適応性 | ◎ | ◎ | ○ | △ |
| 並列性 | △ | △ | ○ | ◎ |
| 見通し・監査 | ✗ | △ | ◎ | ◎ |
| 実装の容易さ | ◎ | ◎ | △ | ✗ |
| コスト最適化 | △ | △ | ◎ | ◎ |
| 適する規模 | 5〜15ステップ | 3〜10ステップ | 15ステップ以上 | 並列可能な多数タスク |
実務では混在させる。 第4章4.3.4節で述べたとおり、単一のアーキテクチャに統一する必要はない。「Planner-Executor で全体を統括し、各ステップの実行は ReAct のサブエージェントに任せる」「DAGのノードの一部が RAGエージェント」といった構成が現実的である。
サブエージェントとマルチエージェント
Section titled “サブエージェントとマルチエージェント”ここで用語を定義しておく。
サブエージェント(subagent) とは、親エージェントから1つのツールとして呼び出される、独立したコンテキストを持つエージェントループである。親から見れば入力を渡して結果を受け取るだけのツールであり、その内部で何ステップ回っているかは見えない。マルチエージェント(multi-agent) は、複数のエージェントが協調して動く構成の総称である。
サブエージェントの利点は、コンテキストの分離にある。調査のために20ステップ回っても、親のコンテキストに戻るのは最終的な要約だけで済む。第8章8.3節のコンテキスト管理を、構造で解決する方法とも言える。
一方、注意点が2つある。
- 親はサブエージェントの出力を「指示」ではなく「データ」として扱う。 サブエージェントが読んだ外部データに攻撃が仕込まれていれば、その出力経由で親が汚染されうる(第13章13.5.5節)
- デバッグが難しくなる。 トレースが入れ子になるため、第12章12.4.2節のスパン階層で親子関係を記録しておく必要がある
9.3 Chain-of-Thought(CoT)
Section titled “9.3 Chain-of-Thought(CoT)”9.3.1 何をするものか
Section titled “9.3.1 何をするものか”Chain-of-Thought(思考の連鎖) は、最終回答の前に推論過程を明示的に生成させる技法である。
第2章2.2.1節で見たとおり、LLMは1トークンずつ自己回帰的に生成しており、途中で「立ち止まって考える」ことができない。CoTは、思考過程を出力させることでモデルに計算のための作業領域を与える。第3章3.3節の推論モデルは、これをモデル内部の機構として組み込んだものである。
9.3.2 現代における位置づけ
Section titled “9.3.2 現代における位置づけ”ここで重要な注意がある。推論モデルを使う場合、CoTを明示的にプロンプトで指示する必要はほとんどない。 adaptive thinking がモデル内部で同等以上のことを行っているためであり、「ステップバイステップで考えてください」と付け加えるのは、多くの場合トークンの無駄になる。
CoTを明示的に使うべきなのは、次の場合に限られる。
| 場面 | 理由 |
|---|---|
| 思考機能のないモデルを使う | 軽量モデルやオープンウェイトモデルなど |
| 思考を無効化している | レイテンシ優先の設定 |
| 推論過程を構造化したい | 特定の観点を必ず検討させたい |
| 推論過程をログに残したい | 監査要件がある(思考ブロックは既定で返らない) |
3つめが実務上いちばん有用である。自由に考えさせるのではなく、検討すべき観点を指定する。
回答の前に、<analysis> タグの中で次の順に検討してください。
1. 前提の確認: この問い合わせで、まだ確認できていない事実は何か2. 該当規程: 参照すべき社内規程はどれか3. 例外の有無: 通常の扱いと異なる可能性がある事情はあるか4. 判断: 上記を踏まえた結論
その後、<answer> タグの中に、担当者向けの回答を書いてください。これは単に「よく考えさせる」のではなく、チェックリストを強制する仕組みである。人間の業務手順書と同じ発想であり、推論モデルを使う場合でも有効である。
9.3.3 エージェントにおける注意点
Section titled “9.3.3 エージェントにおける注意点”CoTの出力はコンテキストを消費する。エージェントループでは、各ステップの思考が履歴に積み上がっていく。第8章8.3.3節の clear_thinking_20251015 は、まさにこれに対処するための機能である。
また、思考過程はユーザーに見せるものではない。中間的な誤りや試行錯誤が含まれるため、そのまま提示すると混乱を招く。<analysis> と <answer> を分け、ユーザーには後者だけを見せる設計にする。
9.4 Tree-of-Thought(ToT)
Section titled “9.4 Tree-of-Thought(ToT)”9.4.1 何をするものか
Section titled “9.4.1 何をするものか”Tree-of-Thought(思考の木) は、CoTを分岐させたものである。1本の推論の連鎖ではなく、複数の可能性を並行して展開し、評価して有望な枝を伸ばす。
手順は次のとおりである。
- 展開(Generate): 現在の状態から、複数の次の一手を生成する
- 評価(Evaluate): 各候補がどれだけ有望かをスコアリングする(LLM自身に評価させることが多い)
- 選択(Select): 有望な枝を選び、見込みのない枝を刈る
- 目標に達するまで1〜3を繰り返す
探索の戦略は幅優先・深さ優先・ビームサーチなど、古典的な探索アルゴリズムがそのまま適用できる。
9.4.2 コストの現実
Section titled “9.4.2 コストの現実”ToTは極めて高価である。 各ノードで展開と評価のために複数回のLLM呼び出しが発生し、木が深くなるほど指数的に増える。標準的なアプローチと比べて10〜100倍のトークンを消費するという報告もある。
この点を踏まえると、ToTの適用範囲は限定的である。
ToTが適する条件(すべて満たす必要がある)
- 中間状態を評価できる。「この途中経過は有望か」を判定する手段がある
- 解の探索空間が広い。単純な逐次推論では正解にたどり着けない
- 正答率の向上がコストに見合う。1回あたり数ドルかけても価値がある
数学パズルやゲームの探索といったベンチマークでは高い成果を挙げているが、業務エージェントで使う場面は多くない。
9.4.3 実務的な代替 — 判定者パネル
Section titled “9.4.3 実務的な代替 — 判定者パネル”ToTの発想を、より安価な形で取り入れる方法がある。複数の案を並列に生成し、評価して最良を選ぶという1段だけの構成である。
async def generate_and_select(task: str, n: int = 3) -> str: """複数案を並列生成し、評価して最良を選ぶ(ToTの1段版)。""" # 異なる観点を与えて多様性を確保する angles = ["最小限の実装を優先して", "リスクの低さを優先して", "拡張性を優先して"] candidates = await asyncio.gather( *(generate(f"{task}\n\n方針: {angles[i % len(angles)]}") for i in range(n)) ) scores = await asyncio.gather(*(evaluate(task, c) for c in candidates)) return max(zip(candidates, scores), key=lambda p: p[1])[0]/** 複数案を並列生成し、評価して最良を選ぶ(ToTの1段版)。 */async function generateAndSelect(task: string, n: number = 3): Promise<string> { // 異なる観点を与えて多様性を確保する const angles = [ '最小限の実装を優先して', 'リスクの低さを優先して', '拡張性を優先して', ] const candidates = await Promise.all( Array.from({ length: n }, (_, i) => generate(`${task}\n\n方針: ${angles[i % angles.length]}`) ) ) // Python は candidates と scores を zip して max を取るが、 // 添字アクセスを避けるため評価と同時に組にする const scored = await Promise.all( candidates.map(async (c) => ({ candidate: c, score: await evaluate(task, c) })) ) return scored.reduce((best, cur) => (cur.score > best.score ? cur : best)) .candidate}これは第4章4.3.2節の並列化(投票)パターンと評価者・最適化者パターンの組み合わせでもある。木を深く探索せず1段で止めるため、LLM呼び出しは生成 n 回 + 評価 n 回の 2n 回にとどまる(n=3 なら6回、単発比で約6倍)。ToTの10〜100倍と比べれば桁違いに安く、実務ではこちらのほうが現実的である。
なお、評価を1回のプロンプトで全候補まとめて行えば n+1 回に減らせる。ただし候補同士を比較させることになるため、独立性は失われる。
9.4.4 CoT / ToT / 推論モデルの整理
Section titled “9.4.4 CoT / ToT / 推論モデルの整理”| CoT | ToT | 推論モデル(adaptive thinking) | |
|---|---|---|---|
| 構造 | 直線 | 木 | モデル内部(不可視) |
| 追加コスト | 小 | 極大(10〜100倍) | 中(effortで調整可) |
| 実装 | プロンプトのみ | 探索ロジックが必要 | パラメータ指定のみ |
| 制御性 | 観点を指定できる | 探索戦略を制御できる | effort のみ |
| 実務での使用 | 構造化目的で有用 | 稀 | 既定の選択肢 |
現代の実務における既定の選択は推論モデルである。 CoTは推論を構造化・監査したい場合に併用し、ToTは特殊な探索問題に限る、という位置づけになる。
9.5 MCP(Model Context Protocol)とは
Section titled “9.5 MCP(Model Context Protocol)とは”9.5.1 解決する問題
Section titled “9.5.1 解決する問題”ここまで、ツールはエージェントのコード内に定義してきた(第7章)。この方式には限界がある。
M個のAIアプリケーションと、N個の外部システムを繋ぐには、M×N個の実装が必要になる。 Claude用のSlack連携、ChatGPT用のSlack連携、VS Code用のSlack連携……と、同じ機能を何度も書くことになる。
MCP(Model Context Protocol) は、この M×N 問題を M+N に変える標準規格である。外部システム側が「MCPサーバー」を1つ実装すれば、MCPに対応したあらゆるAIアプリケーションから使えるようになる。
公式ドキュメントは MCP を「AIアプリケーションにとってのUSB-Cポート」と喩えている。接続の形を標準化することで、どの機器もどのアプリにも繋がるようにする、という発想である。
MCPは Anthropic が策定したオープンな規格で、Claude、ChatGPT、VS Code、Cursor など幅広いクライアントが対応している。
9.5.2 核となる構成要素
Section titled “9.5.2 核となる構成要素”MCPはホスト・クライアント・サーバーの3者で構成される。この3語は紛らわしいので、正確に押さえておきたい。
| 役割 | 実体 | 説明 |
|---|---|---|
| MCPホスト | AIアプリケーション本体 | 複数のMCPクライアントを統括する。例: Claude Code、Claude Desktop、VS Code |
| MCPクライアント | ホスト内部のコンポーネント | 1つのサーバーにつき1つ生成され、その接続を維持する |
| MCPサーバー | 文脈や機能を提供するプログラム | ローカルでもリモートでも動く |
よくある誤解: 「サーバー」という語からリモートで動くものを想像しがちだが、MCPサーバーは実行場所を問わない。Claude Desktop がファイルシステムサーバーを起動する場合、それは同じマシン上のローカルプロセスである。「サーバー」は役割の名前であって、設置場所の名前ではない。
9.5.3 2つの層
Section titled “9.5.3 2つの層”MCPは2層で構成される。
データ層: JSON-RPC 2.0 に基づくメッセージのやりとりを定義する。ツール・リソース・プロンプトといった中核の概念(プリミティブ)はここにある。
トランスポート層: 通信路と認証を定義する。2つの方式がある。
| トランスポート | 用途 | 特徴 |
|---|---|---|
| stdio | ローカルのプロセス間通信 | 標準入出力を使う。ネットワーク越しのオーバーヘッドがなく高速。通常1クライアント専用 |
| Streamable HTTP | リモート接続 | HTTP POST + 必要に応じてSSE。多数のクライアントに対応。認証はOAuthが推奨 |
なお、旧来の HTTP+SSE トランスポート(2024-11-05 版のもの)は、プロトコルバージョン 2025-03-26 の時点ですでに非推奨とされていた。2026-07-28 版では、新設された機能ライフサイクル方針のもとで正式に「Deprecated」へ再分類され、最低12か月の移行期間を経て削除される見込みである。新規実装では Streamable HTTP を使う。
9.5.4 サーバーが提供する3つのプリミティブ
Section titled “9.5.4 サーバーが提供する3つのプリミティブ”| プリミティブ | 内容 | 主なメソッド |
|---|---|---|
| Tools(ツール) | AIが実行できる関数 | tools/list、tools/call |
| Resources(リソース) | 文脈として渡すデータ | resources/list、resources/read |
| Prompts(プロンプト) | 再利用可能なテンプレート | prompts/list、prompts/get |
Tools は第7章で扱ったものと同じ概念である。MCP経由で提供されるツールも、最終的にはモデルに name / description / inputSchema として渡される。したがって**第7章のツール設計の指針は、MCPサーバーを書くときにもそのまま適用される**。
Resources はツールとの違いが分かりにくいが、「実行するもの」か「読むもの」かで区別する。データベースのスキーマ定義、設定ファイル、ドキュメント — こうした参照用のデータはリソースとして提供する。
Prompts は、そのサーバーを使いこなすためのテンプレートである。「このDBに問い合わせるときの定型プロンプト」といった形で、サーバー作者のノウハウを配布できる。
9.5.5 ステートレス化 — 2026-07-28版の重要な変更
Section titled “9.5.5 ステートレス化 — 2026-07-28版の重要な変更”MCPの現行仕様(2026-07-28)は、それ以前と根本的に異なる。 この変更を知らないと、古い解説に基づいた誤った実装をすることになる。
変わったこと
| 項目 | 旧仕様(〜2025-11-25) | 現行(2026-07-28) |
|---|---|---|
| 接続の確立 | initialize / notifications/initialized のハンドシェイクが必須 |
廃止 |
| セッション | Mcp-Session-Id ヘッダで管理 |
廃止 |
| プロトコル版・能力の伝達 | 初期化時に1回ネゴシエート | 各リクエストの _meta で毎回運ぶ |
| 能力の確認 | 初期化時に必須 | server/discover で問い合わせる。サーバーは実装必須、クライアントは呼ぶかどうか任意 |
| 状態の保持 | ステートフル | ステートレス |
各リクエストの _meta に次のキーが載る。
| キー | 内容 | 必須度 |
|---|---|---|
io.modelcontextprotocol/protocolVersion |
プロトコルバージョン | 必須 |
io.modelcontextprotocol/clientCapabilities |
クライアントの能力 | 必須 |
io.modelcontextprotocol/clientInfo |
クライアントの識別情報 | SHOULD |
サーバー側も、各結果の _meta に io.modelcontextprotocol/serverInfo を載せるべきとされている。バージョンが合わない場合は UnsupportedProtocolVersionError が返る。
server/discoverの必須度に注意。 「能力の事前確認は任意」という理解はサーバー実装者にとっては誤りである。仕様は「サーバーはこのRPCを実装しなければならない(MUST)」と定めており、任意なのは「クライアントが呼ぶかどうか」だけである。9.6節でサーバーを作る際、これは実装必須の要件になる。
なぜこうしたのか。 ステートフルな設計では、同じクライアントのリクエストを同じサーバーインスタンスに送り続ける必要があった(スティッキールーティング)。ステートレスにしたことで、どのリクエストがどのインスタンスに届いてもよくなり、通常のロードバランサの背後に共有ストレージなしで展開できる。リモートMCPサーバーを大規模に運用するための変更である。
その他の主な変更
- Roots / Sampling / Logging が非推奨になった。仕様上は動作するが、新規実装は採用すべきでない。移行先として、Roots はツール引数やリソースURIで代替、Sampling は LLM プロバイダのAPIを直接使う、Logging は
stderrか OpenTelemetry を使う、と案内されている ping/logging/setLevel/notifications/roots/list_changedが削除された。ログレベルはリクエストごとに_metaのio.modelcontextprotocol/logLevelで指定する- 変更通知の仕組みが変わった。 HTTP GET エンドポイントと
resources/subscribe/unsubscribeが廃止され、subscriptions/listenという単一の長寿命ストリームに一本化された。クライアントは受け取りたい種別(toolsListChanged、resourcesListChangedなど)を明示的にオプトインする - Tasks が拡張機能に移行した。ブロッキングする
tasks/resultに代わりtasks/getでポーリングする方式になり、tasks/updateが追加された - すべての結果に
resultTypeフィールドが必須になった("complete"または"input_required") - 一覧・読み取りの応答に
ttlMsとcacheScopeが必須になり、キャッシュ可能になった - SSEストリームの再開機能が削除された(
Last-Event-IDとイベントID)。ストリームが切れたらリクエストごと再発行する - Streamable HTTP に標準ヘッダが必要になった。本文を解析せずにルーティングできるようにするためである
| ヘッダ | 対応する本文の値 | 必須となるリクエスト |
|---|---|---|
MCP-Protocol-Version |
_meta のプロトコルバージョン |
すべてのPOST |
Mcp-Method |
method |
すべてのリクエスト |
Mcp-Name |
params.name または params.uri |
tools/call、resources/read、prompts/get のみ |
ヘッダと本文の値が食い違うと HeaderMismatch(エラーコード -32020)で 400 が返る。ロードバランサがヘッダを見てルーティングし、サーバーが本文を見て実行する — という「真実の源が2つある」状態を防ぐための検証である。
- Multi Round-Trip Requests(MRTR) により、ステートレスなまま処理の途中でユーザー確認を挟めるようになった。サーバーは
resultType: "input_required"の結果にinputRequestsを載せて返し、クライアントはinputResponsesを付けて元のリクエストを再送する - 認可が強化された。認可サーバーは RFC 9207 の
issパラメータを含めるべき(SHOULD)とされ、クライアントはissが存在する場合はこれを検証しなければならない(MUST)。動的クライアント登録(DCR)は CIMD(Client ID Metadata Documents)を推奨する形で非推奨になった(後方互換のため当面は動作する)
セッションが必要な場合はどうするか。 セッション的な状態が必要なら、サーバーが発行した明示的なハンドルを、通常のツール引数として受け渡す。プロトコル層ではなくアプリケーション層で状態を扱う、という整理である。
9.6 MCPサーバーを作る
Section titled “9.6 MCPサーバーを作る”9.6.1 最小のサーバー
Section titled “9.6.1 最小のサーバー”公式SDKを使えば、プロトコルの詳細はほぼ隠蔽される。Python SDK での最小例を示す。
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("inventory")
@mcp.tool()def search_inventory(product_name: str, warehouse: str | None = None) -> str: """在庫を商品名で検索する。
在庫数や入荷予定を問われたときに使う。 部分一致で検索されるため、正式名称が不明でも構わない。 warehouse を指定すると特定倉庫に絞り込める(例: tokyo-1)。 返るのは現在庫数と次回入荷予定日のみで、価格情報は含まれない。 """ rows = db.search(product_name, warehouse) if not rows: return ( f"'{product_name}' に該当する在庫はありませんでした。" "商品名を短くするか、別の表記を試してください。" ) return "\n".join( f"- {r.name}(倉庫: {r.warehouse}) 在庫 {r.qty} 個 / 次回入荷 {r.next_arrival}" for r in rows[:20] )
@mcp.resource("schema://inventory")def inventory_schema() -> str: """在庫データベースのスキーマ定義。""" return SCHEMA_DOC
if __name__ == "__main__": mcp.run() # 既定では stdio トランスポートimport { Server } from '@modelcontextprotocol/sdk/server/index.js'import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema,} from '@modelcontextprotocol/sdk/types.js'import type { CallToolResult, ListResourcesResult, ListToolsResult, ReadResourceResult,} from '@modelcontextprotocol/sdk/types.js'
// TypeScript SDK の高水準 API(McpServer)は入力スキーマに zod を要求する。// 本書は依存を増やさない方針のため、低水準の Server に// JSON Schema をそのまま渡す形で書くconst mcp = new Server( { name: 'inventory', version: '1.0.0' }, { capabilities: { tools: {}, resources: {} } })
// Python 版はデコレータと docstring からツール定義を組み立てるが、// TypeScript SDK では一覧の応答と呼び出しの処理を別々に登録するmcp.setRequestHandler(ListToolsRequestSchema, (): ListToolsResult => ({ tools: [ { name: 'search_inventory', description: `在庫を商品名で検索する。
在庫数や入荷予定を問われたときに使う。部分一致で検索されるため、正式名称が不明でも構わない。warehouse を指定すると特定倉庫に絞り込める(例: tokyo-1)。返るのは現在庫数と次回入荷予定日のみで、価格情報は含まれない。`, inputSchema: { type: 'object', properties: { product_name: { type: 'string', description: '検索する商品名' }, warehouse: { type: 'string', description: '絞り込む倉庫ID。例: tokyo-1' }, }, required: ['product_name'], }, }, ],}))
mcp.setRequestHandler(CallToolRequestSchema, (request): CallToolResult => { if (request.params.name !== 'search_inventory') { throw new Error(`不明なツール: ${request.params.name}`) } const { product_name, warehouse } = request.params.arguments as { product_name: string warehouse?: string } const rows = db.search(product_name, warehouse) if (rows.length === 0) { return { content: [ { type: 'text', text: `'${product_name}' に該当する在庫はありませんでした。` + '商品名を短くするか、別の表記を試してください。', }, ], } } return { content: [ { type: 'text', text: rows .slice(0, 20) .map( (r) => `- ${r.name}(倉庫: ${r.warehouse}) 在庫 ${r.qty} 個 / 次回入荷 ${r.nextArrival}` ) .join('\n'), }, ], }})
/** 在庫データベースのスキーマ定義。 */mcp.setRequestHandler(ListResourcesRequestSchema, (): ListResourcesResult => ({ resources: [ { uri: 'schema://inventory', name: 'inventory_schema', description: '在庫データベースのスキーマ定義。', mimeType: 'text/plain', }, ],}))
mcp.setRequestHandler(ReadResourceRequestSchema, (request): ReadResourceResult => ({ contents: [ { uri: request.params.uri, mimeType: 'text/plain', text: SCHEMA_DOC }, ],}))
// Node には Python の `if __name__ == "__main__":` に相当する構文がないため、// エントリポイントのファイルではそのまま接続するawait mcp.connect(new StdioServerTransport()) // 既定では stdio トランスポート注目すべきは、docstring がそのままツールの説明文になる点である。第7章7.3.1節で述べた「3〜4文以上で、何を・いつ・パラメータの意味・制約を書く」という指針が、そのままここに適用される。型ヒントから inputSchema が自動生成されるため、スキーマを手書きする必要もない。
9.6.2 リモートサーバーとして公開する
Section titled “9.6.2 リモートサーバーとして公開する”Streamable HTTP で公開する場合、認証と認可を自前で設計する必要がある。
mcp = FastMCP("inventory", stateless_http=True)
if __name__ == "__main__": mcp.run(transport="streamable-http")import { createServer } from 'node:http'import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
// Python の stateless_http=True に相当。// sessionIdGenerator を undefined にするとセッションを保持しないconst transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined,})await mcp.connect(transport)
// Python 版は mcp.run(transport="streamable-http") が HTTP サーバーごと起動するが、// TypeScript SDK のトランスポートは HTTP サーバーに自分で接続するcreateServer((req, res) => { void transport.handleRequest(req, res)}).listen(3000)リモート公開時に検討すべき事項を挙げる。
| 項目 | 内容 |
|---|---|
| 認証 | OAuth 2.0 が推奨。Bearerトークン、APIキーも可 |
| 認可 | 誰がどのツールを使えるか。ツール単位・リソース単位で制御する |
| マルチテナント | リクエストごとにテナントを識別する。第8章8.4.2節の混入事故と同じ危険がある |
| レート制限 | 1クライアントが資源を占有しないようにする |
| 監査ログ | 誰がいつ何を実行したか(第12章12.2.2節。保持期間は12.3.3節) |
9.6.3 開発とデバッグ
Section titled “9.6.3 開発とデバッグ”MCP Inspector という公式ツールがあり、サーバーに接続してツール一覧の取得や個別実行を対話的に試せる。AIアプリケーションに繋ぐ前に、Inspector で単体検証しておくと切り分けが容易になる。
第7章7.7節で述べたツールの評価は、MCPサーバーでも同様に必要である。サーバーが正しく動くことと、モデルが正しく使えることは別問題である。
9.6.4 展開モード
Section titled “9.6.4 展開モード”| ローカル(stdio) | リモート(Streamable HTTP) | |
|---|---|---|
| 実行場所 | ユーザーのマシン | サーバー / クラウド |
| 起動 | ホストが子プロセスとして起動 | 常時稼働 |
| 認証 | 不要(プロセス権限で動く) | 必須 |
| データの所在 | ローカルに留まる | ネットワークを越える |
| 対象クライアント数 | 通常1 | 多数 |
| 適する用途 | ローカルファイル、開発ツール、個人用DB | SaaS連携、社内システム、チーム共有 |
ローカルサーバーはユーザーの権限で動く点に注意が必要である。ファイルシステムサーバーはユーザーが読めるファイルをすべて読める。この権限範囲は明示的に制限すべきである(第7章7.6.6節)。
9.7 MCPを採用する際の考慮
Section titled “9.7 MCPを採用する際の考慮”9.7.1 利点
Section titled “9.7.1 利点”- 既存のMCPサーバーを利用できる。 主要なSaaSやツールについては公式・コミュニティ製のサーバーが存在する
- 一度書けばどこでも使える。 社内システムのMCPサーバーを1つ作れば、複数のAIアプリから利用できる
- ツールの更新が動的に伝わる。 ただし現行仕様では、クライアントが
subscriptions/listenでtoolsListChangedを明示的にオプトインしている場合に限り、そのストリーム上にnotifications/tools/list_changedが届く。何もしなくても通知が飛んでくるわけではない。あわせて、一覧応答のttlMsを使ってポーリング頻度を抑える設計も可能である
9.7.2 注意点
Section titled “9.7.2 注意点”① ツール定義がコンテキストを消費する。 複数のMCPサーバーを接続すると、全サーバーのツール定義が毎ステップ送られる。第7章7.2.2節で述べた「ツールは多ければ良いわけではない」という原則は、MCPでも変わらない。接続するサーバーは必要なものに絞る。
② 名前の衝突が起きる。 複数サーバーが search というツールを提供すると、モデルは区別できない。第7章7.2.3節の名前空間が必要になる。
③ 信頼できないサーバーは危険である。 これが最も重要な点である。
9.7.3 セキュリティ
Section titled “9.7.3 セキュリティ”MCP仕様自身が、この点を強く警告している。要点を引用に沿って整理する。
ツールは任意コード実行と等価である。 MCPサーバーのツールは、あなたのマシン上で、あるいはあなたの権限で、任意の処理を行いうる。信頼できないサーバーを接続することは、信頼できないプログラムを実行することと同じである。
ツールの説明文は信頼できない。 仕様は「ツールの挙動に関する説明(アノテーションを含む)は、信頼できるサーバーから得たものでない限り、信頼できないものとして扱うべきである」と明記している。悪意あるサーバーは、説明文にプロンプトインジェクションを仕込める。第6章6.4.2節で扱った外部コンテンツの隔離と同じ問題が、ツール定義そのものに存在する。
ユーザーの同意と制御が原則である。 仕様が掲げる原則は3つある。
- ユーザーの同意と制御 — データアクセスと操作にユーザーが明示的に同意し、理解していること
- データのプライバシー — ホストは同意なしにユーザーデータをサーバーへ露出させない
- ツールの安全性 — ホストはツール実行前にユーザーの明示的な同意を得る
実務上の指針をまとめる。
| リスク | 対策 |
|---|---|
| 悪意あるサーバー | 提供元を確認する。社内利用は自前サーバーか監査済みのものに限る |
| 説明文へのインジェクション | 接続時にツール定義を人間がレビューする。動的な変更を検知する |
| 過剰な権限 | ローカルサーバーの権限範囲を制限する。リモートは最小権限の認可を設計する |
| データの外部流出 | どのサーバーがどのデータに触れるかを把握する |
| 副作用のある操作 | 実行前の承認を必須にする(第5章5.7.1節) |
MCPは接続を容易にする規格であり、その容易さがそのままリスクにもなる。 「便利そうだから繋ぐ」のではなく、第13章で扱う脅威モデルの観点から評価してから接続すべきである。
9.8 まとめ
Section titled “9.8 まとめ”- 主要なアーキテクチャは4つ。ReAct(基本形、5〜15ステップ)、RAGエージェント(検索が中心)、Planner-Executor(15ステップ以上、計画の承認やコスト最適化が必要)、DAG(並列実行できる作業が多い)
- ReActの弱点は計画性の欠如である。常に次の一手しか見ておらず、長期タスクで迷走しやすい
- Planner-Executor では再計画の経路を必ず用意する。計画を絶対視すると前提が崩れたときに失敗する
- DAGは並列性が最大の利点だが、事前にグラフを構築する必要があるため動的なタスクには向かない
- 単一のアーキテクチャに統一する必要はない。混在させるのが実務的である
- CoTは推論モデルを使う場合ほぼ不要である。使うのは推論を構造化したい・監査ログに残したい場合に限る
- ToTは10〜100倍のトークンを消費する。 業務エージェントでの出番は少なく、「複数案を並列生成して評価する1段版」が現実的な代替になる
- MCP は M×N 問題を M+N に変える標準規格である。ホスト(AIアプリ)・クライアント(接続1本につき1つ)・サーバー(機能提供側、実行場所は問わない)の3者で構成される
- サーバーのプリミティブは Tools / Resources / Prompts。ツール設計の指針(第7章)はMCPサーバーにもそのまま適用される
- 現行仕様(2026-07-28)はステートレス化された。 直前の 2025-11-25 版まで必須だった
initializeハンドシェイクとセッションIDが廃止され、各リクエストが_metaでプロトコル版と能力を運ぶ。Roots / Sampling / Logging と HTTP+SSE は非推奨 server/discoverはサーバー側は実装必須、クライアントが呼ぶかは任意。変更通知はsubscriptions/listenへのオプトインが前提になった- 古い解説記事やAIの生成コードは旧仕様に基づいていることが多い。実装前に必ず現行仕様を確認する
- 展開モードは stdio(ローカル) と Streamable HTTP(リモート)。後者は認証・認可・マルチテナント分離の設計が必須
- MCPのツールは任意コード実行と等価である。 仕様自身が「ツールの説明文は信頼できないサーバーからのものは信頼するな」と警告している。接続は脅威モデルに基づいて判断する
問1 次の4つのタスクについて、9.2節のどのアーキテクチャが最も適するか判断し、理由を述べよ。複数を組み合わせるべき場合は、その構成を示すこと。
(a) 顧客IDを渡され、購買履歴・問い合わせ履歴・契約情報を集めて統合レポートを作る (b) 「先週リリースした機能でエラー率が上がった原因を調べて」という依頼に答える (c) 社内規程について質問を受け、根拠を示して回答する (d) 仕様書を渡され、20ファイル程度の変更を伴う機能を実装し、テストを通す
問2 9.2.3節の Planner-Executor について答えよ。
(a) 計画立案に Claude Opus 5 を effort: high で、実行に Claude Haiku 4.5 を effort: low で使うとする。全20ステップのうち計画が1回、実行が19回のとき、すべてを Opus 5 の high で行う場合と比べてコストはどう変わるか。第2章の価格表を用い、必要な仮定を明示して概算せよ
(b) 再計画が必要かどうかの判定を、コードのルールではなくLLMに任せる場合の利点と欠点を述べよ
問3
9.2.4節の run_dag 実装について答えよ。
(a) 循環依存があるノード集合を渡した場合、この実装はどう振る舞うか。エラーメッセージは十分か (b) あるノードの実行が失敗した場合、現在の実装ではどうなるか。部分的な失敗を許容する設計にするには、どう変更すべきか (c) 依存先の結果が巨大な場合(たとえば1万行のクエリ結果)、この実装にはどのような問題が生じるか。第7章・第8章の内容を踏まえて対策を述べよ
問4 MCPについて、次の記述はいずれも誤りである。それぞれ何が誤りか、正しくはどうかを述べよ。
(a) 「MCPサーバーはリモートのクラウド上で動くプログラムである」
(b) 「MCPクライアントは、接続するサーバーの数によらずホストに1つだけ存在する」
(c) 「MCPの接続を確立するには、まず initialize リクエストを送ってプロトコルバージョンをネゴシエートする」
(d) 「MCPサーバーが提供するツールの説明文は、プロトコルで検証されているため信頼してよい」
問5 社内の顧客管理システムをMCPサーバーとして公開し、複数の部署のAIアシスタントから利用できるようにしたい。
(a) ローカル(stdio)とリモート(Streamable HTTP)のどちらを選ぶべきか。理由を述べよ (b) 9.6.2節の表を参考に、設計すべきセキュリティ要件を4つ挙げ、それぞれ具体的な実装方針を述べよ (c) 営業部のアシスタントが他部署の顧客データを閲覧できてしまう事故を防ぐには、どこで制御すべきか。第8章8.4.2節の議論と関連づけて述べよ
問6 あるチームが、便利そうなMCPサーバーをコミュニティのリポジトリから見つけ、開発者の端末に接続しようとしている。9.7.3節を踏まえ、接続前に確認すべきことを5つ挙げ、それぞれなぜ必要かを説明せよ。
参考文献・出典
Section titled “参考文献・出典”| 出典 | 内容 | 参照日 |
|---|---|---|
| MCP — Architecture Overview | ホスト/クライアント/サーバーの役割、データ層とトランスポート層、プリミティブとメソッド名 | 2026-07-29 |
| MCP — Specification (latest) | 現行仕様(2026-07-28)、ステートレスな基本プロトコル、セキュリティ原則、拡張機能 | 2026-07-29 |
| MCP Blog — The 2026-07-28 Specification | ステートレス化、initialize とセッションIDの廃止、server/discover、非推奨事項、認可の強化 |
2026-07-29 |
| MCP — Introduction | MCPの目的、対応クライアントの広がり | 2026-07-29 |
| MCP — Key Changes (2026-07-28 changelog) | 直前版が 2025-11-25 であること、server/discover のMUST、subscriptions/listen、削除された ping/logging/setLevel、iss の SHOULD/MUST、HTTP+SSE の再分類 |
2026-07-29 |
| MCP — Streamable HTTP Transport | MCP-Protocol-Version/Mcp-Method/Mcp-Name の必須範囲、HeaderMismatch(-32020)、後方互換の判定手順 |
2026-07-29 |
| Stacktree — MCP 2026-07-28 spec: what changed, what breaks | SEP番号つきの変更点の整理(裏取り用) | 2026-07-29 |
| Agentic Reasoning Patterns (2026) | ReAct / Reflexion / Plan-and-Execute / ToT の比較、ToTのトークンコスト | 2026-07-29 |
| Yao et al., “ReAct: Synergizing Reasoning and Acting in Language Models” (2022) | ReActの原論文 | — |
| Wei et al., “Chain-of-Thought Prompting Elicits Reasoning in Large Language Models” (2022) | CoTの原論文 | — |
| Yao et al., “Tree of Thoughts: Deliberate Problem Solving with Large Language Models” (2023) | ToTの原論文 | — |
| roadmap.sh — AI Agents Roadmap | 章構成の基準 | 2026-07-29 |
次章予告: 第10章ではエージェントの構築方法を扱う。スクラッチ実装、各社のネイティブなFunction Calling、そして LangChain / LlamaIndex / CrewAI / AutoGen といったフレームワーク — それぞれの利点と、どれを選ぶべきかを整理する。本書で最も実装寄りの章になる。
Built with Astro ・ Deployed on Cloudflare Pages
© 2026 watakumi — made with 💜 & ☕ ・watakumi.page