使用 LangChain 构建 Agent 时,模型负责理解任务、判断下一步,但 Agent 执行过程中的很多操作,最终都需要程序真正完成。
例如,Coding Agent 要能够“读取并解释项目中的代码文件”,就需要读取文件的能力;Agent 要想“统计数据库中某月的销售额”,也必须真正查询数据库;要获取今天的天气,则需要访问实时 API。
这些模型之外的能力,通常通过**工具(Tool)**提供。本文主要介绍 Tool,包含它的定义、使用及其原理。
一、Tool 的基本定位
我们可以把 Tool 定义为一种具有明确输入和输出、可以被模型调用的程序能力。它可以查询数据库、访问 API、执行代码,也可以修改外部系统。
可以把一个 Agent 简化成两部分:
Model
负责理解任务、推理和决定下一步
↓
Tool
负责真正执行外部操作
例如用户提出:
查询北京今天的天气,并判断是否适合跑步
模型本身并没有实时天气数据。
如果 Agent 注册了一个 get_weather 工具,模型可以判断需要查询天气,并生成类似下面的调用请求:
get_weather(city="北京")
程序执行这个工具,获得天气结果,再把结果交给模型。
因此,Tool 并不是模型能力的一部分,而是提供给模型使用的程序接口。
它解决的是一个非常实际的问题:
模型负责决定“做什么”,程序负责真正“去做”。
这也是普通 LLM 调用与很多 Agent 应用之间的重要区别。
二、Tool 的调用机制
理解 Tool 时,一个常见误解是:
模型调用了 Python 函数。
严格来说并不是这样。
模型真正生成的是一个工具调用请求(Tool Call),其中包含工具名称、参数以及本次调用的 ID。真正执行 Python 函数的是 LangChain Agent Runtime 或开发者自己的程序。
整个过程可以表示为:
用户输入
↓
模型读取可用 Tool 的定义
↓
模型决定是否调用 Tool
↓
生成 Tool Call
↓
LangChain 执行真实函数
↓
生成 ToolMessage
↓
结果重新交给模型
↓
继续调用 Tool 或生成最终答案
在 LangChain 中使用 create_agent() 后,这套 Model → Tool → Model 的执行循环会由 Agent 自动处理。
这也是 Agent 的重要价值之一:开发者只需要定义模型和工具,不必手动维护整个工具调用循环。模型会根据工具调用结果分析是否继续调用其他工具还是结束任务。
三、LangChain 中定义 Tool
LangChain 当前最直接的 Tool 定义方式,是使用 @tool 装饰器。
先看一个简单例子:
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""查询指定城市当前的天气。"""
return f"{city}今天晴,25℃"
此时,普通 Python 函数被转换成了一个 LangChain Tool。
可以查看它生成的定义:
print(get_weather.name)
print(get_weather.description)
print(get_weather.args)
运行结果
get_weather
查询指定城市当前的天气。
{'city': {'title': 'City', 'type': 'string'}}
这是确定性结果的简化展示,具体 Schema 的打印格式可能随底层版本有所差异。
这里有三个很重要的信息:
| 信息 | 来源 | 作用 |
|---|---|---|
| Name | 函数名 | 告诉模型工具叫什么 |
| Description | Docstring | 告诉模型什么时候应该使用 |
| Args Schema | 类型标注 | 告诉模型需要生成哪些参数 |
LangChain 官方文档明确要求为工具参数提供类型标注,因为这些类型信息会参与工具输入 Schema 的生成;Docstring 则应该简洁、明确地描述工具用途。
这也解释了一个经常被忽略的问题:
模型并不会阅读你的 Python 函数实现来判断工具用途。
模型主要看到的是 Tool 暴露给它的名称、描述以及参数结构。Tool 能不能被模型正确使用,很大程度上取决于这些信息是否设计清楚。
四、模型如何选择 Tool
假设 Agent 中注册两个工具:
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""查询指定城市当前的天气。"""
return f"{city}今天晴,25℃"
@tool
def get_stock_price(symbol: str) -> str:
"""查询指定股票代码的当前价格。"""
return f"{symbol} 当前价格为 100"
然后创建 Agent:
from langchain.agents import create_agent
agent = create_agent(
model="openai:gpt-5.5",
tools=[get_weather, get_stock_price],
)
用户输入:
北京今天适合跑步吗?
模型会结合用户输入以及 Tool Schema,判断 get_weather 更适合当前任务,并生成相应参数。
模型生成内容具有随机性,下面展示一次典型的调用结构:
Tool: get_weather
Args: {"city": "北京"}
这里的 "北京" 并不是 LangChain 从用户输入中通过字符串规则提取出来的,而是模型根据 Tool 参数 Schema 和当前上下文生成的结构化参数。Tool Calling 本身就是模型能力的一部分。
这意味着 Tool 的描述应该具有足够的区分度。
如果定义两个工具:
search_data
搜索数据
find_data
查找数据
模型很难判断什么时候应该使用哪一个。
相比之下:
search_product_catalog
根据关键词搜索商品目录
get_product_inventory
根据商品 ID 查询实时库存
两者的职责边界就清楚得多。
五、Tool 的返回结果
Tool 执行完成以后,结果通常不会直接成为最终回答,而是转换为 ToolMessage,再交给模型继续处理。
最常见的是返回字符串:
@tool
def get_weather(city: str) -> str:
"""查询指定城市的天气。"""
return f"{city}今天晴,25℃"
模型拿到:
北京今天晴,25℃
之后才可能继续回答:
北京今天晴,25℃,从天气条件来看比较适合户外跑步。
如果数据本身存在明确字段,也可以返回 dict:
@tool
def get_weather(city: str) -> dict:
"""查询指定城市的天气数据。"""
return {
"city": city,
"temperature": 25,
"condition": "sunny",
}
一个有用的建议是,当后续推理需要明确读取不同字段时,可以返回结构化对象,而不是把所有内容拼成一段文本。
如果 Tool 不只是返回信息,还需要修改 Agent State,则可以返回 LangGraph 的 Command。这属于更进一步的用法,例如更新当前用户名称、语言偏好或者其他工作流状态。
因此可以简单区分:
返回 string / dict
↓
主要给模型提供信息
返回 Command
↓
修改 Agent 执行状态
六、ToolRuntime 的作用
实际项目中的 Tool 往往不仅需要模型传入的参数。
例如:
@tool
def get_order(order_id: str):
...
除了 order_id,查询订单时通常还需要知道:
当前用户是谁
当前会话是什么
用户拥有什么权限
Agent 当前 State 是什么
长期 Store 中保存了什么
这些信息不应该全部交给模型生成。
LangChain 当前提供 ToolRuntime 统一访问这类运行时数据,包括 State、Context、Store、Stream Writer、执行信息以及 Tool Call ID 等。runtime 参数由运行时自动注入,并不会暴露给模型。
例如:
from langchain.tools import tool, ToolRuntime
@tool
def get_last_user_message(runtime: ToolRuntime) -> str:
"""获取当前会话最近一条用户消息。"""
messages = runtime.state["messages"]
return str(messages[-1].content)
这里模型看到的 Tool Schema 中并不存在 runtime 参数。
也就是说,可以把 Tool 参数分成两类:
模型决定的参数
city
order_id
keyword
limit
运行时注入的数据
State
Context
Store
Tool Call ID
执行信息
这个边界非常重要。
例如查询“我的订单”时,模型可以决定订单编号,但 user_id 更适合从可信的 Runtime Context 中取得,而不是让模型根据对话内容自行填写。
七、Tool 的设计边界
Tool 写得能运行,只解决了第一步。实际项目中更需要关注它是否容易被模型正确、安全地使用。
Tool 的职责要足够明确
与其设计:
manage_database
然后通过大量参数决定查询、更新、删除,不如根据实际权限拆成:
search_orders
get_order_detail
update_order_address
cancel_order
这样模型更容易根据名称和描述选择正确操作,也更容易分别控制权限。
不要暴露模型不需要决定的参数
假设查询订单需要:
get_order(order_id, user_id, database_url, api_token)
真正需要模型决定的通常只有:
order_id
用户身份可以来自 Context,数据库连接和密钥则应该由程序管理。
Tool Schema 越干净,模型需要完成的参数判断越少,也越不容易出现错误。
写操作需要更严格的控制
查询天气和删除数据库记录虽然都可以定义成 Tool,但风险完全不同。
对于删除数据、发送邮件、支付、发布内容等有外部副作用的操作,不应该因为模型“选择了这个工具”就默认允许执行。实际系统通常还需要身份验证、权限判断、参数校验,必要时增加人工确认。
Tool 是模型与真实系统之间的接口,因此程序层面的安全检查不能交给 Prompt 替代。
Tool 不宜无限增加
更多 Tool 并不一定意味着更强的 Agent。
官方文档在动态工具选择部分也指出,暴露过多工具会增加上下文负担,并可能提高模型选择错误的概率,因此可以根据用户权限、会话阶段和运行时状态动态过滤 Tool。
实际设计时,更合理的原则通常是:
当前任务需要哪些能力,就向模型暴露哪些能力。
而不是把整个系统的所有 API 一次性都注册进去。
八、总结
Tool 是 Agent 从“生成文本”走向“执行任务”的重要接口。
模型并不会真正执行 Python 函数。它看到的是 Tool 的名称、描述和参数 Schema,根据当前上下文生成 Tool Call;随后由 LangChain Runtime 执行真正的程序,再把结果通过 ToolMessage 返回给模型,形成 Model 与 Tool 之间的执行循环。
实际开发中,Tool 的设计重点也不只是写一个 @tool。名称和描述决定模型如何选择工具,Schema 决定模型如何生成参数,ToolRuntime 则负责提供不应该由模型决定的 State、Context 和 Store 等运行时数据。
社区讨论
参与讨论
有问题或想法?欢迎继续讨论。