多工具使用-从硬编码到注册表
上篇你学会了 Function Calling——让 AI 从”会说”变成”会干”。但当工具从 2 个变成 10 个、20 个,代码就开始失控了。这篇讲怎么用工具注册表模式,把工具管理这件事变得干净利落。
痛点:工具一多,代码就乱
小明最近很得意。Function Calling 跑通了,AI 能查天气、能搜新闻,老板夸了他两句。
然后老板说:”再加几个功能吧——查股票、查快递、算汇率、翻译、查数据库……先上十个。”
小明心想这还不简单?复制粘贴呗。打开代码一看,傻眼了——
每加一个工具,要改 3 个地方:
| 改动位置 | 干什么 | 举例 |
|---|---|---|
工具列表 tools |
加一段 JSON Schema 定义 | 告诉模型有这个工具 |
映射表 available_functions |
加一行函数映射 | "query_stock": query_stock |
| 执行逻辑 | 加 if-else 或修改循环 | 处理这个工具的特殊逻辑 |
小明去找老张:”这不对劲啊。十个工具就是 30 处改动,漏改一个就出 bug。而且 description 和 parameters 散落在代码各处,找个定义要翻半天。”
老张说:”你遇到的是经典的工程化问题。工具少的时候硬编码没问题,超过 3 个就该上注册表了。”
“注册表?”
“一句话——新增工具只需 register() 一行,其余全自动。”
核心思路:把”工具定义”和”对话编排”解耦
老张在白板上画了张图:
“看到了吧?Agent 代码不再关心’有哪些工具’,它只管调 get_openai_tools() 拿列表、调 execute() 执行。工具的增删改和对话逻辑完全解耦。“
小明问:”怎么做到的?”
“三个组件:ToolSpec 存定义、Registry 管注册、execute 做执行。逐个讲。”
组件一:ToolSpec——工具的”身份证”
老张说:”每个工具需要携带很多信息——函数本身、给模型看的描述、参数定义、中文名、参数回退策略。用一个 dataclass 把它们打包:”
逐字段解释:
| 字段 | 作用 | 举例 |
|---|---|---|
func |
真正干活的 Python 函数 | get_current_weather_online |
description |
模型靠它判断何时调用 | "获取指定城市当前的天气情况" |
parameters |
JSON Schema,描述参数结构 | {"type": "object", "properties": {...}} |
label |
中文名,给 UI 或日志展示用 | "天气" |
fill_from_user |
参数缺失时用用户原话填充 | ["query"] |
小明问:”这个 fill_from_user 是什么意思?”
“还记得你之前搜索工具那个特殊处理吗?模型有时候不传 query 参数,你想用用户原话兜底。之前是硬编码在执行逻辑里的 if-else,现在变成一个配置项——注册时声明 fill_from_user=["query"],执行时自动兜底。”
“原来是把特殊逻辑变成了配置项。”
“对。配置优于编码——能配置的就不要写死。”
组件二:Registry——注册表
注册函数
“核心就是一个字典——_REGISTRY。键是函数名,值是 ToolSpec。register() 做的事就是把函数和它的元信息打包存进去。”
“用函数名 func.__name__ 作为键,保证和模型返回的函数名天然对应——模型说调 get_current_weather_online,你直接在注册表里查这个名字。”
自动生成 OpenAI Tools
小明说:”这不就是遍历注册表,把每个 ToolSpec 转成 OpenAI 要求的格式吗?”
“对。以前你手动维护 tools 列表,现在自动生成。注册表是唯一数据源,get_openai_tools() 只是个格式转换器。永远不会出现’tools 列表里有但映射表里没有’这种不一致问题。”
统一执行入口
老张逐行讲解:
-
查注册表 — 函数名不在表里?返回错误 JSON,不会 KeyError 崩溃 -
参数回退 — 遍历 fill_from_user,缺失的参数用用户原话补上 -
执行函数 — spec.func(**args)解包参数调用真正的函数 -
序列化结果 — json.dumps转字符串,ensure_ascii=False保留中文
“以前你的执行逻辑内联在 Agent 循环里,现在统一收到 execute() 一个函数里。Agent 只管调 execute(),不关心参数回退、错误处理这些细节。”
小明恍然:”所以注册表做了三件事——存定义、转格式、管执行。”
“说到点子上了。”
注册工具:只需两行
老张说:”看实际怎么用。在 tool_registry.py 底部:”
新增工具只需两步:
-
在 tools/下写函数实现(独立文件,不碰其他代码) -
import+register(...)
“想加个计算器?新建 tools/calculator.py,写个 calculate 函数,然后:”
“完事。Agent 代码一行不用改。”
小明对比了一下之前的做法:
| 对比项 | 硬编码版 | 注册表版 |
|---|---|---|
| 新增工具 | 改 3 处代码 | register() 1 处 |
| 参数回退 | 手写 if-else | fill_from_user 配置 |
| 工具列表 | 手动维护,容易不一致 | get_openai_tools() 自动生成 |
| 执行逻辑 | 内联在 Agent 循环中 | execute() 统一入口 |
| 工具实现 | 散落在主文件里 | 独立文件,互不干扰 |
“差距很明显。”小明说,”工具越多,注册表的优势越大。”
Agent 类:干净的编排逻辑
老张说:”注册表把脏活都干了,Agent 类就变得很干净——它不再关心具体有哪些工具:”
“对比你之前硬编码的版本,结构一模一样——两轮调用、messages 累积、tool 消息追加。区别只有两处:”
-
tools=get_openai_tools()— 不再传硬编码列表,从注册表自动获取 -
execute(name, args, user_message)— 不再手动查映射表 + 手动执行,统一入口
“Agent 代码从此稳定不动。加工具、删工具、改工具,都只在 tools/ 目录和 register() 调用里折腾,Agent 连重新部署都不用。”
小明问:”那入口文件呢?”
“更简单了,启动时打印一下当前注册了哪些工具:”
“加个计算器后,启动自动变成 工具:天气 / 搜索 / 计算器——你不用改一行入口代码。”
项目结构:各归各位
老张画了完整的目录结构:
“每个文件职责单一:”
| 文件 | 职责 | 改动频率 |
|---|---|---|
main.py |
启动入口 | 几乎不改 |
agent.py |
对话编排逻辑 | 几乎不改 |
config.py |
API 配置 | 换模型时改 |
tool_registry.py |
注册表核心 + 注册调用 | 加工具时改(加两行) |
tools/*.py |
工具函数实现 | 加工具时新建文件 |
小明注意到:”工具实现放在独立文件里?”
“对。每个工具一个文件,互相不干扰。weather.py 里只有天气逻辑,news.py 里只有搜索逻辑。你想改天气工具,打开 weather.py 改就是了,不用担心碰坏别的工具。”
“这和写代码的模块化原则是一样的——单一职责、高内聚低耦合。”
参数回退:一个容易被忽略的细节
老张说:”注册表里有个设计值得单独讲——fill_from_user。”
“什么场景会用到?”
“用户说’帮我搜一下最新的 AI 进展’。模型可能正确提取了 query="AI 进展",也可能偷懒不传参数,只返回一个空的 arguments: {}。”
“之前你的做法是:”
“这种写法的问题——每加一个需要回退的工具,就要加一段 if。工具多了,if-else 越来越长。”
“注册表的做法是把回退策略配置化:”
“执行时 execute() 自动处理:”
“不用写 if,不用改 Agent 代码,不用关心是哪个工具。声明式优于命令式——你告诉系统’这个参数缺失时怎么办’,系统自己处理。”
| 方式 | 代码量 | 可维护性 |
|---|---|---|
| 硬编码 if-else | 每个工具 N 行 | 低,容易遗漏 |
fill_from_user 配置 |
每个工具 1 行 | 高,一目了然 |
跑起来看看
小明把注册表版代码跑了起来:
小明加了计算器工具后,重新运行:
“没改 Agent 一行代码,工具列表自动更新了。”
老张说:”这就是注册表的价值——工具的增删改和对话逻辑彻底分离。你的 Agent 代码写一次就不用动了,后面全是配置和新增文件。”
四个设计原则
老张总结了注册表模式背后的四个设计原则:
1. 单一数据源
注册表是唯一的数据源。get_openai_tools() 从它生成工具列表,execute() 从它查找函数。永远不会出现”列表里有但映射表里没有”的不一致问题。
2. 配置优于编码
参数回退用 fill_from_user 配置,不用写 if-else。能配置的就不要写死,因为配置比代码更容易修改、更容易审查。
3. 声明式注册
register() 是声明式的——你声明”这个函数是个工具,长这样”,系统自动处理后续。不需要你写”先把这个加到列表里,再把它加到映射表里,再处理它的特殊逻辑”。
4. 开放封闭原则
对扩展开放(加新工具只需 register),对修改封闭(Agent 代码不用改)。这是工程设计的基本原则——加功能不应该改老代码。
| 原则 | 注册表怎么做到的 |
|---|---|
| 单一数据源 | _REGISTRY 是唯一数据源,列表和执行都从它派生 |
| 配置优于编码 | fill_from_user 替代 if-else |
| 声明式注册 | register() 一行声明,系统自动处理 |
| 开放封闭 | 加工具 = 新文件 + register,Agent 不动 |
总结
上篇你学会了 Function Calling 的两轮对话机制。这篇你学会了当工具变多时,怎么用注册表模式管理它们——
核心就三件事:
-
ToolSpec 把函数、描述、参数、回退策略打包成一个数据结构 -
register() 一行注册,自动进入全局注册表 -
get_openai_tools() + execute() 从注册表自动生成工具列表和统一执行入口
Agent 代码从此稳定不动,工具的增删改全是配置和独立文件——新增工具只需一行 register()。
小明把注册表重构完成后,老张问他感觉怎么样。
小明说:”以前加工具像做手术——要翻开 Agent 代码,小心翼翼地改三处地方,生怕碰坏别的逻辑。现在像插 USB——写好驱动文件,插上就行,系统自动识别。”
老张笑了:”这个比喻不错。注册表就是工具的 USB 接口——热插拔,即插即用。“
“就像从 USB 进化到了无线——协议没变,传输方式升级了。”
转载请注明来源:多工具使用-从硬编码到注册表




























