1. なぜそもそも tool-call を理解する必要があるのか
単純化すると、通常の Web アプリは「ユーザーがボタンを押す — 私たちが関数を呼ぶ」という流れで動きます。ChatGPT Apps の世界では少し違います。ユーザーが何かを言い、モデルが考え、必要だと判断すればツール呼び出し(tool-call)という構造化された呼び出しを生成します。
つまり、あなたは 次のようには書きません:
onClick={() => callSuggestGiftsApi(formData)}
その代わりに:
- suggest_gifts というツール(名前、説明、引数スキーマ)を定義します。
- system-prompt 内で、このツールがどんなときに役立つかをモデルに説明します。
- 制御をモデルに渡します。いつどのように呼び出すかはモデルが自律的に判断します。
ここから早い段階で次の2点を理解することが重要です:
- GPT はあなたのバックエンドのコードを見ません。見えるのはツールの「表紙」だけです: 名前、説明、パラメータのスキーマ。
- モデルがどれだけ「賢く」あなたの App を使うかは、これらの説明をどう書くかにほぼ直結します。良い説明はあなたの「ツール用プロンプト」です。
本講義は、ユーザーとあなたのサーバーの間にあるこの「頭脳」についてです。
2. tool-call のメンタルモデル: 実際には何が起きているのか
全体像から始めましょう。GiftGenius の典型的なシナリオ:
- ユーザー: 「30歳の友人向けに、予算100ドルで、ビデオゲーム好きに合うプレゼントを選んで」
- GPT はこのメッセージを読み、利用可能なツールを確認します。たとえば私たちの App には suggest_gifts があります。
- GPT は判断します。「うまく答えるには、このツールを呼ぶ必要がある」
- 通常のテキストの代わりに、ツール名 + JSON 引数という構造を生成します。
- ChatGPT クライアントは「なるほど、これは tool-call だ」と認識し、あなたの MCP/サーバーへ送ります。
- あなたのサーバーはビジネスロジックを実行し、structured output を返します。
- GPT は結果を受け取り、それを読んだ上で、ユーザーにわかりやすい回答を作成したり、ウィジェットを更新したりします。
OpenAI API の観点では、これは LLM-function-calling と同じ仕組みです。モデルの返答には通常のテキストではなくツールの name と arguments を持つオブジェクトが現れ、finish_reason は tool_calls に設定されます。モデル自身はコードを実行しません。どのツールを呼ぶべきかを「提案」するだけで、実際の呼び出しはクライアント(ChatGPT/Apps SDK)が行います。
概ね次のように見えます(簡略化したシーケンス):
sequenceDiagram
participant U as ユーザー
participant G as GPT(モデル)
participant C as ChatGPT クライアント
participant S as あなたの MCP/バックエンド
U->>G: 「プレゼントを選んで…」
G->>C: tool-call: { name: "suggest_gifts", args: {...} }
C->>S: HTTP /mcp tools/call (suggest_gifts, args)
S-->>C: 結果(ギフト一覧の JSON)
C-->>G: tool result
G-->>U: 回答 + 更新されたウィジェット
要点: あなたは if (userAskedAboutGifts) callSuggestGifts() のようには書きません。ツールとその説明を用意し、決定はモデルに任せます。
3. モデルが見ているもの: System Prompt + ツール一覧
GPT がどう判断しているかを理解するには、その時点でモデルがどんな情報セットを持っているかを把握する必要があります。
単純化すると、モデルが見るのは次のとおりです:
- あなたの App の system‑prompt(詳細はモジュール5で扱います)
- 会話履歴: ユーザーのメッセージ、自分の返答、過去の tool-call の結果
- 利用可能なツール一覧(tools)とそれぞれの名前、説明、パラメータスキーマ
- 追加のアノテーション(readOnly/destructive など)
モデルが見ないもの:
- 関数の実装
- SQL クエリ
- テーブル構造
- サービスを含む private リポジトリの中身
MCP については後ほど詳しく説明します。ここでは、MCP レベルではツールはディスクリプタとして宣言される、ということだけ知っておけば十分です。各ツールには name、description、inputSchema(JSON Schema)があり、ハンドシェイク時に ChatGPT が MCP サーバーからツール一覧を取得し、それを利用可能な「アクション」として認識します。
GiftGenius 用のそのようなディスクリプタ(簡略 JSON):
{
"name": "suggest_gifts",
"description": "年齢、興味、予算に基づいてギフトのアイデアを提案する",
"inputSchema": {
"type": "object",
"properties": {
"age": { "type": "integer" },
"budget": { "type": "number" }
},
"required": ["age", "budget"]
}
}
モデルはここでテキストと構造だけを「読みます」: age、budget が何か、このツールが全体として何をするのか。次回の講義では inputSchema の書き方を扱います。ここでは、この説明から「suggest_gifts を呼び出そう」という判断がどう生まれるかに注目します。
4. API の観点から見た tool-call
ChatGPT はあなたの MCP サーバーのツール(tools)を、OpenAI Agent があなたのバックエンド関数を呼ぶのとほぼ同じように呼び出します。ChatGPT Apps SDK では少しラップされていますが、基本的なメカニクスは同じです。
仮に、私たちのバックエンドから通常の OpenAI API リクエストを送り、モデルが返答内で呼べるツールとして suggest_gifts を渡すとします:
const response = await openai.responses.create({
model: 'gpt-5-mini',
messages: [
{
role: 'user',
content: '30歳の友人へのプレゼント。予算は100ドル'
}
],
tools: [ // ここで LLM が「呼び出せる」関数(ツール)の一覧を渡す
{
name: 'suggest_gifts',
description: '年齢・予算・興味に基づいてギフトを選定する',
parameters: {
type: 'object',
properties: {
age: { type: 'integer' },
budget: { type: 'number' }
},
required: ['age', 'budget']
}
}
]
});
モデルがツールを呼び出すと判断した場合、テキストではなく、次のようなアシスタントメッセージを受け取ります:
{
"role": "assistant",
"tool_calls": [
{
"id": "call_1",
"name": "suggest_gifts",
"arguments": "{\"age\":30,\"budget\":100}"
}
],
"content": []
}
この方法で LLM は、バックエンドに「suggest_gifts(30,100) を呼んで」と伝えます。
ここで重要な3点:
- ツール名(name) — モデルは最初のリクエストで送った tools の説明にある文字列をそのまま入れます。
- 引数(arguments) — parameters/inputSchema に基づいて組み立てられた JSON 文字列。
- (当面)通常のテキスト回答がない — その代わりにツール呼び出し用の構造を受け取ります。
ChatGPT アプリの動作でも同様です。モデルは「suggest_gifts をこのパラメータで呼びたい」と返し、クライアント(ChatGPT)があなたの MCP/サーバーに対してツール名と引数を含む tools/call の HTTP リクエストを行います。
5. モデルはどう決めるのか: ツールかテキストか
ここが一番面白いところです。GPT はいつツールの存在を思い出すのでしょうか?
仕組みを単純化するとこうです:
- モデルはユーザーの最新メッセージと現状のコンテキストを見ます。
- 内部では、次のアシスタントメッセージを生成する「層」があり、常にテキストを出す代わりに、次のいずれかの完了パターンを選べます:
- 通常のテキスト回答(finish_reason: "stop")
- 1 つ以上の tool-call(finish_reason: "tool_calls")
- 場合によってはその他(「ユーザーの追加メッセージが必要」など)
- この選択に影響するもの:
- ユーザー要求が、ツールの説明にあるタスクとどれだけ似ているか
- ツールの説明が「このケースでは私を使って」とどれだけ明確に示しているか
- Apps SDK の設定で与えた app system prompt の内容
わかりやすく言えば、モデルはあなたのツールを現在のリクエストに「当てはめて」みます。説明が「年齢や興味に基づいてプレゼントを選ぶ」とあるのに、ユーザーが「国家予算の分析をして」と言えば、モデルはツールを呼ぼうとはしません。説明が曖昧すぎる(「すごいことをする」など)と、どんなリクエストで使うべきかモデルが理解できません。
興味深い点として、ツールを用意していてもモデルが必ず呼ぶわけではありません。GPT は「ここは自分で答えられる」と判断すれば、tool‑call なしで答えることもあります。そこで本講座では、モデルにとってツールを使うのが明快で有利になるような説明文の書き方を徹底的に練習していきます。
6. ツール名: なぜ tool1 は悪い命名か
ツール名は、モデルが呼び出しで使う実質的な識別子です。一見すると技術的なフィールドに思えますが、実際には名前がモデルの振る舞いに大きく影響します。
ツール名を tool1 にしても、モデルには何も伝わりません。単なる文字列です。一方で suggest_gifts、search_products、fetch_user_orders のように名付ければ、名前自体が強いシグナルになります。
見知らぬコードを読むときの自分を想像してください。calculateCartTotal という関数名を見れば、だいたい何をするか想像できます。モデルにも同じ「意味の錨(いかり)」が必要です。
GiftGenius にとって妥当なツール名の例:
suggest_gifts
search_products
get_product_details
create_order
良い名前の条件:
- 短くても意味が伝わること
- スタイルが統一されていること(snake_case、ラテン文字、動詞_名詞)
- 1 つの具体的なアクションを表していること
複数のアクションを 1 つのツールに混ぜるのは良くありません(例: do_all_gift_stuff)。モデルはいつ使うべきか理解しづらくなり、次の講義で見るように引数スキーマが壊れやすく、デバッグも難しくなります。
7. ツールの説明: モデルのためのプロンプト
名前が見出しだとすれば、description はミニドキュメントです。ただし対象は人間の開発者ではなく GPT です。開発者はコードを読みますが、モデルは読みません。モデルは、いつツールを呼ぶか、どんな引数を入れるかの判断に説明文を頼ります。
説明は「使用上の指示」のスタイルで書くのが重要です:
- いつツールを使うのか
- 制約は何か
- 何をしてはいけないか
suggest_gifts を例に、3 つの説明を見てみましょう。
広すぎる:
"プレゼントを選びます。"
誰に、どんな場面で、どんなパラメータで、が分かりません。モデルの一般知識による回答と「競合」し、単にテキストで答えてしまうことが多くなります。
狭すぎる:
"弟の誕生日にしか使いません。"
これではほとんど常に使えないツールになってしまいます。母親、同僚、記念日など他のシナリオは「対象外」なので、モデルは呼び出しを避けます。
最適:
"年齢、関係性の種類(友人、パートナー、同僚など)、予算、興味に基づいて、相手に合うプレゼントを選ぶ必要があるときにこのツールを使ってください。
プレゼントに関係のない質問(例: 政治や天気)では呼び出さないでください。"
何をするツールか、どんなパラメータがあるか、いつ呼ぶのかが明確で、さらに「呼ばない条件」も書かれています。
モデルはこのような明確な枠組みを好みます。どんなユーザー表現(インテント)でツールが適切かをはっきり示すほど、App の振る舞いは予測しやすくなります。
ミニ演習
今すぐ、あなたの将来の App(プレゼント以外でも構いません)を 1 つ想定し、その中の 1 つのツールについて、広すぎる・狭すぎる・バランスがよい、の 3 種類の説明を書いてみましょう。続けて、GPT が各バージョンでどう振る舞うかをテストしてみてください。
8. 引数スキーマ: 意思決定をどう助けるか
JSON Schema の詳細は次回扱いますが、tool-call を理解するには上位レベルの感覚が重要です。
モデルがツールを呼ぶと決めたとき、やるべきことは:
- そのツールがどんな引数を期待しているか理解する。
- それらの値をユーザーテキスト(またはコンテキスト)から抽出する。
- それらの引数で JSON を生成する。
そのため、ツールの説明にはスキーマ(inputSchema)があり、モデルに次のことを伝えます:
- どんなフィールドがあるか(age、budget、relationship_type、interests など)
- 必須フィールドはどれか(required)
- 型は何か(integer、number、string、配列など)
- 場合によっては許容値(enum)やフィールドの補足(description)
suggest_gifts の最も単純な TypeScript インターフェイスは次のように書けます:
interface SuggestGiftsParams {
age: number;
relationship_type: 'friend' | 'partner' | 'colleague';
budget: number;
interests?: string[];
}
モデルの内部ではこれが JSON Schema に変換され、モデルは各フィールドの名前と説明から次のように推測します:
- age は「30歳」「10代向け」などから取得する
- budget は「予算100ドル」「50ユーロまで」から取得する
- relationship_type は「友人」「同僚」から取得する
- interests は「ビデオゲームが好き」などから取得する
説明のないスキーマや、a、b、c のような抽象的なフィールド名を使うと、モデルは引数の埋め方でミスしやすくなります。これはローカリゼーションと UX ヒントのモジュールでも扱いますが、鍵となる考えは簡単です。スキーマはバックエンドのバリデーションだけでなく、モデルに「どこに何を入れるか」を示すヒントです。
スキーマが、モデルに正しく引数を組み立てさせる助けになる話をしました。ですが「何をどう呼ぶか」だけでなく、「今すぐ呼べるのか」「安全か」も重要です。ここでパーミッションとツールのメタ情報が登場します。
9. パーミッションとコンテキスト: いつでも使えるとは限らない
名前、説明、引数スキーマに加えて、重要なのが安全性とアクセスです。実アプリのツールは「危険度」が大きく異なります。公開カタログでのプレゼント検索と、ユーザーのカードからの課金は話が違います。
Apps SDK と MCP は、ツールの説明やアノテーションでこれを表現できます。例えば read-only や destructive といったラベルです。
考え方はこうです:
- 公開データを読むだけのツール(search_products、get_weather)は、追加の確認なしに呼べます。
- 何かを変更するツール(create_order、cancel_order、charge_user)は「破壊的」としてマークされます。ChatGPT UI はユーザーに追加確認(「本当に注文しますか?」)を求めることがあり、モデル自身も明示的な要請なしにそれらを提案しにくくなります。
今後のモジュール(MCP の設定)で、これらのアノテーション(_meta、destructiveHint、readOnlyHint)が実際の JSON ディスクリプタでどう表現されるか、UX や ChatGPT の「Are you sure?」ダイアログ形成にどう影響するかを見ます。いま押さえておくべきポイントは次のとおりです:
- GPT は説明テキストだけでなく、安全性に関するメタ情報も考慮する。
- 認証が必要なツールは、ユーザーがログインする(または App が必要なトークンを取得する)まで使われない。
これは「ツールを起動するか否か」の判断に影響するもう一つの要因です。意味的に適していても、パーミッションで利用不可なら、モデルは別の道を選びます。
10. ChatGPT にツールはどうやって届くのか
アーキテクチャ的には、ツールがモデルに渡る経路は主に 2 つあります。
1 つ目は、あなたの ChatGPT App の設定からです。App を登録する際、どの MCP サーバー(とそのツール)を紐付けるか、またはアプリ自体にどの組み込みツールがあるかを指定します。セッション開始時に ChatGPT がこの設定を受け取り、どのツールが利用可能かを理解します。
2 つ目は MCP から直接です。MCP(Model Context Protocol)は、クライアント(ここでは ChatGPT/Apps SDK)がサーバーの機能を知る標準的な方法を定義します。クライアントは tools/list をリクエストし、ツールの説明が入った JSON を受け取り、それを capabilities として保持します。詳細は MCP のモジュールで扱いますが、今はこの全体像だけ理解しておけば十分です。
図解:
flowchart LR A[ChatGPT Client] -->|handshake| B[MCP Server] B -->|tools/list| A A -->|リストを渡す| G[GPT Model]
これ以降、ツール一覧はモデルのコンテキストの一部になります。サーバー側でスキーマや説明を変更して App を再起動すれば、次のハンドシェイクで新しいディスクリプタが ChatGPT に渡り、モデルの判断も更新されます。
そして実務的に重要なのは、バックエンド(ツールの実装)だけを変えても、モデルはそれを知りません。一方で name/description/schema を変えると、App の「頭脳」を本当に変えることになります。時には、description の 1 行を直すほうが、200 行のヒューリスティックなコードを書くより有益です。
11. GiftGenius で実践: モデルが呼びたくなるツールを作る
ここまでを、学習用アプリ GiftGenius に結びつけましょう。すでに MCP サーバーやバックエンド層があり、そこでツールを登録できるとします。server.registerTool(...) を使って suggest_gifts を登録しましょう。
まずは TypeScript での素朴な下書き(実ロジックはまだなし):
// pseudo-mcp-server/tools/suggestGifts.ts
server.registerTool(
'suggest_gifts', // ツール名
{
title: 'ギフトの選定',
description:
'このツールは、年齢・関係性・予算に基づいてギフトのアイデアを選ぶために使用してください。' +
'ギフトに関係のない質問では呼び出さないでください。',
inputSchema: { // ツールのパラメータの説明
type: 'object',
properties: {
age: { type: 'integer', description: '受取人の年齢(年)' },
relationship_type: {
type: 'string',
description: '関係性の種類: friend, partner, colleague'
},
budget: {
type: 'number',
description: 'ユーザーの通貨でのギフトの最大予算'
}
},
required: ['age', 'budget']
}
},
async ({ age, relationship_type, budget }) => { // 関数/ツールのコード
// 実際のロジックは後で実装
return { suggestions: [] };
}
);
ロジックが「スタブ」の段階でも、次の点をすでに考慮しています:
- 名前: suggest_gifts(tool1 ではない)
- 説明: いつ呼ぶか、いつ呼ばないかを明示
- フィールド説明: ユーザーテキストを引数へ正しくマッピングするのに役立つ
結果として、ユーザーが「同僚向けに 50 ドルでプレゼントを選んで」と書いたとき、モデルは次を認識します:
- suggest_gifts という「プレゼント選定」のツールがある
- そのツールには age、relationship_type、budget のフィールドがある
- budget は「ギフトの最大予算」、relationship_type は「関係性: friend, partner, colleague」を意味する
ユーザーの表現が少し曖昧(「50 ドルまで」「プロジェクトの相棒」など)でも、モデルは文脈から引数 JSON を適切に構成しようとします。
ツールが実際に動作する段階(バックエンドと MCP のモジュール)になれば、あなたはすでにこの領域に慣れているはずです。私たちはインターフェイスと説明をうまく設計したため、GPT は予測可能にツールを呼び出すようになります。
12. ちょっとした実践課題
理論だけで終わらせないために、講義後すぐに小さな実験をおすすめします。
まず、GiftGenius の 1 つのシナリオを取り上げるか、新しい App を考えてみましょう。モデルに明確に提供したい 1 つの関数を紙やエディターに書き出します。たとえば search_products、find_hotels、calculate_shipping のようなものです。
次に、同じツールについて「名前 + 説明」の 3 パターンを考えます:
- 非常に抽象的な名前と説明
- 狭すぎる(ほぼ特殊ケースの)名前と説明
- バランスの良い名前 + 説明。いつ呼ぶべきか、何をしてはいけないかを明確にする
その後、任意で、OpenAI SDK を使って各パターンで簡単なリクエストを送り、モデルの振る舞いがどう変わるか(ツールを呼ぶか、引数の埋め方はどうか)を観察してみましょう。この分野のリサーチでも、suggest_gifts を題材にした同様の演習が紹介されています。
13. tool-call と説明設計でありがちなミス
誤り No.1: ツール名を tool1、handler、doStuff のように付ける。
このような命名はモデルにとって無意味です。GPT はファイル名から「開発者の意図」を推測しません。意味が伝わる名前が必要です。tool1、tool2、tool3 のような名前だけで説明がなければ、モデルはツールをほとんど呼びません。各ツールが何をするのか分からず、無視するか偶然に選ぶだけです。
誤り No.2: description を人間向けコメントだとみなす。
「ギフトを選ぶ関数です」のような形式的な説明だけを書く人がいますが、詳細はコードにあるから大丈夫だと考えています。ところがモデルはコードを見ません。見るのは説明テキストと引数スキーマだけです。曖昧な説明は幻覚の元になります。必要な場面でツールを呼ばずに自分で答えたり、場違いな場面でツールを呼んだりします。
誤り No.3: 説明が広すぎる/狭すぎる。
「すごいことをする」では適用範囲が分かりません。「弟の 18 歳の誕生日にしか使わない」では、ほぼいつも使えません。最適な説明は、扱うタスク領域(複数のパラメータによるギフト選定)を明確に定め、主要なパラメータ(年齢、関係性、予算、興味)を挙げ、使うべきでない質問のクラスも明記します。
誤り No.4: 引数スキーマを「プロンプトの一部」として扱わない。
JSON Schema をサーバー側のバリデーション手段としてしか捉えない開発者もいます。実際には、モデルはフィールド名・型・説明を積極的に分析し、ユーザーテキストからどのデータを引き出すべきかを理解します。x のような名前で説明もなく、任意項目にしてしまうと、GPT はそのフィールドを気まぐれに埋めたり、全く埋めなかったりします。明確な名前と簡潔な説明を持つ正しいスキーマは、無効な tool-call を大きく減らします。
誤り No.5: モデルが「必ず」ツールを呼ぶと期待する。
「ツールを用意したのに、なぜ GPT は呼ばないのか?」と驚くことがあります。答えはほぼ一つです。説明や system‑prompt から、その質問にツールが必要だと読み取れないか、モデルが「自分で答えたほうが簡単」と判断したのです。
誤り No.6: 異なる種類のアクションを 1 つのツールに混在させる。
manage_orders のように、注文検索・作成・取消を 1 つにまとめたくなることがあります。人間には説明できても、モデルには境界が曖昧なツールになります。いつ呼ぶべきかが分かりにくく、引数も複雑になります(任意フィールドだらけ)。get_order、create_order、cancel_order のように分割し、明確な説明とスキーマを用意するのが望ましいです。
誤り No.7: パーミッションと安全性をツール設計で考慮しない。
破壊的な操作(課金、データ削除)を行えるツールを説明で destructive と示さず、使用範囲も絞らなければ、リスクが生まれます。ChatGPT UI は追加確認を出さず、モデルも「境界事例」で呼ぼうとするかもしれません。適切なアノテーションと丁寧な説明(「ユーザーの明示的な同意の後にのみ使用する」など)は、tool‑call の段階からリスクを下げます。
GO TO FULL VERSION