精读 LangChain 官方文档(九)Tools 篇:把函数变成 Agent 可治理的行动入口

精读 LangChain 官方文档(九)Tools 篇:把函数变成 Agent 可治理的行动入口
Agent 系统真正进入工程场景时,核心矛盾会从“模型能不能回答”转向“模型能不能安全、稳定、可观测地调用外部能力”。
模型本身只会生成 token。它不知道数据库在哪,也不会天然拥有订单查询、网页搜索、文件读取、代码执行、业务审批这些能力。只要系统需要访问实时数据、执行业务动作、读写状态或调用第三方 API,就必须在模型和外部世界之间建立一层明确的行动边界。
LangChain 的 Tools 文档解决的就是这个边界问题:怎样把一个普通函数变成模型可以理解、可以选择、可以传参、可以执行、可以追踪的工具。
这篇文档的核心主线可以概括成一句话:
Tool = typed function + model-facing contract + runtime access + execution control。
换成工程视角就是:工具不是“随手塞给模型的函数”,而是 Agent 对外行动的协议入口。它一边向模型暴露名称、描述和参数结构,另一边向程序侧连接状态、上下文、长期记忆、流式进度、错误处理和权限控制。
下面这张图先把 Tools 篇的主线压成一层结构:模型并不是直接碰数据库、API 或浏览器,而是先看到工具契约,再由运行时执行工具。

理解这条线后,@tool、args_schema、ToolRuntime、Command、wrap_tool_call、dynamic tools、headless tools 和 server-side tool use 就不再是零散 API,而是同一个工具工程体系里的不同层次。
1. Tools(工具)到底解决什么问题
它解决的问题:Tools 解决的是“模型如何以结构化方式调用外部能力”。
官方文档开头把工具定义得很直接:tools extend what agents can do。也就是说,工具把 Agent 从只会生成文本,扩展到可以获取实时数据、执行代码、查询外部数据库、调用业务系统,甚至触发真实动作。
这里真正要抓住的是一个边界:模型负责判断“是否需要工具、调用哪个工具、传什么参数”;程序负责定义“这个工具叫什么、参数有哪些、执行逻辑是什么、结果如何回到模型”。
示例:
import os
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_openai import ChatOpenAI
# 定义一个用于查询订单状态的工具函数
@tool
def get_order_status(order_id: str) -> str:
"""根据订单号查询订单状态。"""
return f"订单 {order_id} 当前状态:已支付,待发货。"
model = ChatOpenAI(
model="qwen3.7-max",
api_key=os.environ["QWEN_API_KEY"],
base_url=os.environ["QWEN_BASE_URL"],
)
agent = create_agent(
model=model,
tools=[get_order_status],
system_prompt="你是一个中文订单客服助手,需要在必要时调用工具查询订单状态。",
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "帮我查一下订单 A10086 到哪一步了"}]}
)
这里:
tool:LangChain 提供的装饰器,用来把 Python 函数转换成工具对象。get_order_status:工具函数名,默认会成为模型可见的工具名。order_id:工具参数名,模型调用工具时需要提供这个参数。str:类型标注,LangChain 用它生成工具输入 schema。docstring:函数文档字符串,会影响模型理解什么时候该用这个工具。tools=[get_order_status]:把工具注册给 Agent。
业务场景:
在客服系统里,用户问“订单什么时候发货”,模型不能自己编状态。正确做法是让模型识别到需要查订单,然后调用 get_order_status 这类工具,再基于工具返回值组织回复。
最简记法:
Tool 是 Agent 对外行动的门,不是模型的装饰品。
2. @tool:把函数变成模型可调用的契约
它解决的问题:@tool 解决的是“怎样把普通 Python 函数包装成模型能看懂的工具”。
LangChain 最基础的工具定义方式是给函数加 @tool。这一步会把函数名、参数类型、默认值和 docstring 组合成工具 schema。模型真正看到的不是 Python 源码,而是一个结构化描述:工具叫什么、做什么、需要哪些参数。
下面这张图对应 @tool 的转换关系:函数体仍然在程序侧执行,模型只看到工具契约。

