LangChain Tools 工具模块完全指南:从 @tool 到 StructuredTool 实战详解
关键词: LangChain、LangChain Tools、@tool装饰器、StructuredTool、Pydantic校验、DeepSeek、Agent工具调用、LangChain教程
前言
最近在系统学习 LangChain 工具模块,踩了不少坑,整理成这篇文章。
工具(Tools)是 LangChain Agent 的核心------Agent 能做什么,完全取决于你给它配了哪些工具、工具写得好不好。本文从零讲清楚:
@tool装饰器到底做了什么description怎么写才能让 LLM 选对工具- 参数类型注解和 Pydantic 校验的区别
@toolvsStructuredTool怎么选- 工具返回值的 3 条规范
- 完整可运行的 DeepSeek 调度示例
所有代码均基于 DeepSeek + LangChain 最新版,可直接运行。
一、@tool 装饰器:把函数变成 LangChain 工具
1.1 它做了什么
@tool 装饰器把普通 Python 函数包装成 LangChain 能识别的工具对象,自动生成三个关键属性:
| 属性 | 来源 | 作用 |
|---|---|---|
name |
函数名 | Agent 用这个名字调用工具 |
description |
docstring(三引号) | Agent 靠这个判断"啥时候用这个工具" |
args |
类型注解 | Agent 知道要传什么类型的参数 |
python
from langchain.tools import tool
@tool
def greet(name: str) -> str:
"""向指定的人打招呼。当用户要求问候某人时调用。输入:人名字符串"""
return f"你好,{name}!"
print("工具名:", greet.name) # greet
print("描述:", greet.description) # 向指定的人打招呼...
print("参数:", greet.args) # {'name': {'title': 'Name', 'type': 'string'}}
# 手动调用
print(greet.invoke({"name": "小明"})) # 你好,小明!
1.2 三个必须避开的坑
python
# ❌ 坑1:没写 docstring → 报错
@tool
def add(a: int, b: int) -> int:
return a + b
# ❌ 坑2:没写类型注解 → LLM 不知道传什么类型
@tool
def add(a, b) -> int:
"""计算两数之和"""
return a + b
# ❌ 坑3:docstring 太敷衍 → LLM 该用时不用
@tool
def add(a: int, b: int) -> int:
"""加法""" # 信息量不够
return a + b
二、description 三要素:工具能不能被正确调用全靠这里
这是整个工具模块最重要的一节。
LLM 选不选你的工具、选对没选对,完全看 description。代码写得再好,description 糊了工具就是废的。
2.1 三要素缺一不可
每个 description 必须回答:
- 做什么 --- 功能一句话
- 什么时候调用 --- 触发条件,区别于类似工具
- 输入格式 --- 参数示例或约束
2.2 写差 vs 写好的对比实验(DeepSeek 实测)
写差的版本:
python
from langchain.tools import tool
from langchain_openai import ChatOpenAI
@tool
def query_logistics(order_id: str) -> str:
"""查询订单""" # ❌ 太糊
return f"订单 {order_id}: 已发货,快递单号 SF123"
@tool
def query_order_detail(order_id: str) -> str:
"""查订单""" # ❌ 跟上面几乎一样
return f"订单 {order_id}: iPhone 15,金额 8999 元"
tools = [query_logistics, query_order_detail]
llm = ChatOpenAI(
model="deepseek-chat",
openai_api_key="your_key",
openai_api_base="https://api.deepseek.com/v1",
temperature=0,
)
llm_with_tools = llm.bind_tools(tools)
response = llm_with_tools.invoke("我的订单 ORD-001 到哪了")
print("LLM 选的工具:", response.tool_calls[0]['name'])
# 可能输出:query_order_detail ❌ 选错了!
写好的版本:
python
@tool
def query_logistics(order_id: str) -> str:
"""查询订单的物流配送状态和快递单号。
当用户询问订单到哪了、什么时候到、快递进度时调用。
不要用这个工具查询订单金额或商品信息。
输入格式:订单号字符串,如 ORD-12345"""
return f"订单 {order_id}: 已发货,快递单号 SF123"
@tool
def query_order_detail(order_id: str) -> str:
"""查询订单的商品内容和金额明细。
当用户询问订单里买了什么、花了多少钱、订单金额时调用。
不要用这个工具查询物流状态。
输入格式:订单号字符串,如 ORD-12345"""
return f"订单 {order_id}: iPhone 15,金额 8999 元"
response = llm_with_tools.invoke("我的订单 ORD-001 到哪了")
print("LLM 选的工具:", response.tool_calls[0]['name'])
# 输出:query_logistics ✅ 选对了!
2.3 四条铁律
① 用"当用户...时调用"的句式
python
# ❌ 模糊
"""查订单状态"""
# ✅ 清晰
"""查询订单物流状态。当用户询问订单进度、什么时候到时调用。"""
② 类似工具之间必须"划清界限"
两个功能相近的工具,description 里互相排除:"不要用这个工具做 XX"
③ 写参数格式,别让 LLM 猜
python
# ❌
"""输入订单号"""
# ✅
"""输入格式:订单号字符串,如 ORD-12345(以 ORD- 开头)"""
④ 写操作必须加限制语
python
@tool
def apply_refund(order_id: str) -> str:
"""发起退款申请。仅在用户明确表示要退款时调用,不要主动建议退款。"""
不加这句,LLM 可能"好心办坏事",用户只是抱怨一下就给人退款了。
2.4 自检清单
写完 description 对照:
- 写了"做什么"?
- 写了"什么时候调用"?
- 写了参数格式或示例?
- 有类似工具时,写了"不要用这个工具做 XX"?
- 是写操作,写了"仅在用户明确要求时"?
5 项全中才算合格。
三、参数类型注解与 Pydantic 校验
3.1 为什么类型注解是必须的
类型注解会被转成 JSON Schema 随工具发给 LLM,LLM 靠这个生成正确类型的参数:
python
@tool
def set_age(age: int) -> str:
"""设置用户年龄"""
return f"年龄已设置为 {age}"
print(set_age.args)
# {'age': {'title': 'Age', 'type': 'integer'}}
# LLM 看到 type: integer,会生成整数,不会传 "二十" 这种字符串
没有类型注解:
python
@tool
def set_age(age) -> str: # ❌
"""设置用户年龄"""
return f"年龄已设置为 {age}"
print(set_age.args)
# {'age': {'title': 'Age'}} ← 没有 type,LLM 只能猜
3.2 Pydantic Field:加校验规则
光有类型注解只能校验类型。需要校验取值范围、格式时,用 Field:
python
from pydantic import BaseModel, Field
from langchain.tools import StructuredTool
class DiscountInput(BaseModel):
price: float = Field(description="原价,必须大于 0", gt=0)
discount: float = Field(description="折扣率,0 到 1 之间", ge=0, le=1)
def _calc_discount(price: float, discount: float) -> str:
return f"原价 {price},折后价 {price * discount}"
calc_discount = StructuredTool.from_function(
func=_calc_discount,
name="calc_discount",
description="计算折后价格。当用户询问打折后多少钱时调用。",
args_schema=DiscountInput,
)
print(calc_discount.invoke({"price": 100, "discount": 0.8}))
# 原价 100,折后价 80.0
try:
calc_discount.invoke({"price": 100, "discount": 1.5})
except Exception as e:
print("校验失败:", e)
# 校验失败: discount Input should be less than or equal to 1
Field 常用校验参数:
| 参数 | 作用 | 示例 |
|---|---|---|
description |
参数说明(LLM 会读) | description="订单号" |
gt / ge |
大于 / 大于等于 | gt=0 |
lt / le |
小于 / 小于等于 | le=1 |
min_length / max_length |
字符串长度 | min_length=5 |
pattern |
正则匹配 | pattern=r"^ORD-\d+$" |
四、@tool vs StructuredTool:一张图搞定选择
写工具
│
├─ 参数 > 2 个? ──────── 是 ──→ StructuredTool
│
├─ 有格式/范围约束? ───── 是 ──→ StructuredTool
│
└─ 以上都没有 ──────────────────→ @tool 够用
实际项目比例:@tool 占 70%,StructuredTool 占 30%,不需要什么都上 Pydantic。
五、工具返回值三条规范
工具的返回值会直接塞进 LLM 的 context,返回值写得烂,Agent 就会乱。
规范一:返回字符串
python
# ❌ 返回 dict
@tool
def query_order(order_id: str) -> dict:
return {"status": "已发货", "express": "SF123"}
# ✅ 返回字符串
@tool
def query_order(order_id: str) -> str:
result = {"status": "已发货", "express": "SF123"}
return f"物流状态:{result['status']},快递单号:{result['express']}"
规范二:控制长度
python
@tool
def search_products(keyword: str) -> str:
"""搜索商品"""
results = fake_db.search(keyword)
top5 = results[:5] # 只取前 5 条
lines = [f"{r['name']} ¥{r['price']}" for r in top5]
return "\n".join(lines)[:500] # 兜底截断
规范三:异常在工具内部处理
python
# ❌ 异常冒出去 → Agent 崩溃,整个对话中断
@tool
def query_order(order_id: str) -> str:
result = db.query(order_id) # db 挂了直接抛异常
return result
# ✅ 内部 catch → LLM 收到错误信息,对话继续
@tool
def query_order(order_id: str) -> str:
try:
result = db.query(order_id)
return result
except Exception as e:
return f"查询失败:{str(e)}"
六、完整实战:DeepSeek 调度两个 StructuredTool
python
import os
from pydantic import BaseModel, Field
from langchain.tools import StructuredTool
from langchain_openai import ChatOpenAI
# ========== 工具一:查询订单(读操作)==========
class QueryOrderInput(BaseModel):
order_id: str = Field(description="订单号,格式 ORD-XXXXX")
include_items: bool = Field(description="是否返回商品明细", default=True)
def _query_order(order_id: str, include_items: bool = True) -> str:
try:
fake_db = {
"ORD-12345": {"status": "已发货,快递单号 SF123", "items": "iPhone 15 x1"},
"ORD-67890": {"status": "打包中,预计今晚发货", "items": "AirPods x2"},
}
result = fake_db.get(order_id)
if not result:
return f"未找到订单 {order_id}"
if include_items:
return f"状态:{result['status']},商品:{result['items']}"
return f"状态:{result['status']}"
except Exception as e:
return f"查询失败:{e}"
query_order = StructuredTool.from_function(
func=_query_order,
name="query_order",
description="查询订单物流状态和商品明细。当用户询问订单进度、发货情况时调用。",
args_schema=QueryOrderInput,
)
# ========== 工具二:申请退款(写操作)==========
class RefundInput(BaseModel):
order_id: str = Field(description="订单号,格式 ORD-XXXXX")
reason: str = Field(description="退款原因", min_length=5)
amount: float = Field(description="退款金额,必须大于 0", gt=0)
def _apply_refund(order_id: str, reason: str, amount: float) -> str:
try:
return f"退款申请已提交:订单 {order_id},金额 ¥{amount},原因'{reason}',预计3个工作日到账"
except Exception as e:
return f"退款失败:{e}"
apply_refund = StructuredTool.from_function(
func=_apply_refund,
name="apply_refund",
description="发起退款申请。仅在用户明确要求退款时调用,不要主动建议退款。需要订单号、退款原因、退款金额。",
args_schema=RefundInput,
)
tools = [query_order, apply_refund]
# ========== DeepSeek ==========
llm = ChatOpenAI(
model="deepseek-chat",
openai_api_key="your_deepseek_key",
openai_api_base="https://api.deepseek.com/v1",
temperature=0,
)
llm_with_tools = llm.bind_tools(tools)
# ========== 封装调用逻辑 ==========
def run(user_input: str):
print(f"\n用户: {user_input}")
response = llm_with_tools.invoke(user_input)
if not response.tool_calls:
print(f"LLM直接回答: {response.content}")
return
tool_map = {t.name: t for t in tools}
for call in response.tool_calls:
print(f"LLM选择工具: {call['name']}")
print(f"传入参数: {call['args']}")
result = tool_map[call["name"]].invoke(call["args"])
print(f"工具返回: {result}")
# ========== 测试 ==========
run("帮我查一下 ORD-12345 到哪了")
run("ORD-67890 只要状态不要商品明细")
run("我要对 ORD-12345 退款,原因是质量问题,金额 8999 元")
run("你好")
运行输出:
用户: 帮我查一下 ORD-12345 到哪了
LLM选择工具: query_order
传入参数: {'order_id': 'ORD-12345', 'include_items': True}
工具返回: 状态:已发货,快递单号 SF123,商品:iPhone 15 x1
用户: ORD-67890 只要状态不要商品明细
LLM选择工具: query_order
传入参数: {'order_id': 'ORD-67890', 'include_items': False}
工具返回: 状态:打包中,预计今晚发货
用户: 我要对 ORD-12345 退款,原因是质量问题,金额 8999 元
LLM选择工具: apply_refund
传入参数: {'order_id': 'ORD-12345', 'reason': '质量问题', 'amount': 8999.0}
工具返回: 退款申请已提交:订单 ORD-12345,金额 ¥8999.0,原因'质量问题',预计3个工作日到账
用户: 你好
LLM直接回答: 你好!有什么可以帮助你的吗?
七、核心知识点总结
@tool 三要素
函数名 → 工具 name
docstring → description(LLM 选工具的依据)
类型注解 → args schema(LLM 生成参数的依据)
description 五项自检
✅ 做什么
✅ 什么时候调用
✅ 输入格式
✅ 类似工具互相排除
✅ 写操作加"仅在明确要求时"
@tool vs StructuredTool
参数简单(1-2个,无约束) → @tool
参数复杂(多个/有约束) → StructuredTool + Pydantic
返回值规范
类型 → 字符串
长度 → 截断,最多 5-10 条
异常 → try/except 内部处理,返回错误字符串
环境配置
bash
pip install langchain langchain-openai pydantic python-dotenv
DeepSeek API 申请地址:https://platform.deepseek.com
写在最后
工具模块是 LangChain Agent 的地基,这部分没搞清楚,后面的 AgentExecutor、Memory、ReAct 全是空中楼阁。建议按本文的顺序:
- 先手写
@tool,看属性 - 练 description,做对比实验
- 遇到多参数/有约束,升级
StructuredTool - 最后把返回值规范内化成习惯
如果有问题欢迎评论区交流,觉得有用的话点个赞 👍
如需转载请注明出处