AIエージェント・MCP

LangGraphのTool Calling実装|ToolNode・create_agent・エラー処理とループ上限【2026年10月】

LangGraphのTool Calling実装|ToolNode・create_agent・エラー処理とループ上限【2026年10月】

Tool Callingは「LLMがツールを実行する機能」ではありません。モデルが返すのは「このツールをこの引数で呼んでほしい」というtool_callsだけで、実行するのはアプリケーション側です。LangGraphでは、その実行役がToolNode、往復を続けるか終えるかの判定役がtools_conditionです。この記事は langgraph 1.2.18(2026-10-26公開)・langchain 1.4.7 で挙動を実測し、最小構成のコード、create_agentへの移行、ツールからstateを書き換える方法、ツール例外とループの止め方、GPT-6とClaude 5.5系でつまずくtool_choiceの制約までをまとめます。

まとめ

  • モデルはtool_calls(name・args・id)を返すだけ。実行結果は同じidをtool_call_idに入れたToolMessageで戻す。
  • 最小構成は@tool→bind_tools()→ToolNode→tools_condition。ツールノードの名前を"tools"以外にすると、行き先の辞書を渡さない限りコンパイル時にValueErrorになる。
  • create_react_agentはLangGraph 1.0で非推奨、2.0で削除予定。移行先はlangchain.agents.create_agent。ToolNodeとtools_conditionは現役。
  • ツール内で起きた例外の扱いは既定で拾われない。ToolNodeの既定が拾うのは引数の型違いと存在しないツール名だけで、create_agentも同じ。外部APIの失敗はToolErrorMiddlewareかToolRetryMiddlewareで拾う。
  • recursion_limitの既定は10007。ツールを呼び続けるモデルは事実上止まらないので、ToolCallLimitMiddlewareをexit_behavior="end"か"error"で入れる("continue"は止めない)。
  • GPT-6 Astra・GPT-6.1 SolはChat Completionsでツールを呼べずResponses APIが必要。Claude Opus 5.5・Sonnet 5.5はtool_choiceのanyとtoolが400になる。

Tool Callingの仕組み:モデルは実行要求を返すだけ

LLMは学習済みの重みからテキストを生成するだけなので、為替レートの取得や社内DBの検索はできません。Tool Callingは、ツールの名前・説明・引数スキーマを事前に渡しておき、モデルに「どれを、どの引数で呼ぶか」だけを決めさせる仕組みです。LangChain公式のTools解説も、実行はアプリ側(LangGraphではToolNode)が担うと説明しています。

tool_callsとToolMessageの対応

LangChainでは、モデルの要求はAIMessage.tool_callsに入り、実行結果はToolMessageで戻します。

どこに入るか フィールド 中身
AIMessage.tool_calls name 呼び出すツール名
AIMessage.tool_calls args 引数のdict
AIMessage.tool_calls id 呼び出しの識別子
ToolMessage tool_call_id 対応するidを入れる(必須)
ToolMessage status 既定”success”、失敗時”error”

1回のAIMessageに複数のtool_callsが入っても結果を取り違えないのは、このidとtool_call_idの対応があるためです。1ターンの実体は、モデル呼び出し→tool_calls受領→アプリ側で実行→ToolMessageで差し戻し→再びモデル呼び出し、の往復です。

Function Calling・Tool Useとの呼び名の違い

3つは同じ仕組みの別名です。OpenAIが2023年にfunctionsパラメータで出したときの名前がFunction Callingで、のちにtools/tool_callsへ置き換わりました。AnthropicはTool Useと呼びます。LangChainはどのプロバイダでもbind_tools()で同じように書けるようにしています。各社APIを直接叩く書き方はFunction Calling(ファンクションコーリング)とは|仕組みとOpenAI・Claude・Geminiの実装【2026年10月】、フレームワークに依存しない概念と設計判断はAIツール使用(Tool Use)とは?LLMが外部を呼ぶ仕組みと実装の判断基準を解説で扱っています。この記事はLangGraph側の組み方に絞ります。

