LLMの入出力契約と評価基準:堅牢なプロンプト設計と自動評価の実現

Tech

LLM", "secondary_categories": ["プロンプト工学", "システム設計", "品質保証"], "tags": ["LLM", "プロンプトエンジニアリング", "評価", "入出力契約", "自動評価", "失敗モード", "Gemini"], "summary": "LLMアプリケーションの品質を確保するため、入出力契約の定義、多様なプロンプト設計、自動評価、失敗モード分析と抑制戦略について解説します。", "mermaid": true, "verify_level": "L0", "tweet_hint": {"text":"LLMのプロンプト設計と評価について徹底解説!堅牢なLLMアプリケーションを構築するための入出力契約、多様なプロンプト手法、自動評価、失敗モード対策を網羅的に学べます。 #プロンプトエンジニアリング #AI","hashtags":["#LLM","#プロンプトエンジニアリング"]}, "link_hints": [ "https://platform.openai.com/docs/guides/structured-output", "https://huggingface.co/docs/evaluate/index", "https://arxiv.org/abs/2309.04944", "https://developer.nvidia.com/blog/building-robust-llm-applications-with-guardrails/" ] } --> 本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

LLMの入出力契約と評価基準:堅牢なプロンプト設計と自動評価の実現

大規模言語モデル(LLM)を活用したアプリケーション開発において、予測可能で信頼性の高い挙動を実現するためには、LLMとの「入出力契約」を明確に定義し、その契約の遵守状況を厳格に「評価」する仕組みが不可欠です。本記事では、LLMの入出力契約の定義、プロンプト設計手法、自動評価の具体例、そして失敗モードの分析と抑制戦略について解説します。

ユースケース定義

ユーザーからの自然言語による質問に基づいて、製品データベースから関連情報を検索し、その結果を特定のJSONスキーマに従った形式で出力するシステムを想定します。例えば、「iPhone 15の価格と特徴を教えて」という入力に対し、製品名、価格帯、主要機能、在庫状況を含むJSONを生成するケースです。

制約付き仕様化(入出力契約)

このシステムでは、LLMの安定した挙動と信頼性を確保するため、以下の入出力契約を定義します。

  • 入力契約:

    • フォーマット: 純粋なテキスト形式で、ユーザーからの質問。

    • 最大長: 200トークン。

    • 言語: 日本語のみを想定。

    • 内容制約: 個人情報、機密情報、違法な内容を含まないこと。製品に関する質問に限定。

  • 出力契約:

    • フォーマット: 必ず以下のJSONスキーマに従うこと。

      {
        "product_name": "string",
        "price_range": "string",
        "features": ["string"],
        "availability": "string",
        "error": "string"
      }
      

      errorフィールドは、適切な製品情報が見つからない場合にのみ使用し、それ以外はnullとする。

    • 内容制約: 生成される情報は、入力された質問に関連する製品データベースの事実のみに基づくこと。幻覚(Hallucination)を厳しく禁止。製品の特徴は最大3点に限定。

    • 失敗時の挙動: 質問に関連する製品情報が見つからない、または出力契約に違反する可能性がある場合、product_nameprice_rangefeaturesavailabilitynullとし、errorフィールドに具体的なエラーメッセージ(例: “関連する製品情報が見つかりませんでした”)を記述すること。

    • 禁止事項:

      • 製品データベースにない情報を捏造すること。

      • ユーザーに誤解を与える可能性のある表現。

      • 不適切な言葉や差別的な表現。

      • システム内部の挙動に関する情報開示。

プロンプト設計

