LLMにおけるFunction Callingと構造化出力の設計と評価

Tech

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

LLMにおけるFunction Callingと構造化出力の設計と評価

LLM(大規模言語モデル)の進化により、自然言語処理の能力は飛躍的に向上しました。特にFunction Callingと構造化出力は、LLMを外部システムと連携させたり、複雑な情報を正確に抽出したりするための重要な技術です。本記事では、これらの機能の設計、評価、および改良プロセスについて詳しく解説します。

ユースケース定義

LLMのFunction Callingと構造化出力は、以下のような幅広いユースケースで活用されます。

  1. 外部API連携: ユーザーの自然言語による指示に基づき、天気予報API、データベース検索API、EコマースAPIなどの外部サービスを呼び出し、情報を取得または操作する。

  2. データ抽出と加工: 自由形式のテキスト(例: 顧客レビュー、契約書、医療記録)から、特定のエンティティ(例: 製品名、価格、日付、症状)をJSONなどの構造化された形式で抽出し、後続のデータ処理パイプラインに渡す。

  3. タスク自動化: ユーザーの発話から意図と引数を抽出し、システム内の特定の操作(例: カレンダーイベントの作成、メール送信、リマインダー設定)をトリガーする。

入出力契約と制約付き仕様化

LLMのFunction Callingおよび構造化出力を利用する際、明確な入出力契約を定義し、期待される動作を制約付きで仕様化することが重要です。

入力契約

  • ユーザープロンプト: 自然言語による指示。例:「今日の東京の天気予報を教えて。」

  • ツール定義 (Function Calling): 利用可能な外部関数をJSON Schema形式で記述。関数名、説明、引数の型と説明を含む。

  • 出力スキーマ (構造化出力): 期待されるJSON出力の形式をJSON Schemaで記述。

出力契約

  • Function Calling: モデルは、ユーザープロンプトに基づいて呼び出すべき関数名と、その関数に渡す引数をJSONオブジェクトとして出力します。

    {
      "function_call": {
        "name": "get_weather_forecast",
        "args": {
          "location": "東京",
          "date": "今日"
        }
      }
    }
    
  • 構造化出力: モデルは、指定されたJSON Schemaに厳密に準拠したJSONオブジェクトを出力します。

    {
      "product_name": "スマートウォッチX",
      "price": 299.99,
      "currency": "USD",
      "features": ["心拍数モニター", "GPS"]
    }
    

失敗時の挙動

  • Function Calling: モデルが適切な関数を特定できない場合、または必要な引数を抽出できない場合、通常のテキスト応答を返すか、エラーを示す特定のフォーマットで応答する。実装によっては、エラーメッセージを含むテキストを返すモデルもあります。

  • 構造化出力: モデルが指定されたJSON Schemaに準拠できない場合、パースエラーが発生するか、不完全・不正なJSONが出力される。この場合、アプリケーション側でバリデーションエラーを検出し、リトライまたはフォールバック処理を行う。

禁止事項

  • 不適切なAPI呼び出し: ユーザーの意図に反する、またはセキュリティリスクを伴う外部APIの呼び出し。

  • スキーマ外のデータ生成: 構造化出力において、JSON Schemaで定義されていないフィールドの追加や、型違反のデータ出力。

  • 幻覚による引数生成: 存在しない引数値や、誤ったフォーマットの引数値を生成する。

プロンプト設計

Function Callingと構造化出力を成功させるためには、プロンプト設計が鍵となります。ここでは、3種類のプロンプト案を提示します。

ゼロショットプロンプト

モデルに特別な例を与えず、タスク指示とツール定義/スキーマ情報のみで実行させます。

あなたはユーザーの質問に答えるアシスタントです。
以下のツールが利用可能です。ユーザーの指示に最も合致するツールを選択し、必要な引数をJSON形式で出力してください。
ツールは利用できない場合、通常のテキストで応答してください。

ツール定義:
```json
{
  "type": "function",
  "function": {
    "name": "get_current_weather",
    "description": "指定された都市の現在の天気を取得する",
    "parameters": {
      "type": "object",
      "properties": {
        "location": {
          "type": "string",
          "description": "都市名"
        }
      },
      "required": ["location"]
    }
  }
}

ユーザーの質問: 今日の東京の天気予報を教えてください。

### 少数例プロンプト (Few-shot Prompt)

いくつかの入出力例を示すことで、モデルの挙動を誘導します。

```text
あなたはユーザーの質問に答えるアシスタントです。
以下のツールが利用可能です。ユーザーの指示に最も合致するツールを選択し、必要な引数をJSON形式で出力してください。
ツールは利用できない場合、通常のテキストで応答してください。

