【AIツールの舞台裏】Tool CallingとFunction Callingの違いを実装コードで徹底解説!API設計のベストプラクティス
1. はじめに:なぜ今、この話題が重要なのか?
私たちは日々、ChatGPTやClaudeなどの大規模言語モデル(LLM)を活用したアプリケーションの開発に取り組んでいます。そんな中で、LLMを単なるチャットボットではなく、「実行可能なエージェント」 へと進化させる核心的な技術が、今回のテーマである Tool Calling (および Function Calling) です。
「天気を教えて」「Slackにメッセージを送信して」といったユーザーの自然言語のリクエストを、実際のAPI呼び出しやデータベース操作に変換する——。この魔法のような機能の背後では、どのような仕組みが動いているのでしょうか?本記事では、両者の概念の違いを明確にし、実際のPythonコードを用いてその実装方法、落とし穴、そしてクラウド上でのスケーラブルな運用方法までを解説します。
2. Tool Calling / Function Calling の総論
まずは混乱しがちな2つの用語を整理しましょう。結論から言うと、本質的にはほぼ同じ概念を指しています。
- Function Calling (関数呼び出し): OpenAIのGPTモデルにおいてこの機能が初めて大々的に導入された時の公式名称です。モデルがユーザーのクエリを解析し、開発者が事前に定義した「関数」(の仕様)を呼び出すべきだと判断すると、その関数の詳細と引数をJSON形式で返す機能です。
-
Tool Calling (ツール呼び出し): 「Function Calling」の後継となる、より一般的な概念です。呼び出せるものは「関数」だけではなく、APIエンドポイントや外部ツール全般を含むため、この名称に発展しました。OpenAI APIなどでは現在、
tools
パラメータとして定義します。
つまり、Function CallingはTool Callingの一種であり、現在ではTool Callingがより広義の標準的な用語として定着しつつあります。本記事でも以降はTool Callingに統一して説明します。
基本的なワークフロー
- 定義: 開発者がLLMに使わせたい機能(天気取得API、DB検索、計算など)を「ツール」として関数の名前、説明、パラメータ schema を定義します。
- 判断 & 生成: ユーザーの質問をLLMが解釈し、定義されたツールのうちどれを呼び出すべきか(あるいは呼び出さずに回答すべきか)を判断します。呼び出す場合は、ツールを呼び出すための完全なJSONデータ(関数名と引数)を生成します。
- 実行: LLMはツールを実行しません。生成されたJSONをアプリケーション側(私たちのコード)が受け取り、その指示に基づいて実際の関数やAPIを実行します。
- 応答: 実行結果(戻り値やAPIレスポンス)を再びLLMに渡します。LLMはその結果を解釈し、ユーザーにとって自然な形で整形して最終回答を生成します。
この仕組みの最大の利点は、LLM自身が外部のコードを実行する権限を持たないことです。あくまで「提案」をするだけで、実際の実行は私たちの安全なアプリケーション環境内で行われます。これはセキュリティ上、極めて重要です。
3. 実装例:OpenAI APIを使った実際のコード
ここからは、Pythonを用いた具体的な実装例を見ていきましょう。ここでは、OpenAI
Pythonライブラリを使用します。
ツールの定義
まずは、LLMが呼び出すことができる2つのツールを定義します。ここでは「現在の天気を取得するツール」と「2つの数値を計算するツール」を作成します。
重要なのは、実際の関数の実行コードと、その関数の説明をLLMに教えるための「schema」を分けて考えることです。
import openai
import json
from typing import get_type_hints
# 実際に実行される関数たち
def get_current_weather(location: str, unit: str = "celsius"):
"""
架空の天気APIを呼び出す関数(実際にはAPIキーなどが必要ですが、ここではダミーデータを返します)
"""
print(f"[DEBUG] 天気関数が呼び出されました: {location}, {unit}")
# 実際には requests ライブラリでAPIを呼び出す
return json.dumps({"location": location, "temperature": "22", "unit": unit, "forecast": ["sunny", "windy"]})
def calculator(a: int, b: int, operation: str):
"""
簡単な計算を行う関数
"""
print(f"[DEBUG] 計算関数が呼び出されました: {a}, {b}, {operation}")
if operation == "add":
result = a + b
elif operation == "sub":
result = a - b
elif operation == "mul":
result = a * b
elif operation == "div":
result = a / b
else:
raise ValueError("Invalid operation")
return json.dumps({"result": result, "operation": operation})
# Tool CallingのためにLLMに教えるツールの定義(スキーマ)
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "指定された場所の現在の天気を取得する",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "都市名、例えば'東京,日本'",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度の単位",
},
},
"required": ["location"],
},
},
},
{
"type": "function",
"function": {
"name": "calculator",
"description": "2つの数値に対して指定された演算(加算、減算、乗算、除算)を行う",
"parameters": {
"type": "object",
"properties": {
"a": {"type": "number", "description": "最初の数値"},
"b": {"type": "number", "description": "2番目の数値"},
"operation": {
"type": "string",
"enum": ["add", "sub", "mul", "div"],
"description": "行う計算の種類",
},
},
"required": ["a", "b", "operation"],
},
},
},
]
# ツールの実行をマッピングするディクショナリ
# このマッピングがあることで、LLMが返した関数名と実際の関数オブジェクトを紐付けられます。
available_functions = {
"get_current_weather": get_current_weather,
"calculator": calculator,
}
LLMとの対話とTool Callingの実行
次に、ユーザーのクエリを受け取り、LLMと連携してツールを実行するメインの処理を記述します。
# クライアントの初期化(環境変数 OPENAI_API_KEY の設定が必要です)
client = openai.OpenAI()
def run_conversation(user_query):
# Step 1: ユーザーのメッセージとツールの定義をLLMに送る
messages = [{"role": "user", "content": user_query}]
response = client.chat.completions.create(
model="gpt-3.5-turbo-1106", # または "gpt-4"
messages=messages,
tools=tools,
tool_choice="auto", # "none", "auto", または特定のツールを指定
)
response_message = response.choices[0].message
print(f"[LLMの最初の応答]: {response_message}")
# Step 2: LLMがTool Callingを要求してきているか確認
tool_calls = response_message.tool_calls
if tool_calls:
# LLMの応答を会話の履歴に追加
messages.append(response_message)
# Step 3: 各Tool Callに対して、実際の関数を実行
for tool_call in tool_calls:
function_name = tool_call.function.name
function_to_call = available_functions[function_name]
function_args = json.loads(tool_call.function.arguments)
print(f"[DEBUG] 呼び出す関数: {function_name}")
print(f"[DEBUG] 引数: {function_args}")
# 関数を実行
function_response = function_to_call(**function_args)
# Step 4: 実行結果をLLMに送信するためにメッセージに追加
messages.append({
"role": "tool",
"tool_call_id": tool_call.id, # どのTool Callの結果かを紐付ける
"content": function_response, # 関数の実行結果
})
# Step 5: 関数の実行結果をコンテキストに含めて、LLMに最終回答を生成させる
second_response = client.chat.completions.create(
model="gpt-3.5-turbo-1106",
messages=messages,
)
return second_response.choices[0].message.content
else:
# Tool Callingが不要な場合は、そのままの応答を返す
return response_message.content
# 実行例
if __name__ == "__main__":
query = "東京の天気はどうですか?そして、それと関係ないですけど15と17を足すといくらですか?"
final_answer = run_conversation(query)
print("\n--- 最終回答 ---")
print(final_answer)
【実行結果の例】
[LLMの最初の応答]: ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_abc123', function=Function(arguments='{"location":"東京都","unit":"celsius"}', name='get_current_weather'), type='function'), ChatCompletionMessageToolCall(id='call_def456', function=Function(arguments='{"a":15,"b":17,"operation":"add"}', name='calculator'), type='function')])
[DEBUG] 呼び出す関数: get_current_weather
[DEBUG] 引数: {'location': '東京都', 'unit': 'celsius'}
[DEBUG] 呼び出す関数: calculator
[DEBUG] 引数: {'a': 15, 'b': 17, 'operation': 'add'}
--- 最終回答 ---
東京の現在の天気は晴れで、風がやや強く、気温は22度です。
また、15と17を足すと32になります。
このように、LLMは一つのクエリで複数のツール(tool_calls
がリストであることに注目)を呼び出し、その結果を統合して自然な回答を生成しています。
4. 実践的なティップスとよくある落とし穴
ティップス
-
関数の説明(
description
)は最重要: LLMはこの説明だけを頼りにツールを使うか判断します。「どのような場面でこの関数を使うのか」 を自然言語で明確かつ正確に記述してください。曖昧な説明は誤った呼び出しの原因になります。 -
パラメータの説明も詳細に:
location
パラメータの例のように、具体的な例を示すことでLLMの出力の安定性が格段に向上します。 -
エラーハンドリングの徹底: 実際の関数実行部では、必ず例外処理(
try-except
)を実装し、エラーが発生した場合もその内容をLLMに伝えられるようにcontent
に含めましょう。LLMはエラーメッセージを解釈してユーザーに謝罪したり、別の方法を提案したりできます。 -
ツールの選択制御:
tool_choice
パラメータで"none"
(強制的にツールを使わない)や{"type": "function", "function": {"name": "calculator"}}
(強制的に特定のツールを使う)のように制御できます。ユーザーが明示的に「計算して」と言った場合は、強制的にcalculator
ツールを選択させるといった使い方が可能です。
よくある落とし穴
- 無限ループ: LLMの出力が常にツール呼び出しとなってしまい、会話が終わらない場合があります。会話のターン数に上限を設けるなどの対策が必要です。
- 不正確な引数: LLMが生成する引数が関数の期待する型と合わない場合があります(例: 数値が必要なところに文字列が渡される)。実行前に引数のバリデーションと型変換を行う堅牢なコードを書きましょう。
- コストとレイテンシ: 1回のやり取りで最大3回もAPIコール(ツール推論 → 実行 → 最終回答)が発生するため、コストと応答時間が増加します。本当にツールが必要なクエリかどうか、事前にある程度フィルタリングする検討も有効です。
- セキュリティリスク: ユーザー入力がそのまま関数の引数に渡ります。OSコマンドを実行するような危険な関数を安易にツールとして定義するのは絶対に避け、常に原則 of least privilegeを念頭に置いて実装してください。
5. 応用:クラウドネイティブな環境での実装
実際のプロダクション環境、特にAWSやGCPなどのクラウド上では、よりスケーラブルで堅牢な構成が必要になります。
- マイクロサービスアーキテクチャ: ツールの実体を個別のAPI(Lambda関数、Cloud Run, ECSタスク)としてデプロイします。アプリケーション本体は、LLMからの指示に基づいてこれらのAPIを呼び出す「オーケストレーター」の役割に徹します。
- 非同期処理: 時間のかかるツール実行(例: メール送信、動画変換)は、LLMの応答をブロックすべきではありません。メッセージキュー(SQS, Pub/Sub)を利用して、ツールの実行を非同期で処理し、結果は後からユーザーに通知するような設計が望ましいです。
-
モニタリングとロギング: どのツールがどのくらい呼び出されているか、失敗率はどれくらいか、をモニタリング(CloudWatch, Stackdriver)することは運用上極めて重要です。
tool_call_id
をリクエストIDとして活用し、一連の処理をトレースできるようにしましょう。
6. 結論
Tool Calling / Function Calling は、LLMを単なるテキスト生成器から、現実世界と相互作用できる実用的なエージェントへと昇華させるための決定的に重要な技術です。
-
优点
- 拡張性: LLMの本体知識に依存せず、最新の情報や独自の機能を安全に提供できる。
- 安全性: 実行権限は常にアプリケーション側が保持する。
- 自然な相互作用: ユーザーは自然言語で複雑な操作を指示できる。
-
缺点
- 設計の難しさ: ツールの説明文の設計は試行錯誤が必要で、その品質が成否を分ける。
- コストと速度: API呼び出しが複数回発生するため、コストとレイテンシが増大する傾向にある。
- エラー処理の複雑さ: ネットワークエラーやAPIのレート制限など、外部サービス依存によるエラー処理の必要性が高まる。
将来の展望
この技術は、AIエージェントやオートノマスな業務自動化の核心としてさらに発展していくでしょう。今後は、ツールの説明文を自動最適化したり、複数のツール呼び出しをより複雑に計画(Planning)できるより高度なエージェントフレームワーク(LangChain, AutoGen等)との連携が当たり前になっていくと予想されます。
本記事が、皆さんが実際にTool Callingを活用したアプリケーションを構築する第一歩となれば幸いです。ぜひ、自分だけの面白いツールを定義して、LLMに使わせてみてください!