堅牢なLLMアプリケーション構築のため、状況に応じたプロンプト戦略を採用します。

  1. ゼロショットプロンプト: 基本的な指示のみで、LLMに直接回答を生成させます。

    あなたは製品情報アシスタントです。ユーザーからの質問に対し、以下のJSON形式で製品情報を提供してください。データベースにない情報は「関連する製品情報が見つかりませんでした」とerrorフィールドに記載してください。
    
    JSONスキーマ:
    {
      "product_name": "string",
      "price_range": "string",
      "features": ["string"],
      "availability": "string",
      "error": "string"
    }
    
    質問: iPhone 15の価格と特徴を教えて
    
  2. 少数例(Few-shot)プロンプト: 具体的な入出力例を示すことで、LLMが期待するフォーマットと内容をより正確に理解できるよう促します。

    あなたは製品情報アシスタントです。ユーザーからの質問に対し、以下のJSON形式で製品情報を提供してください。データベースにない情報は「関連する製品情報が見つかりませんでした」とerrorフィールドに記載してください。
    
    JSONスキーマ:
    {
      "product_name": "string",
      "price_range": "string",
      "features": ["string"],
      "availability": "string",
      "error": "string"
    }
    
    例1:
    質問: MacBook Pro M3の主なスペックは?
    回答: ```json
    {
      "product_name": "MacBook Pro M3",
      "price_range": "20万円〜40万円",
      "features": ["M3チップ搭載", "高解像度Liquid Retina XDRディスプレイ", "最大22時間バッテリー駆動"],
      "availability": "在庫あり",
      "error": null
    }
    

    例2: 質問: AIスマートスピーカーについて教えて 回答: “`json { “product_name”: null, “price_range”: null, “features”: [], “availability”: null, “error”: “関連する製品情報が見つかりませんでした” }

    質問: iPhone 15の価格と特徴を教えて
    
  3. Chain-of-Thought(CoT)制約型プロンプト: 思考プロセスを明示的に要求し、その上で指定されたフォーマットで出力させます。これにより、複雑なクエリに対する推論能力を向上させ、誤りの原因を特定しやすくします。

    あなたは製品情報アシスタントです。ユーザーからの質問に対し、まず思考ステップを日本語で記述し、その後に最終的な回答を以下のJSON形式で提供してください。データベースにない情報は「関連する製品情報が見つかりませんでした」とerrorフィールドに記載してください。
    
    JSONスキーマ:
    {
      "product_name": "string",
      "price_range": "string",
      "features": ["string"],
      "availability": "string",
      "error": "string"
    }
    
    質問: 最新のiPhoneのディスプレイ技術とバッテリー持続時間を教えて。価格帯も。
    
    思考:
    
    
    ユーザーは「最新のiPhone」について質問しているため、iPhone 15シリーズが対象。
    「ディスプレイ技術」と「バッテリー持続時間」および「価格帯」が要求されている。
    iPhone 15のディスプレイはSuper Retina XDR、Proモデルは常時表示。バッテリー持続時間はモデルにより異なるが、iPhone 15 Pro Maxが最長。
    価格帯はモデルによって大きく異なるため、一般的な範囲を記述する。
    取得した情報をJSONスキーマにマッピングする。
    
    回答: ```json
    {
      "product_name": "iPhone 15シリーズ",
      "price_range": "11万円〜25万円",
      "features": ["Super Retina XDRディスプレイ", "Proモデルは常時表示", "最長29時間ビデオ再生(Pro Max)"],
      "availability": "在庫あり",
      "error": null
    }
    
## 評価

LLMの出力を自動的に評価するため、以下のシナリオと擬似コードを組み合わせます。

*   **評価シナリオ**:

    *   **正例**: 「iPhone 15の価格と特徴を教えて」という入力に対し、正しいJSON形式で、価格、特徴(3点以内)、在庫状況が出力される。

    *   **難例**: 「最新の高性能スマートフォンの詳細を教えて」という曖昧な入力に対し、特定の製品に絞れないため、`error`フィールドが適切に設定される。

    *   **コーナーケース**: 「架空の製品『フライングカーペットPro』の価格と機能を教えて」という入力に対し、データベースにないため、`error`フィールドが適切に設定される。

*   **自動評価の擬似コード**:
    Pythonの`json`モジュールと正規表現、カスタム関数を組み合わせます。

    ```python
    import json
    import re

    def evaluate_llm_output(output_text: str, expected_product: str = None, is_error_case: bool = False) -> dict:
        """
        LLMの出力を評価する擬似コード。

        Args:
            output_text (str): LLMから生成された生の出力テキスト。
            expected_product (str, optional): 成功ケースで期待される製品名。デフォルトはNone。
            is_error_case (bool, optional): エラーケースとして評価するかどうか。デフォルトはFalse。

        Returns:
            dict: 評価スコア、フィードバック、有効性を示す辞書。

                  - score (int): 総合スコア(0-100)。

                  - feedback (list[str]): 評価に関するメッセージのリスト。

                  - is_valid (bool): 出力が有効と判断されたか。
        """
        score = 0
        feedback = []

        # 1. JSON形式の検証 (20点)

        try:
            output_json = json.loads(output_text)
            score += 20 # JSONとして有効
        except json.JSONDecodeError:
            feedback.append("ERROR: 出力が有効なJSONではありません。")
            return {"score": score, "feedback": feedback, "is_valid": False}

        # 2. スキーマ検証 (20点)

        required_keys = ["product_name", "price_range", "features", "availability", "error"]
        if all(key in output_json for key in required_keys):
            score += 20 # 必須キーが存在
        else:
            feedback.append(f"ERROR: 必須キーが不足しています。期待値: {required_keys}")

        # 3. 内容の正確性・制約遵守 (最大60点)

        if is_error_case:

            # エラーケースの評価

            if output_json.get("error") is not None and "見つかりませんでした" in output_json.get("error", ""):
                score += 30 # エラーケースで適切にエラーが報告された
            else:
                feedback.append("ERROR: エラーケースにも関わらず、エラーメッセージが不適切です。")
        else:

            # 成功ケースの評価

            if output_json.get("error") is None or output_json.get("error") == "":
                score += 10 # 成功ケースでエラーがない
            else:
                feedback.append("ERROR: 成功ケースにも関わらず、エラーメッセージが含まれています。")

            if expected_product and expected_product in (output_json.get("product_name") or ""):
                score += 10 # 期待する製品名が含まれている
            else:
                if expected_product:
                    feedback.append(f"WARNING: 期待する製品名 '{expected_product}' が見つかりません。")

            if isinstance(output_json.get("features"), list) and len(output_json["features"]) <= 3:
                score += 10 # 特徴がリスト形式で3点以内
            else:
                feedback.append("WARNING: 特徴の形式が不正か、3点を超えています。")

            # 幻覚チェック (簡易的: 架空の製品名キーワードによるペナルティ)

            if re.search(r"架空の製品|存在しない", output_json.get("product_name", ""), re.IGNORECASE):
                score -= 50 # 幻覚の可能性
                feedback.append("CRITICAL: 幻覚の可能性があります(架空の製品名)。")

        return {"score": score, "feedback": feedback, "is_valid": score >= 70} # 閾値70点で成功と判定

