LLMのFunction Callingと構造化出力: プロンプト設計と評価戦略

Tech

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

LLMのFunction Callingと構造化出力: プロンプト設計と評価戦略

はじめに

大規模言語モデル(LLM)の進化により、自然言語処理の可能性は大きく広がっています。特に、LLMが外部ツールやAPIと連携するための「Function Calling」機能や、LLMからの出力を機械可読な形式で取得する「構造化出力」は、LLMをシステムに組み込む上で不可欠な要素となっています。Function Callingは、LLMがユーザーの意図を解釈し、適切なツールを特定して引数を生成することで、システムが実行すべきアクションを指示します。構造化出力は、JSONやXMLなどの形式で情報を抽出し、後続の処理やデータベースへの保存を容易にします。 、これらの強力な機能を効果的に活用するためのプロンプト設計、評価戦略、そして一般的な課題と対処法について、プロンプトエンジニアリングの観点から解説します。

ユースケース定義

本記事では、以下のユースケースを想定してプロンプト設計と評価を論じます。

  1. Function Callingのユースケース:

    • 目的: ユーザーからの自然言語によるリクエストから、天気予報APIを呼び出すための都市名と日付(任意)を抽出する。

    • : 「明日の東京の天気は?」「来週の京都の気温を教えて」

  2. 構造化出力のユースケース:

    • 目的: ユーザーからの商品レビューテキストから、商品名、評価点(1-5)、ポジティブ/ネガティブなコメントをJSON形式で抽出する。

    • : 「このスマホ、電池持ちが最高だけどカメラがイマイチ。総合評価は4点。」

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

LLMの挙動を予測可能にするため、厳密な入出力契約を定義します。

  • 入力:

    • 形式: 自然言語のテキスト(最大1000トークン)。

    • 前提: ユーザーの意図または対象データを含む。

  • 出力:

    • 形式: Function Callingの場合は、呼び出す関数名と引数を格納した厳格なJSON形式。構造化出力の場合は、指定されたJSON Schemaに準拠したJSON形式。

      • 例(Function Calling): {"function_call": {"name": "get_weather", "arguments": {"location": "東京", "date": "2024-07-27"}}}

      • 例(構造化出力): {"product_name": "スマホX", "rating": 4, "comments": {"positive": ["電池持ちが良い"], "negative": ["カメラがイマイチ"]}}

    • 文字エンコーディング: UTF-8。

  • 失敗時の挙動:

    • 関数が特定できない、またはスキーマに合致しない場合: モデルは{"error": "Function or schema not matched."}のようなエラーメッセージを含むJSONを生成するか、Function Callingの場合はツールコールを生成しない。

    • JSON形式のバリデーションエラー: モデル出力後に外部バリデーターで検出し、リトライまたはエラー通知を行う。

  • 禁止事項:

    • Function Calling: 定義されていない関数名や引数の生成。

    • 構造化出力: JSONスキーマで定義されていないフィールドの追加、指定されたデータ型(例: 数値が文字列)からの逸脱。

    • 無関係な情報や説明文の混入。

プロンプト設計

最低3種のプロンプト案を提示します。

1. ゼロショットプロンプト (Zero-Shot Prompt)

モデルに明示的な例を与えず、タスクの指示と出力形式のみを伝えます。

あなたはユーザーの入力に基づいて、適切な関数を呼び出すためのJSONを生成するAIアシスタントです。
利用可能な関数は以下の通りです。

