Skip to content

工具调用 Tool Calling

让 LLM 不只是"说",还能"做"——调用计算器、查数据库、发请求。本篇用 LangGraph 的预置组件搭一个标准的"LLM ↔ 工具"循环。

一、工具调用是怎么回事

LLM 本身只能输出文本。但现代模型经过训练,能在回复里输出一种特殊结构——tool_calls,告诉外部程序"我想调用某个工具,参数如下"。程序执行完工具,把结果作为 ToolMessage 喂回 LLM,LLM 再基于结果继续回答。

mermaid
flowchart LR
    U[用户提问] --> LLM[LLM]
    LLM -->|tool_calls| TN[ToolNode 执行]
    TN -->|ToolMessage 结果| LLM
    LLM -->|无 tool_calls| FINAL[最终回复]

这叫 ReAct 模式(Reason + Act)。LangGraph 把这套循环封装成了几个预置组件。

二、用 @tool 定义工具

@tool 装饰器会根据函数名、docstring、类型注解自动生成工具的 schema,LLM 据此知道有哪些工具、怎么调用。

python
from langchain_core.tools import tool

@tool
def add(a: int, b: int) -> int:
    """两个整数相加。"""   # ← docstring 会作为工具描述传给 LLM
    return a + b

@tool
def search_city_weather(city: str) -> str:
    """查询某个城市的天气(示例返回假数据)。"""
    # 实际接你的天气 API
    return f"{city}:晴,25℃"

要点:

  • 函数名 = 工具名。
  • docstring = 工具描述,务必写清楚这个工具干什么、什么时候用,否则 LLM 会在不该调时乱调。
  • 类型注解 = 参数 schema,LLM 会按这个格式生成参数。

三、模型绑定工具 bind_tools

光定义工具还不够,要让 LLM "知道"这些工具:

python
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
tools = [add, search_city_weather]
llm_with_tools = llm.bind_tools(tools)

调用 llm_with_tools.invoke(...) 时,如果模型觉得需要调工具,返回的 AIMessage.tool_calls 就不是空。

四、ToolNode:批量执行工具的预置节点

ToolNode 是 LangGraph 提供的现成节点,它接收消息列表,自动取出最后一条 AIMessage 里的 tool_calls,执行对应工具,把结果包装成 ToolMessage 列表返回。

python
from langgraph.prebuilt import ToolNode

tool_node = ToolNode(tools)

它要求 state["messages"] 最后一条是带 tool_callsAIMessage,否则报错。

五、tools_condition:条件路由

tools_condition 是预置的路由函数,判断 LLM 这轮是否要调工具:

  • 如果 AIMessagetool_calls → 返回 "tools"
  • 否则 → 返回 END

配合 add_conditional_edges 就能搭出自动循环。

六、完整可运行示例

下面这个例子搭出"LLM → 工具 → LLM"循环。为了让例子离线可跑,用假 LLM 模拟;真实用法见后面替换说明。

python
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, tools_condition

# 1. 定义工具
@tool
def add(a: int, b: int) -> int:
    """两个整数相加。"""
    return a + b

@tool
def multiply(a: int, b: int) -> int:
    """两个整数相乘。"""
    return a * b

tools = [add, multiply]
tool_node = ToolNode(tools)

# 2. 用一个假 LLM 模拟"第一轮要调工具,第二轮直接回答"
def fake_llm_node(state):
    msgs = state["messages"]
    last = msgs[-1]
    # 如果历史里还没有 ToolMessage,说明是第一轮,要调工具
    has_tool_result = any(isinstance(m, ToolMessage) for m in msgs)
    if not has_tool_result:
        # 模拟模型决定调用 add(3, 5)
        return {"messages": [AIMessage(
            content="",
            tool_calls=[{"name": "add", "args": {"a": 3, "b": 5}, "id": "call-1"}],
        )]}
    # 拿到工具结果后,给出最终回答
    tool_result = [m for m in msgs if isinstance(m, ToolMessage)][-1].content
    return {"messages": [AIMessage(content=f"计算结果是 {tool_result}")]}