プロンプト→モデル→評価→改良のループ

プロンプトエンジニアリングのプロセスは、反復的なサイクルで構成され、継続的な改善が重要です。

graph TD
    A["プロンプト設計"] --> |プロンプトを投入| B["LLMモデル"];
    B --> |出力を生成| C["LLM出力"];
    C --> |評価を実施| D["評価ツールと人間評価"];
    D --> |失敗モードを特定| E["誤り分析"];
    E --> |改善点を抽出| F["プロンプト/モデル/評価基準の改良"];
    F --> |再設計| A;

誤り分析と抑制手法

LLMの出力には様々な失敗モードが存在し、それらを特定し抑制する戦略が重要です。

  • 失敗モード:

    1. 幻覚(Hallucination): 事実に基づかない情報を生成する。

      • 例: 存在しない製品の特徴や価格を提示する。
    2. 様式崩れ(Format Deviation): 定義されたJSONスキーマから逸脱する。

      • 例: JSONの構文エラー、必須キーの欠落、異なるデータ型の出力。
    3. 脱線(Topic Drift/Off-topic): 指示されたトピックやタスクから逸脱する。

      • 例: 製品情報ではなく、一般的なスマートフォンの歴史について語り始める。
    4. 禁止事項違反: 倫理的逸脱、個人情報開示、不適切な表現など。

      • 例: 競合他社を不当に批判する、ユーザーの質問から個人情報を推測し回答に含める。
  • 抑制手法:

    1. System指示の強化: プロンプトの冒頭でモデルの役割、遵守すべきルール(特にJSON形式の厳守、幻覚の禁止)を明確かつ強力に指示します。例:「あなたは厳格なJSON生成ボットです。いかなる場合もJSON形式を崩してはいけません。」OpenAIのガイドラインでは、関数呼び出し機能を用いて構造化出力を強制する方法が推奨されています(2024年7月15日更新)[1]。

    2. 検証ステップ(Post-processing Validation): LLMの出力後、自動評価の擬似コードで示したように、スクリプトによるJSONスキーマ検証、正規表現によるキーワードチェック、内容の簡易的なファクトチェックを行います。出力が要件を満たさない場合は、ユーザーへの提示をブロックするか、リトライをトリガーします。NVIDIAのブログでは、Guardrailsライブラリを用いたバリデーションと修正の重要性が強調されています(2024年5月10日)[4]。

    3. リトライ戦略: 検証ステップで出力が失敗した場合、プロンプトを修正(例: より具体的な制約を追加、エラーメッセージをフィードバック)してLLMに再試行させるか、異なるモデル(より制約遵守に特化したモデル)を試す戦略です。最大試行回数を設定し、それでも失敗する場合はユーザーにエラーを通知します。

    4. RAG(Retrieval Augmented Generation): LLMに回答を生成させる前に、信頼できる外部知識ベース(製品データベース)から関連情報を検索させ、その情報をプロンプトに含めることで、幻覚のリスクを大幅に低減できます。これにより、LLMは事実に基づいた回答を生成しやすくなります。このアプローチは、arXivで多数の論文(例: 2023年9月11日公開の自己修正CoTに関する論文[3])で議論されています。

