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. 最后把返回值规范内化成习惯

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


如需转载请注明出处

相关推荐
a34funny2 小时前
Python高级之操作Mysql
python·mysql·adb
兰.lan2 小时前
【黑马ai测试】安享智慧理财项目(ai辅助提效)
人工智能·python·功能测试·ai
m0_493934532 小时前
宝塔面板如何实现异地数据库备份_配置远程存储空间
jvm·数据库·python
pele2 小时前
Redis如何实现复杂逻辑的原子操作
jvm·数据库·python
yuanpan2 小时前
Python 读写 Redis 缓存数据库:写给 Python 初学者的入门案例
数据库·python·缓存
AI 编程助手GPT2 小时前
【实战】Codex 接管电脑 + Claude Routines 云端值守:一次 Bug 排查的“无人化”闭环
人工智能·gpt·ai·chatgpt·bug
m0_684501982 小时前
HTML图片怎么用Bitbucket Pipelines发布_Bitbucket自动构建HTML站点
jvm·数据库·python
小江的记录本2 小时前
【分布式】分布式核心组件——分布式限流:固定窗口、滑动窗口、漏桶、令牌桶算法,网关层/服务层限流实现
java·分布式·后端·python·算法·安全·面试
UltraLAB-F2 小时前
有限元分析内存需求深度解析:刚度矩阵、求解器与硬件配置
人工智能·ai·硬件架构