LangChain 五种工具定义方式踩坑总结

0 阅读9分钟

在LangChain Agent开发中,工具是调用外部能力、完成实操任务的核心。很多同学初学都会困惑:官方提供了多种工具定义写法,分不清区别、选不对场景,还容易踩版本和语法坑。

我结合实战整理了五种主流、最新无报错的工具定义方式,统一用「美食探店助手」场景演示,覆盖日常开发、原型调试、生产落地全场景。内容通俗易懂、可直接复用,个人总结有限,有疏漏欢迎大家评论指正、交流学习~

源码地址github.com/MaNongWuDao…

统一安装依赖,规避版本报错:

uv add langchain langchain-core langchain-openai pydantic python-dotenv

一、@tool 基础装饰器(最简入门)

LangChain 最轻量化的工具写法,普通函数加装饰器即可转为Agent工具,适合快速开发简单功能、迭代原型。


from langchain_core.tools import tool

@tool
def get_restaurant_avg_price(restaurant_name: str) -> str:
    """查询餐厅人均消费。
    Args:
        restaurant_name: 餐厅名字
    """
    # 模拟数据库查询
    mock_data = {
        "潮汕牛肉火锅": "人均 95元",
        "日料小馆": "人均 180元",
        "湘菜馆": "人均 75元"
    }
    price = mock_data.get(restaurant_name, "暂无该餐厅价格数据")
    return f"【{restaurant_name}】{price}"

# 测试
print(get_restaurant_avg_price.invoke({"restaurant_name": "潮汕牛肉火锅"}))

适用场景:单逻辑、无复杂参数的简易工具,快速原型开发。

二、@tool + parse_docstring(用得相对较少)

基础装饰器进阶用法,开启参数可自动解析Google风格注释,无需手动写Schema,大幅精简代码。避坑重点:该模式对注释格式严格,简介与Args需空行、参数冒号后需加空格,格式错误会直接报错。

from typing import Optional
from langchain_core.tools import tool

@tool(parse_docstring=True)
def calc_food_calorie(dish_name: str, weight_g: Optional[int] = 100) -> str:
    """估算菜品卡路里。

    Args:
        dish_name: 菜品名称,例如红烧肉、白米饭
        weight_g: 食物重量,单位克,默认100克
    """
    calorie_map = {"红烧肉": 395, "白米饭": 116, "炒青菜": 45}
    per_100g = calorie_map.get(dish_name, 100)
    total = per_100g * weight_g / 100
    return f"{dish_name} {weight_g}g 总热量:{total:.1f} 千卡"

if __name__ == "__main__":
    print(calc_food_calorie.invoke({"dish_name": "红烧肉", "weight_g": 200}))

适用场景:多参数工具,追求代码简洁、规范统一的场景。

三、StructuredTool(存量函数复用)

无需修改已有业务函数,零侵入将普通函数封装为Agent工具,兼容同步、异步方法,非常适合老项目改造、存量代码复用。


from langchain_core.tools import  StructuredTool
def split_aa_bill(total_money: float, person_count: int, tip: float = 0) -> str:
    """多人AA分摊账单,包含小费。
    
    Args:
        total_money: 总账单金额
        person_count: 聚餐人数
        tip: 小费金额,默认0
    """
    total = total_money + tip
    per_person = total / person_count
    return f"总账单{total}元,{person_count}人AA,每人应付:{per_person:.2f}元"

# 封装成StructuredTool
aa_tool = StructuredTool.from_function(split_aa_bill)

# 测试
print(aa_tool.invoke({"total_money": 500, "person_count": 4, "tip": 50}))

适用场景:复用成熟业务函数、不想侵入原有业务逻辑。

四、继承 BaseTool(生产环境首选)

五种写法中扩展性、稳定性最强,支持自定义参数校验、异常捕获、工具状态,是生产环境复杂工具的最优方案,唯一缺点是代码量稍多。


from typing import Optional
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field
# 定义入参Schema
class RecommendDishInput(BaseModel):
    restaurant_type: str = Field(description="餐厅类型,例如火锅、湘菜、日料")
    spicy_prefer: Optional[str] = Field(default="不辣", description="辣度偏好:不辣/微辣/特辣")

