- Tool Callingでモデルに外部関数を呼ばせる仕組み——実行するのは常にアプリ側で、モデルではないこと
- Structured Outputsが出力をJSON Schema・文法に強制拘束する仕組みと、推論モデルとの重大な相性問題
- プロンプトキャッシュが前方一致だけで効く仕組みと、保持期間(TTL)・プライバシー分離の設計
- 推論量を制御するreasoning_effortとthinkingの違い、interleaved thinkingの発火条件
- 本番アプリで落ちないためのタイムアウト・リトライ・エラーコードの読み方
これは何か
この章で扱う機能はすべて、これまでの章で使ってきたPOST /inference/v1/chat/completionsにリクエストパラメータを追加するだけで使える。専用のコンソール画面は無く、常にAPI経由で触る。Tool Callingはtoolsとtool_choice、Structured Outputsはresponse_format、プロンプトキャッシュは全モデル・全デプロイで既定オンのため追加指定は不要(セッション識別だけ任意で渡す)、Reasoningの制御はreasoning_effortかthinkingのどちらかを渡す。いずれもOpenAI互換のパラメータ名を踏襲しているため、OpenAI APIを触ったことがあれば見覚えのある形のはずだ。
いつ使うのか
本章で扱う6機能は目的が異なるため、まず一覧で対応関係を掴んでおく。
| 機能 | 何をするか | いつ使うか |
|---|---|---|
| Tool Calling | モデルに外部関数の呼び出しを提案させる | 天気取得・DB検索など、モデルの知識だけでは答えられない情報やアクションが必要な時 |
| Structured Outputs | 出力をJSON SchemaまたはBNF文法に拘束する | 後続処理が壊れずにパースできるJSONを確実に得たい時 |
| プロンプトキャッシュ | 前置きが一致する分の再計算を省く | 長いシステムプロンプト・ツール定義・会話履歴を繰り返し送る時 |
| Reasoning制御 | 推論モデルの思考量を調整する | 複雑な問題で精度を上げたい、逆に速度・コストを優先したい時 |
| Predicted Outputs | 予想される出力を先に渡し生成を高速化する | 既存テキスト・コードの軽微な編集や書き換え |
| Responses API | 会話状態をサーバ側で保持し、MCP/SSEツールをサーバ側で実行する | 会話履歴の管理やツール実行基盤を自前で持ちたくない時 |
隣接機能どうしの関係も押さえておく。Tool Callingを使うとJSON modeが自動的に有効になるため、両方を手動で同時設定する必要はない。逆にStructured Outputs単体(response_format)と推論モデルの組み合わせには重大な制約があり、これは後述する。
Tool Calling — モデルに道具を持たせる
Tool Callingが機能する仕組みは一貫している。モデルはツールを実行しない。toolsにJSON Schema(JSONデータの構造——型・必須項目・取りうる値——をJSON自身で記述する標準仕様)で関数を定義して渡すと、モデルはユーザーの入力を見て「どの関数をどんな引数で呼ぶべきか」を構造化されたtool_callsとして返すだけだ。実際にその関数を呼び出し、結果をrole: "tool"のメッセージとして会話履歴に積み直し、モデルに再度渡して最終応答を得るところまで、すべてアプリ側の責務になる。
ツール定義の必須項目はname(英数字とアンダースコア・ダッシュのみ、最大64文字)・description(モデルが呼び出し判断に使う説明)・parameters(JSON Schemaオブジェクト)の3つ。parametersのJSON Schema機能セットは後述のStructured Outputsのresponse_formatと共通で、$defsによる再利用や再帰参照も同じように使える。
tool_choiceで呼び出しの強制度を制御できる。既定のautoはモデルに判断を任せ、noneは呼ばせず、requiredは必ず1つ以上呼ばせ、特定の関数名を指定すればその関数を強制できる。1回の応答で複数関数を呼ぶ並列ツール呼び出しに対応するかはモデル依存で、全モデル共通の機能ではない。ストリーミング時は引数がトークン単位で分割して届くため、indexごとに文字列を連結してからfinish_reason == "tool_calls"でまとめてJSONとしてパースする。
もう1つ、プロンプトキャッシュと交差するtool_choiceの形がある——{"type": "allowed_tools", "allowed_tools": {"mode": "auto"|"required", "tools": [...]}}だ。呼び出せるツールを一部に絞り込めるが、プロンプトそのものは書き換えない。ツール定義は全件そのままプロンプトに残り、制約されるのは生成時の文法とパースだけになる。動的にツールを絞ると通常はプロンプト(=キャッシュのプレフィックス)が変わってヒットが壊れるが、allowed_toolsはそれを避けながらツールを絞れる数少ない方法だ(プロンプトキャッシュの前方一致の仕組みは後述のプロンプトキャッシュで扱う)。
公式Tipとして、引数のハルシネーションを抑え呼び出しを決定的にするためtemperatureは0.0〜0.3が推奨されている。モデルがツール対応かどうかはsupportsToolsフィールドで確認できる。$refの解決に失敗すると400エラー(Error resolving schema reference)になり、原因は外部URI参照(サポート対象外。ドキュメント内フラグメントのみ可)かフラグメントパスの誤りであることが多い(専有デプロイで古いイメージを使っている場合は、再帰参照や入れ子の$defsが未対応で400になることもあり、最新イメージへの再デプロイで解決する)。
Structured Outputs — 出力の形を固定する
Structured Outputsには2方式ある。JSON Schemaで拘束するJSON modeと、拡張BNF(GBNF形式。文法規則を「この記号はこの並びに展開できる」という形で書く記法)で拘束するGrammar mode(分類タスクの選択肢限定やJSON以外の任意フォーマットに向く)だ。JSON modeはさらにresponse_formatのtypeで2通りに分かれる——スキーマ無しでとにかく妥当なJSONにするjson_objectと、指定したスキーマに従わせるjson_schema(推奨)。Grammar modeも同じresponse_formatに渡す({"type": "grammar", "grammar": "root ::= ..."})。たとえば診断名を5択に絞る分類器ならroot ::= diagnosis / diagnosis ::= "arthritis" | "dengue" | ...のような文法を渡すと、出力は必ずその選択肢のいずれかになる。
重要なのは、この拘束が出力後のバリデーションではなく生成中(デコード時)にかかる仕組みだという点だ。1トークン生成するたびに、モデルが計算した候補の中からスキーマ的にあり得ない候補が除外され、残った候補からしか選ばれない。これを最後まで繰り返すため、出力は必ずスキーマに従う。
この仕組みゆえの公式Tipがある——response_formatに渡すだけでは不十分で、スキーマはプロンプトにも書くべきだとされている。生成を制約するだけで、モデルがスキーマの意図を「理解」するわけではないからだ。逆にプロンプトで「JSONで答えよ」と指示せずJSON modeだけ使うと、トークン上限まで空白を出し続けることがある。propertiesを持つスキーマは暗黙に存在しないフィールドの捏造を防ぐ扱いになり、$defs/$refによる再利用や再帰的なスキーマ(pydantic v2の出力など)もそのまま渡せる。
推論モデルとの重大な相性問題: response_formatにjson_schemaを渡すと、推論モデルのreasoning_content出力が無効化される。推論過程と構造化JSONの両方が欲しいときはresponse_formatを使わず、スキーマをプロンプト内に書いて自分でパースする(コードフェンスの除去などが必要になる)。単に確実なJSONだけが欲しく推論過程は不要なら、素直にresponse_formatを使う方が生成時に拘束される分パース失敗のリスクが低い。
プロンプトキャッシュ — 同じ計算を二度しない
プロンプトキャッシュは全モデル・全デプロイで既定オン。共有するプレフィックス(前置き部分)が多いリクエストで、Time to First Token(最初のトークンが出るまでの時間)を最大80%短縮する。効く条件はただ1つ、完全な前方一致だけだ。1トークンでもズレれば、そのトークン以降のキャッシュはすべて無効になる。だから静的な指示・ツール定義は先頭に、ユーザー固有の可変内容は末尾に置くのが鉄則になる(ツール定義もプロンプトの一部として扱われる)。
典型的な失敗は、システムプロンプトの先頭に現在時刻を埋め込むことだ。1秒のズレでもキャッシュ全体が効かなくなる。時刻が必要なら、時間単位に丸める・時刻が要る質問だけ動的に付与する・ユーザーメッセージ側に回す、のいずれかで対処する。キャッシュは1レプリカ内でのみ有効なので、Serverlessや複数レプリカのデプロイでは同じ会話を同じレプリカへ送り続けるためのヒントが要る。userフィールドかx-session-affinityヘッダにセッション識別子を渡すとよい。逆に意図的にキャッシュを分離したいときはx-prompt-cache-isolation-keyヘッダ(またはprompt_cache_isolation_keyボディフィールド)で任意の文字列を追加キーにできる。
キャッシュされたプロンプトの保持期間は「通常は最低でも数分」、モデル・負荷・デプロイ構成によっては数時間ということもあり、古いものから追い出される。出力自体はキャッシュされず生成は毎回独立にサンプリングされるため、キャッシュの有無で回答内容が変わることはない。料金面では、Serverlessはキャッシュ済みトークンが割引価格になる(具体的な料金体系は第5章を参照)。専有デプロイではキャッシュがコンテキスト長を圧迫するだけで追加処理が要らないため「ほぼ無料」になる。プライバシーはServerlessならアカウント単位で分離済み。専有デプロイは既定で全リクエスト共有のため、応答時間からキャッシュの有無を推測されるわずかなタイミング攻撃リスクがあり、気になる場合は上記の分離キーを使う。
Reasoning — 思考量を制御する
推論(thinking)モデルは、最終回答とは別に内部の思考過程をreasoning_contentフィールドで返す(モデルによってはcontent内の<think></think>タグに混ざることもある)。思考にどれだけトークンを使わせるかは2つのパラメータのどちらかで制御する——reasoning_effort("low"/"medium"/"high"の文字列)か、Anthropic互換のthinking({"type": "enabled", "budget_tokens": 4096}のようにトークン数を直接指定)。この2つは同時に指定できず、両方渡すとバリデーションエラーになる。budget_tokensは1024以上、かつmax_tokens未満という制約もある。
複数ターンにまたがるツール呼び出しエージェントを作るときは、前ターンのreasoning_contentを次のリクエストにも含める必要がある(SDKのMessageオブジェクトをそのまま渡すのが簡単で、辞書で手動構築するならreasoning_contentを書き忘れると思考の引き継ぎが失われる)。この引き継ぎ——interleaved thinking——はリクエスト内の最後のメッセージがrole: "tool"のときに自動で発火する。非対応モデルは余分な文脈を無視するだけなので、対応可否を気にせず広く使ってよい。さらにユーザーターンをまたいで思考を保持したい場合はreasoning_history="preserved"を指定する。
信頼性とその他の機能
本番投入するアプリは、まずタイムアウトを用途別に変える。公式の推奨値は次のとおりで、エージェント用途は分単位が前提になる点に注意する。
| 用途 | 推奨タイムアウト |
|---|---|
| 対話・チャット | 30〜60秒 |
| エージェント(ツール呼び出し・複数ステップ) | 5〜30分 |
| 大規模モデル推論(長いコンテキスト) | 10〜30分 |
| Batchジョブの投入 | 60秒(結果は非同期) |
エラーコードはリトライしてよいものと、リクエストを直さない限り無駄なものに分かれる。
| コード | 意味 | リトライ |
|---|---|---|
| 429 | レート制限 | する(バックオフ付き) |
| 500 / 502 / 503 / 504 | サーバ側の一時的エラー | する(バックオフ付き) |
| 400 / 422 | リクエスト不正 | しない(内容を直す) |
| 401 | 認証エラー | しない(APIキーを確認) |
| 404 | 見つからない | しない(モデル/デプロイIDを確認) |
500だけは公式ページ間でニュアンスが割れている点に注意したい。信頼性ガイドは一時的なエラーとしてリトライ対象に含めるが、エラーコード一覧ページは「サーバ側のバグで自然回復しない」として即サポート連絡を勧めている。実務としては、少数回のリトライは試してよいが、それでも継続する500は自然には直らないためサポートへ連絡する、という両立した理解をしておく。
429の意味はServerlessと専有デプロイで異なる。Serverlessはアカウントのリクエストレート制限と適応的なTPM制限を組み合わせたレート制限、専有デプロイでは主にGPUの処理能力が飽和したという容量シグナルになる。ただし専有デプロイにレート制限が一切無いわけではなく、支払い方法と有効なクレジットがあるアカウントには全デプロイ形態共通で6,000 RPM(固定・非適応的、2026-09時点)というアカウント全体の上限が別途かかり、これはどのデプロイ形態でも免れない。ダッシュボードの利用状況は「サーバが受理したリクエスト」だけを数えるため、クライアント側だけでエラーが見えるときはタイムアウト前の切断やDNSなど接続前の問題を疑う。
軽く触れておきたい機能
他に3つ、名前だけ知っておくと役立つ機能がある。Predicted Outputs(β版)は、編集・書き換えのように出力の大部分が事前にわかっている場合にpredictionフィールドで「予想される出力」を渡し、生成速度を上げる。2026-09時点で無料だが、予想が大きく外れるとかえって遅くなることがある。Responses APIはchat/completionsとは別のエンドポイントで、previous_response_idにより会話全体を毎回送り直さずに継続でき、MCP(Model Context Protocol。外部ツールをモデルに接続する標準規格)やSSE(Server-Sent Events。サーバから継続的にデータを送る仕組み)によるツールをサーバ側で実行できる点がTool Callingとの違いだ(Tool Callingのfunctionツールはクライアント実行のまま)。ただしChat Completionsとはデータ保持ポリシーが異なり既定で会話が保存される(store=falseで無効化できるが、その場合previous_response_idによる継続はできなくなる)。Anthropic互換Messages API(POST https://api.fireworks.ai/inference/v1/messages)はモデルIDをFireworksのものに読み替えるだけでAnthropic SDKからそのまま使える。ただしAnthropic SDKに渡すbase_urlはhttps://api.fireworks.ai/inference(/v1を付けない。SDKが/v1/messagesを自動で付加する)。cache_controlブロックでキャッシュのTTL(Time to Live、保持期限)を5m(既定)か1hから選べる(ただしツールスキーマ側のcache_controlは非対応)。
ハンズオン
以下3本はいずれもServerlessの従量課金APIを数回呼ぶだけで、デプロイのような永続リソースは作らないため後片付けは不要。正確な料金は使うモデルによって異なるためpricingページで確認する。
ハンズオン 7-1 天気関数のTool Callingを一往復させる
- ターミナルでAPIキーを環境変数にセットする(
export FIREWORKS_API_KEY=...)。Fireworks Python SDK(alpha)をpip install --pre fireworks-aiでインストールする。 - 次のPythonスクリプトを実行する。モデルが提案した関数呼び出しをローカルで実行し、結果を返して最終応答を得るところまでを1本で行う。
import json
from fireworks import Fireworks
client = Fireworks()
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"}
},
"required": ["location"]
}
}
}]
messages = [{"role": "user", "content": "What's the weather in San Francisco?"}]
response = client.chat.completions.create(
model="accounts/fireworks/models/kimi-k2p6",
messages=messages,
tools=tools,
temperature=0.1,
)
if not response.choices[0].message.tool_calls:
raise RuntimeError("tool_calls が空でした(モデルがツールを呼びませんでした)")
tool_call = response.choices[0].message.tool_calls[0]
def get_weather(location):
return {"temperature": 72, "condition": "sunny"}
args = json.loads(tool_call.function.arguments)
result = get_weather(**args)
messages.append(response.choices[0].message)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result),
})
final = client.chat.completions.create(
model="accounts/fireworks/models/kimi-k2p6",
messages=messages,
tools=tools,
temperature=0.1,
)
print(final.choices[0].message.content)
最終出力に72度・sunnyに相当する内容を含む自然文が表示されれば成功。モデルがツールを呼ばなかった場合はコード内のRuntimeErrorで気づける。
ハンズオン 7-2 json_schemaで出力の形を固定する
pip install pydanticでpydanticを、pip install openaiでOpenAI SDK(第2章で導入済みならスキップ)をインストールする。- スキーマを定義し、次のスクリプトを実行する。
import os
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI(
api_key=os.environ.get("FIREWORKS_API_KEY"),
base_url="https://api.fireworks.ai/inference/v1",
)
class Result(BaseModel):
winner: str
response = client.chat.completions.create(
model="accounts/fireworks/models/kimi-k2p6",
response_format={
"type": "json_schema",
"json_schema": {"name": "Result", "schema": Result.model_json_schema()},
},
messages=[{
"role": "user",
"content": "Who won the US presidential election in 2012? Reply in JSON format.",
}],
)
print(response.choices[0].message.content)
{"winner": "Barack Obama"}のように、指定したスキーマどおりの構造でJSONが返れば成功。response_formatを外してプロンプトの指示だけに頼った場合と結果を見比べると、拘束の効果が実感できる。
ハンズオン 7-3 usageでキャッシュヒットを確認する
pip install openaiでOpenAI SDKを入れる(第2章で導入済みならスキップ)。- 同じ長い前置きに続けて質問文だけ変えたリクエストを、Chat Completions APIで2回連続で送る。
x-session-affinityヘッダに同じセッション識別子を渡し、2回とも同じレプリカへ送られるようにする。 - 2回目のレスポンスの
usage.prompt_tokens_details.cached_tokensを読む。
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.fireworks.ai/inference/v1",
api_key=os.environ.get("FIREWORKS_API_KEY"),
)
long_prefix = "You are a helpful assistant with deep expertise in Japanese history. " * 20
questions = [
"What year did the Edo period begin?",
"What year did the Meiji period begin?",
]
for i, question in enumerate(questions):
resp = client.chat.completions.create(
model="accounts/fireworks/models/kimi-k2p6",
messages=[{
"role": "user",
"content": long_prefix + f"\n\nQuestion: {question}",
}],
extra_headers={"x-session-affinity": "ch07-cache-demo"},
)
cached = resp.usage.prompt_tokens_details.cached_tokens
print(f"call {i+1}: prompt_tokens={resp.usage.prompt_tokens} cached_tokens={cached}")
1回目はcached_tokensが0(またはそれに近い値)、2回目は前置き部分のトークン数に近い値まで増えていれば、末尾の質問文を変えても前置きが同じ範囲だけキャッシュから再利用されたことが確認できる。キャッシュは数分〜数時間で失効するため、2回のリクエストの間隔を空けすぎないこと。
つまずきポイント
response_formatにjson_schemaを渡すと推論モデルのreasoning_contentが消える。「推論過程も見たいのに空になる」という詰まりは、拘束をやめてスキーマをプロンプトに書く方式に切り替えれば解決する。
thinkingとreasoning_effortを両方渡すとバリデーションエラーになる。SDKやフレームワークの既定値で片方が自動的に付与されていて、そこに手動でもう片方を足してしまうケースに注意する。
プロンプトキャッシュはServerlessでも複数レプリカに分散しうる。同じユーザーの会話を毎回別レプリカに送ってしまうとヒット率が上がらない。userフィールドかx-session-affinityヘッダで同じ識別子を渡し続ける。
500エラーは「リトライしてよい」と「自然回復しないので即サポート」の両方が公式に書かれている。数回リトライしても直らなければ、それ以上リトライを重ねず問い合わせに切り替える。また専有デプロイだからといってレート制限が無いわけではなく、アカウント全体の6,000 RPM上限は常にかかる。
理解度チェック
Q1. Tool Callingで、実際に関数(ツール)を実行するのはモデルとアプリのどちらですか。
アプリ側。モデルはtool_callsとして「どの関数をどんな引数で呼ぶべきか」を提案するだけで、実行・結果の取得・会話への差し戻しはすべてアプリのコードが行う。
Q2. 推論モデルでresponse_formatにjson_schemaを渡すと何が起きますか。
reasoning_content(推論過程)の出力が無効化される。推論と構造化JSONの両方が必要なら、response_formatを使わずスキーマをプロンプトに書いて自分でパースする。
Q3. プロンプトキャッシュがヒットする条件は何ですか。
プロンプトの先頭からの完全な前方一致のみ。1トークンでもズレるとそこから先のキャッシュはすべて無効になるため、静的な内容を先頭・可変な内容を末尾に置く必要がある。
Q4. reasoning_effortとthinkingを同時に指定するとどうなりますか。
バリデーションエラーになる。この2つは同時に指定できず、どちらか一方だけを渡す。
Q5. 専有デプロイには「デプロイメント固有のレート制限」がありませんが、それでもかかる制限は何ですか。
支払い方法と有効なクレジットがあるアカウントに一律でかかる、アカウント全体で6,000 RPM(固定・非適応的)というリクエスト数の上限。これはServerlessか専有デプロイかを問わず全デプロイ形態が対象になる。
まとめ
- Tool Callingはモデルに「提案」させるだけで、実行は常にアプリ側の責務。
tool_choiceで強制度を、低めのtemperatureで決定性を制御する。 - Structured Outputsは生成中(デコード時)にスキーマ外の候補を除外する仕組み。推論モデルでは
reasoning_contentとresponse_formatのどちらかしか使えない。 - プロンプトキャッシュは完全な前方一致でのみ効く。静的な内容を先頭に、セッション識別子でレプリカを揃えるとヒット率が上がる。
- Reasoningは
reasoning_effortかthinkingのどちらかで制御し、interleaved thinkingは最後のメッセージがrole: "tool"のときに自動発火する。 - 本番運用ではタイムアウトを用途別に変え、429/500系のエラーコードの意味とリトライ可否を切り分ける。専有デプロイでもアカウント全体の6,000 RPM上限は免れない。
次章(第8章)では、急がない大量リクエストをまとめて安く処理するBatch APIを扱う。