LangGraphでTool Callingを組む:@tool・bind_tools・ToolNode

前提はPython 3.10以上です。PyPIのlanggraphは1.2.18、langchainは1.4.7がいずれもrequires_python >=3.10で、3.9以下ではpipが0.x系まで下がりcreate_agentが入りません。

pip install "langgraph==1.2.18" "langchain==1.4.7" "langchain-core==1.7.2" "langgraph-prebuilt==1.1.1" "langchain-openai==1.7.3"

@toolによるツール定義と引数スキーマ

@toolを付けた関数がそのままツールになります。以下の天気・為替ツールは固定値を返すデモであり、現在の天気や実際の為替レートは取得しません。型ヒントが引数スキーマに、docstringがツールの説明になってモデルへ送られます。モデルは会話内容とツール名・説明・引数スキーマを踏まえてツールを選ぶため、説明には引数の形式も書きます。

from langchain.tools import tool

@tool
def get_weather(city: str) -> str:
    """指定した都市の現在の天気を返す。cityは日本語の都市名で渡す。"""
    return f"{city}は晴れ、22度です。"

@tool
def get_exchange_rate(base: str, quote: str) -> str:
    """2通貨間の為替レートを返す。baseとquoteはISO 4217の3文字コード(例: USD, JPY)。"""
    return f"1 {base} = 155.2 {quote}"

引数の形式を説明に明記すると、期待する値をモデルに伝えられます。ただし、説明だけでは形式を強制できないため、必要に応じてスキーマやツール内で検証します。型ヒントのintに"abc"が来た場合の扱いは、後述のエラー処理の章で実測結果を示します。

bind_toolsによるツール登録と呼び出し設定

bind_tools()はツール一覧を結び付けた新しいモデルを返します。元のモデルは変わりません。

from langchain.chat_models import init_chat_model

tools = [get_weather, get_exchange_rate]
model = init_chat_model("openai:gpt-6.1-sol", use_responses_api=True).bind_tools(tools)

response = model.invoke("東京の天気は?")
print(response.tool_calls)
# 例: [{'name': 'get_weather', 'args': {'city': '東京'}, 'id': 'call_...', 'type': 'tool_call'}]

use_responses_api=Trueを付けているのは、GPT-6.1 SolがChat Completionsではツールを呼べないためです(詳細は後述のモデル別の章)。bind_tools()には、ツールを強制するtool_choiceと、並列呼び出しを止めるparallel_tool_calls=Falseも渡せます。langchain-openai 1.7.3ではparallel_tool_calls=Falseがそのままリクエストに載ることを確認しました。

StateGraphへのToolNode配置とtools_conditionの分岐

from langgraph.graph import StateGraph, START, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition

def call_model(state: MessagesState):
    return {"messages": [model.invoke(state["messages"])]}

builder = StateGraph(MessagesState)
builder.add_node("llm", call_model)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "llm")
builder.add_conditional_edges("llm", tools_condition)
builder.add_edge("tools", "llm")
graph = builder.compile()

result = graph.invoke({"messages": [{"role": "user", "content": "東京の天気と、1ドル何円か教えて"}]})
print(result["messages"][-1].content)

tools_conditionは直前のAIMessageにtool_callsがあれば"tools"、なければ"__end__"を返します。builder.add_edge("tools", "llm")を忘れると、ツールを実行したところでグラフが終わり、回答が返りません。

見落としやすいのはノード名です。tools_conditionが返す"tools"はそのままノード名として解決されるため、ツールノードを"my_tools"にして辞書を省くと、コンパイル時にValueError: At 'llm' node, 'tools_condition' branch found unknown target 'tools'で落ちます(実測)。別名にするなら対応表を渡し、既出コードのノード登録と条件分岐を以下に置き換えます。戻りのエッジもbuilder.add_edge("my_tools", "llm")に変更してからコンパイルします。

builder.add_node("my_tools", ToolNode(tools))
builder.add_conditional_edges("llm", tools_condition, {"tools": "my_tools", "__end__": "__end__"})

