Azure OpenAI の Function Calling を有効にした途端に「トークン使用量が倍増した」「請求額が読めなくなった」――そんな声が増えています。本記事では、なぜ関数スキーマが課金対象になるのか、どう最小限のコストで運用するかを、Python SDK と GPT‑4 を前提に整理します。
Azure OpenAI の Function Calling でよくある「トークン爆増」現象
想定シナリオの整理(Python SDK + GPT‑4 8k)
ここで扱うのは、次のようなシンプルな構成です。
- Azure OpenAI(GPT‑4 / GPT‑4 系モデル、8k コンテキスト)
- Python SDK(
openai/OpenAIクライアント) - ユーザーの質問 + function schema(関数定義の JSON) を添付
- 実行時、モデルは関数を呼び出さないケースも多い
にもかかわらず、
- 関数定義なし → 入力トークン 100 前後
- 関数定義あり → 入力トークン 400〜1000 以上
といった「体感的におかしいくらい増えている」ように見える、という相談が頻発しています。
利用者が抱く 3 つの核心的な疑問
典型的な疑問は、次の 3 点に集約できます。
| 疑問 | 結論(先に答えだけ知りたい人向け) |
|---|---|
| 関数スキーマは常にトークンとして計上されるのか? | はい。リクエストペイロードの一部なので、関数が実際に呼ばれなくても入力トークンとして課金されます。 |
| オーバーヘッドを減らす方法はあるか? | スキーマの縮小・条件付き注入・モデル選択・コンテキスト短縮など、設計と実装でかなり削減可能です。 |
| 大規模運用時のコスト最適化のベストプラクティスは? | Azure Monitor / Usage ログによる可視化、AB テスト、段階的ロールアウト、会話履歴の要約・アーカイブ、さらには Prompt Caching の活用が鍵になります。 |
以下では、この 3 点を軸に「なぜそうなるのか」「どうすれば抑えられるか」を、実装レベルの工夫も含めて解説していきます。
なぜ Function Schema がトークンとして確実に課金されるのか
リクエストペイロード = すべてトークン化される
Azure OpenAI では、Chat Completions / Responses API に送った内容は、最終的にすべてトークナイザで分割され、入力トークンとしてカウントされます。ここには当然ながら、
messages(system / user / assistant の各メッセージ)toolsまたはfunctionsパラメータに渡した JSON スキーマ- その他オプション(例:system プロンプトに埋めた長文ガイド)
も含まれます。
Microsoft Q&A でも、Function Calling 利用時に「関数が実際に呼び出されなくても、スキーマ自体がトークンとして計上される」ことが明言されています。
「関数を呼ばないのに課金される」ように感じる理由
多くの人が混乱するポイントは、次の 2 つです。
- 「関数呼び出し」という概念があるので、呼び出されない場合はタダのように錯覚しやすい。
- 関数スキーマの JSON が思った以上に長く、自然言語プロンプトよりもトークン数で支配的になってしまう。
しかし料金体系はあくまで「入力トークンと出力トークンの合算で従量課金」であり、どの部分が「本文」でどの部分が「関数定義」かは関係なく、すべて同じ重みでカウントされます。
JSON スキーマがトークンになるまでのイメージ
内部の挙動をシンプルにイメージすると、次のような流れです。
- Python SDK から Azure OpenAI に、
messages+toolsを含む JSON を送信する。 - サーバー側で、これらを 1 つのシーケンスにシリアライズし、トークナイザに渡す。
- BPE などのアルゴリズムで分割され、
{や"name"、"description"、説明文の日本語・英語の単語まですべてトークン化される。 - そのトークン数 × モデルの単価が「入力側コスト」として課金対象になる。
つまり、Function Schema は「ただの長いプロンプト」として扱われている、という理解が最もシンプルで誤解がありません。
簡易シミュレーション:同じプロンプトでの比較
あくまでイメージですが、同じユーザー質問に対して、Function Schema の有無とサイズを変えると、入力トークンは次のように変化します。
| ケース | 構成 | 入力トークンの目安 | 料金インパクト |
|---|---|---|---|
| A | ユーザーの質問のみ(短い日本語の 1 文) | 約 80〜120 トークン | 基準(1x) |
| B | A + 小さめの Function Schema(1 関数、プロパティ数少なめ) | 約 250〜400 トークン | 約 2.5〜4 倍 |
| C | A + 大きな Function Schema(複数関数 + 長い description) | 約 800〜1200 トークン | 約 8〜12 倍 |
トークン単価はモデルごとに異なりますが、Azure OpenAI では GPT‑4o 系なども含め、入力トークン数に比例して課金されるため、スキーマ設計次第でコストが大きく変動します。
オーバーヘッド削減の具体策(設計・実装レベル)
ここからは、実際にトークン消費を減らすためのテクニックを、「すぐできる順」に紹介します。
1. Function Schema を徹底的にミニマム化する
最も効果が大きく、今すぐ着手できるのが スキーマのダイエットです。
削れるポイント
- プロパティ名・型名を短くする
例:meetingStartDateTime→startなど。クライアント側のマッピングで読みやすい名前に変換すれば、API とコードの可読性は保てます。 - 冗長な description を削る / 短くする
「〜してください」の繰り返しや、自然言語で書いた長文仕様書のような説明は、ほぼそのままトークンになります。 - 使っていないフィールドを消す
将来用に付けていたフィールド・Enum 値などは、一旦削除しておき、必要になったタイミングで追加する方が安全です。
Before / After の例
同じ機能を表すスキーマでも、設計次第でトークン数が大きく変わります。
| バージョン | 特徴 | トークン量の傾向 |
|---|---|---|
| Before | プロパティ名が長い(reservation_start_datetime など) description に日本語・英語の長文を併記 使っていないオプション項目が多数 | 不要な記述がそのままトークン化され、関数 1 つで数百〜千トークンを超えがち |
| After | プロパティ名を短く(start, end, room など) description は 1 行程度に圧縮 不要なフィールド・Enum を削除 | 同じ関数でも 1/2〜1/3 程度のトークンで済むことが多い |
Function Calling では、JSON スキーマ自体は人間が読むためではなく、モデルが構造を理解するためのものです。説明が長くても品質が劇的に上がるわけではないので、「モデルがギリギリ理解できる最小限」を意識しましょう。
2. 条件付きで Function Schema を注入する
次に効くのが、「関数呼び出しの可能性があるときだけ」スキーマを送るというパターンです。
シンプルな判定ロジックの例
完全に 2 段階の LLM 呼び出しを挟まず、アプリ側のルールでざっくり判定する方法です。
def should_use_tools(user_input: str) -> bool:
keywords = ["予約", "登録して", "DBに保存", "APIから取得"]
return any(k in user_input for k in keywords)
tools = HOTEL_BOOKING_TOOLS if should_use_tools(user_input) else None
request_args = {
"model": deployment_name,
"messages": messages,
}
if tools:
request_args["tools"] = tools
request_args["tool_choice"] = "auto"
response = client.chat.completions.create(**request_args)
このようにすれば、雑談や Q&A には関数スキーマを一切送らないため、トークン消費をかなり抑えられます。
2 段階構成(ルーター + Function Calling)
より厳密にやるなら、次のような 2 段構成も有効です。
- ルーター用の軽量モデル(GPT‑4o mini 等)で、「この質問は外部 API 呼び出しが必要か?」を分類する。
- 必要な場合のみ、Function Calling を有効にした本番モデルに、関数スキーマ付きで投げる。
| ステージ | モデル例 | 役割 | トークンコストの傾向 |
|---|---|---|---|
| ルーター | GPT‑4o mini / GPT‑4.1‑mini | 質問をカテゴリ分類(API 呼び出し要/不要) | 1 回あたりのトークン・単価ともに小さい |
| 本番 Function Calling | GPT‑4o / GPT‑4.1 など | 実際の回答生成 + Function Calling | 単価は高めだが、Function を本当に必要なときだけ使える |
この構成にすると、全トラフィックのうちごく一部だけが高価な Function Calling 付き GPT‑4 系を使う形になり、トータルでは大きなコスト削減が期待できます。
3. モデル選択とコンテキストの最適化
Function Calling の有無だけでなく、どのモデルを使うかもコストを左右します。
- GPT‑4 8k から GPT‑4o / GPT‑4.1 系への移行
新しい GPT‑4o / GPT‑4.1 系は、従来の GPT‑4 Turbo 系よりもトークン単価が低く、パフォーマンスも向上しているため、同じスキーマでも単純に安くなる場合があります。 - コンテキスト(履歴)の断捨離
過去の会話をすべて送り続けると、関数スキーマに加えて履歴トークンも膨張します。一定以上古い履歴は「要約して 1 メッセージに圧縮」するか、「思い切って切り捨てる」のが効果的です。 - 用途に応じてモデルを分ける
– FAQ などの単純な応答: 安価なモデル(4o mini 等)
– 予約・決済など外部システム連携: 高性能モデル + Function Calling
4. Prompt Caching を意識したスキーマ配置
Azure OpenAI には、Prompt Caching という仕組みがあります。これは、同じプロンプトの先頭部分をキャッシュして、後続のリクエストでは割引価格(もしくは無料)で処理する機能です。
ポイントは次のとおりです。
- GPT‑4o 以降の一部モデルで利用可能
- プロンプトの先頭 1024 トークン以上が 完全一致 していると、その部分がキャッシュ対象になる
- キャッシュ対象トークンは、通常の入力トークンより低い単価で課金される(または Provisioned 環境では 100% 割引もあり)
これを Function Schema と組み合わせると、
- system プロンプト + Function Schema を「先頭に固定」し
- ユーザーの発話をその後ろに追加する
という構造にすることで、スキーマ部分がキャッシュされやすくなります。同一ユーザーとの連続会話や、バッチ処理など、短時間に同じスキーマを繰り返し送るケースでは、かなりのコスト削減が見込めます。
注意点として、キャッシュは
- プロンプトの先頭が 1 文字でも違うとヒットしない
- 一定時間(数分〜1時間)で自動的に失効する
ため、system メッセージや Function Schema を頻繁に書き換えない設計が重要です。
5. スキーマの「アプリ内キャッシュ」と「API への転送最適化」
よく混同されますが、
- アプリ側でスキーマをテンプレートとして共有する(Python の定数・JSON ファイルなど)
- Azure OpenAI がトークンレベルでキャッシュする(Prompt Caching)
は別物です。
とはいえ、アプリ側でテンプレート化しておくと、次のようなメリットがあります。
- 「API 呼び出しごとに JSON を組み立てているうちに、スキーマが微妙に異なってしまう」事故を防げる
- Prompt Caching を狙って完全一致したスキーマを送りやすくなる
実装としては、
tools_booking,tools_user_profileなど、機能ごとにスキーマをモジュール化- ユーザーの入力に応じて、必要なテンプレートだけ選択して
toolsに渡す
といった形がおすすめです。
Python SDK での実装パターン
ここからは、Python SDK で Azure OpenAI の Function Calling を使うときの具体例を見ていきます。
基本的な Function Calling 実装(Azure OpenAI)
Azure OpenAI では、最新の Python SDK(openai パッケージ)を使う場合でも、次のように OpenAI クライアントに base_url と API キー(またはトークンプロバイダー)を設定して利用します。
from openai import OpenAI
import os
client = OpenAI(
base_url="https://<YOUR-RESOURCE-NAME>.openai.azure.com/openai/v1/",
api_key=os.environ["AZURE_OPENAI_API_KEY"],
)
deployment_name = "gpt4-function-chat"
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "指定された都市の現在の天気を取得する",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "都市名(例: Tokyo, Osaka)",
}
},
"required": ["city"],
},
},
}
]
messages = [
{"role": "system", "content": "あなたは天気案内のアシスタントです。"},
{"role": "user", "content": "東京の天気を教えてください。"},
]
response = client.chat.completions.create(
model=deployment_name,
messages=messages,
tools=tools,
tool_choice="auto",
)
print(response.usage)
tools に渡している tools 配列が、そのままトークンとしてカウントされる部分です。スキーマを太らせるほど、response.usage.prompt_tokens が増加します。
条件付きで Function Schema を付けるサンプル
先ほど解説した「条件付き注入」を、簡略化したコードで示します。
def build_tools(user_input: str):
# ここは業務ロジックに合わせて調整
if any(k in user_input for k in ["予約", "会議室", "空き状況"]):
return BOOKING_TOOLS # 事前に定義したスキーマ
return None
def call_chat_with_optional_tools(user_input: str):
messages = [
{"role": "system", "content": "あなたは社内ヘルプデスクのアシスタントです。"},
{"role": "user", "content": user_input},
]
tools = build_tools(user_input)
kwargs = {
"model": deployment_name,
"messages": messages,
}
if tools:
kwargs["tools"] = tools
kwargs["tool_choice"] = "auto"
response = client.chat.completions.create(**kwargs)
print("prompt_tokens:", response.usage.prompt_tokens)
print("completion_tokens:", response.usage.completion_tokens)
return response
このようにラッパー関数を 1 つ用意するだけで、「必要なときだけ関数スキーマを送る」というポリシーを全リクエストで徹底できます。
トークン数の計測とログ出力
Azure OpenAI のレスポンスには、一般的に次のようなトークン情報が含まれます。
response.usage.prompt_tokens— 入力トークン数response.usage.completion_tokens— 出力トークン数response.usage.total_tokens— 合計トークン数
Prompt Caching 対応モデルでは、さらに prompt_tokens_details.cached_tokens などの詳細情報が返ってくるケースもあり、キャッシュが効いているかどうかも確認できます。
本番運用では、
- トークン数とビジネスイベント(ユーザー、機能名、入力カテゴリなど)
- レスポンス時間
- Function Calling の有無(どの関数を何回呼んだか)
をログに出し、ログ分析基盤やダッシュボードで可視化しておくと、「どの機能がトークンを食い散らかしているのか」が一目で分かります。
大規模運用時のコスト最適化ベストプラクティス
1. Azure Monitor / Usage API でモニタリングする
Azure OpenAI は Azure Monitor と統合されており、Log Analytics ワークスペースにトークン使用量やエラーなどのログを送ることができます。
おすすめの進め方は次のとおりです。
- 診断設定を有効化して、呼び出しログ・メトリックを Log Analytics に送る。
- Kusto Query Language(KQL)でクエリを作成し、
「モデル別」「機能別」「Function Calling 有無別」のトークン使用量を集計する。 - ダッシュボードや Workbook で可視化し、月次の見込みコストを推計する。
- 予算を超えそうな傾向が見えたら、スキーマやモデルを見直す。
| 監視対象 | 見るべき指標 | 気づきやすい問題 |
|---|---|---|
| モデル単位 | トークン数 / 日、リクエスト数 / 日 | 特定モデルだけ極端に高い、スパイクがある |
| Function Calling 有無 | Function 有り / 無し のトークン比率 | Function 利用部分がコストのほとんどを占めている |
| ユーザー / テナント単位 | ユーザー別のトークン消費 | ヘビーユーザーや BOT が想定以上に使っている |
2. AB テストで「スキーマの重さ」と品質を検証する
Function Schema は設計者の好みでいくらでも重くできてしまうため、「本当にその説明やフィールドが品質に寄与しているのか?」を検証することが重要です。
典型的な AB テストの例:
- パターン A: 詳細な description、プロパティ名も長め
- パターン B: description を 1 行に圧縮、プロパティ名も短縮
これをユーザーを 50:50 に振り分けて一定期間動かし、
- 回答品質(ユーザー満足度 / CS の再問い合わせ率など)
- Function Calling 関連のトークン数・コスト
を比較します。もし品質がほぼ同じであれば、より軽いスキーマに統一することで、月間コストを一気に削減できます。
3. 段階的ロールアウトとガードレール
Function Calling のスキーマを大きく変更するときは、いきなり全トラフィックに適用せず、
- 内部ユーザー / テナント限定
- トラフィックの 5% → 20% → 100% と段階的に適用
という形でロールアウトするのが安全です。
その際、
- 1 日あたりのトークン上限
- ユーザー / テナントごとのクォータ
などの ガードレールを併用しておけば、予期せぬバグや無限ループで Function をたたき続ける…といった事故も防げます。
4. 長大な会話履歴の「自動アーカイブ / 要約」
チャット型のアプリでは、会話が続くほど過去履歴が増え、Function Schema の上にさらに履歴トークンが積み上がっていきます。
対策としては、
- 一定ターン(例: 10〜15 往復)ごとに、LLM 自身で履歴を要約させる
- 過去の細かいやり取りは DB に保存し、API には送らない
などが効果的です。
実装イメージ:
- 履歴が閾値を超えたら、内部的に「会話要約」プロンプトを投げる。
- 結果の要約 1 メッセージだけを残し、元の履歴メッセージを削除する。
- 以降のリクエストでは、要約 + 最新の数ターンだけを送信する。
こうすることで、Function Schema のトークン + 履歴トークンという二重の膨張を抑えられます。
よくあるアンチパターンと注意点
アンチパターン 1: 「とりあえず全部の関数を投げる」
開発初期によくあるのが、
- 全機能の Function Schema を 1 つの
tools配列に詰め込む - 毎回それをそのまま API に渡す
という設計です。
これをやると、
- Function を全く使わない会話でも、毎回巨大なスキーマを送る
- スキーマの追加 / 修正が入るたびに、全リクエストのトークンが増える
という状態になり、コストが雪だるま式に膨れ上がります。
必ず、
- 機能ごとにスキーマを分割する
- ユーザーの入力に応じて必要なものだけを送る
という基本方針を徹底しましょう。
アンチパターン 2: 巨大な説明付きスキーマに頼りすぎる
Function Schema の description を仕様書代わりにして、
- 業務ルールやエラーハンドリング方針をびっしり書く
- 日本語と英語の両方で同じ説明を重複させる
といった運用も、トークン消費の観点では望ましくありません。
業務ルールやガイドラインの多くは、
- system プロンプト側に集約する
- あるいはアプリケーションコード側で厳密にチェックする
方が安全かつ安価です。Function Schema の description は、あくまで モデルに構造を理解させるための最低限に抑えましょう。
アンチパターン 3: ログと請求の「ズレ」を無視する
Azure Portal やログから見えるトークン数と、実際の請求額が微妙に合わないことがあります。理由としては、
- Prompt Caching による割引が別カラムで計上されている
- バッチ処理や異なるリージョン・デプロイメントの料金が混在している
などが挙げられます。
精密なコスト分析をしたい場合は、
- モデル別・デプロイメント別・API バージョン別にログを分けて集計する
- Prompt Caching 対応モデルでは、
cached_tokensなどのフィールドも含めて計算する
といった工夫をすると、「Function Schema 部分に実際いくらかかっているか」をより正確に把握できます。
将来のアップデートに備えた設計の考え方
「スキーマ ID 参照」前提にしない設計
現時点(2025 年 11 月)で、Azure OpenAI には「Function Schema を一度登録して ID で参照する」ような API は公開されていません。少なくとも公式ドキュメントの範囲では、毎回スキーマを送る前提になっています。
ただし、Prompt Caching によって、同じスキーマ部分の処理コストを下げる仕組みはすでに提供されています。将来的にスキーマをより効率的に再利用する機能が追加される可能性はありますが、現時点では、
- スキーマを軽量に保つ
- 必要なときだけ送る
- Prompt Caching を活かしやすいよう、先頭に固定する
という設計が、最も現実的なコスト最適化手段です。
「今できること」をやり切っておくメリット
Function Calling は、Azure OpenAI と外部システムをつなぐ強力な仕組みですが、その分 スキーマ設計 = コスト設計 でもあります。今のうちに、
- スキーマのミニマム化
- 条件付き注入・モデル分割
- モニタリング・AB テスト
- Prompt Caching を意識したプロンプト構造
を整えておけば、ユーザー数やユースケースが増えたときにも、慌ててスキーマを削る必要がなくなるでしょう。
まとめ:Function Schema は「常に有料」だからこそ設計で戦う
本記事のポイントを改めて整理すると、次のようになります。
- Function Schema はリクエストの一部として必ずトークン化されるため、関数を呼び出さなくてもコスト増の要因になる。
- オーバーヘッド削減には、スキーマの軽量化・条件付き注入・モデル選択・履歴要約など、設計と実装の工夫が欠かせない。
- Azure Monitor / Log Analytics や Prompt Caching を活用すると、実際の使用状況とコストを定量的に把握・最適化しやすくなる。
- 「将来スキーマ ID が来るはず」と期待するのではなく、現時点の仕組みで最大限コストを抑える設計をしておくことが重要。
Azure OpenAI で Function Calling を本格運用しようとしているチームは、まずは小さな PoC から、スキーマサイズ・モデル構成・履歴の扱いを AB テストで検証し、その結果をもとに本番構成を設計していくのがおすすめです。

コメント