本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
LLM Function Calling設計と契約
ユースケース定義
LLM Function Callingは、大規模言語モデル(LLM)が外部ツールやAPIと連携し、自然言語の指示に基づいて特定のアクションを実行する機能です。本記事では、ユーザーが特定の都市の現在の天気を尋ねるシナリオを対象とします。LLMはユーザーの質問を解釈し、指定された都市の天気情報を取得するための外部API(例: call_weather_api)を適切に呼び出すことを目指します。
制約付き仕様化(入出力契約)
Function Callingを堅牢に機能させるためには、入出力の厳密な契約定義が不可欠です。
Function Callingの定義
利用可能なツールは以下の関数とします。
関数名:
call_weather_api説明: 指定された都市の現在の天気情報を取得します。
パラメータ:
city:str型。天気情報を取得したい都市の名前。非空文字列であり、有効な都市名である必要があります。
入力契約
ユーザープロンプト: 自然言語のテキスト(例: 「東京の天気教えて」)。
LLMへの入力: システム指示、ツール定義、ユーザープロンプト。
LLMからのFunction Calling出力: LLMはツールを呼び出す場合、以下のJSON形式で出力します。
{"function_name": "call_weather_api", "parameters": {"city": "Tokyo"}}cityパラメータは、call_weather_apiで定義されたstr型制約に準拠すること。
出力契約
外部API実行結果:
call_weather_apiから返されるJSON形式のデータ(例:{"temperature": 25, "condition": "Sunny"})。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システムを構築することができます。

