LLM Function Calling スキーマ設計の最適化戦略

Tech

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

LLM Function Calling スキーマ設計の最適化戦略

LLM(大規模言語モデル)のFunction Calling機能は、モデルがユーザーの意図を理解し、外部ツールやAPIを自律的に呼び出すことを可能にする強力なメカニズムです。本記事では、このFunction Callingを最大限に活用するためのスキーマ設計、プロンプト戦略、評価、誤り分析、そして継続的な改良サイクルについて解説します。

1. ユースケース定義

Function Callingの主なユースケースは、LLMが自然言語の指示を構造化されたAPI呼び出しに変換し、外部システムと連携することです。 例えば、以下のようなシナリオが挙げられます。

  • 情報取得: 「明日の東京の天気は?」 → get_weather(location="Tokyo", date="tomorrow")

  • データ操作: 「最新のニュース記事を検索して」 → search_news(query="latest news", limit=5)

  • タスク実行: 「山田さんの今日の会議をキャンセルして」 → cancel_meeting(attendee="Yamada", date="today")

  • 予約・購入: 「レストランを予約したい」 → reserve_restaurant(cuisine="Italian", time="19:00")

これらのユースケースでは、LLMが利用可能なツールの機能と引数を正確に理解し、適切なタイミングで呼び出す能力が求められます。

2. 入出力契約

Function Callingシステムの堅牢性を確保するためには、明確な入出力契約を定義することが不可欠です。

  • 入力:

    • ユーザープロンプト: LLMに与えられる自然言語の指示。

    • 利用可能な関数定義: LLMが呼び出し可能な関数群のスキーマ情報(JSON Schema形式)。

    • システム指示: LLMの振る舞いを制約・誘導する追加の指示。

  • 出力:

    • 関数呼び出し: ユーザーの意図に基づき、LLMが生成する構造化された関数呼び出し(JSON形式)。例: {"function_name": "get_weather", "arguments": {"location": "Tokyo", "unit": "celsius"}}

    • 自然言語応答: 関数呼び出しが不要な場合、または追加情報が必要な場合の自然言語による返答。

  • 失敗時の挙動:

    • エラーメッセージ: 指定された関数が見つからない、引数が無効などの場合、LLMは「指定されたツールは見つかりません」といったエラーメッセージを自然言語で返すか、定義されたエラーコードを返す。

    • 状況説明: 不明瞭な指示や必要な情報が不足している場合、LLMは追加情報を要求する自然言語応答を返す。

  • 禁止事項:

    • 未定義関数の呼び出し: 事前に定義されていない関数名の使用。

    • スキーマに準拠しない引数: 必須引数の欠落、データ型の不一致、enum範囲外の値の使用。

    • 無限ループの誘発: 意図せず同じ関数を繰り返し呼び出すような挙動。

    • 不適切な情報開示: ツール利用によって得られた情報を不適切にユーザーに開示すること。

3. 制約付き仕様化(スキーマ設計)

Function Callingの成功は、提供される関数スキーマの品質に大きく依存します。スキーマはJSON Schema形式で定義されることが一般的です(Google Gemini API [1]、OpenAI API [2]、Anthropic API [3]など)。

3.1 スキーマ定義の原則

  • 明確なdescription: 関数と各引数には、その目的、期待される値、制約を簡潔かつ明確に説明するdescriptionを記述します。

  • 単一目的の関数: 各関数は一つの明確なタスクを実行するように設計します。複雑な処理は複数の関数に分解します。

  • 適切な粒度: 関数は、あまりにも抽象的すぎず、また細かすぎず、適切な粒度で設計します。

  • 厳密な型定義とバリデーション: type (string, number, boolean, array, objectなど)、enumminimum/maximumpatternなどの制約を積極的に活用します。

  • 必須引数の指定: requiredフィールドを正確に指定し、LLMに必須引数を明確に伝えます。

3.2 スキーマ設計例

ユーザーの指示に基づいてレストランを検索する関数を例に挙げます。

{
  "name": "find_restaurant",
  "description": "ユーザーの指定に基づき、レストランを検索します。",
  "parameters": {
    "type": "object",
    "properties": {
      "cuisine": {
        "type": "string",
        "description": "検索する料理のジャンル(例: イタリアン、フレンチ、和食、中華)。",
        "enum": ["Italian", "French", "Japanese", "Chinese", "Indian", "Mexican", "Vegetarian"]
      },
      "location": {
        "type": "string",
        "description": "レストランを検索する都市または地域。"
      },
      "price_range": {
        "type": "string",
        "description": "価格帯(例: 'low', 'medium', 'high')。",
        "enum": ["low", "medium", "high"],
        "default": "medium"
      },
      "has_vegetarian_option": {
        "type": "boolean",
        "description": "ベジタリアンオプションがあるかどうかのフラグ。",
        "default": false
      }
    },
    "required": ["cuisine", "location"]
  }
}

