使用 LangChain 构建 Agent 时,模型负责理解任务、判断下一步,但 Agent 执行过程中的很多操作,最终都需要程序真正完成。

例如,Coding Agent 要能够“读取并解释项目中的代码文件”,就需要读取文件的能力;Agent 要想“统计数据库中某月的销售额”,也必须真正查询数据库;要获取今天的天气,则需要访问实时 API。

这些模型之外的能力,通常通过**工具(Tool)**提供。本文主要介绍 Tool,包含它的定义、使用及其原理。

一、Tool 的基本定位

我们可以把 Tool 定义为一种具有明确输入和输出、可以被模型调用的程序能力。它可以查询数据库、访问 API、执行代码,也可以修改外部系统。

可以把一个 Agent 简化成两部分:

代码片段Text
Model
负责理解任务、推理和决定下一步

Tool
负责真正执行外部操作

例如用户提出:

代码片段Text
查询北京今天的天气,并判断是否适合跑步

模型本身并没有实时天气数据。

如果 Agent 注册了一个 get_weather 工具,模型可以判断需要查询天气,并生成类似下面的调用请求:

代码片段Text
get_weather(city="北京")

程序执行这个工具,获得天气结果,再把结果交给模型。

因此,Tool 并不是模型能力的一部分,而是提供给模型使用的程序接口

它解决的是一个非常实际的问题:

模型负责决定“做什么”,程序负责真正“去做”。

这也是普通 LLM 调用与很多 Agent 应用之间的重要区别。

二、Tool 的调用机制

理解 Tool 时,一个常见误解是:

模型调用了 Python 函数。

严格来说并不是这样。

模型真正生成的是一个工具调用请求(Tool Call),其中包含工具名称、参数以及本次调用的 ID。真正执行 Python 函数的是 LangChain Agent Runtime 或开发者自己的程序。

整个过程可以表示为:

代码片段Text
用户输入

模型读取可用 Tool 的定义

模型决定是否调用 Tool

生成 Tool Call

LangChain 执行真实函数

生成 ToolMessage

结果重新交给模型

继续调用 Tool 或生成最终答案

在 LangChain 中使用 create_agent() 后,这套 Model → Tool → Model 的执行循环会由 Agent 自动处理。

这也是 Agent 的重要价值之一:开发者只需要定义模型和工具,不必手动维护整个工具调用循环。模型会根据工具调用结果分析是否继续调用其他工具还是结束任务。

三、LangChain 中定义 Tool

LangChain 当前最直接的 Tool 定义方式,是使用 @tool 装饰器。

先看一个简单例子:

代码片段Python
from langchain.tools import tool
 
 
@tool
def get_weather(city: str) -> str:
    """查询指定城市当前的天气。"""
    return f"{city}今天晴,25℃"

此时,普通 Python 函数被转换成了一个 LangChain Tool。

可以查看它生成的定义:

代码片段Python
print(get_weather.name)
print(get_weather.description)
print(get_weather.args)

运行结果

代码片段Text
get_weather
查询指定城市当前的天气。
{'city': {'title': 'City', 'type': 'string'}}

这是确定性结果的简化展示,具体 Schema 的打印格式可能随底层版本有所差异。

这里有三个很重要的信息:

信息来源作用
Name函数名告诉模型工具叫什么
DescriptionDocstring告诉模型什么时候应该使用
Args Schema类型标注告诉模型需要生成哪些参数

LangChain 官方文档明确要求为工具参数提供类型标注,因为这些类型信息会参与工具输入 Schema 的生成;Docstring 则应该简洁、明确地描述工具用途。

这也解释了一个经常被忽略的问题:

模型并不会阅读你的 Python 函数实现来判断工具用途。

模型主要看到的是 Tool 暴露给它的名称、描述以及参数结构。Tool 能不能被模型正确使用,很大程度上取决于这些信息是否设计清楚。

四、模型如何选择 Tool

假设 Agent 中注册两个工具:

代码片段Python
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:

代码片段Python
from langchain.agents import create_agent
 
