1. エージェントのツール: 実際には何なのか
前のモジュールでは、Apps SDK の側からツールを「バックエンド関数」として見ました。ChatGPT があなたの App 経由で呼び出す対象です。ここでは視点を変え、Agents SDK の エージェント の目線でツールを眺め、何をどう呼ぶか、エラーをどう扱うかを整理します。
通常のバックエンドでは「エンドポイント」「コントローラメソッド」「サービス関数」という発想に慣れています。エージェントの世界では、行動の最小単位が ツール (tool) になります。エージェントの tools と mcp-tools は別物であり、重なる部分はあるものの同一ではありません。
厳密に言えば、ChatGPT Agents SDK の文脈でのツールとは、モデルが実行を依頼できる関数の記述です。モデル自体はコードを実行しません。構造化されたリクエスト(通常は JSON)を生成し、ランタイム(あなたのコード、MCP サーバー、または Agents SDK)が実際の処理を実行して結果を返します。
ChatGPT Agents SDK のエコシステムでは、ツールは設定で記述します。name、description、parameters(引数の JSON Schema)を持ちます。エージェントはこのツール群をコンテキストに保持し、reasoning の過程でどの tool をどんな引数で呼ぶかを判断します。
エージェント(またはホストとしての ChatGPT)はこの一覧を受け取り、コンテキストに「記憶」した上で、推論(reasoning)の過程でユーザー要求に対しどのツールをどんな引数で呼ぶかを決めます。だからこそ仕様では「tools are a contract(ツールは契約)」というマントラが繰り返されます。ツールはモデルとあなたのコードの間の契約であり、単なる「Python/TS の関数」ではありません。
古典的な API にもたとえられます。ルート /api/gifts/search は URL やメソッド、ボディ形式といった純粋なシンタックスです。一方、tool の search_gifts は セマンティクス、「受取人プロフィールと予算に基づくギフト検索」です。ツールの説明は、人間向けではなくLLM 向けに最適化された、構造化されたプロンプトなのです。
2. ツールのタイプ: LLM エージェントは具体的に何ができるか
「何でもできる関数」のカオスに陥らないためには、ツールをいくつかの典型カテゴリとして捉えるのが有用です。これは SDK の形式的な型付けではなく、アーキテクチャ思考であり大いに役立ちます。
私たちのバックエンドで LLM エージェントが持つツールの出どころは、たいてい次の三つです。
- ローカルな業務ツール。 自前のバックエンドにあるものです。DB 操作、ドメインロジック(フィルタリング、レコメンド、スコアリング)など。たとえば GiftGenius なら、PostgreSQL の自テーブルから商品を取り出したり、「この人にそのギフトがどれくらい刺さるか」のパーソナルスコアを計算したりするツールがありえます。
- MCP ツール。 MCP サーバーがツール(tools)の提供者として機能します。関数・リソース・プロンプトを登録し、クライアント(ChatGPT や LLM エージェント)へ渡します。MCP 経由のツールは外部 API 呼び出し、ファイル操作、プロンプトのテンプレート提供などができます。
- 統合ツール。 ACP/コマース(注文作成やチェックアウト)、メール送信、Webhook、CRM 記録など、外界とつなぐものです。これらのツールは外部システムの状態を変えるためより危険であり、セキュリティや冪等性にとくに厳密である必要があります。
別の有用な分類として、行為の性質で分ける方法があります。LLM ツールの研究では、データ取得系(検索、RAG、get_*)、副作用を伴うアクション系(create_order、send_email)、純粋計算系(calculate_loan)、システム/制御系(handoff_to_human、finish_task)などがよく挙げられます。
これを固定化するため、小さな表で見ておきましょう。
| カテゴリ | GiftGenius の例 | 副作用 | リスク |
|---|---|---|---|
| Data Retrieval | |
なし | 低 |
| Action / Mutating | |
あり | 高 |
| Computation | |
なし | 中 |
| System / Control | |
なし | 論理的 |
アーキテクチャの観点で最重要なのは、読み取り専用ツールは大量・低コスト、書き込み(状態変更)系は希少で極めて慎重に—ログ、冪等性、しばしばユーザー確認つき—にすることです。
以降では、データ取得系とアクション系ツールを主に扱います。GiftGenius のロジックがまさにそれらの上に構築されているからです。
3. JSON Schema はモデルとコードの契約
ここからはツールの記述方法を掘り下げます。ChatGPT Agents SDK(および Apps SDK)では、ツール引数の記述に標準的に JSON Schema を使います。object 型、その properties、フィールド型、必須項目、制約などを定義します。
重要なのは、JSON Schema は単なるバリデーションのためではない、という点です。これはモデルへのプロンプトの一部です。OpenAI の公式ガイドでも、フィールド名や説明をどれだけ詳しく一義的に書けるかが、エージェントの性能を大きく左右すると明言されています。
コース計画で既出の GiftGenius の例を見てみましょう。
{
"name": "search_gifts",
"description": "受取人のタイプ、興味、予算に基づいてギフトを検索します。",
"parameters": {
"type": "object",
"properties": {
"recipient_type": {
"type": "string",
"description": "ギフトの受取人は誰か(例: '男性'、'女性'、'子ども')。"
},
"interests": {
"type": "array",
"items": { "type": "string" },
"description": "主要な興味(スポーツ、書籍、テクノロジー など)。"
},
"budget": {
"type": "number",
"description": "ユーザーの通貨における最大予算。"
}
},
"required": ["recipient_type", "budget"]
}
}
ここには重要なポイントがいくつかあります。
- 第一に、name と description。モデルが「そもそもこのツールをいつ使うのか」を判断する主信号です。セマンティック・ルーティングのドキュメントでも、ツールの説明は事実上モデル向けの API だと強調されています。func1 に「何か役に立つことをする」と書いても、モデルはいつ呼べばよいか分かりません。search_gifts と分かりやすい説明があれば、選択がずっと簡単になります。
- 第二に、parameters。フィールド名と説明は極めて重要です。LLM にとっては type より recipient_type のほうがはるかに明確です。「ギフトの受取人は誰か…」のような良い description は、ここに入れるべき値が受取人タイプであって、たとえば包装の形式ではないことをモデルに暗示します。
- 第三に、required。これはあなた側のバリデーションだけでなく、モデルへのヒントでもあります。モデルは必須項目を埋めようとし、文脈から明らかでない任意項目は省略します。これにより「空」や不正なツール呼び出しが減ります。
Apps SDK の公式ガイドは、「責務を一つに絞った狭いツール」「明確な名前と説明」を推奨し、「ギフトのことは何でもやる」という多機能ツールを避けるよう勧めています。
4. GiftGenius のツール設計: スキーマからコードへ
GiftGenius に、ほぼすべてのシナリオで必要になる二つの中核ツールを加えましょう。
- suggest_gifts(profile, budget) — 候補リストを返す;
- get_gift_details(gift_id) — 個別ギフトの詳細を取得。
suggest_gifts と get_gift_details は前述の分類でいうローカル業務ツールの典型で、主に Data Retrieval に属します。
suggest_gifts のスキーマ
まずは素の JSON Schema を示し、その後でバックエンド/エージェント・ランタイムの TypeScript コード例を見せます。
{
"name": "suggest_gifts",
"description": "受取人のプロフィールと予算に基づいてギフトのリストを提案します。",
"parameters": {
"type": "object",
"properties": {
"age": {
"type": "integer",
"minimum": 0,
"maximum": 120,
"description": "受取人の年齢(歳)。"
},
"relationship": {
"type": "string",
"enum": ["friend", "coworker", "partner", "family"],
"description": "受取人との関係: 友人、同僚、パートナー、家族。"
},
"interests": {
"type": "array",
"items": { "type": "string" },
"description": "受取人の興味(スポーツ、本、テクノロジー など)。"
},
"budget": {
"type": "number",
"minimum": 1,
"description": "ユーザーの通貨における最大予算。"
}
},
"required": ["budget"]
}
}
ここでは relationship に enum を使い、モデルが勝手な文字列(たとえば "bad coworker")を作ってコードへ流し込まないようにしています。緻密なスキーマ設計はモデルに(許容値が見える)利点があり、開発者にも(ランタイムの意外性が減る)利点があります。
ここで、Node.js 上の MCP サーバーに仮の McpServer があるとします。ツール登録は次のようになります。
// MCP サーバーでのツール登録の単純化例
server.registerTool(
{
name: "suggest_gifts",
description: "プロフィールと予算に基づいてギフトを提案します。",
inputSchema: suggestGiftsSchema
},
async (input, ctx) => {
const gifts = await findGiftsInDb(input, ctx.userLocale);
return { items: gifts }; // 後でエージェントが見る JSON
}
);
コードは大幅に単純化していますが、意図は明確です。契約(名前、説明、スキーマ)と実装を分けています。
get_gift_details のスキーマ
二つ目は、ほぼどの棚にも必要なツールです。
{
"name": "get_gift_details",
"description": "ギフトの識別子で完全な情報を取得します。",
"parameters": {
"type": "object",
"properties": {
"gift_id": {
"type": "string",
"description": "GiftGenius データベースにおけるギフトの UUID。"
}
},
"required": ["gift_id"]
}
}
登録は同様です。
server.registerTool(
{
name: "get_gift_details",
description: "ギフトの詳細情報を返します。",
inputSchema: getGiftDetailsSchema
},
async ({ gift_id }) => {
const gift = await db.gifts.findById(gift_id);
if (!gift) return { notFound: true };
return { gift };
}
);
ここではツールが notFound: true を返しうることを示しました。これは セマンティックな(業務)エラーの芽で、後述します。エージェントは「ギフトが見つからない」を見て、別の id を試す、ユーザーに別の商品を提案する、などの判断ができます。
5. エージェントはどのツールを呼ぶかどう決めるか
さて本題のルーティングです。従来の Web アプリでは URL → 特定コントローラという固定ルーティングでした。ChatGPT Apps とエージェントの世界では、ツール選択はセマンティックかつ確率的に行われます。
高レベルのサイクルは次のとおりです。
flowchart TD
U[User message] --> M["モデル(エージェント)"]
M -->|要求の分析| C{ツールが必要?}
C -->|いいえ| T[テキスト応答]
C -->|はい| S[ツールの選択]
S --> K[JSON 引数の生成]
K --> R[ツールの実行]
R --> M2[モデルが結果を見る]
M2 --> T2[最終応答または次のステップ]
各ステップでエージェントは次の情報を見ます。
- 第一に、system 指示(エージェントの役割・制約);
- 第二に、対話履歴;
- 最後に、name、description、inputSchema を備えたツール一覧。
新しいユーザー発話が来ると、モデルは要求の意味とツール説明を照合(セマンティック対応付け)します。「友だち向けに 50 ドル以内でギフトを選んで」という要求に対し、suggest_gifts の説明は get_gift_details より高い関連性を持つため、高確率でそれを選びます。
公式ガイドは、ルーティング品質に大きく効く二点を強調しています。
- 第一に、意味が重複するツールを避けること。search_gifts と find_gifts がどちらも「興味でギフトを探す」だと、モデルは混乱します。
- 第二に、一つのツールに一つの責務という原則を守ること。単一の tool に「ギフトを選ぶ・注文を作る・メールを送る」を詰め込まないこと。
LLM エージェントにはツール選択モードの制御メカニズムがあります。たとえば「auto」(モデルがツール要否を自動判断)、「required」(必ず tool を呼ぶ)、「none」(tools 無効)など。複雑なワークフロー(多段シナリオ)で、特定のステップでは suggest_gifts を強制呼び出ししたい、といったときに役立ちます。
GiftGenius におけるセマンティック・ルーティング例
エージェントに少なくとも suggest_gifts と get_gift_details の二つがあるとします。
- ユーザー: 「同僚向けに 30 ドル以下でギフトを選んで。ボードゲームが好きだよ」。
- エージェントは、目的(ギフト選定)、予算、興味が含まれると理解します。suggest_gifts の説明が最適—このツールを呼びます。
- ツールは id、名前、短い説明を伴う 5 つのギフトを返します。
- ユーザー: 「3 番目の候補を詳しく」。エージェントは前回結果の id と「3 番目の候補」を対応付け、今度は get_gift_details が適合—それを呼びます。
重要なのは、コードのどこにも「要求に『選んで』が含まれていたら suggest_gifts を呼べ」と書いていないことです。これは説明と履歴に基づいてモデル自身が行います。開発者としての責務は、選択を人間にもモデルにも明確にしておくことです。
6. ツールのエラー: 500 ではなくモデルへのシグナル
get_gift_details の例で notFound: true を示しました。これはまさに 業務エラー の例で、エージェントはそれを解釈して処理すべきであり、生の 500 を受け取るべきではありません。
通常の REST API では、バックエンドの奥で何か落ちたら 500 Internal Server Error を返し、スタックトレースをログに残し、あとはユーザー任せ—というやり方が通例です。エージェントの場合、この方法は相性がよくありません。
Agents SDK の実践ガイドは、ツールのエラーを単なるクラッシュではなく、観測可能なイベントとして扱うことを推奨します。しばしば「Error as Observation」というパターン名で語られます。
要するに、説明なく「落ちて」はいけません。何が起きたかをモデルが理解できるよう、構造化された応答を返すべきです。そうすれば、モデルは挙動を適応させ(要求の言い換え、ユーザーへの確認、別ツールの試行など)、次の一手を選べます。
エラーはたいてい三つに分類されます。
- 引数のバリデーションエラー。 モデルは不正なパラメータを生成することがあります。必須を欠落、数値のところに文字列、許容範囲外、など。ここではスキーマとバリデーションを例外送出だけに使うのではなく、意味のある応答(どのフィールドがなぜ不正か)として返しましょう。
- 業務エラー。 「商品が見つからない」「地域が未対応」「この種類のギフトに予算が足りない」のような想定内の状況です。API 的にもエラーではありますが、クラッシュではなく、通常の応答の中で分かりやすいコードとメッセージとして返すべきです。
- システムエラー。 外部サービスのタイムアウト、ネットワーク障害、DB 障害など。ここでは「サービスが一時的に利用できないので後でもう一度」といった控えめで一般化されたメッセージで十分です。スタックトレースやテーブル名など、モデルに不要でセキュリティ的にも危険な詳細は不要です。
Agents SDK の資料には、例外を上に投げる代わりに、モデルが見るエラーテキストを丁寧に整形できる failure_error_function という仕組みも紹介されています。
「フレンドリーな」エラーの構造
エージェントのツール(あなたのバックエンド)では、すべてのエラーを次のようなオブジェクトで返す、といった取り決めができます。
type ToolError = {
code: string; // 'VALIDATION_ERROR', 'OUT_OF_STOCK', ...
message: string; // モデル向けのメッセージ
retryable: boolean;
};
ツールの結果は次の和集合のようにします。
type SuggestGiftsResult =
| {
ok: true;
items: GiftSummary[];
}
| {
ok: false;
error: ToolError;
};
モデル(やエージェントランタイム)はこの JSON を見て、retryable: true なら少し変えて再試行、不再試行の業務エラーならユーザーへ説明して戻る、などの判断ができます。
7. 例: バリデーション、業務エラー、システムエラー
バックエンド/エージェントツールに戻って、同じ発想をコードでどう実装するかを見ます。
バリデーションエラー
suggest_gifts に対し、モデルが負の予算を渡してきたとします。
async function handleSuggestGifts(input: SuggestGiftsInput)
: Promise<SuggestGiftsResult> {
if (input.budget <= 0) {
return {
ok: false,
error: {
code: "VALIDATION_ERROR",
message: "budget は正の数でなければなりません。",
retryable: false
}
};
}
const items = await findGiftsInDb(input);
return { ok: true, items };
}
ここでは例外を投げず、構造化エラーを返しています。エージェントは要求を再考し、通貨の取り違えを疑ってユーザーに確認する、あるいはその予算では選べないと正直に伝えるかもしれません。
業務エラー
次は get_gift_details の例です。指定 id のギフトが存在しないことがあります。
async function handleGetGiftDetails(input: { gift_id: string }) {
const gift = await db.gifts.findById(input.gift_id);
if (!gift) {
return {
ok: false,
error: {
code: "GIFT_NOT_FOUND",
message: "指定された識別子のギフトが見つかりません。",
retryable: false
}
};
}
return { ok: true, gift };
}
モデルの応答は「選んだギフトは利用できなくなったようです。似たカテゴリから代替をいくつか提案しましょうか?」のようになるでしょう。ここでモデルに必要なのは code と message だけで、SQL エラーやスタックトレースではありません。
システムエラー
最後にシステムエラーの例です。外部の配送 API が時々「落ちる」ケースを考えます。
async function handleEstimateDelivery(input: EstimateDeliveryInput) {
try {
const eta = await callDeliveryApi(input);
return { ok: true, eta_days: eta };
} catch (e) {
return {
ok: false,
error: {
code: "DELIVERY_SERVICE_UNAVAILABLE",
message: "配送サービスは一時的に利用できません。",
retryable: true
}
};
}
}
エージェントは「配送サービスが今は使えないようです。ギフト自体は表示しますが、配達日数は前後する可能性があります。続けますか?」と判断できるでしょう。
8. ツールのセキュリティと冪等性(tools 観点のクイックビュー)
セキュリティと権限は別途深掘りしますが、エージェントのツールはこれと密接に関わるため、ここでも触れておきます。
第一に、読み取りツールと書き込みツールを分ける必要があります。説明・スキーマ・権限で、データを読むだけで完全に安全なものと、課金・注文変更などを行うものを明記しましょう。エージェント系の文書やフォーラムでも ReadOnly と Mutating の分離が推奨されています。
第二に、ミューテーション系ツールでは冪等性を考慮します。エージェントや MCP クライアントは(ネットワーク障害などで)呼び出しを再試行し得ます。create_order が二重注文を作らないようにする必要があります。代表的なパターンは次のとおりです。
- ツール引数として渡す idempotency‑key;
- 実行前に操作の既存有無をチェック;
- 「注文の下書きを作成」と「注文を確定」に段階分割。
これらはツール契約の設計と深く結びついています。JSON Schema に idempotency‑key 用のフィールドがないと、後から冪等性を足すのはつらくなります。
9. Agents SDK のちょっとした眺望: ランタイムでどう見えるか
この章は TypeScript 指向の Agents SDK を使う人向けの軽い概観です。本講座の主軸は MCP ですが、Agents SDK がツールをどう見て、ランタイムで典型の tool がどうなるかを理解しておくと役立ちます。
公式ドキュメントでは「関数的ツール」のような実体が説明されます。設定オブジェクト(あるいは tool(...) のようなヘルパー)で記述し型を付けた任意の関数が、ツールへ自動変換されます。SDK が JSON Schema と説明を生成します。
概念レベルでは、これまで議論したものと同じです。関数名・パラメータ・コメント/description がツールの名前・スキーマ・説明の役割を果たします。違いは、スキーマライブラリ(Zod や JSON Schema)や SDK が機械的な部分を引き受けてくれる点です。
仮の例(擬似 TypeScript、簡略化):
type Gift = {
id: string;
title: string;
// ...
};
const suggestGifts = tool({
name: "suggest_gifts",
description: "受取人タイプと予算に基づいてギフトのリストを提案します。",
parameters: {
type: "object",
properties: {
recipient_type: {
type: "string",
description: "ギフトの受取人は誰か(例: '男性'、'女性'、'子ども')。"
},
budget: {
type: "number",
description: "ユーザーの通貨における最大予算。"
}
},
required: ["recipient_type", "budget"]
}
}, async (args: { recipient_type: string; budget: number }): Promise<Gift[]> => {
// ここにドメインロジックを書く
return findGifts(args.recipient_type, args.budget);
});
SDK(や tool ヘルパー)は parameters から JSON Schema を組み立ててエージェントに渡し、ランタイムは引数のバリデーションやマーシャリングを担います。概念的には、TypeScript 製 MCP サーバーで手作業でしたことを、エージェントランタイムに直結させた形です。
ここで大事なのは、良質な型付け + 分かりやすい description/コメント = 良質なツールという考え方であり、tool ヘルパーの具体的な書式暗記ではありません。
まとめると、良いエージェントツールとは、狭く明確な責務、練られた JSON Schema、モデルにとって分かりやすい説明、丁寧なエラー処理を備えた関数です。セマンティック・ルーティングは、ツールの意味が重複しないときにこそ機能します。ミューテーション操作は安全で冪等であるべきで、そうでないと本番のエージェントはすぐサプライズの源になります。
10. エージェントツール設計でありがちな落とし穴
誤り №1: 何でも屋の「do_everything」ツール。
すべてを一つに詰め込みたくなることがあります。manage_gifts が検索・詳細表示・注文作成・メール送信までやる、といった具合です。モデルには荷が重く、説明は曖昧になり、セマンティック・ルーティングは劣化、ツールを「念のため」どこでも呼ぶようになりがちです。責務ごとに分割し、一つのツールに一つの明確な仕事を与えるほうがよいです。
誤り №2: 意味が重なるツールの併存。
search_gifts と find_gifts がどちらも「興味でギフトを探す」なら、モデルはランダムに選んでしまいます。結果として挙動が不安定になり、同じ要求が時に一方、時に他方へ行きます。各名前と説明が意味空間で固有の「居場所」を占めるようにしましょう。
誤り №3: 貧弱または欠落した説明とスキーマ項目。
func1、説明「Does something」、パラメータ data: string は、エージェントを賢くなくする古典例です。モデルはテレパシーではなく、あなたのソースを読めません。description、properties とその description に依存します。recipient_type が何かを説明しなければ、モデルは当て推量で誤ります。
誤り №4: ハッピーパス偏重でエラー無視。
「常に正しい引数が来てサービスも死なないだろう」という前提で実装しがちです。現実には、モデルは簡単に不正パラメータを生成し、外部サービスは落ち、DB はタイムアウトもします。エラー形式を設計せず、モデルへ意味のあるメッセージを返さないと、挙動を調整できず、黙って落ちるか、幻覚を生みます。
誤り №5: 生の 500 とスタックトレースを LLM に投げる。
REST API ではデバッグのためにスタックトレースをフルでログるのが普通です。エージェント文脈でモデルへスタックトレースを渡すのは、役に立たない(モデルはあなたの特定のライブラリの SQLException を知らない)うえ、実装詳細や機密の漏洩リスクがあります。例外は捕捉し、詳細はログに、モデルへは丁寧な code と message を送りましょう。
誤り №6: ミューテーション系ツールに冪等性がない。
create_order に idempotency‑key がなければ、ネットワーク障害や自動リトライ環境で重複注文の招待状です。商用シナリオで動くエージェントなら、金銭が絡むツールは再試行しても二重課金や重複が起きないよう設計されているべきです。
誤り №7: スキーマや説明に秘密や技術詳細を書く。
開発者はつい description に「内部で https://internal-api.example.com のサービス X を呼びます」と書きがちです。モデルにもユーザーにも不要な情報です。スキーマと説明はプロンプトの一部であり、モデルのコンテキストに載ります。内部 URL やプライベートテーブル名、ましてや秘密は書くべきではありません。
誤り №8: よく考えたフィールド集合の代わりに、何でもかんでも渡す。
「ユーザープロンプトの全文字列をそのまま渡して中で解釈しよう」と誘惑されがちです。これでは JSON Schema による構造化の利点を失い、モデルはロジックに重要な要素が何かを理解できず、検証性や予測可能性も落ちます。要求から明示的なフィールド(budget、interests、user_location など)を抽出し、契約の一部として記述しましょう。
GO TO FULL VERSION