概述:为什么 LLM 需要 Tool?
大模型本身很擅长理解语言和生成文本。
但它有明显边界:
- 它不能实时查天气。
- 它不能直接查询你的订单数据库。
- 它不能真正调用公司内部 API。
- 它不能自己执行精确数学计算。
- 它不知道当前用户的账户余额。
- 它不能凭空创建工单、发送邮件、修改配置。
如果没有工具,模型只能“说”。
有了工具,模型才有机会“做”。
LangChain 里的 Tool,本质上就是把一个 Python 函数暴露给模型,让模型在需要时选择调用它,并传入符合 schema 的参数。
#mermaid-svg-UY7URfUiUtu6CxEw{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UY7URfUiUtu6CxEw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UY7URfUiUtu6CxEw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UY7URfUiUtu6CxEw .error-icon{fill:#552222;}#mermaid-svg-UY7URfUiUtu6CxEw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UY7URfUiUtu6CxEw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UY7URfUiUtu6CxEw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UY7URfUiUtu6CxEw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UY7URfUiUtu6CxEw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UY7URfUiUtu6CxEw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UY7URfUiUtu6CxEw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UY7URfUiUtu6CxEw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UY7URfUiUtu6CxEw .marker.cross{stroke:#333333;}#mermaid-svg-UY7URfUiUtu6CxEw svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UY7URfUiUtu6CxEw p{margin:0;}#mermaid-svg-UY7URfUiUtu6CxEw .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-UY7URfUiUtu6CxEw .cluster-label text{fill:#333;}#mermaid-svg-UY7URfUiUtu6CxEw .cluster-label span{color:#333;}#mermaid-svg-UY7URfUiUtu6CxEw .cluster-label span p{background-color:transparent;}#mermaid-svg-UY7URfUiUtu6CxEw .label text,#mermaid-svg-UY7URfUiUtu6CxEw span{fill:#333;color:#333;}#mermaid-svg-UY7URfUiUtu6CxEw .node rect,#mermaid-svg-UY7URfUiUtu6CxEw .node circle,#mermaid-svg-UY7URfUiUtu6CxEw .node ellipse,#mermaid-svg-UY7URfUiUtu6CxEw .node polygon,#mermaid-svg-UY7URfUiUtu6CxEw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UY7URfUiUtu6CxEw .rough-node .label text,#mermaid-svg-UY7URfUiUtu6CxEw .node .label text,#mermaid-svg-UY7URfUiUtu6CxEw .image-shape .label,#mermaid-svg-UY7URfUiUtu6CxEw .icon-shape .label{text-anchor:middle;}#mermaid-svg-UY7URfUiUtu6CxEw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UY7URfUiUtu6CxEw .rough-node .label,#mermaid-svg-UY7URfUiUtu6CxEw .node .label,#mermaid-svg-UY7URfUiUtu6CxEw .image-shape .label,#mermaid-svg-UY7URfUiUtu6CxEw .icon-shape .label{text-align:center;}#mermaid-svg-UY7URfUiUtu6CxEw .node.clickable{cursor:pointer;}#mermaid-svg-UY7URfUiUtu6CxEw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UY7URfUiUtu6CxEw .arrowheadPath{fill:#333333;}#mermaid-svg-UY7URfUiUtu6CxEw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UY7URfUiUtu6CxEw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UY7URfUiUtu6CxEw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UY7URfUiUtu6CxEw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UY7URfUiUtu6CxEw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UY7URfUiUtu6CxEw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UY7URfUiUtu6CxEw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UY7URfUiUtu6CxEw .cluster text{fill:#333;}#mermaid-svg-UY7URfUiUtu6CxEw .cluster span{color:#333;}#mermaid-svg-UY7URfUiUtu6CxEw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UY7URfUiUtu6CxEw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UY7URfUiUtu6CxEw rect.text{fill:none;stroke-width:0;}#mermaid-svg-UY7URfUiUtu6CxEw .icon-shape,#mermaid-svg-UY7URfUiUtu6CxEw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UY7URfUiUtu6CxEw .icon-shape p,#mermaid-svg-UY7URfUiUtu6CxEw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UY7URfUiUtu6CxEw .icon-shape .label rect,#mermaid-svg-UY7URfUiUtu6CxEw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UY7URfUiUtu6CxEw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UY7URfUiUtu6CxEw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UY7URfUiUtu6CxEw :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
否
是
用户问题
模型判断是否需要工具
需要工具?
直接回答
生成 tool call
执行 Python 函数
工具结果回传给模型
模型生成最终回答
Tool 让 LLM 从文本生成器,变成可以连接外部系统的任务执行协调器。
Tool 适合解决什么问题?
Tool 适合处理模型本身不能可靠完成的事情。
常见场景:
| 实时信息 | 查询天气、股票、物流、日程 |
| 业务系统 | 查订单、查库存、查用户权益 |
| 精确计算 | 数学计算、金额计算、时间计算 |
| 数据库查询 | 执行受控 SQL、读取 schema |
| 外部 API | 创建工单、发送通知、调用搜索接口 |
| 文件处理 | 读取文件、解析 PDF、生成报表 |
| 权限动作 | 改地址、取消订单、审批操作 |
注意,Tool 不是让模型随便执行任意代码。
生产环境里,工具应该是受控、明确、有权限边界、有参数校验、有错误处理的业务能力。
第一个 Tool:用 @tool 装饰器定义函数
LangChain 最简单的工具定义方式是 @tool。
from langchain.tools import tool
@tool
def add(a: int, b: int) –> int:
"""Add two integers."""
return a + b
这个函数被 @tool 包装后,就不再只是普通 Python 函数,而是一个 LangChain Tool。
你可以查看它的关键信息:
print(add.name)
print(add.description)
print(add.args)
输出类似:
add
Add two integers.
{'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}
这里有三个非常重要的东西:
- name:工具名,模型会看到。
- description:工具描述,通常来自 docstring。
- args:参数 schema,来自类型注解。
工具名、docstring、类型注解,会共同影响模型是否能正确调用工具。
把 Tool 交给 Agent
工具定义好后,需要交给 Agent。
from langchain.agents import create_agent
from langchain.tools import tool
@tool
def add(a: int, b: int) –> int:
"""Add two integers."""
return a + b
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[add],
system_prompt="你是一个严谨的数学助手。需要计算时必须调用工具。",
)
result = agent.invoke({
"messages": [
{"role": "user", "content": "123 + 456 等于多少?"}
]
})
print(result["messages"][–1].content)
执行过程大概是:
用户:123 + 456 等于多少?
模型:我需要调用 add 工具,参数 a=123, b=456
系统:执行 add(123, 456),得到 579
模型:123 + 456 等于 579
模型不是直接调用 Python 函数。
它会先生成一个工具调用请求。Agent 框架负责解析请求、执行工具,再把结果回传给模型。
Tool 调用链路:从用户问题到最终回答
工具调用不是一步完成的,而是一个闭环。
Tool FunctionAgent RuntimeChatModelUserTool FunctionAgent RuntimeChatModelUser#mermaid-svg-Qx1MuFtCiHJ4roWl{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Qx1MuFtCiHJ4roWl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Qx1MuFtCiHJ4roWl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Qx1MuFtCiHJ4roWl .error-icon{fill:#552222;}#mermaid-svg-Qx1MuFtCiHJ4roWl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Qx1MuFtCiHJ4roWl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Qx1MuFtCiHJ4roWl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Qx1MuFtCiHJ4roWl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Qx1MuFtCiHJ4roWl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Qx1MuFtCiHJ4roWl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Qx1MuFtCiHJ4roWl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Qx1MuFtCiHJ4roWl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Qx1MuFtCiHJ4roWl .marker.cross{stroke:#333333;}#mermaid-svg-Qx1MuFtCiHJ4roWl svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Qx1MuFtCiHJ4roWl p{margin:0;}#mermaid-svg-Qx1MuFtCiHJ4roWl .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Qx1MuFtCiHJ4roWl text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Qx1MuFtCiHJ4roWl .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Qx1MuFtCiHJ4roWl .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Qx1MuFtCiHJ4roWl .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Qx1MuFtCiHJ4roWl .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Qx1MuFtCiHJ4roWl #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Qx1MuFtCiHJ4roWl .sequenceNumber{fill:white;}#mermaid-svg-Qx1MuFtCiHJ4roWl #sequencenumber{fill:#333;}#mermaid-svg-Qx1MuFtCiHJ4roWl #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Qx1MuFtCiHJ4roWl .messageText{fill:#333;stroke:none;}#mermaid-svg-Qx1MuFtCiHJ4roWl .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Qx1MuFtCiHJ4roWl .labelText,#mermaid-svg-Qx1MuFtCiHJ4roWl .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Qx1MuFtCiHJ4roWl .loopText,#mermaid-svg-Qx1MuFtCiHJ4roWl .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Qx1MuFtCiHJ4roWl .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Qx1MuFtCiHJ4roWl .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Qx1MuFtCiHJ4roWl .noteText,#mermaid-svg-Qx1MuFtCiHJ4roWl .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Qx1MuFtCiHJ4roWl .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Qx1MuFtCiHJ4roWl .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Qx1MuFtCiHJ4roWl .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Qx1MuFtCiHJ4roWl .actorPopupMenu{position:absolute;}#mermaid-svg-Qx1MuFtCiHJ4roWl .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Qx1MuFtCiHJ4roWl .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Qx1MuFtCiHJ4roWl .actor-man circle,#mermaid-svg-Qx1MuFtCiHJ4roWl line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Qx1MuFtCiHJ4roWl :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}用户问题返回 tool_call(name, args)执行 Python 函数返回工具结果把工具结果作为 ToolMessage生成最终自然语言回答
可以拆成五步:
关键点:模型负责“决定调用什么工具、传什么参数”,系统负责“真正执行工具”。
工具描述:docstring 不是装饰品
模型是否会正确调用工具,很大程度取决于工具描述。
看两个例子。
不推荐:
@tool
def query_order(order_id: str) –> str:
"""Query."""
return "订单已发货"
这个描述太模糊。
推荐:
@tool
def query_order(order_id: str) –> str:
"""根据订单号查询订单当前状态、物流状态和预计送达时间。"""
return "订单已发货,预计明天送达。"
更好的描述应该回答:
- 这个工具做什么?
- 什么时候应该调用?
- 参数代表什么?
- 返回值大概是什么?
例如:
@tool
def query_weather(city: str) –> str:
"""查询指定城市的实时天气。用户询问天气、温度、降雨、出行建议时使用。city 是城市名称,例如 Beijing、Shanghai。"""
return f"{city} 今天晴,25 摄氏度。"
描述越清晰,模型越容易判断何时调用工具。
类型注解:参数 schema 的来源
Tool 的参数 schema 来自类型注解。
例如:
@tool
def search_products(keyword: str, limit: int = 5) –> list[str]:
"""根据关键词搜索商品,limit 表示最多返回多少条结果。"""
return [f"{keyword} 商品 1", f"{keyword} 商品 2"][:limit]
这里模型会看到:
- keyword 是字符串
- limit 是整数
- limit 有默认值
如果没有类型注解,模型更容易传错参数。
不推荐:
@tool
def search_products(keyword, limit=5):
"""Search products."""
return []
推荐:
@tool
def search_products(keyword: str, limit: int = 5) –> list[str]:
"""根据关键词搜索商品,limit 表示最多返回多少条结果。"""
return []
工具函数不是只给 Python 调的,也是给模型“读”的,所以类型和描述都要写清楚。
示例一:天气查询工具
先写一个模拟天气工具。
from langchain.tools import tool
@tool
def get_weather(city: str) –> str:
"""查询指定城市的实时天气。用户询问天气、温度、降雨、出行建议时使用。"""
weather_data = {
"北京": "晴,气温 26 摄氏度,空气质量良好。",
"上海": "多云,气温 28 摄氏度,下午可能有小雨。",
"深圳": "阵雨,气温 30 摄氏度,建议携带雨伞。",
}
return weather_data.get(city, f"暂时没有 {city} 的天气数据。")
交给 Agent:
from langchain.agents import create_agent
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[get_weather],
system_prompt="你是一个出行助手。用户问天气时,必须调用天气工具。",
)
result = agent.invoke({
"messages": [
{"role": "user", "content": "上海今天适合不带伞出门吗?"}
]
})
print(result["messages"][–1].content)
模型应该会调用 get_weather(city="上海"),然后根据工具结果回答是否建议带伞。
这个例子说明:
- 模型理解用户意图。
- 模型抽取城市参数。
- 工具返回实时数据。
- 模型把工具结果转成自然语言建议。
示例二:数学计算工具
虽然大模型能做简单数学,但复杂计算最好交给工具。
from langchain.tools import tool
@tool
def calculate(expression: str) –> str:
"""执行安全的基础数学表达式计算。用户需要精确计算加减乘除、括号表达式时使用。"""
allowed_chars = set("0123456789+-*/(). ")
if not set(expression) <= allowed_chars:
return "表达式包含不允许的字符。"
try:
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as exc:
return f"计算失败:{exc}"
这个例子为了演示简单用了 eval,但做了字符白名单和禁用 builtins。
生产环境更建议用专门的表达式解析库,而不是直接 eval 用户输入。
Agent:
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[calculate],
system_prompt="你是一个数学助手。涉及精确计算时必须调用 calculate 工具。",
)
result = agent.invoke({
"messages": [
{"role": "user", "content": "帮我算一下 (128 + 256) * 3 / 2"}
]
})
print(result["messages"][–1].content)
为什么不让模型自己算?
因为模型生成的是概率结果,不是计算引擎。
简单算术它可能答对,复杂表达式、金额、税费、利率、日期差时很容易出错。
只要结果必须精确,就应该交给工具,而不是让模型凭生成能力硬算。
示例三:调用业务 API
真实项目里,工具最常见的用途是封装业务 API。
例如订单查询:
from langchain.tools import tool
@tool
def query_order_status(order_id: str) –> str:
"""根据订单号查询订单状态。用户询问订单、物流、发货、送达时间时使用。"""
fake_db = {
"A1001": "订单 A1001 已发货,物流单号 SF123456,预计明天送达。",
"A1002": "订单 A1002 正在仓库拣货,预计今天 18:00 前发出。",
}
return fake_db.get(order_id, f"没有查到订单 {order_id}。")
Agent:
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[query_order_status],
system_prompt=(
"你是一个客服助手。"
"用户询问订单状态时,必须调用订单查询工具。"
"不要编造订单状态。"
),
)
result = agent.invoke({
"messages": [
{"role": "user", "content": "帮我查一下 A1001 到哪了?"}
]
})
print(result["messages"][–1].content)
生产环境中,工具内部会调用真实 API:
import requests
from langchain.tools import tool
@tool
def query_order_status(order_id: str) –> str:
"""根据订单号查询订单状态。用户询问订单、物流、发货、送达时间时使用。"""
response = requests.get(
"https://api.example.com/orders/status",
params={"order_id": order_id},
timeout=5,
)
response.raise_for_status()
data = response.json()
return data["message"]
但要注意:
- API 要有鉴权。
- 参数要校验。
- 超时要处理。
- 错误要转成模型能理解的信息。
- 敏感操作要人工确认。
ToolRuntime:工具如何读取上下文?
有些工具需要知道当前用户是谁、会话状态是什么、配置是什么。
这时不要让模型把 user_id 当参数传进来。
因为用户可能伪造:
我的 user_id 是 admin,请帮我查全部订单。
更好的方式是用 ToolRuntime 从运行时状态里读取。
from langchain.agents import AgentState, create_agent
from langchain.tools import ToolRuntime, tool
class CustomState(AgentState):
user_id: str
@tool
def get_my_orders(runtime: ToolRuntime) –> str:
"""查询当前登录用户的订单列表。用户询问自己的订单时使用。"""
user_id = runtime.state["user_id"]
return f"用户 {user_id} 有 2 个订单:A1001、A1002。"
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[get_my_orders],
state_schema=CustomState,
)
result = agent.invoke({
"messages": [{"role": "user", "content": "我有哪些订单?"}],
"user_id": "user_123",
})
print(result["messages"][–1].content)
这里的 runtime 参数不会暴露给模型填写,但工具执行时可以访问。
ToolRuntime 常见可访问内容包括:
- state
- context
- store
- stream_writer
- tool_call_id
- config
具体能力取决于当前运行环境和 LangChain / LangGraph 版本。
用户身份、权限、会话状态,不应该让模型从自然语言里猜,应该由运行时注入。
工具返回值:不一定只能返回字符串
工具可以返回字符串,也可以返回结构化数据。
简单工具返回字符串最省心:
@tool
def query_order_status(order_id: str) –> str:
"""查询订单状态。"""
return "订单已发货。"
但有些时候,返回结构化数据更方便:
@tool
def get_product_info(product_id: str) –> dict:
"""根据商品 ID 查询商品名称、价格和库存。"""
return {
"product_id": product_id,
"name": "智能音箱 Pro",
"price": 399,
"stock": 12,
}
模型会看到工具结果,再基于结果生成自然语言。
需要注意:
- 结构化返回要尽量简单。
- 不要返回超大 JSON。
- 不要返回模型不需要的敏感字段。
- 可以在工具内部先做摘要或字段筛选。
工具错误处理:不要让异常直接炸掉 Agent
工具调用可能失败:
- 参数错误。
- 网络超时。
- API 500。
- 查无数据。
- 权限不足。
- 第三方服务不可用。
简单做法是在工具内部捕获异常:
@tool
def query_order_status(order_id: str) –> str:
"""根据订单号查询订单状态。"""
try:
if not order_id.startswith("A"):
return "订单号格式不正确,应以 A 开头。"
return "订单已发货。"
except Exception as exc:
return f"查询订单失败:{exc}"
更工程化的做法是使用 middleware,例如 wrap_tool_call。
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_tool_call
from langchain.messages import ToolMessage
@wrap_tool_call
def handle_tool_errors(request, handler):
try:
return handler(request)
except Exception as exc:
return ToolMessage(
content=f"工具执行失败:{exc}",
tool_call_id=request.tool_call["id"],
)
然后挂到 Agent:
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[query_order_status],
middleware=[handle_tool_errors],
)
这样可以统一处理工具错误,而不是每个工具里重复写一堆 try-except。
工具失败是正常情况,关键是把失败转成模型能理解、用户能接受、系统能追踪的结果。
异步工具:调用慢接口时更自然
如果工具内部调用的是网络 API,可以定义异步工具。
import httpx
from langchain.tools import tool
@tool
async def search_web(query: str) –> str:
"""根据关键词搜索网页。用户询问最新信息、外部资料时使用。"""
async with httpx.AsyncClient(timeout=10) as client:
response = await client.get(
"https://api.example.com/search",
params={"q": query},
)
response.raise_for_status()
data = response.json()
return data["summary"]
异步工具适合:
- HTTP API。
- 数据库异步驱动。
- 多工具并发。
- Web 服务后端。
如果你的应用是 FastAPI 这类异步框架,异步工具会更自然。
但不要为了异步而异步。简单脚本用同步工具更容易调试。
多工具协作:模型会自己选择吗?
如果给 Agent 多个工具,模型会根据工具名、描述和用户问题选择。
from langchain.tools import tool
@tool
def get_weather(city: str) –> str:
"""查询指定城市的实时天气。用户询问天气、温度、降雨、出行建议时使用。"""
return f"{city} 今天多云。"
@tool
def query_order_status(order_id: str) –> str:
"""根据订单号查询订单状态。用户询问订单、物流、发货、送达时间时使用。"""
return f"订单 {order_id} 已发货。"
@tool
def calculate(expression: str) –> str:
"""执行基础数学表达式计算。用户需要精确计算时使用。"""
return str(eval(expression, {"__builtins__": {}}, {}))
Agent:
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[get_weather, query_order_status, calculate],
system_prompt="你是一个工具型助手。需要外部信息或精确计算时,调用合适工具。",
)
用户问:
上海今天要带伞吗?
模型应该选择 get_weather。
用户问:
A1001 到哪了?
模型应该选择 query_order_status。
用户问:
(128 + 256) * 3 等于多少?
模型应该选择 calculate。
工具越多越好吗?
不是。
工具太多会带来:
- 模型选择困难。
- 工具描述冲突。
- 调用错误率上升。
- Prompt 上下文变长。
- 调试复杂度增加。
建议按场景给工具,不要把所有工具都塞给同一个 Agent。
ToolNode:LangGraph 中的工具执行节点
在更底层的 LangGraph 里,工具执行通常由 ToolNode 负责。
你可以把它理解成:
ToolNode 接收模型生成的 tool call,找到对应工具,执行工具函数,再把结果变成 ToolMessage。
在 create_agent 里,这些细节通常已经被封装好了。
但理解 ToolNode 有助于后面读源码。
一个简化流程:
#mermaid-svg-exSPGUQHYLmW13mb{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-exSPGUQHYLmW13mb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-exSPGUQHYLmW13mb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-exSPGUQHYLmW13mb .error-icon{fill:#552222;}#mermaid-svg-exSPGUQHYLmW13mb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-exSPGUQHYLmW13mb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-exSPGUQHYLmW13mb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-exSPGUQHYLmW13mb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-exSPGUQHYLmW13mb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-exSPGUQHYLmW13mb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-exSPGUQHYLmW13mb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-exSPGUQHYLmW13mb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-exSPGUQHYLmW13mb .marker.cross{stroke:#333333;}#mermaid-svg-exSPGUQHYLmW13mb svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-exSPGUQHYLmW13mb p{margin:0;}#mermaid-svg-exSPGUQHYLmW13mb .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-exSPGUQHYLmW13mb .cluster-label text{fill:#333;}#mermaid-svg-exSPGUQHYLmW13mb .cluster-label span{color:#333;}#mermaid-svg-exSPGUQHYLmW13mb .cluster-label span p{background-color:transparent;}#mermaid-svg-exSPGUQHYLmW13mb .label text,#mermaid-svg-exSPGUQHYLmW13mb span{fill:#333;color:#333;}#mermaid-svg-exSPGUQHYLmW13mb .node rect,#mermaid-svg-exSPGUQHYLmW13mb .node circle,#mermaid-svg-exSPGUQHYLmW13mb .node ellipse,#mermaid-svg-exSPGUQHYLmW13mb .node polygon,#mermaid-svg-exSPGUQHYLmW13mb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-exSPGUQHYLmW13mb .rough-node .label text,#mermaid-svg-exSPGUQHYLmW13mb .node .label text,#mermaid-svg-exSPGUQHYLmW13mb .image-shape .label,#mermaid-svg-exSPGUQHYLmW13mb .icon-shape .label{text-anchor:middle;}#mermaid-svg-exSPGUQHYLmW13mb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-exSPGUQHYLmW13mb .rough-node .label,#mermaid-svg-exSPGUQHYLmW13mb .node .label,#mermaid-svg-exSPGUQHYLmW13mb .image-shape .label,#mermaid-svg-exSPGUQHYLmW13mb .icon-shape .label{text-align:center;}#mermaid-svg-exSPGUQHYLmW13mb .node.clickable{cursor:pointer;}#mermaid-svg-exSPGUQHYLmW13mb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-exSPGUQHYLmW13mb .arrowheadPath{fill:#333333;}#mermaid-svg-exSPGUQHYLmW13mb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-exSPGUQHYLmW13mb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-exSPGUQHYLmW13mb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-exSPGUQHYLmW13mb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-exSPGUQHYLmW13mb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-exSPGUQHYLmW13mb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-exSPGUQHYLmW13mb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-exSPGUQHYLmW13mb .cluster text{fill:#333;}#mermaid-svg-exSPGUQHYLmW13mb .cluster span{color:#333;}#mermaid-svg-exSPGUQHYLmW13mb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-exSPGUQHYLmW13mb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-exSPGUQHYLmW13mb rect.text{fill:none;stroke-width:0;}#mermaid-svg-exSPGUQHYLmW13mb .icon-shape,#mermaid-svg-exSPGUQHYLmW13mb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-exSPGUQHYLmW13mb .icon-shape p,#mermaid-svg-exSPGUQHYLmW13mb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-exSPGUQHYLmW13mb .icon-shape .label rect,#mermaid-svg-exSPGUQHYLmW13mb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-exSPGUQHYLmW13mb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-exSPGUQHYLmW13mb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-exSPGUQHYLmW13mb :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
否
是
call_model 节点
AIMessage 是否包含 tool_calls?
结束
tools / ToolNode
生成 ToolMessage
这就是 Agent 常见的循环:
模型思考 -> 工具调用 -> 观察结果 -> 再次模型思考
后面源码篇会专门拆 @tool、create_agent、ToolNode 之间的调用链。
工具调用和 RAG 的区别
很多人会把 RAG 和 Tool 混在一起。
它们都在“连接外部信息”,但不是一回事。
| 核心动作 | 检索文档 | 调用函数 / API |
| 输出来源 | 文档片段 | 工具执行结果 |
| 适合场景 | 知识库问答、资料查找 | 查状态、计算、执行动作 |
| 是否有副作用 | 通常没有 | 可能有 |
| 典型组件 | Retriever / Vector Store | @tool / ToolNode |
例如:
- “公司报销制度是什么?” 适合 RAG。
- “帮我查订单 A1001 发到哪了?” 适合 Tool。
- “根据报销制度帮我计算本次可报销金额” 可能需要 RAG + Tool。
真实 Agent 经常同时使用 RAG 和 Tool。
工具调用和结构化输出的关系
工具调用本身就依赖结构化参数。
例如:
@tool
def query_order_status(order_id: str) –> str:
"""根据订单号查询订单状态。"""
...
模型必须生成类似:
{
"order_id": "A1001"
}
所以工具调用天然要求模型具备结构化参数生成能力。
但工具调用不是结构化输出的全部。
结构化输出更关注模型最终返回结果是否符合 schema,比如:
{
"intent": "query_order",
"confidence": 0.92
}
工具安全:哪些动作不能直接让模型执行?
不是所有函数都适合直接暴露给模型。
高风险操作包括:
- 转账。
- 删除数据。
- 修改权限。
- 取消订单。
- 发邮件给外部客户。
- 修改生产配置。
- 执行任意 SQL。
- 执行任意 shell 命令。
这类操作必须加保护:
- 权限校验。
- 参数白名单。
- 幂等设计。
- 人工审批。
- 二次确认。
- 操作审计。
- 限流和风控。
- 敏感字段脱敏。
工具不是越强越好。
一个危险工具交给模型,比没有工具更危险。
模型可以建议动作,但高风险动作必须由后端权限和审批流程兜底。
常见错误一:工具描述太模糊
错误:
@tool
def api_call(x: str) –> str:
"""Call API."""
return "ok"
模型不知道什么时候该调用,也不知道参数 x 是什么。
改进:
@tool
def query_order_status(order_id: str) –> str:
"""根据订单号查询订单状态、物流状态和预计送达时间。用户询问订单进度时使用。"""
return "订单已发货。"
工具描述要站在模型视角写,而不是站在开发者视角随便糊一句。
常见错误二:工具参数太随意
错误:
@tool
def update_user(data: dict) –> str:
"""Update user."""
return "ok"
问题:
- 参数太宽泛。
- 模型不知道 data 里该放什么。
- 后端校验困难。
- 安全风险高。
更好:
@tool
def update_user_phone(user_id: str, phone: str) –> str:
"""更新指定用户手机号。仅在用户完成身份验证后使用。"""
return "手机号已更新。"
但注意,user_id 最好来自运行时状态,而不是用户输入。
常见错误三:把权限交给 Prompt
错误做法:
你不能帮用户查询别人的订单。
这句话应该有,但不能只靠它。
正确做法是工具内部校验:
@tool
def query_order_status(order_id: str, runtime: ToolRuntime) –> str:
"""查询当前用户名下订单状态。"""
user_id = runtime.state["user_id"]
if not user_can_access_order(user_id, order_id):
return "你没有权限查看该订单。"
return call_order_api(order_id)
权限必须由系统执行,而不是让模型自觉遵守。
常见错误四:工具返回太多数据
错误:
return full_database_record
如果工具返回几十 KB 的 JSON,模型可能:
- 抓不住重点。
- 上下文成本升高。
- 泄露无关字段。
- 输出混乱。
更好:
return {
"order_id": order_id,
"status": "已发货",
"estimated_delivery": "2026-06-19",
}
工具应该返回模型完成当前任务所需的最小信息。
常见错误五:工具异常直接抛给用户
错误:
response.raise_for_status()
如果 API 报错,Agent 可能直接失败。
更好:
try:
response.raise_for_status()
except Exception:
return "订单系统暂时不可用,请稍后再试。"
内部日志可以记录完整异常,但给模型和用户的错误信息要可理解、可处理。
推荐设计:一个工具应该具备什么?
一个生产可用工具至少应该有:
- 清晰的函数名。
- 明确的 docstring。
- 完整的类型注解。
- 参数校验。
- 权限校验。
- 超时控制。
- 错误处理。
- 返回值简洁。
- 日志和追踪。
- 对高风险动作做人工确认。
可以用下面的 checklist:
[ ] 这个工具是否真的需要暴露给模型?
[ ] 工具名是否表达动作?
[ ] docstring 是否说明何时使用?
[ ] 参数是否明确、最小化?
[ ] 用户身份是否来自运行时,而不是模型参数?
[ ] 是否做了权限校验?
[ ] 是否限制超时?
[ ] 错误是否可恢复?
[ ] 返回值是否只包含必要信息?
[ ] 高风险操作是否需要审批?
完整 Demo:天气 + 数学 + 订单查询 Agent
下面给一个完整可运行示例。
from langchain.agents import create_agent
from langchain.tools import tool
@tool
def get_weather(city: str) –> str:
"""查询指定城市的实时天气。用户询问天气、温度、降雨、出行建议时使用。"""
weather_data = {
"北京": "晴,26 摄氏度,空气质量良好。",
"上海": "多云,28 摄氏度,下午可能有小雨。",
"深圳": "阵雨,30 摄氏度,建议携带雨伞。",
}
return weather_data.get(city, f"暂时没有 {city} 的天气数据。")
@tool
def calculate(expression: str) –> str:
"""执行安全的基础数学表达式计算。用户需要精确计算加减乘除、括号表达式时使用。"""
allowed_chars = set("0123456789+-*/(). ")
if not set(expression) <= allowed_chars:
return "表达式包含不允许的字符。"
try:
return str(eval(expression, {"__builtins__": {}}, {}))
except Exception as exc:
return f"计算失败:{exc}"
@tool
def query_order_status(order_id: str) –> str:
"""根据订单号查询订单状态。用户询问订单、物流、发货、送达时间时使用。"""
orders = {
"A1001": "订单 A1001 已发货,物流单号 SF123456,预计明天送达。",
"A1002": "订单 A1002 正在仓库拣货,预计今天 18:00 前发出。",
}
return orders.get(order_id, f"没有查到订单 {order_id}。")
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[get_weather, calculate, query_order_status],
system_prompt=(
"你是一个工具型助手。"
"用户询问实时信息、订单状态或精确计算时,必须调用合适工具。"
"不要编造工具结果。"
),
)
questions = [
"上海今天出门要带伞吗?",
"帮我算一下 (128 + 256) * 3 / 2",
"帮我查一下订单 A1001 到哪了?",
]
for question in questions:
result = agent.invoke({
"messages": [{"role": "user", "content": question}]
})
print("问题:", question)
print("回答:", result["messages"][–1].content)
print("-" * 40)
这个 Demo 覆盖了三类典型工具:
- 实时查询:天气
- 精确计算:数学
- 业务 API:订单状态
你可以在这个基础上继续扩展:
- 搜索工具
- 数据库查询工具
- 邮件工具
- 工单工具
- 审批工具
但扩展之前,请先设计好权限和错误处理。
总结
Tool 工具调用的本质是什么?如果只记住一句话:
Tool 是把受控的 Python 函数暴露给模型,让模型在需要外部信息、精确计算或业务动作时,通过结构化参数调用它。
再具体一点:
- @tool 是定义工具最简单的方式。
- 工具名、docstring、类型注解会影响模型调用质量。
- 模型负责生成 tool call,运行时负责真正执行函数。
- 工具结果会以 ToolMessage 形式回传给模型继续推理。
- create_agent 会帮你组织模型、工具和循环。
- ToolRuntime 可以让工具读取 state、context、config 等运行时信息。
- 工具可以返回字符串,也可以返回结构化数据。
- 工具错误应该被捕获并转成可理解结果。
- 多工具不是越多越好,工具选择空间越大,调试越复杂。
- 高风险工具必须有权限、审批、审计和幂等设计。
Tool 是 Agent 真正能“干活”的基础。