create_agentによるツール実行ループと旧APIからの移行

上のグラフは、langchain.agents.create_agentが内部で作るものとほぼ同じです。ツールを呼んで答えるだけなら、こちらで足ります。

移行前後のコードと警告の文言

# 旧(非推奨)
from langgraph.prebuilt import create_react_agent
agent = create_react_agent(model, tools)

# 新
from langchain.agents import create_agent
agent = create_agent(init_chat_model("openai:gpt-6.1-sol", use_responses_api=True), tools)
result = agent.invoke({"messages": [{"role": "user", "content": "東京の天気は?"}]})

langgraph 1.2.18で旧APIを呼ぶと、LangGraphDeprecatedSinceV10の警告としてcreate_react_agent has been moved to `langchain.agents`. Please update your import to `from langchain.agents import create_agent`. Deprecated in LangGraph V1.0 to be removed in V2.0.が出ます(実測)。まだ動きますが、2.0で削除されると明記されています。create_agentはミドルウェアで処理を差し込めるのが違いで、後述のエラー処理とループ上限もミドルウェアで足します。刷新点の全体はLangChain v1.0とは|create_agent・ミドルウェア・langchain-classicを実装例で解説にまとめています。

langgraph.prebuiltは現役

langgraph.prebuilt全体が非推奨になったわけではありません。非推奨はcreate_react_agentとValidationNodeの2つです(ValidationNodeの警告文はPlease use `create_agent` from `langchain.agents` with custom tool error handling.)。langgraph.prebuilt 1.1.1(2026-10-15)のToolNode・tools_condition・ToolRuntime・InjectedState・InjectedStore・ToolCallTransformerは非推奨ではなく、前の章のStateGraph実装はそのまま使えます。LangChainとLangGraphの依存の向きが1.0で逆転した経緯はLangChainとLangGraphの違いと使い分け|v1.0以降の関係をコードで確認【2026年10月】で扱っています。

ツールからstateを読む・書く:ToolRuntimeとCommand

ツールの中で会話履歴やカスタムstateを読みたい、あるいはツールの結果でstateを更新したい場面では、引数にToolRuntimeを足します。公式のTools解説は、従来のInjectedStateよりToolRuntimeを使うよう案内しています。runtime引数はモデルに渡るスキーマから自動で外れます(実測でremember_city.argsはcityだけ)。

from langchain.agents import create_agent, AgentState
from langchain.tools import tool, ToolRuntime
from langchain_core.messages import ToolMessage
from langgraph.types import Command

class MyState(AgentState):
    city: str

@tool
def remember_city(city: str, runtime: ToolRuntime) -> Command:
    """ユーザーの都市を記憶する。"""
    return Command(update={
        "city": city,
        "messages": [ToolMessage(f"{city}を記憶しました", tool_call_id=runtime.tool_call_id)],
    })

@tool
def current_city(runtime: ToolRuntime) -> str:
    """記憶した都市を返す。"""
    return f"city={runtime.state.get('city')}"

agent = create_agent(model, [remember_city, current_city], state_schema=MyState)

フェイクモデルで「remember_city→current_city」の順に呼ばせると、2本目のツールはcity=大阪を返し、最終stateのcityも大阪になりました。注意点は1つで、Commandで返すときはToolMessageを自分で入れることです。公式は「Every tool call in the message history must have a corresponding ToolMessage」と書いており、対応するToolMessageが無いとToolNodeはValueErrorを出します。

ツールのエラー処理:引数不正・未登録ツールと実行時例外の扱い

本番で外部APIを呼ぶと、タイムアウトや認証失敗は必ず起きます。ここは「LangGraphが自動で処理してくれる」と思われやすいところで、実測すると拾われる範囲は狭いです。

ToolNodeの既定で拾われる失敗・拾われない失敗

