OpenAIのtool calling(旧:Function calling)とは?仕組みと実装フローを解説

※当サイトは、アフィリエイト広告を利用しています。

「モデルに外部APIやシステム操作を任せたいけど、安全で安定した設計ってどうすればいいの?」という疑問を持っていませんか。
本記事では、従来「Function calling」と呼ばれていた仕組みが現在は「tool calling(ツール呼び出し)」と呼ばれる点を踏まえつつ、仕組み・実装フロー・JSONスキーマの設計・運用時の注意点まで、実務視点で整理します。

この記事を読めば、ツール(関数)定義の作り方、モデルがいつ・どのようにツールを呼ぶかの判断ロジック、アプリ側での実行と結果の戻し方、さらにtool_choiceの取り扱いなど最新の運用ノウハウがわかります。

なお本記事で使っているコードは以下のGitHubに格納しています。
https://github.com/kuretom-blog/demo-openai-tool-calling

tool calling(旧:Function calling)とは何か

OpenAIのFunction calling(ツール呼び出し)は、モデルが応答を生成する過程「この関数を呼んでほしい」と構造化された指示(ツールコール)を返す仕組みです。

OpenAIのドキュメントでは過去に「Function calling」と呼ばれていた仕組みが、より広い概念として「tool calling(ツール呼び出し)」と表現されるようになっています。

天気情報の取得や顧客データの参照、返金処理の実行など、従来のテキスト生成だけでは実現できなかったリアルタイムの外部連携が可能になります。

具体的なイメージとして、ユーザーが「パリの天気を教えて」と入力したとします。このとき、モデルは自分で天気サービスを叩くのではなく、get_weather(location="Paris")という呼び出し指示を返します。
その指示を受け取ったアプリ側が実際に天気APIを呼び出し、取得した結果を再びモデルへ渡す——この「やり取りの往復」こそがFunction callingの本質です。

典型的なユースケースは天気取得、顧客データ参照、支払い/返金処理など、リアルタイムな外部連携が必要な場面です。

まずは4つの用語を押さえておきましょう。

  • ツール(Tool):アプリ側で実行できる関数やサービスの定義。モデルはこの一覧から「何を呼ぶか」を判断します。
  • 関数(Function):JSONスキーマで記述される個々の呼び出し可能要素。名前・説明・パラメータ定義を持ちます。
  • ツールコール(Tool call / function_call:モデルが「この関数をこの引数で呼んでほしい」と返す応答のこと。
  • ツール出力(Tool output / function_call_output:アプリが関数を実行して得た結果をモデルへ戻す値。これを受けてモデルが最終応答を生成します。
知りたイヌ

モデルが関数を呼ぶだけで実行はアプリがやるんだよね?

くれとむ

そう。モデルは「何を呼ぶか」を決めるだけで、副作用はアプリの責任だよ。

tool callingの全体像

基本フロー

処理はシンプルな往復です。
モデルにツール定義を渡す → モデルがツールコールを返す → アプリが実行 → 実行結果をモデルに渡す → モデルが最終応答を生成する、という5ステップです。

重要なのは「モデルは実行を行わない」点。モデルはあくまで何を・どのような引数で呼ぶかを判断するだけで、実際の副作用(API呼び出し・DB操作など)はアプリ側が責任を持って行います。

  1. ツールをモデルに渡すnamedescriptionparameters(JSONスキーマ)をAPIリクエストに含める。
  2. モデルがツールコールを返す:responseのoutput内にfunction_call/tool_callが生成される。
  3. アプリ側で関数を実行する:指示に従って外部APIやDB操作を実行。
  4. 実行結果をモデルに渡す:messagesやinputにtool_call_outputとして結果を追加して再リクエストする。
  5. モデルが最終応答を返す:ツール結果を踏まえた自然言語応答が得られる。

複数呼び出しや並列の扱い

  • モデルは0回・1回・複数回のツールコールを返す可能性があります。実装は複数のtool_callを順に処理できるようにし、同時に複数のツールコールが返ってきた場合の処理(並列不可 / parallel_tool_calls制御)に備えてください。
  • また、ツール定義を増やすとリクエストトークンが増え、モデルの判断に影響する場合があるので初期は少数から始めましょう。

関数(function)定義の作り方

最小構成とdescriptionの重要性

関数はJSONスキーマ形式で定義します。

最低限必要なフィールドは name(関数名)・description(何をする関数かの説明)・parameters(引数の構造定義)の3つです。
descriptionには「いつ」「どんな条件で」その関数を呼ぶべきかを具体的に書くと呼び出し精度が上がります。

サンプル

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_horoscope",
            "description": "指定された星座の運勢を取得する",
            "parameters": {
                "type": "object",
                "properties": {
                    "sign": {
                        "type": "string",
                        "description": "星座名(例: 'おうし座')",
                    },
                    "day": {
                        "type": "string",
                        "enum": ["today", "tomorrow"],
                        "description": "運勢を取得する日(今日または明日)",
                    },
                },
                "required": ["sign", "day"],
                "additionalProperties": False,
            },
            "strict": True,
        },
    }
]

