OpenAI API の Function Calling で外部ツールを連携する
概要
LLM は学習済みの知識で文章を生成するのは得意ですが、「今日の天気」「社内 DB の在庫数」「外部 API の実行結果」といった、モデルの外側にある最新・固有の情報は知りません。この隙間を埋める仕組みが Function Calling(現在の OpenAI API では tools パラメータ)です。
本記事では、OpenAI API の Function Calling を使い、モデルに「どの関数を・どんな引数で呼ぶべきか」を判断させ、実際の処理はこちら側のコードで実行し、その結果をモデルに返して最終回答を作る一連のループを、ハンズオンで組み立てます。公開ドキュメントに基づく一般的な実装パターンのみを扱います。
環境
- Python 3.11
openaiライブラリ(v1 系)- モデル:
gpt-4o-mini(Function Calling 対応モデルならどれでも可)
pip install openai export OPENAI_API_KEY="(自分のキーを設定)"
Function Calling の仕組み
よくある誤解として「モデルが関数を実行してくれる」と思われがちですが、違います。実際の流れはこうです。
- こちらが「使える関数の一覧(スキーマ)」をリクエストに添えて送る
- モデルは質問に答えるために関数が必要と判断したら、「この関数を・この引数で呼んでほしい」という呼び出し指示(JSON)を返す
- 実際の関数実行はこちら側のコードが行う
- 実行結果をメッセージ履歴に追加して、もう一度モデルに送る
- モデルが結果を踏まえた自然言語の最終回答を返す
つまりモデルは「どの道具を・どう使うか」を決めるだけで、実行責任はアプリケーション側にあります。これはセキュリティ設計上も重要なポイントです。
ツールスキーマを定義する
モデルに教える「関数の説明書」を JSON Schema 形式で定義します。description はモデルが呼び出し判断に使うため、自然言語で丁寧に書くのがコツです。
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "指定した都市の現在の天気を取得する。ユーザーが天気を尋ねたときに使う。",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "都市名。例: Tokyo, Osaka",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度の単位。指定がなければ celsius。",
},
},
"required": ["city"],
},
},
}
]
実装例
実際に呼ばれる関数
ここでは外部 API の代わりにダミー実装にします。実運用では天気 API や社内サービスの呼び出しに置き換えます。
import json
def get_weather(city: str, unit: str = "celsius") -> str:
# 本来はここで外部 API を叩く。今回は固定値を返す。
fake_db = {
"Tokyo": {"celsius": 18, "fahrenheit": 64},
"Osaka": {"celsius": 20, "fahrenheit": 68},
}
temp = fake_db.get(city, {}).get(unit, "unknown")
return json.dumps({"city": city, "unit": unit, "temperature": temp})
available_functions = {
"get_weather": get_weather,
}
呼び出しループ全体
from openai import OpenAI
client = OpenAI()
MODEL = "gpt-4o-mini"
def run_conversation(user_input: str) -> str:
messages = [
{"role": "system", "content": "あなたは天気情報アシスタントです。"},
{"role": "user", "content": user_input},
]
# [1] 1回目のリクエスト(tools を添える)
response = client.chat.completions.create(
model=MODEL,
messages=messages,
tools=tools,
tool_choice="auto", # モデルに呼ぶかどうかを任せる
)
msg = response.choices[0].message
# [2] 関数呼び出しが不要ならそのまま返す
if not msg.tool_calls:
return msg.content
# アシスタントの tool_calls を含むメッセージを履歴に追加
messages.append(msg)
# [3] 要求された関数を順に実行
for tool_call in msg.tool_calls:
fn_name = tool_call.function.name
fn_args = json.loads(tool_call.function.arguments)
fn = available_functions[fn_name]
result = fn(**fn_args)
# [4] 実行結果を role="tool" で履歴に追加
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
# [5] 結果を踏まえて最終回答を生成
second = client.chat.completions.create(
model=MODEL,
messages=messages,
)
return second.choices[0].message.content
if __name__ == "__main__":
print(run_conversation("東京の天気を教えて"))
ポイントの解説
tool_choice="auto": モデルに「関数を使うかどうか」を任せます。必ず特定の関数を呼ばせたい場合は{"type": "function", "function": {"name": "get_weather"}}を指定します。tool_call_idの対応付け: 結果を返すときは、呼び出しごとに割り当てられたidをtool_call_idに必ずセットします。これが一致しないとモデルは結果を紐づけられません。- 複数呼び出し(parallel tool calls):
msg.tool_callsは配列です。モデルが一度に複数の関数呼び出しを要求することがあるため、必ずループで全件処理します。
引数のバリデーションを忘れない
tool_call.function.arguments はモデルが生成した文字列です。スキーマに沿っているはずですが、100% 保証されません。実行前に検証しましょう。
fn_args = json.loads(tool_call.function.arguments)
if "city" not in fn_args:
result = json.dumps({"error": "city is required"})
else:
result = available_functions[fn_name](**fn_args)
エラーも role="tool" で素直に返すと、モデルが「引数が足りなかった」と理解して聞き直したり自己修正したりします。例外で握りつぶすより、エラーを結果としてモデルに戻すほうが会話が破綻しません。
確認方法
- 上記スクリプトを実行し、「東京の天気を教えて」で最終回答に気温が含まれることを確認する。
tool_choice="auto"のまま「こんにちは」とだけ送り、msg.tool_callsがNoneになり関数を呼ばずに通常応答する(=むやみに関数を呼ばない)ことを確認する。- わざと存在しない都市名を聞いて、
unknownを受けたモデルが「データがない」旨を自然文で返すことを確認する。
ログで tool_calls の中身を出力すると、モデルがどんな引数を生成したか追えてデバッグが楽になります。
注意点
- 実行はアプリ側の責任: モデルは「呼べ」と言うだけです。削除・課金・外部送信など副作用のある関数は、引数を検証し、必要なら人間の承認を挟む設計にします。モデルの出力をそのまま危険な操作に流さないこと。
- 無限ループ防止: 結果を返した後にモデルがさらに関数を呼ぶこともあります。ループ回数に上限を設けましょう。
- トークン消費: 関数の実行結果も履歴に積まれるため、大きな JSON を丸ごと返すとトークンを食います。必要な項目だけ返すのが無難です。
- スキーマの
descriptionが命: 関数を呼ぶ/呼ばないの精度は description の質に大きく依存します。曖昧だと誤呼び出しや呼び忘れが起きます。
まとめ
Function Calling は「モデルに関数を実行させる機能」ではなく、「モデルに、どの関数をどの引数で呼ぶべきかを判断させ、実行はアプリ側で行い、結果を返して最終回答を作る」という協調ループです。要点は3つ。
- ツールのスキーマは自然言語の
descriptionを丁寧に書く tool_callsは配列なので全件ループし、tool_call_idで結果を対応付ける- 引数検証と副作用の制御はアプリ側の責任で行う
この基本ループを押さえれば、天気 API に限らず、検索・DB 参照・社内システム連携など、あらゆる外部ツールを LLM に接続できます。
