上一篇我们学习了
interrupt()和 HITL,让 Agent 可以在关键步骤暂停并等待人工确认。接下来继续进入 LangGraph 的另一个核心能力:工具调用。
一个典型的 ReAct Agent,本质上就是:
用户问题 ↓ LLM 判断是否需要工具 ↓ Tool 执行 ↓ ToolMessage 返回结果 ↓ LLM 继续推理本文不展开太多底层细节,重点把
bind_tools()、手动工具节点、ToolNode、ToolRuntime、工具更新 State 和工具容错几个知识点串起来。
1. LangGraph 中工具调用到底怎么走?
先定义两个工具:
from langchain.tools import tool
@tool(parse_docstring=True)
def get_weather(city: str) -> str:
"""
查询指定城市天气
Args:
city: 城市名称
"""
return f"{city} 今天天气不错"
@tool(parse_docstring=True)
def get_news(home_or_abroad: bool) -> str:
"""
查询国内外新闻
Args:
home_or_abroad: True 查询国内,False 查询国外
"""
if home_or_abroad:
return "Kimi 新模型发布"
return "Anthropic 暂停新模型访问"
把工具绑定给模型:
tools = [get_weather, get_news]
model_with_tools = model.bind_tools(
tools
)
这里要注意:
bind_tools()并不会真正执行工具。
它只是让模型知道当前有哪些工具,以及每个工具需要什么参数。
模型可能返回:
AIMessage(
tool_calls=[
{
"name": "get_weather",
"args": {
"city": "北京"
}
}
]
)
真正执行 get_weather(),还需要工具节点完成。
2. 手动工具节点做了哪些事情?
一个最基础的工具节点通常需要:
读取最后一条 AIMessage
↓
取出 tool_calls
↓
根据工具名找到工具
↓
执行工具
↓
封装 ToolMessage
↓
写回 messages
简化代码:
tools_by_name = {
tool.name: tool
for tool in tools
}
def tool_node(state):
last_message = state["messages"][-1]
tool_messages = []
for tool_call in last_message.tool_calls:
tool = tools_by_name[
tool_call["name"]
]
result = tool.invoke(
tool_call["args"]
)
tool_messages.append(
ToolMessage(
content=str(result),
tool_call_id=tool_call["id"]
)
)
return {
"messages": tool_messages
}
完整循环:
START
↓
llm_node
↓
有 tool_calls?
├── 否 → END
│
└── 是
↓
tool_node
↓
llm_node
手动实现最大的价值,是帮助我们理解:
模型只负责产生工具调用请求,真正执行工具的是工具节点。
但真实项目还需要处理工具名无效、参数校验、异常处理、多个工具并行、运行时参数注入以及 Command 返回等问题。
3. ToolNode:把通用逻辑交给 LangGraph
LangGraph 已经提供了预构建:
ToolNode
最基本的使用方式:
from langgraph.prebuilt.tool_node import ToolNode
tool_node = ToolNode(
tools=tools
)
注册到图中:
builder.add_node(
"tool_node",
tool_node
)
这样就不需要自己遍历 tool_calls、查找工具、执行工具和构造 ToolMessage。
例如用户问:
今天北京天气如何?
国内有哪些新闻?
模型可以一次产生:
get_weather
+
get_news
两个工具调用。
ToolNode 会负责执行,并返回对应的 ToolMessage,然后重新进入模型节点生成最终答案。
所以可以简单理解:
手动 tool_node
=
理解原理
ToolNode
=
实际开发中复用通用工具执行逻辑
4. ToolRuntime:工具也能访问 State、Context 和 Store
普通工具只接收模型传入的参数:
def get_weather(city: str):
...
但实际项目中,工具可能还需要:
读取当前 State
读取 Runtime Context
访问长期记忆 Store
获取 tool_call_id
获取运行配置
这时可以使用:
ToolRuntime
例如:
from langgraph.prebuilt.tool_node import ToolRuntime
@tool
def get_weather(
city: str,
runtime: ToolRuntime
):
state = runtime.state
context = runtime.context
store = runtime.store
...
ToolRuntime 可以提供:
runtime.state
当前图状态 / 短期记忆
runtime.context
本次运行上下文
runtime.config
运行配置
runtime.tool_call_id
当前工具调用 ID
runtime.store
长期记忆
runtime.stream_writer
自定义流式输出
这也正好和前面学习的 Memory 联系起来。
需要注意:
ToolRuntime
和图节点函数中的:
langgraph.runtime.Runtime
不是同一个类型。
5. 工具不只返回字符串,还可以更新 State
最简单的工具返回:
return "北京今天天气不错"
这种结果会由 ToolNode 转换成:
ToolMessage
但如果我们还想把工具结果直接写入 State,例如:
weather_res
news_res
工具可以返回:
Command(
update={
...
}
)
例如:
@tool
def get_weather(
city: str,
runtime: ToolRuntime
) -> Command:
result = f"{city} 今天天气不错"
tool_msg = ToolMessage(
content=result,
tool_call_id=runtime.tool_call_id
)
return Command(
update={
"weather_res": result,
"messages": [tool_msg]
}
)
这里有一个重要规则:
当工具由模型发起时,
AIMessage.tool_calls后面必须有与之对应的ToolMessage。
普通返回值时,ToolNode 会自动完成这个转换;如果工具自己返回 Command,开发者就需要在 Command.update 中补上对应的 ToolMessage。
如果多个并行工具同时更新同一个 State 字段,也要重新考虑前面学过的 Reducer,否则可能出现并发状态更新冲突。
6. wrap_tool_call:给工具增加重试和缓存
工具调用很可能涉及网络 API、数据库或远程服务,所以失败很正常。
ToolNode 提供:
wrap_tool_call
允许在真正调用工具前后增加一层包装逻辑。
核心形式:
def wrap_tool_call(
request,
execute
):
...
其中:
request
=
当前工具调用请求
execute(request)
=
真正执行工具
因此可以实现:
重试
缓存
请求修改
短路返回
自定义控制流
例如简单重试:
def wrap_tool_call(
request,
execute
):
max_attempts = 3
for i in range(max_attempts):
try:
return execute(request)
except ConnectionError:
if i == max_attempts - 1:
return ToolMessage(
content="工具调用失败",
tool_call_id=(
request.runtime.tool_call_id
)
)
然后:
ToolNode(
tools=tools,
wrap_tool_call=wrap_tool_call
)
这样容错逻辑就不必重复写进每一个工具函数。

689

被折叠的 条评论
为什么被折叠?