strictを有効にするとモデルはスキーマに厳密に従って関数呼び出しを生成します。
strict運用時は全フィールドをrequiredに列挙し、additionalProperties:falseにする必要があります。

ただしstrictは冗長なスキーマや頻繁に変わるスキーマでレイテンシやキャッシュに影響する場合があるため、運用方針に応じて調整してください。

知りたイヌ

requiredを忘れるとどうなるの?

くれとむ

モデルが値を省略して後続処理が壊れることが多いです

スキーマだけを渡して「構造化JSONジェネレータ」として使う運用

関数を「呼び出させない」運用(ツール定義でスキーマだけ使い、実行は行わない)も有用です。
モデルに「このスキーマに従ったJSONを返してほしい」と指示し、返ってきたargumentsをアプリ側で型検証してから使います。

{
  "name": "generate_user",
  "parameters": {
    "type": "object",
    "properties": {
      "name": { "type": "string" },
      "age": { "type": "number" }
    },
    "required": ["name", "age"]
  },
  "strict": true
}

用途は以下の通りです。

  • モデルに対して「このスキーマに従った構造化JSONを返してほしい」だけを要求する(ツール呼び出しで型定義を利用)
  • 外部呼び出しが不要なケースで、スキーマ検証やフロントエンドの型生成に使う
  • 型定義をベースにバックエンドでpydanticやzodのスキーマを生成して静的検査を行う

使い方のイメージ

STEP

関数を”function”として渡してOpenAIのAPIにリクエスト

STEP

モデルがtool_callを返したら、そのargumentsを受信してアプリ側で型検証だけ行う
(実行はしない)

関数定義:

くれとむ

ユーザー入力:適当なユーザーを生成して。

モデル出力(tool_call):

{
  "name": "generate_user",
  "arguments": {
    "name": "Taro",
    "age": 25
  }
}

結果の受け取り処理

data = response.tool_calls[0].function.arguments

高度な運用とAPI機能(tool_choice, tool_search, namespaces, streaming, custom tools)

tool_choiceで呼び出し挙動を制御する

tool_choiceはモデルに対してツール呼び出しの「どのように」を指定するパラメータです。
主な選択肢は以下の通りです。

  • “auto”(デフォルト):モデルが自律的に0回以上呼ぶ
  • “required”:少なくとも1回はツール呼び出しさせる
  • {“type”:”function”,”name”:”get_weather”}:特定関数を必ず1回呼ばせる(Forced Function)
  • “none”:ツール呼び出しをまったく許可しない(テキストのみ)
  • allowed_tools:渡したツール群のサブセットだけを許可する

tool_searchとnamespaces

ツールが大量にある場合はすべてを最初から渡すとトークン負荷が高くなります。
tool_searchを使うとモデルが必要なツールを検索してロードできます。
またnamespacesでツール群を論理的に分けると選択が安定します。

ストリーミングと関数呼び出し(リアルタイムで引数が埋まる)

ストリーミングを有効にすると、モデルがツールを呼ぶ際に引数がリアルタイムで埋まる様子(delta)が届きます。これを集約して最終的なtool_callオブジェクトを作る必要があります。

# ストリーミング時の疑似的な集約処理(概念)
final_tool_calls = {}
for chunk in stream:
    for tool_call_delta in chunk.choices[0].delta.tool_calls or []:
        idx = tool_call_delta.index
        if idx not in final_tool_calls:
            final_tool_calls[idx] = tool_call_delta
        final_tool_calls[idx].function.arguments += tool_call_delta.function.arguments

ストリーミングは進捗表示や長い引数を段階的に受け取りたい場面で有効ですが、実装は少し複雑になります。

カスタムツールと文法制約(Lark/Regex)

