本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
LLMのJSON Schemaによる出力検証:プロンプト設計と評価
大規模言語モデル(LLM)は自然言語処理において高い能力を発揮しますが、その出力を構造化された形式で安定して得ることは、アプリケーション開発において重要な課題です。本記事では、LLMからJSON Schemaに準拠した出力を得るためのプロンプト設計、検証、評価、そして改良のサイクルについて詳細に解説します。
ユースケース定義
LLMがJSON Schemaに準拠した構造化データを出力する典型的なユースケースは以下の通りです。
顧客問い合わせの分類と情報抽出: 顧客からのテキスト問い合わせから、問い合わせID、顧客名、メールアドレス、件名、カテゴリ、優先度、詳細内容などの情報をJSON形式で抽出する。
レビューの感情分析と属性抽出: 製品レビューテキストから、感情(肯定的/否定的)、対象製品、評価点、改善点などをJSON形式で抽出する。
ドキュメントの要約とキーワード抽出: 長文ドキュメントから、要約文、主要キーワードリスト、関連エンティティなどをJSON形式で抽出する。
これらのユースケースでは、抽出された情報を後続のシステム(データベース、BIツール、CRMなど)で利用するために、一貫性のある構造が求められます。
入出力契約
LLMを活用したJSON Schema出力検証における入出力契約を以下に定義します。
入力: LLMへの自然言語プロンプト(指示、質問、抽出対象テキストなど)。
成功時の出力: 指定されたJSON Schemaに厳密に準拠した単一のJSON文字列。前後に不要なテキスト(例: 説明文、マークダウンのバッククォート囲い)を含まない。
失敗時の挙動:
JSON形式ではない場合: LLMからの出力全体を無効なものとして処理し、後処理でJSONパースエラーとして検出する。
有効なJSONだがスキーマに不適合な場合: JSON Schema検証エラーとして検出する。エラーの詳細(どのフィールドが、どのような理由で不適合か)を特定し、必要に応じてLLMへの再試行やユーザーへの通知を行う。
モデルの内部エラー、API接続エラーなど: システムレベルのエラーとして処理する。
禁止事項:
JSON文字列の先頭や末尾に、
json`やなどのマークダウン記法を含めない。Schemaで定義されていない追加のルートレベルフィールドを含めない。
スキーマで指定された型、形式、列挙値、範囲などの制約に違反する値を生成しない。
制約付き仕様化
LLMに期待するJSON出力の構造は、JSON Schemaを用いて具体的に定義します。これにより、出力のデータ型、必須フィールド、値の範囲、列挙値などを厳密に指定できます。以下に、顧客問い合わせ情報抽出のユースケースで用いるJSON Schemaの例を示します。
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Customer Inquiry",
"description": "顧客からの問い合わせ情報を抽出するスキーマ。",
"type": "object",
"properties": {
"inquiry_id": {
"type": "string",
"description": "問い合わせを識別するユニークなID。",
"pattern": "^CUST-[0-9]{3}-[0-9]{8}$"
},
"customer_name": {
"type": "string",
"description": "顧客の名前。"
},
"email": {
"type": "string",
"format": "email",
"description": "顧客のメールアドレス。"
},
"subject": {
"type": "string",
"description": "問い合わせの件名。"
},
"category": {
"type": "string",
"enum": ["billing", "technical_support", "product_inquiry", "other"],
"description": "問い合わせのカテゴリ。"
},
"priority": {
"type": "integer",
"minimum": 1,
"maximum": 5,
"description": "問い合わせの優先度(1が最高、5が最低)。"
},
"details": {
"type": "string",
"description": "問い合わせの詳細内容。"
},
"attachments": {
"type": "array",
"items": {
"type": "string"
},
"description": "添付ファイル名のリスト。",
"default": []
}
},
"required": ["inquiry_id", "customer_name", "email", "subject", "category", "details"]
}
このスキーマでは、inquiry_id には正規表現 (pattern) を、email には format: "email" を、category には列挙値 (enum) を、priority には数値範囲 (minimum, maximum) をそれぞれ適用し、厳密なデータ制約を設けています。
プロンプト設計
LLMにJSON Schemaに準拠した出力を生成させるためのプロンプトは、様々なアプローチが考えられます。ここでは、3種類のプロンプト案を提示します。共通のSystemプロンプトとして「あなたは入力されたテキストから指定されたJSONスキーマに厳密に準拠したJSONオブジェクトを生成するアシスタントです。余分なテキストやマークダウンは一切含まず、JSONオブジェクトのみを出力してください。」をモデルに事前に与えることを想定します。
1. ゼロショットプロンプト
JSON Schemaを直接プロンプトに含め、追加の例なしでモデルに出力を求めます。
## 指示
以下の顧客からの問い合わせ文を解析し、指定されたJSONスキーマに沿って情報を抽出してください。
## JSONスキーマ
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Customer Inquiry",
"description": "顧客からの問い合わせ情報を抽出するスキーマ。",
"type": "object",
"properties": {
"inquiry_id": { "type": "string", "description": "問い合わせを識別するユニークなID。", "pattern": "^CUST-[0-9]{3}-[0-9]{8}$" },
"customer_name": { "type": "string", "description": "顧客の名前。" },
"email": { "type": "string", "format": "email", "description": "顧客のメールアドレス。" },
"subject": { "type": "string", "description": "問い合わせの件名。" },
"category": { "type": "string", "enum": ["billing", "technical_support", "product_inquiry", "other"], "description": "問い合わせのカテゴリ。" },
"priority": { "type": "integer", "minimum": 1, "maximum": 5, "description": "問い合わせの優先度(1が最高、5が最低)。" },
"details": { "type": "string", "description": "問い合わせの詳細内容。" },
"attachments": { "type": "array", "items": { "type": "string" }, "description": "添付ファイル名のリスト。", "default": [] }
},
"required": ["inquiry_id", "customer_name", "email", "subject", "category", "details"]
}
## 顧客問い合わせ文
件名: 請求に関するお問い合わせ
お客様名: 山田 太郎
メールアドレス: taro.yamada@example.com
メッセージ: 先月の請求書に誤りがあるようです。2024年6月分のサービス利用料が二重に計上されています。確認をお願いします。
優先度: 高
問い合わせID: CUST-001-20240729
2. 少数例プロンプト
JSON Schemaに加えて、正確な出力例をいくつか提示し、モデルが学習できるようにします。
## 指示
以下の顧客からの問い合わせ文を解析し、指定されたJSONスキーマに沿って情報を抽出してください。以下の「例」を参考にしてください。
## JSONスキーマ
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Customer Inquiry",
"description": "顧客からの問い合わせ情報を抽出するスキーマ。",
"type": "object",
"properties": {
"inquiry_id": { "type": "string", "description": "問い合わせを識別するユニークなID。", "pattern": "^CUST-[0-9]{3}-[0-9]{8}$" },
"customer_name": { "type": "string", "description": "顧客の名前。" },
"email": { "type": "string", "format": "email", "description": "顧客のメールアドレス。" },
"subject": { "type": "string", "description": "問い合わせの件名。" },
"category": { "type": "string", "enum": ["billing", "technical_support", "product_inquiry", "other"], "description": "問い合わせのカテゴリ。" },
"priority": { "type": "integer", "minimum": 1, "maximum": 5, "description": "問い合わせの優先度(1が最高、5が最低)。" },
"details": { "type": "string", "description": "問い合わせの詳細内容。" },
"attachments": { "type": "array", "items": { "type": "string" }, "description": "添付ファイル名のリスト。", "default": [] }
},
"required": ["inquiry_id", "customer_name", "email", "subject", "category", "details"]
}
## 例1
### 顧客問い合わせ文
件名: パスワードリセットについて
お客様名: 佐藤 花子
メールアドレス: hanako.sato@example.jp
メッセージ: アカウントのパスワードを忘れてしまいました。リセット手続きをお願いします。
優先度: 中
問い合わせID: CUST-002-20240729
### 抽出結果
{
"inquiry_id": "CUST-002-20240729",
"customer_name": "佐藤 花子",
"email": "hanako.sato@example.jp",
"subject": "パスワードリセットについて",
"category": "technical_support",
"priority": 3,
"details": "アカウントのパスワードを忘れてしまいました。リセット手続きをお願いします。",
"attachments": []
}
## 例2
### 顧客問い合わせ文
件名: 新製品に関する質問
お客様名: 田中 健太
メールアドレス: kenta.tanaka@example.net
メッセージ: 御社の新製品「XYZデバイス」について詳細な情報が知りたいです。特に、API連携の有無が気になります。カタログや仕様書はありますか?
優先度: 低
問い合わせID: CUST-003-20240729
添付ファイル: カタログ請求書.pdf
### 抽出結果
{
"inquiry_id": "CUST-003-20240729",
"customer_name": "田中 健太",
"email": "kenta.tanaka@example.net",
"subject": "新製品に関する質問",
"category": "product_inquiry",
"priority": 4,
"details": "御社の新製品「XYZデバイス」について詳細な情報が知りたいです。特に、API連携の有無が気になります。カタログや仕様書はありますか?",
"attachments": ["カタログ請求書.pdf"]
}
## 顧客問い合わせ文
件名: 請求に関するお問い合わせ
お客様名: 山田 太郎
メールアドレス: taro.yamada@example.com
メッセージ: 先月の請求書に誤りがあるようです。2024年6月分のサービス利用料が二重に計上されています。確認をお願いします。
優先度: 高
問い合わせID: CUST-001-20240729
3. Chain-of-Thought制約型プロンプト
モデルに思考プロセスを明示させ、その後にJSON出力を求めることで、より構造化された思考を促し、出力品質を高めます。
## 指示
以下の顧客からの問い合わせ文を解析し、指定されたJSONスキーマに沿って情報を抽出してください。回答は思考プロセスをまず書き、その後に最終的なJSONオブジェクトを出力してください。
## JSONスキーマ
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Customer Inquiry",
"description": "顧客からの問い合わせ情報を抽出するスキーマ。",
"type": "object",
"properties": {
"inquiry_id": { "type": "string", "description": "問い合わせを識別するユニークなID。", "pattern": "^CUST-[0-9]{3}-[0-9]{8}$" },
"customer_name": { "type": "string", "description": "顧客の名前。" },
"email": { "type": "string", "format": "email", "description": "顧客のメールアドレス。" },
"subject": { "type": "string", "description": "問い合わせの件名。" },
"category": { "type": "string", "enum": ["billing", "technical_support", "product_inquiry", "other"], "description": "問い合わせのカテゴリ。" },
"priority": { "type": "integer", "minimum": 1, "maximum": 5, "description": "問い合わせの優先度(1が最高、5が最低)。" },
"details": { "type": "string", "description": "問い合わせの詳細内容。" },
"attachments": { "type": "array", "items": { "type": "string" }, "description": "添付ファイル名のリスト。", "default": [] }
},
"required": ["inquiry_id", "customer_name", "email", "subject", "category", "details"]
}
## 顧客問い合わせ文
件名: 請求に関するお問い合わせ
お客様名: 山田 太郎
メールアドレス: taro.yamada@example.com
メッセージ: 先月の請求書に誤りがあるようです。2024年6月分のサービス利用料が二重に計上されています。確認をお願いします。
優先度: 高
問い合わせID: CUST-001-20240729
## 思考プロセス
1. 問い合わせIDを特定: 問い合わせ文中に「問い合わせID: CUST-001-20240729」とあるため、これを抽出する。パターン `^CUST-[0-9]{3}-[0-9]{8}$` に適合しているか確認。
2. 顧客名を特定: 問い合わせ文中に「お客様名: 山田 太郎」とあるため、これを抽出する。
3. メールアドレスを特定: 問い合わせ文中に「メールアドレス: taro.yamada@example.com」とあるため、これを抽出する。`format: "email"` に適合しているか確認。
4. 件名を特定: 問い合わせ文中に「件名: 請求に関するお問い合わせ」とあるため、これを抽出する。
5. カテゴリを特定: 問い合わせの件名およびメッセージ内容から、請求に関する問題であると判断できる。JSONスキーマの `enum` に定義されている "billing" を選択する。
6. 優先度を特定: 問い合わせ文中に「優先度: 高」とあるため、JSONスキーマの `enum` に従い整数値の `1` を選択する。`minimum: 1`, `maximum: 5` の範囲内か確認。
7. 詳細内容を抽出: 問い合わせ文の「メッセージ」部分を詳細内容として抽出する。
8. 添付ファイルを特定: 問い合わせ文中に添付ファイルに関する言及がないため、空の配列 `[]` を設定する。
9. 上記で特定した情報をJSONスキーマの `required` フィールドが全て満たされていることを確認し、JSONオブジェクトを構築する。
## 抽出結果
(ここにJSON出力)
評価
LLMのJSON出力の品質を評価するために、多様なシナリオと自動評価の仕組みを準備します。
評価シナリオ
正例: 全ての情報が明確に記述されており、スキーマに完全に準拠する出力が期待される典型的な問い合わせ文。
難例:
情報不足: 優先度が明記されていない、特定のフィールドが欠落しているなど。
曖昧な情報: カテゴリが複数示唆される、解釈が難しい表現が含まれるなど。
形式不正: メールアドレスのフォーマットが誤っている、数値が文字列として記載されているなど。
複雑な入力: 長文で情報が散在している、複数の要求が混在している。
コーナーケース:
空の入力: 問い合わせ文が空の場合。
無関係な入力: 全く関係のないテキストが与えられた場合。
特殊文字: 抽出されるべき値に、JSONのパースを困難にする特殊文字(引用符、バックスラッシュなど)が含まれる場合。
自動評価の擬似コード
Pythonのjsonschemaライブラリを用いて、LLMの出力を自動で検証・採点する擬似コードを示します。
import json
from jsonschema import validate, ValidationError, FormatChecker
# JSONスキーマを読み込む
# (上記で定義したinquiry_schemaをPython辞書として用意)
inquiry_schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Customer Inquiry",
"description": "顧客からの問い合わせ情報を抽出するスキーマ。",
"type": "object",
"properties": {
"inquiry_id": { "type": "string", "description": "問い合わせを識別するユニークなID。", "pattern": "^CUST-[0-9]{3}-[0-9]{8}$" },
"customer_name": { "type": "string", "description": "顧客の名前。" },
"email": { "type": "string", "format": "email", "description": "顧客のメールアドレス。" },
"subject": { "type": "string", "description": "問い合わせの件名。" },
"category": { "type": "string", "enum": ["billing", "technical_support", "product_inquiry", "other"], "description": "問い合わせのカテゴリ。" },
"priority": { "type": "integer", "minimum": 1, "maximum": 5, "description": "問い合わせの優先度(1が最高、5が最低)。" },
"details": { "type": "string", "description": "問い合わせの詳細内容。" },
"attachments": { "type": "array", "items": { "type": "string" }, "description": "添付ファイル名のリスト。", "default": [] }
},
"required": ["inquiry_id", "customer_name", "email", "subject", "category", "details"]
}
def evaluate_llm_output(llm_output: str, expected_schema: dict, golden_data: dict = None) -> dict:
"""
LLMの出力を評価する関数。
入力:
llm_output (str): LLMから得られたJSON文字列。Chain-of-Thoughtプロンプトの場合、JSON部分を抽出する必要がある。
expected_schema (dict): 期待されるJSON Schema。
golden_data (dict, optional): 比較対象となる正解データ。内容の正確性評価に利用。
出力:
dict: 評価結果と採点。
計算量: JSONのパースはO(N_json), スキーマ検証はO(N_json * N_schema_rules) (入力JSONサイズとスキーマの複雑さに比例)。
メモリ条件: 入力JSONとスキーマ、オプションでゴールデンデータを保持するためのメモリ。O(N_json + N_schema + N_golden)
"""
score = 0
errors = []
is_valid_json = False
is_schema_compliant = False
parsed_json = None
# Chain-of-Thoughtプロンプトの場合、```json```ブロックからJSONを抽出する
import re
json_match = re.search(r'```(?:json)?\s*({.*?})\s*```', llm_output, re.DOTALL)
if json_match:
json_string_to_parse = json_match.group(1)
else:
json_string_to_parse = llm_output # マークダウンブロックがない場合は全体をパース試行
# 1. JSON形式のチェック (10点)
try:
parsed_json = json.loads(json_string_to_parse)
score += 10
is_valid_json = True
except json.JSONDecodeError as e:
errors.append(f"JSONパースエラー: {e}")
return {"score": score, "errors": errors, "is_valid": False, "is_schema_compliant": False}
# 2. JSON Schema準拠のチェック (20点)
if is_valid_json:
try:
validate(instance=parsed_json, schema=expected_schema, format_checker=FormatChecker())
score += 20
is_schema_compliant = True
except ValidationError as e:
errors.append(f"JSONスキーマ検証エラー: {e.message} (Path: {'.'.join(map(str, e.path))})")
return {"score": score, "errors": errors, "is_valid": True, "is_schema_compliant": False}
# 3. 内容の正確性チェック (70点) - golden_dataが存在する場合
if golden_data and is_schema_compliant:
content_score = 0
total_fields = 0
correct_fields = 0
for key, expected_value in golden_data.items():
if key in parsed_json:
total_fields += 1
if parsed_json[key] == expected_value:
correct_fields += 1
else:
errors.append(f"フィールド '{key}' の値が不正確です。期待値: {expected_value}, 実際: {parsed_json[key]}")
elif key in expected_schema.get("required", []):
errors.append(f"必須フィールド '{key}' が出力に存在しませんでした (スキーマ検証で検出済みであるべき)。")
# 必須でないフィールドが出力になくてもエラーとしない
if total_fields > 0:
content_score = (correct_fields / total_fields) * 70
score += content_score
return {
"score": score,
"errors": errors,
"is_valid": is_valid_json,
"is_schema_compliant": is_schema_compliant,
"output": parsed_json
}
# --- 採点ルーブリック ---
# - 10点: 有効なJSON形式であること。
# - 20点: JSON Schemaに完全に準拠していること(型、必須フィールド、enum、format、pattern、minimum/maximumなど)。
# - 70点: 抽出された情報の内容が正解データと一致すること(フィールドごとの正確性に応じて配分)。
# 合計100点
誤り分析
LLMのJSON出力検証で発生する主な失敗モードは以下の通りです。
幻覚 (Hallucination): LLMが元の入力テキストに存在しない情報を生成し、JSONフィールドに含めてしまう。
- 例: 問い合わせ文に記載のない「会社の所在地」を生成する。
様式崩れ (Malformation):
無効なJSON構文: JSONとしてパースできない出力(例: カンマの欠落、閉じ括弧の不足、キーや値の引用符の誤り)。
スキーマ不適合:
requiredフィールドの欠落。指定されたデータ型(
string、integerなど)と異なる値の出力。enumで定義されていない値を出力(例:categoryが”technical”ではなく”support”と出力される)。formatやpatternに準拠しない値の出力(例: 無効なメールアドレス形式、inquiry_idが正規表現パターンに合致しない)。minimum/maximumなどの数値制約違反(例:priorityが0や6と出力される)。
脱線 (Off-topic/Jailbreak): JSON文字列の前後に追加の自然言語テキストや、無関係な情報が出力される。また、LLMがJSON形式を無視して全く異なる形式で応答してしまうケースも含む。
改良
誤り分析で特定された失敗モードに対する抑制手法を適用し、プロンプトとシステム設計を改良します。
System指示の明確化:
- 「余分なテキストやマークダウンは一切含まず、JSONオブジェクトのみを出力してください」といった指示をSystemプロンプトで強調し、モデルの出力形式を厳しく制約します。
モデル機能の活用:
JSON Mode: OpenAI APIの
response_format: { type: "json_object" }や類似の機能を利用します。これにより、モデルは有効なJSONを生成しようと努めるため、JSON構文エラーが大幅に減少します[2]。Function Calling / Structured Output: Google Cloud Vertex AI の
tool_config.function_calling_config.schemaやoutput_schemaのように、JSON Schemaを直接モデルのAPIに渡して厳密な構造化出力を強制する機能を利用します[1]。これはスキーマ不適合の最も効果的な抑制手法です。ソース: Google Cloud Generative AI Docs, “Structured output with function calling” (2024年4月11日最終更新), Google Cloud. URL
ソース: OpenAI Docs, “JSON Mode” (2024年5月13日最終更新), OpenAI. URL
プロンプトの改善:
少数例学習の強化: 失敗したシナリオに近い、より具体的で多様な正例を追加します。
Chain-of-Thoughtの調整: 思考プロセスにおいて、各フィールドの制約(enum、patternなど)への言及を促し、モデルが制約を意識して思考するように誘導します。
後処理とリトライ戦略:
Pythonの
jsonschemaライブラリによる厳密な検証を必須とします。検証に失敗した場合、エラーメッセージ(例: 「’category’フィールドは’billing’,’technical_support’,’product_inquiry’,’other’のいずれかである必要があります」)をLLMにフィードバックし、再生成を促すリトライループを実装します。この際、リトライ回数を制限し、無限ループを防ぎます。
Pydantic / Instructorの利用: PydanticはPythonオブジェクトからJSON Schemaを生成し、JSON出力をPythonオブジェクトにパース・検証する強力な機能を提供します[4]。Instructorライブラリはこれをさらに発展させ、OpenAI APIなどと統合し、Pydanticモデルを直接
response_modelとして渡すことで、スキーマ準拠の出力と自動リトライメカニズムを簡単に実装できます[5]。ソース: Pydantic Docs, “Using Pydantic with LLMs” (2024年7月23日最終更新), Pydantic. URL
ソース: Instructor GitHub Repository (2024年7月28日最終コミット), Jxnl. URL
温度 (Temperature) の調整: 構造化出力のような決定論的なタスクでは、モデルの創造性を抑制し、より一貫した出力を得るために、
temperatureパラメータを低い値(例: 0.0〜0.3)に設定します。
再評価
改良されたプロンプトやシステム設定を用いて、再び評価シナリオを実行します。特に、前回の評価で失敗した難例やコーナーケースを中心に再テストし、自動評価スクリプトでスコアの向上とエラーの減少を確認します。この反復的な評価と改良のループによって、LLMの出力品質を段階的に改善していきます。
まとめ
LLMからJSON Schemaに準拠した構造化出力を得ることは、堅牢なAIアプリケーションを構築する上で不可欠です。本記事では、ユースケース定義から始まり、厳密な入出力契約とJSON Schemaによる仕様化、そしてゼロショット、少数例、Chain-of-Thoughtといった多様なプロンプト設計手法を提示しました。
LLMの出力品質を評価するためには、正例、難例、コーナーケースを網羅した評価シナリオと、jsonschemaライブラリを用いた自動評価の仕組みが有効です。幻覚や様式崩れ、脱線といった失敗モードを特定し、System指示の明確化、JSON ModeやFunction Callingといったモデル固有機能の活用、そしてPydanticやInstructorのような強力なライブラリを用いた後処理とリトライ戦略を通じて、これらの課題を効果的に抑制できます。
プロンプト設計、評価、誤り分析、そして改良という一連のサイクルを継続的に回すことで、LLMの構造化出力の信頼性と正確性を最大化し、実用性の高いAIソリューションを実現することが可能となります。
graph TD
A["プロンプト設計"] --> |プロンプトを送信| B{"LLMモデル"};
B -- |JSON出力| C["JSON Schema検証"];
C -- |検証成功| D["処理完了"];
C -- |検証失敗| E["誤り分析とログ記録"];
E --> |改善指示| F["プロンプト/システム改良"];
F --> |再試行/新しいプロンプト| B;

コメント