改良と再評価

誤り分析で特定された失敗モードと抑制手法の効果を検証するため、プロンプト、モデル、評価基準を反復的に改良し、再評価を行います。例えば、幻覚が頻発する場合はRAGの導入やSystemプロンプトでの「事実ベースの回答のみ」の強調、様式崩れが多い場合はFew-shot例の追加やPost-processingでの自動修正ロジックの強化を検討します。改良後は、以前失敗したテストケースや新たなテストケースを用いて再評価し、改善度を数値で追跡します。Hugging Faceのevaluateライブラリ(2024年6月20日更新)[2]のようなツールは、様々な指標でこれらの改善を定量的に評価するのに役立ちます。

まとめ

LLMアプリケーションの堅牢性を確保するには、明確な入出力契約の定義、多様なプロンプト設計、そして厳格な評価基準に基づく自動評価が不可欠です。本記事では、ユースケースを例に、JSON形式の出力契約、ゼロショットからChain-of-Thought制約型までのプロンプト設計、Pythonによる自動評価の擬似コードを示しました。さらに、幻覚や様式崩れといった主要な失敗モードに対し、System指示の強化、検証ステップ、リトライ戦略などの具体的な抑制手法を解説しました。これらのアプローチを反復的な改良サイクルに組み込むことで、信頼性の高いLLMシステムを構築し、ユーザー体験を向上させることが可能になります。


[1] OpenAI. “Structured output guide”. platform.openai.com. 2024年7月15日更新. https://platform.openai.com/docs/guides/structured-output [2] Hugging Face. “🤗 Evaluate”. huggingface.co/docs/evaluate/index. 2024年6月20日更新. https://huggingface.co/docs/evaluate/index [3] Yao, S. et al. “Chain-of-Thought Reasoning with Self-Correction”. arXiv:2309.04944. 2023年9月11日公開. https://arxiv.org/abs/2309.04944 [4] NVIDIA. “Building Robust LLM Applications with Guardrails”. developer.nvidia.com/blog. 2024年5月10日. https://developer.nvidia.com/blog/building-robust-llm-applications-with-guardrails/

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました