第10章 エージェントの構築(Building Agents)
10.1 概要
Section titled “10.1 概要”ここまでの9章で、エージェントを構成する要素はひととおり揃った。本章はそれをどう実装するかという問いに答える。
選択肢は大きく3つある。
本章の立場を先に述べておく。まずスクラッチで一度書くべきである。 第5章で組み立てたループは50行程度であり、書けば「エージェントとは結局のところ while ループとツールディスパッチである」ということが体で分かる。この理解がないままフレームワークに乗ると、問題が起きたときに何が起きているのか分からなくなる。
そのうえで、本番システムでは適切な抽象化に乗ることを検討する。永続化、再開、可観測性、マルチエージェント — こうした機能を自前で作るのは相当な労力である。
本章の情報の鮮度について: フレームワークの世界は変化が速い。本章では2026年7月時点の状況を記す。ただし、AutoGen がメンテナンスモードに移行し、OpenAI Assistants API が2026年8月26日にサンセットするなど、大きな変化が進行中である。実装前には必ず公式ドキュメントで現況を確認してほしい。
10.2 スクラッチで作る
Section titled “10.2 スクラッチで作る”10.2.1 何を自分で書くことになるか
Section titled “10.2.1 何を自分で書くことになるか”第5章5.3節でループ本体は示した。ここでは、その周辺で自分で書く必要があるものを整理する。実際に本番運用しようとすると、ループ本体より周辺のほうが分量が多くなる。
| 要素 | 内容 | 参照 |
|---|---|---|
| LLM APIの呼び出し | 認証、リクエスト構築、レスポンスのパース | 10.2.2 |
| エージェントループ | 継続・終了の判定、履歴の管理 | 第5章5.3節 |
| ツールのディスパッチ | 名前から実装への解決、引数の検証 | 第5章5.3.1節 |
| 出力のパース | 構造化出力の取り出しと検証 | 10.2.3 |
| エラーとレート制限 | 再試行、バックオフ、部分的失敗の扱い | 10.2.4 |
| コンテキスト管理 | 切り詰め、圧縮 | 第8章8.3節 |
| 予算と上限 | ステップ数・コスト・時間 | 第5章5.4.1節 |
| トレースとログ | 何が起きたかの記録 | 第12章 |
| 永続化と再開 | 中断したところから続ける | 10.2.5 |
10.2.2 LLM APIの直接呼び出し
Section titled “10.2.2 LLM APIの直接呼び出し”第5章で見たとおり、基本形は「ツール定義を添えて呼び、tool_use が返ったら実行して tool_result を返す」の繰り返しである。ここで補足しておきたいのは、プロバイダ非依存にしたい場合の抽象化である。
複数のプロバイダに対応するなら、差異を吸収する薄い層を挟む。
from abc import ABC, abstractmethodfrom dataclasses import dataclassfrom typing import Any
@dataclassclass ToolCall: id: str name: str arguments: dict[str, Any]
@dataclassclass LLMResponse: text: str tool_calls: list[ToolCall] stop_reason: str # "end_turn" | "tool_use" | "max_tokens" | ... input_tokens: int output_tokens: int
class LLMProvider(ABC): """プロバイダ間の差異を吸収する最小のインターフェース。"""
@abstractmethod def complete( self, system: str, messages: list[dict], tools: list[dict], max_tokens: int, ) -> LLMResponse: ...
@abstractmethod def format_tool_result(self, call_id: str, content: str, is_error: bool) -> dict: ...// Python 版は dataclass と ABC で書くが、TypeScript では interface で表すinterface ToolCall { id: string name: string arguments: Record<string, unknown>}
interface LLMResponse { text: string toolCalls: ToolCall[] stopReason: string // "end_turn" | "tool_use" | "max_tokens" | ... inputTokens: number outputTokens: number}
/** プロバイダ間の差異を吸収する最小のインターフェース。 */interface LLMProvider { // Python 版は同期メソッドだが、JavaScript のHTTP呼び出しは同期にできない。 // 戻り値は Promise になる complete( system: string, messages: Record<string, unknown>[], tools: Record<string, unknown>[], maxTokens: number ): Promise<LLMResponse>
formatToolResult( callId: string, content: string, isError: boolean ): Record<string, unknown>}ただし、この抽象化を最初から作るべきかは慎重に判断してほしい。 第2章2.3節で見たとおり、プロバイダごとにパラメータ体系が異なる。たとえば Anthropic にはペナルティパラメータがなく、temperature の有効範囲も違う。無理に共通化すると、最小公倍数的な貧弱なインターフェースになるか、抽象化が漏れて結局分岐だらけになる。
単一プロバイダで始め、必要になってから抽象化するのが実務的である。
10.2.3 モデル出力のパース
Section titled “10.2.3 モデル出力のパース”ツール呼び出しについては、各社のAPIが構造化された形(tool_use ブロックなど)で返してくれるため、パースの苦労はほとんどない。問題になるのは、ツール以外で構造化データが欲しい場合である。
素朴にやるなら「JSONで返して」とプロンプトで頼むことになるが、これは脆い。前後に説明文が付く、コードフェンスで囲まれる、末尾のカンマが混入する、といった揺れが起きる。
対策は3段階で考える。
① 構造化出力の機能を使う(最良)
各社が、スキーマへの適合を保証する機能を提供している。OpenAIでは strict: true による Structured Outputs、Anthropicではツールを1つだけ定義して tool_choice でそれを強制する方法が使える。
# Anthropic: 構造化出力を「ツールの強制呼び出し」として実現するEXTRACT_TOOL = { "name": "record_extraction", "description": "問い合わせから抽出した情報を記録する。", "input_schema": { "type": "object", "properties": { "category": {"type": "string", "enum": ["billing", "technical", "sales"]}, "urgency": {"type": "integer", "minimum": 1, "maximum": 5}, "summary": {"type": "string"}, }, "required": ["category", "urgency", "summary"], },}
response = client.messages.create( model="claude-sonnet-5", max_tokens=1024, temperature=0, # 第2章2.3.1節 tools=[EXTRACT_TOOL], tool_choice={"type": "tool", "name": "record_extraction"}, # 必ず呼ばせる messages=[{"role": "user", "content": inquiry}],)extracted = next(b.input for b in response.content if b.type == "tool_use")// Anthropic: 構造化出力を「ツールの強制呼び出し」として実現するconst EXTRACT_TOOL: Anthropic.Tool = { name: 'record_extraction', description: '問い合わせから抽出した情報を記録する。', input_schema: { type: 'object', properties: { category: { type: 'string', enum: ['billing', 'technical', 'sales'] }, urgency: { type: 'integer', minimum: 1, maximum: 5 }, summary: { type: 'string' }, }, required: ['category', 'urgency', 'summary'], },}
const response = await client.messages.create({ model: 'claude-sonnet-5', max_tokens: 1024, temperature: 0, // 第2章2.3.1節 tools: [EXTRACT_TOOL], tool_choice: { type: 'tool', name: 'record_extraction' }, // 必ず呼ばせる messages: [{ role: 'user', content: inquiry }],})// Python の next() は該当がなければ StopIteration を投げるが、// find は undefined を返すだけなので、ここで明示的に確かめるconst block = response.content.find((b) => b.type === 'tool_use')if (block === undefined) throw new Error('tool_use ブロックがありません')const extracted = block.input注意: 手動の extended thinking(
thinking: {"type": "enabled"})を使っている場合、tool_choiceのany/toolによる強制は使えない。adaptive thinking なら併用できる(第3章3.3節)。
② 検証してから使う
構造化出力を使っていても、値の妥当性(業務上ありえない組み合わせなど)までは保証されない。Pydanticなどで検証する。
③ 検証に失敗したらモデルに直させる
パースや検証に失敗したら、エラー内容を添えて再試行する。第7章7.5.1節の「エラーは回復のための情報である」という原則は、ここにも適用される。
from pydantic import BaseModel, ValidationError, Field
class Extraction(BaseModel): category: str = Field(pattern="^(billing|technical|sales)$") urgency: int = Field(ge=1, le=5) summary: str = Field(min_length=1, max_length=500)
def extract_with_repair(inquiry: str, max_attempts: int = 3) -> Extraction: messages = [{"role": "user", "content": inquiry}] for attempt in range(max_attempts): response = call_with_tool(messages) block = next(b for b in response.content if b.type == "tool_use") try: return Extraction(**block.input) except ValidationError as e: if attempt == max_attempts - 1: raise # 何がどう不正だったかを具体的に伝えて直させる。 # 検証エラーは必ず tool_result として返すこと(下の注意を参照) messages += [ {"role": "assistant", "content": response.content}, {"role": "user", "content": [{ "type": "tool_result", "tool_use_id": block.id, "content": f"抽出結果が検証に失敗しました:\n{e}\n" "指摘された項目を修正して、もう一度 record_extraction を呼んでください。", "is_error": True, }]}, ] raise RuntimeError("到達不能")/** Python 版は pydantic の BaseModel で宣言的に検証するが、 * TypeScript の型は実行時に残らない。型と検証関数を対にして手書きする。 */interface Extraction { category: 'billing' | 'technical' | 'sales' urgency: number summary: string}
class ValidationError extends Error {}
/** pydantic と同じく、不正な項目をすべて集めてから1つの例外にまとめる。 */function parseExtraction(value: unknown): Extraction { const v = typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : {} const errors: string[] = []
const category = v['category'] if ( category !== 'billing' && category !== 'technical' && category !== 'sales' ) { errors.push( "category: 'billing' | 'technical' | 'sales' のいずれかである必要があります" + `(受け取った値: ${JSON.stringify(category)})` ) } const urgency = v['urgency'] if ( typeof urgency !== 'number' || !Number.isInteger(urgency) || urgency < 1 || urgency > 5 ) { errors.push( 'urgency: 1以上5以下の整数である必要があります' + `(受け取った値: ${JSON.stringify(urgency)})` ) } const summary = v['summary'] if (typeof summary !== 'string' || summary.length < 1 || summary.length > 500) { errors.push('summary: 1文字以上500文字以下の文字列である必要があります') }
if (errors.length > 0) throw new ValidationError(errors.join('\n')) return value as Extraction // 上の検査を通っているので形は保証されている}
async function extractWithRepair( inquiry: string, maxAttempts: number = 3): Promise<Extraction> { const messages: Anthropic.MessageParam[] = [{ role: 'user', content: inquiry }] for (let attempt = 0; attempt < maxAttempts; attempt++) { const response = await callWithTool(messages) const block = response.content.find((b) => b.type === 'tool_use') if (block === undefined) throw new Error('tool_use ブロックがありません') try { return parseExtraction(block.input) } catch (e) { if (!(e instanceof ValidationError)) throw e if (attempt === maxAttempts - 1) throw e // 何がどう不正だったかを具体的に伝えて直させる。 // 検証エラーは必ず tool_result として返すこと(下の注意を参照) messages.push( { role: 'assistant', content: response.content }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: block.id, content: `抽出結果が検証に失敗しました:\n${e.message}\n` + '指摘された項目を修正して、もう一度 record_extraction を呼んでください。', is_error: true, }, ], } ) } } throw new Error('到達不能')}
tool_resultは必ず直後に置く。 ここで「検証に失敗しました」を素のテキストのuserメッセージとして送りたくなるが、これはAPIエラーになる。仕様上、tool_resultブロックは対応するtool_useブロックの直後のメッセージに置かなければならず、あいだに他のメッセージを挟むことはできない。違反するとtool_use ids were found without tool_result blocks immediately afterというエラーが返る。この制約のため、
tool_useブロックのidを保持しておく必要がある。b.inputだけを取り出してidを捨てると、再試行の実装で行き詰まる。第5章5.3.2節でブロックそのものを保持していたのは、このためでもある。
10.2.4 エラーとレート制限の処理
Section titled “10.2.4 エラーとレート制限の処理”第1章1.4.2節で再試行の基本形を示した。ここではエージェント特有の論点を扱う。
① レート制限は2種類ある。 リクエスト数(RPM)とトークン数(TPM)である。エージェントは1タスクで何十回も呼ぶため、どちらも枯渇しうる。特にコンテキストが膨らむとTPMのほうが先に尽きる。
② 再試行にはコストがかかる。 通常のAPIと違い、エージェントの再試行は「巨大な入力を再送する」ことを意味する。第2章2.4節で見たとおり、20ステップ目の入力は数万トークンに達しうる。再試行の回数は控えめに設定し、それより並行度を下げるほうが有効なことが多い。
③ 部分的失敗をどう扱うか。 並列にツールを実行しているとき、1つだけ失敗した場合の扱いを決めておく必要がある。原則は「失敗したものだけをエラーとして返し、成功したものはそのまま返す」である。全体を失敗にするとモデルは何が起きたか分からない。
import asyncio
async def execute_all(registry, tool_uses) -> list[dict]: """並列実行。1つの失敗が他を巻き込まないようにする。""" outcomes = await asyncio.gather( *(registry.execute_async(tu.name, tu.input) for tu in tool_uses), return_exceptions=True, # 例外もまとめて受け取る ) results = [] for tu, out in zip(tool_uses, outcomes): # BaseException で受けること。asyncio.CancelledError は Python 3.8 以降 # Exception ではなく BaseException の直接の子であり、Exception だけで # 判定するとタイムアウト時にこの分岐に入らず AttributeError になる if isinstance(out, BaseException): results.append({ "type": "tool_result", "tool_use_id": tu.id, "content": f"ツールの実行に失敗しました: {type(out).__name__}: {out}", "is_error": True, }) else: results.append({ "type": "tool_result", "tool_use_id": tu.id, "content": out.content, "is_error": out.is_error, }) return results/** 並列実行。1つの失敗が他を巻き込まないようにする。 */async function executeAll( registry: AsyncToolRegistry, toolUses: Anthropic.ToolUseBlock[]): Promise<Anthropic.ToolResultBlockParam[]> { // Python の gather(return_exceptions=True) に相当するのが Promise.allSettled。 // Promise.all は最初の reject でただちに失敗し、成功した結果まで捨ててしまう const outcomes = await Promise.allSettled( toolUses.map((tu) => registry.executeAsync(tu.name, tu.input as Record<string, unknown>) ) ) const results: Anthropic.ToolResultBlockParam[] = [] for (const [i, tu] of toolUses.entries()) { const out = outcomes[i]! // allSettled は入力と同じ長さの配列を返す // Python の BaseException に相当する区別は JavaScript にはない。 // reject された値は Error とは限らないため、Error かどうかを確かめてから名前を取る if (out.status === 'rejected') { const reason: unknown = out.reason const detail = reason instanceof Error ? `${reason.name}: ${reason.message}` : String(reason) results.push({ type: 'tool_result', tool_use_id: tu.id, content: `ツールの実行に失敗しました: ${detail}`, is_error: true, }) } else { results.push({ type: 'tool_result', tool_use_id: tu.id, content: out.value.content, is_error: out.value.isError, }) } } return results}return_exceptions=True が要点である。これがないと、1つの例外が gather 全体を中断させ、成功した結果まで捨てることになる。
④ 並行度を制御する。 複数のエージェントを同時に走らせる場合、レート制限に当たりやすい。セマフォで上限を設ける。
class RateLimitedClient: def __init__(self, client, max_concurrent: int = 5): self._client = client self._sem = asyncio.Semaphore(max_concurrent)
async def create(self, **kwargs): async with self._sem: return await self._client.messages.create(**kwargs)/** JavaScript には asyncio.Semaphore に相当する組み込みがないため、最小限を自前で書く。 */class Semaphore { #permits: number readonly #waiting: (() => void)[] = []
constructor(permits: number) { this.#permits = permits }
async acquire(): Promise<void> { if (this.#permits > 0) { this.#permits -= 1 return } // 起こされた時点で、解放側から空き枠を直接譲られている await new Promise<void>((resolve) => this.#waiting.push(resolve)) }
release(): void { const next = this.#waiting.shift() // 待機者がいれば枠を数え直さず、そのまま渡す。 // 一度 permits に戻すと、待機者より後から来た呼び出しに割り込まれる if (next === undefined) this.#permits += 1 else next() }
/** Python の `async with sem:` に相当。finally で必ず解放する。 */ async run<T>(fn: () => Promise<T>): Promise<T> { await this.acquire() try { return await fn() } finally { this.release() } }}
class RateLimitedClient { readonly #client: Anthropic readonly #sem: Semaphore
constructor(client: Anthropic, maxConcurrent: number = 5) { this.#client = client this.#sem = new Semaphore(maxConcurrent) }
async create( params: Anthropic.MessageCreateParamsNonStreaming ): Promise<Anthropic.Message> { return this.#sem.run(() => this.#client.messages.create(params)) }}10.2.5 永続化と再開
Section titled “10.2.5 永続化と再開”長時間動くエージェントでは、途中で落ちたときに最初からやり直さない仕組みが要る。これはスクラッチ実装で最も面倒な部分であり、フレームワークを検討する主な動機のひとつでもある。
最低限必要なのは、各ステップの終了時に次を保存することである。
- メッセージ履歴(そのまま復元できる形で)
- 予算の消費状況(第5章5.4.1節)
- ツールの副作用の記録(どこまで実行済みか)
3つめが重要である。「メールを送信した」という副作用は、再開時に繰り返してはならない。第7章7.5.3節の冪等キーが効いてくる。
10.3 ネイティブなツール呼び出し
Section titled “10.3 ネイティブなツール呼び出し”各プロバイダは、ツール呼び出しをAPIレベルで提供している。第4章・第5章で見たAnthropicの形式を基準に、他社との違いを整理する。
10.3.1 Anthropic — Tool Use
Section titled “10.3.1 Anthropic — Tool Use”すでに詳しく見たので要点のみ再掲する。
tools = [{ "name": "get_weather", "description": "...", "input_schema": {"type": "object", "properties": {...}, "required": [...]},}]// Python の {...} / [...] は Ellipsis を含むリテラルだが、// TypeScript に相当する記法はないためコメントで省略を示すconst tools: Anthropic.Tool[] = [ { name: 'get_weather', description: '...', input_schema: { type: 'object', properties: { /* ... */ }, required: [/* ... */], }, },]- ツール定義はフラット(
name/description/input_schemaがトップレベル) - モデルの要求は
tool_useブロック、結果はtool_resultブロックで返す tool_choiceはauto/any/tool/none(第5章5.4.1節)- 複数ツールの並列呼び出しに対応
Tool Runner SDK という補助機能もある。ループを自動で回してくれるもので、スクラッチとフレームワークの中間に位置する。
from anthropic import Anthropic, beta_toolimport json
client = Anthropic()
@beta_tooldef get_weather(location: str, unit: str = "celsius") -> str: """指定した地点の現在の天気を取得する。
Args: location: 都市名と国名。例: Tokyo, Japan unit: 温度の単位。'celsius' または 'fahrenheit' """ return json.dumps({"temperature": "20°C", "condition": "晴れ"})
runner = client.beta.messages.tool_runner( model="claude-opus-5", max_tokens=1024, tools=[get_weather], messages=[{"role": "user", "content": "東京の天気は?"}],)
final_message = runner.until_done()import Anthropic from '@anthropic-ai/sdk'import { betaTool } from '@anthropic-ai/sdk/helpers/beta/json-schema'
const client = new Anthropic()
// Python 版は関数の型注釈と docstring からスキーマを起こすが、// TypeScript の型は実行時に残らないため inputSchema を明示するconst getWeather = betaTool({ name: 'get_weather', description: '指定した地点の現在の天気を取得する。', inputSchema: { type: 'object', properties: { location: { type: 'string', description: '都市名と国名。例: Tokyo, Japan' }, unit: { type: 'string', enum: ['celsius', 'fahrenheit'], description: "温度の単位。'celsius' または 'fahrenheit'", }, }, required: ['location'], }, run: () => JSON.stringify({ temperature: '20°C', condition: '晴れ' }),})
const runner = client.beta.messages.toolRunner({ model: 'claude-opus-5', max_tokens: 1024, tools: [getWeather], messages: [{ role: 'user', content: '東京の天気は?' }],})
const finalMessage = await runner.runUntilDone() // Python の until_done() に相当デコレータが型ヒントとdocstringから自動でスキーマを生成する点が便利である。ただし人間の承認を挟みたい場合やカスタムのログを取りたい場合は、手動ループのほうが適するとドキュメント自身が述べている。第5章で組み立てたようなループが必要になる場面は残る。
10.3.2 OpenAI — Function Calling
Section titled “10.3.2 OpenAI — Function Calling”OpenAIには2つのAPIがあり、ツール定義の形が異なる。 これが混乱の元になるので明確にしておく。
# Responses API(現行の推奨)— フラット{ "type": "function", "name": "get_horoscope", "description": "...", "parameters": {...}, "strict": True,}
# Chat Completions API(従来)— "function" キーの下に入れ子{ "type": "function", "function": { "name": "get_horoscope", "description": "...", "parameters": {...}, "strict": True, },}// Responses API(現行の推奨)— フラットconst responsesTool: OpenAI.Responses.FunctionTool = { type: 'function', name: 'get_horoscope', description: '...', parameters: { /* ... */ }, strict: true,}
// Chat Completions API(従来)— "function" キーの下に入れ子const chatCompletionsTool: OpenAI.Chat.Completions.ChatCompletionFunctionTool = { type: 'function', function: { name: 'get_horoscope', description: '...', parameters: { /* ... */ }, strict: true, },}第1章1.4.3節で「JSON Schemaを包む外側の構造はプロバイダごとに異なる」と述べたが、同じプロバイダの中でもAPIによって異なる。コピーしたコードが動かない原因の定番である。
Responses API の完全な往復は次のとおり。
from openai import OpenAIimport json
client = OpenAI()
tools = [{ "type": "function", "name": "get_weather", "description": "指定した地点の現在の天気を取得する。", "parameters": { "type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"], "additionalProperties": False, # strict モードでは必須 }, "strict": True,}]
input_list = [{"role": "user", "content": "東京の天気は?"}]
response = client.responses.create(model="gpt-5.6-terra", tools=tools, input=input_list)input_list += response.output
for item in response.output: if item.type == "function_call": args = json.loads(item.arguments) # arguments は JSON 文字列 result = execute_weather(args["location"]) input_list.append({ "type": "function_call_output", "call_id": item.call_id, # tool_use_id に相当 "output": result, })
final = client.responses.create(model="gpt-5.6-terra", tools=tools, input=input_list)print(final.output_text)import OpenAI from 'openai'
const client = new OpenAI()
const tools: OpenAI.Responses.FunctionTool[] = [ { type: 'function', name: 'get_weather', description: '指定した地点の現在の天気を取得する。', parameters: { type: 'object', properties: { location: { type: 'string' } }, required: ['location'], additionalProperties: false, // strict モードでは必須 }, strict: true, },]
const inputList: OpenAI.Responses.ResponseInput = [ { role: 'user', content: '東京の天気は?' },]
const response = await client.responses.create({ model: 'gpt-5.6-terra', tools, input: inputList,})// Python の `input_list += response.output` に相当。// 出力アイテムの型は入力アイテムの型より広いため、ここで絞り込むinputList.push(...(response.output as OpenAI.Responses.ResponseInput))
for (const item of response.output) { if (item.type === 'function_call') { const args = JSON.parse(item.arguments) as { location: string } // arguments は JSON 文字列 const result = executeWeather(args.location) inputList.push({ type: 'function_call_output', call_id: item.call_id, // tool_use_id に相当 output: result, }) }}
const final = await client.responses.create({ model: 'gpt-5.6-terra', tools, input: inputList,})console.log(final.output_text)Anthropicとの主な差異を整理する。
| Anthropic | OpenAI (Responses) | |
|---|---|---|
| スキーマのキー | input_schema |
parameters |
| 引数の型 | dict(パース済み) | JSON文字列(自分で json.loads) |
| 呼び出しのID | tool_use.id |
function_call.call_id |
| 結果の返し方 | tool_result ブロック |
function_call_output |
| スキーマ厳格化 | — | strict: True(要 additionalProperties: false と全項目 required) |
strict: True は有用な機能である。スキーマへの適合が保証されるため、パースエラーが実質的になくなる。制約として、すべてのフィールドを required にする必要があり、任意項目は型に null を含める形で表現する。
10.3.3 OpenAI — Assistants API(サンセット間近)
Section titled “10.3.3 OpenAI — Assistants API(サンセット間近)”ロードマップは Assistants API を挙げているが、2026年8月26日にサンセットする。本書執筆時点(2026年7月29日)から1か月を切っている。
新規に採用してはならない。 既存の実装は Responses API と Conversations API への移行が必要である。
あわせて、他にも移行が必要な機能がある。
| 機能 | 非推奨化 | 停止 | 移行先 |
|---|---|---|---|
| Assistants API | 2025-08-26 | 2026-08-26 | Responses API + Conversations API |
| Agent Builder | 2026-06-03 | 2026-11-30 | ChatKit / Agents SDK / ChatGPT Workspace Agents |
| Evals Platform | 2026-06-03 | 2026-11-30 | Promptfoo など(第11章) |
| Reusable Prompts | 2026-06-03 | 2026-11-30 | プロンプトをコードに直接持つ |
この表は、本書が繰り返し述べてきた「プロバイダ固有の高水準機能に深く依存すると移行コストが発生する」という論点の実例である。 Assistants API はスレッド管理・ファイル検索・コード実行を統合した便利な仕組みだったが、2年ほどで畳まれることになった。
猶予期間にも注目してほしい。Assistants API は非推奨化からサンセットまでちょうど1年が与えられたが、Agent Builder や Evals Platform は約6か月である。抽象度の高い機能ほど、その寿命と移行猶予を見積もったうえで採用する必要がある。
10.3.4 Google — Gemini Function Calling
Section titled “10.3.4 Google — Gemini Function Calling”Gemini にも2つのAPIがある。 OpenAI と同じ構図であり、こちらのほうが混乱しやすい。現在の推奨は Interactions API で、従来の generateContent はレガシー扱いになっている。
Interactions API(現行の推奨)
- ツール定義はフラット(
type/name/description/parametersがトップレベル)。OpenAI の Responses API とよく似た形である - モデルの要求は
function_callステップ(name/arguments/id)。argumentsはパース済みのオブジェクトであり、OpenAI のように JSON 文字列ではない - 結果は
function_resultステップで返す(name/call_id/result) - 強制呼び出しは
generation_configのtool_choice。値はauto/any/none/validated(プレビュー)
tools = [{ "type": "function", "name": "get_weather", "description": "指定した地点の現在の天気を取得する。", "parameters": { "type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"], },}]
# 結果の返し方{ "type": "function_result", "name": "get_weather", "call_id": call_id, "result": [{"type": "text", "text": "20°C、晴れ"}],}/* @google/genai は依存に含めていないため、SDK の型は使わず手書きの型で表す。 * 実際に呼び出す場合は fetch で REST エンドポイントを直接叩けばよく、 * ここで示す payload の形はそのまま使える(第10章の他プロバイダと同じ形式)。 */interface GeminiFunctionTool { type: 'function' name: string description: string parameters: Record<string, unknown>}
interface GeminiFunctionResult { type: 'function_result' name: string call_id: string result: { type: 'text'; text: string }[]}
const tools: GeminiFunctionTool[] = [ { type: 'function', name: 'get_weather', description: '指定した地点の現在の天気を取得する。', parameters: { type: 'object', properties: { location: { type: 'string' } }, required: ['location'], }, },]
// 結果の返し方const functionResult: GeminiFunctionResult = { type: 'function_result', name: 'get_weather', call_id: callId, result: [{ type: 'text', text: '20°C、晴れ' }],}generateContent API(レガシー)
- ツール定義は
function_declarationsの配列で包む - 結果は
function_responseとして返す - 強制呼び出しは
function_calling_configのmode
ネット上の Gemini のサンプルコードは、まだ大半がレガシー側の記法である。function_declarations で包んでいるコードを見たら、それは旧APIのものだと判断できる。
10.3.5 3社の比較
Section titled “10.3.5 3社の比較”| Anthropic | OpenAI (Responses) | Google (Interactions) | |
|---|---|---|---|
| ツール定義 | フラット | フラット | フラット |
| スキーマのキー | input_schema |
parameters |
parameters |
| 引数の型 | dict | JSON文字列 | dict |
| 呼び出しID | tool_use.id |
function_call.call_id |
function_call.id |
| 結果の返し方 | tool_result ブロック |
function_call_output |
function_result ステップ |
| 強制呼び出し | tool_choice |
tool_choice |
generation_config.tool_choice |
| スキーマ厳格化 | — | strict: True |
tool_choice: "validated"(プレビュー) |
3社に共通する構造は次のとおりである。名前・説明・JSON Schemaでツールを定義し、モデルが「呼びたい」という構造化データを返し、アプリケーションが実行して結果を返す。 第4章4.4.2節で述べた「実行の主導権はアプリケーション側にある」という原則は、どのプロバイダでも変わらない。
差異はキー名と細部の作法にとどまる。したがって、第4〜7章で学んだ設計の考え方はプロバイダを問わず通用する。
一方で、同一プロバイダ内で新旧2つのAPIが併存している点には注意が必要である。OpenAI(Responses / Chat Completions)も Google(Interactions / generateContent)もそうなっている。ネット上のサンプルや生成されたコードが「どちらのAPIのものか」を見分けられないと、動かない原因が分からなくなる。
10.4 フレームワークを使う
Section titled “10.4 フレームワークを使う”10.4.1 フレームワークが提供するもの
Section titled “10.4.1 フレームワークが提供するもの”自分で書くと大変で、フレームワークなら手に入るものを列挙する。
| 機能 | 自前実装の労力 |
|---|---|
| 永続化と再開(durable execution) | 大 |
| チェックポイントと巻き戻し | 大 |
| マルチエージェントの協調・委譲 | 大 |
| Human-in-the-Loop の中断・再開 | 中〜大 |
| ストリーミングの統一的な扱い | 中 |
| トレースと可観測性 | 中 |
| プロバイダ抽象化 | 中 |
| 多数のツール・データソース連携 | 中 |
特に永続化と再開は、自前で作ると相当に厄介である。「20ステップ目で落ちたので18ステップ目から再開する」を正しく実装するには、状態のスナップショットと副作用の追跡が必要になる。ここがフレームワークを採用する最大の動機になることが多い。
10.4.2 主要フレームワークの現況(2026年7月)
Section titled “10.4.2 主要フレームワークの現況(2026年7月)”LangGraph / LangChain
グラフベースのオーケストレーション基盤。ノードが処理、エッジが遷移を表す状態機械として書く。永続化・再開・ストリーミング・Human-in-the-Loop が中核機能であり、この分野では最も成熟している。
現在の整理では、LangChain がエージェントフレームワーク、LangGraph がその下で動くオーケストレーション・ランタイムという関係になっている。単純なエージェントは create_agent で作れる。
from langchain.agents import create_agentfrom langchain.tools import tool
@tooldef search(query: str) -> str: """情報を検索する。""" return f"検索結果: {query}"
agent = create_agent(model, tools=[search])result = agent.invoke({"messages": [{"role": "user", "content": "..."}]})// Python 版は langchain.agents / langchain.tools に分かれているが、// JavaScript 版は createAgent も tool も langchain のルートから公開されているimport { createAgent, tool } from 'langchain'
const search = tool( ({ query }: { query: string }) => `検索結果: ${query}`, { name: 'search', description: '情報を検索する。', // Python 版は型注釈と docstring からスキーマを起こすが、 // TypeScript の型は実行時に残らないため JSON Schema を明示する schema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'], }, })
const agent = createAgent({ model, tools: [search] })const result = await agent.invoke({ messages: [{ role: 'user', content: '...' }],})より細かい制御が要るなら StateGraph を直接組む。
- 強み: 明示的な制御、耐久実行、監査しやすさ。規制の厳しい環境や複雑な分岐に向く
- 弱み: 定型コードが多く、学習コストが高い。単純なタスクには過剰
- 向く場面: 複雑な多段処理、失敗からの復旧が必要なもの
CrewAI
役割ベースのマルチエージェント協調。Crew(協調して働くエージェントのチーム)と Flow(状態管理とイベント駆動の制御フロー)という2階層で構成される。推奨される使い方は「Flow をアプリケーションの骨格にし、複雑な部分で Crew を呼ぶ」形である。
- 強み: 立ち上がりが速い。役割分担の記述が直感的
- 弱み: 単純な単一エージェントには過剰。トークンのオーバーヘッドが大きい(単純なワークフローで LangGraph の最大3倍という独立ベンチマークの報告がある)
- 向く場面: 専門役割を持つ複数エージェントの協調、プロトタイプ
OpenAI Agents SDK
エージェント・ハンドオフ・ガードレール・セッションという軽量なプリミティブを提供する。抽象化が薄く、トレーシングが組み込まれている。名前に反して100以上のモデルに対応しており、OpenAI専用ではない。
- 強み: 抽象化のオーバーヘッドが小さい。トレーシング内蔵
- 弱み: まだ 0.x 系でありAPIが安定していない
- 向く場面: OpenAIエコシステム中心の構成、軽量なマルチエージェント
Pydantic AI
型安全を前面に出したフレームワーク。入出力がPydanticで検証され、OpenTelemetryによる計装が組み込まれている。
- 強み: 検証と可観測性。構造化されたテスト可能なコードになる
- 弱み: Python専用
- 向く場面: 型と検証を重視するPythonチーム
Microsoft Agent Framework
AutoGen と Semantic Kernel を統合したもので、2026年4月に v1.0 に到達した。グラフによるオーケストレーション、可観測性、ガバナンス機能(タスク遵守の監視、PII検出、プロンプトインジェクション対策)を備える。
- 向く場面: .NET / Azure 中心の企業システム
LlamaIndex
RAGとデータ連携に強みを持つ。多数のデータソースコネクタとインデックス構造を提供する。エージェント機能も持つが、中心的な価値はRAG基盤にある。
Haystack
パイプライン指向のNLPフレームワーク。検索・QA・RAGのコンポーネントを組み合わせて構成する。エージェント機能も追加されている。
- 向く場面: 検索パイプラインの構築、既存のNLPワークフローとの統合
smolagents
Hugging Face による極小のフレームワーク(エージェントのロジックがおよそ1,000行に収まる)。2種類のエージェントを提供する。
-
CodeAgent(既定): ReActと同様に動くが、行動をPythonコードとして書いて実行する点が特徴的である -
ToolCallingAgent: 従来型の、組み込みのツール呼び出しを使う方式 -
強み: 徹底して単純。読めば全部わかるので学習教材として優れている
-
弱み: 複雑なワークフローのための機能が乏しい。リリース頻度が低い
-
向く場面: 軽い自動化、学習目的
コードを生成して実行する CodeAgent は、第7章7.6.2節で扱ったコード実行のリスクをそのまま抱える。サンドボックス実行は必須である。
ロードマップの「Smol Depot」について: これは Hugging Face の smolagents を指していると思われる。同名の別プロジェクトは確認できない。
AutoGen — メンテナンスモードに移行
ロードマップは AutoGen を挙げているが、現在は新機能の開発が行われていない。公式リポジトリは「AutoGen はメンテナンスモードに入った。新機能や機能強化は行われず、今後はコミュニティ管理となる」と明記している。後継は上述の Microsoft Agent Framework である。
つまり保守の担い手が Microsoft からコミュニティへ移っている。既存のワークロードに破壊的変更は予定されていないとされるが、新規採用は避けるべきである。
10.4.3 一覧
Section titled “10.4.3 一覧”| フレームワーク | 中核の抽象 | 主な強み | 注意点 |
|---|---|---|---|
| LangGraph / LangChain | 状態グラフ | 耐久実行、制御の明示性、成熟度 | 定型コードと学習コスト |
| CrewAI | 役割とクルー | 立ち上がりの速さ | トークンのオーバーヘッド |
| OpenAI Agents SDK | エージェントとハンドオフ | 軽量、トレーシング内蔵 | 0.x 系でAPIが不安定 |
| Pydantic AI | 型安全なエージェント | 検証、OpenTelemetry | Python専用 |
| Microsoft Agent Framework | グラフとワークフロー | 企業向けガバナンス | .NET / Azure 寄り |
| LlamaIndex | インデックスとクエリ | RAG基盤 | エージェント機能は副次的 |
| Haystack | パイプライン | 検索・QA | エージェントは後付け |
| smolagents | コード生成型と従来型の2種 | 極小・可読 | 機能が限定的 |
| AutoGen | 会話するエージェント群 | — | メンテナンスモード・コミュニティ管理。新規採用非推奨 |
10.4.4 フレームワーク選択が性能に効く
Section titled “10.4.4 フレームワーク選択が性能に効く”見落とされがちな事実として、オーケストレーションの設計は、モデル選択に匹敵するほど性能に影響する。同一モデルでも、足回り(scaffold)が違えばベンチマークのスコアが大きく変わるという報告がある。
これは第7章7.1節で見た「ツール設計への投資がプロンプト調整より効果的だった」という観察と同じ方向を指している。モデルの周辺をどう作るかが、モデルそのものと同じくらい重要である。
10.5 どう選ぶか
Section titled “10.5 どう選ぶか”10.5.1 判断の順序
Section titled “10.5.1 判断の順序”10.5.2 フレームワークを「選ばない」判断
Section titled “10.5.2 フレームワークを「選ばない」判断”フレームワークには代償がある。
① 抽象化の漏れ。 問題が起きたとき、フレームワークの内部を読む羽目になる。第5章のループを理解していれば読めるが、していなければ手詰まりになる。
② コンテキストの見えにくさ。 フレームワークが組み立てたプロンプトが何なのか分かりにくいことがある。第2章のコスト構造や第8章のコンテキスト管理を制御したいとき、これは障害になる。CrewAI のトークンオーバーヘッドが大きいという報告は、この種の不透明さの現れである。
③ 変化の速さ。 本章で見たとおり、AutoGen はメンテナンスモードに入り、Assistants API はサンセットする。依存先の寿命はリスクである。
④ 過剰性。 多くのエージェントは、第5章のループ + 第7章のツール + 第8章の context editing で十分に動く。永続化も再開もマルチエージェントも要らないなら、フレームワークの価値は限定的である。
実務的な指針を示す。
- まずスクラッチで動くものを作る。 何が必要かは、作ってみないと分からない
- 具体的に困った機能が出てきてからフレームワークを検討する。「あったほうが良さそう」で選ばない
- 採用するなら、フレームワークが生成しているプロンプトとコンテキストを一度は自分の目で確認する
- 移行可能性を残す。 ツールの実装本体はフレームワークに依存しない純粋な関数として書き、フレームワークは薄い接続層に留める
最後の点は重要である。第7章で設計したツールが、フレームワークのデコレータと密結合していなければ、乗り換えは接続層の書き換えで済む。
# ✅ 移行しやすい: 実装は純粋な関数def search_internal_docs(query: str, doc_type: str | None = None) -> str: """(第7章で設計した実装。フレームワークに依存しない)""" ...
# フレームワークへの接続は薄い層で行うlangchain_tool = tool(search_internal_docs)anthropic_tool = beta_tool(search_internal_docs)// ✅ 移行しやすい: 実装は純粋な関数/** (第7章で設計した実装。フレームワークに依存しない) */function searchInternalDocs( query: string, docType: string | null = null): string { // Python の `...` に相当する省略記法はないため、本体は第7章の実装に置き換える return `${query} / ${docType ?? '(指定なし)'}`}
// フレームワークへの接続は薄い層で行う。// Python 版は tool(search_internal_docs) の一行で済むが、TypeScript には// 型注釈からスキーマを起こす仕組みがないため、接続層でスキーマを明示するconst SEARCH_SCHEMA = { type: 'object', properties: { query: { type: 'string' }, doc_type: { type: 'string' }, }, required: ['query'],} as const
const langchainTool = tool( (args: { query: string; doc_type?: string }) => searchInternalDocs(args.query, args.doc_type ?? null), { name: 'search_internal_docs', description: '社内文書を検索する。', schema: SEARCH_SCHEMA, })
const anthropicTool = betaTool({ name: 'search_internal_docs', description: '社内文書を検索する。', inputSchema: SEARCH_SCHEMA, run: (args) => searchInternalDocs(args.query, args.doc_type ?? null),})10.6 まとめ
Section titled “10.6 まとめ”- 実装の選択肢は3つ: スクラッチ・SDK付属のループ・フレームワーク
- まずスクラッチで一度書く。 エージェントが
whileループとツールディスパッチであることを理解していないと、フレームワークで問題が起きたときに手が出ない - スクラッチで面倒なのはループ本体ではなく、出力のパース・エラーとレート制限・永続化と再開である
- 構造化出力はプロバイダの機能を使うのが最良。失敗したらエラーを添えて直させる
- エージェントの再試行は巨大な入力の再送を意味する。回数を増やすより並行度を下げるほうが有効なことが多い
- 並列ツール実行では
return_exceptions=Trueを使い、1つの失敗が成功した結果を巻き込まないようにする - 3社のツール呼び出しは構造が同じで、キー名と作法が違うだけである。第4〜7章の設計の考え方はプロバイダを問わず通用する
- 同一プロバイダ内で新旧2つのAPIが併存している。 OpenAI は Responses(フラット)と Chat Completions(入れ子)、Google は Interactions(現行)と generateContent(レガシー)。サンプルコードがどちらのものか見分けられないと詰まる
- 検証エラーをモデルに返すときも、必ず
tool_resultとしてtool_useの直後に置く。 素のテキストで返すとAPIエラーになる - OpenAI Assistants API は2026年8月26日にサンセットする。 新規採用してはならない。猶予は非推奨化から1年、より新しい機能では約6か月しかない
- AutoGen はメンテナンスモードに移行し、コミュニティ管理になった。 後継は Microsoft Agent Framework(2026年4月に v1.0)
- フレームワークの最大の価値は永続化と再開にある。ここが不要なら採用の動機は弱い
- オーケストレーションの設計は、モデル選択に匹敵するほど性能に効く
- フレームワークの代償は、抽象化の漏れ・コンテキストの不透明さ・依存先の寿命・過剰性である
- ツールの実装は純粋な関数として書き、フレームワークは薄い接続層に留める。 移行可能性を残す
問1 第5章5.3節で示したスクラッチのループを本番運用するために、本章10.2.1節の表のうち「まだ実装していないもの」を挙げよ。そのうち、次の3つの状況それぞれで最優先で実装すべきものを1つずつ選び、理由を述べよ。
(a) 社内向けの調査アシスタント。1タスク5〜10ステップ、失敗したらユーザーがやり直せばよい (b) 夜間バッチで1万件の文書を処理する。1件あたり3ステップ (c) 数時間かかるコード移行作業を自律実行する
問2 次のコードには、エージェント特有の問題が2つある。指摘し、修正せよ。
async def run_tools(registry, tool_uses): outcomes = await asyncio.gather( *(registry.execute_async(tu.name, tu.input) for tu in tool_uses) ) return [{"type": "tool_result", "tool_use_id": tu.id, "content": o.content} for tu, o in zip(tool_uses, outcomes)]async function runTools( registry: AsyncToolRegistry, toolUses: Anthropic.ToolUseBlock[]): Promise<Anthropic.ToolResultBlockParam[]> { const outcomes = await Promise.all( toolUses.map((tu) => registry.executeAsync(tu.name, tu.input as Record<string, unknown>) ) ) return toolUses.map((tu, i) => ({ type: 'tool_result' as const, tool_use_id: tu.id, content: outcomes[i]!.content, }))}問3
Anthropic向けに書かれた次のツール定義を、OpenAI の Responses API 向けに書き直せ。strict: True を有効にすること。変更点を箇条書きで説明すること。
なお status と limit は任意項目のつもりで書かれているが、strict モードではすべての項目を required にする必要がある。この2つをどう表現するか、10.3.2節の記述を踏まえて設計し、その判断も説明に含めよ。その際、default のようなキーワードが strict モードで使えるとは限らない点にも触れること。
{ "name": "search_orders", "description": "顧客の注文履歴を検索する。", "input_schema": { "type": "object", "properties": { "customer_id": {"type": "string"}, "status": {"type": "string", "enum": ["pending", "shipped"]}, "limit": {"type": "integer", "default": 10}, }, "required": ["customer_id"], },}const searchOrdersTool: Anthropic.Tool = { name: 'search_orders', description: '顧客の注文履歴を検索する。', input_schema: { type: 'object', properties: { customer_id: { type: 'string' }, status: { type: 'string', enum: ['pending', 'shipped'] }, limit: { type: 'integer', default: 10 }, }, required: ['customer_id'], },}問4 あるチームが、AutoGen で構築したマルチエージェントシステムを本番運用している。10.4.2節の状況を踏まえ、次に答えよ。
(a) ただちに移行すべきか。判断の材料となる要素を3つ挙げよ (b) 移行するとした場合、10.4.3節の表からどの選択肢が考えられるか。チームがPython中心である場合とAzure中心である場合に分けて述べよ (c) 10.5.2節の最後で述べた「移行可能性を残す」設計が事前にできていた場合、移行コストはどう変わるか
問5 次の3つのプロジェクトについて、10.5.1節のフローチャートに従って実装方法を選び、理由を述べよ。フローチャートだけでは決まらない場合、追加で確認すべきことを挙げよ。
(a) 問い合わせメールを分類して担当部署にタグ付けする。1日5,000件 (b) 顧客ごとに月次レポートを生成する。データ収集から作図まで20ステップ程度、途中で失敗することがある (c) 社内の技術文書について質問に答えるチャットボット。RAGが中心
問6 10.4.4節で「オーケストレーションの設計はモデル選択に匹敵するほど性能に効く」と述べた。本書のこれまでの章から、この主張を支持する具体例を3つ挙げ、それぞれどの章の内容かを示せ。
参考文献・出典
Section titled “参考文献・出典”| 出典 | 内容 | 参照日 |
|---|---|---|
| Anthropic — Tool Runner | @beta_tool デコレータ、tool_runner、until_done()、手動ループを使うべき場面 |
2026-07-29 |
| OpenAI — Function Calling | Responses API と Chat Completions のツール定義の差、strict モード、function_call_output |
2026-07-29 |
| Anthropic — Handle Tool Calls | tool_result を tool_use の直後に置く制約 |
2026-07-29 |
| Google — Gemini Function Calling | Interactions API(現行)のツール定義・function_result・tool_choice、generateContent がレガシーであること |
2026-07-29 |
| smolagents (GitHub) | CodeAgent と ToolCallingAgent、約1,000行という規模 |
2026-07-29 |
| microsoft/autogen (GitHub) | メンテナンスモードとコミュニティ管理への移行 | 2026-07-29 |
| OpenAI — Deprecations | Assistants API のサンセット日(2026-08-26)、Agent Builder / Evals Platform / Reusable Prompts の停止予定 | 2026-07-29 |
| LangChain — Agents | create_agent の現行API |
2026-07-29 |
| LangGraph — Overview | LangChain と LangGraph の役割分担、StateGraph、耐久実行 |
2026-07-29 |
| CrewAI — Introduction | Crews と Flows の役割分担 | 2026-07-29 |
| Langfuse — Comparing Open-Source AI Agent Frameworks | 各フレームワークの中核抽象・強み・弱み | 2026-07-29 |
| Uvik — Agentic AI Frameworks 2026 | 本番採用の状況、CrewAI のトークンオーバーヘッド、足回りによる性能差 | 2026-07-29 |
| VentureBeat — Microsoft retires AutoGen and debuts Agent Framework | AutoGen と Semantic Kernel のメンテナンスモード移行、Microsoft Agent Framework | 2026-07-29 |
| roadmap.sh — AI Agents Roadmap | 章構成の基準 | 2026-07-29 |
次章予告: 第11章では評価とテストを扱う。本書はここまで何度も「評価セットで測る」と述べてきたが、その中身にあたる章である。追跡すべき指標、ツールの単体テスト、フローの統合テスト、Human-in-the-Loop 評価、そして LLM-as-a-Judge の実際を見ていく。
Built with Astro ・ Deployed on Cloudflare Pages
© 2026 watakumi — made with 💜 & ☕ ・watakumi.page