class DishRecommendTool(BaseTool):
    name: str = "recommend_dish"
    description: str = "根据餐厅类型和辣度推荐招牌菜"
    args_schema: type[BaseModel] = RecommendDishInput

    def _run(self, restaurant_type: str, spicy_prefer: str = "不辣") -> str:
        menu_map = {
            "火锅": {"不辣": "骨汤锅底+肥牛", "微辣": "鸳鸯锅+毛肚", "特辣": "红汤牛油锅+鸭肠"},
            "湘菜": {"不辣": "小炒黄牛肉(减辣)", "微辣": "剁椒鱼头", "特辣": "爆辣口味虾"}
        }
        dish = menu_map.get(restaurant_type, {}).get(spicy_prefer, "暂无推荐")
        return f"【{restaurant_type}】推荐菜品:{dish}"

# 实例化工具
recommend_tool = DishRecommendTool()

# 测试
print(recommend_tool.invoke({"restaurant_type": "湘菜", "spicy_prefer": "微辣"}))

适用场景:生产环境、复杂业务工具、需要强参数校验和异常处理的场景。

五、Runnable 转 Tool(链路复用)

针对已有LangChain Runnable流水线,可直接封装为Agent工具,避免重复造轮子。版本避坑:新版必须使用 args_schema,不可拼写错误,且需自定义入参模型。


from langchain_core.runnables import RunnableLambda
from langchain_core.tools import convert_runnable_to_tool
from pydantic import BaseModel, Field

# 定义输入参数模型
class ReviewInput(BaseModel):
    review_text: str = Field(description="用户的餐厅点评文本")

# 原始Runnable:清洗点评文本,提取一句话摘要
review_runnable = RunnableLambda(lambda x: f"【点评摘要】{x['review_text'].split('。')[0]}")

# Runnable转Tool,修正参数名 args_schema
review_summary_tool = convert_runnable_to_tool(
    review_runnable,
    name="review_summary",
    description="提取用户点评第一句话摘要",
    args_schema=ReviewInput,  # 这里!!改成 args_schema,并且传入我们定义好的ReviewInput
)

# 测试调用
print(review_summary_tool.invoke({"review_text": "这家店牛肉很嫩,上菜速度快。但是排队太久,周末建议提前预约。"}))

适用场景:已有Runnable业务链路,需要快速接入Agent复用逻辑。

六、五种工具写法选型对照表

极简总结,日常开发直接按需选用:

工具定义方式核心优势适用场景
@tool 基础装饰器极简零配置、上手快简单工具、快速原型开发
@tool 智能解析自动解析注释、精简代码多参数、规范型工具
StructuredTool零侵入、复用存量代码老项目改造、旧函数复用
BaseTool 继承类高自定义、稳定可扩展生产环境、复杂业务工具
Runnable 转 Tool复用流水线、避免冗余已有Runnable链路快速接入

七、核心实操避坑总结

  1. 开发选型:简易工具用装饰器,生产复杂工具优先 BaseTool,兼顾稳定性和扩展性。

  2. 注释报错解决:不想严格遵循Google注释格式,可添加 error_on_invalid_docstring=False 关闭强制校验。

  3. 版本适配:废弃 create_tool_calling_agent、create_react_agent,统一使用官方最新 create_agent。

  4. 新版参数坑:Runnable转工具必须写复数 args_schema,拼写错误直接报错。

以上就是LangChain五大工具定义的方式,都是踩坑后的总结,可直接落地复用。如果有错误或更好的实践,欢迎大家评论交流,一起学习进步!

八、完整实战:全部工具接入新版Agent

前面我们单独演示了五种工具的定义方式,这里将所有工具统一整合,接入LangChain官方最新稳定的 create_agent 实现完整Agent调用流程。

规避版本坑:彻底废弃老旧且已淘汰的 create_tool_calling_agent、create_react_agent,代码适配最新版LangChain,同时兼容阿里通义千问等国内大模型,开箱即用。

环境配置

项目根目录新建 .env 文件,配置模型密钥,避免硬编码泄露:

DASHSCOPE_API_KEY=你的通义千问密钥
DASHSCOPE_MODEL=qwen3.7-flash-2026-07-15
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1

完整整合代码

import os
from dotenv import load_dotenv
from typing import Optional
from langchain_core.tools import tool, StructuredTool, BaseTool, convert_runnable_to_tool
from langchain_core.runnables import RunnableLambda
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent

# 加载本地环境变量
load_dotenv()

