# LangGraph Tool Calling Agent 深度实战:从零构建 ReAct 循环与工具调用链

LangGraph Tool Calling Agent 深度实战:从零构建 ReAct 循环与工具调用链


导读:Tool Calling Agent 是 LangGraph 最实用的能力之一------让 LLM 根据用户问题自主选择工具、执行工具、再根据结果生成回答。本文从 @tool 装饰器、bind_tools、AIMessage.tool_calls、ToolNode、tools_condition 五个核心组件讲起,深入 ToolMessage 与 tool_call_id 的关联机制,最后手写一个完整的 ReAct 循环。配合「智能订单查询 Agent」案例,一次性讲透工具调用链。


适合读者

  • 需要让 LLM 调用外部 API 或数据库的开发者
  • 想理解 Tool Calling Agent 底层机制而非只会用封装库的工程师
  • 准备面试中回答「ReAct 是什么」的求职者

阅读收益

  • 掌握 @tool 装饰器的底层原理与 docstring 规范
  • 理解 bind_tools 只是「注册」,不执行工具
  • 掌握 ToolNode 的执行流程与异常处理
  • 理解 tool_call_id 的消息关联机制
  • 获得一份从零手写的 ReAct 循环完整代码

目录

  1. [Tool Calling Agent 是什么](#Tool Calling Agent 是什么)
  2. 核心组件总览
  3. @tool:定义工具
  4. bind_tools:注册工具
  5. AIMessage.tool_calls:模型决策
  6. ToolNode:执行工具
  7. tools_condition:判断是否继续
  8. [ToolMessage 与 tool_call_id](#ToolMessage 与 tool_call_id)
  9. [完整案例:智能订单查询 Agent](#完整案例:智能订单查询 Agent)
  10. [手写 ReAct 循环](#手写 ReAct 循环)
  11. 踩坑清单
  12. 面试速答版
  13. 总结与延伸
  14. 思考题

1. Tool Calling Agent 是什么

普通模型调用只有一次输入和输出:

复制代码
用户问题 -> LLM -> 回答

如果用户询问订单状态、实时天气或数据库数据,模型本身并不知道真实答案。此时需要让模型调用外部工具:

复制代码
用户消息 -> LLM --没有 tool_calls--> 最终回答
              |
              |--生成 tool_calls-> 执行工具 -> ToolMessage -> LLM

工具执行后必须回到模型。工具返回的是原始数据,模型还需要根据这些数据组织最终回答,也可能判断是否需要继续调用其他工具。

这个循环可以概括为 ReAct(Reason + Act):

复制代码
Reason:模型判断下一步做什么
Act:模型生成工具调用
Observe:模型读取工具执行结果
Reason:模型继续调用工具或生成最终回答

2. 核心组件总览

组件 作用
@tool 把 Python 函数声明为工具
bind_tools 把工具名称、描述和参数 schema 提供给模型
AIMessage.tool_calls 保存模型生成的工具调用请求
ToolNode 找到并执行模型请求的工具
ToolMessage 保存工具执行结果,并关联原调用 ID
tools_condition 判断进入工具节点还是结束
MessagesState 保存用户、模型和工具消息

核心消息链路:

复制代码
HumanMessage
    |
    v
AIMessage(tool_calls=[...])
    |
    v
ToolMessage(tool_call_id=...)
    |
    v
AIMessage(content="最终回答")

3. @tool:定义工具

工具本质上是带有明确 schema 的 Python 函数。

python 复制代码
from langchain_core.tools import tool


@tool
def query_order(order_id: str) -> str:
    """根据订单号查询订单状态。仅用于查询,不修改订单。"""
    orders = {
        "1001": "已发货",
        "1002": "待付款",
    }
    return orders.get(order_id, "未找到该订单")

模型主要通过以下信息理解工具:

信息来源 作用
函数名 工具执行什么动作
docstring 什么情况下应该调用,以及有哪些限制
参数名 模型需要提供什么数据
类型标注 参数应采用什么格式

例如,模型看到的工具信息可以理解为:

复制代码
工具名:query_order
用途:根据订单号查询订单状态
参数:order_id,字符串

3.1 工具设计原则

  • 尽量小而明确:查询订单与取消订单应拆成两个工具
  • 内部仍需校验:工具收到的参数来自模型,必须做数据格式、权限和业务规则校验
  • 返回结构化数据:尽量返回 dict 而非自然语言,方便模型解析

4. bind_tools:注册工具

定义工具后,需要将工具绑定到模型:

python 复制代码
tools = [query_order]
model_with_tools = model.bind_tools(tools)

bind_tools 会把工具的名称、说明和参数 schema 发送给模型,但不会执行工具

调用绑定后的模型:

python 复制代码
message = model_with_tools.invoke("查询订单 1001")
print(message.tool_calls)

如果模型决定调用工具,tool_calls 可能是:

json 复制代码
[
  {
    "name": "query_order",
    "args": {"order_id": "1001"},
    "id": "call_123",
    "type": "tool_call"
  }
]
字段 含义
name 要调用的工具名
args 模型生成的工具参数
id 本次工具调用的唯一标识
type 消息类型

如果模型不需要工具,tool_calls 通常为空,并直接在 content 中生成回答。


5. AIMessage.tool_calls:模型决策

AIMessage 是 LangChain 的消息类型,代表模型的回复。当模型决定调用工具时:

python 复制代码
from langchain_core.messages import AIMessage

# tool_calls 是 AIMessage 的属性
message = model_with_tools.invoke("查询订单 1001")

if message.tool_calls:
    # 模型要求调用工具
    for call in message.tool_calls:
        print(f"调用工具: {call['name']}, 参数: {call['args']}")
else:
    # 模型直接回答
    print(message.content)

tool_calls 是一个列表,说明模型可能一次请求调用多个工具


6. ToolNode:执行工具

python 复制代码
from langgraph.prebuilt import ToolNode

tools = [query_order]
tool_node = ToolNode(tools)

ToolNode 会完成以下工作:

  1. 读取最后一条 AIMessage
  2. 遍历其中的 tool_calls
  3. 根据 name 找到对应工具
  4. 使用 args 调用工具
  5. 将结果包装成 ToolMessage
  6. 使用 tool_call_id 关联原始调用

如果一条 AI 消息中包含多个相互独立的工具调用,ToolNode 可以并行执行它们。

6.1 异常处理

python 复制代码
# 自动处理工具异常
ToolNode(tools, handle_tool_errors=True)

# 自定义错误信息
ToolNode(tools, handle_tool_errors="工具执行失败,请检查参数。")

# 只处理特定异常类型
ToolNode(tools, handle_tool_errors=(ValueError, TypeError))

错误信息会以工具结果的形式返回给模型,模型可以据此修改参数或向用户补充提问。


7. tools_condition:判断是否继续

tools_condition 检查最后一条 AI 消息:

  • 存在 tool_calls:进入名为 tools 的节点
  • 不存在 tool_calls:说明模型已经给出最终回答,进入 END

标准写法:

python 复制代码
from langgraph.graph import START, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode, tools_condition

builder = StateGraph(MessagesState)
builder.add_node("agent", call_model)
builder.add_node("tools", ToolNode(tools))

builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", tools_condition)
builder.add_edge("tools", "agent")

graph = builder.compile()

注意tools_condition 返回的分支名字硬编码是两个值:"tools" 和 END。因此工具节点必须命名为 tools

这条回边 tools -> agent 形成 Tool Calling Agent 的循环。模型读取 ToolMessage 后,再决定继续调用工具还是生成最终回答。


8. ToolMessage 与 tool_call_id

工具结果必须与原来的工具调用一一对应

python 复制代码
from langchain_core.messages import ToolMessage

tool_message = ToolMessage(
    content="订单 1001 已发货",
    name="query_order",
    tool_call_id="call_123",  # 必须与 AIMessage.tool_calls 中的 id 一致
)

tool_call_id 必须与 AIMessage.tool_calls 中的 id 一致。模型可能在一条消息中请求多个工具,如果缺少这个 ID,就无法判断结果属于哪个调用。

ToolNode 会自动维护这种对应关系。只有需要自定义工具执行过程时,才需要手动构造 ToolMessage。


9. 完整案例:智能订单查询 Agent

9.1 流程设计

复制代码
START -> agent --无工具调用--> END
          |
          |--有工具调用-> tools --> agent

9.2 完整代码

python 复制代码
import os
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langgraph.constants import START
from langgraph.graph import MessagesState, StateGraph
from langgraph.prebuilt import ToolNode, tools_condition


# ========== 模拟订单数据库 ==========
ORDERS = {
    "1001": {"status": "已发货", "carrier": "顺丰", "address": "北京市"},
    "1002": {"status": "待付款", "carrier": None, "address": "上海市"},
    "1003": {"status": "已签收", "carrier": "京东", "address": "广州市"},
}


# ========== 定义工具 ==========
@tool
def query_order(order_id: str) -> dict:
    """根据订单号查询订单状态、承运商和收货地址;找不到时返回错误信息。"""
    order = ORDERS.get(order_id)
    if order is None:
        return {
            "ok": False,
            "order_id": order_id,
            "message": "订单不存在",
        }
    return {"ok": True, "order_id": order_id, **order}


@tool
def list_orders() -> list:
    """列出所有订单号。"""
    return list(ORDERS.keys())


# ========== 初始化模型 ==========
load_dotenv()
llm = init_chat_model(
    model=os.getenv("DEEPSEEK_MODEL", "deepseek-v4-flash"),
    model_provider="openai",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
    temperature=0,
)

# 绑定工具
tools = [query_order, list_orders]
llm_with_tools = llm.bind_tools(tools)


# ========== 定义节点 ==========
def call_model(state: MessagesState):
    """让模型根据消息历史决定回答,或者生成工具调用。"""
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}


# ========== 构建图 ==========
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_node("tools", ToolNode(tools))

builder.add_edge(START, "call_model")
builder.add_conditional_edges("call_model", tools_condition)
builder.add_edge("tools", "call_model")

graph = builder.compile()


# ========== 运行 ==========
result = graph.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": "请查询订单 1001 的状态和承运商。",
            }
        ]
    }
)