ツール定義:
```json
{
  "type": "function",
  "function": {
    "name": "get_current_weather",
    "description": "指定された都市の現在の天気を取得する",
    "parameters": {
      "type": "object",
      "properties": {
        "location": {
          "type": "string",
          "description": "都市名"
        }
      },
      "required": ["location"]
    }
  }
}

例1: ユーザーの質問: 今日のニューヨークの天気は? モデルの出力:

{
  "function_call": {
    "name": "get_current_weather",
    "args": {
      "location": "ニューヨーク"
    }
  }
}

例2: ユーザーの質問: モデルの出力: こんにちは!何かお手伝いできることはありますか?

ユーザーの質問: 今日の東京の天気予報を教えてください。

### Chain-of-Thought(CoT)制約型プロンプト

モデルに思考プロセスを明示させ、出力を特定の形式に制約します。

```text
あなたはユーザーの質問に答えるアシスタントです。
以下のツールが利用可能です。ユーザーの指示に最も合致するツールを選択し、必要な引数をJSON形式で出力してください。
ツールは利用できない場合、通常のテキストで応答してください。
出力形式は必ずJSONです。Function Callingを行う場合は {"tool_call": {...}}、テキスト応答の場合は {"text_response": "..."} としてください。

ツール定義:
```json
{
  "type": "function",
  "function": {
    "name": "get_current_weather",
    "description": "指定された都市の現在の天気を取得する",
    "parameters": {
      "type": "object",
      "properties": {
        "location": {
          "type": "string",
          "description": "都市名"
        }
      },
      "required": ["location"]
    }
  }
}

思考プロセス:

  1. ユーザーの質問「今日の東京の天気予報を教えてください。」を分析します。

  2. 質問内容は「天気予報」に関するものであり、利用可能なツール get_current_weather と関連性があります。

  3. get_current_weather ツールに必要な引数は location です。質問から「東京」が抽出できます。

  4. ツールを呼び出すべきだと判断し、適切なJSON形式で出力を生成します。

ユーザーの質問: 今日の東京の天気予報を教えてください。

## 評価シナリオと自動評価

モデルのFunction Callingおよび構造化出力の性能を評価するためには、多様なシナリオに基づいたテストと自動評価が必要です。

### 評価シナリオ

1.  **正例**:

    *   明確な指示: 「明日の大阪の最高気温を教えて。」

    *   全引数指定: 「2024年7月15日(JST)のパリの天気は?」

2.  **難例**:

    *   引数不足: 「天気予報をお願い。」(都市名が不足)

    *   曖昧な指示: 「何か旅行の計画を立てたい。」(複数ツールが候補となる可能性)

    *   複数関数が選択肢: 「ホテルを予約して、その後観光地を検索して。」(Tool Chainingの必要性)

3.  **コーナーケース**:

    *   無関係な指示: 「今日のランチは何がいいかな?」

    *   セキュリティリスクのある指示: 「システムの設定を変更して。」

    *   無効な引数: 「存在しない都市 'Hogehoge' の天気予報を。」

### 自動評価の擬似コード

評価は、出力の正確性、形式の遵守、および意図の理解度に基づいて行われます。

```python
import json
from jsonschema import validate, ValidationError
import re