这里有一个容易被忽略的点:类型标注是必需的。没有类型标注,模型就很难得到稳定的参数结构,工具调用也容易变成“自然语言猜参”。
示例:
from langchain.tools import tool
# 定义一个用于搜索客户资料的工具函数
@tool
def search_customer_profile(query: str, limit: int = 10) -> str:
"""搜索客户资料库,返回和查询词匹配的客户记录。
Args:
query: 要搜索的客户姓名、手机号或会员编号。
limit: 最多返回多少条结果。
"""
return f"根据 {query} 找到 {limit} 条客户资料。"
print(search_customer_profile.name)
print(search_customer_profile.description)
print(search_customer_profile.args)
这里:
query:搜索关键词,真实系统里可以是姓名、手机号、订单号、会员编号。limit:最大返回数量,默认值为10。search_customer_profile.name:工具名称。默认来自函数名。search_customer_profile.description:工具描述。默认来自 docstring。search_customer_profile.args:工具参数 schema。
业务场景:
企业知识库、CRM、工单系统都需要类似工具。模型只负责判断“用户在查客户资料”,真正的数据检索要交给工具函数。
最简记法:@tool 做的不是魔法,它只是把函数包装成模型可见的调用合同。
3. 工具名称、描述和参数:模型选择工具的三根线索
它解决的问题:
工具名称、描述和参数解决的是“模型凭什么知道该调用哪个工具,以及该怎么填参数”。
LangChain 文档提醒了一条很实用的规则:工具名建议使用 snake_case,例如 web_search,不要写成带空格或特殊符号的名字。原因很现实,不同模型供应商对工具名的兼容性不同,带空格或特殊字符的名字更容易被拒绝。
工具契约主要由三部分组成:
name:工具名,适合短、稳定、可被模型复用。description:工具说明,告诉模型什么时候该调用它。args_schema:参数结构,约束模型必须传什么字段。
示例:
from langchain.tools import tool
# 定义一个用于按关键词检索知识库的工具函数
@tool(
"knowledge_base_search",
description="根据中文问题检索企业知识库,适合查询制度、产品说明和内部流程。"
)
def search_docs(query: str, top_k: int = 5) -> str:
"""检索企业知识库。"""
return f"为问题 {query} 返回 {top_k} 条知识库片段。"
这里:
knowledge_base_search:自定义工具名,比search_docs更明确地说明工具用途。description:模型选择工具时的重要依据。它应该讲清楚适用场景,不要写成空泛介绍。top_k:检索返回数量,给模型一个可调节的检索范围。
业务场景:
一个客服 Agent 可能同时拥有 order_search、refund_policy_search、knowledge_base_search、ticket_create。描述写得越清楚,模型越容易在“查资料”和“创建工单”之间做出正确选择。
最简记法:
工具描述不是给人看的注释,而是给模型看的路标。
4. args_schema:让复杂输入可校验、可解释、可维护
它解决的问题:args_schema 解决的是“复杂工具参数如何变成稳定、可校验的结构”。
简单工具可以只靠类型标注,但业务工具经常有复杂输入。例如天气查询需要城市、温度单位、是否包含未来预报;订单查询可能需要订单号、用户 ID、查询范围、是否包含物流轨迹。此时应该用 Pydantic model 或 JSON Schema 明确定义参数。
下面这张图说明 args_schema 的位置:它夹在模型和工具函数之间,负责把“模型想传什么”约束成“程序能接收什么”。

