LangGraph ToolNode 工具调用详解:从手动执行到 ToolRuntime 与容错

上一篇我们学习了 interrupt() 和 HITL,让 Agent 可以在关键步骤暂停并等待人工确认。

接下来继续进入 LangGraph 的另一个核心能力:工具调用

一个典型的 ReAct Agent,本质上就是:

用户问题
↓
LLM 判断是否需要工具
↓
Tool 执行
↓
ToolMessage 返回结果
↓
LLM 继续推理

本文不展开太多底层细节,重点把 bind_tools()、手动工具节点、ToolNodeToolRuntime、工具更新 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
)

这样容错逻辑就不必重复写进每一个工具函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值