Knowledge Notebook
一覧に戻る

LLMの出力をJSONに固定する「Structured Outputs(構造化出力)」の仕組みと実装

大規模言語モデル(LLM)を外部システムやデータベースと連携させる際、出力フォーマットを安定させることは実務上の大きな課題です。プロンプトで「JSON形式で出力してください」と指示しても、不要な前置きが含まれたり、キー名が変動したりする問題が発生します。

この問題を解消するため、主要なAPIプロバイダーは、指定したスキーマ通りのJSONを保証するStructured Outputs(構造化出力)機能を提供しています。本記事では、この機能が確実にJSONを出力できる仕組みと、具体的な実装コードを解説します。

1. Structured Outputsの仕組み

従来のプロンプトによる指示や単なる「JSONモード」の指定では、構文エラーは防げても必要なキーが欠落する問題を防ぎきれませんでした。

これに対し、Structured Outputsでは文法制約デコーディング(Constrained Decoding)と呼ばれる技術を使用します。LLMが次のトークン(単語の断片)を出力する際、API側が指定したJSONスキーマのルールに適合しないトークンを選択肢から除外(確率をゼロにマスク)します。この仕組みにより、モデルはスキーマに従った有効なJSONトークンのみを順番に生成するよう強制されます。ハルシネーションが発生した場合でも、JSONの文法自体が崩れることは理論上なくなります。

2. 代表的なユースケース(活用例)

Structured Outputsは、LLMが生成したデータをプログラムで自動処理する以下のような場面で力を発揮します。

  • RAG(検索拡張生成)における情報抽出: 雑多な文章やPDFから、「著者名」「発行日」「重要なキーワード」「要約」などを正確に抽出し、データベースへ直接保存するケース。
  • データ可視化(グラフ)用のデータ作成: グラフ描画に必要な「X軸のラベルリスト」と「Y軸のプロット数値リスト」を、正確な配列データとして取得するケース。
  • 自律型AIエージェントの意思決定: エージェントが実行すべき「ツール名」と、ツールに渡す「引数」をオブジェクト形式で出力し、自動で関数を実行させるケース。

3. OpenAI APIでの実装方法

OpenAIのAPIでは、PythonのデータバリデーションライブラリであるPydanticを用いて出力スキーマを定義できます。Pydanticモデルを response_format 引数に渡すことで、APIのレスポンスが自動的にそのクラスのインスタンスとしてパースされます。

from pydantic import BaseModel, Field
from openai import OpenAI

# 1. 出力してほしいJSONの構造をPydanticモデルで定義
class CalendarEvent(BaseModel):
    name: str = Field(description="イベントの名称")
    date: str = Field(description="開催日時(YYYY-MM-DD形式)")
    participants: list[str] = Field(description="参加者の名前リスト")

client = OpenAI()

# 2. beta.chat.completions.parse メソッドを使用
completion = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "入力テキストからイベントの情報を正確に抽出してください。"},
        {"role": "user", "content": "木曜日の午後3時から、太郎さんと花子さんと一緒に定例会議を行います。今日のイベントはこれだけです。なお、今日は2026年6月15日の月曜日です。"}
    ],
    response_format=CalendarEvent,
)

# 3. 型安全にパースされたオブジェクトを取得
event = completion.choices[0].message.parsed

# オブジェクトのプロパティとして直接アクセス可能
print(f"イベント名: {event.name}")
print(f"日付: {event.date}")
print(f"参加者: {', '.join(event.participants)}")

このコードを実行すると、LLMからのテキスト出力を手動でJSONデコードしたり、エラーハンドリングを長々と記述したりすることなく、直接 event.nameevent.date のような形で安全にデータを扱えます。

4. Gemini APIでの実装方法

GoogleのGemini APIでも、最新の google-genai SDKを用いることで、同様にPydanticモデルを使用した構造化出力が可能です。設定オブジェクトの response_mime_type"application/json" を指定し、response_schema にPydanticのクラスを渡します。

from google import genai
from pydantic import BaseModel, Field

# 1. 出力スキーマの定義
class Ingredient(BaseModel):
    name: str = Field(description="材料名")
    quantity: str = Field(description="分量")

class Recipe(BaseModel):
    recipe_name: str = Field(description="料理名")
    ingredients: list[Ingredient] = Field(description="材料のリスト")

# 2. クライアントの初期化
client = genai.Client()