print(result["messages"][-1].content)

# 打印完整消息链路
for message in result["messages"]:
    print(f"{type(message).__name__}: {message.content}")

9.3 执行过程

复制代码
1. HumanMessage:请查询订单 1001 的状态和承运商
2. AIMessage:调用 query_order(order_id="1001")
3. ToolMessage:返回 status="已发货"、carrier="顺丰"
4. AIMessage:订单 1001 已发货,承运商是顺丰

9.4 多工具并行

如果用户问「我有几个订单?分别是什么状态?」,模型可能一次调用两个工具:

json 复制代码
[
  {"name": "list_orders", "args": {}, "id": "call_001"},
  {"name": "query_order", "args": {"order_id": "1001"}, "id": "call_002"}
]

ToolNode 会并行执行这两个工具调用。


10. 手写 ReAct 循环

如果不用 ToolNode 和 tools_condition,完全手写工具调用循环:

python 复制代码
from langchain_core.messages import ToolMessage


def custom_react_loop(user_input: str):
    """手写 ReAct 循环:理解底层机制"""
    messages = [{"role": "user", "content": user_input}]
    
    while True:
        # Reason + Act:模型决定做什么
        response = llm_with_tools.invoke(messages)
        messages.append(response)
        
        # 检查是否有工具调用
        if not response.tool_calls:
            # 没有工具调用,生成最终回答
            break
        
        # Observe:执行工具
        for call in response.tool_calls:
            tool_name = call["name"]
            tool_args = call["args"]
            tool_call_id = call["id"]
            
            # 找到并执行工具
            selected_tool = tools_by_name[tool_name]
            result = selected_tool.invoke(tool_args)
            
            # 构造 ToolMessage
            tool_message = ToolMessage(
                content=str(result),
                name=tool_name,
                tool_call_id=tool_call_id,
            )
            messages.append(tool_message)
    
    return messages[-1].content