起きたこと 既定の結果
int引数に”abc” status=errorのToolMessage
存在しないツール名 status=errorのToolMessage
ツール内でRuntimeError 例外がそのまま上がる
handle_tool_errors=True 例外もToolMessageに変換

引数の型違いでは「Error invoking tool ‘broken’ with kwargs {‘x’: ‘abc’} with error: x: Input should be a valid integer…Please fix the error and try again.」がモデルに戻り、モデルは引数を直して呼び直せます。一方、ツール内のRuntimeError("upstream 503")はグラフの外まで上がりました。ToolNodeのリファレンスとソースのdocstringも、既定は「Ignores tool execution errors (they will be re-raised)」です。create_agentも同じ既定で、ミドルウェア無しでツールがTimeoutErrorを出すとinvokeごと落ちました(実測)。

handle_tool_errorsにはTrueのほか、固定の文字列、例外クラスやそのタプル(捕まえる例外を限定)、例外からメッセージを組み立てる関数を渡せます。Trueは「Error: RuntimeError(‘upstream 503’)」のように例外の文言をそのままモデルへ渡すので、内部のホスト名やSQLが例外に入る実装では、関数を渡して文言を選びます。

create_agentの例外処理:ToolErrorMiddlewareとToolRetryMiddleware

create_agentでは、ツールの例外をToolErrorMiddlewareでToolMessage(status="error")に変え、再試行をToolRetryMiddlewareで足します。再試行させてから最後にエラー文を返すなら、リトライを内側に置いてon_failure="error"にします(ToolErrorMiddlewareのdocstringの指定どおり)。

from langchain.agents.middleware import ToolErrorMiddleware, ToolRetryMiddleware

def on_error(exc, request):
    if isinstance(exc, TimeoutError):
        return f"{request.tool_call['name']} がタイムアウトしました。時間をおいて再実行してください。"
    return None  # それ以外の例外は止める

@tool
def flaky(q: str) -> str:
    """タイムアウト時のエラー処理を確認するデモ用ツール。"""
    raise TimeoutError("timeout")

agent = create_agent(
    model,
    [flaky],
    middleware=[
        ToolErrorMiddleware(on_error=on_error),
        ToolRetryMiddleware(max_retries=2, on_failure="error"),
    ],
)

常にTimeoutErrorを出すツールで試すと、ツールは3回実行され(初回+再試行2回)、モデルには「flaky がタイムアウトしました。時間をおいて再実行してください。」がstatus="error"で戻りました。ToolRetryMiddleware単体(既定のon_failure="continue")でも3回後に「Tool ‘flaky’ failed after 3 attempts with TimeoutError: timeout. Please try again.」が返り、こちらは例外の文言を含みます。on_errorでNoneを返した例外は止まるので、予期しない例外まで握りつぶさずに済みます。例外メッセージの整形は共通化できます。再試行はツールごとに判断し、決済や登録などの更新処理では、重複実行を防ぐ仕組みを確認してから適用します。

ツール呼び出しのループを止める:recursion_limitとToolCallLimitMiddleware

モデルが同じツールを呼び続けるループは、検索結果に満足しないエージェントでよく起きます。langgraph 1.2.18のrecursion_limitの既定は10007(環境変数LANGGRAPH_DEFAULT_RECURSION_LIMITで変更可)で、かつての25ではありません。1ターンがモデルとツールの2ステップなので、上限まで約5,000回モデルを呼べてしまいます。実測ではrecursion_limit=25を渡したとき、モデル呼び出し13回でGraphRecursionErrorになりました。反復上限の考え方はエージェントループとは?止まる条件とSDK別の反復上限を実測で解説【2026年10月】で他のSDKと並べています。

回数で止めるのはToolCallLimitMiddlewareです。thread_limit(スレッド全体。複数回の呼び出しにまたがる集計にはcheckpointerと同じthread_idが必要)とrun_limit(1回のinvoke)を指定でき、tool_nameで特定のツールだけに掛けられます。超えたときの動きはexit_behaviorで決まり、毎回ツールを呼ぶモデルにrun_limit=3で掛けた実測は次のとおりです。