def evaluate_function_calling_output(output_json: str, expected_call: dict, tool_schema: dict) -> dict:
    """
    Function Callingの出力を評価する。
    :param output_json: LLMからの出力JSON文字列
    :param expected_call: 期待される関数呼び出し {"name": "func_name", "args": {...}}
    :param tool_schema: ツール定義のJSON Schema
    :return: 評価結果 (dict)
    """
    results = {
        "is_valid_json": False,
        "is_schema_compliant": False,
        "function_name_match": False,
        "args_match": False,
        "score": 0.0
    }

    # 1. JSON形式の検証

    try:
        parsed_output = json.loads(output_json)
        results["is_valid_json"] = True
    except json.JSONDecodeError:
        return results # JSONが無効ならここで終了

    # 2. JSONスキーマの検証(Function Callingの出力構造全体)


    # 通常、ツール呼び出しは特定の構造(例: {"function_call": {...}})を持つため、それに対するスキーマを定義


    # ここでは簡略化のため、tool_schemaが関数の引数部分のみを指すと仮定し、


    # 実際のツール定義スキーマを別途用意する必要がある。


    # 例: Function Callingのトップレベルスキーマ

    function_call_schema = {
        "type": "object",
        "properties": {
            "function_call": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "args": tool_schema["function"]["parameters"] # 引数スキーマ
                },
                "required": ["name", "args"]
            }
        },
        "required": ["function_call"]
    }

    try:
        validate(instance=parsed_output, schema=function_call_schema)
        results["is_schema_compliant"] = True
    except ValidationError:
        pass # スキーマ不適合

    # 3. 関数名の一致

    if "function_call" in parsed_output and "name" in parsed_output["function_call"]:
        if parsed_output["function_call"]["name"] == expected_call["name"]:
            results["function_name_match"] = True
            results["score"] += 0.5

    # 4. 引数の一致(順序不問、キーと値の一致)

    if results["function_name_match"] and "args" in parsed_output["function_call"]:
        actual_args = parsed_output["function_call"]["args"]
        expected_args = expected_call["args"]
        if actual_args == expected_args: # 辞書比較
            results["args_match"] = True
            results["score"] += 0.5

    # スコアリングの調整

    if not results["is_valid_json"] or not results["is_schema_compliant"]:
        results["score"] = 0.0 # JSON形式またはスキーマ不適合ならスコア0

    return results

def evaluate_structured_output(output_json: str, expected_data: dict, schema: dict) -> dict:
    """
    構造化出力の出力を評価する。
    :param output_json: LLMからの出力JSON文字列
    :param expected_data: 期待される構造化データ (dict)
    :param schema: 期待される出力のJSON Schema
    :return: 評価結果 (dict)
    """
    results = {
        "is_valid_json": False,
        "is_schema_compliant": False,
        "data_match": False,
        "score": 0.0
    }

    # 1. JSON形式の検証

    try:
        parsed_output = json.loads(output_json)
        results["is_valid_json"] = True
    except json.JSONDecodeError:
        return results

    # 2. JSONスキーマの検証

    try:
        validate(instance=parsed_output, schema=schema)
        results["is_schema_compliant"] = True
    except ValidationError:
        pass

    # 3. データの正確性検証(期待値との比較)

    if results["is_valid_json"] and results["is_schema_compliant"]:

        # ここでは単純な辞書比較を行うが、実運用ではキーごとに比較ロジックを調整する

        if parsed_output == expected_data:
            results["data_match"] = True
            results["score"] = 1.0 # 完全に一致すれば1.0
        else:

            # 部分一致や特定のフィールドの重要度に応じてスコアを調整可能


            # 例: 必須フィールドが一致すれば0.5、全フィールド一致で1.0


            # 今回は単純化し、完全一致のみ1.0とする

            pass

    return results

# 例: JSON Schema for structured output

product_schema = {
  "type": "object",
  "properties": {
    "product_name": {"type": "string"},
    "price": {"type": "number"},
    "currency": {"type": "string", "enum": ["USD", "JPY", "EUR"]},
    "features": {"type": "array", "items": {"type": "string"}}
  },
  "required": ["product_name", "price", "currency"]
}

# LLM出力例 (成功)

llm_output_success = '{"product_name": "スマートウォッチX", "price": 299.99, "currency": "USD", "features": ["心拍数モニター", "GPS"]}'
expected_product_data = {"product_name": "スマートウォッチX", "price": 299.99, "currency": "USD", "features": ["心拍数モニター", "GPS"]}

# 評価実行


# print(evaluate_structured_output(llm_output_success, expected_product_data, product_schema))


# {"is_valid_json": true, "is_schema_compliant": true, "data_match": true, "score": 1.0}

誤り分析と抑制手法

Function Callingと構造化出力には固有の失敗モードがあり、これらを理解し、抑制する手法が必要です。

