13. ツールをLLMに接続する
第12章では、エージェントが使用できるツールを構築しました。次は、これらのツールをLLMに接続し、エージェントが実際にツールを使えるようにする番です。
ツールがどれだけうまく作られていても、その存在を知らなければLLMはツールを使えません。LLMは何が利用可能かを知り、各リクエストに適したツールを選ぶ必要があります。しかし、第12章で学んだように、LLMはツールを直接実行するわけではありません。代わりに、「このツールをこれらの引数で呼び出してほしい」とリクエストし、私たちのエージェントコードがそのツールを呼び出します。この一連の仕組み全体をツール呼び出し(tool calling)と呼びます。
この章では、ツールをLLMにバインドし、ツール呼び出しリクエストを実行し、その結果をLLMに返すという完全なサイクルを実装します。このサイクルは、第14章で構築するエージェントループの基盤となります。
13.1) ツールのバインドとツール呼び出しリクエストの確認
LLMがツールを使うには、まず何が利用可能かを知る必要があります。各ツールの name、description、入力スキーマをLLMに渡すと、LLMは各ツールをいつどのように使うべきかを学習します。このプロセスをツールバインド(tool binding)と呼びます。
ツールがバインドされると、質問を受け取ったLLMは2つのうちのいずれかを行います。ツールが不要な場合は、通常通りテキストで応答します。ツールが必要な場合は、「このツールをこれらの引数で呼び出してほしい」という構造化されたリクエストを返します。その後、エージェントがこのリクエストを確認し、指定されたツールを呼び出します。
まずはツールのバインド方法から始めましょう。
13.1.1) bind_tools() を使ったツールのバインド
ツールのバインドは、単一のメソッド bind_tools() で処理されます。ツール呼び出しをサポートするすべてのチャットモデルがこのメソッドを提供しています。bind_tools() を呼び出すと、指定したツールがバインドされた新しいモデルオブジェクトが返されます。
第12章で構築した get_weather ツールをバインドしてみましょう。
from langchain_openai import ChatOpenAI
from langchain.tools import tool
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""指定された都市の現在の天気を取得します。"""
return f"It's always sunny in {city}!"
# ツールをモデルにバインドする — ツールがバインドされた新しいモデルオブジェクトが返される
llm_with_tools = llm.bind_tools([get_weather])これだけです。返された llm_with_tools は、get_weather ツールについて知っているモデルです。元の llm はまったく変更されていない点に注目してください。元の llm は依然としてどのツールについても知りません。
bind_tools() は引数としてツールのリストを取り、それらのツールがバインドされた新しいモデルオブジェクトを返します。内部的には、各ツールのメタデータ(name、description、入力スキーマ)をLLMプロバイダーが理解できる形式に変換するため、モデルを呼び出すたびにツールスキーマが一緒に送信されます。
13.1.2) ツール呼び出しリクエストの確認
天気ツールがバインドされたモデルに天気に関する質問を送ると、何が起こるのでしょうか?
response = llm_with_tools.invoke("What's the weather in Paris?")
print(f"type: {type(response)}\n")
print(f"tool_calls: {response.tool_calls}\n")
print(f"content: {repr(response.content)}\n")出力:
type: <class 'langchain_core.messages.ai.AIMessage'>
tool_calls: [{'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': 'call_hgXrHGD', 'type': 'tool_call'}]
content: ''llm_with_tools は依然としてチャットモデルなので、AIMessage を返します。しかし、この応答には2つの違いがあります。
第一に、response.tool_calls にツール呼び出しリクエストのリストが含まれています。 各リクエストは4つのキーを持つ辞書です。
name— 呼び出すツールの名前。これはツールのメタデータのnameに対応します。args— ツールを呼び出す際に渡す引数。モデルは入力スキーマとユーザーの質問を見て{'city': 'Paris'}を構築しました。id— この呼び出しの一意の識別子。LLMは後でこれを使って、ツールの実行結果とツール呼び出しリクエストを照合します。type— 常に'tool_call'です。
第二に、response.content は空文字列です。 この応答は最終的なテキスト回答ではなく、ツール呼び出しリクエストです。
応答がツール呼び出しリクエストかどうかを判断するには、content ではなく tool_calls を確認します。tool_calls が空でなければツール呼び出しリクエストです。空であれば、LLMが直接テキスト回答を提供したことになります。
13.2) ツールの実行と結果の返送
LLMが get_weather ツールの呼び出しをリクエストしました。次に、私たちのエージェントコードがツールを実行し、その結果をLLMに返す必要があります。
ツール呼び出しリクエストの処理には3つのステップがあります。
tool_callsからツール名と引数を抽出し、対応するツールを実行する。- 実行結果を
ToolMessageに変換する。 - 会話全体(ユーザーの質問 + LLMのツール呼び出しリクエスト + ツールの実行結果)をLLMに送信して応答を受け取る。
このプロセスの図を以下に示します。
13.2.1) ツールの実行と ToolMessage の作成
まず、tool_calls からツール名と引数を抽出し、対応するツールを実行しましょう。名前でツールを検索するために、ツール名をキーとする辞書を作成します。
# 名前でツールを検索するための辞書
tool_map = {get_weather.name: get_weather}次に、tool_calls からツール呼び出しリクエストを取り出し、tool_map の中から一致するツールを見つけて実行します。
if response.tool_calls:
tool_call = response.tool_calls[0]
# {'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': 'call_hgXrHGD', 'type': 'tool_call'}
selected_tool = tool_map[tool_call["name"]]
result = selected_tool.invoke(tool_call["args"])
print(result) # 出力: It's always sunny in Paris!ツールの実行結果をLLMに返すには、それを ToolMessage でラップする必要があります。ToolMessage には必須フィールドが2つあります。
content— 文字列としてのツールの実行結果。tool_call_id—tool_callsのid。LLMはこれを使って、その結果がどのリクエストに属するかを識別します。
from langchain_core.messages import ToolMessage
if response.tool_calls:
tool_call = response.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
result = selected_tool.invoke(tool_call["args"])
tool_message = ToolMessage(
content=result,
tool_call_id=tool_call["id"], # リクエストの id と一致している必要がある
)
print(tool_message)出力:
content="It's always sunny in Paris!" tool_call_id='call_hgXrHGD'上記のコードでは、args を渡してツールを実行し、その結果を tool_call_id と組み合わせて ToolMessage を構築しました。
これをワンステップで行うより簡単な方法があります。.invoke() に args だけを渡す代わりに、tool_call 辞書全体を渡します。LangChainがツールを実行し、自動的に ToolMessage を返してくれます。
if response.tool_calls:
tool_call = response.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
print(tool_message)出力:
content="It's always sunny in Paris!" tool_call_id='call_hgXrHGD'結果は同じですが、コードがはるかにシンプルになり、tool_call_id の不一致が起こる可能性もありません。今後はこのアプローチを使用します。
13.2.2) ツールの結果をLLMに送信する
それでは、ツールの実行結果をLLMに渡しましょう。会話履歴全体(ユーザーの質問、LLMのツール呼び出しリクエスト、ツールの実行結果)をLLMに送り返すと、LLMはツールの結果に基づいて回答を生成します。
プロセス全体を最初から最後まで1つのコードにまとめてみましょう。
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
# ツールを定義してバインドする
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""指定された都市の現在の天気を取得します。"""
return f"It's always sunny in {city}!"
llm_with_tools = llm.bind_tools([get_weather])
tool_map = {get_weather.name: get_weather}
# ステップ1: ユーザーの質問を送信し、LLMの応答を受け取る
messages = [HumanMessage("What's the weather in Paris?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
# ステップ2: ツール呼び出しがリクエストされた場合、ツールを実行して結果を追加する
if ai_msg.tool_calls:
tool_call = ai_msg.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
# ステップ3: 会話履歴全体をLLMに送り返す
final_response = llm_with_tools.invoke(messages)
print(final_response.content)出力:
The current weather in Paris is sunny. Have a great day!LLMはツールの実行結果に基づいて最終的なテキスト回答を生成しました。13.1では content は空文字列でしたが、ツールの結果を返した後は、実際の回答を受け取ることができました。
ステップ3の時点で messages リストにどのようなメッセージが入っているか確認してみましょう。
for m in messages:
print(f"{type(m).__name__}: {m.content!r}")出力:
HumanMessage: "What's the weather in Paris?"
AIMessage: ''
ToolMessage: "It's always sunny in Paris!"3つのメッセージが順番に格納されています。ユーザーの質問、LLMのツール呼び出しリクエスト(空のコンテンツ)、そしてツールの実行結果です。LLMはユーザーの質問を見て、ツールが必要だと判断し、get_weather の呼び出しをリクエストしました。エージェントコードは get_weather を実行し、その結果をメッセージリストに追加しました。必要なものがすべて揃ったところで、LLMは最終的な応答を生成しました。
13.3) LLMに複数のツールを与える
実際のエージェントは通常、複数のツールを持っています。これまで説明してきたことはすべて、複数のツールでもまったく同じように動作します。
get_weather と並べて calculate と search_web を追加し、3つのツールをバインドしてみましょう。
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""指定された都市の現在の天気を取得します。"""
return f"It's always sunny in {city}!"
@tool
def calculate(expression: str) -> str:
"""'123 * 456' のような単純な算術式を評価します。"""
return str(eval(expression)) # 警告: eval() はセキュリティリスクです。本番環境では使用しないでください。
@tool
def search_web(query: str) -> str:
"""あるトピックに関する最新情報をウェブで検索します。"""
return f"Top result for '{query}': ..."
tools = [get_weather, calculate, search_web]
llm_with_tools = llm.bind_tools(tools)
tool_map = {t.name: t for t in tools}計算に関する質問をしてみましょう。
response = llm_with_tools.invoke("What is 123 multiplied by 456?")
print(response.tool_calls)出力:
[{'name': 'calculate', 'args': {'expression': '123 * 456'}, 'id': 'call_xyz789', 'type': 'tool_call'}]LLMは calculate の呼び出しをリクエストしました。各ツールの description をユーザーの質問と比較し、calculate が適切なツールであると判断したのです。
13.2と同じパターンを使って、ツールを実行し最終的な回答を得ることができます。
messages = [HumanMessage("What is 123 multiplied by 456?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
if ai_msg.tool_calls:
for tool_call in ai_msg.tool_calls:
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
final_response = llm_with_tools.invoke(messages)
print(final_response.content) # 出力: 123 multiplied by 456 is 56,088.13.2では tool_calls[0] で最初のリクエストだけを処理しましたが、ここでは for ループですべてのリクエストを反復処理しています。その理由は、LLMが一度に複数のツール呼び出しをリクエストできるからです。
複数のツールを同時にリクエストする
複数のツールが同時にリクエストされた場合に何が起こるか見てみましょう。コードは同じで、変わるのは質問だけです。
messages = [HumanMessage("What's the weather in Tokyo, and what is 123 * 456?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
if ai_msg.tool_calls:
for tool_call in ai_msg.tool_calls:
print(f"Tool: {tool_call['name']}")
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
final_response = llm_with_tools.invoke(messages)
print(final_response.content)出力:
Tool: get_weather
Tool: calculate
The weather in Tokyo is currently sunny, and 123 multiplied by 456 is 56,088.LLMは get_weather と calculate の両方をリクエストし、for ループが tool_calls のすべてのリクエストを実行して、それぞれの結果をメッセージリストに追加しました。その後、LLMはこれらの結果を使って最終的な回答を生成しました。