Skip to content

🛠️ 9.5 自定义工具生态与参数校验 ​

“大模型的大脑再聪明,没有工具也只是个被困在玻璃瓶里的先知。”
工具(Tools)让 Agent 可以查询外部 API、访问数据库或执行受控操作。LangChain 1.4 提供了统一的定义方式,但参数校验、权限与副作用仍需由应用负责。


💡 先看一个直观例子:瑞士军刀卡槽与机械手接头 ​

1. 传统手写工具注册的痛点 ​

在第八章手搓 Agent 时,我们需要自己手写 JSON Schema 解析器、反射 Python 函数的签名、校验传入的参数类型、手动把工具声明拼进 System Prompt 中。
一旦函数参数变多,很容易出现参数类型不匹配、大模型漏传关键参数的问题。

2. 生活比喻:瑞士军刀卡槽与带卡尺的精密接口 ​

  • @tool 装饰器:就像在 Python 函数上装一个瑞士军刀标准卡槽,LangChain 会自动通过 Python 类型注解提取出符合 OpenAI / Anthropic 标准的 JSON Schema;
  • 函数注释(Docstring)即提示词契约(Prompt as Interface):Docstring 不是写给人看的,而是直接作为给大模型的说明书!大模型会根据这篇说明书判断“什么时候该用这个工具”;
  • Pydantic args_schema:就像在工具接口处放了一个高精度游标卡尺,大模型传过来的参数如果有负数、缺漏或格式错误,会在进入业务函数前直接被拦截并提示修复。

2. 生活比喻:瑞士军刀卡槽与带卡尺的精密接口


💻 核心实操:定义标准工具与多参数校验 ​

1. 轻量单参数工具(Docstring 驱动) ​

python
# code/s05_custom_tools.py
from langchain_core.tools import tool

@tool
def get_stock_price(ticker: str) -> str:
    """获取指定股票代码的实时市场最新价格与涨跌幅。
    
    Args:
        ticker: 股票代码,例如 'NVDA'、'AAPL'、'600519'
    """
    mock_data = {
        "NVDA": "英伟达 (NVDA) 当前价格: $128.50, 今日涨跌: +3.8%",
        "AAPL": "苹果 (AAPL) 当前价格: $224.30, 今日涨跌: -0.5%"
    }
    return mock_data.get(ticker.upper(), f"未查询到股票代码 {ticker} 的最新行情。")

2. 复杂多参数工具(Pydantic args_schema 严格防御) ​

对于金融测算、转账交易、系统指令等敏感场景,必须使用 Pydantic 定义输入参数边界:

python
from pydantic import BaseModel, Field

class LoanCalculatorInput(BaseModel):
    principal: float = Field(description="贷款总本金,单位:万元", gt=0)
    years: int = Field(description="贷款年限,例如 10、20、30", ge=1, le=30)
    annual_rate: float = Field(description="年化利率百分比,例如 3.5 代表 3.5%", ge=0.1, le=20.0)

@tool(args_schema=LoanCalculatorInput)
def calculate_equal_monthly_loan(principal: float, years: int, annual_rate: float) -> str:
    """计算等额本息房贷/车贷的每月还款额与总利息支出。
    
    用于精准金融测算,禁止大模型自行心算产生幻觉。
    """
    total_months = years * 12
    monthly_rate = (annual_rate / 100) / 12
    principal_yuan = principal * 10000
    
    # 等额本息月供公式
    monthly_payment = (principal_yuan * monthly_rate * ((1 + monthly_rate) ** total_months)) / (
        ((1 + monthly_rate) ** total_months) - 1
    )
    total_repayment = monthly_payment * total_months
    total_interest = total_repayment - principal_yuan
    
    return (
        f"【贷款测算结果】本金: {principal} 万元, 年限: {years} 年, 年利率: {annual_rate}%\n"
        f"• 每月还款额 (等额本息): ¥{monthly_payment:.2f} 元\n"
        f"• 累计支付总利息: ¥{total_interest/10000:.2f} 万元"
    )

3. 底层机制:llm.bind_tools 与 tool_calls 拦截 ​

大模型本身并不会直接运行 Python 代码,它是通过输出标准的 tool_calls 结构让运行时执行的:

python
from s01_model_io import get_chat_model

tools = [get_stock_price, calculate_equal_monthly_loan]
llm = get_chat_model()

# 1. 将工具声明绑定给模型
llm_with_tools = llm.bind_tools(tools)

# 2. 发起提问
ai_msg = llm_with_tools.invoke("查一下 NVDA 股价,并算一下贷 100 万 30 年 3.2% 利率每月还多少?")

# 3. 观察模型的决策
print("模型生成的 tool_calls 任务清单:")
for tc in ai_msg.tool_calls:
    print(f"👉 目标工具: {tc['name']}, 注入参数: {tc['args']}")

