Flaps

LangGraph エージェントのツール呼び出しを確認する

LangGraphを利用してモデルがどのようにツールを呼び出しているのか確認します

記事作成日:2025-11-29, 著者: Hi6

Overview

LangGraph でモデルノードとツールノードを定義し、LLM がどのようにツールを呼び出しているかを確認したいと思います。プロンプトに「ゴジラ2体 + ケンタウルス3体 - ヘビ1匹の足の合計は?」を投げてみます。動物の足の本数を取得できるツールで LLM が足の本数を取得し、その結果をつっかって、ツールで用意した計算用ツールを使用するところを確認します。

モデルの準備

from langchain_ollama import ChatOllama

model = ChatOllama(
    model="qwen3-vl:8b",
    temperature=0.0
)

各ツールの定義

@toolデコレータを使って各ツールを定義します。除算の場合は 0 除算でのエラーも試してみたいと思います。 各ツールが何をするのかを@tool(description="何をするのかの指示")でも指定できますが。これを指定しないとドックストリングの内容をdescriptionとして受け取ります。またツール名の指定も必要ですか指定しない場合は関数名がツール名になります。

from langchain.tools import tool, ToolException

@tool
def multiply(a: int, b: int) -> int:
    """Multiply `a` and `b`.

    Args:
        a: First int
        b: Second int
    """
    return a * b

@tool
def add(a: int, b: int) -> int:
    """Adds `a` and `b`.

    Args:
        a: First int
        b: Second int
    """
    return a + b

@tool
def divide(a: int, b: int) -> int:
    """Divide `a` and `b`.

    Args:
        a: First int
        b: Second int
    """
    if b == 0:
        raise ToolException("Cannot divide by zero")
    return a / b

@tool
def Sub(a: int, b: int) -> int:
    """Subtraction `a` and `b`.

    Args:
        a: First int
        b: Second int
    """
    return a - b

@tool
def search_animals_legs(name: str) -> int:
    """動物やモンスターの足の本数の辞書.

    Args:
        name: 動物名やモンスター
    """
    match name:
        case "ゴジラ":
            return 2
        case "ヘビ":
            return 1
        case "ウルトラマン":
            return 2
        case "スライム":
            return 0
        case "ケンタウルス":
            return 4
        case _:
            return 0


# ツールをリストでまとめておく
tools = [add, multiply, divide, Sub, search_animals_legs]
# LLMにツールをバインドしてツール呼び出しができるようにしておく
model_with_tools = model.bind_tools(tools)
# 各ツールをtool.nameで呼び出せるような辞書型の作成
# {Dict[str, RunnableTool]} を作成している、tool.nameが辞書のkey
tools_by_name = {tool.name: tool for tool in tools} 

モデルノードの定義

どのツールを呼び出すか選択するモデルノードを定義します。0除算に関する一文はこれを入れておかないとLLMが気を聞かせて、0で割るを0でプラスに変えて処理してしまいエラーが確認できなかったため入れています。 @taskデコレータで@toolと同じように、ドックストリングでタスクの動作を指示します。

from langchain_core.messages import BaseMessage
from langgraph.func import task
from langchain.messages import SystemMessage

@task
def call_llm(messages: list[BaseMessage]):
    """LLM はツールを呼び出すかどうかを決定します"""
    return model_with_tools.invoke(
        [
            SystemMessage(
                content="あなたは、一連の入力に対して演算を実行するという任務を負った、役に立つアシスタントです。動物やモンスターの足の本数の確認は search_animals_legs を、計算には[add, multiply, divide, Sub]使用してください。0除算によるエラー処理はツールに実装済みです。"
            )
        ]
        + messages
    )

ツールノードの定義

実際にツールをAIが用意した引数で呼び出すノードをタスク定義します。

from langgraph.func import task
from langchain.messages import ToolCall

@task
def call_tool(tool_call: ToolCall):
    # ToolCall:AI がツールを呼び出すリクエストに使用する型を表します。
    # {"name": "add", "args": {"a": 1}, "id": "123"}
    """ツール呼び出しを実行します"""
    tool = tools_by_name[tool_call["name"]]# tool名 がキーの辞書型。
    return tool.invoke(tool_call) # ツールの実行

エントリーポイントのエージェント定義

LangGraphのエントリーポイントを@entrypoint()デコレータで指定しエージェントを定義します。

from langgraph.func import entrypoint
from langchain_core.messages import BaseMessage

@entrypoint()
def agent(messages: list[BaseMessage]):
    # モデルノードを呼び出してツールの利用を判断させます。
    model_response = call_llm(messages).result()
    # LLM がツールの利用が必要だと判断した場合、
    # model_response.tool_callsに必要なモデルが
    # {'name': 'search_animals_legs', 'args': {'name': 'ゴジラ'}, 'id': '123', 'type': 'tool_call'}のようなToolCall型で返却されます。

    while True:
        if not model_response.tool_calls:
            # ツールの実行が必要ない場合、ループを抜けてそのままレスポンスを返す
            break

        tool_result_futures = [
            # LLMが要求した、必要なツールをToolCall型の引数で全て実行します
            call_tool(tool_call) for tool_call in model_response.tool_calls
        ]

        # ツール実行の結果をメッセージに追記
        # fut.result() の使用は、非同期実行の終了を待っていることを示唆します
        tool_results = [fut.result() for fut in tool_result_futures]
        messages = add_messages(messages, [model_response, *tool_results])
        # ツールの結果とメッセージを添えてLLMの実行
        model_response = call_llm(messages).result()

    # 最終メッセージを追記して返す
    messages = add_messages(messages, model_response)

    return messages

実行

messages = [HumanMessage(content=\
"(ゴジラ2体 + ケンタウルス3体) - ヘビ1匹の足の合計は?")]
# "(ゴジラ2体 + ケンタウルス3体) / スライム1匹の足の合計は?"を利用すれば0除算の例外発生を確認できるはずです。

for chunk in agent.stream(messages, stream_mode="updates"):
	# ツール呼び出しの出力
    if "call_tool" in chunk:
        print(f"called tool: {chunk["call_tool"].name}, \
        result : {chunk["call_tool"].content}")
    # 最終AI出力
    elif "agent" in chunk:
        chunk["agent"][-1].pretty_print()

下記が私の環境での実行結果ですが、LLMがタスクの実行に必要だと判断したツールをそれぞれ実行して情報を取得しさらにそれを利用して計算ツールを利用して計算処理をしている様子が見て取れます。

Called tool: search_animals_legs, result : 2 Called tool: search_animals_legs, result : 4 Called tool: search_animals_legs, result : 1 Called tool: multiply, result : 4 Called tool: multiply, result : 12 Called tool: add, result : 16 Called tool: Sub, result : 15 Ai Message ==================================

(ゴジラ2体 + ケンタウルス3体) - ヘビ1匹の足の合計は 15 本です。

計算の詳細:


[!NOTE] 参照サイト

LangGraph 公式ドキュメント