工具体系深入
Day 03 建立了工具的基本概念,今天深入:复合工具、工具作者的 token 管理、ToolConfiguration 配置声明、@tool 装饰器把普通函数变工具。
回顾四要素
BaseTool(base_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。
复合工具:DuckDuckGoSearch
DuckDuckGoSearchTool(tools/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" + ...
summarise_result(:176)里 self.llm.chat_completion(...) 就是通过统一 LLM 接口(Day 10)总结——工具内部也能用 LLM(Day 03 讲过)。{"tool":"DuckDuckGoSearch","args":{"query":"2024 最省电的显卡"}};工具内部:搜 10 条 → 抓前几个网页正文(几万字)→ 组装 → LLM 总结;
返回给智能体:
"综合多个来源,最省电的是…(150 字)\n\nLinks: url1, url2"。关键:几万字的原始网页没有进智能体上下文,只有总结后的 150 字进了。
工具作者管 token
上面 get_formatted_webpages(:98)里用 TokenCounter.count_text_tokens(...) > 3000 控制喂给 LLM 的内容量。
context_length_exceeded,本次运行失败;② 就算模型上下文够大,一次多花几毛钱、而且几万字里 99% 是导航栏/广告等噪声,LLM 反而抓不住重点。先总结/截断到 3000 token 以内再返回,这个机制就是防这两种事故的。LLM 上下文有限(Day 10)。工具如果返回超长内容,会撑爆智能体的上下文。所以工具作者要主动管 token——比如"抓来的网页内容超 3000 token 就截断/总结"。
BaseTool.max_token_limit(默认 600,Day 03)也是这个用途。写工具不只是"实现功能",还要考虑"输出别太大"——这是 Agent 工具开发和普通函数开发的重要区别。返回给 LLM 的东西,多一个 token 都是钱和上下文空间。ToolConfiguration:声明需要的密钥
需要密钥的工具箱(如 Google 搜索)在 get_env_keys() 里声明 ToolConfiguration(tools/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", ...)
]
ToolConfiguration(base_tool.py:207)描述一个配置项:key 名、类型、是否必填、是否敏感(is_secret)。工具箱声明"我需要哪些密钥",前端据此知道"该让用户填哪些字段、哪些要加密存"。get_env_keys() 让工具箱告诉平台"我需要 GOOGLE_API_KEY(必填、敏感)和 SEARCH_ENGINE_ID"。于是前端自动生成对应的输入框(敏感的打码存),运行时把用户填的值注入给工具。"工具声明所需配置,平台负责收集和注入"——这让工具即插即用。文件工具箱不需密钥就返回空。生活类比:
ToolConfiguration 就像电动工具包装盒上的"需自备电池:18V 锂电 ×1"标签——工具出厂时就声明清楚自己缺什么,商店(平台)照着标签提醒买家配齐(前端生成输入框),装上电池(注入密钥)才能开动。没有这张标签,买回家才发现转不起来。@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)
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 装饰器一行搞定:用函数的 docstring 当 description、用 函数签名(参数类型注解)自动生成 args_schema。包成 FunctionalTool(:156)。这和 eino 的 InferTool、AutoGPT 的 @tool 装饰器一模一样——给"轻量自定义函数工具"的快捷入口。五个框架都提供了这种"函数 → 工具"的糖。配置读取分层
工具运行时通过 get_tool_config(key)(base_tool.py:152)读配置,委托给 toolkit_config。默认从 config.yaml 读(BaseToolkitConfiguration.get_tool_config,:67),但真跑起来时被替换成数据库版 DBToolkitConfiguration(Day 12),从 DB 读并解密密钥。
toolkit_config 是可替换的:默认读文件,运行时替换成读数据库的版本。工具代码里只调 get_tool_config(key),不管配置从哪来——又是"面向接口"的解耦。permission_required
BaseTool 有个 permission_required: bool = True(base_tool.py)——标记这个工具执行前是否需要人工确认。
permission_required=False)。标了 permission_required=True 的工具,在受限模式下执行前会停下、等用户批准(Day 06 的 WAITING_FOR_PERMISSION)。这和 eino/OpenHands/CrewAI/AutoGPT 的 HITL 完全一致——能执行危险操作的智能体,关键动作必须能停下等人。五个框架第 N 次印证这条铁律。ThinkingTool 设 permission_required=False——纯思考无副作用,不用问。今日小结 + 动手
🧠 今天你应该能回答
- 复合工具(搜索)内部做了哪几件事?为什么要总结?
- 工具作者为什么要主动管 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