工具抽象:一个函数的两副面孔
工具(tool)是 agent 的「手脚」——模型说「我要搜」,真正上网的是工具。本章讲 smolagents 怎么把「一个 Python 函数」包装成 agent 能理解、能校验、能跨来源共享的东西,以及它如何同时服务两种 agent。
1. 它要解决的小问题
agent 要用工具,得先「认识」工具:名字叫什么、干什么、要什么参数、返回什么类型。这套元数据既要塞进提示词给模型看,又要在调用时校验参数对不对。手工维护「文档字符串 + 校验代码 + 提示词」三处一致很痛。smolagents 用一个 Tool 类把它们收拢到一处。
2. 思路/直觉:声明式元数据 + 一个 forward
一个工具 = 四个类属性(名/描述/入参/出参)+ 一个 forward 方法干实事:
# 示意,非源码
class WeatherTool(Tool):
name = "get_weather"
description = "查某城市当前天气"
inputs = {"city": {"type": "string", "description": "城市名"}}
output_type = "string"
def forward(self, city: str) -> str:
return call_weather_api(city)
Tool 基类(tools.py:106)在子类定义时就校验这些属性齐不齐、类型对不对(__init_subclass__ → validate_after_init → validate_arguments,tools.py:140-227)。校验很严:入参类型必须在 AUTHORIZED_TYPES 白名单内,forward 的签名参数名必须和 inputs 的 key 完全对上,连「可空参数」两处是否都标了 nullable 都查。这样错误在定义时就暴露,而非跑到一半才崩。
__call__(tools.py:231)在首次调用时惰性 setup()(给「加载大模型才能用」的重工具留的口子),再转发到 forward。
3. 关键机制:同一个工具的两副面孔
两种 agent 需要不同的「工具说明书」,Tool 用两个方法各渲染一副:
| 面孔 | 给谁 | 长什么样 | 方法 |
|---|---|---|---|
| 代码签名 | CodeAgent | 一个带 docstring 的 Python 函数声明 def name(city: string) -> string: | to_code_prompt(tools.py:258) |
| JSON schema | ToolCallingAgent | OpenAI 风格的 function schema | to_tool_calling_prompt(tools.py:289)/ get_tool_json_schema(models.py:288) |
为什么分两副:CodeAgent 让模型写代码调工具,那模型需要看到的是「函数签名 + 文档」,和它平时读 Python 一样自然;ToolCallingAgent 走原生 function calling,模型 API 需要的是 JSON schema。同一份元数据,两种投影。
to_code_prompt 还有个体贴细节:若工具声明了 output_schema(结构化返回),它会在 docstring 里插一句「本工具直接返回 dict,用 result['field'] 取字段,别再 print」——专门提示小模型别多此一举(tools.py:267-269)。
4. 关键机制:调用时的参数校验
工具被调用前,validate_tool_arguments(tools.py:1361)按 inputs 声明核对实参:类型对不对、必填项缺没缺。ToolCallingAgent 在 execute_tool_call(agents.py:1453)里调它,校验失败抛 AgentToolCallError——这条错误又会回喂给模型,让它下轮改正。
5. 工具从哪来:多来源生成
smolagents 让「写工具」尽量省事,提供多条生成路径:
| 来源 | 怎么来 | 入口 |
|---|---|---|
@tool 装饰器 | 给一个带类型注解 + docstring 的普通函数加装饰器,自动生成 Tool 子类 | tool(tools.py:1061) |
| HF Hub | 从 Hub 仓库拉别人分享的工具代码 | Tool.from_hub(tools.py:517) |
| HF Space | 把一个 Gradio Space 当工具调 | Tool.from_space(tools.py:600) |
| MCP server | 从 Model Context Protocol server 批量拉工具 | ToolCollection.from_mcp(tools.py:951) |
| LangChain | 包一个 LangChain 工具 | Tool.from_langchain(tools.py:763) |
@tool 装饰器最常用:它解析函数的类型注解和 docstring(靠 _function_type_hints_utils.py 把 Python 类型转 JSON schema),自动填好 inputs/output_type。
安全相关:从 Hub/代码字符串加载工具会执行外部代码,tool_validation.py 的 MethodChecker(:11)对工具的 forward 做静态 AST 检查——确保它只用已定义的名字、没有可疑的本地 import,给「加载他人工具」加一道审查。
6. 边界与坑
- 入参/出参类型受限:必须是
AUTHORIZED_TYPES白名单里的(string/int/boolean/image/audio/any 等),不能是任意 Python 类型。 forward签名必须和inputs严格对齐,否则定义即报错——好处是早失败,代价是改工具要两处同步。- MCP/Hub 工具引入外部依赖与信任问题:方便,但拉的是别人的代码,
MethodChecker只做轻量静态检查,不等于安全隔离。