最初に結論です。モデルの最終レスポンスを決まったJSON Schemaに合わせたいならStructured Outputs(Responses APIのtext.format)、アプリケーションへ検索・取得・更新を依頼したいならFunction Callingを選びます。両方が必要なのは、ツールの結果を受け取った後にも型付きの最終UIオブジェクトを返すときだけです。
Function Callingを「AIが関数を実行する機能」として扱うと、認可、監査、重複実行を見落とします。モデルは実行要求を作るだけです。実行するのは自分のアプリであり、結果をfunction_call_outputとして返す責任もアプリにあります。
| いま達成したいこと | 選ぶもの | 主な合格条件 | そこで止める条件 |
|---|---|---|---|
| 受け取った問い合わせを担当キュー用のJSONへ分類する | Structured Outputs | Schemaと業務ルールを満たす | 外部の最新状態が不要 |
| 注文・社内DB・外部APIを参照する、または変更する | Function Calling | アプリが認可して実行結果を確認する | 最終テキストだけでよい |
| 注文を照会し、画面用の固定カードも返す | ハイブリッド | tool loopと最終Schemaの両方を検証する | 検証済みのtool結果をそのまま表示できる |
| 自由な説明や発想支援 | 通常のテキスト | プロダクト側の品質・安全基準 | 機械可読な契約がない |
同じSchemaを使えても、所有する層は違います
OpenAIのStructured Outputsガイドで説明されるtext.formatは、最終応答の形の契約です。画面カード、抽出結果、分類結果のように、すでに与えられた入力をアプリが読めるオブジェクトにする場面に向きます。
Function Callingガイドのfunction toolは、モデルがアプリへ出す作業依頼です。function_callを受けたら、サーバーが引数を検査し、利用者の権限を確認して関数を実行し、同じcall_idを持つfunction_call_outputを次のResponses API呼び出しへ渡します。
function toolにstrict: trueを付けると、引数もStructured Outputsを使ってSchemaに従わせられます。しかし、これは「最終応答を構造化する」ことと同一ではありません。strictが示すのは引数の封筒の形であり、選ばれたtoolが正しいこと、値が事実であること、利用者に実行権限があること、外部処理が一度だけ成功したことまでは示しません。
同じ注文サポートを3通りに分ける
たとえば「注文8421はいつ届きますか」という問い合わせを考えます。
- 注文情報がすでに信頼できるコンテキストにあり、画面用に
status、delivery_window、needs_humanを返すだけならtext.formatです。形式のためだけにformat_answerという偽のtoolを作りません。 - 最新の配送状態を取りに行くならFunction Callingです。モデルは
get_order_statusを要求し、アプリが注文の所有者を確認してから照会します。 - 照会結果とポリシーを合わせ、固定形式の顧客向けカードにしたいときだけ、tool結果を返した後の最終Responses呼び出しに
text.formatを追加します。
この区分は、余分なモデル往復を減らすだけではありません。「JSONを返したので配送を照会したはず」という誤った成功判定を防ぎます。
最終オブジェクトだけならtext.formatを使う
以下は、すでに取得済みの問い合わせを分類する最小のPython例です。モデル名とSDKの細かなhelperは変わり得るため、デプロイ時には現行の公式リファレンスとprojectで使えるモデルを確認してください。
pythonimport json import os from openai import OpenAI client = OpenAI() schema = { "type": "object", "properties": { "queue": {"type": "string", "enum": ["delivery", "refund", "account"]}, "needs_human": {"type": "boolean"}, "reason": {"type": "string"}, }, "required": ["queue", "needs_human", "reason"], "additionalProperties": False, } response = client.responses.create( model=os.environ["OPENAI_MODEL"], input="問い合わせ: 決済済みですが配送追跡が更新されません。", text={"format": { "type": "json_schema", "name": "support_ticket", "strict": True, "schema": schema, }}, ) if response.status != "completed": raise RuntimeError(f"レスポンスが完了していません: {response.status}") refusal = next( ( part.refusal for item in response.output if item.type == "message" for part in item.content if part.type == "refusal" ), None, ) if refusal: raise RuntimeError(f"モデルが拒否しました: {refusal}") if not response.output_text: raise RuntimeError("parse対象の最終テキストがありません") ticket = json.loads(response.output_text) if ticket["queue"] not in {"delivery", "refund", "account"} or not ticket["reason"].strip(): raise ValueError("Schemaは通ったが、業務上の意味を検証できません") print(ticket)
requiredはキーを必ず出す指定です。「日付はまだ不明」のような業務上の任意値までキーの省略で表す必要はありません。strict Schemaでは各objectにadditionalProperties: falseを置き、すべてのpropertyをrequiredに入れたうえで、値をnullableにするか明示的な状態enumを使います。
また、Schemaに合う出力が常に返るわけではありません。Structured Outputsのリクエストは通常のparse対象とは別にrefusalを返し得ます。不適切な入力や拒否をJSONDecodeErrorとして再試行し続けず、拒否・入力不足・切り詰めを別の画面または人手確認へ分岐させてください。parse成功は意味の正しさや事実性の保証ではありません。
外部作業があるなら、tool loopをアプリ側で完結させる
次のskeletonでは、モデルが要求した注文照会をアプリが実行し、その結果をcall_idに対応付けて返します。実サービスのget_order_statusには、ログイン中の利用者とorder_idの対応確認、timeout、監査ログを必ず加えてください。
pythonimport json import os from openai import OpenAI client = OpenAI() tools = [{ "type": "function", "name": "get_order_status", "description": "認証済み利用者の注文配送状態を取得する。", "strict": True, "parameters": { "type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"], "additionalProperties": False, }, }] def get_order_status(order_id: str) -> dict: # 実装では、現在の利用者の所有権を検証してからDB/APIを読む。 return {"ok": True, "order_id": order_id, "state": "in_transit"} input_items = [{"role": "user", "content": "注文8421の現在の状態を教えて"}] first = client.responses.create( model=os.environ["OPENAI_MODEL"], input=input_items, tools=tools, tool_choice="required", ) input_items += first.output outputs = [] for item in first.output: if item.type != "function_call": continue if item.name != "get_order_status": raise RuntimeError(f"許可していないtoolです: {item.name}") args = json.loads(item.arguments) order_id = args["order_id"] if not isinstance(order_id, str) or not order_id.isdigit(): raise ValueError("order_idが業務形式に合いません") try: result = get_order_status(order_id) except TimeoutError: result = {"ok": False, "error": "lookup_timeout"} outputs.append({ "type": "function_call_output", "call_id": item.call_id, "output": json.dumps(result), }) if not outputs: raise RuntimeError("必須の注文照会toolが要求されませんでした") final = client.responses.create( model=os.environ["OPENAI_MODEL"], input=input_items + outputs, tools=tools, tool_choice="none", ) if final.status != "completed": raise RuntimeError(f"最終レスポンスが完了していません: {final.status}") print(final.output_text)
このコードでfunction_call_outputを返せても、照会や更新が成功した証拠にはなりません。HTTP 2xx、toolの構造化された成否、データ更新時刻、監査ログを分けて確認します。返金、メール送信、削除のような書き込みtoolでは、アプリが発行するidempotency key、明示確認、権限検査、実行結果の保存が必要です。同じ呼び出しが再送されても副作用が重複しないようにします。
ハイブリッドにするのは、tool結果の後にも型が必要なときだけ
toolが返した検証済みデータをアプリがそのまま安全に表示できるなら、二度目のモデル呼び出しは不要です。逆に、複数の照会結果からsummary、next_action、escalateを持つ固定UIカードを作るなら、tool loopの完了後に最終text.formatを使う価値があります。
次のJavaScript skeletonは、同じ注文照会をアプリで実行し、その結果を固定UIオブジェクトへ収束させるところまでを一つの流れで示します。ローカルの固定データは責任境界を再現するためのもので、実サービスでは認証済み利用者にスコープしたDB/API clientへ置き換えてください。
javascriptimport OpenAI from "openai"; const openai = new OpenAI(); const model = process.env.OPENAI_MODEL; if (!model) throw new Error("OPENAI_MODELを設定してください"); const tools = [{ type: "function", name: "get_order_status", description: "認証済み利用者の注文状態を取得する", strict: true, parameters: { type: "object", properties: { order_id: { type: "string" } }, required: ["order_id"], additionalProperties: false, }, }]; const verifiedOrders = new Map([ ["8421", { order_id: "8421", state: "in_transit", checked_at: "2026-07-28T09:00:00Z" }], ]); const input = [{ role: "user", content: "注文8421の現在の状態を教えて" }]; const first = await openai.responses.create({ model, input, tools, tool_choice: "required", }); input.push(...first.output); const toolOutputs = []; for (const item of first.output) { if (item.type !== "function_call") continue; if (item.name !== "get_order_status") { throw new Error(`許可していないtoolです: ${item.name}`); } const { order_id } = JSON.parse(item.arguments); if (!/^\d+$/.test(order_id)) throw new Error("order_idの形式が不正です"); const result = verifiedOrders.get(order_id) ?? { order_id, state: "not_found", checked_at: null, }; toolOutputs.push({ type: "function_call_output", call_id: item.call_id, output: JSON.stringify(result), }); } if (toolOutputs.length === 0) { throw new Error("必須の注文照会toolが要求されませんでした"); } const final = await openai.responses.create({ model, input: [...input, ...toolOutputs], tools, tool_choice: "none", text: { format: { type: "json_schema", name: "order_card", strict: true, schema: { type: "object", properties: { order_id: { type: "string" }, status: { type: "string", enum: ["in_transit", "delivered", "not_found"], }, checked_at: { type: ["string", "null"] }, customer_message: { type: "string" }, }, required: ["order_id", "status", "checked_at", "customer_message"], additionalProperties: false, }, }, }, }); if (final.status !== "completed") { throw new Error(`最終レスポンスが完了していません: ${final.status}`); } console.log(JSON.parse(final.output_text));
ただし、権限判定、返金の可否、実行そのものを最終JSONの文字列に委ねてはいけません。ハイブリッドは「toolの実行責任」と「最終出力形式」の二契約を同時にテストする設計です。外部作業がなければ使わない、固定の最終型が不要なら使わない、が停止条件です。
JSON modeと成功判定の境界
JSON modeは有効なJSONを得るための機能ですが、特定のSchemaへの適合までは保証しません。Structured Outputsを利用できる現行経路では、最終契約の代わりとしてJSON modeだけに頼らないでください。移行や互換性のためJSON modeを使う場合も、アプリ側のSchema validationと不完全出力の扱いが必要です。
リリース前には、次の5層を別々にテストします。
- request completion:認証、timeout、rate limit、request IDを記録し、呼び出しが完了したか確認する。
- shape:refusalを先に分岐し、JSONとSchemaを検証する。
- semantic validation:enum、日付、金額、ユーザー入力、tool引数が業務規則と一致するか確認する。
- authorization / idempotency:実行主体の権限と、重複要求でも一度しか書き込まれないことを確認する。
- actual effect:最新の照会結果、または外部システムへの実際の反映と監査ログを確認する。
happy pathだけでなく、refusal、無効な注文番号、tool timeout、権限拒否、tool error、同じ書き込み要求の再送を含めてください。「200が返った」「JSONをparseできた」は、このうち最初の二層の一部にすぎません。
Responses APIをまだ接続していない場合は、まずOpenAI APIキーを発行して最初のResponses API応答を確認する手順でproject、環境変数、Billing、Usageを確認してください。Chat Completionsのfunctionを移行する場合は、Responses API向けfunction calling移行ガイドでcall_idとアプリ側の戻り値ループも一緒に見直します。



