5. 在 LangChain 中,如何为 Agent 注册工具?

5. 在 LangChain 中,如何为 Agent 注册工具?

👔面试官:在 LangChain 中,怎么给 Agent 注册工具?

🙋‍♂️我:给函数加一个 @tool,然后放进 Agent 就行。

@tool

👔面试官:已有的普通函数能不能直接传?复杂参数怎么校验?什么情况下需要 StructuredTool 或 BaseTool?

StructuredTool
BaseTool

🙋‍♂️我:那就全部继承 BaseTool,这样最规范。

BaseTool

👔面试官:为了查询一次天气也写一个类,只会增加样板代码。用户身份和权限又应该怎么传给工具?

🙋‍♂️我:让模型生成一个 user_id 参数。

user_id

👔面试官:可信身份不能由模型填写,否则既可能填错,也可能被提示注入利用。

这道题并不是让你背诵所有 Tool 类,而是考察你是否理解工具协议,能否根据输入复杂度和工程要求选择合适的实现方式。

💡 简要回答

💡 简要回答

LangChain 中注册 Tool 的本质,是同时向模型提供一份工具说明,并向运行时提供一个真正可执行的函数。工具说明主要包含名称、用途和参数 Schema,模型根据它选择工具并生成参数,LangChain 再执行对应函数。

最常用的实现方式有四种:

简单的已有函数,可以带上类型注解和 docstring 后直接放入 tools。

tools

大多数业务工具使用 @tool,便于自定义名称、描述和参数 Schema。

@tool

需要在运行时组装同步函数、异步函数和 Schema 时,可以使用 StructuredTool。

StructuredTool

工具需要封装客户端、维护资源或定制执行过程时,再继承 BaseTool。

BaseTool

在 LangChain v1 中,通常通过 create_agent(model=..., tools=[...]) 完成注册。城市、关键词、订单号等任务参数可以让模型填写;用户 ID、租户、权限和存储对象等可信参数,应通过 ToolRuntime 从运行时注入,不能暴露给模型。

create_agent(model=..., tools=[...])
ToolRuntime

生产环境还要关注参数校验、权限检查、超时、重试、幂等和错误分类。只有网络超时等临时故障适合自动重试,参数错误和业务拒绝应该返回清楚的信息,程序 Bug 则不应该被统一吞掉。

📝 详细解析

📝 详细解析

Tool 注册了什么?

Tool 注册了什么?

模型看不到 Python 函数的源码,那它凭什么知道该调用谁?注册工具时,LangChain 会先把函数转换成一份模型能够理解的说明。

模型首先看到 name 和 description,借此判断工具叫什么、应该在什么情况下使用。决定调用后,它再按照 args_schema 生成满足类型和约束的参数。真正拿着这些参数做事的,则是应用侧的函数或协程,也就是 executor。

name
description
args_schema

模型先生成包含工具名和参数的调用请求,运行时执行函数,再把结果作为 ToolMessage 返回给模型。模型拿到结果后,决定继续调用工具还是生成最终回答。

ToolMessage

所以,工具描述和 Schema 不是普通注释,而是模型与业务代码之间的调用合同。描述过于模糊,模型可能选错工具;参数缺少约束,模型可能生成无法执行的数据。

常用方式怎么选?

常用方式怎么选?

日常开发中,没有必要为了显得专业而直接继承最底层的类。选择方式时,可以先问这个工具到底还是不是一个普通函数。

如果已有函数的名称、类型注解和 docstring 已经能把用途说清楚,直接放入 tools 就够了。可一旦我们希望明确修改工具名称、补充参数描述,或者用 Pydantic 限制枚举和范围,普通函数提供的信息就不够,这时 @tool 会成为大多数业务工具的自然选择。

tools
@tool

再往上走,问题可能不是 Schema 不清楚,而是原函数不能修改,或者工具要在运行时动态组装同步与异步实现。StructuredTool 解决的正是这个组装问题。只有当工具需要长期持有客户端、维护资源并定制完整执行过程时,它才从「一个函数」变成「一个组件」,这时再考虑继承 BaseTool 才划算。

StructuredTool
BaseTool

所以,这四种方式不是互相竞争的功能清单,而是一条随复杂度上升的路径:先让函数说清楚,再补充工具契约,接着处理动态组装,最后才管理组件生命周期。

模型厂商提供的 Web Search、代码执行器等服务端工具,有时会使用厂商约定的字典配置。这类工具属于特定 Provider 能力,使用时应单独查看对应集成文档,不需要把它当作通用 Python Tool 的主要定义方式。

为什么优先使用 @tool?

为什么优先使用 @tool?

@tool

@tool 能从函数签名和 docstring 自动推导 Schema,也允许通过 Pydantic 显式描述复杂参数,是大多数业务工具的首选。