#  工具1:@tool 基础装饰器
@tool
def get_restaurant_avg_price(restaurant_name: str) -> str:
    """查询餐厅人均消费。
    Args:
        restaurant_name: 餐厅名字
    """
    mock_data = {"潮汕牛肉火锅": "人均 95元", "日料小馆": "人均 180元", "湘菜馆": "人均 75元"}
    price = mock_data.get(restaurant_name, "暂无该餐厅价格数据")
    return f"【{restaurant_name}】{price}"

#  工具2:@tool + parse_docstring
@tool(parse_docstring=True)
def calc_food_calorie(dish_name: str, weight_g: Optional[int] = 100) -> str:
    """估算菜品卡路里。

    Args:
        dish_name: 菜品名称,例如红烧肉、白米饭
        weight_g: 食物重量,单位克,默认100克
    """
    calorie_map = {"红烧肉": 395, "白米饭": 116, "炒青菜": 45}
    per_100g = calorie_map.get(dish_name, 100)
    total = per_100g * weight_g / 100
    return f"{dish_name} {weight_g}g 总热量:{total:.1f} 千卡"

#  工具3:StructuredTool
def split_aa_bill(total_money: float, person_count: int, tip: float = 0) -> str:
    """多人AA分摊账单,包含小费。
    Args:
        total_money: 总账单金额
        person_count: 聚餐人数
        tip: 小费金额,默认0
    """
    total = total_money + tip
    per_person = total / person_count
    return f"总账单{total}元,{person_count}人AA,每人应付:{per_person:.2f}元"

aa_tool = StructuredTool.from_function(split_aa_bill)

#  工具4:继承 BaseTool
class RecommendDishInput(BaseModel):
    restaurant_type: str = Field(description="餐厅类型,例如火锅、湘菜、日料")
    spicy_prefer: Optional[str] = Field(default="不辣", description="辣度偏好:不辣/微辣/特辣")

class DishRecommendTool(BaseTool):
    name: str = "recommend_dish"
    description: str = "根据餐厅类型和辣度推荐招牌菜"
    args_schema: type[BaseModel] = RecommendDishInput

    def _run(self, restaurant_type: str, spicy_prefer: str = "不辣") -> str:
        menu_map = {
            "火锅": {"不辣": "骨汤锅底+肥牛", "微辣": "鸳鸯锅+毛肚", "特辣": "红汤牛油锅+鸭肠"},
            "湘菜": {"不辣": "小炒黄牛肉(减辣)", "微辣": "剁椒鱼头", "特辣": "爆辣口味虾"}
        }
        dish = menu_map.get(restaurant_type, {}).get(spicy_prefer, "暂无推荐")
        return f"【{restaurant_type}】推荐菜品:{dish}"

recommend_tool = DishRecommendTool()

#  工具5:Runnable 转 Tool
class ReviewInput(BaseModel):
    review_text: str = Field(description="用户的餐厅点评文本")

review_runnable = RunnableLambda(lambda x: f"【点评摘要】{x['review_text'].split('。')[0]}")
review_summary_tool = convert_runnable_to_tool(
    review_runnable,
    name="review_summary",
    description="提取用户点评核心摘要",
    args_schema=ReviewInput,
)

#  Agent 组装与运行
if __name__ == "__main__":
    # 初始化大模型
    llm = ChatOpenAI(
        model=os.getenv('DASHSCOPE_MODEL'),
        api_key=os.getenv("DASHSCOPE_API_KEY"),
        base_url=os.getenv('DASHSCOPE_BASE_URL')
    )

    # 注册全部工具
    tools = [get_restaurant_avg_price, calc_food_calorie, aa_tool, recommend_tool, review_summary_tool]

    # 创建新版Agent
    agent = create_agent(
        model=llm,
        tools=tools,
        system_prompt="你是美食探店助手,可调用对应工具完成用户需求,整合工具结果给出完整、清晰的回答。"
    )

    # 测试对话
    user_query = "周末4个人去湘菜馆,总消费500元,加50元小费,推荐微辣菜品并计算AA账单,同时帮我摘要这条点评:这家店牛肉很嫩,上菜速度快。但是排队太久,周末建议提前预约。"
    result = agent.invoke({"messages": [("human", user_query)]})

    print("n==== 最终回答 ====")
    print(result["messages"][-1].content)

运行说明

  1. Agent 会智能解析用户需求,自动匹配、调用对应工具,无需手动指定;

  2. 全程无过时API、无版本报错,可直接用于学习、项目开发。