Day 03 / 共 20 天 · 第 1 周 核心概念

工具 Tools

工具是智能体的"手"。今天读 tools/base_tool.py:一个工具 = 名字 + 描述 + pydantic 参数 + _execute,看它怎么定义、怎么被 LLM 调用。

📍 你在整门课的位置 · 第 1 周 核心概念(D1-5)· 昨天看了 Agent,今天看它的"手"——工具
D01 全景架构 D02 Agent D03 工具 D04 运行平台 D05 完整旅程· 第2周 Agent执行· 第3周 工具/记忆/资源· 第4周 平台/异步/生态
L01

工具四要素

🤔 LLM 只会"吐文本",它怎么可能真的去写文件、发邮件、上网搜索? 大模型的输出只是一串字符串,它没法自己执行 Python 函数、更碰不到你的磁盘和网络。那智能体"动手干活"到底是怎么发生的?
💡 一句话本质:工具是"文本决策"和"真实动作"之间的翻译桥 工具机制干的事就一句话:把每个能力写成"说明书(给 LLM 看)+ 参数校验器 + 一段真实执行代码"。LLM 只负责用文本"点名+填参数",框架照着说明书找到工具、校验参数、真正执行。所以工具四要素 = 说明书(name/description)+ 参数表(args_schema)+ 干活的(_execute)。
贯穿今天的类比是"五金店买螺丝刀"一个工具 = 一把螺丝刀name = 刀身刻的名字;description = 包装上的"用途说明"(客人靠它挑对工具);args_schema = 说明书上"配几号螺丝";_execute = 真拿它去拧;一组螺丝刀 = 一套 Toolkit;工具市场 = 五金店。

BaseTooltools/base_tool.py:76)——记住四个字段就懂了一个工具:

class BaseTool(BaseModel):
    name: str                       # ① 名字:LLM 靠它"点名"调用
    description: str                # ② 描述:告诉 LLM 这工具能干嘛、何时用
    args_schema: Type[BaseModel]    # ③ 参数表:pydantic 声明需要哪些参数
    permission_required: bool = True
    # ④ _execute(抽象方法):子类实现,真正干活
name工具名 description说明书 args_schema参数表 _execute干活
🛠️ 如果让你自己实现:让你给 LLM 加"写文件"能力,你大概会想——写个 if "写文件" in llm_output: open(...).write(...)。问题立刻来了:工具一多,这堆 if/elif 就爆炸;参数从文本里怎么可靠地抠出来?名字对不上怎么办?SuperAGI 的答案就是把每个工具规范成"四要素"对象,用统一的 name 匹配 + schema 校验取代满屏 if——下面就看它怎么做到的。
四要素直觉 name=工具叫什么("Write File");description=自然语言说明书(会拼进 LLM 提示,是 LLM 选工具的唯一依据,所以写好坏很关键);args_schema=用 pydantic 声明"要填哪些参数、什么类型";_execute=真正执行的代码。和 AutoGPT/eino/OpenHands 的工具三/四件套完全同构——天下工具一个样。

👶 小白:LLM 只会吐一段文本,它到底怎么就"调用"到了真正的 Python 函数、真把文件写到磁盘上?

👨‍🏫 老师:LLM 并不直接碰你的磁盘。它只吐一段 JSON:"我要用 Write File,参数是这些"。真正干活的是框架:按 name 在工具表里找到对应的工具对象 → 用 args_schema 校验参数合不合法 → 调它的 _execute 执行。"文本 → 找对象 → 校验 → 执行"这段翻译,就是工具机制存在的全部意义——LLM 负责动脑点名,框架负责动手拧螺丝。

L02

最简工具精读

WriteFileTooltools/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)
读法:WriteFileInputField(..., description=...) 声明两个必填参数,_execute 只有一行——把活儿委托给注入进来的 resource_manager注意 _execute 的形参名(file_name/content)和 schema 字段一一对应——框架靠这个把 LLM 填的参数对上号。
📝 一次工具调用:LLM 的"输入" → 真实"输出" 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 干的。
L03

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_inputargs_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 输出擦屁股。
"校验"这一步为什么重要? LLM 填的参数可能格式错、缺字段、类型不对。execute 在调 _execute 前先用 pydantic schema 校验——不合法直接报错,不会带着脏参数去执行。这道校验是"LLM 不可靠输出"和"确定性代码执行"之间的安全阀。和 AutoGPT 的 _execute 生命周期校验(Day 06)同理。
L04

