LLM アプリ開発 #5 構造化された出力を受け取る

読了 5分

第4回では、プロンプトで出力形式を絞る方法を見ました。ところが「一語だけで答えて」とお願いしても、モデルがときどき文で答えることがあります。アプリでその結果をコードで直接扱うには、形式がぶれては困ります。今回は出力形式を強制して、受け取った結果をそのままコードに差し込んで使う方法を扱います。

プロンプトだけでは足りない #

第4回の分類の例を思い出してください。「ポジティブ、ネガティブ、中立のいずれかだけで答えて」と指示しました。たいていはきちんと従いますが、モデルがまれに「ややネガティブです」のような文で答えることもあります。人が見るぶんには問題ありませんが、コードがその結果をこう使っていたらどうでしょう。

if answer == "ネガティブ":
    flag_review(review)

答えが「ネガティブ」ではなく「ややネガティブです」で来たら、この条件はそのまま外れます。一度でも形式がずれれば処理が壊れます。アプリで結果を機械的に使うには、形式が100%保証されている必要があります。

構造化された出力(structured output)がこの問題を解決します。応答を、自分が定めた JSON スキーマに必ず合わせるよう強制する機能です。モデルはその形式から外れることができません。

Pydantic で出力形式を定義する #

Python で最も楽な方法は、望む構造を Pydantic モデルで定義して messages.parse に渡すことです。応答が検証済みのオブジェクトとして返ってきます。

structured_pydantic.py
from typing import Literal
from pydantic import BaseModel
import anthropic

class Review(BaseModel):
    sentiment: Literal["ポジティブ", "ネガティブ", "中立"]
    summary: str
    score: int

client = anthropic.Anthropic()

response = client.messages.parse(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": "次のレビューを分析してください: 配送は速かったのですが製品が不良品でした。返金は受けました。",
    }],
    output_format=Review,
)

review = response.parsed_output
print(review.sentiment)  # 必ずポジティブ/ネガティブ/中立のいずれか
print(review.score)      # 整数

response.parsed_output は検証を終えた Review インスタンスです。sentiment は必ず三つのいずれかで、score は整数です。JSON 文字列を自分でパースしたり形式を検査したりする必要はありません。モデルが形式を守ったか心配せずに、オブジェクトの属性をそのまま取り出して使えます。

スキーマを直接書く #

Pydantic を使わないなら、output_config に JSON スキーマを直接入れます。

structured_schema.py
import json

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": "次のレビューを分析してください: 配送は速かったのですが製品が不良品でした。返金は受けました。",
    }],
    output_config={
        "format": {
            "type": "json_schema",
            "schema": {
                "type": "object",
                "properties": {
                    "sentiment": {"type": "string", "enum": ["ポジティブ", "ネガティブ", "中立"]},
                    "summary": {"type": "string"},
                    "score": {"type": "integer"},
                },
                "required": ["sentiment", "summary", "score"],
                "additionalProperties": False,
            },
        },
    },
)

text = next(b.text for b in response.content if b.type == "text")
data = json.loads(text)
print(data["sentiment"])

結果の最初の text ブロックが、スキーマに従う有効な JSON です。enum に入れた値だけが出て、required で指定したフィールドはすべて含まれます。ですから json.loads のあとにそのままキーでアクセスしても安全です。

テキストから構造化されたデータを取り出す #

構造化された出力が本当に力を発揮するのは、自由形式のテキストを整ったデータに変えるときです。メール、議事録、レビューのように人が書いた文から、必要な情報だけを取り出して構造に収められます。入れ子のオブジェクトやリストも扱えます。

extract_tasks.py
from typing import Literal
from pydantic import BaseModel

class Task(BaseModel):
    title: str
    priority: Literal["高", "中", "低"]

class MeetingNotes(BaseModel):
    summary: str
    tasks: list[Task]

notes = """今日の会議で来四半期のリリース日程を決めた。
デザイン案は来週までに急いで仕上げることにし、
QA 人員の増員は時間のあるときにゆっくり調べることにした。"""

response = client.messages.parse(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": f"次の議事録を整理してください:\n\n{notes}"}],
    output_format=MeetingNotes,
)

result = response.parsed_output
for task in result.tasks:
    print(f"[{task.priority}] {task.title}")

自由に書かれた議事録が、summary 一行と tasks のリストに整って返ってきます。各 task はタイトルと優先度を持つオブジェクトで、優先度は決めておいた三つの値のいずれかです。こうした取り出しを手で正規表現や文字列処理で書くのは厄介ですが、構造化された出力ならモデルが一度で解決します。

注記
スキーマには制約があります。数値の最小・最大値や文字列の長さといった細かい制約、そして再帰構造はサポートされません。Python と TypeScript の SDK はサポートされない制約を自動で取り除き、クライアント側で検証するので、まずは Pydantic で自然に書けば大丈夫です。構造化された出力は claude-opus-4-8claude-sonnet-4-6claude-haiku-4-5 で使えます。

よくつまずくところ #

  • JSON が途中で切れるmax_tokens が小さいと、JSON が閉じる前に切れてパースに失敗します。構造化された出力では、結果の長さを見積もって余裕をもって取ります。
  • 拒否のときは形式が保証されない — モデルが安全上の理由で答えを拒否すると(stop_reasonrefusal)、スキーマを守らないことがあります。ユーザー入力を扱うアプリなら、この場合を別途処理します。
  • スキーマを変えすぎる — 新しいスキーマは最初の呼び出しで一度コンパイルの費用がかかり、同じスキーマは24時間キャッシュされます。毎回スキーマを少しずつ変えると、このキャッシュを使えません。スキーマは固定して使うほうがよいです。

まとめ #

今回は出力形式を強制する構造化された出力を扱いました。

  • プロンプトは形式を絞るだけで保証はしませんが、構造化された出力はスキーマに合うよう強制します。
  • Python では Pydantic モデルと messages.parse で検証済みのオブジェクトをそのまま受け取ります。
  • Pydantic なしで output_config に JSON スキーマを直接入れることもできます。
  • 自由形式のテキストから、入れ子のオブジェクトやリストを含む構造化されたデータを取り出せます。

ここまでは、Claude がテキストや整ったデータを「答え」として返す流れでした。次回の「LLM アプリ開発 #6 ツール呼び出しで外部機能を連携」では、向きが変わります。Claude に自分たちが定義した関数を直接呼び出させて、検索やデータベース、外部 API といった外の世界とつなげます。

X