LLM Function Calling設計と契約

Tech

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

LLM Function Calling設計と契約

ユースケース定義

LLM Function Callingは、大規模言語モデル(LLM)が外部ツールやAPIと連携し、自然言語の指示に基づいて特定のアクションを実行する機能です。本記事では、ユーザーが特定の都市の現在の天気を尋ねるシナリオを対象とします。LLMはユーザーの質問を解釈し、指定された都市の天気情報を取得するための外部API(例: call_weather_api)を適切に呼び出すことを目指します。

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

Function Callingを堅牢に機能させるためには、入出力の厳密な契約定義が不可欠です。

Function Callingの定義

利用可能なツールは以下の関数とします。

  • 関数名: call_weather_api

  • 説明: 指定された都市の現在の天気情報を取得します。

  • パラメータ:

    • city: str型。天気情報を取得したい都市の名前。非空文字列であり、有効な都市名である必要があります。

入力契約

  1. ユーザープロンプト: 自然言語のテキスト(例: 「東京の天気教えて」)。

  2. LLMへの入力: システム指示、ツール定義、ユーザープロンプト。

  3. LLMからのFunction Calling出力: LLMはツールを呼び出す場合、以下のJSON形式で出力します。

    • {"function_name": "call_weather_api", "parameters": {"city": "Tokyo"}}

    • cityパラメータは、call_weather_apiで定義されたstr型制約に準拠すること。

出力契約

  1. 外部API実行結果: call_weather_apiから返されるJSON形式のデータ(例: {"temperature": 25, "condition": "Sunny"})。

  2. LLMの最終応答: ユーザーに対して自然言語で状況を説明するテキスト(例: 「東京の現在の天気は晴れで、気温は25度です。」)。

失敗時の挙動

  • API呼び出しパラメータ不正:

    • LLMが生成したパラメータがツールのスキーマに適合しない場合(例: cityが数値)、LLMは修正を試みるか、ユーザーに明確なエラーメッセージを返す。

    • 外部システムがパラメータの有効性を検証し、エラーをLLMにフィードバック。

  • API実行エラー:

    • 外部APIがエラー(例: 存在しない都市名、ネットワーク障害)を返した場合、LLMはエラー内容をユーザーに伝え、代替案(例: 「申し訳ありませんが、その都市の天気情報は取得できませんでした」)を提示する。
  • 意図しないFunction Calling:

    • ユーザーの意図と異なるツール呼び出しを生成した場合、LLMは直接呼び出さずに、ユーザーにその呼び出しの意図を確認するプロンプトを返す。
  • ツール呼び出しの失敗: JSONパースエラーなど、システム的な失敗の場合は、システム側でエラーを検知し、LLMに再試行を促すか、エラーログを記録する。

禁止事項

  • ユーザーの許可なき機密情報の渡航: ユーザーが明示的に許可しない限り、個人情報や機密性の高い情報を外部APIに渡すことを禁止します。

  • 無限ループの発生: ツール呼び出しが連続し、システムに負荷をかけるような挙動を禁止します。システム側で呼び出し回数や深さの制限を設けます。

  • 機能の誤用: 定義された目的外でのツールの使用、またはユーザーが明示的に拒否した機能の呼び出しを禁止します。

プロンプト設計

以下に、天気APIを呼び出すためのプロンプト案を3種類提示します。

1. ゼロショットプロンプト

あなたは賢いアシスタントで、ユーザーの質問に答えるためにツールを使用できます。
使用可能なツールは以下の通りです。

ツール定義:
call_weather_api(city: str): 指定された都市の現在の天気を取得します。

ユーザーの要求を分析し、ツール呼び出しが必要な場合は、JSON形式でツール呼び出しを生成してください。ツールが不要な場合は、自然言語で直接応答してください。
JSON形式の例: {"function_name": "ツール名", "parameters": {"パラメータ名": "値"}}

ユーザー: 東京の天気は?
アシスタント:

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

ゼロショットプロンプトに具体的な入出力例を追加し、LLMの挙動を誘導します。

あなたは賢いアシスタントで、ユーザーの質問に答えるためにツールを使用できます。
使用可能なツールは以下の通りです。

ツール定義:
call_weather_api(city: str): 指定された都市の現在の天気を取得します。

ユーザーの要求を分析し、ツール呼び出しが必要な場合は、JSON形式でツール呼び出しを生成してください。ツールが不要な場合は、自然言語で直接応答してください。
JSON形式の例: {"function_name": "ツール名", "parameters": {"パラメータ名": "値"}}