このスキーマでは、cuisinelocationが必須であり、cuisineprice_rangeenumで選択肢を限定しています。これにより、LLMは曖昧な指示に対しても適切な引数を生成しやすくなります。

4. プロンプト設計

LLMがFunction Callingを適切に利用するためには、プロンプトの設計が鍵となります。以下に3つのプロンプト案を提示します。

4.1 ゼロショットプロンプト

Function Callingの最も基本的なアプローチです。システムプロンプトで役割と利用可能な関数を提示します。

SYSTEM:
あなたはユーザーの質問に回答するために、利用可能なツールを呼び出すAIアシスタントです。
ツールを呼び出す場合は、以下の形式でJSONを出力してください:
{"function_name": "...", "arguments": {...}}
利用可能なツール:
{"name": "find_restaurant", "description": "ユーザーの指定に基づき、レストランを検索します。...", "parameters": {...}}
{"name": "get_current_time", "description": "現在の時刻を取得します。引数は不要です。", "parameters": {"type": "object", "properties": {}}}

USER:
新宿でイタリアンレストランを探してください。ベジタリアンオプションがあると嬉しいです。

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

いくつかの具体例(正例と難例)を提示することで、LLMの挙動をより細かくガイドします。

SYSTEM:
あなたはユーザーの質問に回答するために、利用可能なツールを呼び出すAIアシスタントです。
ツールを呼び出す場合は、以下の形式でJSONを出力してください:
{"function_name": "...", "arguments": {...}}
利用可能なツール:
{"name": "find_restaurant", "description": "ユーザーの指定に基づき、レストランを検索します。...", "parameters": {...}}
{"name": "get_current_time", "description": "現在の時刻を取得します。引数は不要です。", "parameters": {"type": "object", "properties": {}}}

Example 1:
USER: 東京でフレンチレストランを探して。
ASSISTANT: {"function_name": "find_restaurant", "arguments": {"cuisine": "French", "location": "Tokyo"}}

Example 2:
USER: 大阪で中華、高価格帯で、ベジタリアンオプションありのレストラン。
ASSISTANT: {"function_name": "find_restaurant", "arguments": {"cuisine": "Chinese", "location": "Osaka", "price_range": "high", "has_vegetarian_option": true}}

Example 3 (Ambiguous):
USER: 何か美味しいレストランを探して。
ASSISTANT: どの地域のどんな料理がお好みですか?もう少し詳しく教えていただけますか?

USER:
札幌で和食のレストランを探してほしいな。

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

LLMに思考プロセスを明示させ、ツール呼び出しに至る理由付けをさせることで、より堅牢な判断を促します。

SYSTEM:
あなたはユーザーの質問に回答するために、利用可能なツールを呼び出すAIアシスタントです。
ツールを呼び出す場合は、まず思考プロセスを`<thought>`タグ内に記述し、その後に以下の形式でJSONを出力してください:
{"function_name": "...", "arguments": {...}}
利用可能なツール:
{"name": "find_restaurant", "description": "ユーザーの指定に基づき、レストランを検索します。...", "parameters": {...}}
{"name": "get_current_time", "description": "現在の時刻を取得します。引数は不要です。", "parameters": {"type": "object", "properties": {}}}

USER:
渋谷でメキシカンレストランを探してください。

LLMの期待される出力例:

<thought>ユーザーは渋谷でメキシカンレストランを探している。find_restaurantツールが適切であり、cuisineとlocation引数を指定する必要がある。</thought>
{"function_name": "find_restaurant", "arguments": {"cuisine": "Mexican", "location": "Shibuya"}}

5. 評価

Function Callingの評価は、モデルがユーザーの意図を正確に解釈し、適切な関数と引数を生成できるかを測定します。

5.1 評価シナリオ

  • 正例(Happy Path): 明確な指示に対し、正しく関数が呼び出されるケース。

    • 例: 「明日の東京の天気」→ get_weather(location="Tokyo", date="tomorrow")
  • 難例(Ambiguous/Complex):

    • 情報不足: 「レストランを探して」→追加情報要求

    • 複数ステップ: 「A社の株価を取得し、もし高ければ購入を検討する」→ get_stock_price("A") の後、購入検討のための別の関数呼び出しまたは自然言語応答

    • 否定形: 「天気は知りたくない」→関数を呼び出さない

  • コーナーケース(Edge Case):

    • 無関係な質問: 「今日の晩御飯は何?」→ツール不要

    • スキーマ外の引数: enumにない値の指定→エラーまたは代替案提示

    • 複数の適合するツール: 優先順位付けの確認

