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 の仕組み

よくある誤解として「モデルが関数を実行してくれる」と思われがちですが、違います。実際の流れはこうです。

  1. こちらが「使える関数の一覧(スキーマ)」をリクエストに添えて送る
  2. モデルは質問に答えるために関数が必要と判断したら、「この関数を・この引数で呼んでほしい」という呼び出し指示(JSON)を返す
  3. 実際の関数実行はこちら側のコードが行う
  4. 実行結果をメッセージ履歴に追加して、もう一度モデルに送る
  5. モデルが結果を踏まえた自然言語の最終回答を返す

つまりモデルは「どの道具を・どう使うか」を決めるだけで、実行責任はアプリケーション側にあります。これはセキュリティ設計上も重要なポイントです。

ツールスキーマを定義する

モデルに教える「関数の説明書」を 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" で素直に返すと、モデルが「引数が足りなかった」と理解して聞き直したり自己修正したりします。例外で握りつぶすより、エラーを結果としてモデルに戻すほうが会話が破綻しません。

確認方法

  1. 上記スクリプトを実行し、「東京の天気を教えて」で最終回答に気温が含まれることを確認する。
  2. tool_choice="auto" のまま「こんにちは」とだけ送り、msg.tool_calls が None になり関数を呼ばずに通常応答する(=むやみに関数を呼ばない)ことを確認する。
  3. わざと存在しない都市名を聞いて、unknown を受けたモデルが「データがない」旨を自然文で返すことを確認する。

ログで tool_calls の中身を出力すると、モデルがどんな引数を生成したか追えてデバッグが楽になります。

注意点

  • 実行はアプリ側の責任: モデルは「呼べ」と言うだけです。削除・課金・外部送信など副作用のある関数は、引数を検証し、必要なら人間の承認を挟む設計にします。モデルの出力をそのまま危険な操作に流さないこと。
  • 無限ループ防止: 結果を返した後にモデルがさらに関数を呼ぶこともあります。ループ回数に上限を設けましょう。
  • トークン消費: 関数の実行結果も履歴に積まれるため、大きな JSON を丸ごと返すとトークンを食います。必要な項目だけ返すのが無難です。
  • スキーマの description が命: 関数を呼ぶ/呼ばないの精度は description の質に大きく依存します。曖昧だと誤呼び出しや呼び忘れが起きます。

まとめ

Function Calling は「モデルに関数を実行させる機能」ではなく、「モデルに、どの関数をどの引数で呼ぶべきかを判断させ、実行はアプリ側で行い、結果を返して最終回答を作る」という協調ループです。要点は3つ。

  1. ツールのスキーマは自然言語の description を丁寧に書く
  2. tool_calls は配列なので全件ループし、tool_call_id で結果を対応付ける
  3. 引数検証と副作用の制御はアプリ側の責任で行う

この基本ループを押さえれば、天気 API に限らず、検索・DB 参照・社内システム連携など、あらゆる外部ツールを LLM に接続できます。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。