工具 Tools
工具是智能体的"手"。今天读 tools/base_tool.py:一个工具 = 名字 + 描述 + pydantic 参数 + _execute,看它怎么定义、怎么被 LLM 调用。
工具四要素
贯穿今天的类比是"五金店买螺丝刀":一个工具 = 一把螺丝刀;
name = 刀身刻的名字;description = 包装上的"用途说明"(客人靠它挑对工具);args_schema = 说明书上"配几号螺丝";_execute = 真拿它去拧;一组螺丝刀 = 一套 Toolkit;工具市场 = 五金店。BaseTool(tools/base_tool.py:76)——记住四个字段就懂了一个工具:
class BaseTool(BaseModel):
name: str # ① 名字:LLM 靠它"点名"调用
description: str # ② 描述:告诉 LLM 这工具能干嘛、何时用
args_schema: Type[BaseModel] # ③ 参数表:pydantic 声明需要哪些参数
permission_required: bool = True
# ④ _execute(抽象方法):子类实现,真正干活
if "写文件" in llm_output: open(...).write(...)。问题立刻来了:工具一多,这堆 if/elif 就爆炸;参数从文本里怎么可靠地抠出来?名字对不上怎么办?SuperAGI 的答案就是把每个工具规范成"四要素"对象,用统一的 name 匹配 + schema 校验取代满屏 if——下面就看它怎么做到的。👶 小白:LLM 只会吐一段文本,它到底怎么就"调用"到了真正的 Python 函数、真把文件写到磁盘上?
👨🏫 老师:LLM 并不直接碰你的磁盘。它只吐一段 JSON:"我要用 Write File,参数是这些"。真正干活的是框架:按 name 在工具表里找到对应的工具对象 → 用 args_schema 校验参数合不合法 → 调它的 _execute 执行。"文本 → 找对象 → 校验 → 执行"这段翻译,就是工具机制存在的全部意义——LLM 负责动脑点名,框架负责动手拧螺丝。
最简工具精读
WriteFileTool(tools/file/write_file.py:13)——最干净的范例:
class WriteFileInput(BaseModel):
file_name: str = Field(..., description="要写的文件名") # ... 表示必填
content: str = Field(..., description="文件内容")
class WriteFileTool(BaseTool):
name: str = "Write File"
args_schema = WriteFileInput
description: str = "Writes text to a file"
resource_manager: Optional[FileManager] = None # 框架注入(Day 12)
def _execute(self, file_name: str, content: str):
return self.resource_manager.write_file(file_name, content)
WriteFileInput 用 Field(..., description=...) 声明两个必填参数,_execute 只有一行——把活儿委托给注入进来的 resource_manager。注意 _execute 的形参名(file_name/content)和 schema 字段一一对应——框架靠这个把 LLM 填的参数对上号。{"tool":"Write File","args":{"file_name":"竞品定价.csv","content":"竞品A,99元\n竞品B,129元"}}框架:按
"Write File" 找到 WriteFileTool → 用 WriteFileInput 校验参数(两个必填都在 ✓)→ 调 _execute(file_name, content)真实发生:磁盘上多了个
竞品定价.csv;返回观察 "Successfully wrote to 竞品定价.csv" 写回 Feed。LLM 只吐了一段 JSON,真实写文件是 _execute 干的。execute 模板方法
外界调 execute()(base_tool.py:128),它固定流程、把业务留给 _execute:
def execute(self, tool_input, **kwargs):
parsed_input = self._parse_input(tool_input) # 用 args_schema 校验
tool_args, tool_kwargs = self._to_args_and_kwargs(parsed_input)
observation = self._execute(*tool_args, **tool_kwargs) # 调子类实现
return observation
execute 固定"解析校验输入 → 拆参数 → 调 _execute"这套骨架,子类只写 _execute 业务钩子。_parse_input 用 args_schema.parse_obj 把 LLM 传来的 dict 校验成对象。def run(args): return do_work(args["file_name"], args["content"])(直接拿 dict 就用)真实版
execute 多出的每一步都在补一个坑:①
_parse_input(pydantic 校验)→ 补"LLM 参数可能缺字段/类型错"的坑;②
_to_args_and_kwargs(拆成位置/关键字参数)→ 补"schema 字段名要对上 _execute 形参名"的坑;③ 才调
_execute → 真正干活。"看起来多余"的每一步,都是在替不可靠的 LLM 输出擦屁股。execute 在调 _execute 前先用 pydantic schema 校验——不合法直接报错,不会带着脏参数去执行。这道校验是"LLM 不可靠输出"和"确定性代码执行"之间的安全阀。和 AutoGPT 的 _execute 生命周期校验(Day 06)同理。参数 schema 的双重作用
args 属性(base_tool.py:86)把 args_schema 转成 JSON schema 的 properties——就是喂给 LLM 的参数清单。
args_schema:① 对上(面向 LLM)——转成 JSON schema,作为"这个工具要填什么参数"的说明喂给模型(模型据此填参);② 对下(面向执行)——校验模型填回的参数是否合法,合法才 _execute。一份声明,既当"给 LLM 的参数文档",又当"执行前的校验器"——不重复、不会不一致。这是用 pydantic 做工具参数的精妙之处(AutoGPT/eino 的工具也这么干)。max_token_limit(base_tool.py:99,默认 600)限制工具输出别撑爆上下文——工具作者要主动管 token(LLM 上下文有限且按 token 计费)。工具也能用 LLM
工具不只是"调外部 API",也可以"用 LLM 做一次子任务"。ThinkingTool(tools/thinking/tools.py:22):
class ThinkingTool(BaseTool):
llm: Optional[BaseLlm] = None # 工具自己持有一个 LLM
name = "ThinkingTool"
def _execute(self, task_description: str):
prompt = ...填充 thinking.txt 模板...
result = self.llm.chat_completion([{"role":"system","content":prompt}], ...)
return result["content"]
llm 字段,在 _execute 里再调一次大模型。ThinkingTool 就是"让 LLM 专门做一次推理思考"的工具。搜索类工具(如 DuckDuckGoSearchTool)更复杂——搜索 + 抓网页 + 用 LLM 总结,是"外部调用 + LLM"的复合工具。llm 字段由框架注入(Day 12),工具不自己创建。Toolkit 工具箱
相关工具打包成 BaseToolkit(base_tool.py:232)。文件工具箱(tools/file/file_toolkit.py:13):
class FileToolkit(BaseToolkit):
name = "File Toolkit"
def get_tools(self): # 这个工具箱包含哪些工具
return [AppendFileTool(), DeleteFileTool(), ListFileTool(), ReadFileTool(), WriteFileTool()]
def get_env_keys(self): # 需要哪些配置/密钥(文件工具箱不需要 → 空)
return []
get_env_keys() 里声明 ToolConfiguration(key 名、类型、是否必填、是否敏感)——前端据此知道"该让用户填哪些字段、哪些要加密存"。工具箱是工具市场(Day 12)的组织单位。为什么这样设计
LLM 只会输出文本,不能直接调 Python 函数。SuperAGI 的工具机制搭起了桥:
- 把每个工具的
name+description+args_schema转成"工具说明书"喂给 LLM。 - LLM 输出一段 JSON(选哪个工具 + 参数)。
- 框架按 name 找到工具、用 pydantic 校验参数、调
_execute。 - 结果作为"观察"存回 Feed(Day 02),喂回下一轮。
BaseTool(模板方法)+ pydantic schema(双重作用)+ Toolkit(组织)+ 依赖注入(Day 12)实现。五个框架的工具机制惊人一致——因为"把 LLM 的文本决策转成结构化工具调用"就这一条正道。"处理消息"。LLM 分不清它到底干嘛——该发邮件时它可能视而不见,或者拿它去干别的活,参数也乱填。因为 description 是 LLM 选工具的唯一依据(它看不到你的源码),描述模糊 = 给客人一把没贴标签的螺丝刀,只能瞎抓。所以"写好 description"不是文档洁癖,是直接决定工具能不能被正确调用。今日小结 + 动手
🧠 今天你应该能回答
- 工具四要素是什么?description 为什么关键?
- execute 模板方法怎么工作?校验为什么重要?
- pydantic schema 的双重作用(对上/对下)?
- 工具怎么在内部用 LLM?
- Toolkit 为什么存在?
✋ 动手
P=superagi/tools
sed -n '76,153p' $P/base_tool.py | head -50 # BaseTool + execute
sed -n '13,50p' $P/file/write_file.py # 最简工具
sed -n '22,72p' $P/thinking/tools.py # 工具用 LLM
sed -n '13,25p' $P/file/file_toolkit.py # Toolkit