5.2 自動評価の擬似コード

Pythonと正規表現、JSONスキーマバリデーションを用いた自動評価の例です。

import json
import re
from jsonschema import validate, ValidationError

# 定義されたFunction Callingスキーマ

FUNCTION_SCHEMAS = {
    "find_restaurant": {
        "type": "object",
        "properties": {
            "cuisine": {"type": "string", "enum": ["Italian", "French", "Japanese", "Chinese", "Indian", "Mexican", "Vegetarian"]},
            "location": {"type": "string"},
            "price_range": {"type": "string", "enum": ["low", "medium", "high"], "default": "medium"},
            "has_vegetarian_option": {"type": "boolean", "default": False}
        },
        "required": ["cuisine", "location"]
    },
    "get_current_time": {
        "type": "object",
        "properties": {}
    }
}

def evaluate_function_call(llm_output: str, expected_call: dict, expected_response_type: str = "function_call") -> dict:
    """
    LLMの出力が期待されるFunction Callingまたは自然言語応答と一致するかを評価する。
    :param llm_output: LLMからの出力文字列
    :param expected_call: 期待される関数呼び出し辞書(自然言語応答の場合はNone)
    :param expected_response_type: "function_call" または "natural_language"
    :return: 評価結果(成功/失敗、理由)
    """
    result = {"success": False, "reason": ""}

    if expected_response_type == "natural_language":
        if not re.search(r'{"function_name":', llm_output):
            result["success"] = True
            result["reason"] = "Expected natural language, no function call detected."
        else:
            result["reason"] = "Expected natural language, but detected a function call."
        return result

    # Function Callingの正規表現パターン

    match = re.search(r'{"function_name":\s*"(?P<name>[^"]+)",\s*"arguments":\s*(?P<args>{.*?})}', llm_output)

    if not match:
        result["reason"] = "No valid function call JSON detected."
        return result

    try:
        function_name = match.group("name")
        arguments_str = match.group("args")
        arguments = json.loads(arguments_str)

        # 1. 関数名の検証

        if function_name != expected_call["function_name"]:
            result["reason"] = f"Function name mismatch. Expected '{expected_call['function_name']}', got '{function_name}'."
            return result

        # 2. スキーマバリデーション

        if function_name not in FUNCTION_SCHEMAS:
            result["reason"] = f"Called an undefined function: '{function_name}'."
            return result

        # 3. 引数の値とスキーマの検証

        try:
            validate(instance=arguments, schema=FUNCTION_SCHEMAS[function_name])
        except ValidationError as e:
            result["reason"] = f"Argument validation failed for '{function_name}': {e.message}"
            return result

        # 4. 期待される引数との比較 (CoTプロンプトの場合、思考タグの除去が必要)

        actual_call = {"function_name": function_name, "arguments": arguments}
        if actual_call == expected_call:
            result["success"] = True
            result["reason"] = "Function call matches expected output."
        else:
            result["reason"] = f"Function call argument mismatch. Expected {expected_call}, got {actual_call}."

    except json.JSONDecodeError:
        result["reason"] = "Malformed JSON in function call."
    except Exception as e:
        result["reason"] = f"An unexpected error occurred during evaluation: {str(e)}"

    return result

# 評価例 (2024年7月29日 JST)


# llm_output_example = '{"function_name": "find_restaurant", "arguments": {"cuisine": "Italian", "location": "Shinjuku", "has_vegetarian_option": true}}'


# expected_output_example = {"function_name": "find_restaurant", "arguments": {"cuisine": "Italian", "location": "Shinjuku", "has_vegetarian_option": True}}


# print(evaluate_function_call(llm_output_example, expected_output_example))

6. 誤り分析と抑制手法