# 3. response_schemaを設定して呼び出し
response = client.models.generate_content(
    model="gemini-2.0-flash",
    contents="クッキーの簡単なレシピを教えてください。",
    config={
        "response_mime_type": "application/json",
        "response_schema": Recipe,
    },
)

# 4. パースされた結果を取得
recipe = response.parsed
print(f"料理名: {recipe.recipe_name}")
for ing in recipe.ingredients:
    print(f"- {ing.name}: {ing.quantity}")

Gemini API側でスキーマが検証されるため、返されるデータは必ず定義した Recipe の形式に一致します。余分な説明やMarkdownの装飾コード(```json など)が混入することもありません。

5. 導入時のメリットと注意点

Structured Outputsは非常に強力ですが、実際に運用に組み込むにあたってはいくつかの注意点があります。

主なメリット

  • パースエラーの排除: 生成された文字列の構文エラーや、必要なキーが欠落する問題が解消されます。
  • コードの簡素化: 自前で再試行(リトライ)処理や、複雑な正規表現によるJSON抽出の処理を書く必要がなくなります。
  • 型安全な開発: Pydanticモデルがそのまま戻り値となるため、静的解析ツールによる型チェックの恩恵を受けられます。

知っておくべき制限事項と対策

  • スキーマの厳格性: OpenAIのStructured Outputsを使用する場合、すべてのプロパティについて required を有効にする必要があります。オプショナル(任意)の項目を設定したい場合は、型定義に None または null を許容するスキーマを設定します。
  • 初回アクセスの遅延: スキーマが新しく指定された初回リクエスト時、APIサーバー側でスキーマを処理するためのコンパイル処理が走ります。このため、最初の1回だけ応答速度(最初のトークンが返るまでの時間)が長くなる傾向があります。ただし、2回目以降はコンパイル結果がキャッシュされるため高速に動作します。
  • 表現力の制限: 自由回答と比べてトークン生成の自由度が下がるため、非常に複雑な論理推論を直接構造化させようとすると、回答の質がわずかに低下する場合があります。その場合は、思考プロセス用のフィールド(例: thought_process)をスキーマ内に定義し、モデルに推論を記述させてから最終回答を出力させる構成が効果的です。

6. 実装時によくある失敗例と対策

Structured Outputsを実際に構築する際、エラーや挙動の不一致に遭遇しやすいポイントとその対策をまとめました。

  • 失敗例1: キーの欠落によるスキーマ違反エラー(OpenAI)
    • 状況: Pydanticモデルでデフォルト値を設定したため、LLMからデータが送られなくてもエラーにならないと想定したが、APIから拒否された。
    • 原因: OpenAIの制限として、すべてのプロパティについて required を有効にする(スキーマ上は必須項目として扱う)必要があるためです。
    • 対策: データが空になる可能性のあるフィールドは、型定義に Nonenull)を明示的に含めて定義します(例: description: str | None = None)。
  • 失敗例2: 複雑な再帰構造や深いネストによるコンパイルエラー
    • 状況: フォルダ構造のような自己参照(再帰的)オブジェクトや、5階層以上深くネストした複雑なスキーマを定義したところ、APIがエラーを返した。
    • 原因: スキーマが複雑すぎると、API側で行われる「文法制約デコーディングのプリプロセス(コンパイル)」の処理限界を超えてしまうためです。
    • 対策: スキーマは可能な限り平坦(フラット)に設計し、ネストさせる場合でも2〜3階層程度に留めるようにします。
  • 失敗例3: Pydanticの Field(description="...") がモデルに無視される
    • 状況: Pydanticの Field(description="...") で「日付はYYYY-MM-DD形式」と指示したが、無視された。
    • 原因: Pydanticの description メタデータはモデルに伝わりますが、プロンプトに比べて指示の優先度が下がる傾向にあるためです。
    • 対策: 重要な制限事項は、スキーマの説明文だけでなく、システムプロンプト側でも重複して指示を記述することで改善します。

7. まとめ

Structured Outputsは、LLMの応答をプログラムから確実に利用可能にするための必須の機能です。プロンプトエンジニアリングの試行錯誤から開発者を解放し、RAGシステムやAPI呼び出しの連携(Tool Use)といった高度な処理の安定性を向上させることができます。

利用するモデルやSDKのバージョンによって記述方法が多少異なるため、公式の開発者ドキュメントを参照しながら実装を進めるようにしてください。


参考URL