exit_behavior 実測の結果
“end” 4回目をerrorで返し、run終了
“error” ToolCallLimitExceededError
“continue”(既定) 超過分をerrorで返すだけ

既定の"continue"は、超過したツール呼び出しに「Tool call limit exceeded. Do not make additional tool calls.」を返すだけで、モデルが呼び続けるとループは続きます。実測ではrecursion_limit=60まで回ってGraphRecursionErrorになりました。止めたいなら"end"(最後に「Tool call limit reached: run limit exceeded (4/3 calls).」を返して終わる)か"error"を明示します。

from langchain.agents.middleware import ToolCallLimitMiddleware

agent = create_agent(
    model,
    tools,
    middleware=[ToolCallLimitMiddleware(run_limit=10, exit_behavior="end")],
)
agent.invoke({"messages": [{"role": "user", "content": "東京の天気は?"}]}, {"recursion_limit": 50})

create_agentでツールの結果をモデルに読ませず、そのまま最終応答にしたいときは@tool(return_direct=True)を使います。実測では、ツール実行後にモデルは呼ばれず、最後のメッセージがToolMessageのまま終わりました。公式によると、並列で呼ばれたツールの全部がreturn_direct=Trueのときだけ終了し、1本でも違えばモデルへ戻ります。

並列実行とスレッドセーフの要件

1つのAIMessageにtool_callsが複数あると、ToolNodeは同期実行ではThreadPoolExecutor、非同期実行ではasyncio.gatherで並列に走らせます。各ツールで1秒待機させるフェイクモデルの検証では、2回の実行で処理時間は1.05秒と1.16秒(逐次なら2秒以上)で、実行スレッドはThreadPoolExecutor-2_0とThreadPoolExecutor-2_1に分かれていました。

速くなる代わりに、ツール関数はスレッドセーフでなければなりません。モジュールレベルの変数を書き換える、スレッドセーフでないDBクライアントを共有する、といった実装は競合します。実行順序も保証されないため、Aの結果を使ってBを呼ぶ処理は1つのツールにまとめるか、bind_tools(..., parallel_tool_calls=False)でモデル側に1本ずつ呼ばせます。

モデル別の落とし穴:GPT-6系とClaude 5.5系

2026年10月時点で、ツール呼び出しの制約がモデルごとに分かれています。LangChain側のコードが正しくても、モデルを差し替えただけで400エラーやツール未呼び出しになるのはここが原因です。

モデル 制約 LangChainでの対処
GPT-6 Astra・6.1 Sol・6.1 Terra ツールはResponses APIのみ use_responses_api=True
GPT-6 Sol・GPT-6 Luna Chat Completionsはeffort none時のみ Responses APIを使う
Claude Opus 5.5・Sonnet 5.5・Fable 5.1 tool_choiceのany・toolが400 autoのまま使う

OpenAIの最新モデルの移行ガイドは「GPT-6 Astra, GPT-6.1 Sol, and GPT-6.1 Terra support Chat Completions, but tool calling requires Responses.」と書いています。langchain-openai 1.7.3のソースでは、モデル名がgpt-6で始まりツールが付いたリクエストは自動でResponses APIに切り替わります(手元で確認)。ただし古い版ではこの判定が無い可能性があるので、use_responses_api=Trueを明示しておけば版に左右されません。

Anthropicのツール定義のドキュメントは、Claude Opus 5.5・Sonnet 5.5・Fable 5.1・Mythos 5.1ではtool_choiceのanyとtoolが400エラーになり、代わりにautoとstrict tool use、または構造化出力を使うよう案内しています。langchain-anthropic 1.7.7はwith_structured_output()ではこれらのモデルで強制指定を避けますが、bind_tools(tools, tool_choice="any")は、thinkingを設定していなければ強制指定をそのままAPIへ送り、これらのモデルでは400になります。thinking.typeがenabledまたはadaptiveのときは、警告を出して強制指定を外します(ツールが呼ばれる保証は無くなる)。必ずツールを呼ばせたい処理は、プロンプトで指示し、呼ばれなかったときの分岐をグラフ側に持たせます。