agent = create_agent(
    model="openai:gpt-5.5",
    tools=[get_weather, get_stock_price],
)

用户输入:

代码片段Text
北京今天适合跑步吗?

模型会结合用户输入以及 Tool Schema,判断 get_weather 更适合当前任务,并生成相应参数。

模型生成内容具有随机性,下面展示一次典型的调用结构:

代码片段Text
Tool: get_weather
Args: {"city": "北京"}

这里的 "北京" 并不是 LangChain 从用户输入中通过字符串规则提取出来的,而是模型根据 Tool 参数 Schema 和当前上下文生成的结构化参数。Tool Calling 本身就是模型能力的一部分。

这意味着 Tool 的描述应该具有足够的区分度。

如果定义两个工具:

代码片段Text
search_data
搜索数据

find_data
查找数据

模型很难判断什么时候应该使用哪一个。

相比之下:

代码片段Text
search_product_catalog
根据关键词搜索商品目录

get_product_inventory
根据商品 ID 查询实时库存

两者的职责边界就清楚得多。

五、Tool 的返回结果

Tool 执行完成以后,结果通常不会直接成为最终回答,而是转换为 ToolMessage,再交给模型继续处理。

最常见的是返回字符串:

代码片段Python
@tool
def get_weather(city: str) -> str:
    """查询指定城市的天气。"""
    return f"{city}今天晴,25℃"

模型拿到:

代码片段Text
北京今天晴,25℃

之后才可能继续回答:

代码片段Text
北京今天晴,25℃,从天气条件来看比较适合户外跑步。

如果数据本身存在明确字段,也可以返回 dict

代码片段Python
@tool
def get_weather(city: str) -> dict:
    """查询指定城市的天气数据。"""
    return {
        "city": city,
        "temperature": 25,
        "condition": "sunny",
    }

一个有用的建议是,当后续推理需要明确读取不同字段时,可以返回结构化对象,而不是把所有内容拼成一段文本。

如果 Tool 不只是返回信息,还需要修改 Agent State,则可以返回 LangGraph 的 Command。这属于更进一步的用法,例如更新当前用户名称、语言偏好或者其他工作流状态。

因此可以简单区分:

代码片段Text
返回 string / dict

主要给模型提供信息

返回 Command

修改 Agent 执行状态

六、ToolRuntime 的作用

实际项目中的 Tool 往往不仅需要模型传入的参数。

例如:

代码片段Python
@tool
def get_order(order_id: str):
    ...

除了 order_id,查询订单时通常还需要知道:

代码片段Text
当前用户是谁
当前会话是什么
用户拥有什么权限
Agent 当前 State 是什么
长期 Store 中保存了什么

这些信息不应该全部交给模型生成。

LangChain 当前提供 ToolRuntime 统一访问这类运行时数据,包括 State、Context、Store、Stream Writer、执行信息以及 Tool Call ID 等。runtime 参数由运行时自动注入,并不会暴露给模型。

例如:

代码片段Python
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 参数分成两类:

代码片段Text
模型决定的参数
city
order_id
keyword
limit

运行时注入的数据
State
Context
Store
Tool Call ID
执行信息

这个边界非常重要。

例如查询“我的订单”时,模型可以决定订单编号,但 user_id 更适合从可信的 Runtime Context 中取得,而不是让模型根据对话内容自行填写。

七、Tool 的设计边界

Tool 写得能运行,只解决了第一步。实际项目中更需要关注它是否容易被模型正确、安全地使用。

Tool 的职责要足够明确

与其设计:

代码片段Text
manage_database

然后通过大量参数决定查询、更新、删除,不如根据实际权限拆成:

代码片段Text
search_orders
get_order_detail
update_order_address
cancel_order

这样模型更容易根据名称和描述选择正确操作,也更容易分别控制权限。

不要暴露模型不需要决定的参数

假设查询订单需要:

代码片段Python
get_order(order_id, user_id, database_url, api_token)

真正需要模型决定的通常只有:

代码片段Text
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 等运行时数据。