なぜ構造化出力が必要か

LLMをアプリに組み込む際、「レスポンスをJSONでパースしてデータとして使いたい」という場面が頻繁にあります。ところがLLMに「JSONで答えてください」と指示しただけでは、マークダウンのコードブロックで囲んだり、末尾にコメントを付けたり、JSON以外の説明文を混入させたりすることがあります。

構造化出力(Structured Output)はLLMの出力フォーマットを機械的に強制する手法で、パースエラーを防ぎ信頼性の高いアプリを作るために重要です。

3つのアプローチ

1. JSON mode

OpenAIやAnthropicが提供するJSON modeは、モデルに「必ず有効なJSONを返すこと」を指示するモードです。

OpenAIの例:

from openai import OpenAI
client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o-mini",
    response_format={"type": "json_object"},
    messages=[
        {"role": "system", "content": "必ずJSONで回答してください"},
        {"role": "user", "content": "東京の人気観光地を3つ、name・descriptionのJSONで教えて"}
    ]
)
import json
data = json.loads(response.choices[0].message.content)

JSON modeの欠点は、JSONの「中身の構造」は保証しない点です。キー名が違ったり、ネスト構造が期待と異なる場合があります。

2. Function Calling(ツール呼び出し)

Function Callingでは、期待するJSONのスキーマを関数の引数として定義します。LLMはその関数を「呼び出す」という形で構造化データを返します。スキーマに合わない出力は自動的に修正されるため、より確実な方法です。

OpenAIのFunction Callingの例:

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_attractions",
            "description": "観光地情報を返す",
            "parameters": {
                "type": "object",
                "properties": {
                    "attractions": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "name": {"type": "string"},
                                "category": {"type": "string"},
                                "rating": {"type": "number"}
                            },
                            "required": ["name", "category"]
                        }
                    }
                }
            }
        }
    }
]

response = client.chat.completions.create(
    model="gpt-4o",
    tools=tools,
    tool_choice="required",
    messages=[{"role": "user", "content": "東京の観光地3つを教えて"}]
)

tool_call = response.choices[0].message.tool_calls[0]
data = json.loads(tool_call.function.arguments)

3. Pydantic × OpenAI(Structured Outputs)

OpenAIのPython SDKはPydanticモデルを使った構造化出力を直接サポートしています。スキーマをPythonクラスで定義できるため、型安全でコードが読みやすくなります。

from pydantic import BaseModel
from openai import OpenAI

client = OpenAI()

class Attraction(BaseModel):
    name: str
    category: str
    description: str
    rating: float

class AttractionList(BaseModel):
    attractions: list[Attraction]

response = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[{"role": "user", "content": "東京の観光地3つを教えて"}],
    response_format=AttractionList
)

result = response.choices[0].message.parsed
for attraction in result.attractions:
    print(f"{attraction.name}: {attraction.rating}点")

Pydanticの型定義がそのままJSONスキーマに変換されるため、スキーマの二重管理が不要です。

ClaudeのツールによるStructured Output

ClaudeではFunction Callingに相当する機能を「ツール」と呼びます。Anthropic SDKでの実装例です。

import anthropic
import json

client = anthropic.Anthropic()

tools = [
    {
        "name": "extract_product_info",
        "description": "テキストから製品情報を抽出する",
        "input_schema": {
            "type": "object",
            "properties": {
                "product_name": {"type": "string", "description": "製品名"},
                "price": {"type": "number", "description": "価格(円)"},
                "features": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "主な機能のリスト"
                }
            },
            "required": ["product_name", "price"]
        }
    }
]

response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "tool", "name": "extract_product_info"},
    messages=[{
        "role": "user",
        "content": "新型スマートウォッチ、価格は49,800円。心拍数モニター・GPS・防水対応の製品説明から情報を抽出して"
    }]
)

tool_use = next(b for b in response.content if b.type == "tool_use")
data = tool_use.input
print(f"製品名: {data['product_name']}, 価格: {data['price']}円")

tool_choice で特定のツールを強制指定すると、必ずそのツールを呼び出してくれます。これが構造化出力を確実にする鍵です。

Function CallingとJSON modeの使い分け

スキーマが固定で型の保証が必要な場合はFunction Callingが適しています。返ってくるデータをそのままDBに保存したり、別のシステムと連携したりする場合は特にFunction Callingを推奨します。

より柔軟なJSON出力が欲しい場合や、スキーマが動的に変わる場合はJSON modeが向いています。ただし出力のバリデーションはアプリ側で行う必要があります。

まとめ

構造化出力にはJSON mode・Function Calling・Pydantic統合の3つのアプローチがあります。確実な型保証が必要な本番アプリではFunction Calling(またはPydanticを使ったStructured Outputs)が信頼性が高く、スキーマが固定されているほど出力の一貫性が上がります。ClaudeのツールもOpenAIのFunction Callingと同じ考え方で実装でき、tool_choice で特定ツールを強制指定するのが確実な使い方です。