自前グラフとcreate_agentの選び方

ツールを呼んで答えるだけのエージェントなら、create_agentを使い、StateGraphは自前で組みません。エラー処理・回数上限・人の承認(HumanInTheLoopMiddleware)・履歴の要約までミドルウェアで足せるため、自前グラフは保守するコードを増やすだけになります。承認の組み込み方はヒューマンインザループ(HITL)とは?意味・HOTLとの違い・承認の実装と導入判断で扱っています。

自前グラフにするのは次の場合です。

  • ツール実行の前後に、エージェントのループと関係ないノード(別モデルでの検証、バッチ処理、外部ワークフローへの受け渡し)を挟む。
  • ツールの結果で、複数の別エージェントへ振り分ける。
  • ループの継続条件が「tool_callsがあるか」では表せない(特定ツールの戻り値で打ち切る等)。

状態管理やチェックポイントまで含めてLangGraphでエージェント全体を組む手順はLangGraphでAIエージェントを実装する手順:状態管理から承認フローまで、検索を組み合わせる構成はAgentic RAGとは?従来RAGとの違い・設計パターンとLangGraph実装の落とし穴が近い内容です。業務システムへのエージェント組み込みを外部に任せる場合は、AIエージェント開発で要件整理から実装まで支援しています。

よくある質問

Tool CallingとFunction Callingは何が違いますか?

仕組みは同じで、呼び名の違いです。OpenAIが2023年に出したfunctionsパラメータの名前がFunction Callingで、のちにAPIもtools/tool_callsに変わりました。AnthropicはTool Useと呼びます。LangChainではbind_tools()で書き方がそろうため、モデルを替えてもツール定義は変わりません。

create_react_agentはもう使えないのですか?

langgraph 1.2.18でも動きますが、呼ぶと「Deprecated in LangGraph V1.0 to be removed in V2.0.」という警告が出ます。新規のコードではfrom langchain.agents import create_agentを使ってください。ToolNodeとtools_conditionは非推奨ではありません。

ToolNodeを使えばツールのエラーは自動で処理されますか?

既定で処理されるのは、引数の型違いと存在しないツール名だけです。ツール関数の中で起きたHTTPエラーやタイムアウトは例外のまま上がります。ToolNodeならhandle_tool_errors、create_agentならToolErrorMiddlewareとToolRetryMiddlewareで拾ってください。

LangGraphのrecursion_limitの既定値はいくつですか?

langgraph 1.2.18では10007です。以前の25ではないため、ツールを呼び続けるエージェントは長時間止まりません。invokeの第2引数で{"recursion_limit": 50}のように渡すか、ToolCallLimitMiddlewareをexit_behavior="end"で入れてください。

ツールの中から会話履歴やstateを参照できますか?

できます。ツールの引数にruntime: ToolRuntimeを足すと、runtime.stateでstate、runtime.tool_call_idで呼び出しIDを参照できます。この引数はモデルに渡るスキーマには含まれません。stateを書き換えるときはCommand(update=...)を返し、その中にToolMessageを含めます。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.03 テックブログ ノジマ不正アクセスで約700万件漏えい:対象確認・窓口と乗っ取り注文を止める実装
  2. 2026.10.29 テックブログ ソニー生命の情報漏洩38.5万件|ログインできない理由、再開状況と窓口の電話番号
  3. 2026.11.03 テックブログ クラブツーリズム不正アクセス:約432万件の対象確認と偽メールの見分け方・流出検知の設計
  4. 2026.11.02 テックブログ アララの不正アクセス:メール約7,700万件の持ち出しと契約企業が行うAPIキー再設定・代替送信・連名報告
  5. 2026.11.02 テックブログ サントリーの不正アクセスで約10万人分漏えい|M365のファイル大量ダウンロードを監査ログで調べる手順

RELATED POSTS 関連記事

目次