在这里插入图片描述

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

Agent 系统真正进入工程场景时,核心矛盾会从“模型能不能回答”转向“模型能不能安全、稳定、可观测地调用外部能力”。

模型本身只会生成 token。它不知道数据库在哪,也不会天然拥有订单查询、网页搜索、文件读取、代码执行、业务审批这些能力。只要系统需要访问实时数据、执行业务动作、读写状态或调用第三方 API,就必须在模型和外部世界之间建立一层明确的行动边界。

LangChain 的 Tools 文档解决的就是这个边界问题:怎样把一个普通函数变成模型可以理解、可以选择、可以传参、可以执行、可以追踪的工具。

这篇文档的核心主线可以概括成一句话:

Tool = typed function + model-facing contract + runtime access + execution control

换成工程视角就是:工具不是“随手塞给模型的函数”,而是 Agent 对外行动的协议入口。它一边向模型暴露名称、描述和参数结构,另一边向程序侧连接状态、上下文、长期记忆、流式进度、错误处理和权限控制。

下面这张图先把 Tools 篇的主线压成一层结构:模型并不是直接碰数据库、API 或浏览器,而是先看到工具契约,再由运行时执行工具。

工具工程主线图

理解这条线后,@toolargs_schemaToolRuntimeCommandwrap_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_searchrefund_policy_searchknowledge_base_searchticket_create。描述写得越清楚,模型越容易在“查资料”和“创建工单”之间做出正确选择。

最简记法:
工具描述不是给人看的注释,而是给模型看的路标。



4. args_schema:让复杂输入可校验、可解释、可维护

它解决的问题:
args_schema 解决的是“复杂工具参数如何变成稳定、可校验的结构”。

简单工具可以只靠类型标注,但业务工具经常有复杂输入。例如天气查询需要城市、温度单位、是否包含未来预报;订单查询可能需要订单号、用户 ID、查询范围、是否包含物流轨迹。此时应该用 Pydantic model 或 JSON Schema 明确定义参数。

下面这张图说明 args_schema 的位置:它夹在模型和工具函数之间,负责把“模型想传什么”约束成“程序能接收什么”。

参数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. 保留参数名:为什么不能随便把参数叫 configruntime

它解决的问题:
保留参数名解决的是“工具参数和 LangChain 内部注入参数如何避免冲突”。

官方文档特别列出两个保留参数名:

  • config:LangChain 内部用于传递 RunnableConfig
  • runtime:LangChain 内部用于注入 ToolRuntime

如果把普通业务参数命名为 configruntime,运行时就可能产生冲突。真正需要读取运行时信息时,应该显式使用 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 里的能力列得很完整:statecontextstorestream_writerexecution_infoserver_infoconfigtool_call_id。这些信息分别对应短期记忆、本次调用配置、长期记忆、进度流、执行身份、服务端身份、回调配置和工具调用唯一标识。

这张图把 ToolRuntime 拆成几个常用入口。工具函数不需要自己到处找依赖,只要通过 runtime 读取需要的资源。

ToolRuntime资源入口图

示例:

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:三种数据不要混在一起

它解决的问题:
statecontextstore 解决的是“工具能读的数据到底应该放在哪里”。

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 能力。动态工具选择就是在运行时根据状态、权限、功能开关或会话阶段过滤工具集合。

这张图对应三种常见过滤依据:statestoreruntime.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 怎样临时接入新工具”。

官方文档把动态工具分成两类:

  1. 已知工具的过滤:所有工具启动时已经注册,只是在运行时筛选。
  2. 运行时工具注册:工具来自 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工具边界图

适合 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:客户端执行动作所需的结构化参数。
  • xy:画布坐标。
  • 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,很容易把重点放在“怎么写一个工具”。真正做业务系统时,更重要的问题是“这个工具是否应该被模型看到、参数是否安全、返回值是否可用、失败后是否可恢复、调用过程是否能追踪”。

可以用下面五个问题检查工具设计:

问题 对应设计点
模型为什么会选择这个工具? namedescription、适用场景
模型应该传哪些参数? type hints、args_schema、字段说明
工具执行需要哪些系统信息? ToolRuntimecontextstatestore
工具失败后怎么办? wrap_tool_callToolMessage、重试与降级
谁有权限看到这个工具? dynamic tool selection、权限过滤、feature flags

业务场景:
一个真实企业 Agent 往往有几十个工具。没有这层治理,模型很快会在相似工具之间选错,或者在用户权限不足时看到不该看到的动作。

最简记法:
工具越接近真实动作,越需要 schema、权限、错误和观测治理。



总结:Tools 是 Agent 的行动协议层

LangChain 的 Tools 文档讲的不是“给模型加几个函数”这么轻的事情,而是在定义 Agent 和外部世界之间的行动协议。

@tool 负责把函数包装成模型可见的契约;args_schema 负责把参数结构化;ToolRuntime 负责让工具读取状态、上下文、长期记忆和执行信息;CommandToolMessage 让工具结果回到 Agent 循环;wrap_tool_call 和动态工具选择让工具调用进入可治理路径;Headless tools 与 server-side tools 则进一步说明,工具的执行位置可以在服务端、客户端或模型供应商侧。

把这些层次合起来,Tools 篇真正建立的是一个心智模型:

工具是 Agent 的行动边界。模型负责选择和填参,系统负责执行、校验、授权、记录和恢复。

理解这一层后,再读 MCP、Human-in-the-loop 和 Guardrails 时,它们就不再是后续附加能力,而是围绕“工具如何安全连接外部世界”继续展开的工程治理层。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