0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LangChainで構造化出力 -- AIの応答をJSONで確実に受け取る

0
Last updated at Posted at 2026-08-29

はじめに

LLMに「JSON形式で出力して」とお願いした経験はないでしょうか。しかし、実際にやってみると次のような問題に直面します。

  • キー名が「category」になったり「カテゴリ」になったりする(形式のゆれ)
  • 数値を期待したのに文字列で返される(型のゆれ)
  • 必須フィールドが抜けることがある(フィールドの欠落)
  • JSONの前後に説明文がつく(余分なテキスト)

LangChainの とPydanticを組み合わせれば、これらの問題を解決し、AIの応答を型安全に受け取れます。本記事ではその方法を解説します。

Pydanticで出力スキーマを定義する

PydanticはPythonの型ヒントを使ってデータモデルを定義し、型の検証と変換を自動で行うライブラリです。FastAPIでも広く使われている事実上の標準ライブラリと言えます。

まずは出力の構造をPydanticモデルで定義します。

from pydantic import BaseModel, Field

class InquiryAnalysis(BaseModel):
    """問い合わせ内容の分析結果"""
    category: str = Field(description="問い合わせのカテゴリ(請求、技術、返品、その他)")
    priority: str = Field(description="優先度: high, medium, low のいずれか")
    summary: str = Field(description="問い合わせ内容の要約(50文字以内)")

ここで重要なのが です。この説明文はLLMに渡され、AIが「どのフィールドに何を入れるべきか」を判断する手がかりになります。つまり、Pydanticのスキーマ定義は単なる型定義ではなく、AIへの出力指示書でもあるのです。

入れ子モデルと任意フィールド

実務では複雑なデータ構造が必要になることがよくあります。モデルを入れ子にしたり、任意フィールドを設定したりできます。

from typing import List, Optional
from pydantic import BaseModel, Field

class CustomerInfo(BaseModel):
    """顧客情報"""
    name: str = Field(description="顧客の氏名")
    email: str = Field(description="顧客のメールアドレス")
    is_premium: bool = Field(description="プレミアム会員かどうか")

class SupportTicket(BaseModel):
    """サポートチケット"""
    customer: CustomerInfo = Field(description="問い合わせた顧客の情報")
    category: str = Field(description="問い合わせのカテゴリ")
    priority: str = Field(description="優先度: high, medium, low")
    related_products: List[str] = Field(description="関連する商品名のリスト")
    summary: str = Field(description="問い合わせ内容の要約")
    note: Optional[str] = Field(default=None, description="追加メモ(なければ省略可)")

と を使えば任意フィールドに、 でリスト型を表現できます。これで複雑な構造も簡潔に定義できます。

with_structured_outputで構造化出力チェーンを作る

LangChain 0.3系では、 メソッドにPydanticモデルを渡すだけで、LLMの出力がPydanticのインスタンスとして返ってきます。

from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field

class InquiryAnalysis(BaseModel):
    """問い合わせ内容の分析結果"""
    category: str = Field(description="問い合わせのカテゴリ(請求、技術、返品、その他)")
    priority: str = Field(description="優先度: high, medium, low のいずれか")
    summary: str = Field(description="問い合わせ内容の要約(50文字以内)")

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
structured_llm = llm.with_structured_output(InquiryAnalysis)

result = structured_llm.invoke("先月の請求額が二重に引き落とされているようです。至急確認してください。")

print(type(result))     # <class 'InquiryAnalysis'>
print(result.category)  # 請求
print(result.priority)  # high
print(result.summary)   # 先月の請求額が二重に引き落とされているとの問い合わせ

JSON文字列を自分でパースする必要はありません。LLMが返したJSONがそのままPydanticのインスタンスに変換されています。

プロンプトテンプレートとの組み合わせ

LCELのパイプ演算子 を使えば、プロンプトの組み立てから構造化出力までを一つのチェーンにまとめられます。

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "あなたは感情分析の専門家です。与えられたテキストの感情を分析し、指定された形式で出力してください。"),
    ("human", "以下のテキストの感情を分析してください。

テキスト: {text}")
])

class SentimentResult(BaseModel):
    """感情分析の結果"""
    sentiment: str = Field(description="感情: positive, negative, neutral のいずれか")
    confidence: float = Field(description="確信度(0.0-1.0)")
    reason: str = Field(description="その判定理由(30文字以内)")

structured_llm = llm.with_structured_output(SentimentResult)
chain = prompt | structured_llm

