このレッスンのロックを解除するには、まず「レッスン3:プロンプトからエージェントワークフローへ」を完了してください。前のレッスンに戻る

はじめに

AIエージェントが真に役立つ存在になるのは、自身のコンテキストウィンドウの外にアクセスできるようになったときです。ツール呼び出しとは、言語モデルが「データベースから1行を取得する必要がある」「天気APIを叩く必要がある」「サーバー処理を起動する必要がある」と判断し、その結果を次の推論ステップの一部として受け取るための仕組みです。このレッスンでは、Webアプリケーション内でそのループを信頼性高く動作させるための設計方法を扱います。

ツールは3つの要素を持つ単なる関数として扱います。安定した名前、パラメータを記述するJSONスキーマ、そしてモデルにいつそれを使うべきかを伝える説明文です。それ以外はすべてフロントエンドとバックエンドの配管に過ぎません。

ユーザーメッセージ -> LLMがツール呼び出しを判断 -> バックエンドが関数を実行 -> ツール結果がLLMに返される -> ユーザーに最終回答、というループを示す図

ツールスキーマと関数呼び出しの基本

ほとんどの現代的なLLM APIは同じ構造を公開しています。ツール定義の配列を渡し、それぞれの要素が名前、説明、パラメータスキーマを含みます。モデルはそのツールのいずれかを呼び出すための構造化リクエストを返すことがあり、それをバックエンド側で実行します。

以下はOpenAI Python SDK風の最小限のツール宣言です。スキーマこそが、モデルが引数を生成するかどうかを判断するために実際に読むものです。

python
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city. Use when the user asks about weather, temperature, or conditions in a specific location.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. 'Hanoi'"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
}
}
}
]

重要な設計判断が3つあります。第一に、説明文はシステムプロンプトと同じ口調で書かれた、「いつそのツールを発火させるか」をモデルに伝える短い指示です。第二に、パラメータは標準的なJSON Schemaを使い、enumや必須フィールドを強制します。第三に、モデルが省略可能だと誤解しない限り、デフォルト値はスキーマに含めません。

Webアプリバックエンドへのツール接続

実際のWebアプリでは、エージェントはブラウザ内では動作しません。ブラウザが会話をバックエンドに送信し、バックエンドがツール一覧とともにLLMを呼び出し、モデルがツール呼び出しを返したら、サーバーが自前のサービスに対してそれを実行します。結果がツールメッセージとしてモデルに送り返され、モデルが最終的な返答を生成します。

最小限のFastAPIハンドラは次のようになります。レスポンス内のツール呼び出しを検出し、対応するPython関数を実行し、その出力を会話にフィードバックします。

python
from fastapi import FastAPI
from openai import OpenAI
app = FastAPI()
client = OpenAI()
def get_weather(city: str, unit: str = "celsius") -> dict:
# call your real weather provider here
return {"city": city, "unit": unit, "temp": 30, "condition": "sunny"}
dispatch = {"get_weather": get_weather}
@app.post("/chat")
def chat(messages: list):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
)
msg = response.choices[0].message
if msg.tool_calls:
for call in msg.tool_calls:
fn = dispatch[call.function.name]
args = json.loads(call.function.arguments)
result = fn(**args)
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
response = client.chat.completions.create(model="gpt-4o-mini", messages=messages, tools=tools)
return {"reply": response.choices[0].message.content}

小さなプロジェクトでは、ディスパッチ辞書が最もクリーンなパターンです。大規模なシステムでは、ハンドラを編集せずにツールを追加できるよう、ツール登録をレジストリオブジェクトに移動します。

パラメータ設計のパターン

ツールの失敗の大半は、モデルではなくパラメータ設計の悪さに起因します。これを大きく減らす2つのパターンがあります。

第一に、ネストされたオブジェクトよりもフラットなパラメータを優先してください。ネストされたlocationオブジェクト1つを受け取るツールよりも、latとlonを別々に受け取るツールの方が、モデルが正しく値を埋めやすいです。第二に、ユーザーが実際に使う語彙を反映してください。ユーザーが「郵便番号」と言うなら、postal_codeではなくzip_codeというパラメータ名を公開します。

json
{
"name": "search_products",
"description": "Search the product catalog by keyword and optional filters.",
"parameters": {
"type": "object",
"properties": {
"keyword": {"type": "string", "description": "Search term, e.g. 'running shoes'"},
"max_price_usd": {"type": "number", "description": "Optional upper price limit in USD"},
"in_stock_only": {"type": "boolean", "description": "If true, only return items currently in stock"}
},
"required": ["keyword"]
}
}

フラットなパラメータが3つ、1つは必須、残りにはモデルが空白にできる妥当なデフォルト値があります。

よくある落とし穴

ほぼすべての連携でいくつかのミスが繰り返し現れます。存在しないツール名をモデルがハルシネーションするため、ディスパッチ前に必ずcall.function.nameを登録済みツールと照合して検証してください。関数が整数を期待している場所にモデルが文字列を渡すため、呼び出し前にスキーマに基づいて引数を型変換してください。また、モデルが「まだ情報が必要だ」と思ってツール呼び出しの無限ループに入るため、ループは3〜5回に上限を設け、収束しない場合はフォールバック回答を提示してください。

すべてのツール呼び出しを引数と結果とともにログ記録することが、本番環境で構築できる最もレバレッジの高いデバッグ習慣です。本番環境で障害が起きたとき、そのログを見れば、モデルが誤ったツールを選んだのか、正しいツールを誤った引数で選んだのか、バックエンドが想定外の形式を返したのかがわかります。

まとめ

ツール呼び出しはチャットボットをエージェントへと変えます。ツールをJSON Schemaとして定義し、すべてのLLM呼び出し時に渡し、モデルが要求したものをバックエンドで実行し、その結果をツールメッセージとしてフィードバックします。良いパラメータ設計はフラットで、ユーザーの語彙を使用し、必須フィールドを明確に示します。ツール名は必ず検証し、ループには上限を設け、すべてをログに残してください。次のレッスンでは、このループの上にプランニングとマルチステップ推論を積み重ねていきます。

レッスンの練習

レッスンのチェックポイント

学んだ内容を復習し、回答へのフィードバックを受けましょう。

0/1
回答済み · 0/1
レッスン4:ツール呼び出しと外部連携