Function Calling入門|Pythonで複数ツールを呼ぶ

Function CallingをPythonで実装し、複数ツールを呼び分ける記事のアイキャッチ LLM

OpenAI APIのFunction Callingは、モデルに「使える関数」を伝え、必要なときに関数名と引数を返してもらう仕組みです。モデルがPython関数を直接実行するわけではなく、実行するかどうかと、実行結果を返す処理はアプリ側が管理します

前回の「AIエージェントの作り方|Pythonで動かす最小構成を図解」では、天気取得ツールを1つだけ持つ最小ループを作りました。今回は一段進めて、商品情報と送料という複数のツールを安全に振り分ける方法を解説します。

この記事で扱うのは次の内容です。

  • Function Callingと通常の文章生成の違い
  • 複数ツールのJSON Schema定義
  • 1回の応答に含まれる複数のfunction_callの処理
  • call_idを使った結果の対応付け
  • 不正なJSON、未知のツール、業務エラーの返し方
  • tool_choiceparallel_tool_callsの使い分け
スポンサーリンク

Function Callingの処理の流れ

今回のサンプルでは、「商品KB-101の在庫と東京都への送料を調べて、合計額を教えて」と依頼します。モデルは商品取得と送料取得の2ツールを選び、Python側がそれぞれを実行します。

Function Callingで複数ツールを実行し結果を統合する流れ

処理は次の5段階です。

  1. アプリがユーザーの依頼とツール定義をResponses APIへ送る
  2. モデルが必要なツール名と引数をfunction_callとして返す
  3. Python側が許可済み関数だけを実行する
  4. 各結果を同じcall_idfunction_call_outputとして返す
  5. モデルがツール結果を統合して最終回答を作る

Function Callingは「関数の実行機能」ではなく、モデルとアプリの間で関数呼び出しを受け渡すためのインターフェースと考えると分かりやすくなります。

スポンサーリンク

事前準備

Python 3.10以降を用意し、OpenAIのPythonライブラリをインストールします。

python -m venv .venv
source .venv/bin/activate
pip install --upgrade openai

Windows PowerShellでは、仮想環境の有効化コマンドを次のように変更します。

.venv\Scripts\Activate.ps1

APIキーはコードへ直接書かず、環境変数へ設定します。

export OPENAI_API_KEY="あなたのAPIキー"

OpenAI APIの利用枠と課金はChatGPTの月額プランとは別に管理されます。

Step 1:実行するPython関数を作る

学習用として、商品情報と送料を固定データから返す2つの関数を作ります。

from typing import Any

def get_product(product_id: str) -> dict[str, Any]:
    products = {
        "KB-101": {"name": "ワイヤレスキーボード", "price_yen": 6800, "stock": 12},
        "MS-205": {"name": "ワイヤレスマウス", "price_yen": 4200, "stock": 0},
    }
    if product_id not in products:
        raise ValueError("商品IDが見つかりません")
    return {"product_id": product_id, **products[product_id]}

def get_shipping_fee(prefecture: str) -> dict[str, Any]:
    fees = {"東京都": 550, "大阪府": 660, "沖縄県": 1320}
    if prefecture not in fees:
        raise ValueError("送料を計算できない都道府県です")
    return {"prefecture": prefecture, "shipping_fee_yen": fees[prefecture]}

実運用では、データベースや在庫API、配送APIなどへ置き換えます。ここではFunction Callingの流れに集中するため、外部通信を使いません。

Step 2:複数ツールをJSON Schemaで定義する

モデルへ関数名、用途、引数の形式を伝えます。Responses APIでは、関数ツールをtype: "function"として定義します。

TOOLS = [
    {
        "type": "function",
        "name": "get_product",
        "description": "商品IDから商品名、価格、在庫数を取得する。",
        "parameters": {
            "type": "object",
            "properties": {
                "product_id": {
                    "type": "string",
                    "description": "商品ID。例: KB-101",
                }
            },
            "required": ["product_id"],
            "additionalProperties": False,
        },
        "strict": True,
    },
    {
        "type": "function",
        "name": "get_shipping_fee",
        "description": "配送先の都道府県から送料を取得する。",
        "parameters": {
            "type": "object",
            "properties": {
                "prefecture": {
                    "type": "string",
                    "description": "都道府県名。例: 東京都",
                }
            },
            "required": ["prefecture"],
            "additionalProperties": False,
        },
        "strict": True,
    },
]