例1:
ユーザー: 今日のニューヨークの天気はどう?
アシスタント: {"function_name": "call_weather_api", "parameters": {"city": "New York"}}

例2:
ユーザー: 株価を教えてください。
アシスタント: 申し訳ありませんが、株価に関する情報を提供するツールは利用できません。

例3:
ユーザー: 大阪の天気は?
アシスタント: {"function_name": "call_weather_api", "parameters": {"city": "大阪"}}

ユーザー: {{ユーザーの質問}}
アシスタント:

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

LLMに思考プロセスを明示させ、より信頼性の高いツール呼び出しを生成させます。

あなたは賢いアシスタントで、ユーザーの質問に答えるためにツールを使用できます。
使用可能なツールは以下の通りです。

ツール定義:
call_weather_api(city: str): 指定された都市の現在の天気を取得します。

ユーザーの要求を分析し、**まず、どのようなツールが必要か、なぜそのツールを使うのかを検討してください。その上で、ツール呼び出しが必要な場合は、JSON形式でツール呼び出しを生成してください。**
ツールが不要な場合は、自然言語で直接応答してください。
JSON形式の例: {"function_name": "ツール名", "parameters": {"パラメータ名": "値"}}

思考プロセス:

1. ユーザーの意図を理解する。

2. 意図を満たすために利用可能なツールがあるか確認する。

3. ツールのパラメータを特定する。

4. JSON形式でツール呼び出しを生成する、またはツールが不要な場合は自然言語で応答する。

ユーザー: 東京の天気は?
アシスタント:
思考: ユーザーは東京の天気を尋ねています。これは`call_weather_api`ツールで対応可能です。都市は「東京」です。
{"function_name": "call_weather_api", "parameters": {"city": "東京"}}

ユーザー: {{ユーザーの質問}}
アシスタント:

評価シナリオと自動評価

Function Callingの品質を保証するためには、多様なシナリオでの評価が重要です。

評価シナリオ

  • 正例:

    • 「福岡の天気は?」 -> call_weather_api(city="福岡")

    • 「今日のロンドンの天気」 -> call_weather_api(city="London")

  • 難例(曖昧性・複数要求・関連性なし):

    • 「京都の天気は?」(京都府か京都市か) -> 曖昧な場合は「京都市ですか、それとも京都府ですか?」と質問するか、デフォルト(京都市)で呼び出す。

    • 「東京と大阪の天気は?」 -> call_weather_api(city="東京")call_weather_api(city="大阪")を生成するか、複数都市に対応しない旨を伝える。

    • 「今日のニュースを教えて」 -> ツール呼び出しなし。

    • 「東今日」 (誤字脱字) -> 正しい都市名に修正して呼び出すか、ユーザーに確認。

  • コーナーケース(不正な入力):

    • 「天気は?」(都市名なし) -> ツール呼び出しなし、または都市名を尋ねる。

    • 「123」 -> エラー、またはツール呼び出しなし。

    • 「!@#$」 -> エラー、またはツール呼び出しなし。

自動評価の擬似コード

LLMの出力からFunction Callingを抽出し、期待値と比較する擬似コードをPythonで示します。

import json
import re

def evaluate_function_calling(model_output: str, expected_call: dict, scenario_type: str) -> dict:
    """
    LLMのFunction Calling出力を自動評価する関数。

    Args:
        model_output (str): LLMから生成された出力文字列。
        expected_call (dict or None): 期待されるFunction CallingのJSON辞書。
                                      ツール呼び出しが期待されない場合はNone。
        scenario_type (str): 評価シナリオのタイプ(例: "正例", "難例", "コーナーケース")。

    Returns:
        dict: 評価結果(スコア、理由、シナリオタイプ)。

    計算量: JSONパースはO(N) (Nは文字列長)、辞書比較はO(K) (Kはキー数)。
    メモリ: モデル出力、期待値の辞書サイズに依存。
    """
    score = 0.0
    reason = []
    actual_call = None

    # LLM出力からJSON形式のFunction Callingを抽出

    try:
        match = re.search(r'\{.*\}', model_output.strip(), re.DOTALL)
        if match:
            json_str = match.group(0)
            actual_call = json.loads(json_str)
        else:

            # JSON形式が見つからない場合、ツール呼び出しなしと判断

            actual_call = None
    except json.JSONDecodeError:
        actual_call = None
        reason.append("出力が有効なJSON形式ではありません。")

    # 評価ロジック

    if expected_call is None:  # ツール呼び出しが期待されないシナリオ
        if actual_call is None:
            score = 1.0
            reason.append("期待通りツール呼び出しがありませんでした。")
        else:
            score = 0.0
            reason.append(f"ツール呼び出しが期待されていませんでしたが、'{actual_call.get('function_name')}'が検出されました。")
    else:  # ツール呼び出しが期待されるシナリオ
        if actual_call:
            if actual_call.get("function_name") == expected_call.get("function_name"):
                reason.append("関数名が一致しました。")
                if actual_call.get("parameters") == expected_call.get("parameters"):
                    score = 1.0
                    reason.append("パラメータが期待通りでした。")
                else:

                    # パラメータの曖昧性や部分一致を考慮する場合、ここで詳細な比較を行う

                    score = 0.5  # 部分点
                    reason.append(f"パラメータが不一致です。期待: {expected_call.get('parameters')}, 実際: {actual_call.get('parameters')}")
            else:
                score = 0.0
                reason.append(f"期待する関数名と異なります。期待: '{expected_call.get('function_name')}', 実際: '{actual_call.get('function_name')}'")
        else:
            score = 0.0
            reason.append("ツール呼び出しが期待されましたが、検出されませんでした。")

    return {"score": score, "reason": " ".join(reason), "scenario_type": scenario_type}

