主题
🛠️ 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:就像在工具接口处放了一个高精度游标卡尺,大模型传过来的参数如果有负数、缺漏或格式错误,会在进入业务函数前直接被拦截并提示修复。
💻 核心实操:定义标准工具与多参数校验
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} 报告……"📚 权威官方资料直达
- 🔗 LangChain 自定义工具官方指南:How to create custom tools
- 🔗 工具调用 (Tool Calling) 核心概念:Tool Calling Concepts
- 🔗 模型绑定工具深入指南:Binding tools to models
- 🔗 工具与 extras / 内建工具:LangChain Tools
- 🔗 ProviderToolSearchMiddleware(延迟挂载):Built-in Middleware · Provider tool search
工具描述要让模型知道何时不该调用
除了名称、用途和参数,工具说明还应写出前置条件、可能副作用和返回值含义。例如“查询订单”应说明需要订单号;“取消订单”应说明会改变外部状态,并在执行前校验身份与订单状态。
工具端必须把模型当作不可信调用方:做权限检查、参数归一化、超时限制和审计记录。错误结果应区分可重试与不可重试,例如网络超时可以稍后再试,权限不足则应停止并向用户说明所需条件。
🎯 本节小结与思考
- 核心收获:掌握了
@tool装饰器、Pydanticargs_schema参数边界约束、大模型tool_calls底层触发机制,以及 1.2~1.4 的工具生态增量(extras多厂商参数、厂商内建工具、1.4 的apply_patch内建工具与defer_loading延迟挂载)。 - 下一步探索:工具能够单次执行,但当用户和 AI 进行十多轮连续交谈时,历史记录越来越长,如何持久化保存并在 Token 超标时智能裁剪?下一节我们学习 9.6 记忆管理与会话状态持久化。