OpenAIの公式ドキュメントでは、スキーマに従う精度を高めるためstrict: trueが推奨されています。Strict modeでは、オブジェクトごとにadditionalProperties: falseを設定し、propertiesに書いたフィールドをrequiredへ含めます。省略可能な値は、型にnullを含めて表現できます。

descriptionは具体的に書く

ツール名だけでなく、「いつ使うのか」「何を返すのか」が分かる説明にします。似たツールが複数ある場合、説明が曖昧だとモデルが選び分けにくくなります。

悪い例:

"description": "商品を取得する"

改善例:

"description": "商品IDから商品名、税込価格、現在の在庫数を取得する。"

Step 3:許可した関数だけを振り分ける

モデルが返した文字列をeval()などで実行してはいけません。許可する関数を辞書へ登録し、名前が一致したものだけを呼び出します。

from typing import Callable

TOOL_HANDLERS: dict[str, Callable[..., dict]] = {
    "get_product": get_product,
    "get_shipping_fee": get_shipping_fee,
}

続いて、JSONの解析や業務エラーを処理する共通関数を作ります。

import json
from typing import Any

def call_tool(name: str, arguments_json: str) -> dict[str, Any]:
    handler = TOOL_HANDLERS.get(name)
    if handler is None:
        return {"ok": False, "error": "許可されていないツールです"}

    try:
        arguments = json.loads(arguments_json)
        if not isinstance(arguments, dict):
            raise ValueError("引数はJSONオブジェクトである必要があります")
        return {"ok": True, "data": handler(**arguments)}
    except (json.JSONDecodeError, TypeError, ValueError) as exc:
        return {"ok": False, "error": str(exc)}

失敗を例外のまま終了させず、ok: falseの構造化データとしてモデルへ返しています。これによりモデルは、確認できなかった項目をユーザーへ説明できます。ただし、内部のスタックトレース、認証情報、SQL文などをそのままモデルへ返さないようにしてください。

Step 4:複数のfunction_callを処理する

Responses APIへ、ツール定義とユーザー入力を送ります。

from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-5.6-terra",
    instructions=(
        "あなたは商品案内アシスタントです。商品情報や送料は必ずツールで確認し、"
        "ツールが失敗した場合は推測で補わないでください。"
    ),
    tools=TOOLS,
    parallel_tool_calls=True,
    input=[
        {
            "role": "user",
            "content": "商品KB-101の在庫と東京都への送料を確認して、合計額を教えてください。",
        }
    ],
)

モデルは1回の応答に複数のfunction_callを含める場合があります。そのため、最初の1件だけではなく、response.outputをすべて確認します。

input_items = [{"role": "user", "content": user_input}]
input_items += response.output

for tool_call in response.output:
    if tool_call.type != "function_call":
        continue

    result = call_tool(tool_call.name, tool_call.arguments)
    input_items.append(
        {
            "type": "function_call_output",
            "call_id": tool_call.call_id,
            "output": json.dumps(result, ensure_ascii=False),
        }
    )

response.output自体も次の入力へ追加します。推論項目を含むモデルでは、関数呼び出しだけを抜き出さず、返された出力項目をまとめて引き継ぐのが重要です。

call_idは変更しない

call_idは「どの関数呼び出しに対する結果か」を対応付ける識別子です。商品結果と送料結果が入れ替わらないよう、受け取った値をそのままfunction_call_outputへ設定します。

Step 5:最終回答までループする

ツール結果を追加してもう一度Responses APIへ送ります。新しいツール呼び出しがなければ、response.output_textが最終回答です。

def run_assistant(user_input: str, max_turns: int = 4) -> str:
    input_items = [{"role": "user", "content": user_input}]

    for _ in range(max_turns):
        response = client.responses.create(
            model="gpt-5.6-terra",
            instructions=(
                "商品情報や送料は必ずツールで確認し、"
                "失敗した項目を推測で補わないでください。"
            ),
            tools=TOOLS,
            parallel_tool_calls=True,
            input=input_items,
        )

        input_items += response.output
        tool_calls = [item for item in response.output if item.type == "function_call"]

        if not tool_calls:
            return response.output_text

        for tool_call in tool_calls:
            result = call_tool(tool_call.name, tool_call.arguments)
            input_items.append(
                {
                    "type": "function_call_output",
                    "call_id": tool_call.call_id,
                    "output": json.dumps(result, ensure_ascii=False),
                }
            )

    raise RuntimeError("最大ターン数に達したため処理を停止しました")

