如果说模型负责“理解、推理和表达”,那么工具(Tools)负责“接触真实世界”。没有工具的模型,只能基于训练数据和当前上下文进行回答;有了工具,模型才能查天气、查订单、读数据库、检索知识库、发送草稿、访问内部接口。因此,工具是从“会聊天”走向“能办事”的关键桥梁。
本章会从 Tool 的概念、定义方式、Schema 设计、返回值设计、状态访问、错误处理,到与 Agent 协同工作的方法,系统讲清楚如何构建真正“好用”的工具。
Tool 本质上是一段可被模型调用的函数能力。根据 LangChain 官方文档,一个工具通常包含三个关键元素:
最容易理解的比喻是:Tool 就是给模型看的 API 文档。
如果你只给程序员看,它是函数;如果你既要给程序员看,也要给模型看,它就必须设计成 Tool。
普通函数通常只考虑:
而 Tool 还要额外考虑:
from langchain_core.tools import tool @tool def get_weather(city: str) -> str: """查询指定城市天气。""" return f"{city} 今日晴,24 到 31 摄氏度。"
这个工具已经具备:
LangChain 官方文档明确强调,工具 docstring 应当简洁且信息充分,因为它直接帮助模型理解何时使用该工具。
一个好的 docstring 应该回答:
坏例子:
好例子:
如果参数很少,直接靠函数签名即可。
@tool def exchange_rate(base_currency: str, quote_currency: str) -> str: """查询两种货币之间的汇率。""" return f"1 {base_currency} = 7.21 {quote_currency}"
复杂工具更推荐使用 Pydantic 定义 Schema,这样字段描述会更清晰。
from pydantic import BaseModel, Field from typing import Literal from langchain_core.tools import tool class WeatherInput(BaseModel): city: str = Field(description="城市名称,例如北京、上海") units: Literal["celsius", "fahrenheit"] = Field( default="celsius", description="温度单位" ) include_forecast: bool = Field( default=False, description="是否返回未来 3 天预报" ) @tool(args_schema=WeatherInput) def get_weather(city: str, units: str = "celsius", include_forecast: bool = False) -> str: """查询城市天气,可选返回未来天气预报。""" temp = 26 if units == "celsius" else 78 result = f"{city} 当前温度:{temp}°" if include_forecast: result += ";未来三天:多云、小雨、晴" return result
根据 LangChain 官方文档,工具返回值可以是:
适合模型直接阅读的自然语言结果。
@tool def get_inventory(sku: str) -> str: """查询商品库存。""" return f"SKU {sku} 当前库存 23 件,位于华东一号仓。"
适合场景:
如果你希望模型能显式读取字段,返回 `dict` 更合适。
@tool def get_inventory_data(sku: str) -> dict: """查询商品库存详情。""" return { "sku": sku, "available": 23, "reserved": 5, "warehouse": "east_01", "last_updated": "2026-04-03 10:30:00" }
适合场景:
在 LangChain / LangGraph 体系中,某些工具不仅返回数据,还要直接修改 Agent 状态。这时可以返回 `Command`。
from langchain_core.tools import ToolRuntime, tool from langchain_core.messages import ToolMessage from langgraph.types import Command @tool def set_language(language: str, runtime: ToolRuntime) -> Command: """设置用户偏好的回复语言。""" return Command( update={ "preferred_language": language, "messages": [ ToolMessage( content=f"已将语言偏好设置为 {language}", tool_call_id=runtime.tool_call_id, ) ], } )
这类工具适合:
LangChain 官方文档提到,可以通过 `runtime: ToolRuntime` 访问会话状态,这个参数不会暴露给模型,只会在运行时自动注入。
from langchain_core.tools import tool, ToolRuntime from langchain_core.messages import HumanMessage @tool def get_last_user_message(runtime: ToolRuntime) -> str: """读取最近一条用户消息。""" messages = runtime.state["messages"] for message in reversed(messages): if isinstance(message, HumanMessage): return message.content return "未找到用户消息"
@tool def get_user_preference(pref_name: str, runtime: ToolRuntime) -> str: """读取用户偏好设置。""" preferences = runtime.state.get("user_preferences", {}) return str(preferences.get(pref_name, "未设置"))
这种方式的好处是:
from langchain_openai import ChatOpenAI model = ChatOpenAI(model="gpt-4o-mini", temperature=0) model_with_tools = model.bind_tools([get_weather, get_inventory]) response = model_with_tools.invoke("查询 SKU A-100 库存,并顺便看看上海天气") print(response.tool_calls)
绑定之后,模型就具备了“选择工具”的能力。但你仍需要决定:
response = model_with_tools.invoke("北京天气怎么样?") for tool_call in response.tool_calls: print("tool:", tool_call["name"]) print("args:", tool_call["args"])
这个阶段很适合调试:
大多数真实项目不会手写每一轮工具循环,而是直接交给 `create_agent()`。
from langchain.agents import create_agent agent = create_agent( model="openai:gpt-4o-mini", tools=[get_weather, get_inventory], system_prompt="你是企业运营助手,涉及事实问题时优先调用工具核实。" ) result = agent.invoke({ "messages": [{"role": "user", "content": "帮我查一下上海天气,并看看 SKU A-100 库存是否够发货。"}] }) print(result)
此时 Agent 会:
工具连接的是外部世界,所以错误不是异常情况,而是常态:
@tool def safe_order_query(order_id: str) -> str: """查询订单状态。""" try: if not order_id.startswith("ORD"): return "订单号格式错误,请提供以 ORD 开头的订单编号。" return f"订单 {order_id} 当前状态:已出库,预计明日送达。" except TimeoutError: return "订单服务超时,请稍后重试。" except Exception as exc: return f"订单查询失败:{exc}"
LangChain 官方自定义中间件文档提供了 `@wrap_tool_call` 的模式,适合做日志、监控、统一错误包装。
from collections.abc import Callable from langchain.agents.middleware import wrap_tool_call from langchain.messages import ToolMessage from langchain.tools.tool_node import ToolCallRequest from langgraph.types import Command @wrap_tool_call def monitor_tool( request: ToolCallRequest, handler: Callable[[ToolCallRequest], ToolMessage | Command], ) -> ToolMessage | Command: print(f"Executing tool: {request.tool_call['name']}") print(f"Arguments: {request.tool_call['args']}") try: result = handler(request) print("Tool completed successfully") return result except Exception as e: print(f"Tool failed: {e}") raise
这类中间件适合做:
坏工具:
好工具:
拆分的好处:
模型关心的不是你的内部接口路径,而是“我什么时候该调用这个工具”。
例如:
不要机械返回原始 JSON。更好的方式通常是:
不是所有工具都能直接暴露给 Agent。尤其是:
这类工具建议采取分层策略:
@tool def draft_email(to: str, subject: str, body: str) -> dict: """生成待确认的邮件草稿,不会真正发送。""" return { "status": "draft", "to": to, "subject": subject, "body": body }
真正发送邮件应由:
下面给出一个更接近真实业务的例子:
from langchain.agents import create_agent from langchain_core.tools import tool @tool def query_order(order_id: str) -> dict: """根据订单号查询订单状态和物流信息。""" return { "order_id": order_id, "status": "已出库", "logistics_status": "运输中", "eta": "2026-04-04" } @tool def query_inventory(sku: str) -> dict: """根据 SKU 查询当前可用库存。""" return { "sku": sku, "available": 23, "warehouse": "华东一号仓" } @tool def draft_delay_notice(order_id: str, reason: str) -> str: """生成延迟发货通知草稿,不会真正发送。""" return f"订单 {order_id} 延迟通知草稿:由于 {reason},预计发货时间将顺延 1 天。" agent = create_agent( model="openai:gpt-4o-mini", tools=[query_order, query_inventory, draft_delay_notice], system_prompt=""" 你是订单运营助手。 1. 涉及订单、库存等事实信息时,必须优先调用工具。 2. 不要编造库存数字或发货状态。 3. 涉及通知用户时,只生成草稿,不得声称已经发送。 """ ) result = agent.invoke({ "messages": [{ "role": "user", "content": "请帮我看订单 ORD20260403001 是否会延迟,如果延迟顺便生成一份通知草稿。" }] }) print(result)
1. 实现 3 个工具:天气查询、订单查询、库存查询,并为每个工具编写清晰的 docstring 与参数 Schema。
2. 设计一个“邮件发送”双阶段流程:Agent 只能生成草稿,用户确认后才能真正发送。
3. 将一个原始 JSON 接口返回值,重构为更适合模型理解的文本或结构化对象输出。
4. 使用 `@wrap_tool_call` 编写一个工具监控中间件,记录工具名、参数和执行时长。
5. 为你的业务设计一个不超过 8 个工具的 Agent 工具集,并说明每个工具的边界。