示例:
from typing import Literal
from langchain.tools import tool
from pydantic import BaseModel, Field
class OrderQueryInput(BaseModel):
"""订单查询工具的输入结构。"""
order_id: str = Field(description="订单编号,例如 A10086。")
include_logistics: bool = Field(default=True, description="是否返回物流轨迹。")
detail_level: Literal["brief", "full"] = Field(
default="brief",
description="返回简要信息还是完整明细。",
)
# 定义一个按结构化参数查询订单的工具函数
@tool(args_schema=OrderQueryInput)
def query_order(order_id: str, include_logistics: bool = True, detail_level: str = "brief") -> str:
"""查询订单状态、支付状态和物流进度。"""
if include_logistics:
return f"订单 {order_id} 状态:待发货;返回级别:{detail_level};包含物流轨迹。"
return f"订单 {order_id} 状态:待发货;返回级别:{detail_level}。"
这里:
OrderQueryInput:工具输入模型,用于定义参数字段。Field(description=...):字段说明,帮助模型理解字段含义。Literal["brief", "full"]:枚举约束,避免模型传入任意字符串。include_logistics:布尔参数,决定是否返回物流信息。detail_level:业务返回粒度,防止每次都返回过量信息。
业务场景:
当工具最终会访问生产数据库、ERP、CRM 或支付系统时,参数越明确,系统越容易做校验、审计和权限控制。自然语言参数适合演示,结构化参数才适合生产。
最简记法:args_schema 是工具调用的参数边界。
5. 保留参数名:为什么不能随便把参数叫 config 或 runtime
它解决的问题:
保留参数名解决的是“工具参数和 LangChain 内部注入参数如何避免冲突”。
官方文档特别列出两个保留参数名:
config:LangChain 内部用于传递RunnableConfig。runtime:LangChain 内部用于注入ToolRuntime。
如果把普通业务参数命名为 config 或 runtime,运行时就可能产生冲突。真正需要读取运行时信息时,应该显式使用 ToolRuntime 类型参数,而不是让模型自己填一个 runtime 字段。
示例:
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.tools import ToolRuntime, tool
from langchain_openai import ChatOpenAI
import os
@dataclass
class CustomerContext:
customer_id: str
tenant_id: str
# 定义一个从运行时上下文读取客户身份的工具函数
@tool
def get_customer_level(runtime: ToolRuntime[CustomerContext]) -> str:
"""读取当前客户的会员等级。"""
customer_id = runtime.context.customer_id
tenant_id = runtime.context.tenant_id
return f"租户 {tenant_id} 下的客户 {customer_id} 是黄金会员。"
model = ChatOpenAI(
model="qwen3.7-max",
api_key=os.environ["QWEN_API_KEY"],
base_url=os.environ["QWEN_BASE_URL"],
)
agent = create_agent(
model=model,
tools=[get_customer_level],
context_schema=CustomerContext,
system_prompt="你是一个会员权益助手,需要根据当前客户上下文回答问题。",
)
这里:
ToolRuntime[CustomerContext]:表示工具可以读取类型为CustomerContext的运行时上下文。runtime.context.customer_id:程序侧注入的客户 ID,不需要模型填写。runtime.context.tenant_id:租户 ID,用于多租户隔离。context_schema:定义运行时上下文结构。
业务场景:
多租户 SaaS 里,客户身份和租户身份不应该由模型从自然语言里猜。它们应该来自登录态、网关或后端会话,再通过 ToolRuntime 注入工具。
最简记法:
模型填业务参数,Runtime 注入系统参数。
6. ToolRuntime:工具访问状态、上下文、记忆和流式进度的入口
它解决的问题:ToolRuntime 解决的是“工具执行时如何读取运行期资源”。
官方文档把 ToolRuntime 里的能力列得很完整:state、context、store、stream_writer、execution_info、server_info、config、tool_call_id。这些信息分别对应短期记忆、本次调用配置、长期记忆、进度流、执行身份、服务端身份、回调配置和工具调用唯一标识。
这张图把 ToolRuntime 拆成几个常用入口。工具函数不需要自己到处找依赖,只要通过 runtime 读取需要的资源。