```json
{
  "functions": [
    {
      "name": "get_weather",
      "description": "指定された場所と日付の天気予報を取得します。",
      "parameters": {
        "type": "object",
        "properties": {
          "location": {
            "type": "string",
            "description": "天気予報を取得する都市名。必須。",
            "enum": ["東京", "大阪", "京都", "札幌", "福岡"]
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "天気予報を取得する日付(YYYY-MM-DD形式)。デフォルトは今日。"
          }
        },
        "required": ["location"]
      }
    }
  ]
}

指示: ユーザーの入力から関数呼び出しに必要な引数を抽出し、{"function_call": {"name": "...", "arguments": {...}}}形式のJSONを生成してください。関数が特定できない場合は{"error": "Function not matched."}と出力してください。

ユーザー入力: 明日の札幌の天気を教えてください。

### 2. 少数例プロンプト (Few-Shot Prompt)

具体的な入出力例をいくつか提示し、モデルにタスクのパターンを学習させます。

```text
あなたはユーザーの入力に基づいて、適切な関数を呼び出すためのJSONを生成するAIアシスタントです。
利用可能な関数は以下の通りです。

```json
{
  "functions": [
    {
      "name": "get_weather",
      "description": "指定された場所と日付の天気予報を取得します。",
      "parameters": {
        "type": "object",
        "properties": {
          "location": {
            "type": "string",
            "description": "天気予報を取得する都市名。必須。",
            "enum": ["東京", "大阪", "京都", "札幌", "福岡"]
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "天気予報を取得する日付(YYYY-MM-DD形式)。デフォルトは今日。"
          }
        },
        "required": ["location"]
      }
    }
  ]
}

以下の例を参考に、ユーザー入力から関数呼び出しに必要な引数を抽出し、{"function_call": {"name": "...", "arguments": {...}}}形式のJSONを生成してください。関数が特定できない場合は{"error": "Function not matched."}と出力してください。


ユーザー入力: 東京の今日の天気は?

出力: {“function_call”: {“name”: “get_weather”, “arguments”: {“location”: “東京”, “date”: “{{今日の日付: YYYY-MM-DD}}”}}}`

ユーザー入力: 来週金曜日の京都の天気はどうなる?

出力: {“function_call”: {“name”: “get_weather”, “arguments”: {“location”: “京都”, “date”: “{{来週金曜日の日付: YYYY-MM-DD}}”}}}`

ユーザー入力: ニューヨークの気温を教えて。

出力: {“error”: “Function not matched.”}

ユーザー入力: 明日の札幌の天気を教えてください。

### 3. Chain-of-Thought制約型プロンプト (CoT Constrained Prompt)

モデルに思考プロセスを段階的に指示し、各ステップで制約を設けます。

```text
あなたはユーザーの入力に基づいて、適切な関数を呼び出すためのJSONを生成するAIアシスタントです。
利用可能な関数は以下の通りです。

```json
{
  "functions": [
    {
      "name": "get_weather",
      "description": "指定された場所と日付の天気予報を取得します。",
      "parameters": {
        "type": "object",
        "properties": {
          "location": {
            "type": "string",
            "description": "天気予報を取得する都市名。必須。",
            "enum": ["東京", "大阪", "京都", "札幌", "福岡"]
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "天気予報を取得する日付(YYYY-MM-DD形式)。デフォルトは今日。"
          }
        },
        "required": ["location"]
      }
    }
  ]
}

以下の手順に従って、ユーザー入力から関数呼び出しに必要な引数を抽出し、JSONを生成してください。

  1. 意図特定: ユーザー入力がどの関数を呼び出そうとしているか特定します。利用可能な関数リストから選択してください。関数が特定できない場合は、「Function not matched」と判断してください。

  2. 引数抽出: 選択した関数のパラメーター定義に従い、ユーザー入力から必要な引数(location, date)を厳密に抽出します。

    • location: 必ずenumで指定された都市名から選んでください。

    • date: 必ずYYYY-MM-DD形式に変換してください。今日を示す場合は「{{JST_TODAY: YYYY-MM-DD}}」を使用してください。

  3. JSON生成: 抽出した情報に基づいて、{"function_call": {"name": "...", "arguments": {...}}}形式のJSONを生成します。意図特定で「Function not matched」と判断した場合は、{"error": "Function not matched."}を生成してください。

ユーザー入力: 明日の札幌の天気を教えてください。

## 評価

LLMの出力を客観的に評価するためのシナリオと自動評価の擬似コードを定義します。

### 評価シナリオ

*   **正例**:

    *   `「今日の東京の天気」` -> `get_weather(location="東京", date="{{2024-07-26}}")`

    *   `「この本、面白かったけど終わり方が微妙。総合評価は3点」` -> `{"product_name": "本", "rating": 3, "comments": {"positive": ["面白かった"], "negative": ["終わり方が微妙"]}}`

*   **難例**:

    *   `「来週の火曜日の福岡の気温は?」` (日付計算が必要)

    *   `「これは最高のゲーム!だけど少しバグがある。4点」` (商品名が明示されていない)

*   **コーナーケース**:

    *   `「天気予報を教えて」` (場所が不明) -> `{"error": "Function not matched."}`

    *   `「評価は5点。でも製品名は何だろう?」` (商品名がない) -> `{"error": "Schema not matched (missing product_name)."}` (スキーマ定義による)

### 自動評価(擬似コード)

採点ルーブリックに基づき、Pythonで記述された擬似コードで自動評価を行います。

```python
import json
import re
from datetime import datetime, timedelta

