Day 11 / 共 20 天 · 第 3 周 工具/记忆/资源

工具体系深入

Day 03 建立了工具的基本概念,今天深入:复合工具、工具作者的 token 管理、ToolConfiguration 配置声明、@tool 装饰器把普通函数变工具。

📍 你在整门课的位置
第1周 核心概念 第2周 Agent执行 D11 工具体系深入 D12 工具市场 D13-15 记忆/资源/模型 第4周 平台/异步/生态
L01

回顾四要素

🤔 Day 03 已经学过工具了,为什么还要再花一天? 因为 Day 03 只讲了"工具长什么样"(四要素)。可真正读/写一个工具时,你会撞到一堆没讲过的细节:复合工具内部怎么组织?返回太长撑爆上下文怎么办?需要密钥的工具怎么声明?懒得写完整类怎么办?今天补齐这些"实战知识"。
💡 一句话本质 工具 = 给智能体的"外挂能力",而 BaseToolbase_tool.py:76)是所有能力的统一模具。模具规定了四要素(name/description/args_schema:79/execute:128),今天学的复合、token 管理、配置声明、@tool 糖,都是围绕这个模具的"用法进阶"——掌握模具的全部用法,你才能真正读懂平台里几十个内置工具。

BaseTool 就像生活中的"电动工具接口标准":工具箱里无论电钻、砂轮还是电锯,插头形状、开关位置、说明书格式全统一(对应 name/description/args_schema/execute)。智能体这个"工匠"拿到任何新工具都不用重新学怎么握、怎么开——今天全天沿用这个"五金工具箱"的类比世界观。

复习 Day 03:一个工具 = name(名字)+ description(说明书)+ args_schema(pydantic 参数)+ _execute(干活)execute()(模板方法)负责校验输入、调 _execute

今天看更复杂的真实工具、工具作者要注意的细节(token 管理)、以及配置声明和快捷定义方式。这些让你能真正读懂/写好一个工具。
L02

复合工具:DuckDuckGoSearch

DuckDuckGoSearchTooltools/duck_duck_go/duck_duck_go_search.py:28)是"搜索 + 抓取 + LLM 总结"的复合工具:

def _execute(self, query: str) -> tuple:
    search_results = self.get_raw_duckduckgo_results(query)  # 抓搜索结果
    webpages = self.get_content_from_url(links)              # 抓网页正文
    results = self.get_formatted_webpages(...)               # 组装
    summary = self.summarise_result(query, results)          # ★ 用 LLM 总结
    return summary + "\n\nLinks:\n" + ...
读法:一个"搜索"工具内部其实做了四件事:搜 → 抓网页 → 组装 → 用 LLM 总结。summarise_result:176)里 self.llm.chat_completion(...) 就是通过统一 LLM 接口(Day 10)总结——工具内部也能用 LLM(Day 03 讲过)。
复合工具 DuckDuckGoSearch 内部:一个 _execute 里串了 4 步 智能体调用 query="AI 新闻" ①搜索结果 ②抓网页正文 ③组装拼接 ④LLM 总结省 token 返回精炼一段 + 少量链接 对智能体而言只是"调了一个搜索工具";脏活(4 步 + 控 token)全被工具封装。好工具替智能体处理复杂度。
复合工具:对外一个简单接口(搜索),对内串起 4 个步骤并主动控 token。
📝 最小输入→输出 智能体发出动作 {"tool":"DuckDuckGoSearch","args":{"query":"2024 最省电的显卡"}}
工具内部:搜 10 条 → 抓前几个网页正文(几万字)→ 组装 → LLM 总结;
返回给智能体:"综合多个来源,最省电的是…(150 字)\n\nLinks: url1, url2"
关键:几万字的原始网页没有进智能体上下文,只有总结后的 150 字进了。
为什么搜索工具要"总结"? 原始搜索结果 + 多个网页正文可能几万字,直接塞回智能体上下文会撑爆、也没重点。工具内部先用 LLM 把搜索结果总结成精炼的一段,再返回给智能体——省 token、抓重点。这体现工具可以封装"复杂的、需要智能的操作",对智能体暴露一个简单接口("搜索并总结")。好工具替智能体处理脏活,只返回它需要的干净结果。
L03

