OpenAIとClaudeで構造化JSON出力をマスターする:堅牢なAIパイプラインの構築

AI tutorial - IT technology blog
AI tutorial - IT technology blog

背景と目的:正規表現の悪夢からの解放

GPT-3.5を使って初めて自動データ抽出パイプラインを構築しようとした時のことを覚えています。モデルにユーザーリストをJSON形式で返すよう指示しました。90%の確率はうまくいきました。しかし、残りの10%は悲惨なものでした。モデルは時折、レスポンスの冒頭に「もちろんです。こちらがデータです:」という文言を付け加えたり、パーサーが想定していないマークダウンのコードブロックでJSONを囲んだりしました。プロダクション環境のログは JSONDecodeError で溢れかえりました。

あるLLMの出力が別のサービスやデータベースの入力となるようなAIパイプラインを構築する場合、「クリエイティブな」フォーマットを許容する余裕はありません。決定論的で、スキーマに厳格なデータが必要です。実務経験上、プロトタイプからプロダクション対応のエージェントシステムへと移行したいのであれば、これは習得すべき必須スキルの1つです。

OpenAIとAnthropicは共に, この問題を解決するための機能を導入しました。OpenAIには「構造化出力(Structured Outputs / JSON Schema)」があり、Claudeは「Tool Use(Function Calling)」を利用して同様の結果を実現します。文字列操作から構造化オブジェクトへの移行こそが、趣味のプロジェクトと堅牢なエンタープライズアプリケーションを分ける境界線です。

インストール:環境構築

設定を確認する前に、適切なライブラリが必要です。生のHTTPリクエストよりも公式SDKを使用することをお勧めします。ヘッダーの定型処理やリトライロジックをより効果的に処理できるからです。

新しい仮想環境を作成し、必要なパッケージをインストールします:

bash
pip install openai anthropic pydantic python-dotenv

また、Pydantic の使用も強く推奨します。これはPythonにおけるデータバリデーションの業界標準です。期待する出力をPydanticモデルとして定義することで、LLMの生のレスポンスとアプリケーションのデータ構造の間のギャップを埋めることができます。

プロジェクトのルートに .env ファイルを作成し、APIキーを安全に保存します:

OPENAI_API_KEY=your_openai_key_here
ANTHROPIC_API_KEY=your_anthropic_key_here

設定:構造化出力の実装

実装の詳細はOpenAIとClaudeでわずかに異なります。OpenAIは厳格なスキーマ強制が可能ですが、Claudeはツール定義に従う優れた能力に依存しています。

1. OpenAIの構造化出力(JSON Schema)

OpenAIは最近、JSONスキーマ用の strict モードを導入しました。これを有効にすると、モデルがスキーマに正確に従うことが保証されます。これは、トークンレベルでサンプリングプロセスを制約することで実現されています。

python
import os
from openai import OpenAI
from pydantic import BaseModel
from dotenv import load_dotenv

load_dotenv()
client = OpenAI()

# 期待する構造を定義
class InventoryUpdate(BaseModel):
    item_name: str
    quantity: int
    category: str
    tags: list[str]

# API呼び出し
response = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "ユーザーのテキストから在庫の詳細を抽出してください。"},
        {"role": "user", "content": "電気部門向けに高品質の銅線を50ユニット受け取りました。'urgent'(緊急)と'industrial'(産業用)というタグを付けてください。"}
    ],
    response_format=InventoryUpdate, # これが魔法の一行です
)

# Pythonオブジェクトとしてデータにアクセス
structured_data = response.choices[0].message.parsed
print(f"アイテム: {structured_data.item_name}, 数量: {structured_data.quantity}")

.parse() メソッドに注目してください。JSON文字列からPydanticオブジェクトへの変換を自動的に処理します。モデルがスキーマに従わなかった場合(strict モードではほぼあり得ませんが)、呼び出し中にバリデーションエラーが発生します。

2. Claude (Anthropic) のTool Useによる構造化データ

AnthropicにはOpenAIのような特定の「JSONモード」フラグはありませんが、彼らの「Tool Use」の実装は非常に堅牢です。ツールを定義し、モデルにその使用を強制する(tool_choice を使用)ことで、完全に構造化されたデータを抽出できます。

python
import anthropic

client = anthropic.Anthropic()

# ツールを定義(これがスキーマとして機能します)
tools = [
    {
        "name": "record_inventory",
        "description": "在庫の更新をデータベースに記録します。",
        "input_schema": {
            "type": "object",
            "properties": {
                "item_name": {"type": "string"},
                "quantity": {"type": "integer"},
                "category": {"type": "string"},
                "tags": {"type": "array", "items": {"type": "string"}}
            },
            "required": ["item_name", "quantity", "category", "tags"]
        }
    }
]

message = client.messages.create(
    model="claude-3-5-sonnet-20240620",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "tool", "name": "record_inventory"}, # Claudeにツールの使用を強制する
    messages=[{"role": "user", "content": "エネルギー部門にソーラーパネルを10枚追加してください。ラベルは'renewable'(再生可能)にしてください。"}]
)

# ツール使用の入力を抽出
for content in message.content:
    if content.type == 'tool_use':
        print(content.input)

tool_choice を使用することで、Claudeはユーザーへの回答を試みることさえせず、定義された関数のためのJSON引数の生成に直行します。

検証とモニタリング:パイプラインの安定性を確保する

これらのネイティブ機能があっても、問題が発生する可能性はあります。ネットワークのタイムアウト、レート制限、コンテキストウィンドウのオーバーフローなどは、依然としてパイプラインを破壊する要因になります。私は管理するすべてのプロダクションAIサービスにおいて、3段階の検証プロセスに従っています。

1. スキーマバリデーション(セーフティネット)

APIの出力を盲目的に信用してはいけません。常に結果をPydanticバリデーターに通してください。OpenAI의 .parse() を使用している場合はこれが組み込まれています。Claudeや標準のJSONモードを使用している場合は、手動で行う必要があります:

python
try:
    # 'data' はAPIからの辞書形式のデータと想定
    validated_obj = InventoryUpdate(**data)
except Exception as e:
    # 失敗をログに記録し、おそらくtemperatureを下げて再試行をトリガーする
    print(f"バリデーション失敗: {e}")

2. 出力の途切れ(Truncation)への対処

よくある問題の1つは finish_reason です。モデルが max_tokens 制限に達して停止した場合、JSONは不完全で無効なものになります。パースを試みる前に、必ず停止理由を確認してください。

  • OpenAI: choice.finish_reason == "stop" かどうかを確認します。もし "length" であれば、JSONが途切れています。
  • Claude: message.stop_reason == "end_turn" または "tool_use" であることを確認します。

3. コストとレイテンシのモニタリング

構造化出力は、モデルがキーを繰り返したり特定のフォーマットに従ったりする必要があるため、通常よりもわずかに多くのトークンを必要とすることがあります。私はミドルウェアやシンプルなデコレータを使用して、トークン使用量をログに記録しています。単純な抽出でトークン使用量が急増している場合は、スキーマが複雑すぎるか、システムプロンプトに例示(Few-shot)を与えすぎている可能性があります。

私の経験では、厳格なデータ入力には GPT-4oの構造化出力 を、複雑な推論には Claude 3.5 SonnetのTool Use を組み合わせるのが、現代のAIエージェントにとって最も安定した基盤となります。モデルにJSONで話すことを強制することで、モデルを単なるチャットボットとしてではなく、本来の強力な計算エンジンとして扱うことができるようになります。

Share: