llm-structured-output について
LLMの応答から型付きの検証済みデータを抽出する開発者向けスキルです。各プロバイダーの構造化出力、スキーマ検証、再試行と、後続処理の安全策を解説します。
主な機能
- 01OpenAI JSON Schema出力
- 02Anthropic tool_use抽出
- 03Gemini responseSchema
- 04ローカル制約付きデコード
- 05スキーマ検証と再試行
- 06本番環境向けの安全策
活用シーン
- 01テキストからJSON項目を抽出
- 02検証済みLLMデータをコードに渡す
- 03形式不正や項目不足の出力を修正
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ sickn33/ agentic-awesome-skills --skill llm-structured-outputインストール後、Claude Code で/llm-structured-outputを入力して読み込まれたか確認します。
詳細
LLM の構造化出力
このスキルでできること
自由記述のテキストを解析する代わりに、LLM API のレスポンスから型付けされ、検証済みのデータを抽出します。このスキルでは、主な3つの方法を扱います。OpenAI の JSON Schema を使った response_format、構造化抽出に使う Anthropic の tool_use ブロック、Gemini における Google の responseSchema です。それぞれの方法が機能する場合と失敗する場合、そして本番システムで必ず遭遇するスキーマ検証エラーに対する再試行ロジックの構築方法を学びます。
このスキルを使う場面
- ユーザーが LLM のレスポンスから構造化データ(JSON オブジェクト、配列、列挙型)を抽出したい
- LLM の出力をコードに直接渡すパイプライン(データベースへの書き込み、API 呼び出し、UI のレンダリング)をユーザーが構築している
- ユーザーが OpenAI の
response_format、json_mode、json_object、json_schemaについて尋ねている - ユーザーがデータ抽出のために Anthropic の
tool_useまたはtool_resultブロックを使う方法について尋ねている(実際のツール実行ではない) - ユーザーが
openainpm パッケージのzodResponseFormat()を使った Zod スキーマについて尋ねている - ユーザーが
instructor、marvin、または手動の検証を使って、LLM の出力を Pydantic モデルに解析する必要がある - ユーザーが LLM のレスポンスから不正な JSON、フィールドの欠落、または型の誤りが生じる問題を抱えており、解決方法を必要としている
- ユーザーがローカルモデルにおける
controlled generation、constrained decoding、またはgrammar-based samplingについて尋ねている
次の場合は、このスキルを使用しないでください。
- ユーザーが自由形式のテキスト生成(要約、エッセイ、チャット)を求めている
- ユーザーがフォーム検証または API 入力検証のための Zod について尋ねている(代わりに
zod-validation-expertを使用してください) - ユーザーがテキストの品質を高めるためのプロンプトエンジニアリングを必要としている(構造化が目的ではない)
- ユーザーが実際の外部ツールや API を呼び出したい(このスキルは構造化出力の裏技として
tool_useを使う方法を扱うものであり、実際のツールのオーケストレーションは扱いません)
例では過去のモデル ID と SDK ヘルパーのパスを使っています。デプロイ前に利用可能かどうかを確認し、サポート対象の SDK とモデルを固定してください。厳密な出力への対応状況とスキーマのサブセットは、プロバイダーや API によって異なります。スキーマに適合した値も信頼できないデータであり、データベースへの書き込みや外部呼び出しの承認にはなりません。ソース文書の指示は抽出の指示と分離し、許可する操作はアプリケーションコードで強制してください。
基本的なワークフロー
-
対象のスキーマを特定します。抽出する必要があるフィールドをユーザーに尋ねます。各フィールドについて、型、必須か任意か、該当する場合は有効な列挙値を定義します。具体的なスキーマが定まるまで先に進まないでください。
-
プロバイダーに適した方法を選択します。
- OpenAI(gpt-4o、gpt-4o-mini):
response_format: { type: "json_schema", json_schema: { ... } }を使用します。これにより、制約付きデコーディングによるスキーマ準拠が保証された Structured Outputs が有効になります。 - Anthropic(Claude): 対象のスキーマを
input_schemaとして持つ単一のツールを定義し、tool_choice: { type: "tool", name: "extract_data" }を設定します。Claude はtool_useコンテンツブロックで構造化データを返します。 - Google(Gemini): JSON Schema オブジェクトを使って
generationConfig.responseSchemaを設定し、responseMimeType: "application/json"を指定します。 - ローカルモデル(llama.cpp、vLLM): GBNF 文法または
--json-schemaフラグを使い、トークンレベルで制約付きデコーディングを行います。
- OpenAI(gpt-4o、gpt-4o-mini):
-
ユーザーの言語でスキーマ定義を記述します。Python では、Pydantic の
BaseModelを定義します。TypeScript では、Zod スキーマを定義し、zodResponseFormat()で変換します。生の API 呼び出しでは、JSON Schema を直接記述します。 -
スキーマにフィールドごとの説明を含めます。すべてのフィールドに、そのフィールドに何を入れるかをモデルに伝える
description文字列を指定します。モデルはこうした説明を暗黙のプロンプト指示として利用します。説明のないsentiment: strよりも、"The user's sentiment as positive, negative, or neutral"と説明されたフィールドのほうが、より良い結果が得られます。 -
構造を徹底するようシステムプロンプトを設定します。モデルの役割は会話ではなくデータ抽出であると伝えます。例:
"You are a data extraction system. Analyze the input and return the requested fields. Do not include explanations outside the JSON structure." -
OpenAI の
json_schemaモードを使用する場合は、スキーマ定義に"strict": trueを設定します。これにより制約付きデコーディングが有効になり、モデルはスキーマに準拠するトークンのみを出力できます。strict: trueがないと、無効な JSON が出力される場合があります。 -
Anthropic の tool_use アプローチを使用する場合は、
response.contentからtype == "tool_use"であるブロックを見つけ、そのinputフィールドを読み取って構造化データを抽出します。テキストブロックは解析しないでください。構造化データは tool_use ブロックにのみ含まれています。 -
アプリケーションコードで、スキーマに照らしてレスポンスを検証します。制約付きデコーディングを使っている場合でも、データを後続処理に渡す前に、Pydantic の
model_validate()または Zod の.parse()で検証します。これにより、スキーマ準拠だけでは防げない意味上の問題(空文字列や範囲外の数値など)を検出できます。 -
検証に失敗したときに再試行するループを作成します。検証に失敗した場合は、元の入力、失敗した出力、検証エラーをモデルに送り返し、
"Your previous output failed validation: {error}. Fix the output."のような指示を添えます。再試行は最大 3 回に制限します。 -
スキーマとモデルのバージョン、リクエスト ID、検証結果、編集済みの診断用サンプルを記録します。元の入力やレスポンスを保存するのは、許可があり必要な場合に限り、アクセス制御と保持期間の制限を適用します。シークレットや機密性の高いソース文書を日常的に記録しないでください。本番環境で構造化出力に問題が発生した際、これらのログがあれば、スキーマ設計、プロンプト、モデルのリグレッションのいずれが原因かを特定できます。
例
例 1:Pydantic を使った OpenAI Structured Outputs(Python)
from pydantic import BaseModel, Field
from openai import OpenAI
from enum import Enum
class Sentiment(str, Enum):
positive = "positive"
negative = "negative"
neutral = "neutral"
class ReviewAnalysis(BaseModel):
sentiment: Sentiment = Field(description="Overall sentiment of the review")
key_topics: list[str] = Field(max_length=5, description="Main topics mentioned, max 5")
purchase_intent: bool = Field(description="Whether the reviewer would buy again")
confidence_score: float = Field(ge=0.0, le=1.0, description="Model confidence 0-1")
client = OpenAI()
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "Extract structured review analysis."},
{"role": "user", "content": "This laptop is amazing. The battery lasts forever and the keyboard feels great. Definitely buying the next version."}
],
response_format=ReviewAnalysis,
)
message = response.choices[0].message
if message.refusal or message.parsed is None:
raise ValueError("Refused or incomplete extraction; do not write downstream")
result = message.parsed
# result.sentiment == Sentiment.positive
# result.key_topics == ["battery life", "keyboard"]
# result.purchase_intent == True
例 2:構造化抽出のための Anthropic tool_use(Python)
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
system="You are a data extraction system. Use the provided tool to return structured data.",
tools=[{
"name": "extract_invoice",
"description": "Extract invoice fields from text",
"input_schema": {
"type": "object",
"properties": {
"vendor_name": {"type": "string", "description": "Company that issued the invoice"},
"total_amount": {"type": "number", "description": "Total amount in USD"},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantity": {"type": "integer"},
"unit_price": {"type": "number"}
},
"required": ["description", "quantity", "unit_price"]
}
}
},
"required": ["vendor_name", "total_amount", "line_items"]
}
}],
tool_choice={"type": "tool", "name": "extract_invoice"},
messages=[{"role": "user", "content": "Invoice from Acme Corp: 3x Widget A at $10 each, 1x Widget B at $25. Total: $55."}]
)
# Find the tool_use block — do NOT parse text blocks
blocks = [b for b in response.content if b.type == "tool_use" and b.name == "extract_invoice"]
if response.stop_reason != "tool_use" or len(blocks) != 1:
raise ValueError("Missing, ambiguous or truncated extraction")
invoice = blocks[0].input # Validate against the invoice schema before any downstream use.
# invoice["vendor_name"] == "Acme Corp"
# invoice["total_amount"] == 55.0
例 3:Zod + zodResponseFormat を使った TypeScript
import OpenAI from "openai";
import { z } from "zod";
import { zodResponseFormat } from "openai/helpers/zod";
const EventSchema = z.object({
event_name: z.string().describe("Name of the event"),
date: z.string().describe("ISO 8601 date string"),
location: z.string().describe("City and venue"),
attendee_count: z.number().int().describe("Expected number of attendees"),
is_virtual: z.boolean().describe("Whether the event is online-only"),
});
const client = new OpenAI();
const completion = await client.beta.chat.completions.parse({
model: "gpt-4o-2024-08-06",
messages: [
{ role: "system", content: "Extract event details from the text." },
{ role: "user", content: "Tech Summit 2025 in Austin at the Convention Center on March 15th. Expecting 2000 attendees, in-person only." },
],
response_format: zodResponseFormat(EventSchema, "event_extraction"),
});
const message = completion.choices[0].message;
if (message.refusal || !message.parsed) throw new Error("Refused or incomplete extraction");
const event = EventSchema.parse(message.parsed);
// event.event_name === "Tech Summit 2025"
// event.is_virtual === false
絶対にやってはいけないこと
-
スキーマなしで
response_format: { type: "json_object" }を絶対に使わないでください。 これは OpenAI のレガシーな JSON モードです。有効な JSON 構文は保証しますが、スキーマへの準拠は保証しません。{"name": str, "age": int}を期待していたのに、モデルが{"result": "hello"}を返すことがあります。代わりに、完全なスキーマ定義を伴うjson_schemaを常に使用してください。 -
構造化データのために Anthropic のテキストブロックを絶対に解析しないでください。
tool_choiceを使って構造化出力を強制する場合、データはtool_useコンテンツブロックにあり、どのtextブロックにもありません。response.content[0].textを解析すると、空文字列か会話の前置きが返るだけであり、必要なデータは決して得られません。 -
説明のないスキーマフィールドを絶対に定義しないでください。 説明のない
statusという名前のフィールドは、HTTP ステータス、注文ステータス、レビューステータスのいずれを意味するかわかりません。モデルはフィールドの説明を抽出指示として使用します。これを省略することは、プロンプトの半分を省略することと同じです。 -
strict モードのスキーマで
additionalProperties: trueを絶対に使わないでください。 OpenAI の strict モードでは、スキーマ内のすべてのオブジェクトにadditionalProperties: falseが必要です。これを true に設定したり省略したりすると、API は応答時ではなく 400 エラーでリクエストを拒否します。つまり、応答をまったく受け取れなくなります。 -
抽出指示をシステムプロンプトではなくユーザーメッセージにだけ絶対に置かないでください。 システムプロンプトは、動作に関する指示に対してより高いアテンション重みを持ちます。「以下のフィールドを抽出してください」をソーステキストとともにユーザーメッセージにだけ置くと、モデルは指示とデータの間でアテンションを分割せざるを得なくなります。システムプロンプトは動作を定義し、ユーザーメッセージは入力データを提供します。
-
構造化出力が正しい出力を意味すると絶対に仮定しないでください。 制約付きデコーディングは、応答がスキーマの型と構造に一致することを保証します。値が正しいことは保証しません。ソーステキストが曖昧な場合、モデルはネガティブなレビューに対して
{"sentiment": "positive"}を返すことがあります。スキーマ検証の後、アプリケーションコードで必ずセマンティクスを検証してください。 -
テストなしで再帰的または深くネストされたスキーマを絶対に使わないでください。 再帰型(同じ定義を指す
$ref)や 3 レベルより深いスキーマは、デコーディングのレイテンシを大幅に増加させ、モデルが JSON 構造を完成させる前に max_tokens に達する確率を高めます。ネストされたスキーマは可能な限りフラット化してください。
エッジケース
-
コンテキストウィンドウを超える長いソーステキスト。 入力テキストが長すぎると、モデルが読み取りを途中で打ち切り、不完全な抽出結果を返す場合があります。長いドキュメントはチャンクに分割し、各チャンクから個別に抽出してから、アプリケーションコードで結果を統合してください。50ページのドキュメントを1回の呼び出しで処理できるとモデルに期待しないでください。
-
モデルが構造化データの代わりに
refusalを返す。 OpenAIの構造化出力では、モデルがリクエストを安全でないと判断した場合に、refusalフィールドが返されることがあります。.parsedにアクセスする前に、response.choices[0].message.refusalを確認してください。refusalがNoneでない場合、解析済みデータはNoneとなり、アクセスするとエラーが発生します。 -
データが存在するのに配列フィールドが空で返される。 ソーステキストにデータが含まれていても、フィールドの説明が曖昧すぎると、モデルが配列フィールドに
[]を返すことがあります。説明を具体的に指定して修正してください。例:"テキストで言及されているすべての商品名のリスト。商品が参照されている場合は、少なくとも1つを返すこと。"。 -
大文字・小文字の違いにより列挙値が一致しない。 列挙型を
["Active", "Inactive"]と定義しているのに、モデルが"active"を返すと、検証に失敗します。列挙値をすべて小文字にするか、検証前に正規化する処理を追加してください。OpenAIの厳格モードでは大文字・小文字が正確に区別されますが、Anthropicではそうでない場合があります。 -
構造化出力をストリーミングする場合。 OpenAIは、部分的なJSONがチャンクごとに届く構造化出力のストリーミングに対応しています。途中のチャンクを有効なJSONとして解析することはできません。
openaiSDKの組み込み部分解析を使用するか、ストリームが完了するまでチャンクをバッファーに蓄積してください。Anthropicのツール入力はinput_json_deltaの断片として届きます。ブロックインデックスごとにpartial_jsonを蓄積し、content_block_stopの後に解析するか、SDKの最終メッセージアキュムレーターを使用してください。
ベストプラクティス
-
問題を解決できる最もシンプルなスキーマから始めます。 3~5個のフィールドを持つフラットなオブジェクトは、20個以上のフィールドを持つネストされたスキーマよりも高い精度が得られます。複雑なデータが必要な場合は、2回に分けて抽出します。まず最上位のエンティティを抽出し、次にエンティティごとに詳細を抽出するための呼び出しを行います。
-
カテゴリーデータには自由形式の文字列ではなく、列挙型を使用します。
mood: strというフィールドでは、どのような値でも返される可能性があります。mood: Literal["happy", "sad", "neutral", "angry"]というフィールドでは、モデルがこれらの値だけを返すよう制約できます。これにより、後続の解析ロジックを一切不要にできます。 -
本番環境ではモデルのバージョンを固定します。
gpt-4oは、OpenAIが新しいバージョンをリリースすると変更されるエイリアスです。構造化出力の動作はバージョン間で変わる可能性があります。gpt-4o-2024-08-06を明示的に使用し、意図的にアップグレードするまでスキーマとプロンプトの組み合わせを安定させてください。 -
デプロイ前に、実際の入力を20件以上使ってスキーマの変更をテストします。 スキーマの変更(フィールドの追加、型の変更、説明の修正)により、以前は正しく処理できていた入力の抽出が失敗することがあります。期待される出力を設定した実際の入力を使ってテストスイートを作成し、スキーマを変更するたびに実行してください。これは構造化出力におけるユニットテストに相当します。
-
オプションのフィールドには、Pydanticモデルで
default値を使用します。 ソーステキストに関連データが含まれていない場合があるフィールドには、プロバイダースキーマで必須かつnull許容のフィールド(例:str | Noneまたは.nullable())を使用し、未知の事実にはnullを返すよう指示してください。OpenAIの厳格なスキーマでは、すべてのプロパティをrequiredに含める必要があります。キーが存在しないことと、値がnullであることは同じではありません。検証後にのみ、nullをアプリケーションのデフォルト値にマッピングしてください。 -
抽出用スキーマとアプリケーション用スキーマを分けます。 LLMの抽出用スキーマは、モデルが確実に生成できる形式にしてください。アプリケーションのデータベーススキーマには、計算フィールド、外部キー、制約などを追加できます。両者のマッピングはアプリケーションコードで行い、LLMにデータベーススキーマを理解させようとしないでください。
制限事項
- このスキルは、タスクが上記のスコープに明確に一致する場合にのみ使用してください。
- 出力を、環境固有の検証、テスト、専門家によるレビューの代替として扱わないでください。
- 必要な入力、権限、安全境界、成功基準が欠けている場合は、作業を中断して確認を求めてください。