JSONスキーマだけでなく、customツール(任意の文字列入力を受け取るツール)や文法(Lark/Regex)を使ってツール入力を厳密に制約できます。
たとえばコード実行や数式生成の入力文法をCFGで定義すると、モデルの出力を構文レベルで守れます。

動作確認してみた(Python / JavaScript)

OpenAI公式ドキュメントで紹介されている get_horoscope を題材に、Function calling(Tool calling)の動作を実際に確認してみました。
以下のコードでは最初のレスポンスでfunction_callが返ってきたら、アプリで実行・結果をmessagesに追加して再度投げ直しています。これが標準的な流れです。

実際の運用ではAPIキーは環境変数から取得し、SDKバージョン・モデル名を最新の公式ドキュメントに合わせてください。

本記事で使っているコードは以下のGitHubに格納しています。
https://github.com/kuretom-blog/demo-openai-tool-calling

Pythonコード(SDK呼び出し例)

import json
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv(dotenv_path="../.env")

client = OpenAI()

messages = [
    {"role": "system", "content": "あなたは親切なアシスタントです。"},
    {"role": "user", "content": "おうし座の今日の運勢を教えてください。"},
]

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_horoscope",
            "description": "指定された星座の運勢を取得する",
            "parameters": {
                "type": "object",
                "properties": {
                    "sign": {
                        "type": "string",
                        "description": "星座名(例: 'おうし座')",
                    },
                    "day": {
                        "type": "string",
                        "enum": ["today", "tomorrow"],
                        "description": "運勢を取得する日(今日または明日)",
                    },
                },
                "required": ["sign", "day"],
                "additionalProperties": False,
            },
            "strict": True,
        },
    }
]

response = client.chat.completions.create(
    model="gpt-5-mini",
    messages=messages,
    tools=tools,
)

message = response.choices[0].message
print("finish_reason:", response.choices[0].finish_reason)

if message.tool_calls:
    for tool_call in message.tool_calls:
        func_name = tool_call.function.name
        func_args = json.loads(tool_call.function.arguments)
        print(f"tool_call: {func_name}({func_args})")

        if func_name == "get_horoscope":
            tool_result = {
                "horoscope": f"今日の{func_args.get('sign')}の運勢: 全体運は好調です!積極的に行動すると吉。"
            }
        else:
            raise RuntimeError(f"Unknown function: {func_name}")

        messages.append(message)
        messages.append(
            {
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(tool_result, ensure_ascii=False),
            }
        )

    response2 = client.chat.completions.create(
        model="gpt-5-mini",
        messages=messages,
    )

    print(response2.choices[0].message.content)
else:
    print(message.content)
# 実行コマンド
python demo-function-call.py

JavaScript

import OpenAI from "openai";

const client = new OpenAI(); // OPENAI_API_KEY 環境変数を自動で読み取る

const messages = [
    { role: "system", content: "あなたは親切なアシスタントです。" },
    { role: "user",   content: "おうし座の今日の運勢を教えてください。" },
];

const tools = [
    {
        type: "function",
        function: {
            name: "get_horoscope",
            description: "指定された星座の運勢を取得する",
            parameters: {
                type: "object",
                properties: {
                    sign: { type: "string", description: "星座名(例: 'おうし座')" },
                    day:  { type: "string", enum: ["today", "tomorrow"], description: "運勢を取得する日(今日または明日)" },
                },
                required: ["sign", "day"],
                additionalProperties: false,
            },
            strict: true,
        },
    },
];

// ステップ1〜2: ツール定義を添えてモデルへ送信する
const response = await client.chat.completions.create({
    model: "gpt-5-mini",
    messages,
    tools,
});

const message = response.choices[0].message;
console.log("finish_reason:", response.choices[0].finish_reason);

// ステップ3〜4: tool_callsが返ってきたらアプリ側で処理して結果を返す
if (message.tool_calls) {
    for (const toolCall of message.tool_calls) {
        const funcName = toolCall.function.name;
        const funcArgs = JSON.parse(toolCall.function.arguments);
        console.log(`tool_call: ${funcName}(${JSON.stringify(funcArgs)})`);

        let toolResult;
        if (funcName === "get_horoscope") {
            // ダミーの実行結果(実務では外部APIやDBを呼び出す)
            toolResult = { horoscope: `今日の${funcArgs.sign}の運勢: 全体運は好調です!積極的に行動すると吉。` };
        } else {
            throw new Error(`Unknown function: ${funcName}`);
        }

        messages.push(message);
        messages.push({
            role: "tool",
            tool_call_id: toolCall.id,
            content: JSON.stringify(toolResult),
        });
    }

    // ステップ5: 最終応答を取得する
    const response2 = await client.chat.completions.create({
        model: "gpt-5-mini",
        messages,
    });

    console.log(response2.choices[0].message.content);
} else {
    console.log(message.content);
}
# 実行コマンド
node --env-file=../.env demo-function-call.mjs