4. 1.4 工具生态补充:更宽容、更强大的工具契约 ​

  • 普通 Python 函数即可当工具:在 9.9 的 create_agent(model=..., tools=[...]) 中,官方文档明确“可传入任意 Python 可调用对象”——LangChain 会自动通过类型注解 + Docstring 生成 Schema,无需 @tool 也能用(加 @tool 是为了精细化控制);
  • 导入路径双通道:from langchain_core.tools import tool 与 from langchain.tools import tool 在 1.4 中等价;
  • 工具即 Runnable:BaseTool 实现了 Runnable 协议,因此工具可以直接 | 进 LCEL 管道,也可以被 LangGraph 的 ToolNode 批量调度执行(create_agent 内部正是用 ToolNode 跑工具的);
  • 工具抛错自动反馈模型:create_agent 运行工具时若抛出异常,会把错误信息以 ToolMessage 形式回灌给模型,让模型“自己意识到出错并重新规划”,而不是直接崩溃。
python
# 1.4 里甚至可以连 @tool 都省略(类型注解 + Docstring 驱动)
def get_weather(city: str) -> str:
    """查询指定城市天气。Args: city: 城市名"""
    return f"{city}: 晴,22°C"

from langchain.agents import create_agent
agent = create_agent(model=get_chat_model(), tools=[get_weather], system_prompt="你是天气助手。")

5. 工具 extras:一份工具,多厂商专属参数(1.2 新特性) ​

不同厂商对工具的配置各不相同(并行/串行执行、strict 模式、Token 上限……)。1.2 给工具新增 extras 属性,把厂商专属参数直接挂在工具定义上,一处配置、随处生效——切换厂商不用再改多处代码:

python
# code/s05_custom_tools.py —— demo_tool_extras()
from langchain_core.tools import tool

@tool(extras={
    "openai": {"parallel_tool_calls": False},      # OpenAI:禁用并行工具调用
    "anthropic": {"tool_choice_type": "auto"},     # Anthropic:工具选择策略
})
def get_weather(city: str) -> str:
    """查询指定城市天气。Args: city: 城市名"""
    return f"{city}: 晴 22°C"

print(get_weather.extras)   # {'openai': {...}, 'anthropic': {...}}

典型能力:Anthropic 的 programmatic tool calling(编程式工具调用) 与 tool search(工具搜索) 等厂商特性,都能通过 extras 统一注入。

6. 厂商“内建工具”:声明即用,不再手搓(1.2 新特性) ​

不用再自己拼 Prompt 实现“文件搜索、画图、开终端”,1.2 直接内置了由厂商在客户端本地执行的工具:

厂商内置工具
OpenAI文件搜索、联网搜索、代码解释器、图像生成、计算机使用(Computer Use)、Shell、MCP 连接器
Anthropic文本编辑器、网页抓取(Web Fetch)、计算机使用、工具搜索、MCP 工具集
python
# 示意:把 OpenAI 官方“文件搜索 / 联网搜索”等内建工具装配进 Agent
# (具体导入路径与工具名以 1.2+ 官方 Tools 文档为准)
from langchain_openai.tools import FileSearchTool, WebSearchTool

agent = create_agent(model=llm, tools=[FileSearchTool(...), my_custom_tool])

📌 与本节前面讲的“工具即 Runnable / 交给 ToolNode 执行”不冲突:内建工具是由模型提供商官方封装、可声明即用的进阶形态。

7. 🆕 1.4 工具侧新货:apply_patch 内建工具 + 延迟挂载(defer_loading) ​

1.4 在工具侧也有两个实用增量:

  • apply_patch 内建工具:仿 OpenAI Codex CLI 的补丁式文件编辑(用 diff/patch 语法一次精准改一段,而非重写全文件),已作为官方内建工具提供——编码类 Agent 的标配。
  • 延迟挂载:工具可以声明 extras={"defer_loading": True},或交给 ProviderToolSearchMiddleware(见 9.7)统一管理——模型需要时才由厂商服务端工具搜索取回完整 schema。比喻:主管出差不用背全部 50 份合同,要用哪份让助理现送(省 Token,尤其工具上百个时)。
python
from langchain_core.tools import tool

@tool(extras={"defer_loading": True})       # 标记为"可延迟加载"
def niche_report(region: str) -> str:
    """生成长尾地区报告。Args: region: 地区名"""
    return f"{region} 报告……"

📚 权威官方资料直达 ​


工具描述要让模型知道何时不该调用 ​

除了名称、用途和参数,工具说明还应写出前置条件、可能副作用和返回值含义。例如“查询订单”应说明需要订单号;“取消订单”应说明会改变外部状态,并在执行前校验身份与订单状态。

工具端必须把模型当作不可信调用方:做权限检查、参数归一化、超时限制和审计记录。错误结果应区分可重试与不可重试,例如网络超时可以稍后再试,权限不足则应停止并向用户说明所需条件。

🎯 本节小结与思考 ​

  1. 核心收获:掌握了 @tool 装饰器、Pydantic args_schema 参数边界约束、大模型 tool_calls 底层触发机制,以及 1.2~1.4 的工具生态增量(extras 多厂商参数、厂商内建工具、1.4 的 apply_patch 内建工具与 defer_loading 延迟挂载)。
  2. 下一步探索:工具能够单次执行,但当用户和 AI 进行十多轮连续交谈时,历史记录越来越长,如何持久化保存并在 Token 超标时智能裁剪?下一节我们学习 9.6 记忆管理与会话状态持久化。