失敗モード

  1. 幻覚(Hallucination):

    • 内容: 存在しない関数を呼び出そうとする、または引数に誤った値や意味のない値を生成する。

    • : get_weather_forecast ツールしかないのに book_flight を呼び出そうとする。

  2. 様式崩れ(Format Deviation):

    • 内容: JSON Schemaで定義されたフォーマット(型、必須フィールド、列挙型など)に準拠しない出力を生成する。

    • : { "product_name": "...", "price": "299.99ドル" }priceが文字列型になっている)

  3. 脱線(Off-topic Generation):

    • 内容: ユーザーの意図やタスクから逸脱し、無関係なテキストを生成したり、不要な関数を呼び出したりする。

    • : 天気予報の質問に対して、レシピの関数を呼び出そうとする。

  4. 禁止事項違反:

    • 内容: セキュリティポリシーや運用ルールで禁じられているアクションを提案または実行しようとする。

    • : ユーザーの個人情報を抽出して外部に送信する関数を呼び出す。

抑制手法

  1. 厳密なSystem Instruction:

    • モデルの役割、遵守すべきルール、出力形式を明確かつ具体的に指示します。特に、ツール呼び出しをしない場合の挙動を定義することが重要です。

    • 例:「あなたはユーザーの質問にのみ答え、提供されたツールのみを使用してください。ツールが適切でない場合は、その旨をユーザーに伝えてください。」

  2. 詳細なJSON Schemaとツール定義:

    • 引数や出力フィールドの型、enumpatternminimum/maximumなどの制約を最大限に活用し、モデルが生成できる値の範囲を制限します。

    • 各ツールのdescriptionを明確にし、モデルがそのツールの用途を正確に理解できるようにします[1]。

  3. 応答のパースとバリデーション:

    • LLMからの出力を受け取った後、アプリケーション側でJSONパースを行い、JSON Schemaに照らしてバリデーションを厳格に実施します。

    • 不正な出力はエラーとして扱い、再試行やフォールバック処理に移行します。

  4. リトライ戦略とフォールバック:

    • バリデーションエラーが発生した場合、単に失敗とするのではなく、モデルにフィードバックを加えて再試行させる(例:「出力されたJSONがスキーマに準拠していません。再度正しい形式で生成してください。」)。

    • 複数回の再試行後も成功しない場合は、人間による介入を促す、デフォルトの応答を返す、あるいは別のLLMモデルに切り替えるなどのフォールバックメカニズムを用意します。

  5. 少数例の活用:

    • 成功例だけでなく、意図しない出力を避けるための「これは誤りである」というネガティブな例や、境界条件の例を少数例として提供することも有効です。

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

Function Callingと構造化出力の品質を継続的に向上させるためには、以下のサイクルを回すことが不可欠です。

graph TD
    A["プロンプト設計"] --> B("LLM実行");
    B --> C{"出力結果"};
    C -- 正しい --> D["評価"];
    C -- 間違っている --> D;
    D -- 成功 --> E["運用/デプロイ"];
    D -- 失敗 --> F["誤り分析"];
    F --> A;
    E --> G["監視/フィードバック"];
    G --> A;

図1: プロンプト設計から運用の継続的改善ループ

このループにおいて、プロンプト設計はLLMの振る舞いを定義し、LLM実行でその定義に基づいた出力が生成されます。評価ステップでは、自動評価スクリプトや手動レビューを用いて出力の品質が測定され、成功・失敗が判定されます。失敗した場合は誤り分析を通じて原因を特定し、プロンプトの改善点を見つけ出します。そして、再びプロンプト設計に戻ることで、システムの性能を段階的に向上させていきます。運用後の監視とフィードバックも、新たなユースケースやエッジケースの発見につながり、ループの起点となり得ます。

まとめ

LLMにおけるFunction Callingと構造化出力は、AIシステムが外部環境とインタラクトし、より賢明なタスクを実行するための強力な機能です。本記事では、ユースケース定義から始まり、入出力契約の確立、ゼロショット、少数例、Chain-of-Thought制約型といったプロンプト設計の手法、さらに自動評価のシナリオと擬似コードを紹介しました。また、幻覚や様式崩れといった失敗モードを分析し、厳密なSystem Instructionやバリデーション、リトライ戦略などの抑制手法を提示しました。これらの設計・評価・改良のプロセスを継続的に実施することで、信頼性と堅牢性の高いLLMアプリケーションを構築できます。


[1] OpenAI Blog. “Function calling and other API updates.” 2023年6月14日(JST)更新. https://openai.com/blog/function-calling-and-other-api-updates [2] Google AI for Developers. “Gemini API Function calling.” 2024年5月16日(JST)更新. https://ai.google.dev/docs/function_calling [3] JSON Schema. “JSON Schema Core.” 2020年12月15日(JST)公開. https://json-schema.org/draft/2020-12/json-schema-validation.html

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

コメント

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