8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【中級者向け】LLMの構造化出力(Structured Output)実践ガイド ── AIの回答をJSONで受け取る技術

8
Posted at

はじめに

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 の EnumLiteral で定義すると出力が安定します。

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() を呼んでみてください。世界が変わります。


参考:

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?