# JSON Schema for Function Calling output

FUNCTION_CALL_SCHEMA = {
    "type": "object",
    "properties": {
        "function_call": {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "arguments": {"type": "object"}
            },
            "required": ["name", "arguments"]
        },
        "error": {"type": "string"}
    },
    "oneOf": [
        {"required": ["function_call"]},
        {"required": ["error"]}
    ]
}

# JSON Schema for Structured Output (example for product review)

REVIEW_SCHEMA = {
    "type": "object",
    "properties": {
        "product_name": {"type": "string"},
        "rating": {"type": "integer", "minimum": 1, "maximum": 5},
        "comments": {
            "type": "object",
            "properties": {
                "positive": {"type": "array", "items": {"type": "string"}},
                "negative": {"type": "array", "items": {"type": "string"}}
            },
            "required": ["positive", "negative"]
        }
    },
    "required": ["product_name", "rating", "comments"]
}

def validate_json_schema(data, schema):
    """
    JSONデータを指定されたスキーマで検証します。
    前提: 'jsonschema'ライブラリがインストールされていること。
    入出力: data (dict), schema (dict) -> bool
    計算量: スキーマの複雑さに比例。
    メモリ: スキーマとデータのサイズに比例。
    """
    try:
        from jsonschema import validate
        validate(instance=data, schema=schema)
        return True
    except Exception as e:
        print(f"Schema validation error: {e}")
        return False

