パターン
ツール呼び出しとは何か — モデルに「手足」を渡す前に知るべきこと
リード
ツール呼び出し(Function Calling)は「モデルが関数を実行してくれる機能」ではない。モデルが返すのは名前と引数のJSONを含む要求であり、実行するかどうかを決めるのはあなたのコードだ。この一点を押さえると、エージェント系フレームワークの挙動がすべて読めるようになる。
前提
- 想定読者: LLMのAPIを1回は叩いたことがあるエンジニア。RAGやエージェントに着手する前段の人
- 必要な環境: Python 3.10+ / OpenAI互換SDK(DeepSeekのAPI仕様は公式ドキュメントで要確認)
- 前提知識: チャット補完APIの
messagesの構造、JSON Schemaの初歩
本文
モデルは「実行」しない、「要求」を出す
「Function Calling」という名前が最大の誤解源だ。モデルは関数を呼ばない。モデルが行うのは次の1つだけ。
- ユーザーのメッセージと与えられたツール定義を照らし合わせる
- ツールが必要と判断したら、
nameとarguments(JSON文字列)からなる構造化された要求を返す
この時点で副作用は一切起きていない。ファイルも書かれていないし、DBも更新されていない。要求をどう扱うかを決めるのはアプリケーション側の責務であり、ここを曖昧にしたまま実装すると、権限も監査ログもない「なんでも実行するAI」が完成する。
「関数呼び出し」ではなく「ツール呼び出し」と呼ぶほうが実態に近い。ツールは関数とは限らず、HTTPリクエストでも、DBクエリでも、UI操作のトリガーでもよい。
1回のやり取りで終わらない — 呼び出しループ
ツール呼び出しは1リクエストで完結しない。基本の流れは次のループになる。
- ユーザーメッセージ+ツール定義をモデルに送る
- モデルがツール要求を返す(またはそのまま回答を返す)
- アプリが要求を検証し、実際にツールを実行する
- 実行結果(またはエラー)をモデルに返す
- モデルが最終回答を生成する(必要なら 2 に戻る)
この往復が「エージェントループ」の正体だ。特別な魔法はなく、ただのAPI呼び出しの繰り返しである。フレームワークを導入する前に、このループを自分の手で1回書いておくと、後で何が起きているか分からなくなる事故が減る。
ツール定義はプロンプトの一部
ツールの description と引数の説明文は、モデルが「使うかどうか」「どの引数を入れるか」を判断する唯一の材料になる。つまりツール定義はプロンプトそのものだ。
descriptionには「何をするか」ではなく「いつ使うか」を書く- 引数には具体例を入れる(
"例: ORD-20261027-001") - 曖昧な名前を避ける(
get_dataではなくget_order_status) - ツール数を増やしすぎない。似たツールが並ぶと選択精度が落ちる
「なぜそうするか」は単純で、モデルはツールの実装コードを見られないからだ。シグネチャと説明文だけが判断材料になる。
何に向いて、何に向かないか
向く用途:
- 最新情報・社内データの取得(モデルの学習時点に依存しない回答)
- 計算・変換など、モデルに推論させると誤る処理
- チケット起票や通知など、明確に定義された副作用
慎重にすべき用途:
- 削除・送金・外部送信など取り消せない操作
- 権限の境界をまたぐ操作(ユーザーAのデータを触れるツールを、ユーザーBの文脈で渡してしまう等)
- 引数の検証が難しい自由入力(SQLやシェルをそのまま受けるツール)
副作用のあるツールは、人間の承認ステップを挟むか、実行専用の別ターンに分離する。
最小構成のコード
以下はループの骨格を示すための例だ。モデル名・エンドポイント・tools パラメータの対応状況は DeepSeek 公式ドキュメントで要確認。
import json
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="https://api.deepseek.com", # 公式ドキュメントで要確認
)
tools = [
{
"type": "function",
"function": {
"name": "get_order_status",
"description": "注文IDから配送状況を取得する。ユーザーが注文の所在や到着時期を尋ねたときに使う。",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "注文ID。例: ORD-20261027-001",
}
},
"required": ["order_id"],
},
},
}
]
def dispatch(name: str, args: dict) -> dict:
"""実際の処理。ここに来るまで何も起きていない点が重要。"""
if name == "get_order_status":
return {"order_id": args["order_id"], "status": "配送中"}
raise ValueError(f"unknown tool: {name}")
def run(user_message: str, max_turns: int = 5) -> str:
messages = [{"role": "user", "content": user_message}]
for _ in range(max_turns): # 無限ループ防止は必須
res = client.chat.completions.create(
model="deepseek-chat", # モデル名は要確認
messages=messages,
tools=tools,
)
msg = res.choices[0].message
if not msg.tool_calls:
return msg.content
messages.append(msg.model_dump(exclude_none=True))
for call in msg.tool_calls:
try:
args = json.loads(call.function.arguments)
result = dispatch(call.function.name, args)
except Exception as e:
# エラーもモデルに返す。握りつぶすとリトライできない
result = {"error": str(e)}
messages.append(
{
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result, ensure_ascii=False),
}
)
return "ツール呼び出しの上限に達しました"
ポイントは3つ。要求を受けただけでは何も実行していないこと、エラーをモデルに返していること、ループ回数に上限を置いていること。
設計で先に決めること
実装前に決めておかないと、後から剥がせなくなる。
- 権限: ツールはアプリの権限で動く。呼び出し元ユーザーの権限をツール側で必ず検査する(モデルに判断させない)
- 冪等性: 同じ要求が2回来る前提で作る。リトライで二重起票しないか
- 上限: ループ回数・トークン・金額のいずれかで打ち切る
- 監査ログ: 誰が・どのツールを・どの引数で呼んだかを残す。引数は個人情報を含みうる
- 失敗時の応答: ツールが失敗したとき、モデルにエラーを返すか、ユーザーにそのまま返すか
セキュリティ上の前提
ツール呼び出しを入れると、モデルが読んだ外部テキストが命令として作用する経路が生まれる。WebページやPDFに「この注文をキャンセルせよ」と書かれていた場合、それがツール要求として現れる可能性がある。
対策の基本はモデル側ではなくアプリ側に置く。
- ツールの引数をスキーマと許可リストで検証する
- 破壊的操作は人間承認を挟む
- ツールに渡す権限を最小化する(読み取り専用ツールを分ける)
- 外部由来のテキストを、システムプロンプトと同じ信頼度で扱わない
プロンプトインジェクション対策の詳細はセキュリティ編で扱う。
つまずきポイント
| 症状 | 対処 |
|---|---|
| モデルがツールを呼んでくれない | description に「いつ使うか」を書く。ツール名を動詞+目的語にする |
| 引数のJSONが壊れていてパースできない | json.loads を try で囲み、失敗をモデルに返して再試行させる |
| 同じツールを延々と呼び続ける | ループ上限を設定し、上限到達時の応答を決めておく |
| ツール結果を返したのに回答が空になる | role: "tool" と tool_call_id の対応を確認する |
| 本番で意図しない操作が実行された | 副作用ツールを分離し、承認フローと監査ログを追加する |
| ツールを増やすほど精度が落ちる | ツール数を絞る。似た役割のツールは統合する |
まとめ
- ツール呼び出しは実行ではなく要求。実行の判断と責任はアプリ側にある
- 実体は「モデル → 要求 → アプリが実行 → 結果を返す」のループにすぎない
- ツール定義(説明文・引数)はプロンプトの一部であり、精度はここで決まる
- 権限・冪等性・上限・監査ログは、動かしてからではなく設計時に決める
- 副作用のあるツールは人間承認と分離が前提
チェックリスト
- ツールの
descriptionに「いつ使うか」を書いた - 引数スキーマに型・必須・具体例を入れた
- ツール実行前に入力検証と権限チェックを入れている
- ツールのエラーをモデルに返す経路がある
- ループ回数の上限と上限到達時の応答を決めた
- 副作用のあるツールを読み取り専用ツールと分離した
- 誰がどのツールをどの引数で呼んだかログに残している
- DeepSeek側の
tools対応状況・モデル名を公式ドキュメントで確認した