powered by TechFeed
表示モード
ハウツー

LLMに確実にJSON出力させる実践パイプライン — プロンプト設計からスキーマ検証・API統合まで

9月1日、Collabnixが「Mastering Structured JSON Output from LLMs: Techniques for OpenAI, Claude, and Gemini」と題した記事を公開した。この記事では、OpenAI・Claude・GeminiといったLLMから構造化されたJSON出力を確実に得るための実践的テクニックについて詳しく紹介されている。LLMをプロダクションに組み込む際、最も厄介な問題のひとつが「出力が毎回違う」ことだ。自然言語として流暢な文章を返してくれるが、そのままではバックエンド処理やDB連携に使えない。JSON形式で確実に出力させる技術は、LLMをシステムの一部として扱う上で避けて通れないスキルになっている。なお、タイトルで謳う「確実に」を支える手段は、プロンプト設計だけでなく、各LLMプロバイダが提供するネイティブなJSON強制機能や、スキーマ検証ライブラリとの組み合わせによって初めて実現できる。以下ではそのパイプライン全体を順に解説する。Step 1:プロンプト設計が出力品質を決める最初の関門はプロンプトの書き方だ。LLMは...

9月1日、Collabnixが「Mastering Structured JSON Output from LLMs: Techniques for OpenAI, Claude, and Gemini」と題した記事を公開した。この記事では、OpenAI・Claude・GeminiといったLLMから構造化されたJSON出力を確実に得るための実践的テクニックについて詳しく紹介されている。

LLMをプロダクションに組み込む際、最も厄介な問題のひとつが「出力が毎回違う」ことだ。自然言語として流暢な文章を返してくれるが、そのままではバックエンド処理やDB連携に使えない。JSON形式で確実に出力させる技術は、LLMをシステムの一部として扱う上で避けて通れないスキルになっている。なお、タイトルで謳う「確実に」を支える手段は、プロンプト設計だけでなく、各LLMプロバイダが提供するネイティブなJSON強制機能や、スキーマ検証ライブラリとの組み合わせによって初めて実現できる。以下ではそのパイプライン全体を順に解説する。

Step 1:プロンプト設計が出力品質を決める

最初の関門はプロンプトの書き方だ。LLMはプロンプトで「JSON形式で出力せよ」と明示するだけで、出力の傾向が大きく変わる。

元記事のサンプルコードは旧世代のCompletion API(text-davinci-003)を使用しているが、このエンジンはOpenAIにより廃止済みである。現在はChat Completions APIへの移行が必須だ。また、後述するOpenAIのResponse Format機能を使えば、プロンプトだけに頼らずAPIレベルでJSON出力を強制できる。

# 参考:旧来のCompletion API(text-davinci-003)による例示
# ※このエンジンは廃止済み。現行実装にはChat Completions APIを使用すること
import openai
openai.api_key = 'YOUR_API_KEY'

response = openai.Completion.create(
  engine="text-davinci-003",
  prompt='Extract the following information as JSON: "John Doe, 35, Software Engineer, New York"',
  max_tokens=100
)
print(response['choices'][0]['text'])

ポイントは以下の3点だ。

  • フォーマットを明示する:「JSON形式で」と指示するだけでなく、期待するキー名まで示すと精度が上がる
  • Few-shotプロンプト(例示駆動):入出力の例をプロンプト内に含めることで、モデルが同様の構造を模倣しやすくなる
  • max_tokensの制御:出力が途中で切れてJSONが壊れるケースを防ぐため、十分なトークン数を確保する

ただし、いくら丁寧にプロンプトを書いても、LLMが完全に正しいJSONを返す保証はない。そのため後処理が必須になる。

各モデル固有のJSON出力制御

プロンプト設計と並行して、各LLMプロバイダが提供するネイティブ機能を活用することが、「確実なJSON出力」の鍵となる。

OpenAIでは、Chat Completions APIのresponse_formatパラメータによって出力形式をAPIレベルで制御できる。json_objectモードではJSONオブジェクトの出力を強制し、json_schemaモード(Structured Outputs)ではPydanticモデルやJSONスキーマに厳密に準拠した出力を保証する。

# Chat Completions API + json_objectモードの例
from openai import OpenAI
client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    response_format={"type": "json_object"},
    messages=[
        {"role": "system", "content": "You are a helpful assistant that outputs JSON."},
        {"role": "user", "content": 'Extract as JSON: "John Doe, 35, Software Engineer, New York"'}
    ]
)
print(response.choices[0].message.content)