def evaluate_function_call_output(llm_output: str, expected_call: dict, jst_today_str: str) -> dict:
    """
    LLMのFunction Calling出力を評価します。
    入出力: llm_output (str), expected_call (dict), jst_today_str (str) -> dict
    前提: llm_outputはJSON形式、jst_today_strはYYYY-MM-DD形式。
    計算量: JSONパースと辞書比較に依存。
    メモリ: 出力と期待値のサイズに比例。
    """
    score = 0
    feedback = []

    try:
        output_data = json.loads(llm_output)
    except json.JSONDecodeError:
        feedback.append("Malformed JSON output.")
        return {"score": 0, "feedback": feedback}

    if not validate_json_schema(output_data, FUNCTION_CALL_SCHEMA):
        feedback.append("Output does not conform to Function Calling schema.")
        return {"score": 0, "feedback": feedback}

    if "error" in output_data:
        if "error" in expected_call and output_data["error"] == expected_call["error"]:
            score += 1 # エラーメッセージが一致
            feedback.append("Correctly identified as error and matched error type.")
        else:
            feedback.append(f"Identified as error but expected a function call or different error. Expected: {expected_call}, Got: {output_data}")
        return {"score": score, "feedback": feedback}

    if "function_call" not in output_data:
        feedback.append("Expected function call but not found.")
        return {"score": 0, "feedback": feedback}

    actual_call = output_data["function_call"]

    # 関数名の検証

    if actual_call.get("name") == expected_call["name"]:
        score += 0.5
        feedback.append("Function name matched.")
    else:
        feedback.append(f"Function name mismatch. Expected: {expected_call['name']}, Got: {actual_call.get('name')}")

    # 引数の検証

    actual_args = actual_call.get("arguments", {})
    expected_args = expected_call["arguments"]

    # 日付の動的処理

    if "date" in expected_args and expected_args["date"] == "{{2024-07-26}}":
        expected_args["date"] = jst_today_str
    elif "date" in expected_args and expected_args["date"].startswith("{{"):

        # 例: {{来週金曜日の日付: YYYY-MM-DD}}のようなパターンを処理

        match_date_pattern = re.search(r"\{\{(.+?)(?::\s*(.+?))?\}\}", expected_args["date"])
        if match_date_pattern:
            date_modifier = match_date_pattern.group(1).lower()
            today = datetime.strptime(jst_today_str, "%Y-%m-%d")
            target_date = today
            if "明日" in date_modifier:
                target_date += timedelta(days=1)
            elif "来週金曜日" in date_modifier:

                # 今日の曜日 (月=0, ... 土=5, 日=6)

                days_until_friday = (4 - today.weekday() + 7) % 7
                if days_until_friday == 0: # 今日が金曜日の場合、次の金曜日
                    days_until_friday = 7
                target_date += timedelta(days=days_until_friday + 7) # 次の金曜日

            expected_args["date"] = target_date.strftime("%Y-%m-%d")

    # 全ての期待される引数が存在し、値が一致するか

    all_args_match = True
    for key, value in expected_args.items():
        if key not in actual_args or actual_args[key] != value:
            all_args_match = False
            feedback.append(f"Argument '{key}' mismatch. Expected: '{value}', Got: '{actual_args.get(key)}'")
            break

    if all_args_match:
        score += 0.5
        feedback.append("All expected arguments matched.")

    return {"score": score, "feedback": feedback}

# --- 評価実行例 ---


# jst_today = "2024-07-26" # JSTの今日の日付


# # 正例


# llm_output_correct = '{"function_call": {"name": "get_weather", "arguments": {"location": "東京", "date": "2024-07-26"}}}'


# expected_correct = {"name": "get_weather", "arguments": {"location": "東京", "date": "{{2024-07-26}}"}}


# print(evaluate_function_call_output(llm_output_correct, expected_correct, jst_today)) # 期待: {"score": 1.0, "feedback": ["Function name matched.", "All expected arguments matched."]}

# # 難例 (日付計算)


# # 仮に "来週火曜日" が 2024-07-30 とする (2024-07-26が金曜日なので、来週火曜日は4日後)


# jst_today = "2024-07-26" # 金曜日


# llm_output_difficult = '{"function_call": {"name": "get_weather", "arguments": {"location": "福岡", "date": "2024-07-30"}}}'


# expected_difficult = {"name": "get_weather", "arguments": {"location": "福岡", "date": "{{来週火曜日の日付: YYYY-MM-DD}}"}}


# print(evaluate_function_call_output(llm_output_difficult, expected_difficult, jst_today))

# # コーナーケース (場所不明)


# llm_output_error = '{"error": "Function not matched."}'


# expected_error = {"error": "Function not matched."}


# print(evaluate_function_call_output(llm_output_error, expected_error, jst_today))

誤り分析と抑制手法

LLMは完全に完璧ではなく、いくつかの失敗モードが存在します。

失敗モード

  • 幻覚(Hallucination):

    • 存在しない関数名や引数、または定義されていないenum値などを生成する。

    • 構造化出力において、ソーステキストにない情報を捏造する。

  • 様式崩れ(Malformed output):

    • JSON形式が壊れている、必須フィールドが欠落している、データ型が異なる(例: 数値が文字列になっている)。

    • Function Callingで、function_callオブジェクトやargumentsオブジェクトが欠落している。

  • 脱線(Off-topic generation):

    • 関数呼び出しのJSONだけでなく、余計な説明文やコメントを付加する。

    • 構造化出力のJSONの後に、関連性のない自然言語テキストを続けて生成する。

  • 禁止事項の無視:

    • プロンプトで明示的に禁止された動作(例: 特定のキーワードの使用、外部URLへのアクセス試行)を実行する。