# 工具映射表
tools_by_name = {tool.name: tool for tool in tools}

这个手写循环展示了 ToolNode 和 tools_condition 的底层逻辑:

  1. 模型生成 tool_calls(Reason + Act)
  2. 遍历 tool_calls 执行工具(Observe)
  3. 构造 ToolMessage 并关联 tool_call_id
  4. 回到模型继续 Reason
  5. 直到没有 tool_calls,生成最终回答

思考题 2

上面的手写循环中,如果模型一次请求调用 5 个工具,是串行执行还是并行执行?如何改成并行执行?并行执行时如何处理异常?


11. 踩坑清单

序号 坑点 现象 正确做法
1 工具节点不叫 tools ValueError: Node tools not found 工具节点必须命名为 tools
2 忘记 bind_tools 模型从不生成 tool_calls 调用 model.bind_tools(tools)
3 工具名不一致 KeyError: tool_name 确保 tool_calls 中的 name 和工具函数名一致
4 缺少 tool_call_id 模型无法关联工具结果 ToolMessage 必须包含正确的 tool_call_id
5 工具返回非字符串 ToolMessage content 必须是字符串 用 str() 转换
6 docstring 写得不清楚 模型调用错误的工具或参数 docstring 要明确用途、参数含义和限制
7 一个工具做太多事 模型调用混乱 遵循单一职责,拆分成多个小工具
8 忽略工具异常处理 工具报错导致整个流程中断 配置 handle_tool_errors 或 try-except

