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を含めます。