@tool
from typing import Literal

from langchain.agents import create_agent
from langchain.tools import tool
from pydantic import BaseModel, Field

class OrderQuery(BaseModel):
    # Field 描述和类型约束都会进入模型看到的工具 Schema
    order_id: str = Field(description="要查询的订单号")
    detail: Literal["summary", "full"] = Field(
        default="summary",
        description="返回摘要还是完整信息",
    )

# args_schema 显式指定工具参数的校验模型
@tool(args_schema=OrderQuery)
def query_order(order_id: str, detail: str = "summary") -> str:
    """查询订单状态。用户询问某个订单时调用。"""
    return f"订单 {order_id} 的状态为已发货,返回模式:{detail}"

# 注册时把 Tool 放进 create_agent 的 tools 列表
agent = create_agent(
    model="openai:gpt-5.4-mini",
    tools=[query_order],
)
from typing import Literal

from langchain.agents import create_agent
from langchain.tools import tool
from pydantic import BaseModel, Field

class OrderQuery(BaseModel):
    # Field 描述和类型约束都会进入模型看到的工具 Schema
    order_id: str = Field(description="要查询的订单号")
    detail: Literal["summary", "full"] = Field(
        default="summary",
        description="返回摘要还是完整信息",
    )

# args_schema 显式指定工具参数的校验模型
@tool(args_schema=OrderQuery)
def query_order(order_id: str, detail: str = "summary") -> str:
    """查询订单状态。用户询问某个订单时调用。"""
    return f"订单 {order_id} 的状态为已发货,返回模式:{detail}"

# 注册时把 Tool 放进 create_agent 的 tools 列表
agent = create_agent(
    model="openai:gpt-5.4-mini",
    tools=[query_order],
)

这个例子中,模型只需要决定 order_id 和 detail。Pydantic 的字段描述和枚举限制会进入工具 Schema,既帮助模型正确填写参数,也能在执行前拦截非法输入。

order_id
detail

如果只是已有的简单函数,也可以不加装饰器,直接传给 tools。不过函数必须有清楚的名称、类型注解和 docstring,否则自动生成的工具说明很难指导模型正确调用。

tools

何时使用高级定义?

何时使用高级定义?

StructuredTool.from_function 更适合「原函数不能修改,但需要改变它对模型的呈现方式」的场景。例如,同一个业务函数需要注册成不同名称,或者要把同步函数和异步协程组合成一个工具对象。

StructuredTool.from_function

什么时候才需要 BaseTool?当工具不再只是一个函数,而是要长期持有数据库或第三方客户端,并同时管理同步、异步、tags、metadata 和回调时,它已经变成了一个有生命周期的组件。此时使用 BaseTool,才值得承担更多样板代码。

BaseTool
BaseTool

选择原则可以概括为:函数能够说清楚就用普通函数,需要明确 Schema 就用 @tool,需要运行时组装再用 StructuredTool,出现组件生命周期后才考虑 BaseTool。

@tool
StructuredTool
BaseTool

可信参数如何注入?

可信参数如何注入?

假设「查询我的账户余额」工具需要用户 ID。如果把 user_id 放进模型可见的 Schema,模型可能填错用户,也可能被恶意提示诱导查询其他账户。

user_id

因此,需要区分两类参数:

参数类型示例参数来源任务参数城市、关键词、订单号模型根据用户问题生成可信参数用户 ID、租户、权限、当前状态应用运行时注入

参数类型示例参数来源

参数类型

示例

参数来源

任务参数城市、关键词、订单号模型根据用户问题生成

任务参数

城市、关键词、订单号

模型根据用户问题生成

可信参数用户 ID、租户、权限、当前状态应用运行时注入

可信参数

用户 ID、租户、权限、当前状态

应用运行时注入

LangChain v1 使用 ToolRuntime 把这些可信信息送进工具,但不同信息仍有不同作用域。用户身份、租户和依赖属于本次调用上下文,从 runtime.context 读取;当前会话消息和短期状态放在 runtime.state;跨会话仍要保留的长期数据,才进入 runtime.store。

ToolRuntime
runtime.context
runtime.state
runtime.store
from dataclasses import dataclass

from langchain.tools import ToolRuntime, tool

@dataclass
class UserContext:
    # 这些字段由应用运行时提供,不让模型生成
    user_id: str
    role: str

@tool
def get_balance(
    account_type: str,
    runtime: ToolRuntime[UserContext],
) -> str:
    """查询当前登录用户的账户余额。"""
    # 先使用可信 Context 做权限检查
    if runtime.context.role not in {"user", "finance_admin"}:
        return "当前用户无权查询余额"

    # 用户 ID 来自 Runtime,而不是模型参数
    user_id = runtime.context.user_id
    return f"用户 {user_id} 的 {account_type} 账户余额为 100 元"