Function Callingにおける主な失敗モードとその抑制手法は以下の通りです。

  • 幻覚(Hallucination):

    • 現象: 存在しない関数や引数を呼び出す、スキーマにない値を生成する。

    • 抑制手法:

      • 厳密なスキーマ設計: enumpatternを多用し、LLMが自由に生成できる範囲を狭める。

      • システム指示の強化: 「提供されたツール以外は使用しないこと」「引数はスキーマに厳密に従うこと」などを明記。

      • 少数例プロンプト: 正しい呼び出し方だけでなく、誤った呼び出しを避ける例も提示する。

      • スキーマバリデーション: LLMの出力後に必ずスキーマに従って検証し、不適合な場合はリトライまたはエラーを返す。

  • 様式崩れ(Malformed Output):

    • 現象: JSON形式が壊れている、必須引数が欠落している。

    • 抑制手法:

      • 明確な出力フォーマット指示: プロンプトでJSONの形式を具体的に示す。

      • CoTプロンプト: 思考プロセスを挟むことで、構造化された出力への意識を高める。

      • リトライ戦略: JSONパースエラーが発生した場合、一定回数プロンプトを再送し、修正を促す。

      • 出力後処理: 不完全なJSONを修正する簡単な正規表現やパーサーを実装する。

  • 脱線(Irrelevant Function Call):

    • 現象: ユーザーの意図に無関係な関数を呼び出す、自然言語応答で十分な場合にツールを呼び出す。

    • 抑制手法:

      • 関数のdescriptionの精度向上: 各関数がカバーするスコープを明確にする。

      • システム指示の強化: 「ツールが必要ない場合は、自然言語で直接回答すること」を明記。

      • 難例を含む少数例: ツールが不要な場合の対応を示す。

  • 禁止事項違反:

    • 現象: スキーマで許容されていない引数の組み合わせや、ビジネスロジックで禁止されている操作を実行しようとする。

    • 抑制手法:

      • スキーマ設計の工夫: 可能な限りスキーマレベルで制約を表現する。

      • 後続の検証ステップ: ツール実行前に、出力された関数呼び出しをビジネスロジックで検証する層を設ける。

7. 改良と再評価

Function Callingの最適化は、一度きりのプロセスではありません。プロンプト、スキーマ、モデルは継続的に改良されるべきです。

  1. 評価データの収集: 実際のユーザーインタラクションや評価シナリオから多様なデータ(成功例、失敗例)を収集します。

  2. 誤り分析: 失敗したケースを詳細に分析し、その根本原因(スキーマの不備、プロンプトの曖昧さ、モデルの限界など)を特定します。

  3. 改良:

    • スキーマの修正: descriptionの改善、enumの追加、requiredフィールドの調整など。

    • プロンプトの改善: システム指示の明確化、少数例の追加・修正、CoTの導入など。

    • モデルの選択: 必要に応じて、よりFunction Calling性能の高いモデルバージョンや、ファインチューニングを検討。

  4. 再評価: 改良後のシステムを、新しい評価データセットや既存の評価データセットで再度評価し、改善度を測定します。このサイクルを繰り返すことで、Function Callingの精度と堅牢性を段階的に向上させることができます。

8. プロンプトエンジニアリングの改良サイクル(Mermaid Flowchart)

graph TD
    A["ユーザープロンプト & 関数スキーマ"] --> B{LLM}
    B -- 関数呼び出し/自然言語 --> C["モデル出力"]
    C --> D{"評価ツール"}
    D -- 成功/失敗ログ & 理由 --> E["誤り分析"]
    E --> F["スキーマ/プロンプト改良"]
    F --> A
    D -- 評価レポート --> G["システム改善の意思決定"]
  • ノードA: LLMへの入力データ(ユーザーからの問い合わせと、利用可能な関数に関する定義情報)を示します。

  • ノードB: 大規模言語モデル(LLM)自体を表し、Function Callingを実行するコアコンポーネントです。

  • ノードC: LLMが生成する具体的な出力(関数呼び出しのJSON形式、または自然言語による応答)です。

  • ノードD: モデルの出力を自動的または手動で評価するツールやプロセスを示します。スキーマバリデーションや期待される出力との比較が行われます。

  • ノードE: 評価結果に基づき、失敗したケースの原因を特定し、問題点を発見するプロセスです。

  • ノードF: 誤り分析で得られた知見に基づいて、関数スキーマやプロンプトの記述を改善するステップです。

  • ノードG: 評価レポートや分析結果を基に、Function Callingシステムの全体的な改善方針を決定します。

9. まとめ

LLM Function Callingは、AIアプリケーションの可能性を大きく広げる強力な機能です。効果的なスキーマ設計、戦略的なプロンプト作成、継続的な評価と改良のサイクルを通じて、LLMのツール利用能力を最大化し、より堅牢でインテリジェントなアプリケーションを構築することが可能になります。本記事で述べた原則と手法が、皆様のFunction Calling実装の一助となれば幸いです。


[1] Google Gemini API. Function Calling. https://ai.google.dev/docs/function_calling (最終アクセス日: 2024年7月29日 JST) [2] OpenAI API. Tool use. https://platform.openai.com/docs/guides/function-calling (最終アクセス日: 2024年7月29日 JST) [3] Anthropic. Tools. https://docs.anthropic.com/en/docs/tool-use (最終アクセス日: 2024年7月29日 JST)

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

コメント

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