LangChain Tools工具模块完全指南:@tool装饰器+StructuredTool+Pydantic校验+实战案例

LangChain Tools 工具模块完全指南:从 @tool 到 StructuredTool 实战详解

关键词: LangChain、LangChain Tools、@tool装饰器、StructuredTool、Pydantic校验、DeepSeek、Agent工具调用、LangChain教程


前言

最近在系统学习 LangChain 工具模块,踩了不少坑,整理成这篇文章。

工具(Tools)是 LangChain Agent 的核心------Agent 能做什么,完全取决于你给它配了哪些工具、工具写得好不好。本文从零讲清楚:

  • @tool 装饰器到底做了什么
  • description 怎么写才能让 LLM 选对工具
  • 参数类型注解和 Pydantic 校验的区别
  • @tool vs StructuredTool 怎么选
  • 工具返回值的 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 必须回答:

  1. 做什么 --- 功能一句话
  2. 什么时候调用 --- 触发条件,区别于类似工具
  3. 输入格式 --- 参数示例或约束

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 全是空中楼阁。建议按本文的顺序:

  1. 先手写 @tool,看属性
  2. 练 description,做对比实验
  3. 遇到多参数/有约束,升级 StructuredTool
  4. 最后把返回值规范内化成习惯

如果有问题欢迎评论区交流,觉得有用的话点个赞 👍


如需转载请注明出处

相关推荐
花酒锄作田5 小时前
FastAPI 使用 session 认证
python·fastapi
lsswear5 小时前
Python 并发 线程
开发语言·python
Ivanqhz6 小时前
MLIR OpBuilder
开发语言·python·mlir
修电脑的猫6 小时前
在中国使用 Claude Code 解决 403 错误(VS CODE)
ai·sap
蚕豆糯米饭7 小时前
Github Copilot 研发效能提升实战指南
ai·github·copilot·ai编程
威联通安全存储7 小时前
TS-h2287XU-RP 在家电制造总装与质检数据场景的部署
python·制造
泡泡鱼(敲代码中)8 小时前
Python 字符串 str 完整学习笔记
python·学习
Token掘金室8 小时前
Aider配置自定义API教程
ai
troy1288 小时前
Python 基础语法(八):Web 后端开发、数据分析与可视化、网络爬虫、人工智能 / 大模型应用
前端·python·数据分析
我不会起名字3229 小时前
一天一道算法题(34):回溯法的经典例题(子集)
java·数据结构·python·算法·golang·深度优先·力扣