本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
LLMの入出力契約設計とJSON Schema
大規模言語モデル(LLM)をシステムに組み込む際、その出力が常に意図した構造と形式を持つとは限りません。特に自動化されたワークフローでは、LLMの自由な発話ではなく、厳密に定義されたデータフォーマットが必要です。本記事では、LLMの入出力契約をJSON Schemaを用いて設計し、堅牢なシステムを構築するためのプロンプト設計、評価、および誤り分析の手法について詳述します。
ユースケース定義
LLMの構造化出力を必要とする典型的なユースケースは以下の通りです。
WebアプリケーションのAPIレスポンス生成: ユーザーの自然言語クエリから、バックエンドAPIが期待するJSON形式のデータ(例: 商品検索フィルター、予約情報)を生成。
データ分析レポートの要約と構造化: 長文のレポートから、主要な指標、結論、推奨事項などを特定のキーを持つJSONオブジェクトとして抽出。
自動化ワークフローにおけるパラメータ抽出: ユーザーの指示(例: 「来週の金曜日に佐藤さんに会議室Aを予約して」)から、日時、担当者、リソースなどの予約システムが要求するパラメータをJSON形式で抽出。
入出力契約の定義
LLMの入出力契約は、信頼性の高いシステム統合のために不可欠です。
1. フォーマット
出力形式: 常にJSON形式であること。
スキーマ定義: JSON Schema [1] を用いて、出力されるJSONオブジェクトの構造、必須フィールド、データ型、制約(例: 最大長、最小値、列挙型)を厳密に定義します。これにより、LLMの出力がプログラムで容易に解析可能であることを保証します。
2. 失敗時の挙動
スキーマ不適合: LLMの出力が定義されたJSON Schemaに適合しない場合、システムはこれをエラーとして処理し、後続の処理を停止します。
エラー通知: 不適合の詳細(どのフィールドが、どのような理由で不適合か)をログに記録し、開発者または運用者に通知します。
リトライ戦略: スキーマ不適合が発生した場合、特定の回数までプロンプトを再送信するリトライ戦略を実装することを推奨します。その際、失敗理由をLLMにフィードバックし、修正を促す再プロンプトを用いると効果的です。
3. 禁止事項
個人情報(PII)の不適切な出力: ユーザーの同意なく個人を特定できる情報(氏名、住所、電話番号など)をJSON出力に含めることを禁止します。
機密情報の漏洩: 企業秘密、認証情報、その他システム運用上機密とされる情報を出力に含めることを禁止します。
不適切なコンテンツ: 差別的、暴力的、性的な表現など、公序良俗に反する内容の出力を禁止します。
制約付き仕様化(JSON Schemaの活用)
LLMの出力構造をJSON Schemaで定義することで、期待するデータ形式をモデルに明確に伝達し、またその後のバリデーションを自動化できます。
例: イベント予約システムでのパラメータ抽出
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Event Reservation",
"description": "Extracts event reservation details from user query.",
"type": "object",
"properties": {
"event_name": {
"type": "string",
"description": "Name of the event or meeting."
},
"attendees": {
"type": "array",
"description": "List of attendees for the event.",
"items": {
"type": "string"
},
"minItems": 1
},
"start_time": {
"type": "string",
"format": "date-time",
"description": "Start time of the event in ISO 8601 format (e.g., 2024-03-29T10:00:00+09:00)."
},
"end_time": {
"type": "string",
"format": "date-time",
"description": "End time of the event in ISO 8601 format (e.g., 2024-03-29T11:00:00+09:00)."
},
"location": {
"type": "string",
"description": "Physical location or meeting room."
},
"is_tentative": {
"type": "boolean",
"description": "True if the reservation is tentative, false otherwise. Default is false."
}
},
"required": ["event_name", "attendees", "start_time", "end_time"]
}
このようなJSON Schemaをプロンプト内で提供することで、LLMは自身の応答をこの構造に沿って生成するように誘導されます。特にOpenAIのFunction Calling [2] やGoogleのFunction Calling [3] など、最新のLLMにはJSON Schemaを直接渡して、それに従った構造化出力を生成させる機能が組み込まれています。
PythonのPydanticライブラリ [4] を利用すると、Pythonの型ヒントでデータモデルを定義し、そこからJSON Schemaを自動生成し、またLLMのJSON出力をPythonオブジェクトにパース・検証することが可能です。
プロンプト設計
LLMが上記のJSON Schemaに準拠した出力を生成するためのプロンプト案を3種類提示します。
1. ゼロショットプロンプト
あなたはイベント予約システムのための情報抽出アシスタントです。ユーザーの予約リクエストから、以下のJSON Schemaに厳密に従って情報を抽出し、JSON形式で出力してください。他のテキストは一切含めないでください。
JSON Schema:
{
"properties": {
"event_name": {"type": "string", "description": "Name of the event or meeting."},
"attendees": {"type": "array", "items": {"type": "string"}, "minItems": 1},
"start_time": {"type": "string", "format": "date-time", "description": "Start time in ISO 8601."},
"end_time": {"type": "string", "format": "date-time", "description": "End time in ISO 8601."},
"location": {"type": "string"},
"is_tentative": {"type": "boolean", "default": false}
},
"required": ["event_name", "attendees", "start_time", "end_time"],
"type": "object"
}
ユーザーリクエスト:
来週水曜日の午後3時から4時まで、ジョンとメアリーとの企画会議を会議室Cで設定して。
2. 少数例(Few-shot)プロンプト
あなたはイベント予約システムのための情報抽出アシスタントです。ユーザーの予約リクエストから、以下のJSON Schemaに厳密に従って情報を抽出し、JSON形式で出力してください。他のテキストは一切含めないでください。
JSON Schema:
{
"properties": {
"event_name": {"type": "string", "description": "Name of the event or meeting."},
"attendees": {"type": "array", "items": {"type": "string"}, "minItems": 1},
"start_time": {"type": "string", "format": "date-time", "description": "Start time in ISO 8601."},
"end_time": {"type": "string", "format": "date-time", "description": "End time in ISO 8601."},
"location": {"type": "string"},
"is_tentative": {"type": "boolean", "default": false}
},
"required": ["event_name", "attendees", "start_time", "end_time"],
"type": "object"
}
例1:
ユーザーリクエスト:
明日の午前10時から11時まで、田中さんとの週次報告会をオンラインでお願いします。
出力:
```json
{
"event_name": "週次報告会",
"attendees": ["田中さん"],
"start_time": "2024-03-30T10:00:00+09:00",
"end_time": "2024-03-30T11:00:00+09:00",
"location": "オンライン",
"is_tentative": false
}
ユーザーリクエスト: 来週水曜日の午後3時から4時まで、ジョンとメアリーとの企画会議を会議室Cで設定して。
### 3. Chain-of-Thought制約型プロンプト
```text
System:
あなたはイベント予約システムのための情報抽出アシスタントです。以下のJSON Schemaに従い、ユーザーのリクエストからイベントの詳細を抽出し、JSON形式で出力してください。思考のプロセスをMarkdownの「思考:」セクションに記述し、最終的なJSONは「出力:」セクションに含めてください。JSON以外の余計な情報は一切含めないでください。ISO 8601フォーマットでは、タイムゾーンとして+09:00を使用してください。本日の日付は {{jst_today}} です。
JSON Schema:
{
"properties": {
"event_name": {"type": "string", "description": "Name of the event or meeting."},
"attendees": {"type": "array", "items": {"type": "string"}, "minItems": 1},
"start_time": {"type": "string", "format": "date-time", "description": "Start time in ISO 8601."},
"end_time": {"type": "string", "format": "date-time", "description": "End time in ISO 8601."},
"location": {"type": "string"},
"is_tentative": {"type": "boolean", "default": false}
},
"required": ["event_name", "attendees", "start_time", "end_time"],
"type": "object"
}
User:
来週水曜日の午後3時から4時まで、ジョンとメアリーとの企画会議を会議室Cで設定して。
Assistant:
思考:
1. イベント名を特定: 「企画会議」
2. 参加者を特定: 「ジョン」と「メアリー」。配列形式にする。
3. 日付と時間を特定: 「来週水曜日の午後3時から4時まで」。本日の日付 {{jst_today}} を基準に来週水曜日の日付を計算し、ISO 8601形式(YYYY-MM-DDTHH:MM:SS+09:00)に変換する。
4. 場所を特定: 「会議室C」。
5. 仮予約フラグを特定: リクエストに明示されていないため、デフォルトのfalseを使用する。
6. 全ての情報をJSON Schemaにマッピングし、JSONオブジェクトを生成する。
出力:
```json
{
"event_name": "企画会議",
"attendees": ["ジョン", "メアリー"],
"start_time": "2024-04-03T15:00:00+09:00",
"end_time": "2024-04-03T16:00:00+09:00",
"location": "会議室C",
"is_tentative": false
}
評価
LLMの出力が期待通りであるかを検証するための評価シナリオと自動評価の擬似コードを提案します。
評価シナリオ
正例: 指示されたJSON Schemaに完全に合致し、かつ内容も正確な出力。
- 例: 「明日の朝9時から10時半まで、開発チームとの進捗会議をビデオ会議で。」
難例:
曖昧な指示: 「来週の打ち合わせ」のように具体的な日付や時間が欠けている。
複雑なネスト構造: (今回のスキーマにはないが)ネストされたオブジェクトや配列を含む場合。
複数イベント: 1つのリクエストで複数のイベント予約を要求する場合。
コーナーケース:
空のリスト: 参加者が指定されていない場合(
attendeesが空の配列になるか、minItems制約に違反するか)。不正なデータ型: 時間が数字で与えられるなど、
"start_time": 1000のようなケース。エスケープ文字: JSON文字列内に特殊文字が含まれる場合。
必須フィールド欠落: 意図的に必須フィールドを省略したリクエスト。
自動評価の擬似コード
LLMの出力を受け取り、定義されたJSON Schemaに基づいて自動的に評価するルーブリックを実装します。
import json
from jsonschema import validate, ValidationError
import re
# 定義されたJSON Schema
event_schema = {
"properties": {
"event_name": {"type": "string"},
"attendees": {"type": "array", "items": {"type": "string"}, "minItems": 1},
"start_time": {"type": "string", "format": "date-time"},
"end_time": {"type": "string", "format": "date-time"},
"location": {"type": "string"},
"is_tentative": {"type": "boolean", "default": False}
},
"required": ["event_name", "attendees", "start_time", "end_time"],
"type": "object"
}
def evaluate_llm_output(llm_output_str: str, schema: dict) -> dict:
"""
LLM出力をJSON Schemaと内容で自動評価する。
Args:
llm_output_str (str): LLMが生成した生の文字列出力。
schema (dict): 評価に用いるJSON Schema。
Returns:
dict: 評価結果(スコア、詳細、エラー)。
"""
results = {
"score": 0,
"schema_valid": False,
"content_accuracy": 0,
"details": [],
"errors": []
}
# 1. JSON形式のバリデーション (40点)
try:
# LLMの出力からJSONコードブロックを抽出
match = re.search(r"```json\n(.*?)\n```", llm_output_str, re.DOTALL)
if match:
json_payload = json.loads(match.group(1))
results["details"].append("JSONコードブロックを正常に抽出・パースしました。")
results["score"] += 10 # JSON形式として有効
else:
json_payload = json.loads(llm_output_str) # JSONコードブロックがない場合を想定
results["details"].append("生の出力がJSONとして正常にパースされました。")
results["score"] += 10 # JSON形式として有効
except json.JSONDecodeError as e:
results["details"].append(f"JSON形式が無効です: {e}")
results["errors"].append(f"JSONDecodeError: {e}")
return results # JSONとして無効なら以降のチェックは意味がない
# 2. JSON Schemaバリデーション (40点)
try:
validate(instance=json_payload, schema=schema)
results["schema_valid"] = True
results["score"] += 40
results["details"].append("JSON Schemaに適合しています。")
except ValidationError as e:
results["details"].append(f"JSON Schemaに不適合です: {e.message} at {e.path}")
results["errors"].append(f"ValidationError: {e.message} at {e.path}")
# 3. 特定のビジネスロジック/コンテンツの正確性 (20点)
# 例: start_timeがend_timeより前であること
if results["schema_valid"]: # スキーマが有効な場合のみコンテンツ検証
start_time_str = json_payload.get("start_time")
end_time_str = json_payload.get("end_time")
if start_time_str and end_time_str:
try:
# ISO 8601文字列をdatetimeオブジェクトに変換
from datetime import datetime
start_dt = datetime.fromisoformat(start_time_str)
end_dt = datetime.fromisoformat(end_time_str)
if start_dt < end_dt:
results["content_accuracy"] = 1 # 1点
results["score"] += 20
results["details"].append("start_timeがend_timeより前です。")
else:
results["details"].append("start_timeがend_timeより後か同じです。")
except ValueError as e:
results["details"].append(f"日付時刻フォーマットエラー: {e}")
results["details"].append("コンテンツの正確性チェック完了。")
return results
# 使用例:
# user_query = "来週水曜日の午後3時から4時まで、ジョンとメアリーとの企画会議を会議室Cで設定して。"
# llm_response = """
# 思考:
# ...
# 出力:
# ```json
# {
# "event_name": "企画会議",
# "attendees": ["ジョン", "メアリー"],
# "start_time": "2024-04-03T15:00:00+09:00",
# "end_time": "2024-04-03T16:00:00+09:00",
# "location": "会議室C",
# "is_tentative": false
# }
#
“””
evaluation_result = evaluate_llm_output(llm_response, event_schema)
print(json.dumps(evaluation_result, indent=2, ensure_ascii=False))
llm_bad_response = “””{“event_name”: “会議”, “attendees”: [], “start_time”: “invalid_date”}”””
evaluation_result_bad = evaluate_llm_output(llm_bad_response, event_schema)
print(json.dumps(evaluation_result_bad, indent=2, ensure_ascii=False))
## 誤り分析と抑制手法
LLMが期待通りの出力を生成できない「失敗モード」を特定し、それぞれの抑制手法を解説します。
### 失敗モード
1. **幻覚 (Hallucination)**: LLMがプロンプトには存在しない、あるいは事実に反する情報を生成し、それをJSONに含める。
* 例: 存在しない参加者名や、誤った日付。
2. **様式崩れ (Malformed JSON/Schema Mismatch)**:
* **不正なJSON**: JSON構文エラー(括弧の閉じ忘れ、カンマの欠落など)。
* **スキーマ不適合**: 定義されたJSON Schemaの型、必須フィールド、制約に違反する出力。
* 例: `event_name` が文字列ではなく数値になる、`attendees` が配列ではなく文字列になる。
3. **脱線 (Off-topic/Irrelevant Output)**: プロンプトの指示内容から逸脱し、JSON以外の余計なテキストを生成したり、要求されていない情報を追加したりする。
4. **禁止事項違反**: 個人情報や機密情報、不適切な内容を意図せず出力してしまう。
### 抑制手法
1. **System指示の強化**:
* LLMの役割と出力形式(「JSON Schemaに厳密に従い、JSONのみを出力せよ」)をSystemプロンプトで明確に指示します。
* 禁止事項についてもSystemプロンプトで明示的に伝えます。
2. **Function Calling / Toolsの活用**:
* OpenAIやGoogleのFunction Calling機能を利用すると、JSON Schemaで定義された関数引数をLLMに生成させることができます [2][3]。これにより、LLMの自由な生成を特定のJSON構造に強制し、様式崩れのリスクを大幅に低減できます。
3. **JSON Schemaによる検証ステップ**:
* LLMの出力を受け取った直後に、`jsonschema` ライブラリ [1] やPydantic [4] を用いて厳密なバリデーションを実行します。
* 不適合の場合は、後続の処理に進まず、エラーとして処理します。
4. **リトライ戦略と再プロンプト**:
* バリデーションに失敗した場合、エラー内容をLLMにフィードバックし、「出力がJSON Schemaに適合していません。エラー詳細: [エラーメッセージ]。修正して再度出力してください。」のように再プロンプトしてリトライさせます。
* 複数回のリトライ後も失敗する場合は、人間の介入を促す。
5. **入力の正規化とバリデーション**:
* LLMへの入力(ユーザーリクエストなど)自体も、事前に整形・フィルタリングすることで、不正確な情報や悪意のある入力を抑制し、結果としてLLMの出力品質を向上させます。
6. **Guardrails/コンテンツモデレーション**:
* LLMの出力が禁止事項に抵触しないよう、専門のコンテンツモデレーションAPIや、キーワードフィルターなどのGuardrailsを導入し、不適切な内容を検知・ブロックします。
## 改良と再評価のループ
LLMのプロンプト設計は一度行えば終わりではありません。評価結果に基づいて継続的に改良と再評価を繰り返すことが重要です。
```mermaid
graph TD
A["要求定義"] --> B{"入出力契約<br>JSON Schema"};
B --> C["プロンプト設計"];
C --> D["LLMモデル"];
D --> E["LLM出力"];
E --> F{"出力検証<br>JSON Schemaバリデーション"};
F -- |適合| --> G["評価シナリオ実行"];
F -- |不適合| --> H["誤り分析"];
G --> I{"評価結果"};
H --> J["改良"];
I -- |基準達成| --> K["デプロイ"];
I -- |基準未達成| --> J;
J --> C;
改良ステップ:
プロンプト修正: 評価で顕著な失敗モードが見つかった場合、その原因に合わせてプロンプトの指示を明確化したり、Few-shotの例を追加・修正したりします。
スキーマの厳格化: LLMが曖昧な解釈をする可能性のあるフィールドや制約があれば、JSON Schemaをより厳密に定義し直します。
モデル選定・チューニング: 特定のタスクで継続的にパフォーマンスが低い場合、より高性能なLLMモデルへの切り替えや、ファインチューニングを検討します。
これらの改良を適用後、再度評価シナリオを用いて、改良が効果的であったかを定量的に確認します。このループを繰り返すことで、LLMの出力品質と信頼性を継続的に向上させます。
まとめ
LLMを実用的なアプリケーションに組み込む上で、その出力を予測可能で堅牢なものにする「入出力契約」の設計は極めて重要です。JSON Schemaを核とした契約定義、明確なプロンプト設計、そして自動化された評価と誤り分析、継続的な改良のループを通じて、LLMの能力を最大限に引き出し、信頼性の高いシステムを構築することが可能になります。特に、Function Callingのような機能は、構造化出力の精度を劇的に向上させる強力なツールであり、積極的に活用すべきです。
参考文献:
[1] JSON Schema公式サイト. https://json-schema.org/ (最終アクセス日: {{jst_today}})
[2] OpenAI. “Function calling”. OpenAI Docs. https://platform.openai.com/docs/guides/function-calling (公開日: 2023年6月13日, 最終アクセス日: {{jst_today}})
[3] Google. “Function calling”. Google AI for Developers. https://ai.google.dev/docs/function_calling (最終アクセス日: {{jst_today}})
[4] Pydantic公式サイト. https://pydantic-docs.helpmanual.io/ (最終アクセス日: {{jst_today}})

