0Pricing
AI Engineering Academy · レッスン

不正な出力の検証と再試行

抽出したデータをビジネスルールに照らして確認する検証レイヤーを実装し、検証に失敗した場合は修正フィードバック付きで自動的に再試行して、失敗パターンをログに記録します。

「不正な出力の検証と再試行」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。

LLMの出力に検証が必要な理由

構造化出力とPydanticスキーマを使用していても、LLMによる抽出では、構文的には有効でも意味的には誤った出力が生成されることがあります。1.5という確信度スコア(0~1の範囲外)、-99.99という価格、解析できない日付文字列、文字を含む電話番号などは、いずれもJSONの解析には通りますが、ビジネスルールには違反します。

検証は抽出とは別の関心事です。抽出が問うのは「構造化データを取得できたか」です。一方、検証が問うのは「その構造化データは正しく、利用可能か」です。本番品質のパイプラインには、どちらの層も必要です。2段階のフィルターだと考えてください。LLMが抽出し、検証器が受け入れるか拒否します。

検証のレイヤー

堅牢な出力検証システムは、複数のレベルで動作します。

  1. スキーマ検証(Pydantic):フィールド型が正しいこと、必須フィールドが存在すること、列挙値が許可された値と一致すること。構造化出力によって自動的に処理されます
  2. 形式検証:電話番号が正規表現に一致すること、メールアドレスが有効であること、日付を解析できること、金額が現実的な範囲内であること
  3. ビジネスロジック検証:請求書の合計が明細の合計と一致すること、終了日が開始日より後であること、数量が正の整数であること
  4. フィールド間検証:あるフィールドの値が別のフィールドの値に依存すること(例:割引率が100を超えないこと)
  5. 意味検証:抽出した会社名がデータベースに登録された既知の会社と一致すること

形式チェックのためのPydanticバリデーター

Pydanticのfield_validatorデコレーターを使うと、モデルのインスタンス化時に実行されるカスタム検証ロジックを追加できます。電話番号やメールアドレスの正規表現による検証、日付の解析、数値フィールドの範囲チェックなど、形式レベルのチェックに使用します。

from pydantic import BaseModel, Field, field_validator
from typing import Optional
import re
from datetime import datetime

class ExtractedInvoice(BaseModel):
    vendor: str
    invoice_number: Optional[str]
    amount: float = Field(gt=0, description='Must be positive')
    currency: str = Field(min_length=3, max_length=3)
    invoice_date: str

    @field_validator('currency')
    @classmethod
    def currency_must_be_uppercase(cls, v):
        return v.upper()

    @field_validator('invoice_date')
    @classmethod
    def parse_date(cls, v):
        # Try to parse common date formats
        for fmt in ('%Y-%m-%d', '%d/%m/%Y', '%m/%d/%Y', '%B %d, %Y'):
            try:
                datetime.strptime(v, fmt)
                return v
            except ValueError:
                continue
        raise ValueError(f'Cannot parse date: {v}')

    @field_validator('amount')
    @classmethod
    def reasonable_amount(cls, v):
        if v > 10_000_000:
            raise ValueError(f'Amount {v} seems unreasonably large. Flag for review.')
        return round(v, 2)

修正フィードバック付き再試行パターン

検証に失敗した場合、最も効果的な復旧戦略は修正フィードバック付きの再試行です。検証エラーメッセージをコンテキストとしてモデルに返し、何が問題だったのかを説明したうえで、失敗したフィールドだけを修正するよう求めます。これにより、やみくもに再試行するのではなく、モデルが出力を修正するために必要な情報を与えられます。

import openai
from pydantic import BaseModel, ValidationError, Field

client = openai.OpenAI()

class PriceExtraction(BaseModel):
    product: str
    price_usd: float = Field(gt=0, lt=100000)
    quantity: int = Field(ge=1)

def extract_with_retry(text: str, max_retries: int = 3) -> PriceExtraction:
    messages = [
        {'role': 'system', 'content': 'Extract product pricing information.'},
        {'role': 'user', 'content': text}
    ]

    for attempt in range(max_retries):
        result = client.beta.chat.completions.parse(
            model='gpt-4o-mini',
            messages=messages,
            response_format=PriceExtraction
        )
        msg = result.choices[0].message
        if msg.refusal:
            raise ValueError(f'Model refused: {msg.refusal}')

        try:
            return msg.parsed  # Pydantic validates on parse
        except ValidationError as e:
            if attempt == max_retries - 1:
                raise
            # Add corrective feedback for the next attempt
            messages.append({'role': 'assistant', 'content': msg.content})
            messages.append({'role': 'user', 'content': f'The previous extraction failed validation: {e}\nPlease correct and try again.'})
            print(f'Attempt {attempt+1} failed. Retrying with feedback...')

ビジネスロジック検証

ビジネスロジック検証では、複数のフィールドにまたがるプロパティや、外部データソースに依存するプロパティを確認します。Pydanticのmodel_validatorは、すべてのフィールドレベルのバリデーターが実行された後に動作し、完全に値が設定されたモデルにアクセスできます。そのため、フィールド間のチェックに適しています。

