第 6 章 · 01 工具注册表模式 本节摘要:本节是「LLM 工具调用体系」的开篇,讲一个常被忽略却很关键的设计—— 。整个文件只有十几行,却用最朴素的方式回答了一个问题:「Agent 到底能用哪些函数?」。它的做法是写一个 函数,返回一个函数对象的列表(不是字符串、不是字典),把分散在各模块的工具统一汇聚到一个入口。这种「注册表」模式把「工具实现」和「工具消费方」解耦——Agent 不需要知道工具在哪个文件,只要从注册表拿就行。本节逐行拆解这个最小注册表,并讨论它的优缺点,为后两节(具体工具、调用机制)打底。 内容来源:原项目源码 ,精读并套用体系化模板。 学习目标 阅读完本节,你应当能够: 读懂 的全部代码(它很短)。 说清「注册表模式」要解决的解耦问题。
本节摘要:本节是「LLM 工具调用体系」的开篇,讲一个常被忽略却很关键的设计——
tools_registry.py。整个文件只有十几行,却用最朴素的方式回答了一个问题:「Agent 到底能用哪些函数?」。它的做法是写一个get_tools()函数,返回一个函数对象的列表(不是字符串、不是字典),把分散在各模块的工具统一汇聚到一个入口。这种「注册表」模式把「工具实现」和「工具消费方」解耦——Agent 不需要知道工具在哪个文件,只要从注册表拿就行。本节逐行拆解这个最小注册表,并讨论它的优缺点,为后两节(具体工具、调用机制)打底。
内容来源:原项目源码
autohedge/tools/tools_registry.py,精读并套用体系化模板。
阅读完本节,你应当能够:
tools_registry.py 的全部代码(它很短)。get_tools() 返回的是函数对象列表,而不是字符串或字典。先想一个朴素的问题:Agent 想调工具,它怎么知道有哪些工具可调?
最直接的写法是,在实例化 Agent 的地方直接 import 并塞进 tools=[...]:
from autohedge.tools.jupiter_search import search_tokens from autohedge.tools.ultra_tools import execute_trade agent = Agent(tools=[search_tokens, execute_trade])
这样写能用,但有个毛病:工具的「清单」散落在各个消费方。如果今天新增一个工具,你得去每个用工具的地方都改一遍;如果某个工具改了名字,所有引用处都要跟着改。消费方(谁用工具)和实现方(工具在哪)耦合在了一起。
注册表模式就是来解这个耦的:单独弄一个地方登记「全部可用工具」,谁都别直接 import 实现方,统一从注册表要。AutoHedge 的 tools_registry.py 就是这个登记处。
整个文件只有 15 行,逐行看:
from autohedge.tools.jupiter_search import search_tokens # ① 导入:Jupiter 代币搜索 from autohedge.tools.jupiter_price import get_token_price # ② 导入:Jupiter 代币价格 from autohedge.tools.ultra_tools import execute_trade, get_holdings # ③ 导入:Solana 执行+持仓 from autohedge.tools.ultra_tools import get_order # ④ 导入:Jupiter 取未签名交易 def get_tools(): # ⑤ 注册表入口函数 return [ search_tokens, # ⑥ 列表元素是函数对象本身 get_token_price, execute_trade, get_holdings, get_order, ]
几个关键点:
ultra_tools 导入了两个函数(execute_trade、get_holdings),第 ④ 行又单独导入 get_order——这是源码里实际的写法,分两行不影响功能。get_tools():注册表的对外接口。它是个函数,不是变量;每次调用都会构造一个新列表返回(列表是可变对象,这样避免共享可变状态)。search_tokens,没有括号。有括号就变成「调用结果」了,这里要的是函数本身,供后续 LLM 框架按需调用。💡 核心心法:「函数对象」是一等公民——可以放进列表、当参数传、被别人调用。
get_tools()返回的就是「五个待调用的函数」,至于什么时候调、调哪个,交给 LLM 框架在运行时决定(下一节详讲)。
对比几种可能的注册表设计,体会这种选择:
| 设计方案 | 形式 | 优缺点 |
|---|---|---|
| 字符串列表 | ["search_tokens", ...] |
简单,但调用时要按名字反射查函数,运行时易出错 |
| 字典 | {"search_tokens": search_tokens, ...} |
可按名查,但 LLM 框架通常不按名取,按函数签名取更直接 |
| 函数对象列表(本项目) | [search_tokens, ...] |
直接是可调用对象,框架读 __name__、__doc__、签名就能用 |
| 装饰器注册 | @register_tool 装饰函数 |
更优雅、自动收集,但要维护全局表,复杂度高 |
swarms(以及多数 LLM 工具框架)拿到的就是函数对象,然后通过 Python 内省机制读出:
func.__name__:工具名(如 search_tokens)。func.__doc__:工具说明(给 LLM 看的「这个工具干嘛的」)。inspect.signature(func):参数名与类型(给 LLM 决定怎么传参)。所以,函数本身的命名、文档字符串、参数注解就是工具的「说明书」——这也是为什么本项目的工具函数(下一节看 exa_search)都写了详尽的 docstring。
这五个被注册的函数,覆盖了 Solana 链上交易的全套动作:
| 函数 | 来源模块 | 作用 |
|---|---|---|
search_tokens |
jupiter_search | 按 symbol/mint 搜 Solana 代币 |
get_token_price |
jupiter_price | 按 mint 查 Solana 代币 USD 价格 |
get_order |
ultra_tools | 向 Jupiter 拿一笔未签名交易 |
execute_trade |
ultra_tools | 用私钥签名并广播交易 |
get_holdings |
ultra_tools | 查钱包持仓(SOL + 各代币) |
注意一个显著缺口:这个注册表里没有任何股票端工具——没有 yahoo_api、没有 polygon_api,也没有情绪分析用的 exa_search。换句话说,这个统一注册表只收录了 Solana 链路工具。
💡 关键观察:这与第 1 章看到的现象一致——项目里情绪 Agent 是单独在
workers.py里直接绑了tools=[exa_search],绕开了注册表;而 yahoo/polygon 等工具根本没被绑给任何 Agent。注册表是个「好设计」,但本项目并没有把它贯彻到所有工具——这是「未完成」特征的又一处体现。
在整个项目里搜 get_tools,会发现它主要被外部脚本(example.py 一类的入口)或可扩展点引用,用来「拿一套工具配给某个 Agent」。典型用法:
from autohedge.tools.tools_registry import get_tools agent = Agent( agent_name="Solana-Agent", tools=get_tools(), # 一次性把五个工具全配上 ... )
注意 get_tools() 后面有括号——调用函数、拿到列表;而列表里的元素是函数对象(无括号)。两者别混淆。
优点:
get_tools,不直接依赖各实现模块,实现方可自由重构。缺点(也是改进方向):
execute_trade——生产里应分级授权。⚠️ 现实澄清:这个注册表本身是个干净的设计,但它注册的五个工具里有
execute_trade这种真实动钱的函数,且没有任何分级或确认机制。把「读数据」和「花钱」混在同一个列表里、用同一套权限对待,在生产环境是危险的。
tools_registry.py 用一个 get_tools() 函数把分散的工具汇聚成单一入口,解耦「实现方」与「消费方」。search_tokens、get_token_price、execute_trade、get_holdings、get_order),get_tools() 返回它们的列表。__name__、__doc__、签名做内省,所以工具函数的 docstring 就是给 LLM 的说明书。下一节,我们精读注册表外的另一个工具——情绪 Agent 用的
exa_search_tool.py,看一个完整的 LLM 工具函数长什么样。