参数 schema 的双重作用

args 属性(base_tool.py:86)把 args_schema 转成 JSON schema 的 properties——就是喂给 LLM 的参数清单。

pydantic schema 一举两得 同一份 args_schema:① 对上(面向 LLM)——转成 JSON schema,作为"这个工具要填什么参数"的说明喂给模型(模型据此填参);② 对下(面向执行)——校验模型填回的参数是否合法,合法才 _execute一份声明,既当"给 LLM 的参数文档",又当"执行前的校验器"——不重复、不会不一致。这是用 pydantic 做工具参数的精妙之处(AutoGPT/eino 的工具也这么干)。
max_token_limitbase_tool.py:99,默认 600)限制工具输出别撑爆上下文——工具作者要主动管 token(LLM 上下文有限且按 token 计费)。
L05

工具也能用 LLM

工具不只是"调外部 API",也可以"用 LLM 做一次子任务"。ThinkingTooltools/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"意味着什么? 智能体的主循环调 LLM 决定"用哪个工具",而某些工具内部调 LLM 干具体活(思考、总结)。于是形成"LLM 套 LLM"——主 LLM 做决策,工具 LLM 做子任务。这让工具能封装"需要智能的操作"(如总结长文),而不只是机械动作。llm 字段由框架注入(Day 12),工具不自己创建。
L06

Toolkit 工具箱

相关工具打包成 BaseToolkitbase_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 []
为什么要工具箱? 一个"能力域"往往由多个工具组成——文件读/写/删/列共 5 个工具,它们共享同一组配置/密钥。Toolkit 把"一组工具 + 一组配置声明"封装在一起,方便按工具箱粒度授权、安装(工具市场)、配置。需要密钥的工具箱(如 Google 搜索)在 get_env_keys() 里声明 ToolConfiguration(key 名、类型、是否必填、是否敏感)——前端据此知道"该让用户填哪些字段、哪些要加密存"。工具箱是工具市场(Day 12)的组织单位。
L07

为什么这样设计

LLM 只会输出文本,不能直接调 Python 函数。SuperAGI 的工具机制搭起了桥:

  1. 把每个工具的 name+description+args_schema 转成"工具说明书"喂给 LLM。
  2. LLM 输出一段 JSON(选哪个工具 + 参数)。
  3. 框架按 name 找到工具、用 pydantic 校验参数、调 _execute
  4. 结果作为"观察"存回 Feed(Day 02),喂回下一轮。
工具说明书name+desc+schema LLM输出 JSON 决策 pydantic 校验参数合法吗? _execute真实动作 观察→Feed喂回下一轮 喂给 点名+填参 合法才 观察结果拼回上下文,进入下一轮决策 ↺
"让 LLM 用工具"的通用范式:说明书喂给 LLM → LLM 吐 JSON 决策 → pydantic 校验参数 → _execute 干真活 → 观察写回 Feed → 喂回下一轮。这条链在五个框架里高度一致。
这就是"让 LLM 使用工具"的通用范式——你在 eino、OpenHands、CrewAI、AutoGPT 都见过。SuperAGI 用 BaseTool(模板方法)+ pydantic schema(双重作用)+ Toolkit(组织)+ 依赖注入(Day 12)实现。五个框架的工具机制惊人一致——因为"把 LLM 的文本决策转成结构化工具调用"就这一条正道。
💥 description 写砸了会出什么事故(错误驱动):假设你把发邮件工具的描述写成含糊的 "处理消息"。LLM 分不清它到底干嘛——该发邮件时它可能视而不见,或者拿它去干别的活,参数也乱填。因为 description 是 LLM 选工具的唯一依据(它看不到你的源码),描述模糊 = 给客人一把没贴标签的螺丝刀,只能瞎抓。所以"写好 description"不是文档洁癖,是直接决定工具能不能被正确调用。
🗣️ 一句话复述今天 工具就是"五金店里一把把贴好说明书的螺丝刀"——name 让 LLM 点名、description 让它选对、args_schema 既当参数文档又当校验器、_execute 才真拧螺丝;一套螺丝刀打包成 Toolkit,摆进工具市场。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 工具四要素是什么?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
明天预告 · Day 04:概念够了,跑起来!运行平台——docker compose 六服务、两个 entrypoint 脚本揭示的启动步骤、startup seeding。
← Day 02 Agent Day 04 · 运行平台 →