工具作者管 token

上面 get_formatted_webpages:98)里用 TokenCounter.count_text_tokens(...) > 3000 控制喂给 LLM 的内容量。

⚡ 错误驱动:如果工具不管 token,会出什么事故? 设想搜索工具把抓到的 5 个网页原文(约 5 万 token)原样返回:① 智能体上下文(如 GPT-4 的 8k)当场撑爆,直接报 context_length_exceeded,本次运行失败;② 就算模型上下文够大,一次多花几毛钱、而且几万字里 99% 是导航栏/广告等噪声,LLM 反而抓不住重点。先总结/截断到 3000 token 以内再返回,这个机制就是防这两种事故的。
工具作者的责任 打个比方(还是工匠世界观):好工匠交活时只交装裱好的成品,不会把满地的刨花、锯末一起搬到客户家里。工具返回值也一样——只交"干净结果",别把中间产物全倒给智能体。
LLM 上下文有限(Day 10)。工具如果返回超长内容,会撑爆智能体的上下文。所以工具作者要主动管 token——比如"抓来的网页内容超 3000 token 就截断/总结"。BaseTool.max_token_limit(默认 600,Day 03)也是这个用途。写工具不只是"实现功能",还要考虑"输出别太大"——这是 Agent 工具开发和普通函数开发的重要区别。返回给 LLM 的东西,多一个 token 都是钱和上下文空间。
L04

ToolConfiguration:声明需要的密钥

需要密钥的工具箱(如 Google 搜索)在 get_env_keys() 里声明 ToolConfigurationtools/google_search/google_search_toolkit.py:16):

def get_env_keys(self):
    return [
        ToolConfiguration(key="GOOGLE_API_KEY", key_type=STRING, is_required=True, is_secret=True),
        ToolConfiguration(key="SEARCH_ENGINE_ID", ...)
    ]
读法:ToolConfigurationbase_tool.py:207)描述一个配置项:key 名、类型、是否必填、是否敏感(is_secret)工具箱声明"我需要哪些密钥",前端据此知道"该让用户填哪些字段、哪些要加密存"。
为什么工具箱要"声明"自己需要什么密钥? Google 搜索工具需要 API key,但平台不知道该问用户要什么——除非工具自己声明。get_env_keys() 让工具箱告诉平台"我需要 GOOGLE_API_KEY(必填、敏感)和 SEARCH_ENGINE_ID"。于是前端自动生成对应的输入框(敏感的打码存),运行时把用户填的值注入给工具。"工具声明所需配置,平台负责收集和注入"——这让工具即插即用。文件工具箱不需密钥就返回空。
生活类比:ToolConfiguration 就像电动工具包装盒上的"需自备电池:18V 锂电 ×1"标签——工具出厂时就声明清楚自己缺什么,商店(平台)照着标签提醒买家配齐(前端生成输入框),装上电池(注入密钥)才能开动。没有这张标签,买回家才发现转不起来。
L05

@tool 装饰器

先对照感受一下:"如果让你自己实现"一个加法工具(简化版完整类) vs 真实的 @tool 一行糖

# —— 简化版:老老实实写完整 Tool 类,你大概会写成这样 ——
class AddInput(BaseModel):
    a: int = Field(..., description="加数")
    b: int = Field(..., description="被加数")

class AddTool(BaseTool):
    name = "Add"
    description = "把两个数相加"
    args_schema = AddInput
    def _execute(self, a: int, b: int) -> int:
        return a + b
# 10 行,三份重复信息:参数在类型注解里写一遍、schema 里再写一遍

