Tool 抽象:@tool 一贴,普通函数就有了模型能读懂的"说明书"
进入阶段4。RAG 给模型"喂材料",工具给模型"发扳手"——让它能查天气、算数、调 API。今天读 libs/core/langchain_core/tools/:①BaseTool 抽象:一个工具 = 名字 + 说明书 + 参数 schema + 执行体;②@tool 装饰器怎么把普通 Python 函数自动变成工具;③StructuredTool 怎么从函数签名和 docstring 里"抄"出 args_schema。今天只讲"工具长什么样、怎么造",明天 Day14 讲模型怎么"下单"用它。
痛点:模型知识有截止日期、双手够不着外界
invoke/run 执行入口和回调埋点。而 @tool 装饰器的全部魔法,就是利用 Python 的反射(inspect 签名 + 类型注解 + docstring),把你"本来就写了"的信息自动搬进说明书——不用你手写一遍 JSON Schema。BaseTool:一个工具的法定构成
基类在 libs/core/langchain_core/tools/base.py:427。先看它"是什么"和"带什么":
# libs/core/langchain_core/tools/base.py:427(字段裁剪汇总)
class BaseTool(RunnableSerializable[str | dict[str, Any] | ToolCall, Any]):
"""Interface LangChain tools must implement."""
name: str # :468 品名:模型用它点名调用(唯一)
description: str # :471 用途说明:模型靠它决定"何时用这件工具"
args_schema: ArgsSchema | None = Field( # :477 插口规格:参数的 Pydantic 模型
default=None, description="The tool schema.")
return_direct: bool = False # :489 结果直接返回用户,不再交回模型(Day15 会用到)
response_format: Literal["content", "content_and_artifact"] = "content" # :541
@abstractmethod
def _run(self, *args: Any, **kwargs: Any) -> Any: # :877 ★子类唯一必须实现的:真干活
...
泛型入参三选一类签名说输入可以是 str | dict | ToolCall:字符串(单参数工具的便捷写法)、字典(多参数)、或者模型返回的 ToolCall 对象——第三种是明天闭环的关键伏笔。name + description这两个字段是给模型看的,不是给人看的。模型在几件工具里挑哪件,全凭 description 的文字描述——所以它是"提示词的一部分",写得含糊模型就乱用。args_schema一个 Pydantic BaseModel 子类(或 JSON Schema dict)。三重用途:①序列化成 JSON Schema 发给模型;②模型传参回来时做校验;③tool.args 属性(base.py:602)暴露给你调试看。_run 抽象方法又见模板方法!和 Day12 的 Retriever 一样:公开入口 run(L05)管回调/校验/异常,_run 只管业务逻辑。继承 RunnableSerializable 则让每件工具都是标准 Runnable,能单独 invoke、能进链。BaseTool 读成一张"上架审核表":没有品名不能上架(name 必填)、没有用途说明不能上架(description 必填)、要么声明插口规格要么走默认、必须真的能干活(_run 必须实现)。凡是过了审的,货架(明天的 bind_tools)照单全收。@tool 装饰器:自动贴标机的传送带
手写 BaseTool 子类太啰嗦,99% 的场景用 @tool。实现在 libs/core/langchain_core/tools/convert.py:77(前面 :18/:32/:48/:63 是四个 @overload 类型重载,真身是第五个):
# libs/core/langchain_core/tools/convert.py:77
def tool(
name_or_callable=None, # 既支持 @tool 也支持 @tool("自定义名")
runnable=None, *args,
description=None, # 不给就用函数 docstring
return_direct=False,
args_schema=None, # 不给就从函数签名推断
infer_schema=True,
response_format="content",
parse_docstring=False, ...
) -> BaseTool | Callable[..., BaseTool]:
它的核心分拣逻辑(convert.py:297-319,裁剪)——判断你给的是同步函数、异步函数还是 Runnable,最后统一送进 StructuredTool.from_function:
# libs/core/langchain_core/tools/convert.py:297(裁剪)
elif inspect.iscoroutinefunction(dec_func):
coroutine = dec_func # async def → 装进 coroutine 槽
func = None
schema = args_schema
else:
coroutine = None
func = dec_func # 普通 def → 装进 func 槽
schema = args_schema
if infer_schema or args_schema is not None:
return StructuredTool.from_function( # ★绝大多数工具的最终归宿
func, coroutine,
name=tool_name, # 默认 = 函数名
description=tool_description, # 默认 = docstring
args_schema=schema,
infer_schema=infer_schema, ...)
四个 @overload只为类型提示服务:让 @tool、@tool("名字")、@tool(parse_docstring=True) 各种用法在 IDE 里都能推断出正确返回类型。运行时逻辑全在 :77 的真身里。iscoroutinefunction 分拣同步函数进 func 槽、异步进 coroutine 槽——StructuredTool 两个槽都有(L04),调 invoke 走 func、ainvoke 走 coroutine,一件工具同时支持同步异步。Runnable 也能变工具:280 附近还有一段:传入 Runnable 时自动包一层 invoke_wrapper,schema 直接用 runnable.input_schema——你 Day03 学的任何链都能一键上货架。description 的优先级docstring 写在 :118-121:显式 description 参数 > 函数 docstring > args_schema 的描述。所以给工具函数写好 docstring 不是代码洁癖,是在写提示词。StructuredTool:args_schema 是从函数签名"抄"来的
StructuredTool(libs/core/langchain_core/tools/structured.py:40)是 @tool 产物的真身——"函数 + 说明书"的合体:
# libs/core/langchain_core/tools/structured.py:40
class StructuredTool(BaseTool):
"""Tool that can operate on any number of inputs."""
description: str = ""
args_schema: Annotated[ArgsSchema, SkipValidation()] = Field(
..., description="The tool schema.") # 这里变成必填(基类里可为 None)
func: Callable[..., Any] | None = None # 同步执行体
coroutine: Callable[..., Awaitable[Any]] | None = None # 异步执行体
def _run(self, *args, config, run_manager=None, **kwargs): # :74
if self.func:
...
return self.func(*args, **kwargs) # ★_run 就是转调你的原函数
造它的工厂 from_function(structured.py:133)里,最关键的是"推断 schema"这几行(structured.py:203):
# libs/core/langchain_core/tools/structured.py:202
name = name or source_function.__name__ # 品名 = 函数名
if args_schema is None and infer_schema:
args_schema = create_schema_from_function( # ★读函数签名,现场生成 Pydantic 模型
name, source_function,
parse_docstring=parse_docstring, # True 时还会解析 Google 风格 docstring
error_on_invalid_docstring=error_on_invalid_docstring,
filter_args=_filter_schema_args(source_function))
...
if description is None and not parse_docstring:
description_ = source_function.__doc__ or None # 说明书 = docstring
create_schema_from_function★贴标机的心脏(定义在 tools/base.py:265 附近的辅助区):用 inspect 读函数签名——参数名、类型注解、默认值——动态构造出一个 Pydantic 模型类。def get_weather(city: str, unit: str = "c") 就变成"必填 city:str,选填 unit:str 默认 c"。parse_docstring=True还能更进一步:解析 Google 风格 docstring 的 Args: 段,把每个参数的中文描述也塞进 schema——模型看到的参数说明更丰富,传参更准。func / coroutine 双槽_run(:74)转调 self.func,_arun(:101)转调 self.coroutine;只给了同步版时,ainvoke(:60)自动 run_in_executor 降级——和 Embeddings 的异步兜底同款套路,全框架一致。@tooldef get_weather(city: str, unit: str = "c") -> str: """查询指定城市当前天气。""" return f"{city} 26 度,晴"造出来的是
StructuredTool 实例:get_weather.name == "get_weather"、get_weather.description == "查询指定城市当前天气。"、get_weather.args == {"city": {"title": "City", "type": "string"}, "unit": {"default": "c", "title": "Unit", "type": "string"}}。直接执行:get_weather.invoke({"city": "上海"}) → "上海 26 度,晴"。注意:它已经不是函数了,想本地直调要用 .invoke() 或 .func()。run:执行一次工具,杂务全在壳里
执行入口 BaseTool.run(libs/core/langchain_core/tools/base.py:977)——又一次模板方法,骨架如下:
# libs/core/langchain_core/tools/base.py:977(裁剪骨架)
def run(self, tool_input, ..., tool_call_id=None, **kwargs) -> Any:
callback_manager = CallbackManager.configure(...) # ① 装回调
run_manager = callback_manager.on_tool_start( # ② 记"工具开始"(含入参)
{"name": self.name, "description": self.description},
tool_input_str, ..., tool_call_id=tool_call_id)
try:
tool_args, tool_kwargs = self._to_args_and_kwargs( # ③ ★解析+schema 校验入参
tool_input, tool_call_id)
response = context.run(self._run, *tool_args, **tool_kwargs) # ④ ★真干活
if self.response_format == "content_and_artifact": # ⑤ 双通道返回的拆包
content, artifact = response
...
# (异常处理 / ToolException 兜底 / on_tool_end 上报,略)
_to_args_and_kwargs③ 里干了两件事:_parse_input(base.py:752)用 args_schema 校验入参——模型传错类型、漏必填参数,在这一步被拦下来抛清晰错误,而不是让你的函数收到脏数据;再把 dict 拆成位置参数/关键字参数喂 _run。on_tool_start/end工具也有专属回调事件——LangSmith 里每次工具调用是独立节点:用什么参数调的、返回了什么、耗时多少。排查"Agent 为什么抽风"全靠它。content_and_artifact双通道返回:content 是给模型看的简短文本,artifact 是给程序用的原始大对象(图片、DataFrame)。避免把 10MB 的原始数据塞进对话历史。tool_call_id 参数注意 run 签名里有个 tool_call_id——它是明天的主角:模型下的"工单号"。今天先记住:run 会把它一路带着,最后用来生成带回执号的 ToolMessage。def f(city) 不写 : str,schema 推断只能猜;docstring 不写,description 就是空的——模型面对一件"三无产品",要么不用它,要么乱传参。@tool 的自动化质量 = 你函数签名和 docstring 的质量。另外工具名会被点名调用,别用 lambda(没有名字)也别重名。串起来 + 今日小结
| 角色 | 位置 | 一句话 |
|---|---|---|
BaseTool | tools/base.py:427 | 工具的法定构成:name/description/args_schema + 抽象 _run |
@tool | tools/convert.py:77 | 自动贴标机:分拣同步/异步/Runnable → 送去 from_function |
StructuredTool | tools/structured.py:40 | @tool 的产物:func/coroutine 双槽 + 必有 args_schema |
from_function | tools/structured.py:133 | 用 create_schema_from_function 从签名/docstring 抄出说明书 |
run | tools/base.py:977 | 执行壳:回调 + schema 校验 + 调 _run + 异常兜底 |
👶 小白:description 随便写写不行吗?反正真正干活的是代码。
👨🏫 老师:恰恰相反——description 是工具最重要的部分。代码只决定"调用之后对不对",description 决定"模型会不会调、什么时候调"。模型选工具的过程就是读一遍所有工具的 name+description+参数说明,然后凭语义判断。写"查天气"太干,写"查询指定城市的当前实时天气(温度/天气状况),city 传中文城市名",命中率天差地别。把 description 当成写给模型的提示词来打磨,是工具工程的第一课。
🧠 今天你应该能回答
- 一个 Tool 的法定构成?(name 品名 + description 用途说明 + args_schema 参数规格 + _run 执行体)
@tool装饰器从函数身上"抄"了哪三样?(函数名→name、docstring→description、签名注解→args_schema)- @tool 产出的具体类型是什么?(StructuredTool,func/coroutine 双槽)
- args_schema 的三重用途?(发给模型的 JSON Schema / 入参校验 / .args 调试查看)
- run 和 _run 的分工?(模板方法:run 管回调+校验+异常,_run 只管业务)
- 为什么 docstring 和类型注解必须认真写?(它们就是模型看到的说明书,决定工具会不会被正确使用)
✋ 10 分钟动手
cd /Users/bitmart/work/codes/github/AI_WORK/langchain
# 1. BaseTool 的字段们
sed -n '427,480p' libs/core/langchain_core/tools/base.py
sed -n '875,880p' libs/core/langchain_core/tools/base.py # 抽象 _run
# 2. @tool 真身与分拣逻辑
sed -n '77,95p' libs/core/langchain_core/tools/convert.py
sed -n '297,320p' libs/core/langchain_core/tools/convert.py
# 3. StructuredTool:schema 推断
sed -n '40,60p' libs/core/langchain_core/tools/structured.py
sed -n '200,215p' libs/core/langchain_core/tools/structured.py
bind_tools 把说明书发给模型 → 模型回一条带 tool_calls 的 AIMessage(开工单)→ 你执行工具 → 用 ToolMessage(带工单号回执)贴回对话 → 模型看着结果说人话。四种消息缺一环都闭不了环。