なぜ構造化出力が必要か
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 で特定ツールを強制指定するのが確実な使い方です。