Python & AI Tutorials Logo
LangChain & LangGraph

13. ツールをLLMに接続する

第12章では、エージェントが使用できるツールを構築しました。次は、これらのツールをLLMに接続し、エージェントが実際にツールを使えるようにする番です。

ツールがどれだけうまく作られていても、その存在を知らなければLLMはツールを使えません。LLMは何が利用可能かを知り、各リクエストに適したツールを選ぶ必要があります。しかし、第12章で学んだように、LLMはツールを直接実行するわけではありません。代わりに、「このツールをこれらの引数で呼び出してほしい」とリクエストし、私たちのエージェントコードがそのツールを呼び出します。この一連の仕組み全体をツール呼び出し(tool calling)と呼びます。

この章では、ツールをLLMにバインドし、ツール呼び出しリクエストを実行し、その結果をLLMに返すという完全なサイクルを実装します。このサイクルは、第14章で構築するエージェントループの基盤となります。

13.1) ツールのバインドとツール呼び出しリクエストの確認

LLMがツールを使うには、まず何が利用可能かを知る必要があります。各ツールの namedescription、入力スキーマをLLMに渡すと、LLMは各ツールをいつどのように使うべきかを学習します。このプロセスをツールバインド(tool binding)と呼びます。

ツールがバインドされると、質問を受け取ったLLMは2つのうちのいずれかを行います。ツールが不要な場合は、通常通りテキストで応答します。ツールが必要な場合は、「このツールをこれらの引数で呼び出してほしい」という構造化されたリクエストを返します。その後、エージェントがこのリクエストを確認し、指定されたツールを呼び出します。

まずはツールのバインド方法から始めましょう。

13.1.1) bind_tools() を使ったツールのバインド

ツールのバインドは、単一のメソッド bind_tools() で処理されます。ツール呼び出しをサポートするすべてのチャットモデルがこのメソッドを提供しています。bind_tools() を呼び出すと、指定したツールがバインドされた新しいモデルオブジェクトが返されます。

第12章で構築した get_weather ツールをバインドしてみましょう。

python
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() は引数としてツールのリストを取り、それらのツールがバインドされた新しいモデルオブジェクトを返します。内部的には、各ツールのメタデータ(namedescription、入力スキーマ)をLLMプロバイダーが理解できる形式に変換するため、モデルを呼び出すたびにツールスキーマが一緒に送信されます。

13.1.2) ツール呼び出しリクエストの確認

天気ツールがバインドされたモデルに天気に関する質問を送ると、何が起こるのでしょうか?

python
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つのステップがあります。

  1. tool_calls からツール名と引数を抽出し、対応するツールを実行する。
  2. 実行結果を ToolMessage変換する。
  3. 会話全体(ユーザーの質問 + LLMのツール呼び出しリクエスト + ツールの実行結果)をLLMに送信して応答を受け取る。

このプロセスの図を以下に示します。

LLMget_weatherエージェントユーザーLLMget_weatherエージェントユーザー"What's the weather in Paris?"HumanMessageAIMessage (tool_calls: get_weather)get_weather(city='Paris')"It's always sunny in Paris!"ToolMessageAIMessage ("The weather in Paris is sunny!")"The weather in Paris is sunny!"

13.2.1) ツールの実行と ToolMessage の作成

まず、tool_calls からツール名と引数を抽出し、対応するツールを実行しましょう。名前でツールを検索するために、ツール名をキーとする辞書を作成します。

python
# 名前でツールを検索するための辞書
tool_map = {get_weather.name: get_weather}

次に、tool_calls からツール呼び出しリクエストを取り出し、tool_map の中から一致するツールを見つけて実行します。

python
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_idtool_callsid。LLMはこれを使って、その結果がどのリクエストに属するかを識別します。
python
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 を返してくれます。

python
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つのコードにまとめてみましょう。

python
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 リストにどのようなメッセージが入っているか確認してみましょう。

python
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 と並べて calculatesearch_web を追加し、3つのツールをバインドしてみましょう。

python
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}

計算に関する質問をしてみましょう。

python
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と同じパターンを使って、ツールを実行し最終的な回答を得ることができます。

python
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が一度に複数のツール呼び出しをリクエストできるからです。

複数のツールを同時にリクエストする

複数のツールが同時にリクエストされた場合に何が起こるか見てみましょう。コードは同じで、変わるのは質問だけです。

python
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_weathercalculate の両方をリクエストし、for ループが tool_calls のすべてのリクエストを実行して、それぞれの結果をメッセージリストに追加しました。その後、LLMはこれらの結果を使って最終的な回答を生成しました。