Python AI構造化出力実戦:LLMから安定したJSONを取得する5つのコアパターン
AI与大数据
AI構造化出力:なぜLLMの「自由テキスト」は本番環境の悪夢なのか
LLMはデフォルトで自由テキストを出力します。JSONを要求すると説明文が混ざり、整数を要求すると文字列"42"が返り、Schemaを定義してもフィールドがランダムに欠落します。構造化出力(Structured Output)は、LLMに事前定義されたSchemaに厳密に従ってデータを返させる仕組みです——これはAIが「チャット玩具」から「本番システム」へ進むための重要なステップです。2026年、OpenAIはStructured Outputをネイティブサポートし、Instructorライブラリは自動リトライを実現し、Outlinesは制約デコーディングで100%のフォーマット準拠を保証します。
本記事では5つのコアパターンから、Pydantic Schema定義→Function Calling→Instructorリトライ→制約デコーディング→マルチモデル適応のフルパイプライン実戦を解説します。
コア概念
| 概念 | 説明 |
|---|---|
| Structured Output | LLMが事前定義Schemaに従って構造化データ(JSONなど)を出力 |
| Pydantic Model | Pythonデータ検証ライブラリ、型アノテーションで出力Schemaを定義 |
| Function Calling | OpenAI APIの関数呼び出しメカニズム、出力フォーマットを制約 |
| Instructor | PydanticベースのLLM構造化出力ライブラリ、自動リトライ対応 |
| Constrained Decoding | トークンレベルで出力がSchemaに準拠することを保証する制約デコーディング |
| JSON Schema | JSONデータ構造を記述する仕様、構造化出力の基盤 |
| Output Validation | LLM出力を検証し、型とフィールドの完全性を保証 |
| Multi-Model Adapter | 異なるLLMプロバイダー間で統一的な構造化出力を実現するアダプター層 |
問題分析:LLMの非構造化出力における5つの課題
- フォーマットの不安定性:同じPromptでも、LLMはJSONを返したりMarkdownでラップされたJSONを返したりする
- 型の信頼性不足:integerを要求しても、LLMは文字列"42"や浮動小数点42.0を返す可能性がある
- フィールドの欠落:Schemaで10個のフィールドを定義しても、LLMはランダムに2-3個省略する
- ハルシネーション内容:LLMがJSON値に説明テキストを混入させる、例:
"price": "約100円くらい" - パースのクラッシュ:本番環境でのJSONパース失敗率は5-15%に達し、毎回の手動介入が必要
ステップバイステップ:5つのAI構造化出力コアパターン
パターン1:Pydantic Schema定義 + 基本構造化出力
pip install pydantic==2.11 openai==1.82
from pydantic import BaseModel, Field
from openai import OpenAI
import json
class ProductInfo(BaseModel):
name: str = Field(description="製品名")
price: float = Field(description="製品価格(円)")
category: str = Field(description="製品カテゴリ")
in_stock: bool = Field(description="在庫ありかどうか")
tags: list[str] = Field(description="製品タグリスト")
client = OpenAI()
# 方法1:response_formatパラメータの使用(OpenAIネイティブ構造化出力)
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "あなたは製品情報抽出アシスタントです。ユーザーの説明から構造化された製品情報を抽出してください。"},
{"role": "user", "content": "このMacBook Pro 14インチは149,800円で、ノートパソコンカテゴリに属し、現在在庫あり、タグはApple、プレミアム、生産性ツールです"},
],
response_format=ProductInfo,
)
product = response.choices[0].message.parsed
print(f"製品: {product.name}")
print(f"価格: {product.price}")
print(f"カテゴリ: {product.category}")
print(f"在庫: {product.in_stock}")
print(f"タグ: {product.tags}")
print(f"型検証: priceは{type(product.price).__name__}")
# 方法2:手動JSON Schema + パース検証
class OrderItem(BaseModel):
item_name: str = Field(description="商品名")
quantity: int = Field(gt=0, description="購入数量")
unit_price: float = Field(ge=0, description="単価")
class CustomerOrder(BaseModel):
order_id: str = Field(description="注文番号")
customer_name: str = Field(description="顧客名")
items: list[OrderItem] = Field(description="注文明細リスト")
total_amount: float = Field(ge=0, description="注文合計金額")
response2 = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "注文情報を抽出し、厳密にJSONで返してください。"},
{"role": "user", "content": "注文ORD-20260621、顧客田中太郎、キーボード2個各4,980円、モニター1台49,800円、合計59,760円"},
],
response_format={"type": "json_object"},
)
raw_json = json.loads(response2.choices[0].message.content)
order = CustomerOrder.model_validate(raw_json)
print(f"\n注文: {order.order_id}, 顧客: {order.customer_name}")
for item in order.items:
print(f" {item.item_name} x{item.quantity} @ {item.unit_price}")
print(f"合計: {order.total_amount}")
パターン2:OpenAI Function Calling構造化出力
from pydantic import BaseModel, Field
from openai import OpenAI
import json
class WeatherQuery(BaseModel):
city: str = Field(description="都市名")
temperature: float = Field(description="気温(摂氏)")
condition: str = Field(description="天気状況:晴れ/曇り/雨/雪")
humidity: int = Field(ge=0, le=100, description="湿度パーセンテージ")
wind_speed: float = Field(ge=0, description="風速(km/h)")
class TravelPlan(BaseModel):
destination: str = Field(description="目的地")
days: int = Field(gt=0, description="旅行日数")
budget: float = Field(ge=0, description="予算(円)")
activities: list[str] = Field(description="おすすめアクティビティリスト")
weather_advice: str = Field(description="天気に関するアドバイス")
client = OpenAI()
# Function Callingで構造化出力を強制
tools = [
{
"type": "function",
"function": {
"name": "get_travel_plan",
"description": "旅行プランを生成",
"parameters": TravelPlan.model_json_schema(),
},
},
{
"type": "function",
"function": {
"name": "get_weather",
"description": "天気情報を取得",
"parameters": WeatherQuery.model_json_schema(),
},
},
]
response = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "あなたは旅行プランニングアシスタントです。ユーザーのニーズに基づいて旅行プランと天気情報を生成してください。"},
{"role": "user", "content": "京都に3日間旅行する予定です、予算は10万円"},
],
tools=tools,
tool_choice="auto",
)
for tool_call in response.choices[0].message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
if func_name == "get_travel_plan":
plan = TravelPlan.model_validate(func_args)
print(f"目的地: {plan.destination}")
print(f"日数: {plan.days}")
print(f"予算: {plan.budget}")
print(f"アクティビティ: {', '.join(plan.activities)}")
print(f"天気アドバイス: {plan.weather_advice}")
elif func_name == "get_weather":
weather = WeatherQuery.model_validate(func_args)
print(f"\n{weather.city}: {weather.temperature}°C, {weather.condition}, 湿度{weather.humidity}%, 風速{weather.wind_speed}km/h")
パターン3:Instructorライブラリによるリトライベース構造化出力
pip install instructor==1.7 pydantic==2.11
import instructor
from pydantic import BaseModel, Field
from openai import OpenAI
class SentimentResult(BaseModel):
text: str = Field(description="分析対象テキスト")
sentiment: str = Field(description="感情:positive/negative/neutral")
confidence: float = Field(ge=0, le=1, description="信頼度0-1")
keywords: list[str] = Field(description="キーワードリスト")
class ArticleSummary(BaseModel):
title: str = Field(description="記事タイトル")
summary: str = Field(min_length=50, max_length=200, description="50-200文字の要約")
key_points: list[str] = Field(min_length=2, max_length=5, description="2-5個の要点")
reading_time_minutes: int = Field(gt=0, description="推定読了時間(分)")
difficulty: str = Field(description="難易度:beginner/intermediate/advanced")
# instructorでOpenAIクライアントをラップ
client = instructor.from_openai(OpenAI())
# 自動リトライ:出力がSchemaに合わない場合、instructorが自動的にリトライ
sentiment = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "あなたは感情分析の専門家です。"},
{"role": "user", "content": "この製品は本当に使いやすい!インターフェースがシンプルで機能が強力、強くおすすめします!"},
],
response_model=SentimentResult,
max_retries=3,
)
print(f"感情: {sentiment.sentiment}")
print(f"信頼度: {sentiment.confidence}")
print(f"キーワード: {', '.join(sentiment.keywords)}")
# ネストされたモデル + 厳格な検証
summary = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "あなたは記事要約ジェネレーターです。"},
{"role": "user", "content": "Python 3.13が2024年10月に正式リリースされ、フリースレッドモード(PEP 703)、改良されたインタラクティブインタープリタ、改善されたエラーメッセージなどの主要なアップデートが含まれています。フリースレッドモードにより、PythonはGILなしで実行できるようになり、マルチスレッドパフォーマンスが大幅に向上します。新しいREPLは複数行編集とシンタックスハイライトをサポートし、開発体験を大きく向上させています。"},
],
response_model=ArticleSummary,
max_retries=3,
)
print(f"\nタイトル: {summary.title}")
print(f"要約: {summary.summary}")
for i, point in enumerate(summary.key_points, 1):
print(f" 要点{i}: {point}")
print(f"読了時間: {summary.reading_time_minutes}分")
print(f"難易度: {summary.difficulty}")
パターン4:Outlines/構造化生成(制約デコーディング)
pip install outlines==0.1 pydantic==2.11 transformers
import outlines
from pydantic import BaseModel, Field
import json
class EntityExtraction(BaseModel):
person_names: list[str] = Field(description="人名リスト")
organizations: list[str] = Field(description="組織名リスト")
locations: list[str] = Field(description="場所リスト")
dates: list[str] = Field(description="日付リスト")
class CodeAnalysis(BaseModel):
language: str = Field(description="プログラミング言語")
complexity: str = Field(description="複雑度:low/medium/high")
functions: list[str] = Field(description="関数名リスト")
imports: list[str] = Field(description="インポートモジュールリスト")
issues: list[str] = Field(description="潜在的な問題リスト")
# Outlinesで制約デコーディング(ローカルモデル)
# 注意:初回実行時はモデルをダウンロードします(約1.5GB)
model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct")
# 方法1:JSON Schemaベースの制約生成
generator = outlines.generate.json(model, EntityExtraction.model_json_schema())
text = "2026年3月15日、田中さんは東京でGoogle開発者カンファレンスに参加し、佐藤さんと大阪でのAI技術応用について議論しました。"
result = generator(text)
print(f"人名: {result['person_names']}")
print(f"組織: {result['organizations']}")
print(f"場所: {result['locations']}")
print(f"日付: {result['dates']}")
# 方法2:正規表現ベースの制約生成
# 出力フォーマットの100%準拠を保証
phone_generator = outlines.generate.regex(model, r"\d{3}-\d{4}-\d{4}")
phone = phone_generator("私の電話番号は")
print(f"\n電話番号: {phone}")
# 方法3:Choiceベースの制約生成(分類タスク)
choice_generator = outlines.generate.choice(model, ["positive", "negative", "neutral"])
sentiment = choice_generator("この製品はとても使いやすい、大満足です!")
print(f"感情分類: {sentiment}")
# 方法4:Pydantic Modelベースの制約生成
code_generator = outlines.generate.json(model, CodeAnalysis.model_json_schema())
code_text = """
import os
import sys
from typing import List
def process_data(items: List[str]) -> dict:
result = {}
for item in items:
result[item] = len(item)
return result
def main():
data = process_data(["hello", "world"])
print(data)
"""
code_result = code_generator(code_text)
print(f"\n言語: {code_result['language']}")
print(f"複雑度: {code_result['complexity']}")
print(f"関数: {code_result['functions']}")
パターン5:マルチモデル構造化出力アダプター
from pydantic import BaseModel, Field
from openai import OpenAI
import anthropic
import google.generativeai as genai
import json
from abc import ABC, abstractmethod
from typing import TypeVar, Type
T = TypeVar("T", bound=BaseModel)
class BookRecommendation(BaseModel):
title: str = Field(description="書籍名")
author: str = Field(description="著者名")
genre: str = Field(description="ジャンル")
rating: float = Field(ge=0, le=5, description="評価0-5")
reason: str = Field(description="おすすめ理由")
class StructuredOutputAdapter(ABC):
"""マルチモデル構造化出力アダプター基底クラス"""
@abstractmethod
def generate(self, prompt: str, response_model: Type[T]) -> T:
pass
class OpenAIAdapter(StructuredOutputAdapter):
def __init__(self, api_key: str, model: str = "gpt-4o-2024-08-06"):
self.client = OpenAI(api_key=api_key)
self.model = model
def generate(self, prompt: str, response_model: Type[T]) -> T:
response = self.client.beta.chat.completions.parse(
model=self.model,
messages=[
{"role": "system", "content": "Schemaに厳密に従ってデータを返してください。"},
{"role": "user", "content": prompt},
],
response_format=response_model,
)
return response.choices[0].message.parsed
class AnthropicAdapter(StructuredOutputAdapter):
def __init__(self, api_key: str, model: str = "claude-sonnet-4-20250514"):
self.client = anthropic.Anthropic(api_key=api_key)
self.model = model
def generate(self, prompt: str, response_model: Type[T]) -> T:
schema = response_model.model_json_schema()
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
tools=[{
"name": "structured_output",
"description": "構造化データを返す",
"input_schema": schema,
}],
tool_choice={"type": "tool", "name": "structured_output"},
)
for block in response.content:
if block.type == "tool_use":
return response_model.model_validate(block.input)
raise ValueError("No tool use in response")
class GeminiAdapter(StructuredOutputAdapter):
def __init__(self, api_key: str, model: str = "gemini-2.0-flash"):
genai.configure(api_key=api_key)
self.model_name = model
def generate(self, prompt: str, response_model: Type[T]) -> T:
model = genai.GenerativeModel(
self.model_name,
generation_config={"response_mime_type": "application/json"},
)
schema_instructions = f"以下のJSON Schemaに厳密に準拠したデータを返してください:\n{json.dumps(response_model.model_json_schema(), ensure_ascii=False)}"
response = model.generate_content(f"{schema_instructions}\n\n{prompt}")
raw = json.loads(response.text)
return response_model.model_validate(raw)
# 統一呼び出しインターフェース
def get_recommendation(adapter: StructuredOutputAdapter, query: str) -> BookRecommendation:
return adapter.generate(query, BookRecommendation)
# 使用例(対応するAPI Keyの設定が必要)
# openai_adapter = OpenAIAdapter(api_key="sk-xxx")
# result = get_recommendation(openai_adapter, "AI初心者向けの本を推薦してください")
# print(f"推薦: 『{result.title}』 by {result.author}, 評価{result.rating}")
# 汎用検証レイヤー:モデルに関わらず、最終的にPydanticで検証
def safe_parse(raw_json: dict, model: Type[T]) -> T:
"""汎用セーフパース:検証 + デフォルト値補充"""
try:
return model.model_validate(raw_json)
except Exception as e:
print(f"検証失敗: {e}")
defaults = {}
for field_name, field_info in model.model_fields.items():
if field_info.is_required():
raise ValueError(f"必須フィールド {field_name} が欠落")
defaults[field_name] = field_info.default
return model.model_validate(defaults)
よくある落とし穴
落とし穴1:Pydanticフィールドにdescriptionがない
# ❌ 間違い:descriptionがない、LLMが何を入力すべきか分からない
class BadModel(BaseModel):
x1: str
x2: float
x3: list[str]
# ✅ 正しい:すべてのフィールドにdescriptionを追加
class GoodModel(BaseModel):
product_name: str = Field(description="製品のフルネーム")
price: float = Field(description="価格(円)")
tags: list[str] = Field(description="製品タグ、例:'電子機器''セール'")
落とし穴2:response_formatを設定せずにLLMの自動JSON出力に依存
# ❌ 間違い:PromptだけでJSONを要求、フォーマットが不安定
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "製品情報をJSON形式で返してください"}],
)
# ✅ 正しい:response_formatを明示的に設定
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "製品情報をJSON形式で返してください"}],
response_format={"type": "json_object"},
)
落とし穴3:Pydantic検証エラーを無視
# ❌ 間違い:json.loadsのまま検証なし
data = json.loads(response.choices[0].message.content)
price = data["price"] # 文字列"42.0"の可能性があり、floatではない
# ✅ 正しい:Pydantic検証を使用
from pydantic import ValidationError
try:
product = ProductInfo.model_validate_json(response.choices[0].message.content)
price = product.price # float型であることが保証される
except ValidationError as e:
print(f"検証失敗: {e}")
落とし穴4:ネストが深すぎてLLMの出力が混乱
# ❌ 間違い:3層以上のネスト、LLMがエラーを起こしやすい
class DeepNested(BaseModel):
level1: "Level1Model" # -> level2: Level2Model -> level3: Level3Model
# ✅ 正しい:フラットな設計、ネストは最大2層
class OrderItem(BaseModel):
name: str = Field(description="商品名")
qty: int = Field(description="数量")
class Order(BaseModel):
items: list[OrderItem] = Field(description="商品リスト") # ネストは1層のみ
total: float = Field(description="合計金額")
落とし穴5:Function Callingでのtool_choiceの不適切な設定
# ❌ 間違い:tool_choice="auto"ではLLMが関数を呼ばない可能性がある
response = client.chat.completions.create(
model="gpt-4o",
messages=[...],
tools=tools,
tool_choice="auto", # LLMがプレーンテキストで応答する可能性
)
# ✅ 正しい:特定の関数の呼び出しを強制
response = client.chat.completions.create(
model="gpt-4o",
messages=[...],
tools=tools,
tool_choice={"type": "function", "function": {"name": "get_travel_plan"}},
)
エラートラブルシューティング
| # | エラーメッセージ | 原因 | 解決方法 |
|---|---|---|---|
| 1 | ValidationError: missing field |
LLM出力に必須フィールドが欠落 | Field(default=...)を使用するかmax_retriesを増やす |
| 2 | json.decoder.JSONDecodeError |
LLM出力が有効なJSONではない | response_format={"type": "json_object"}を設定 |
| 3 | ValidationError: value is not a valid float |
LLMが数値の代わりに文字列を返す | Pydanticの自動変換またはField(coerce_numbers=True)を使用 |
| 4 | InstructorRetryError: max retries exceeded |
N回リトライ後もSchemaに合わない | Schemaを簡素化するかPromptの説明を最適化 |
| 5 | TypeError: 'NoneType' object is not subscriptable |
LLMが空の内容を返す | API Keyのクォータとモデルの可用性を確認 |
| 6 | ValidationError: list should have at least N items |
リストフィールドの要素が不足 | Field(min_length=N)を使用しPromptを最適化 |
| 7 | StructuredOutputError: schema too complex |
Schemaが複雑すぎる | モデルをフラット化し、ネストレベルを減らす |
| 8 | RateLimitError |
API呼び出し頻度が制限を超過 | バックオフリトライを追加するか同時実行を減らす |
| 9 | ValidationError: string too long/short |
文字列の長さが制約に合わない | max_length/min_lengthを調整するかPromptを最適化 |
| 10 | ToolCallNotFoundError |
LLMがtoolを呼び出さなかった | tool_choiceを設定して強制呼び出し |
高度な最適化
- ストリーミング構造化出力:instructorの
create_partialメソッドでストリーミングパースを実現、生成と同時に検証 - Schemaキャッシュ:同じSchemaのリクエストに対してJSON Schemaをキャッシュし、重複シリアライズのオーバーヘッドを削減
- マルチSchemaルーティング:ユーザーの意図に基づいて異なるPydantic Modelを動的に選択し、柔軟な構造化出力を実現
- 出力後処理パイプライン:Pydantic検証→ビジネスルールチェック→デフォルト値補充→ログ記録、完全なデータ品質保証チェーンを形成
- A/BテストSchema:同じタスクに異なる粒度のSchemaを使用し、LLM出力品質とトークン消費を比較
比較分析
| 次元 | OpenAI Structured Output | Instructor | Outlines | LMQL | Guidance |
|---|---|---|---|---|---|
| フォーマット保証 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 自動リトライ | ❌ | ⭐⭐⭐⭐⭐ | ❌ | ⭐⭐⭐ | ⭐⭐⭐ |
| ローカルモデル対応 | ❌ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| マルチモデル適応 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| 学習曲線 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
| 型安全性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
まとめ:AI構造化出力は「チャット型AI」から「本番級AI」への必須の道です。Pydantic Schema→Function Calling→Instructorリトライ→制約デコーディング→マルチモデル適応の五位一体、これが2026年のPython AI構造化出力のベストプラクティスです。コア原則:Schemaは契約、検証は保証、リトライはフォールトトレランス。
オンラインツールおすすめ
- JSONフォーマッター:/ja/json/format
- Hash計算:/ja/encode/hash
- cURL→コード変換:/ja/dev/curl-to-code
ブラウザローカルツールを無料で試す →
#结构化输出#AI JSON#Pydantic#Python AI#2026#AI与大数据