コードの説明(各行の役割)

STEP

messages配列:モデルとのやり取りの時系列履歴を保持します。最初にsystem指示、次にuser質問、その後モデルのassistantメッセージ(function_callを含む)、アプリのtoolメッセージ(実行結果)と順に積み上げます。

STEP

tools変数(関数定義):モデルへ渡すツール定義の配列です。サンプルではtoolsにget_horoscopeの定義を入れています。

STEP

client.chat.completions.create(…):モデルへmessagesとtoolsを送信して応答を取得する呼び出しです。response.choices[0].messageに応答が入ります。

STEP

message.tool_callsの確認:モデルがfunction_callを作った場合、assistantメッセージにtool_callsフィールド(配列)が含まれます。ここに複数のtool_callがある可能性があるため、ループで処理します。

STEP

tool_call.function.argumentsの扱い:argumentsはJSON文字列で返ってくることがあるため、Pythonではjson.loads、JSではJSON.parseでパースして実際のオブジェクトにします。

STEP

実行(アプリ側の処理):func_nameで分岐し、外部API呼び出しやDBアクセスを行ってツール結果を作成します。

STEP

messagesへの追加:まずモデルから返ってきたassistantメッセージ(tool_callを含むメッセージ)を履歴に追加すること。次に、アプリが実行した結果をrole=”tool”として追加すること。

STEP

tool_call_idの復元:アプリが返すtoolメッセージには元のtool_callのIDを含める必要があります。

STEP

再リクエストで最終応答を取得:実行結果を含めたmessagesを再度モデルへ送ると、モデルはツールの結果を参照して最終的な自然言語応答を生成します。

STEP

finish_reasonの確認response.choices[0].finish_reasonは処理が完了した理由(例:”stop”や”length”など)を示します。

知りたイヌ

messagesの積み上げがカギなんだね。

チェックリスト

  • 関数名とdescriptionは明確に:動詞+目的語(例:get_weather)と、いつ呼ぶかを明記することで誤呼び出しを減らせます。
  • パラメータは最小限に:セッション等で取得可能な情報は関数パラメータに入れず、アプリ側で埋めることでスキーマをシンプルに保ちます。
  • 複数呼びが予想される処理はまとめる:頻繁に呼ばれる細かい関数は一括処理可能な形にして呼び出し回数を削減します。
  • スキーマ検証を徹底する:requiredやadditionalPropertiesで期待形を定義し、アプリ側で入力検証と正規化を行ってください。
  • 少数の関数から始める:初期は関数を絞り、運用で必要に応じて段階的に追加します。定義が多すぎるとモデルの判断が鈍ることがあります。
  • 権限・認可・ログの設計:実行するAPIやDB操作には認可チェックを必須にし、実行ログを残しておくこと(監査のため)を推奨します。

まとめ

tool calling(旧Function calling)は、モデルに「何を・どの引数で実行させるか」を判断させ、実行の責任をアプリ側が持つことで安全に外部連携を実現する仕組みです。

正しく使いこなすための鍵は2つです。

  • 明確なJSONスキーマの設計
  • モデルからの呼び出し指示をアプリ側で確実に実行して結果を返すフローの構築
知りたイヌ

まずは1つの関数で試すのが良さそうだね。

くれとむ

そう、スキーマを整えてから徐々に増やしていこう。

参考・リソース

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


ABOUT US
くれとむ
IT企業で働いているシステムエンジニアです。 AWSなどIT技術のトレンドを発信します。 また、日常の課題を解決するライフハック記事や実体験をもとにしたレビューも発信します。 エンジニアならではの視点で、技術の楽しさと日常の快適さを繋げます! AWS認定(CLF, SAA, DVA, SOA, SAP, DOP, ANS, SCS, MLS)、基本情報技術者、応用情報技術者、情報処理安全確保支援士、TOEIC L&R 870点 ※このサイトはアフィリエイト広告(Amazonアソシエイト含む)を掲載しています。