示例:
from dataclasses import dataclass
from langchain.tools import ToolRuntime, tool
@dataclass
class UserContext:
user_id: str
# 定义一个读取当前会话摘要的工具函数
@tool
def summarize_current_session(runtime: ToolRuntime[UserContext]) -> str:
"""总结当前会话里的最新用户问题。"""
messages = runtime.state.get("messages", [])
user_id = runtime.context.user_id
latest_user_message = ""
for message in reversed(messages):
if getattr(message, "type", "") == "human":
latest_user_message = str(message.content)
break
return f"用户 {user_id} 最新问题:{latest_user_message}"
这里:
runtime.state:当前对话状态,通常包含messages。runtime.context:本次调用注入的不可变配置,例如用户 ID。runtime.store:长期记忆存储,可以跨会话保存数据。runtime.stream_writer:让工具输出自定义进度。runtime.tool_call_id:当前工具调用 ID,返回ToolMessage时常用。
业务场景:
在一个订单客服 Agent 中,工具可能需要同时知道当前对话、当前用户、租户、长期偏好、工具调用 ID 和进度输出通道。ToolRuntime 把这些资源收束到一个入口,避免工具散落读取全局变量。
最简记法:ToolRuntime 是工具执行时的运行期控制台。
7. State、Context、Store:三种数据不要混在一起
它解决的问题:state、context、store 解决的是“工具能读的数据到底应该放在哪里”。
Tools 文档里最容易和 Runtime 篇接上的部分,就是工具如何访问上下文。这里建议建立三分法:
state:当前会话里的短期状态,例如消息历史、临时计数、自定义状态字段。context:本次调用注入的静态配置,例如用户 ID、租户 ID、权限等级。store:跨会话持久化的数据,例如用户偏好、长期记忆、知识片段。
下面这张图专门用来区分三类数据。读图时注意:它们都能被工具读取,但生命周期和使用场景完全不同。

示例:
from langchain.messages import ToolMessage
from langchain.tools import ToolRuntime, tool
from langgraph.types import Command
# 定义一个更新当前会话状态的工具函数
@tool
def set_current_topic(topic: str, runtime: ToolRuntime) -> Command:
"""设置当前会话正在讨论的主题。"""
return Command(
update={
"current_topic": topic,
"messages": [
ToolMessage(
content=f"当前会话主题已设置为:{topic}",
tool_call_id=runtime.tool_call_id,
)
],
}
)
这里:
Command:LangGraph 提供的状态更新指令。current_topic:自定义短期状态字段,只属于当前对话过程。ToolMessage:工具返回给模型看的消息。tool_call_id:把工具结果和本次工具调用关联起来。
业务场景:
在售后系统里,state 可以记录当前工单阶段,context 可以记录当前客服账号和租户,store 可以保存客户长期偏好。三者混用会让系统变难调试,也会增加越权风险。
最简记法:state 管一轮会话,context 管本次调用,store 管长期记忆。
8. Tool execution:工具返回值要能被模型继续使用
它解决的问题:
Tool execution 解决的是“工具执行后,结果如何回到 Agent 循环”。
工具返回值不只是给开发者看的日志,它会成为模型后续推理的输入。简单工具可以返回字符串;需要更新状态时可以返回 Command;需要携带更细的工具消息时可以返回 ToolMessage。关键点是:返回内容要让模型能继续回答,而不是只满足程序侧调试。
示例:
from langchain.tools import tool
# 定义一个计算优惠金额的工具函数
@tool
def calculate_discount(total_price: float, coupon_amount: float) -> str:
"""计算订单优惠后的应付金额。"""
final_price = max(total_price - coupon_amount, 0)
return f"订单原价 {total_price:.2f} 元,优惠 {coupon_amount:.2f} 元,应付 {final_price:.2f} 元。"
这里:
total_price:订单原价,浮点数。coupon_amount:优惠金额,浮点数。final_price:工具内部计算结果。- 返回字符串:模型会看到这段文本,并据此组织最终回答。
业务场景:
如果工具返回的是“OK”或一段不完整 JSON,模型很难把结果解释给用户。生产工具最好返回紧凑、明确、面向下一步回答的信息。
最简记法:
工具返回值是给 Agent 循环继续使用的,不只是函数调用结果。
9. Error handling:工具失败也要进入可治理路径
它解决的问题:
Error handling 解决的是“工具调用异常时,Agent 如何恢复而不是直接崩掉”。
工具会失败。数据库超时、参数不合法、第三方 API 限流、权限不足、网络错误,都可能发生。LangChain 文档给出的关键方式是通过 middleware 包裹工具调用,例如 wrap_tool_call,把异常转换成模型可以理解的 ToolMessage。
下面这张图说明错误处理的位置:异常不应该直接暴露给用户,也不应该让 Agent 流程断掉,而是先经过中间件归一化。