12. 面试速答版

Q1:ReAct 是什么?

A:Reason(推理)+ Act(行动)的循环。模型先推理下一步做什么,生成工具调用,观察工具结果,再推理,直到不需要工具直接回答。

Q2:bind_tools 做了什么?

A:把工具的名称、描述和参数 schema 提供给模型,让模型知道有哪些工具可以调用。不执行工具。

Q3:ToolNode 和手写工具执行的区别?

A:ToolNode 自动处理工具查找、参数传递、结果包装、异常处理和 tool_call_id 关联。手写需要自己实现这些逻辑。

Q4:tool_call_id 的作用?

A:关联工具调用请求和工具执行结果。模型可能在一条消息中请求多个工具,tool_call_id 让模型知道每个结果对应哪个调用。

Q5:tools_condition 返回什么?

A:硬编码返回 "tools" 或 END。因此工具节点必须命名为 "tools"。


13. 总结与延伸

本章核心要点

  • Tool Calling Agent 是 "模型决策 -> 工具执行 -> 结果反馈 -> 再次决策" 的循环
  • @tool 将 Python 函数转换为模型可理解的工具 schema
  • bind_tools 只提供工具信息,不负责执行工具
  • 模型通过 AIMessage.tool_calls 请求调用工具
  • ToolNode 执行工具并生成对应的 ToolMessage
  • tools_condition 决定进入工具节点还是结束
  • 工具执行后必须回到模型,才能生成最终回答或继续调用工具

延伸思考

  • 工具权限控制:不同用户应该看到不同的工具列表,可以在 bind_tools 前做权限过滤
  • 工具调用审计:记录谁调用了什么工具、参数是什么、结果如何
  • 工具返回格式:尽量返回结构化数据(dict),方便模型解析
  • 多工具并行:ToolNode 自动并行执行独立工具调用

14. 思考题

  1. 工具权限:设计一个系统,让普通用户只能调用查询类工具,管理员才能调用修改类工具。应该在哪个环节做权限控制?

  2. 工具调用审计:如何在不影响性能的前提下,记录每次工具调用的调用者、参数、结果和耗时?

  3. 工具返回格式:如果工具返回的是复杂 JSON,模型可能无法正确解析。如何设计工具返回格式,既保留完整信息又方便模型理解?

  4. 工具异常降级:如果查询数据库的工具超时或报错,系统应该如何优雅降级?是直接报错,还是返回缓存数据,还是让模型告知用户?

  5. 多轮工具调用:如果用户问「比较订单 1001 和 1002 的状态」,模型需要先查 1001,再查 1002,最后比较。如何设计 State 让模型记住之前的查询结果?


如果本文对你有帮助,欢迎点赞 + 收藏 + 关注!有任何问题欢迎在评论区交流。

参考资料:LangGraph 官方文档 Tool Calling 章节

相关推荐
VIP_CQCRE13 分钟前
用 Ace Data Cloud 快速接入 MiniMax H3:从提示词到 2K 商业级视频生成
人工智能·api·ai视频·minimax·ace data cloud
迁移科技19 分钟前
3D视觉引导销轴上下料:单相机双工位高效方案
人工智能·自动化·视觉检测
reasonsummer21 分钟前
【办公类-119-03】20260901三个园区“国旗下讲话” 按班级组合docx模板(AI+excel+python、deepseek和豆包、微信自动私发)
python
数据皮皮侠22 分钟前
企业知识重组能力数据(1988-2025)
大数据·人工智能·搜索引擎·智慧城市·制造
戴西软件23 分钟前
远程协同仿真是什么体验?
运维·jvm·人工智能·自动化·rpa
leeyi27 分钟前
Eino ADK——Agent 的完整生命周期:从创建到中断恢复(第98篇-E84)
llm·aigc·agent
慢云智慧空间27 分钟前
从设备联网到空间理解,智能建筑的系统架构正在经历哪些关键变化?
python·系统架构
m4Rk_30 分钟前
【论文阅读】Agent 记忆机制(53):Experience-Following——为什么错误经验会在记忆中不断传播
论文阅读·人工智能·学习·开源·github
艾莉丝努力练剑2 小时前
【AI大模型接入SDK】Provider分析与实现
c++·人工智能·学习·面试·大模型·llm