LLMの構造化出力を安定させる実装

LLMに関する記事のアイキャッチ画像 - LLMの構造化出力を安定させる実装 AI×自動化

構造化出力とは、LLMの応答をJSONなど決まった形式に固定して受け取る仕組みである。

関数呼び出しの引数が、ときどき壊れる。閉じ括弧が足りない、余計な前置きの日本語が混ざる、あるいはJSONとしては正しいのにキー名が微妙に違う。手元で1回ずつ叩いている間は気づかないのに、バッチ処理やエージェントに組み込んだ途端、後段のパースが落ちてパイプラインごと止まる。LLMを業務に載せようとした人の多くが、この「たまに壊れる出力」で足を取られます。

厄介なのは、この壊れ方が一様ではないこと。JSONそのものが壊れているケースと、JSONは有効なのに期待した形と違うケースでは、効く対策のレイヤーが変わります。そして対策の作法は、OpenAI・Gemini・Claudeの3社で異なる。本記事では型崩れを2種類に分解したうえで、各社のネイティブ機能とライブラリ層の役割分担を実装目線で整理していきます。

この記事の要点

  • 構造化出力の型崩れは「JSONとして壊れている構文崩れ」と「JSONは有効だが形が違うスキーマ不一致」の2種類に分かれ、対策のレイヤーも別になる
  • 各社のネイティブ構造化出力は生成時にスキーマ準拠を制御して「形状」を強制し、Instructorなどのライブラリは「値の妥当性検証と失敗時のリトライ」を足す二層構造で考える
  • OpenAI・Gemini・Claudeでスキーマの指定方法・厳格モードの粒度・制約の上限が異なるため、実装の作法を分けて押さえる

本文で示すコードとAPIの記述は、仕組みを理解するための最小例です。モデルID・パラメータ名・料金・上限値は更新が速いため、実装時は各社の公式ドキュメントで最新の仕様を確認してください。数値や仕様の記述は2026年7月時点の公式情報を基準にしています。

function callingの「型崩れ」とは何か(構文崩れとスキーマ不一致)

LLMをチャット相手として使う分には、多少表現が揺れても人間が読んで補完できます。ところが応答をプログラムが受け取る前提になると話が変わる。「話すだけのモデル」を「処理を実行するエージェント」に変える転換点が、まさにこの構造化出力です。出力の形が固定されて初めて、次の関数へ引数として渡せる。だからこそ、その形が崩れる問題は避けて通れません。

型崩れは大きく2種類に分けられます。この分類が、後で出てくる対策の守備範囲を理解する下敷きになります。JSONとして壊れているのか、JSONは成立しているのに中身の構造が違うのか。同じ「壊れた」でも原因も対処も別物です。

構文崩れ(invalid JSON)の典型パターン