示例:
import os
from collections.abc import Callable
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_tool_call
from langchain.messages import ToolMessage
from langchain.tools.tool_node import ToolCallRequest
from langchain_openai import ChatOpenAI
# 定义一个把工具异常转换为可读 ToolMessage 的中间件函数
@wrap_tool_call
def handle_tool_errors(
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage],
) -> ToolMessage:
"""把工具异常转换成模型可以继续处理的工具消息。"""
try:
return handler(request)
except Exception as exc:
return ToolMessage(
content=f"工具调用失败:请检查输入参数或稍后重试。错误信息:{exc}",
tool_call_id=request.tool_call["id"],
)
model = ChatOpenAI(
model="qwen3.7-max",
api_key=os.environ["QWEN_API_KEY"],
base_url=os.environ["QWEN_BASE_URL"],
)
agent = create_agent(
model=model,
tools=[],
middleware=[handle_tool_errors],
)
这里:
wrap_tool_call:工具调用中间件装饰器。ToolCallRequest:当前工具调用请求,包含工具名、参数、调用 ID 等信息。handler(request):继续执行原本的工具调用。ToolMessage:异常被转换后的工具消息。request.tool_call["id"]:本次工具调用 ID,用来关联返回消息。
业务场景:
在支付、发券、退款、库存查询这些场景里,工具失败不能只抛异常。用户需要知道下一步该怎么做,系统也需要保留可追踪的失败记录。
最简记法:
工具异常也要变成 Agent 能处理的消息。
10. Dynamic tool selection:工具不是越多越好
它解决的问题:
Dynamic tool selection 解决的是“不同用户、不同阶段、不同权限下,Agent 该暴露哪些工具”。
官方文档说得很实用:工具太多会让模型负担变重,也会增加调用错误。工具太少又限制 Agent 能力。动态工具选择就是在运行时根据状态、权限、功能开关或会话阶段过滤工具集合。
这张图对应三种常见过滤依据:state、store、runtime.context。它们都能影响模型本轮能看到哪些工具。