最大ターン数を設定しておけば、想定外にツール呼び出しが続いたときに停止できます。実運用ではAPIタイムアウト、再試行回数、1リクエスト当たりの予算上限も設定します。

parallel_tool_callsの使い方

parallel_tool_calls=Trueでは、モデルが1ターンで複数の関数を要求できます。今回の商品取得と送料取得のように、互いの結果を必要としない処理に向いています。

parallel_tool_calls=True

一度に0個または1個だけ呼ばせたい場合は、Falseを設定します。

parallel_tool_calls=False

「商品価格を取得し、その金額を使って割引額を計算する」のように前後関係がある処理は、同時実行せず、結果を返した次のターンで別のツールを選ばせます。

tool_choiceで呼び出し方を制御する

デフォルトのautoでは、モデルがツールを使うか判断します。

tool_choice="auto"

少なくとも1つの関数を使わせる場合はrequiredを指定します。

tool_choice="required"

特定の関数を1つだけ要求することもできます。

tool_choice={"type": "function", "name": "get_product"}

ユーザーの質問へ通常回答できる場面ではauto、外部データの確認が必須な処理ではrequiredという使い分けが基本です。モデルに強制するだけでなく、アプリ側でも「必要な結果がすべて揃ったか」を検証してください。

よくあるエラーと対策

function_callが返らない

  • ツールのdescriptionを具体的にする
  • 「商品情報は必ずツールで確認する」など、利用条件をinstructionsへ書く
  • ツール利用が必須ならtool_choice="required"を検討する

strict: trueでリクエストが拒否される

  • すべてのオブジェクトにadditionalProperties: falseがあるか確認する
  • propertiesの全フィールドがrequiredへ含まれているか確認する
  • 省略可能な値は、型へnullを含める

複数ツールの結果が混ざる

function_call_outputへ、元の呼び出しと同じcall_idを設定します。配列の順序だけに依存して対応付けないでください。

ツール内で例外が発生する

ユーザーへ説明できるエラーだけを構造化して返します。一時的なネットワーク障害なら回数制限付きで再試行し、入力ミスや権限不足は再試行しません。

同じツールを繰り返し呼ぶ

最大ターン数とツール回数を制限し、同じ引数の呼び出し結果を短時間キャッシュします。ログにはツール名、引数の要約、実行時間、成功・失敗、call_idを残すと原因を追いやすくなります。

Function CallingとStructured Outputsの違い

機能 主な用途
Function Calling アプリ側の関数やAPIを選び、必要な引数を渡す
Structured Outputs 最終回答そのものを決めたJSON Schemaで受け取る

外部データの取得や操作にはFunction Calling、画面表示用JSONなど最終出力の形式固定にはStructured Outputsを使います。両方を組み合わせることもできます。

実運用前のチェックリスト

  • ☐ 呼び出せる関数を許可リストで限定した
  • strict: trueとJSON Schemaの要件を確認した
  • ☐ 引数の値をPython側でも検証した
  • ☐ 外部送信、購入、削除、公開の前に承認を入れた
  • ☐ 最大ターン数、タイムアウト、再試行回数を設定した
  • ☐ ツール名、成功・失敗、所要時間をログへ残した
  • ☐ エラーメッセージから機密情報を除いた
  • ☐ 正常系だけでなく、未知IDやAPI障害もテストした

まとめ

Pythonで複数のFunction Callingを扱うポイントは次の5つです。

  • ツールごとに用途が分かるJSON Schemaを定義する
  • strict: trueを使い、引数の形式を固定する
  • 返されたすべてのfunction_callを処理する
  • 結果は同じcall_idfunction_call_outputとして返す
  • ツールの実行可否と安全確認はPython側で管理する

まずは読み取り専用のツール2つから始め、ログと失敗処理を確認してから外部操作へ広げるのがおすすめです。AIエージェント全体の構造を先に確認したい場合は、「AIエージェントとは?生成AI・RAGとの違いを図解」も参照してください。

参考資料

コメント

タイトルとURLをコピーしました