Appearance
工具调用 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_calls 的 AIMessage,否则报错。
五、tools_condition:条件路由
tools_condition 是预置的路由函数,判断 LLM 这轮是否要调工具:
- 如果
AIMessage有tool_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 决策质量的命脉。
下一篇 错误处理与重试 讲工具失败时怎么办。