示例:
import os
from collections.abc import Callable
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.agents.middleware import ModelRequest, ModelResponse, wrap_model_call
from langchain_openai import ChatOpenAI
@dataclass
class PermissionContext:
user_role: str
# 定义一个根据用户角色过滤工具列表的中间件函数
@wrap_model_call
def filter_tools_by_role(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
"""根据运行时角色过滤本轮模型可见工具。"""
runtime = request.runtime
user_role = "viewer"
if runtime is not None and runtime.context is not None:
user_role = runtime.context.user_role
if user_role == "admin":
return handler(request)
tools = [tool for tool in request.tools if not tool.name.startswith("delete_")]
return handler(request.override(tools=tools))
model = ChatOpenAI(
model="qwen3.7-max",
api_key=os.environ["QWEN_API_KEY"],
base_url=os.environ["QWEN_BASE_URL"],
)
agent = create_agent(
model=model,
tools=[],
middleware=[filter_tools_by_role],
context_schema=PermissionContext,
)
这里:
wrap_model_call:模型调用前的中间件装饰器。ModelRequest:本轮模型调用请求,包含工具列表、状态、runtime 等信息。request.tools:当前暴露给模型的工具集合。request.override(tools=tools):用过滤后的工具集合替换原请求。PermissionContext.user_role:运行时注入的用户角色。
业务场景:
管理员可以看到删除、退款、导出工具;普通客服只能看到查询和创建工单工具;访客只能访问公开搜索工具。工具权限应该由系统控制,不应该全量交给模型自己判断。
最简记法:
动态工具选择是在运行时给模型发“可用工具菜单”。
11. Runtime tool registration:运行时发现的工具要同时注册和执行
它解决的问题:
Runtime tool registration 解决的是“工具不是启动时就全部确定时,Agent 怎样临时接入新工具”。
官方文档把动态工具分成两类:
- 已知工具的过滤:所有工具启动时已经注册,只是在运行时筛选。
- 运行时工具注册:工具来自 MCP server、数据库、远程 registry 或用户配置,需要在运行时加入。
第二类更复杂,因为只把工具加到模型请求里还不够。Agent 还需要知道这个临时工具真正怎么执行。所以通常要同时处理两个中间件位置:
wrap_model_call:把动态工具加到请求里。wrap_tool_call:真正执行或转发这个动态工具。
示例:
from langchain.agents.middleware import AgentMiddleware, ModelRequest, ToolCallRequest
from langchain.tools import tool
# 定义一个运行时可以被加入的计算小费工具函数
@tool
def calculate_tip(bill_amount: float, tip_percentage: float = 20.0) -> str:
"""计算账单小费和总金额。"""
tip = bill_amount * tip_percentage / 100
total = bill_amount + tip
return f"小费 {tip:.2f} 元,总金额 {total:.2f} 元。"
class DynamicToolMiddleware(AgentMiddleware):
"""把动态工具加入模型请求,并在工具调用阶段处理执行。"""
# 这个方法用于把动态工具加入本轮模型可见工具列表
def wrap_model_call(self, request: ModelRequest, handler):
updated_request = request.override(tools=[*request.tools, calculate_tip])
return handler(updated_request)
# 这个方法用于处理动态工具的真实执行
def wrap_tool_call(self, request: ToolCallRequest, handler):
if request.tool_call["name"] == "calculate_tip":
return handler(request.override(tool=calculate_tip))
return handler(request)
这里:
AgentMiddleware:Agent 中间件基类。wrap_model_call:模型调用前扩展工具列表。wrap_tool_call:工具调用时绑定真实执行逻辑。calculate_tip:运行时加入的工具。request.override(tool=...):把工具调用请求切换到指定工具实现。
业务场景:
企业 Agent 可能从 MCP server 读取用户所在部门可用的工具,也可能按租户启用不同插件。动态注册让工具生态更灵活,但必须同时管住“展示给模型”和“实际怎么执行”两件事。
最简记法:
运行时工具要双重接入:模型看得到,系统执行得了。
12. Headless tools:工具执行位置可以在客户端
它解决的问题:
Headless tools 解决的是“有些能力只能在用户设备或前端环境里执行”。
普通工具在服务端执行。Headless tools 则不同:服务端只注册工具 schema,让模型知道可以调用;真实实现放在客户端、浏览器、移动端或另一个环境里。模型发起工具调用后,图会暂停,前端拿到 interrupt payload,执行本地动作,再 resume。
这张图表达 Headless tools 的关键边界:模型看到 schema,服务端暂停,客户端执行,最终把结果带回 Agent。

适合 Headless tools 的场景包括:
- 浏览器 API:定位、剪贴板、IndexedDB、Canvas。
- 本地隐私数据:数据不出设备,只在客户端处理。
- 低延迟 UI 动作:用户界面内部的小操作。
- 可控副作用:通过小而明确的工具 schema 约束客户端动作。
示例:
from pydantic import BaseModel, Field
from langchain.tools import tool
class CanvasDrawInput(BaseModel):
"""前端画布绘制工具的输入结构。"""
x: int = Field(description="绘制位置的横坐标。")
y: int = Field(description="绘制位置的纵坐标。")
text: str = Field(description="需要写入画布的文字。")
draw_on_canvas = tool(
"draw_on_canvas",
description="在用户浏览器里的画布上写入一段文字。",
args_schema=CanvasDrawInput,
)
这里:
draw_on_canvas:schema-only 工具,Python 侧没有真实函数体。CanvasDrawInput:客户端执行动作所需的结构化参数。x、y:画布坐标。text:要写入的内容。
业务场景:
低代码设计器、浏览器自动化助手、可视化报表编辑器经常需要前端本地能力。Headless tools 可以把“模型决策”和“本地执行”分开,让数据和权限边界更清楚。
最简记法:
Headless tool 是服务端定义合同,客户端完成动作。
13. Prebuilt tools 与 server-side tools:不要把所有能力都自己写一遍
它解决的问题:
预置工具和服务端工具解决的是“常见能力应该复用,还是自己封装”。
LangChain 提供了不少 prebuilt tools 和 toolkits,用于搜索、代码解释、数据库访问等常见任务。另一类能力则来自模型供应商的 server-side tool use,例如由模型服务端执行的 web search 或 code interpreter。
两者的边界可以这样理解:
- 自定义工具:业务逻辑强、权限强、需要接企业系统。
- 预置工具:通用能力,LangChain 或生态已有稳定封装。
- Server-side tools:模型供应商直接执行,适合能力由供应商托管的场景。
- Headless tools:能力在客户端或用户设备上,不能放到服务端执行。
示例:
# 这里保留为工程示意:真实项目应按所选工具包的官方安装方式导入
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
import os
model = ChatOpenAI(
model="qwen3.7-max",
api_key=os.environ["QWEN_API_KEY"],
base_url=os.environ["QWEN_BASE_URL"],
)
agent = create_agent(
model=model,
tools=[], # 真实项目里可以放自定义工具、预置工具或按权限筛选后的工具。
system_prompt="你是一个中文业务助手,需要在工具结果可靠时再给出结论。",
)
这里:
tools=[]:示意位置,真实项目会放工具集合。- 自定义工具:适合订单、库存、合同、报销等企业私有动作。
- 预置工具:适合搜索、数据库、代码执行等通用能力。
- Server-side tools:适合模型供应商托管的内建能力。
业务场景:
如果要查企业订单,应该写自定义工具;如果要做通用网页搜索,可以考虑现成工具或供应商内建工具;如果要改浏览器上的本地 UI,Headless tools 更合适。
最简记法:
工具选型先看执行位置,再看数据边界。
14. 工程落地:设计工具时先问五个问题
它解决的问题:
工程落地解决的是“工具数量变多后,如何保持 Agent 可控”。
Tools 篇如果只读 API,很容易把重点放在“怎么写一个工具”。真正做业务系统时,更重要的问题是“这个工具是否应该被模型看到、参数是否安全、返回值是否可用、失败后是否可恢复、调用过程是否能追踪”。
可以用下面五个问题检查工具设计:
| 问题 | 对应设计点 |
|---|---|
| 模型为什么会选择这个工具? | name、description、适用场景 |
| 模型应该传哪些参数? | type hints、args_schema、字段说明 |
| 工具执行需要哪些系统信息? | ToolRuntime、context、state、store |
| 工具失败后怎么办? | wrap_tool_call、ToolMessage、重试与降级 |
| 谁有权限看到这个工具? | dynamic tool selection、权限过滤、feature flags |
业务场景:
一个真实企业 Agent 往往有几十个工具。没有这层治理,模型很快会在相似工具之间选错,或者在用户权限不足时看到不该看到的动作。
最简记法:
工具越接近真实动作,越需要 schema、权限、错误和观测治理。
总结:Tools 是 Agent 的行动协议层
LangChain 的 Tools 文档讲的不是“给模型加几个函数”这么轻的事情,而是在定义 Agent 和外部世界之间的行动协议。
@tool 负责把函数包装成模型可见的契约;args_schema 负责把参数结构化;ToolRuntime 负责让工具读取状态、上下文、长期记忆和执行信息;Command 与 ToolMessage 让工具结果回到 Agent 循环;wrap_tool_call 和动态工具选择让工具调用进入可治理路径;Headless tools 与 server-side tools 则进一步说明,工具的执行位置可以在服务端、客户端或模型供应商侧。
把这些层次合起来,Tools 篇真正建立的是一个心智模型:
工具是 Agent 的行动边界。模型负责选择和填参,系统负责执行、校验、授权、记录和恢复。
理解这一层后,再读 MCP、Human-in-the-loop 和 Guardrails 时,它们就不再是后续附加能力,而是围绕“工具如何安全连接外部世界”继续展开的工程治理层。
更多推荐

所有评论(0)