from pydantic import BaseModel, Field, model_validator
from typing import List

class LineItem(BaseModel):
    description: str
    quantity: int = Field(ge=1)
    unit_price: float = Field(ge=0)
    line_total: float

    @model_validator(mode='after')
    def check_line_total(self):
        expected = round(self.quantity * self.unit_price, 2)
        actual = round(self.line_total, 2)
        if abs(expected - actual) > 0.02:  # Allow 2-cent rounding tolerance
            raise ValueError(
                f'Line total {actual} does not match quantity*price={expected}'
            )
        return self

class Invoice(BaseModel):
    line_items: List[LineItem]
    subtotal: float
    tax: float
    total: float

    @model_validator(mode='after')
    def check_invoice_total(self):
        expected_total = round(self.subtotal + self.tax, 2)
        if abs(expected_total - round(self.total, 2)) > 0.02:
            raise ValueError(
                f'Invoice total {self.total} != subtotal+tax ({expected_total})'
            )
        return self

検証失敗のログ記録

検証に失敗するたびに、パイプラインのどこで問題が発生しているかを示すシグナルが得られます。各失敗について、入力テキスト(プライバシー保護のため、ハッシュでも構いません)、抽出結果、具体的な検証エラー、試行回数を記録してください。これらのログを集計して体系的なパターンを特定します。モデルが特定のフィールドを一貫して間違えていないか、失敗を引き起こすドキュメントの種類がないかを確認します。このデータをもとに、対象を絞ったプロンプト改善を行えます。

import logging
from pydantic import ValidationError

logger = logging.getLogger(__name__)

def extract_with_logging(text: str, doc_id: str) -> dict:
    result = None
    for attempt in range(3):
        try:
            result = run_extraction(text)  # Your extraction function
            logger.info('Extraction success', extra={
                'doc_id': doc_id,
                'attempt': attempt + 1
            })
            return result
        except ValidationError as e:
            logger.warning('Validation failure', extra={
                'doc_id': doc_id,
                'attempt': attempt + 1,
                'errors': e.errors(),
                'error_count': len(e.errors())
            })
    # All retries failed
    logger.error('Extraction failed after max retries', extra={'doc_id': doc_id})
    return {'error': 'extraction_failed', 'doc_id': doc_id}

def run_extraction(text):
    pass  # Placeholder for actual extraction logic

確信度スコアとしきい値

抽出スキーマにconfidenceフィールドを追加し、各抽出について0から1の範囲で確信度を評価するようモデルに指示します。そのうえで、確信度に基づくビジネスルールを適用します。確信度の高い抽出はそのままデータベースに登録し、中程度の抽出にはスポットチェック用のフラグを付け、確信度の低い抽出は人による確認キューに送ります。

この確率的なアプローチは、LLMに100%の精度を求めるよりもはるかに現実的です。不確実性が存在しないふりをするのではなく、不確実性を適切に扱えるようにパイプラインを設計します。

from pydantic import BaseModel, Field
from typing import Optional

class ExtractedWithConfidence(BaseModel):
    value: Optional[str]
    confidence: float = Field(ge=0.0, le=1.0)
    reason: Optional[str] = None  # Why confidence is low, if below threshold

class DocumentExtraction(BaseModel):
    vendor_name: ExtractedWithConfidence
    invoice_amount: ExtractedWithConfidence
    due_date: ExtractedWithConfidence

def route_by_confidence(extraction: DocumentExtraction, threshold=0.85):
    low_confidence_fields = []
    for field_name, field_val in extraction.model_dump().items():
        if isinstance(field_val, dict) and field_val.get('confidence', 1.0) < threshold:
            low_confidence_fields.append(field_name)

    if not low_confidence_fields:
        return 'auto_approve'
    elif len(low_confidence_fields) > 2:
        return 'human_review'
    else:
        return f'spot_check: {low_confidence_fields}'

再試行に失敗した場合のフォールバック戦略

すべての再試行を使い果たしても検証に失敗する場合は、フォールバック戦略が必要です。優先順位の高い順に、次の選択肢があります。

  1. 部分的な結果:検証に成功したフィールドを返し、失敗したフィールドにはnullを設定します
  2. 人による確認キュー:特に価値の高いドキュメントを、手動確認用のキューに追加します
  3. 精度を落とした抽出:より少ないフィールドを求める単純なスキーマに切り替え、構造の少なさと引き換えに堅牢性を高めます
  4. 生テキストの保存:抽出パイプラインを改善した後で再処理できるよう、元のテキストをメタデータとともに保存します

ドキュメントを黙って破棄してはいけません。必ず失敗を記録し、後から再確認できるようにしてください。

外部データに対する意味検証

一部のバリデーションルールでは、Pydanticのバリデータ内では実行できない外部データの参照が必要です。たとえば、抽出した会社名がCRMに登録されているか、抽出した商品SKUが在庫に存在するかを確認する場合です。こうした確認は、Pydanticによるバリデーションが成功した後に実行する、抽出後のバリデーション手順に含める必要があります。

