はじめに
LLMをプロダクションで使うとき、最大の課題のひとつが 「出力のフォーマットが安定しない」 ことです。
「JSONで返して」とプロンプトに書いても、余計な説明文がついたり、キーの名前が揺れたり、そもそもJSONとして壊れた文字列が返ってきたりします。
この問題を根本的に解決するのが 「構造化出力(Structured Output)」 です。2024年にOpenAIが正式サポートして以降、Anthropic(Claude)、Google(Gemini)など主要プロバイダが続々と対応し、2026年現在は プロダクションLLMアプリの標準機能 になっています。
この記事では、構造化出力の仕組みと実装方法を、Pythonのコード例とともに解説します。
なぜ構造化出力が必要なのか
たとえば、商品レビューの感情分析をLLMにやらせるケースを考えましょう。
構造化出力なし(従来の方法)
response = llm.invoke("このレビューを分析して: '配送が早くて助かりました'")
# → "このレビューはポジティブな感情を示しています。星5つ相当と考えられます。"
テキストとしては正しいですが、これをプログラムで処理するには 文字列をパースする 必要があります。「星5つ」を数値として取り出すには正規表現や文字列操作が必要で、LLMの出力フォーマットが変わるたびにパーサーが壊れます。
構造化出力あり
# → {"sentiment": "positive", "score": 5, "summary": "配送速度への満足"}
最初から決められたスキーマのJSONで返ってくるので、パース不要で即座にプログラムで利用できます。
構造化出力の仕組み
構造化出力の基本的な考え方は、「LLMにJSON Schemaを渡して、そのスキーマに完全準拠した出力を強制する」ことです。
① 開発者がスキーマ(型定義)を用意
↓
② APIリクエスト時にスキーマを添付
↓
③ LLMがスキーマに従ったJSONだけを生成
↓
④ SDKがJSONをPydanticオブジェクトに自動変換
LLM側では、トークン生成時にスキーマに違反するトークンの出力確率をゼロにする 制約付きデコーディング が行われています。これにより、OpenAIの公式評価では 100%のスキーマ準拠率 を達成しています。
実装方法①:OpenAI SDK
OpenAIは2024年8月に構造化出力を正式サポートしました。PydanticモデルをそのままAPIに渡せます。
Pydanticでスキーマを定義
from pydantic import BaseModel
class ReviewAnalysis(BaseModel):
sentiment: str # "positive" / "negative" / "neutral"
score: int # 1〜5
summary: str # 要約(1文)
keywords: list[str] # キーワードのリスト
APIに渡して構造化出力を取得
from openai import OpenAI
client = OpenAI()
completion = client.beta.chat.completions.parse(
model="gpt-4o",
messages=[
{"role": "system", "content": "商品レビューを分析してください。"},
{"role": "user", "content": "配送が早くて助かりました。梱包も丁寧でした。"},
],
response_format=ReviewAnalysis,
)
result = completion.choices[0].message.parsed
print(result.sentiment) # → "positive"
print(result.score) # → 5
print(result.keywords) # → ["配送", "梱包", "丁寧"]
.parse() メソッドが Pydantic モデルを自動的にJSON Schemaに変換し、返ってきたJSONを型付きオブジェクトに戻してくれます。result.sentiment のようにドット記法でアクセスでき、IDE の補完も効きます。
実装方法②:LangChainで抽象化する
複数のLLMプロバイダに対応したい場合は、LangChainの with_structured_output() が便利です。OpenAI、Anthropic、Google など、プロバイダを問わず同じインターフェースで構造化出力を利用できます。
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
class ReviewAnalysis(BaseModel):
sentiment: str = Field(description="positive / negative / neutral")
score: int = Field(ge=1, le=5, description="1〜5の評価スコア")
summary: str = Field(description="レビューの要約(1文)")
keywords: list[str] = Field(description="キーワードのリスト")
llm = ChatOpenAI(model="gpt-4o")
structured_llm = llm.with_structured_output(ReviewAnalysis)
result = structured_llm.invoke("配送が早くて助かりました。梱包も丁寧でした。")
print(type(result)) # → <class 'ReviewAnalysis'>
print(result.score) # → 5
Ollamaに切り替える場合
from langchain_ollama import ChatOllama
llm = ChatOllama(model="llama3.2")
structured_llm = llm.with_structured_output(ReviewAnalysis)
# → ローカルLLMでも同じインターフェースで構造化出力が使える
LangChainの抽象化レイヤーを使えば、バックエンドのモデルを差し替えてもアプリケーションコードを変更する必要がありません。
実践例:レビュー一括分析パイプライン
構造化出力の真価は、複数データを一括処理してプログラムで集計するとき に発揮されます。
reviews = [
"配送が早くて助かりました。梱包も丁寧でした。",
"サイズが合わなかった。返品手続きが面倒。",
"普通です。可もなく不可もなく。",
"デザインが写真と全然違う。がっかりした。",
]
results = [structured_llm.invoke(r) for r in reviews]
# 平均スコアを計算
avg = sum(r.score for r in results) / len(results)
print(f"平均スコア: {avg}") # → 平均スコア: 2.75
# ネガティブレビューだけ抽出
negatives = [r for r in results if r.sentiment == "negative"]
print(f"ネガティブ件数: {len(negatives)}") # → ネガティブ件数: 2
フリーテキストの回答では不可能な 「LLMの出力を集計・フィルタリング・DB保存する」 といった後続処理が、構造化出力なら自然に書けます。
ベストプラクティス
① Field(description=...) で各フィールドに説明をつける
Pydanticの Field にdescriptionを書くと、それがJSON Schemaに含まれてLLMへの指示として機能します。フィールド名だけでは伝わらないニュアンスを補足できます。
class Article(BaseModel):
title: str = Field(description="記事タイトル。30文字以内")
body: str = Field(description="本文。Markdown形式")
tags: list[str] = Field(description="タグ。最大5個")
② Enumで選択肢を制限する
sentiment のような有限の選択肢は、Python の Enum や Literal で定義すると出力が安定します。
from typing import Literal
class ReviewAnalysis(BaseModel):
sentiment: Literal["positive", "negative", "neutral"]
score: int = Field(ge=1, le=5)
③ 必ずバリデーションを行う
構造化出力はスキーマ準拠率が非常に高いですが、ビジネスロジック上の妥当性 は保証されません。たとえば「sentimentがnegativeなのにscoreが5」といった矛盾が生じる可能性があります。Pydanticの model_validator で追加のバリデーションを入れるのがおすすめです。
シリーズとの繋がり
| シリーズ記事 | 構造化出力との関係 |
|---|---|
| RAG | 検索結果を構造化してLLMに渡すことで、回答の精度と一貫性が向上 |
| LangGraph | エージェントのStateに構造化出力を格納し、次のノードの判断材料にする |
| Ollama | LangChain経由でローカルLLMでも構造化出力を利用可能 |
| Dify | DifyのワークフローノードでJSON出力を指定し、後続ノードで活用 |
まとめ
| キーワード | 一言で言うと |
|---|---|
| 構造化出力 | LLMの出力をJSON Schemaに従わせる技術 |
| Pydantic | Pythonの型定義ライブラリ。スキーマ定義に最適 |
with_structured_output() |
LangChainの抽象化メソッド。プロバイダ非依存 |
| 制約付きデコーディング | スキーマ違反トークンの出力を抑制する仕組み |
| ベストプラクティス | description付与 + Enum/Literal + バリデーション |
構造化出力は「LLMをAPIとして組み込む」ための必須技術です。フリーテキストのパースに苦しんでいる方は、まずは1つのPydanticモデルを定義して .with_structured_output() を呼んでみてください。世界が変わります。
参考: