この記事の結論(先読み)
- OpenAI Structured Outputs は
response_format={"type":"json_schema", ..., "strict": true}で 100%スキーマ準拠を文法レベルで保証。GPT-4o-2024-08-06以降で利用可能。 - Anthropic Tool Use は厳密な「strict mode」フラグこそないが、
tool_choiceで関数呼び出しを強制し、input_schemaで JSON Schema を渡せば実用上 99% 以上の精度で構造化出力が得られる。 - Pydantic v2(Python) と Zod(TypeScript) の
model_json_schema()/z.toJSONSchema()出力を直接 LLM に渡せる時代になった。Python 公式 SDK のclient.responses.parse(text_format=YourModel)や Vercel AI SDK のgenerateObject({ schema })がデファクト。 - Streaming + Structured は OpenAI が
stream=True+ Responses API のresponse.output_text.deltaで部分パース、Anthropic もinput_json_deltaイベントで途中受信可。両者ともパーシャル JSON は未完成のまま到達することを前提に設計する。 - エンタープライズ運用では「スキーマ差分管理 → CI で
$schema検証 → LLM 出力をアプリケーション側で 再 validate」の三層が必須。LLM 側の “strict” を信用しきらない。
はじめに:なぜ「文字列ストリーム」から「型」へ移行するのか
2024 年までの LLM アプリケーション開発で最も時間を奪っていた工程は、間違いなく「自然言語応答から JSON を取り出す」処理だった。プロンプトの末尾に「以下の JSON 形式で答えてください」と書き、温度を下げ、出力を json.loads() に食わせ、9 割は通るが 1 割は壊れて JSONDecodeError を吐く——この「JSON parsing roulette」が、エージェント開発の本質ではない部分で開発者の体力を削っていた。
2024 年 8 月 6 日、OpenAI が Structured Outputs を一般提供すると発表した時、AIエージェント設計の前提が静かに変わった。OpenAI の公式アナウンス(Introducing Structured Outputs in the API)は、評価セットにおける JSON Schema 準拠率を「GPT-4 の 35.9% から 100% に引き上げた」と明記している。これは出力後にバリデーションするのではなく、デコード段階で文法的に逸脱できないようにするという、根本的に異なるアプローチだ。
同じ頃、Anthropic Claude も Tool Use の精度を着実に高め、Claude 3.5 Sonnet 以降では input_schema に書いた JSON Schema をほぼ完全に守るようになった。両者の方法論はわずかに違うが、向かう先は同じ——LLM の出力を「文字列」ではなく「型」として扱う世界だ。
本稿では、OpenAI と Anthropic の Structured Outputs / Tool Use を、実装コード・Schema 設計マトリクス・Pydantic/Zod 連携・Streaming・エンタープライズ運用の 5 軸で深掘りする。情報源は OpenAI 公式ドキュメント(platform.openai.com / openai.com)、Anthropic 公式ドキュメント(docs.anthropic.com / anthropic.com)に限定し、コミュニティの噂レベルは排除する。
1. Structured Outputs と Tool Use — 用語と概念の整理
1-1. 「構造化出力」が指すもの
「構造化出力(Structured Output)」という用語は、ベンダーによって微妙に範囲が違う。整理するとこうなる。
| 用語 | OpenAI | Anthropic | 共通の意味 |
|---|---|---|---|
| JSON Mode | あり(旧来)response_format={"type":"json_object"} |
なし(プロンプトでJSON強制) | 「何らかのJSONを返す」だけ保証 |
| Structured Outputs | あり(2024-08-06〜)response_format={"type":"json_schema", "strict": true} |
Tool Use 経由で実質達成 | 「指定したJSON Schemaに100%準拠」 |
| Function Calling / Tool Use | tools=[...] + strict=true |
tools=[...] + tool_choice |
「特定の関数を呼ばせる」と「引数を構造化する」が一体化 |
重要なのは、「Structured Outputs」と「Tool Use」は技術的にほぼ同じことをしているという点だ。OpenAI の Structured Outputs は内部的には Function Calling の制約ロジック(Constrained Decoding)を、自由テキスト応答にも適用したものだ。Anthropic の Tool Use は、構造化データを返したい時に「ダミーの関数」を定義し、その引数として欲しい構造を受け取る、という設計になっている。
1-2. Constrained Decoding(制約付きデコード)— 内部で起きていること
OpenAI が「100% 準拠」を主張できる根拠は、出力時のトークンサンプリングを JSON Schema が許す範囲のトークンだけに制限しているからだ。スキーマを正規表現/文法ツリーに変換し、各ステップで「次に来てよいトークン集合」をマスクする。スキーマに違反するトークンは確率分布から除外されるため、文法的にスキーマを満たさない出力は原理的に生成されない。
これがプロンプトエンジニアリングと根本的に違う点だ。プロンプトは「お願い」だが、Constrained Decoding は「物理的不可能化」である。OpenAI のエンジニアリングブログでもこのアプローチが言及されており、JSON Schema を context-free grammar に変換してから sampling に適用していると説明されている。
2. OpenAI Structured Outputs 完全実装
2-1. Responses API(推奨・2026年現在のデファクト)
2024年後半に登場した Responses API は、Chat Completions API の上位互換として位置づけられ、Structured Outputs を最も自然に書ける。Python SDK では client.responses.parse() が用意され、Pydantic モデルを直接渡せる。
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import Literal
client = OpenAI()
class ResearchPaper(BaseModel):
title: str
authors: list[str]
publication_year: int = Field(ge=1900, le=2100)
venue: Literal["NeurIPS", "ICML", "ICLR", "ACL", "EMNLP", "other"]
key_findings: list[str] = Field(min_length=3, max_length=5)
citation_count: int | None = None
response = client.responses.parse(
model="gpt-4o-2024-08-06",
input=[
{"role": "system", "content": "あなたは学術論文の要約専門アシスタントです。"},
{"role": "user", "content": "Attention Is All You Need の構造化サマリーを返してください。"}
],
text_format=ResearchPaper,
)
paper: ResearchPaper = response.output_parsed
print(paper.title) # "Attention Is All You Need"
print(paper.authors) # ["Ashish Vaswani", ...]
print(paper.venue) # "NeurIPS"
このコードの肝は text_format=ResearchPaper の一行だ。SDK が Pydantic モデルから JSON Schema を自動生成し、response_format に strict: true で組み立て、返ってきた JSON を再度 Pydantic でパースして output_parsed に格納する。開発者は型を一度定義するだけで、リクエスト・レスポンス両方の型安全が手に入る。
2-2. Chat Completions API での生 JSON Schema 指定
レガシーシステムや細かい制御が必要な場面では、Chat Completions に直接 JSON Schema を渡す。
from openai import OpenAI
client = OpenAI()
schema = {
"type": "object",
"properties": {
"ticket_priority": {
"type": "string",
"enum": ["P0", "P1", "P2", "P3"]
},
"category": {
"type": "string",
"enum": ["bug", "feature_request", "question", "incident"]
},
"summary": {"type": "string", "maxLength": 200},
"suggested_owner": {
"type": "string",
"enum": ["frontend", "backend", "infra", "data", "qa"]
},
"estimated_hours": {"type": "number", "minimum": 0, "maximum": 80}
},
"required": ["ticket_priority", "category", "summary", "suggested_owner", "estimated_hours"],
"additionalProperties": False
}
completion = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "サポートチケットを分類してください。"},
{"role": "user", "content": "ログイン時に500エラーが断続的に発生。SREチームから問い合わせ。"}
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "ticket_classification",
"strict": True,
"schema": schema
}
}
)
import json
result = json.loads(completion.choices[0].message.content)
2-3. strict mode の 制約事項(落とし穴)
OpenAI の Structured Outputs(strict mode)は強力だが、JSON Schema の全機能をサポートしているわけではない。公式ドキュメント(platform.openai.com/docs/guides/structured-outputs)に記載された主要な制約を整理する。
| JSON Schema 機能 | strict mode サポート | 備考 |
|---|---|---|
type(string/number/integer/boolean/array/object/null) |
✅ | 基本型は全対応 |
enum |
✅ | 列挙値の制約 |
required |
⚠️ 全プロパティを必須化必須 | optionalにしたければ type: ["string","null"] でnull許容 |
additionalProperties |
⚠️ 必ず false | これを指定しないとエラー |
anyOf / oneOf |
✅ | union 型に使える |
$ref |
✅ | 再帰スキーマも可 |
minLength / maxLength / pattern |
❌ → ✅(後に対応) | 文字列制約は段階的にサポート拡大中 |
minimum / maximum |
段階的サポート | 数値制約は最新モデルで対応 |
| ネスト深さ | 最大 5 階層 | それ以上は分割設計 |
| プロパティ数 | 最大 100 | 大きすぎるスキーマは分割 |
最も引っかかりやすいのが「required に全プロパティを書け」と「additionalProperties は必ず false」の 2 点だ。Pydantic で Optional[str] を書くと自動的に "type": ["string","null"] + required から外れる挙動になるが、strict mode ではこれが拒否される。SDK の responses.parse() 経由なら自動的に required 化してくれるが、生 JSON Schema を組む場合は注意が必要だ。
3. Anthropic Tool Use と JSON Schema
3-1. 基本的な使い方
Anthropic Claude では、構造化データを得たい場合「実際には実行しないダミー関数」を Tool として定義し、その引数として欲しい構造を受け取る。Anthropic 公式ドキュメント(docs.anthropic.com/en/docs/build-with-claude/tool-use)の推奨パターンだ。
from anthropic import Anthropic
client = Anthropic()
extract_invoice_tool = {
"name": "extract_invoice",
"description": "請求書PDFから構造化データを抽出する",
"input_schema": {
"type": "object",
"properties": {
"invoice_number": {"type": "string"},
"issue_date": {"type": "string", "format": "date"},
"due_date": {"type": "string", "format": "date"},
"issuer": {
"type": "object",
"properties": {
"name": {"type": "string"},
"registration_number": {"type": "string"}
},
"required": ["name"]
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantity": {"type": "integer"},
"unit_price": {"type": "number"},
"amount": {"type": "number"}
},
"required": ["description", "amount"]
}
},
"subtotal": {"type": "number"},
"tax": {"type": "number"},
"total": {"type": "number"}
},
"required": ["invoice_number", "issue_date", "total", "line_items"]
}
}
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
tools=[extract_invoice_tool],
tool_choice={"type": "tool", "name": "extract_invoice"},
messages=[
{
"role": "user",
"content": "以下の請求書から情報を抽出してください:nn[請求書テキスト]"
}
]
)
# tool_use ブロックから構造化データを取り出す
for block in response.content:
if block.type == "tool_use" and block.name == "extract_invoice":
invoice_data = block.input
print(invoice_data["total"])
ポイントは tool_choice={"type": "tool", "name": "extract_invoice"}。これにより Claude は「必ずこの関数を呼ぶ」状態に強制される。普段は {"type": "auto"} でモデルが判断するが、構造化出力を取りたい場面では {"type": "tool", ...} で明示的に呼び出しを強制する。
3-2. tool_choice の 4 モード
| tool_choice | 挙動 | 使い所 |
|---|---|---|
{"type": "auto"}(デフォルト) |
Claude が必要と判断したら呼ぶ | 会話エージェント・自由会話 |
{"type": "any"} |
定義された tools の中から必ずどれか呼ぶ | 「何か必ずアクションする」エージェント |
{"type": "tool", "name": "X"} |
指定した特定 tool を必ず呼ぶ | 構造化出力・固定処理 |
{"type": "none"} |
tool を呼ばない(説明だけ要約) | tools を context として渡したい時 |
3-3. disable_parallel_tool_use の活用
Claude のデフォルトは複数 tool を並列に呼ぶ「parallel tool use」を許可しているが、構造化出力用途では 1 つの tool を確実に 1 回だけ呼んでほしい。その時は tool_choice に "disable_parallel_tool_use": True を加える。
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
tools=[extract_invoice_tool],
tool_choice={
"type": "tool",
"name": "extract_invoice",
"disable_parallel_tool_use": True
},
messages=[...]
)
3-4. Anthropic の「strict」相当機能
2026年5月現在、Anthropic の Tool Use には OpenAI の strict: true フラグに相当する明示的な強制モードはないが、実用上は以下の組み合わせで 99% 以上のスキーマ準拠率が得られる。
tool_choiceで特定 tool を強制呼出input_schemaに 明確な型・enum・description を書く(description は精度に効く)- System プロンプトで「
requiredフィールドは必ず埋めること」と明記 - 受信後にアプリケーション側で再 validation(後述)
Anthropic 公式の Tool use overview ドキュメントでも、構造化出力ユースケースに対しては「Tool Use を使ってください」と明確に案内されている。
4. Pydantic / Zod との連携 — 型ファーストの開発体験
4-1. Pydantic v2 と OpenAI
Python では Pydantic v2 がデファクトの型検証ライブラリだ。OpenAI SDK は Pydantic モデルをネイティブサポートしている。
from pydantic import BaseModel, Field, field_validator
from typing import Literal
from datetime import date
class Address(BaseModel):
postal_code: str = Field(pattern=r"^d{3}-d{4}$")
prefecture: str
city: str
street: str
class Customer(BaseModel):
customer_id: str
name: str
email: str = Field(pattern=r".+@.+..+")
age: int = Field(ge=0, le=150)
membership_tier: Literal["bronze", "silver", "gold", "platinum"]
address: Address
registration_date: date
@field_validator("name")
@classmethod
def name_not_empty(cls, v: str) -> str:
if not v.strip():
raise ValueError("name は空にできない")
return v
# 1. JSON Schema を取り出す(必要なら)
schema = Customer.model_json_schema()
# 2. SDK に丸ごと渡す
response = client.responses.parse(
model="gpt-4o-2024-08-06",
input=[{"role": "user", "content": "..."}],
text_format=Customer
)
customer: Customer = response.output_parsed # 型安全
Pydantic の Field(pattern=...) / Field(ge=..., le=...) などの制約は JSON Schema に変換され、最新の GPT-4o では一部反映される。一方、複雑なカスタムバリデーション(@field_validator)は LLM 側には伝わらないが、受信後の Pydantic 検証で必ず走るため、二重防御になる。これが「LLM の strict を信用しきらない」という設計思想に直結する。
4-2. Anthropic + Pydantic
Anthropic SDK は Pydantic ネイティブ統合がない(2026年5月時点)が、簡単に橋渡しできる。
from anthropic import Anthropic
from pydantic import BaseModel
client = Anthropic()
class TaskBreakdown(BaseModel):
project_name: str
tasks: list[dict] # 詳細は別モデル
estimated_total_hours: int
# Pydantic → JSON Schema
schema = TaskBreakdown.model_json_schema()
tool = {
"name": "submit_task_breakdown",
"description": "プロジェクトのタスク分解結果を返す",
"input_schema": schema
}
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
tools=[tool],
tool_choice={"type": "tool", "name": "submit_task_breakdown"},
messages=[{"role": "user", "content": "ECサイト改修プロジェクトをタスク分解してください"}]
)
for block in response.content:
if block.type == "tool_use":
# 受信後に Pydantic で再検証
result = TaskBreakdown.model_validate(block.input)
4-3. Zod(TypeScript)と Vercel AI SDK
TypeScript エコシステムでは Zod が事実上の標準だ。Vercel AI SDK の generateObject 関数は Zod スキーマを直接受け取れる。
import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
const ReviewSchema = z.object({
product_id: z.string(),
rating: z.number().int().min(1).max(5),
sentiment: z.enum(["positive", "neutral", "negative"]),
aspects: z.array(z.object({
aspect: z.string(),
sentiment: z.enum(["positive", "neutral", "negative"]),
excerpt: z.string()
})).min(1).max(5),
recommendation: z.boolean(),
summary: z.string().max(200)
});
type Review = z.infer;
const { object } = await generateObject({
model: openai("gpt-4o-2024-08-06"),
schema: ReviewSchema,
prompt: "以下のレビューを構造化してください: ..."
});
// object は Review 型として推論される
console.log(object.rating); // number
console.log(object.aspects); // Array
Vercel AI SDK は内部で Zod を JSON Schema に変換し、OpenAI / Anthropic / Google Gemini のそれぞれ Structured Outputs / Tool Use 形式に橋渡ししてくれる。プロバイダ間の差を抽象化できる点で、エンタープライズの「マルチプロバイダ戦略」と相性が良い。
4-4. Pydantic vs Zod — 機能比較マトリクス
| 機能 | Pydantic v2(Python) | Zod(TypeScript) | 備考 |
|---|---|---|---|
| JSON Schema 出力 | model_json_schema() |
z.toJSONSchema() / zod-to-json-schema |
両者とも draft-2020-12 ベース |
| OpenAI SDK ネイティブ統合 | ✅ responses.parse() |
✅ responses.parse()(Node SDK) |
2024年後半以降 |
| Anthropic SDK ネイティブ統合 | ❌(手動) | ❌(手動) | tool input_schema に変換 |
| Vercel AI SDK 統合 | —(TS用) | ✅ generateObject({ schema }) |
TSではZodがデファクト |
| ランタイム検証 | model_validate() |
schema.parse() / safeParse() |
必須機能 |
| 制約表現力 | 非常に高い | 非常に高い | refine/transform両者あり |
| パフォーマンス | Rust実装で高速 | 純TSだが十分速い | 大量データなら Pydantic 有利 |
| エラーメッセージ | 詳細 + i18n対応 | 詳細 + カスタマイズ可 | 本番のロギングで重要 |
5. Schema 設計マトリクス — 「壊れにくい」型を作る 7 原則
LLM 用の JSON Schema は、人間がアプリの内部 API 用に書く Schema とは設計指針が違う。LLM が「迷わない」設計が必要だ。経験則として有効な 7 原則を提示する。
原則 1: 全フィールドを required にする(OpenAI strict は必須)
Optional を作らない。任意値は type: ["string", "null"] で表現し、required に入れる。これにより LLM は「埋めるか null を入れるか」の二択を迫られ、フィールド忘れがなくなる。
原則 2: enum を最大限活用する
カテゴリ値・ステータス・優先度などは必ず enum 化する。フリーテキスト type: string にすると、LLM の表記揺れ(”high” / “High” / “HIGH” / “高”)でアプリ側が壊れる。
原則 3: description を「LLM に向けた」言葉で書く
JSON Schema の description フィールドは LLM が読む。「summary: 記事の要約」ではなく「summary: 記事全体を 150 字以内の日本語で要約。第三者視点で書く。固有名詞は省略しない」のように、出力ガイドラインを混ぜる。
原則 4: 配列に minItems/maxItems を書く
LLM は配列を 0 件で返すこともあれば 30 件返すこともある。期待件数は明示する。OpenAI strict ではバージョンにより minItems/maxItems サポート状況が違うため、サポートされない場合は description で補完する。
原則 5: ネストは 3 階層までを推奨
strict mode の上限は 5 階層だが、深いネストは LLM の精度を下げ、デバッグも難しい。3 階層を超える場合は別オブジェクトに分割し、ID で参照する設計に変える。
原則 6: 自由テキストフィールドは最後に置く
JSON 出力は左から右へ順に生成される。category: enum → priority: enum → notes: string の順番だと、enum で意思決定が固まった上で notes を書ける。逆だと notes に引きずられて enum が揺れる。
原則 7: スキーマに $schema / バージョンを付け、Git で履歴管理
スキーマは「データ契約」だ。本番運用では破壊的変更が事故を呼ぶ。$schema ファイル化 + Git 管理 + CI で jsonschema ライブラリによる検証を回す。Pydantic の場合は Python ファイルそのものが履歴になる。
6. Streaming + Structured Outputs — 部分的に構造化を受信する
6-1. なぜ Streaming + Structured が難しいか
JSON は構造上、最後の } が来るまで「完成した」状態にならない。途中で受信したテキストは構文的に不完全な JSON 断片だ。これを部分パースしながら UI に表示するには、専用のロジックがいる。
6-2. OpenAI の Streaming Structured Outputs
Responses API では stream=True でイベントを購読できる。Python SDK のヘルパー client.responses.stream() が便利だ。
with client.responses.stream(
model="gpt-4o-2024-08-06",
input=[{"role": "user", "content": "5件の本のおすすめを返して"}],
text_format=BookList
) as stream:
for event in stream:
if event.type == "response.refusal.delta":
print(f"[拒否] {event.delta}", end="")
elif event.type == "response.output_text.delta":
print(event.delta, end="", flush=True)
elif event.type == "response.completed":
final: BookList = event.response.output_parsed
print(f"n完了: {len(final.books)}件")
受信中の部分テキストは構文的に不完全な JSON 断片だ。それでも SDK は途中で partial オブジェクトとしてアクセスできる場合があり、進行表示に使える。ただし「途中の値はあてにならない(途切れた配列・未閉じの文字列を含む)」前提で扱う。
6-3. Anthropic の Streaming Tool Use
Anthropic は stream=True で input_json_delta イベントを送ってくる。これは Tool 引数の JSON 文字列の差分だ。
with client.messages.stream(
model="claude-sonnet-4-5",
max_tokens=4096,
tools=[my_tool],
tool_choice={"type": "tool", "name": "submit_report"},
messages=[...]
) as stream:
accumulated_json = ""
for event in stream:
if event.type == "content_block_delta":
if event.delta.type == "input_json_delta":
accumulated_json += event.delta.partial_json
# 部分パース(外部ライブラリ partial-json-parser など)
elif event.type == "message_stop":
final_message = stream.get_final_message()
部分 JSON のパースは partial-json-parser(npm)や Python の jsonstreams 等を使う。本番では「最初に title が確定したら UI 上のローディングをタイトル表示に切り替える」「最初のアイテムが完成したらリスト表示を始める」など、UX 改善に有効だ。
6-4. Streaming を諦めるべきケース
すべての場面で Streaming が必要なわけではない。以下のケースは Streaming を諦めて 同期受信のほうが運用が楽だ。
- 受信後に必ず Pydantic / Zod でフル検証する場合(部分受信に意味がない)
- DB 書き込み・別 API 呼出など、完成データを前提とする後続処理がある場合
- 応答が短い(200 トークン以下)場合(体感差がない)
- バッチ処理・cron ジョブ
7. エラーハンドリングとフィールドバリデーション
7-1. 失敗モードの分類
Structured Outputs / Tool Use が「失敗」する経路は複数ある。それぞれ対処法が違う。
| 失敗モード | 原因 | 対処 |
|---|---|---|
| JSON 構文エラー | strict mode 以外 / 古いモデル / Anthropicでまれに発生 | retry + temperature=0 |
| Schema 違反 | required 漏れ / enum 外の値 / 型違い | 受信後 Pydantic/Zod で validate → 失敗ならretry |
| 意味的に不正 | 「在庫数が負」「期日が過去」など型は正しいが業務的にNG | field_validator / refine で検出 |
| Refusal(拒否) | OpenAIが安全ポリシーで応答拒否 | response.output[0].refusal を読む |
| length finish_reason | max_tokens で途中切断 | max_tokensを増やす / 出力分割 |
| レート制限 / 障害 | API側 | exponential backoff + circuit breaker |
7-2. 「受信後に必ず再検証する」パターン
from pydantic import ValidationError
import logging
logger = logging.getLogger(__name__)
def extract_with_retry(text: str, max_retries: int = 3) -> Customer:
last_error = None
for attempt in range(max_retries):
try:
response = client.responses.parse(
model="gpt-4o-2024-08-06",
input=[{"role": "user", "content": f"以下から顧客情報を抽出: {text}"}],
text_format=Customer
)
# 1. Refusal チェック
if hasattr(response, "refusal") and response.refusal:
raise ValueError(f"Refused: {response.refusal}")
# 2. Pydantic は SDK で既に通っているが、明示的に再検証
customer = Customer.model_validate(
response.output_parsed.model_dump()
)
# 3. 業務ルール検証
if customer.age < 18 and customer.membership_tier == "platinum":
raise ValueError("18歳未満のプラチナ会員はあり得ない")
return customer
except ValidationError as e:
last_error = e
logger.warning(f"検証失敗 attempt={attempt}: {e}")
except Exception as e:
last_error = e
logger.error(f"その他エラー attempt={attempt}: {e}")
raise RuntimeError(f"3回失敗: {last_error}")
7-3. Anthropic 側のエラーチェック
response = client.messages.create(...)
# stop_reason をまず確認
if response.stop_reason == "max_tokens":
raise RuntimeError("出力が長すぎて切れた")
if response.stop_reason == "refusal":
raise RuntimeError("Claude が拒否")
# tool_use ブロックを取り出す
tool_use_block = next(
(b for b in response.content if b.type == "tool_use"),
None
)
if tool_use_block is None:
raise RuntimeError("tool_use ブロックなし — tool_choice 設定を見直す")
# Pydantic 検証
try:
result = TaskBreakdown.model_validate(tool_use_block.input)
except ValidationError as e:
# ここに来たら、LLM が input_schema を守らなかったということ
logger.error(f"Schema violation by Claude: {e}")
raise
8. エンタープライズ運用での型安全 — 5 つの実践
8-1. スキーマレジストリを持つ
本番運用では、複数チーム・複数サービスが同じスキーマを参照する。「schemas/v3/customer_extraction.py」のような形でスキーマを集中管理し、SemVer で版管理する。CI で破壊的変更を検出する仕組み(プロパティ削除・型変更・enum 値削除)を入れる。
8-2. LLM 入力スキーマと DB スキーマを分離する
LLM に渡すスキーマは「LLM が出しやすい」設計、DB スキーマは「正規化された」設計。両者を直結すると、片方の都合で両方が崩れる。アダプター層を挟み、LLM 出力 → アプリケーション内 DTO → DB エンティティ、と段階的に変換する。
8-3. 「黄金スキーマ」のリグレッションテスト
本番に出すスキーマには、過去の実データ 50〜200 件で「正解の出力」をスナップショット化しておく。スキーマ変更時はこのデータセットで LLM を叩き直し、出力差分が「意図した変更」か「リグレッション」かを人間がレビューする。これは Anthropic 自身も「Tool use accuracy evaluation」として推奨しているパターンだ。
8-4. 観測性(Observability)— 4 つの指標
| 指標 | 定義 | 目標値(参考) |
|---|---|---|
| Schema 準拠率 | 受信した出力がスキーマを満たした割合 | strict: 99.9% / Tool Use: 99%以上 |
| Field 充足率 | required フィールドが「null や空文字以外」で埋まった割合 | フィールド別に SLO 設定 |
| 業務ルール準拠率 | field_validator / refine を通過した割合 | 95% 以上 |
| Retry 率 | 1 回目で正解が取れた割合 | 90% 以上 |
これらは Datadog / OpenTelemetry / 自社ログに必ず仕込む。スキーマ違反が増えてきたら、モデル劣化・プロンプト drift・スキーマ自体の老朽化のいずれかが起きている。
8-5. プロバイダ抽象化レイヤー
2025-2026 年は OpenAI / Anthropic / Google Gemini / Mistral と、Structured Output を提供するプロバイダが増えた。マルチプロバイダ運用するなら、Vercel AI SDK や LangChain の with_structured_output() など、共通インターフェースに寄せる。これによりベンダーロックインを避けつつ、コスト最適化(タスクごとに安いモデルへ切替)が可能になる。
9. Function Calling から構造化データへの変換パターン
9-1. 「ダミー関数」パターン
Anthropic では既に説明した通り、「実行しない関数」を定義して引数として構造化データを受け取る。これは OpenAI でも有効なパターンだ。Function Calling のほうが Structured Outputs より「意図」が明確に伝わる場合がある。
9-2. 「複数候補から選ばせる」パターン
分類タスクは tools を複数定義し、tool_choice="auto" で「どれかを呼ばせる」と精度が出やすい。enum で 1 つのフィールドに収めるより、tool 自体を別にしたほうが LLM の意思決定境界が明確になる。
tools = [
{
"name": "categorize_as_bug",
"description": "バグ報告として分類する場合",
"input_schema": {
"type": "object",
"properties": {
"severity": {"type": "string", "enum": ["S0","S1","S2","S3"]},
"reproduction_steps": {"type": "array", "items": {"type": "string"}}
},
"required": ["severity", "reproduction_steps"]
}
},
{
"name": "categorize_as_feature_request",
"description": "機能要望として分類する場合",
"input_schema": {
"type": "object",
"properties": {
"use_case": {"type": "string"},
"estimated_users": {"type": "integer"}
},
"required": ["use_case"]
}
},
{
"name": "categorize_as_question",
"description": "ユーザーからの質問として分類する場合",
"input_schema": {
"type": "object",
"properties": {
"topic": {"type": "string"}
},
"required": ["topic"]
}
}
]
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "any"},
messages=[{"role": "user", "content": "サポートチケット本文..."}]
)
9-3. 「段階的に深掘り」パターン
複雑な構造化タスクは 1 回の API 呼出で完結させず、複数ステップに分ける。
- Step 1: ざっくり構造(カテゴリ・大枠の項目)だけ抽出
- Step 2: カテゴリに応じた詳細スキーマで再抽出
- Step 3: 業務ルール検証 + 不足分を人間にエスカレーション
1 回で全部やろうとすると、スキーマが肥大化し、LLM の注意が散る。分割すると、各ステップを安いモデル(Haiku / GPT-4o-mini)で回せる場合があり、コストも下がる。
10. ベンチマーク:プロンプト指示 vs Structured Outputs
「プロンプトに『JSON で返して』と書く」と「strict mode を使う」で、どれくらい差が出るのか。OpenAI 公式ブログ(前出 Introducing Structured Outputs)のデータが最も信頼できる。
| 手法 | JSON Schema 準拠率 |
|---|---|
| GPT-4(プロンプトのみ) | 35.9% |
| GPT-4o(プロンプトのみ) | 85% 前後(モデル進化で大幅改善) |
| GPT-4o-2024-08-06 + strict mode | 100% |
「100%」は OpenAI の評価セット上の数字であり、実運用での 100% を保証するものではない(ネットワーク切断やタイムアウトなどスキーマ以前の失敗はある)。それでも、文法的なスキーマ違反が ゼロにできる という性質は、エンタープライズの開発工数を劇的に減らす。
Anthropic はベンチマーク公開数値こそ控えめだが、Claude 3.5 Sonnet 以降の Tool Use 精度は実用上 99% 以上で安定しており、Anthropic Cookbook(公式)のサンプルでも同等の信頼性で利用されている。
11. 失敗パターン集 — 本番で踏んだ落とし穴
失敗 1: Optional だらけのスキーマで「ほぼ全部 null」が返る
Python で Optional[str] を多用すると、LLM は「迷ったら null」を選ぶ傾向が出る。本来抽出してほしい情報まで null になる。解決: 「不明な場合は "unknown" という文字列を入れる」のように、null 以外の選択肢を強制する。
失敗 2: enum 値に和英混在
enum: ["高", "中", "low"] のように混在させると LLM が混乱する。すべて同じ言語に統一する。多言語対応が必要なら、内部値は英語("high")にして、UI 表示の翻訳はアプリ側で行う。
失敗 3: description を書かない
「name: string」だけのスキーマと、「name: string(人物の正式氏名。敬称・肩書きは含めない。複数いる場合は最も中心的な一人)」のスキーマでは、後者の精度が圧倒的に高い。スキーマは LLM への指示書だ。
失敗 4: required 違反を strict 任せにする
OpenAI strict は文法レベルで required を保証するが、Anthropic は強制力が弱い。両者を抽象化した共通レイヤーを書く際、「strict があるから受信側検証は不要」と思い込むとプロバイダ切替時に事故る。必ず受信側で再検証する。
失敗 5: スキーマのバージョン管理を怠る
本番が読んでいるスキーマと、開発が書いているスキーマがずれる。マイクロサービスがそれぞれ違うバージョンを持って通信失敗する。SemVer + デプロイ時の互換性チェック を入れる。
失敗 6: Streaming で部分 JSON を信用する
Streaming 中に取得した「{"name": "山田"」を見て、後続処理を走らせると、後で {"name": "山田太郎"} に変わって不整合が起きる。Streaming はあくまで UX 改善用と割り切る。
失敗 7: 大きすぎるスキーマ
プロパティ 100 個・ネスト 5 段のスキーマを一発で渡すと、strict mode の上限に引っかかるか、引っかからなくても精度が下がる。Step 9-3 の段階的抽出に分割する。
12. 2026 年の展望 — Structured Outputs の進化
12-1. 出力スキーマの「動的化」
2025 年後半から、ユーザー入力に応じてスキーマ自体を動的生成するパターンが普及した。「アンケート設計エージェント」が回答スキーマを動的に生成し、それを次の LLM 呼出に渡す、という構成だ。スキーマ自体も型安全に管理する必要が出てくる(メタスキーマ)。
12-2. MCP(Model Context Protocol)との接続
Anthropic が提唱した MCP はツール定義の標準化を目指す。MCP サーバが提供する tools はそのまま Anthropic Tool Use にマッピングでき、構造化出力エコシステムが標準化に向かう。
12-3. マルチモーダル + Structured
画像入力 → 構造化 JSON 出力のユースケースが急増している(OCR 系・帳票処理・医療画像)。両プロバイダとも画像とテキストを同じスキーマ生成に統合できる。「請求書 PDF → 完全構造化 JSON」が 1 回の API 呼出で済む時代だ。
12-4. オープンソース側の追い上げ
Llama 3.x / Mistral 系も Outlines / lm-format-enforcer / Guidance などで Constrained Decoding が利用可能になっている。プロプライエタリ API に依存しない構造化出力もオプションになりつつある。
13. 関連記事
本記事と合わせて読むと理解が深まる Agent Lab の関連コンテンツ。
- OpenAI Agents SDK TypeScript / Python 徹底比較2026 — 本記事の Structured Outputs を Agents SDK の中で使うときの設計指針
- Claude Agent SDK Python 自律エージェント構築ガイド2026 — Anthropic Tool Use を Agent ループに組み込む実装
- MCP Tool Annotations と安全な権限設計2026 — MCP で公開する Tool のスキーマ設計と権限制御
14. まとめ — 型安全な LLM アプリの設計指針
本稿のエッセンスを 10 行に圧縮する。
- OpenAI Structured Outputs(strict)は文法レベルで 100% スキーマ準拠を保証する。最強。
- Anthropic Tool Use は明示的な「strict」フラグはないが、tool_choice + 明確な input_schema で 99% 以上の準拠率。
- Pydantic(Python)/ Zod(TypeScript)から JSON Schema を自動生成する流れがデファクト。手書きスキーマはほぼ不要。
- strict mode は「全フィールド required」「additionalProperties false」が必須。Optional は
type: [..., "null"]で表現。 - スキーマ設計 7 原則:全 required / enum 活用 / LLM向け description / 配列件数明示 / 3 階層まで / 自由テキストは後 / バージョン管理。
- Streaming は UX 改善用。部分 JSON で後続処理を走らせない。
- 受信後は必ず Pydantic / Zod で再検証。LLM 側の strict を 100% 信用しない。
- 失敗モードは 6 種類。それぞれに別の対処(retry / 再validate / refusal handling / backoff)。
- 本番運用は SLO 化:Schema 準拠率・Field 充足率・業務ルール準拠率・Retry 率の 4 指標。
- マルチプロバイダ抽象化(Vercel AI SDK 等)でベンダーロックインを避ける。
「JSON parsing roulette」の時代は終わった。これからのエージェント開発は 型ファーストで設計する。スキーマがエージェントの契約であり、契約が守られることがプロダクトの信頼性そのものになる。
15. 参考リンク(Tier 1 一次情報)
- OpenAI: Introducing Structured Outputs in the API
- OpenAI: Structured Outputs Guide (platform.openai.com)
- OpenAI: Function Calling Guide
- Anthropic: Tool use overview (docs.anthropic.com)
- Anthropic: Implement tool use
- Anthropic: Anthropic News (公式アナウンス)
- JSON Schema: JSON Schema 公式
この記事を読んで導入イメージが固まってきた方へ
UravationではAIエージェント導入の研修・コンサルを行っています。Structured Outputs を活用した型安全なエージェント設計、Pydantic / Zod を組み合わせた本番運用基盤の構築、マルチプロバイダ環境でのスキーマ統一など、エンタープライズの「壊れない LLM アプリ」構築をお手伝いします。
執筆: 佐藤傑(さとう・すぐる)。株式会社Uravation代表取締役。X(@SuguruKun_ai)フォロワー約10万人。著書『AIエージェント仕事術』。AIエージェントの設計・導入支援を専門とし、Structured Outputs を活用した型安全な本番運用設計を多数手掛ける。