from typing import Optional

# Simulated external data source
KNOWN_VENDORS = {'acme corp', 'techsupplies inc', 'globex corporation'}

def validate_against_crm(extraction: dict) -> dict:
    vendor = extraction.get('vendor', '').lower()
    warnings = []

    if vendor and vendor not in KNOWN_VENDORS:
        warnings.append({
            'field': 'vendor',
            'issue': f'Vendor "{vendor}" not found in CRM',
            'severity': 'warning'
        })
        # Optionally suggest closest match
        # from difflib import get_close_matches
        # matches = get_close_matches(vendor, KNOWN_VENDORS, n=1, cutoff=0.8)
        # if matches: warnings[-1]['suggestion'] = matches[0]

    return {
        'extraction': extraction,
        'warnings': warnings,
        'requires_review': len(warnings) > 0
    }

print('Semantic validation pattern defined')

バリデーションパイプラインのテスト

バリデーションロジックには、抽出テストとは別に独自のテストスイートが必要です。既知の不正な出力をバリデータに渡し、適切なエラーが発生することを確認する単体テストを作成してください。境界値の金額、通常とは異なる形式の日付、予期しない空白を含むフィールド、数値ではなく数値文字列になっているフィールドなど、エッジケースもテストします。

このバリデーションテストスイートはAPI呼び出しなしで実行できるため、コードを変更するたびに高速かつ低コストで実行できます。また、バリデーションルールを記録する最適なドキュメントでもあります。テストケースによって、パイプラインが適用する各形式やビジネス上の制約が明確になるためです。

from pydantic import ValidationError

def test_extraction_validation():
    # Test cases: (input_data, should_pass)
    test_cases = [
        ({'vendor': 'ACME', 'amount': 150.00, 'currency': 'USD', 'invoice_date': '2025-01-15'}, True),
        ({'vendor': 'ACME', 'amount': -50.00, 'currency': 'USD', 'invoice_date': '2025-01-15'}, False),  # negative
        ({'vendor': 'ACME', 'amount': 150.00, 'currency': 'EURO', 'invoice_date': '2025-01-15'}, False), # 4-char
        ({'vendor': 'ACME', 'amount': 150.00, 'currency': 'USD', 'invoice_date': 'yesterday'}, False),   # bad date
    ]
    passed = failed = 0
    for data, should_pass in test_cases:
        try:
            # ExtractedInvoice(**data)  # Your Pydantic model
            if should_pass:
                passed += 1
            else:
                print(f'MISSED: Should have failed for {data}')
                failed += 1
        except (ValidationError, ValueError):
            if not should_pass:
                passed += 1
            else:
                print(f'UNEXPECTED FAIL for {data}')
                failed += 1
    print(f'Tests: {passed} passed, {failed} failed')

test_extraction_validation()

パターン別の失敗分析とプロンプトの調整

実際のドキュメントのサンプルに対して抽出パイプラインを実行し、バリデーションの失敗を確認すると、失敗の80%が少数の根本原因に集約されていることに気付くでしょう。よくある原因としては、特定のロケールの日付をモデルが一貫して誤った形式にする、税額と合計額を取り違える、バリデータがハイフンを要求しているのにハイフンなしの電話番号を生成する、といったものがあります。

繰り返し発生する失敗パターンごとに、同じ誤りを防ぐ具体例と制約を抽出プロンプトに追加してください。更新後はテストセット全体を再実行し、他のケースを退行させることなく精度が向上したことを確認します。このように、プロンプトを更新して評価する反復サイクルによって、実際のデータを扱う本番の抽出パイプラインは95%以上の精度に到達します。

クイックチェック

このレッスンで学んだAI Engineeringの概念について、理解度を確認します。

レッスンのまとめ

このレッスンでは、バリデーションはスキーマ、形式、ビジネスロジック、セマンティックという複数の層で行われ、それぞれ異なる実装方法が必要であること、修正フィードバック付きのリトライによって、モデルに具体的なエラーのコンテキストを与え、出力を効率的に修正できること、そして信頼度スコアによって、抽出の確実性に基づき、自動承認、スポットチェック、人手レビューのキューへ確率的に振り分けられることを学びました。これでStructured Output and JSON Modeコースは完了です。次は、すべてのRAGシステムの基盤となるベクトル埋め込みについて学びます。

よくある質問

「不正な出力の検証と再試行」レッスンは無料ですか?

はい。「不正な出力の検証と再試行」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。

「不正な出力の検証と再試行」で何を学びますか?

抽出したデータをビジネスルールに照らして確認する検証レイヤーを実装し、検証に失敗した場合は修正フィードバック付きで自動的に再試行して、失敗パターンをログに記録します。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

AI Engineering Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「不正な出力の検証と再試行」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このAI Engineering Academyレッスンでコードを書いて実行できますか?

はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. JSONモードとresponse_format
  2. Pydanticによる構造化出力
  3. 非構造化テキストからのデータ抽出
  4. 不正な出力の検証と再試行
← AI Engineering Academyに戻る