# 例:


# print(evaluate_function_calling('{"function_name": "call_weather_api", "parameters": {"city": "福岡"}}', {"function_name": "call_weather_api", "parameters": {"city": "福岡"}}, "正例"))


# # 出力: {'score': 1.0, 'reason': '関数名が一致しました。パラメータが期待通りでした。', 'scenario_type': '正例'}


# print(evaluate_function_calling('申し訳ありませんが、株価に関する情報を提供するツールは利用できません。', None, "難例"))


# # 出力: {'score': 1.0, 'reason': '期待通りツール呼び出しがありませんでした。', 'scenario_type': '難例'}


# print(evaluate_function_calling('{"function_name": "call_news_api", "parameters": {"query": "今日のニュース"}}', {"function_name": "call_weather_api", "parameters": {"city": "東京"}}, "難例"))


# # 出力: {'score': 0.0, 'reason': "期待する関数名と異なります。期待: 'call_weather_api', 実際: 'call_news_api'", 'scenario_type': '難例'}

プロンプト設計・評価・改良のループ

Function Callingの性能を継続的に向上させるためには、プロンプト設計、LLM推論、評価、改良のフィードバックループを確立することが重要です。

graph LR
    A["プロンプト設計"] -- |新しいプロンプト| --> B("LLM推論")
    B -- |Function Calling結果| --> C{"自動評価"}
    C -- |評価指標 & 失敗例| --> D["評価レポート"]
    D -- |改善点特定| --> E["改善戦略立案"]
    E -- |プロンプト修正| --> A

失敗モードと抑制手法

LLM Function Callingの設計において、様々な失敗モードを想定し、その抑制策を講じることが重要です。本記事執筆時点の2024年7月29日現在、これらの課題は活発に研究・開発されています。

失敗モード 問題点 抑制手法
幻覚 (Hallucination) 存在しない関数名やパラメータを生成する。 システム指示でのツール定義の厳格化、出力スキーマの強制、API呼び出し前の厳密な検証。
様式崩れ (Format Breakage) JSON形式が崩れる、不完全なJSONを生成する。 JSONスキーマの提供と強制、出力パース後の厳格なバリデーション、リトライ戦略。
脱線 (Off-topic Calling) ユーザーの意図と異なるツールを呼び出す。 Few-shot例で非関連クエリに対する適切な拒否応答を示す、CoTで思考プロセスを明示。
禁止事項の逸脱 ユーザーの許可なく機密情報を使用、無限ループを引き起こす。 システム指示での明確な禁止事項の提示、ユーザーの明示的な確認プロンプト、サンドボックス環境でのAPI実行、Rate Limit。
パラメータ誤解釈 パラメータの値がユーザーの意図と異なる。 CoTによる思考プロセスの明示、Few-shot例での多様なパターン、正規表現による入力検証。

まとめ

LLM Function Callingは、LLMが外部ツールと連携することでその能力を飛躍的に拡張する強力な機能です。しかし、その効果的な利用には、明確な入出力契約の定義、ユースケースに合わせたプロンプト設計、そして体系的な評価と継続的な改良プロセスが不可欠です。本記事で示したように、各失敗モードに対する抑制策を講じながら、プロンプトの洗練と自動評価を繰り返すことで、より堅牢で信頼性の高いLLMシステムを構築することができます。

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

コメント

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