1つめが構文崩れ、つまりJSONパーサに通した時点でエラーになる壊れ方です。よくあるのは以下のような形。

  • 閉じ括弧や閉じ引用符が欠落し、途中で切れている
  • {...} の前後に「はい、以下がJSONです」といった説明文が付いてくる
  • 配列やオブジェクトの末尾に余計なカンマが残る(trailing comma)
  • コードブロックの ```json フェンスごと文字列に混ざる

この手の崩れは、json.loads() のような標準パーサが即座に例外を投げます。プログラムから見れば「データですらない文字列」なので、後段は何もできません。構文崩れは検知こそ簡単ですが、発生した時点で処理が止まるため、自動化の中では致命傷になりやすい。

スキーマ不一致(valid JSONだが形が違う)の典型パターン

もうひとつがスキーマ不一致です。JSONとしては完全に有効で、パースも通る。ところが期待していた構造と食い違っている。こちらのほうが厄介で、静かに間違ったデータが下流へ流れます。

たとえば priority を整数の 1〜3 で受け取るはずが、"high" という文字列で返ってくる。必須のはずの user_id キーが丸ごと欠けている。列挙値(enum)に定義していない "unknown" が紛れ込む。数値であるべきフィールドが "42" と文字列でクオートされている。いずれもJSONパーサは通してしまうので、エラーにならないまま後段の型変換や条件分岐で初めて破綻します。

構文崩れが「入口で弾かれる」のに対し、スキーマ不一致は「奥まで入り込んでから壊れる」。構造化出力の失敗は「JSONとして壊れている構文エラー」と「JSONは有効だがスキーマ・型が合わない型崩れ」の2種類に分かれ、対策も別レイヤになります。この先で見るネイティブ機能はJSON構文と対応範囲内のスキーマ形状の両方を強制し、ライブラリの検証層は業務ルールを含む意味的な検証と失敗時の再生成を担う、という布石をここで置いておきます。

なぜ型崩れが起きるのか(自由生成とループへの波及)

型崩れの根っこは、LLMの生成方式そのものにあります。モデルは次に来るトークンを確率的に選んでいく仕組みで、本来「必ずこの形で出力せよ」という強制を内蔵していません。プロンプトで「JSONで返して」と頼むだけでは、それはあくまで願い。確率分布が少し揺れれば、余計な前置きが付いたりキーが変わったりします。自由生成に任せている限り、形の揺れは統計的に一定の割合で出続けます。

プロンプトの工夫、たとえば「JSONだけを出力し、説明を加えるな」といった指示は、崩れる頻度をある程度下げます。ただし頻度を下げるだけで、ゼロにはできない。指示を強めても、モデルが確率的に「JSONです:」と言い出す余地は残ります。ここが、後述するネイティブの構造化出力との決定的な差になります。

単発利用と自律ループで型崩れの重みが変わる理由

同じ型崩れでも、使い方によって痛みの大きさが変わります。人間が1回ずつ結果を見る単発利用なら、壊れた出力に気づいて手で直せばいい。損害は小さく収まります。

問題は、LLMを自律的に反復させる「ループ」型の使い方です。各ステップの出力を機械が受け取り、そのまま次のステップの入力に渡す。ここで1つ形が崩れると、後続がまとめて破綻します。こうした自律反復の運用では、単発のプロンプトを書くことよりも、反復と評価のプロセスそのものを設計することが中心になります。ここで問題になるのがドリフト、つまり初期の小さなズレが反復のなかで連鎖して増幅していく現象です。

構造化出力の型崩れは、このドリフトの入口になりやすい。1ステップ目のわずかな形の乱れが、2ステップ目のパース失敗を招き、リトライやフォールバックが噛み合わなければループ全体が迷走します。だから型の安定は、LLMを単なるチャットからエージェントへ引き上げるための前提条件になります。型崩れ対策は「あると便利な機能」ではなく、自律運用を成立させる土台なのです。

ネイティブの構造化出力で「形状」を強制する(生成時にスキーマ準拠を制御する仕組み)

各社が用意する構造化出力機能は、プロンプトのお願いとは根本から発想が違います。プロンプトで形式をお願いするのではなく、生成時にスキーマ準拠を制御します。OpenAI と Claude は「制約付きデコード」、つまり生成の各ステップでスキーマに適合するトークンだけを候補として許可し、それ以外を選べなくする方式を公式に説明しています。願いではなく、生成経路そのものを塞ぐアプローチです。

制約付きデコードが通常生成時の構文崩れを防ぐ原理

制約付きデコードのもとでは、モデルが「次に許されるトークン」の範囲を常にスキーマで縛られます。オブジェクトを開いたら、次に来られるのはキーか閉じ括弧だけ。文字列を開いたら、閉じ引用符が来るまで別の構造には移れない。この仕組みが働くため、正常に完了した応答では、閉じ括弧の欠落や余計な前置き文といった構文崩れはほぼ発生しなくなります。「JSONです:」と言い出すトークン自体が候補から外れるからです。

スキーマの形、つまりキー名・型・列挙値(enum)も同時に強制できます。Claudeの場合、この制約はJSON Schemaを文法(grammar)へコンパイルし、制約付きサンプリングで生成することで実現されます。公式ドキュメントによれば、特定スキーマの初回はコンパイルの追加レイテンシが生じ、コンパイル済みの文法は最終使用から24時間キャッシュされる。スキーマやtool構成を変えるとキャッシュは無効化されますが、name/descriptionのみの変更では無効化されない、という挙動も明記されています。

構造化出力を概念として捉えるなら、渡すスキーマはおおむね次のような形になります。

{
 "type": "object",
 "properties": {
 "priority": { "type": "integer", "enum": [1, 2, 3] },
 "assignee": { "type": "string" },
 "done": { "type": "boolean" }
 },
 "required": ["priority", "assignee", "done"],
 "additionalProperties": false
}

これは最小構成イメージであり、そのまま動くサンプルではありません。各社でフィールド名や渡し方が異なるため、実際の指定方法は次のセクションで比較します。ここで押さえたいのは、こうしたスキーマを制約として与えると、正常に完了した応答について、出力が対応範囲内のスキーマの形に収まるという点です。ただし拒否応答や出力上限による途中終了は、この強制の外側で別途処理する必要があります。

「形は保証・中身の妥当性は別」という境界

ここに重要な境界があります。制約付きデコードが保証するのは、あくまで「形状」です。キーが揃っていて、型が合っていて、enumの範囲に収まっている。ここまでは強制できます。しかし、その値が業務的に正しいかどうかまでは保証しません。

たとえば priority が 1〜3 のいずれかであることは形として保証できても、「本来は緊急案件なのにモデルが 3 を返した」という中身の誤りは防げない。assignee に実在しないユーザー名が入っていても、文字列である限りスキーマは通します。ネイティブ構造化出力はスキーマ・型の形状を強制しますが、値の意味的な正しさ(内容の真偽)までは保証せず、各社の公式ドキュメントも追加のバリデーションを推奨しています。

つまりネイティブ機能は「正常完了時の構文崩れをほぼ防ぎ、スキーマの形を強制する」ところまでが守備範囲。値の妥当性チェックや、失敗時に何を返すかといった業務ロジックは、この先で扱うライブラリ層やアプリ側の責務になります。形状保証と値検証は別レイヤーだと分けて考えると、どこに何を実装すべきかが見えてきます。

OpenAI・Gemini・Claudeの実装差を比較

3社ともスキーマ準拠の出力機能を持ちますが、パラメータ名も厳格モードの粒度も異なります。まず全体像を表で押さえ、その後で各社の作法と共通の注意点を見ていきます。以下は2026年7月時点の各社公式ドキュメントに基づく整理です。

主要3社の構造化出力機構 比較表

比較軸 OpenAI Gemini Claude
機能名 Structured Outputs Structured outputs 構造化出力 / strict tool use
スキーマ指定方法 Responses API は text.formatjson_schema/Chat Completions は response_format Interactions API は response_formattypemime_typeschema)/generateContent 系は API・SDK により指定方法が異なる output_config.format / tool定義
厳格モードの有無 あり(strict: true スキーマ準拠を指定 あり(strict: true
JSON強制の粒度 応答本体・関数引数の両方 応答本体 JSON出力・tool呼び出し
向いている場面 厳格なスキーマ固定 Pydanticモデルからのスキーマ生成 文法コンパイル・tool併用

OpenAI公式によれば、Structured Outputs は Chat Completions では response_format に、Responses API では text.formattype: "json_schema"strict: true を指定すると、正常に完了した応答は供給したJSON Schemaに適合します。旧来の「JSON mode」(response_format: {type: "json_object"})は有効なJSON構文のみを保証し、スキーマ準拠までは保証しません。現在も引き続きサポートされていますが、スキーマ適合が必要な場合は Structured Outputs が推奨されます。同じ strict: true はfunction calling側のtool定義でも有効化でき、tool呼び出し引数の型崩れを防げます。OpenAI公式ドキュメントによれば、strictスキーマでは全プロパティがrequired、additionalProperties: false が必須で、任意項目は type: ["string", "null"] のようなnullable unionで表現する決まりです。

Geminiは新規開発向けに Interactions API を推奨しています。Interactions API では response_formattypemime_typeapplication/json)・schema を指定すると、スキーマに沿ったJSONを生成します。従来の generateContent 系も引き続き利用できますが、指定方法は API と SDK・言語によって異なるため、実装前に公式の現行例を確認してください。Google Gemini公式ドキュメントによれば、対応するスキーマはJSON Schemaのサブセットで、type・properties・required・enum・format(date-time等)・itemsといった基本キーワードに対応し、高度な機能は一部非対応です。プロパティを任意にする場合は required から外します。値として null も許可する場合は type: ["string", "null"] のような型配列で表現します。Python SDK(google-genai)では、Pydanticモデルから生成したJSON Schemaを渡せます。

Claudeは構造化出力を2つの機能で提供します。JSON出力(output_config.formattype: "json_schema" とスキーマを指定)と、strict tool use(tool定義に strict: true を指定)です。Anthropic Claude公式ドキュメントによれば、strict構造化出力には1リクエストあたりの複雑度上限があり、strict指定のtoolは最大20個、任意(optional)パラメータは全スキーマ合計で最大24個、union型(anyOf・型配列)パラメータは最大16個までとされています。対応するJSON Schemaはサブセットで、使える参照や制約の種類にも制限があります。生のAPIにコンパイルできないスキーマを渡すと400エラーが返るため、利用するSDKとAPIバージョンの対応表を確認してください。各SDK(代表例として Python / TypeScript / Ruby / PHP)は非対応のスキーマ機能を自動除去し、その制約をフィールドのdescriptionへ移す変換を行える点も、実装上の逃げ道として押さえておくと安心です。なお公式は、文字列の enumconst について大文字・小文字の扱いに注意が要ることを明記しています。列挙値の表記を統一するか、アプリ側で大文字・小文字を無視して比較しておくと安全です。

3社に共通する注意点

作法は違えど、共通してつまずきやすいポイントがあります。まず、深いネストや多数のtool・任意項目を含むスキーマは、API側の複雑度上限やコンパイル負荷に抵触する可能性があります。Claudeのように明示的な上限が公開されている機能もあり、複雑度は現実的な制約として意識する必要があります。

もうひとつ、同じプロバイダでも新旧のAPI surfaceでフィールド名が違う場合があります。Geminiは Interactions API と従来の generateContent 系で指定方法が異なり、さらに SDK・言語によっても差があります。フィールド名が異なるため、1つのコード例に両方を混ぜると動きません。実装時は、利用するエンドポイントがどちらの方式かを最初に確認してください。

なお、各社の「型崩れ率」や「どこが一番崩れにくいか」といった順位付けは、一次ソースのある比較数値が存在しないため本記事では断定しません。信頼性の優劣は、対象のスキーマと実際のトラフィックで計測して判断するのが確実です。この計測とリトライをどう実装に落とすかは、次のライブラリ層の話につながります。

ライブラリで「検証+リトライ」を足す(Instructor中心)

ネイティブ機能が守るのは「形」まで。キーの有無や型は保証しても、値の中身が業務ルール的に正しいかは別の話です。assignee に実在しないユーザー名が入っていても、文字列である限りスキーマは通ってしまう。ここを埋めるのが検証+リトライ型のライブラリで、代表格がInstructorになります。

InstructorはPydanticを利用し、各プロバイダのLLMを統一的なクライアントインターフェースから扱います。スキーマ定義から検証、失敗時の再生成までを1つの流れにまとめる形。

import instructor
from pydantic import BaseModel, field_validator


class Task(BaseModel):
    title: str
    assignee: str

    @field_validator("assignee")
    @classmethod
    def known_user(cls, value: str) -> str:
        if value not in VALID_USERS:
            raise ValueError(f"unknown user: {value}")
        return value


client = instructor.from_provider("openai/<model-id>")

task = client.create(
    response_model=Task,
    max_retries=3,
    messages=[{"role": "user", "content": prompt}],
)

Pydanticのバリデーションが失敗すると、Instructorはエラー内容をモデルに差し戻して再生成させます。これが「生成→検証→失敗ならリトライ」の骨格。OpenAI・Anthropic・Googleなど15以上のプロバイダに対応し、コードを大きく変えずにモデルを差し替えられる点も実装上の利点です。

ではネイティブとライブラリ、どちらを使うべきか。単一プロバイダで形の保証だけ欲しいならネイティブで十分。複数プロバイダを横断したい、値の意味的な検証やリトライまで一括で欲しいならライブラリ層を足す、という切り分けになります。ただしライブラリ方式は生成→検証→再生成でAPI往復が増えうるため、リトライ回数の上限は必ず設けてください。

スキーマ設計で型崩れを減らす実装の勘所

対策の半分はスキーマの書き方で決まります。ネイティブの構造化出力を使っても、自由記述の多い緩いスキーマでは値の意味的な揺れが残ります。また、複雑すぎるスキーマはAPI側の拒否やコンパイル負荷を招くため、スキーマ設計は依然として重要です。自由記述を必要以上に使わず、enumや明確な型を使うと、出力の解釈揺れを抑えやすくなります。

  • 自由記述の string を減らし、選択肢が決まっている項目は enum で固定する
  • 必須と任意を明確に分ける。OpenAIのstrictでは全項目が必須扱いのため、任意項目は ["string", "null"] のnullable unionで表現する
  • 各フィールドに description を付け、何を入れる欄かをモデルに伝える
  • ネストが深い巨大な出力は、複数回の呼び出しに分割する
スキーマを一度に欲張ると、ネイティブの構造化出力でもAPI側の複雑度上限やコンパイル負荷に抵触しやすくなります。Claudeのように strict tool 最大20個・任意パラメータ最大24個という複雑度上限が公開されている機能もあります。まず小さく始め、通ることを確認してから項目を増やす段階的な設計が安全です。

型崩れが自律ループを壊さないための検証・停止設計

型安定が本当に効いてくるのは、出力を次の処理へ渡し続ける自律ループです。1回きりなら人が直せますが、反復では壊れた出力が次ステップの入力になり、小さなズレが連鎖する「ドリフト」に育ちます。

プロンプト単体の調整より、反復ループ全体の設計を重視する考え方が広がっています。ここで鍵になるのが、AIの判断に任せる部分と、固定スクリプトで機械的に弾く部分の使い分けです。

ループに組み込む骨格は次の通り。各ステップの出力を構造化スキーマで検証し、崩れていれば固定処理で捕捉。リトライ上限を設け、収束しなければ人へエスカレーションする。この停止条件(達成・上限・収束・エスカレーション)を最初に決めておかないと、無限ループやコスト暴走につながります。ドリフトは初期の型検証で早く止めるほど傷が浅い、という順序も押さえておきたいところ。

まとめ

型崩れ対策は、どの層で何を守るかを分けて考えると迷いません。構文崩れは、正常に完了した応答についてはネイティブの構造化出力でほぼ防げますが、値が業務的に正しいかは各社の機能では保証されないため、そこは検証層かアプリ側で受け持ちます。自動化に載せるなら、リトライ回数の上限と、ループの停止条件(達成・上限・収束・エスカレーション)を先に決めておくのが肝心。どのプロバイダが崩れにくいかは公開された一次ソースの比較数値がないので、運用するスキーマと実トラフィックで計測して判断することになります。手を動かす順序としては、まず小さなスキーマで1社のネイティブ機能を通し、値の検証やリトライが要る場面でInstructorのような層を足す形が現実的。仕様や上限は2026年7月時点の公式情報が基準です。

よくある質問

Q. 構造化出力とfunction callingは何が違いますか?

function callingは、モデルに使うtoolと引数を選ばせ、アプリケーション側の処理へ接続する仕組みです。構造化出力は、tool引数または通常の応答本文を、指定したスキーマに沿った形式で受け取る仕組みです。両者は重なる部分がありますが、同じ機能ではありません。strictを指定すると、関数呼び出しの引数も型崩れしにくくなります。

Q. プロンプトで「JSONで返して」と指示するだけでは不十分ですか?

プロンプトの工夫は崩れる頻度を下げますが、確率的に前置きが付いたりキーが揺れたりする余地は残り、ゼロにはできません。ネイティブの構造化出力は生成経路そのものをスキーマで縛るため、正常に完了した応答についてはほぼ構文崩れを防げます。自動化に載せるなら後者が確実です。

Q. どのプロバイダが一番型崩れしにくいですか?

公開された一次ソースの比較数値がないため、順位は断定できません。OpenAI・Gemini・Claudeともスキーマ準拠の出力機能を備えていますが、崩れにくさは、運用するスキーマと実際のトラフィックで計測して判断するのが確実です。

Q. 旧来のJSON modeとStructured Outputsは何が違いますか?

JSON modeは出力が有効なJSON構文であることだけを保証し、キー名や型がスキーマ通りかまでは保証しません。現在も引き続きサポートされていますが、スキーマの形まで固定したい場合はstrictを指定したStructured Outputsが推奨されます。

参考資料

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