# 3. 搭图
graph = StateGraph(MessagesState)
graph.add_node("llm", fake_llm_node)
graph.add_node("tools", tool_node)
graph.add_edge(START, "llm")
graph.add_conditional_edges("llm", tools_condition)  # 有 tool_calls→tools,否则→END
graph.add_edge("tools", "llm")  # 工具执行完回到 LLM
app = graph.compile()

# 4. 运行
result = app.invoke({"messages": [HumanMessage(content="帮我算 3+5")]})
for m in result["messages"]:
    print(type(m).__name__, "-", getattr(m, "content", "")[:50],
          "| tool_calls:", getattr(m, "tool_calls", None))

输出能看到:HumanMessage → AIMessage(带tool_calls) → ToolMessage(结果8) → AIMessage(最终回答)

七、换成真实 LLM

fake_llm_node 换成绑定工具的真实模型即可:

python
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools)

def llm_node(state):
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}

其余图结构完全不变,这就是预置组件的好处。

八、循环图解

mermaid
flowchart LR
    START([START]) --> LLM[LLM 节点]
    LLM -->|有 tool_calls| TN[ToolNode]
    LLM -->|无 tool_calls| END([END])
    TN --> LLM

只要 LLM 一直要调工具,就会在 LLM↔ToolNode 之间循环;一旦 LLM 不再要调工具(直接给出最终回答),tools_condition 把它导向 END,循环结束。

九、create_react_agent:一行搭建

如果不想手动搭上面的图,可以直接用预置的 ReAct Agent:

python
from langgraph.prebuilt import create_react_agent

agent = create_react_agent(llm, tools)
result = agent.invoke({"messages": [HumanMessage(content="3+5 等于几")]})

create_react_agent 内部做的就是上面那一套图。它返回的是一个编译好的图,可以正常 invoke/stream。更多见 智能体模块

十、常见踩坑

踩坑 1:工具描述不清,LLM 乱调

python
@tool
def search(x: str) -> str:
    """搜索"""   # ❌ 太模糊,LLM 不知道何时该用
    ...

描述要写清楚功能 + 使用场景

python
@tool
def search_city_weather(city: str) -> str:
    """查询指定城市的实时天气。当用户问'某地天气如何'、'要不要带伞'等天气相关问题时使用。"""
    ...

踩坑 2:ToolNode 报错"最后一条不是带 tool_calls 的 AIMessage"

ToolNode 默认看 messages[-1]。如果你在 LLM 节点后又插了别的节点(比如日志节点往 messages 里塞了消息),messages[-1] 就不是 AIMessage 了。解决:保证 ToolNode 执行前最后一条是带 tool_calls 的 AIMessage,或调整图结构。

踩坑 3:工具抛异常没人处理

工具内部报错会冒泡到图执行。要么在工具内 try/except 返回错误信息字符串,要么用 ToolNode(handle_tool_errors=True) 让错误自动变成 ToolMessage 喂回 LLM 让它自我纠正。详见 错误处理与重试

踩坑 4:参数 schema 和函数签名不符

类型注解决定了 LLM 生成的参数格式。如果函数要 int 但注解写成 str,LLM 传 "3",工具里 a + b 就变成字符串拼接。保持注解和实现一致。

踩坑 5:工具返回不可序列化对象

工具返回的值会被放进 ToolMessage(要序列化)。返回自定义类实例、文件对象会失败。统一返回 str / dict / 基本类型。

十一、小结

  • @tool 装饰器根据函数名+docstring+注解自动生成工具 schema。
  • llm.bind_tools(tools) 让模型"知道"工具。
  • ToolNode 批量执行工具,tools_condition 做条件路由。
  • 三者拼出标准 ReAct 循环,或直接用 create_react_agent 一行搭建。
  • 工具描述务必清晰,是 LLM 决策质量的命脉。

下一篇 错误处理与重试 讲工具失败时怎么办。