result = chain.invoke({"text": "今日買ったケーキが想像以上に美味しくて、思わず笑顔になった。"})
print(result.sentiment)   # positive
print(result.confidence)  # 0.95
print(result.reason)      # 美味しいケーキで喜びを感じているため

複数件のデータを一度に抽出する

リストを含むモデルを定義すれば、一度の呼び出しで複数件のデータを構造化して取り出せます。

class EventInfo(BaseModel):
    """個別の事件情報"""
    name: str = Field(description="事件名")
    location: str = Field(description="発生場所")
    date: str = Field(description="発生日(YYYY年MM月DD日形式)")

class EventList(BaseModel):
    """事件情報のリスト"""
    events: List[EventInfo] = Field(description="抽出した事件情報のリスト")

prompt = ChatPromptTemplate.from_messages([
    ("system", "あなたはニュース記事の分析を行うアシスタントです。記事から事件情報を抽出してください。"),
    ("human", "以下の記事から事件情報を抽出してください。

記事: {article}")
])

structured_llm = llm.with_structured_output(EventList)
chain = prompt | structured_llm

article = "3日午後、新宿区で商店街の火事が発生。同日夕方、大阪市でビル解体工事中の事故が報告。"
result = chain.invoke({"article": article})

for event in result.events:
    print(f"{event.name} / {event.location} / {event.date}")

出力方式の選択とエラー対処

method引数で方式を切り替える

は背後で「関数呼び出し(function calling)」または「JSONモード」を使っています。 引数で明示的に指定可能です。

# 関数呼び出し方式(デフォルト・推奨)
structured_llm = llm.with_structured_output(SentimentResult, method="function_calling")

# JSONモード方式(関数呼び出し非対応モデル向け)
structured_llm = llm.with_structured_output(SentimentResult, method="json_mode")

基本的はデフォルトのままで問題ありません。利用するモデルが関数呼び出しをサポートしていない場合のみ を指定します。

スキーマ不合致時のエラーハンドリング

LLMがスキーマに合わない出力をした場合は が発生します。

from pydantic import ValidationError

try:
    result = chain.invoke({"text": "感情が読み取れないテキスト"})
except ValidationError as e:
    print("出力の検証に失敗しました")
    print(e)

エラーが頻発する場合は、プロンプトの指示をより明確にするか、必須フィールドを任意フィールドに変更するのが有効です。

モデル間の互換性 -- 同じスキーマを使い回せる

はOpenAI・Anthropic・Googleのいずれのモデルでも動作します。モデルを切り替えてもPydanticスキーマとチェーン構造はそのまま流用できます。

from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain_google_genai import ChatGoogleGenerativeAI

models = {
    "openai": ChatOpenAI(model="gpt-4o-mini", temperature=0),
    "anthropic": ChatAnthropic(model="claude-3-5-sonnet-20241022", temperature=0),
    "google": ChatGoogleGenerativeAI(model="gemini-1.5-flash", temperature=0),
}

for name, model in models.items():
    structured_llm = model.with_structured_output(SentimentResult)
    chain = prompt | structured_llm
    result = chain.invoke({"text": "素晴らしいサービスに感謝しています。"})
    print(f"{name}: {result.sentiment}")

プロバイダごとに異なるAPI仕様を意識する必要はなく、LangChainが背後で差を吸収してくれます。これがLangChainを使う大きな利点の一つです。

まとめ

  • Pydanticの はAIへの出力指示として機能する
  • にPydanticモデルを渡すだけで、LLMの出力が型安全なインスタンスとして返る
  • LCELのパイプ演算子でプロンプトテンプレートと構造化出力を一つのチェーンにまとめられる
  • リストを含むモデルで複数件のデータ抽出が可能
  • OpenAI・Anthropic・Googleのどのモデルでも同じスキーマを使い回せる

構造化出力を活用すれば、LLMの応答をそのまま後続の処理(データベース保存、API呼び出しなど)に渡せるようになります。ぜひ実際のプロジェクトで試してみてください。

読者特典(無料)

本記事のような実装パターンを横断的にまとめたチートシートと、Obsidian・Notionのテンプレート集を無料配布しています。8冊の内容から「何をどのツールでやるか」の判断チャートも含みます。

🎁 読者特典を受け取る(無料・メール登録)


著者: 葉山悠希

参考: LangChain x AI実践入門(Amazon)(Kindle・読み放題対象)

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?