抑制手法

  • System指示の強化:

    • プロンプトの冒頭で「あなたはJSONのみを生成するAIアシスタントです。一切の追加テキストは禁止します。」といった明確な役割と出力形式の制約を課す。

    • Function Callingのスキーマを正確にプロンプトに含め、enumなどの制約を強調する。

    • 2024年4月10日にGoogle Cloud Blogで発表されたGeminiモデルの機能強化により、より複雑なFunction Callingの指示や引数生成の精度が向上しています[3]。

  • 出力の検証ステップ:

    • LLMからの出力後、JSON Schemaバリデーションライブラリ(Pythonのjsonschemaなど)を使用して、生成されたJSONが定義されたスキーマに準拠しているかを確認する。

    • Pydanticのようなライブラリを利用し、モデルの出力から直接Pythonオブジェクトを生成し、型チェックとバリデーションを自動化する。

  • リトライ戦略:

    • バリデーションエラーが発生した場合、エラーメッセージとともに元のプロンプトを修正し、モデルに再試行させる。

    • 例: 「出力されたJSONはスキーマに準拠していません。以下のエラーを修正し、再度JSONを生成してください: [エラーメッセージ]」

  • Few-shot学習の質の向上:

    • 提供する少数例は、多様なケース(成功例、エラー例、エッジケース)を網羅し、誤解を招かないように注意深く選定する。

    • OpenAIのFunction Callingに関するガイド[1]やGoogleのFunction Callingドキュメント[2]でも、具体的なスキーマ定義や例の提示が重要であると示唆されています。

改良と再評価のループ

プロンプト設計は一度で完璧になるものではなく、継続的な評価と改良が必要です。

graph TD
    A["ユーザー入力"] --> B{"プロンプト設計"};
    B --> C["LLM推論"];
    C --> D{"LLM出力"};
    D --> E{"出力検証"}|JSON Schemaバリデーション/型チェック|;
    E -- 成功 --> F["評価指標計算"]|正解率/F1スコアなど|;
    E -- 失敗 --> G["誤り分析"]|幻覚/様式崩れ/脱線|;
    F --> H{"評価結果/レポート"};
    G --> H;
    H -- 改善点あり --> B;
    H -- 改善完了 --> I["デプロイ"];

このループでは、ユーザー入力からLLMへのプロンプト設計、LLMからの出力、その出力の検証、そして評価と誤り分析を行います。評価結果や誤り分析で見つかった課題は、プロンプトの改良(System指示の調整、Few-shot例の追加、CoTステップの修正など)にフィードバックされ、精度が目標レベルに達するまで繰り返されます。

まとめ

LLMのFunction Callingと構造化出力は、AIシステムをより柔軟かつ堅牢にするための重要な技術です。効果的なプロンプト設計、厳密な入出力契約、包括的な評価シナリオ、そして反復的な改良サイクルを通じて、LLMの精度と信頼性を最大限に引き出すことができます。幻覚や様式崩れといった失敗モードに対しては、プロンプトでの明確な指示、出力後の検証、リトライ戦略を組み合わせることで、堅牢なシステム構築が可能です。今後もモデルの進化とともに、これらの技術の応用範囲はさらに広がっていくでしょう。


参考文献 [1] OpenAI Developers. “Function calling.” (2023年11月6日更新). https://platform.openai.com/docs/guides/function-calling [2] Google AI for Developers. “Function calling in Gemini models.” (2024年1月公開). https://ai.google.dev/docs/function_calling [3] Google Cloud Blog. “New updates to Gemini models: enhanced function calling and more.” (2024年4月10日). https://cloud.google.com/blog/products/ai-machine-learning/new-updates-to-gemini-models-enhanced-function-calling-and-more?hl=ja

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

コメント

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