from dataclasses import dataclass

from langchain.tools import ToolRuntime, tool

@dataclass
class UserContext:
    # 这些字段由应用运行时提供,不让模型生成
    user_id: str
    role: str

@tool
def get_balance(
    account_type: str,
    runtime: ToolRuntime[UserContext],
) -> str:
    """查询当前登录用户的账户余额。"""
    # 先使用可信 Context 做权限检查
    if runtime.context.role not in {"user", "finance_admin"}:
        return "当前用户无权查询余额"

    # 用户 ID 来自 Runtime,而不是模型参数
    user_id = runtime.context.user_id
    return f"用户 {user_id} 的 {account_type} 账户余额为 100 元"

模型能够看到并填写 account_type,却看不到 runtime。可信用户身份由应用传入,而不是由模型生成。这是工具权限控制的重要边界。

account_type
runtime

异步工具怎么处理?

异步工具怎么处理?

搜索、数据库和远程 API 通常属于 I/O 密集型操作。如果底层客户端支持异步,工具也应使用原生 async def,并通过 Agent 的 ainvoke 或异步流式接口调用。

async def
ainvoke

不要只把函数声明成 async def,内部却继续调用阻塞式 HTTP 客户端;这种写法不会自动提高并发能力。工具是否异步,应该和底层客户端以及整条 Agent 调用链保持一致。

async def

错误应该怎么处理?

错误应该怎么处理?

工具调用失败时,先别急着统一重试,因为不同失败意味着完全不同的下一步。

如果日期格式错误或缺少必填字段,问题出在模型生成的参数,应先让 Schema 拦截,再把可修正的信息交给模型重新填写。库存不足、没有权限或订单不存在则不是参数格式问题,而是一次正常的业务结果。工具应该把原因说清楚,让 Agent 决定换条路径或直接告知用户。

网络超时、限流和服务暂不可用才属于可以尝试恢复的临时故障。这类错误可以做有上限的重试,但必须同时设置退避和总超时,否则 Agent 只会在一个坏掉的服务前反复等待。

程序 Bug、数据损坏和权限配置错误不应该被统一转换成「调用失败」后继续执行,否则系统会掩盖真正的问题。对于付款、发邮件、创建订单等有副作用的工具,还必须设计幂等键和人工审批,避免重试造成重复执行。

注册后还要检查什么?

注册后还要检查什么?

一个能被 Agent 调用的函数,并不等于一个可以安全上线的工具。检查时可以沿着一次真实调用往下走,而不是背一张清单。

模型准备调用之前,先看名称和描述是否会与其他工具混淆,Schema 有没有限制枚举、范围和必填字段。这一步决定模型能不能选对工具、填对参数。

请求进入执行阶段后,再确认用户身份和权限来自可信 Runtime,而不是模型参数。远程调用还要有超时、重试上限和并发限制;只要动作会改变外部状态,就继续补上幂等、审批与审计,防止一次恢复或重试变成重复扣款、重复发信。

调用结束也不是安全边界的终点。日志与 Trace 需要帮助排查错误,但不能记录密钥、完整身份凭证或不必要的敏感数据。这样从模型选择、业务执行到事后追踪,工具契约才算真正闭环。

工具数量也不是越多越好。一次性向模型暴露大量相似工具,会增加选择错误和参数混淆的概率。更合理的做法是根据用户权限和当前任务动态缩小工具集合。

🎯 面试总结

🎯 面试总结

回答这道题时,不要把重点放在背诵所有 Tool 类名上。先说明 Tool 是「模型可见的调用合同 + 运行时可执行函数」,再讲清楚四种常用定义方式的选择边界。

实际项目中,普通函数和 @tool 能覆盖大多数需求;StructuredTool 用于运行时组装,BaseTool 用于复杂组件。可信身份和权限通过 ToolRuntime 注入,不能交给模型生成。

@tool
StructuredTool
BaseTool
ToolRuntime

最后补充 Schema 校验、异步 I/O、错误分类、幂等和权限治理,就能体现你不仅会注册工具,也理解工具如何安全、稳定地运行在生产环境中。

📚 参考资料

📚 参考资料

LangChain 官方文档:Tools

LangChain 官方文档:Tools

LangChain 官方文档:Agents

LangChain 官方文档:Agents

LangChain API:StructuredTool

LangChain API:StructuredTool

LangChain API:BaseTool

LangChain API:BaseTool

LangChain 官方文档:Middleware

LangChain 官方文档:Middleware

对了,AI Agent的面试题会在「公众号@小宇宙面试笔记题」持续更新,林友们赶紧关注起来,别错过最新干货哦!