Claude(Anthropic)は、XMLタグを使った構造化指示との親和性が高い設計になっており、プロンプト内で<json>タグなどを活用して出力範囲を明示する手法が有効とされている。また、Anthropicの公式ドキュメントでは、JSONモードの実現にシステムプロンプトでのスキーマ明示と、prefill(アシスタントの返答冒頭に{を挿入する)テクニックが推奨されている。

Gemini(Google)では、generation_configresponse_mime_type="application/json"を指定することで、APIレベルでJSONレスポンスを強制できる。さらにresponse_schemaパラメータを組み合わせることで、出力スキーマの構造まで制御可能だ。詳細はGoogle AI公式ドキュメントを参照されたい。

※編集部の考察:上記のネイティブJSON強制機能は、プロンプトだけに依存する手法より信頼性が高く、2026年時点のプロダクション開発では事実上の標準アプローチとなっている。さらに踏み込んだスキーマ制御を求める場合は、instructorLangChainのwith_structured_outputといったPydanticベースのライブラリも主流の選択肢だ。

Step 2:後処理でJSONを確実にパースする

モデルの出力には余分なテキストや構文エラーが混入することがある。json.loads() で直接パースしようとすると例外が発生するケースも多い。

import json

raw_output = response['choices'][0]['text']

try:
    structured_data = json.loads(raw_output)
except json.JSONDecodeError:
    structured_data = {}
    print("Failed to parse JSON output")

print(structured_data)

try-exceptJSONDecodeError を捕捉し、フォールバック処理(空辞書返却やエラーログ)を挟むのが基本パターンだ。出力が壊れていてもシステム全体が止まらないよう、防御的なパース処理を必ず入れておくべきだ。

Step 3:JSON Schemaで構造を検証する

パースに成功しても、フィールドが欠けていたり型が違ったりする場合がある。そこで有効なのがJSON Schemaによる検証だ。

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "ExampleSchema",
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "age": { "type": "integer", "minimum": 0 },
    "email": { "type": "string", "format": "email" }
  },
  "required": ["name", "age", "email"]
}

Pythonでは jsonschema ライブラリを使って検証を組み込める。

from jsonschema import validate

def validate_json(json_data, schema):
    try:
        validate(instance=json_data, schema=schema)
        print("JSON is valid!")
    except Exception as e:
        print(f"JSON validation error: {e}")

スキーマを定義しておくことで、フィールドの型・必須項目・値の範囲まで自動チェックできる。LLMの出力を後続の処理に流す前に、このバリデーション層を挟むのが安全な設計だ。

Step 4:検証済みJSONをアプリケーションに統合する

バリデーション済みのJSONをバックエンドに渡す一般的な手段がRESTful API経由での連携だ。記事ではFlaskを使った実装例が示されている。

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/process_json', methods=['POST'])
def process_json():
    data = request.get_json()
    response = {'status': 'success', 'processed_data': data}
    return jsonify(response)

if __name__ == '__main__':
    app.run(debug=True)

LLMからJSONを取得→バリデーション→FlaskエンドポイントへPOST、という流れが基本パターンになる。

よくあるピットフォール

記事では以下の失敗パターンが挙げられている。

  • JSONが途中で切れるmax_tokens が不足している
  • マークダウンが混入する:モデルがJSONをコードブロック(```json )で囲んで返すことがある。正規表現などで除去する必要がある
  • フィールド名がブレる:プロンプトでキー名を明示しないと、モデルが毎回異なるキー名を使う

プロダクション向けの最適化

  • キャッシュ:同一プロンプトに対する応答をキャッシュすることでAPIコストとレイテンシを削減できる
  • ロードバランシング:複数のAPIエンドポイントに負荷分散する設計を検討する

LLMの出力を「使える構造化データ」に変える一連のパイプライン——プロンプト設計・各モデル固有のJSON強制機能・後処理・スキーマ検証・API統合——を体系的にまとめた内容だ。すぐに使えるコードサンプルが各ステップに付いているため、実装の出発点として参照しやすい。ただし、コードサンプルの一部は旧来APIに基づいている点に留意し、現行のAPIドキュメントと照合しながら読み進めることを推奨する。

詳細はMastering Structured JSON Output from LLMs: Techniques for OpenAI, Claude, and Geminiを参照していただきたい。