真实版:SuperAGI 提供 @tool 装饰器(base_tool.py:186),把一个普通函数直接包成工具——上面 10 行压缩成 4 行,重复信息由框架从函数签名/docstring 自动推导:

@tool
def my_func(a: int, b: int) -> int:
    """把两个数相加"""   # docstring 当 description
    return a + b
# 用函数签名自动生成 schema(create_function_schema, :47)
📝 @tool 自动推导了什么 上面这个被装饰的 my_func,框架自动得到:
• name = "My Func"(由函数名生成)
• description = "把两个数相加"(取自 docstring)
• args_schema = {a: int, b: int}(取自类型注解)
你一行 @tool 都没多写,它就已经是一个能被智能体识别、能进提示、能被调用的合法工具了。
⚠️ 小白常误以为 @tool 只是"打个标记",其实它做了实打实的三件事:读函数签名生成 pydantic schema(create_function_schema:47)、取 docstring 当 description、把函数包成 FunctionalTool(:156) 实例——产物和手写的完整 Tool 类同等地位,一样能进提示、被 LLM 选中、被 execute 调用。
@tool 的便利 写完整 Tool 类要定义 Input/Output/name/description——对简单函数太啰嗦。@tool 装饰器一行搞定:用函数的 docstring 当 description、用 函数签名(参数类型注解)自动生成 args_schema包成 FunctionalTool:156)。这和 eino 的 InferTool、AutoGPT 的 @tool 装饰器一模一样——给"轻量自定义函数工具"的快捷入口。五个框架都提供了这种"函数 → 工具"的糖。
L06

配置读取分层

工具运行时通过 get_tool_config(key)base_tool.py:152)读配置,委托给 toolkit_config。默认从 config.yaml 读(BaseToolkitConfiguration.get_tool_config:67),但真跑起来时被替换成数据库版 DBToolkitConfiguration(Day 12),从 DB 读并解密密钥。

为什么配置读取要"分层可替换"? 开发时配置放 config.yaml 文件方便。但生产/多租户时,不同组织的工具配置(密钥)不同、且要加密存数据库——不能都放一个文件。所以 toolkit_config 是可替换的:默认读文件,运行时替换成读数据库的版本。工具代码里只调 get_tool_config(key),不管配置从哪来——又是"面向接口"的解耦。
L07

permission_required

BaseTool 有个 permission_required: bool = Truebase_tool.py)——标记这个工具执行前是否需要人工确认。

又见 Human-in-the-loop 有些工具危险(发邮件、删文件、发推文),有些安全(ThinkingTool 只是思考,permission_required=False)。标了 permission_required=True 的工具,在受限模式下执行前会停下、等用户批准(Day 06 的 WAITING_FOR_PERMISSION)。这和 eino/OpenHands/CrewAI/AutoGPT 的 HITL 完全一致——能执行危险操作的智能体,关键动作必须能停下等人。五个框架第 N 次印证这条铁律。ThinkingTool 设 permission_required=False——纯思考无副作用,不用问。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 复合工具(搜索)内部做了哪几件事?为什么要总结?
  • 工具作者为什么要主动管 token?
  • ToolConfiguration 声明什么?为什么工具箱要声明所需密钥?
  • @tool 装饰器怎么把函数变工具?和别的框架像吗?
  • 配置读取为什么分层可替换?permission_required 干嘛?

✋ 动手

P=superagi/tools
sed -n '28,110p' duck_duck_go/duck_duck_go_search.py | head -50   # 复合工具
sed -n '16,20p' google_search/google_search_toolkit.py           # ToolConfiguration
sed -n '156,205p' base_tool.py                                    # @tool + FunctionalTool
明天预告 · Day 12tool_manager 与工具市场——工具怎么从 GitHub 下载、DB 登记、ToolBuilder 反射加载、依赖注入。"工具市场"的完整链路。
← Day 10 LLM Day 12 · 工具市场 →