第 06 篇 · 工具系统:可插拔的工具注册机制
app/tools.py 里的。三个工具都写在了一个文件里面,工具执行逻辑,参数定义,工具申明都在这里面。如果我们要再添加第四个工具,比如要加一个 web_search,就需要做如下的修改:
在文件里写一个
tool_web_search(query)函数把它加进
TOOL_FUNCTIONS字典在
TOOL_DEFINITIONS列表里再写一份 JSON 描述
三个地方都要改。工具一多,这个文件会膨胀到几百行,多个人协作时,三处改动遗漏一处就会出莫名其妙的 bug。
这一篇我们重构工具系统,每个工具独立一个文件,丢进 tools/ 目录就能用,启动时做一个自动发现,后面的Agent就可以自动发现可用工具,后面添加工具,就不用改动其他文件。
工具格式约定
我们要做一个工具的自动发现机制,首先我们需要一个扫描器,来自动扫描发现工具。
那么扫描器怎么知道某个文件是一个工具?工具的元信息(name、description、参数)从哪取?怎么执行它?
我们定一套简单的约定。每个工具文件必须导出两个东西:
DEFINITION:一个 dict 字典,描述这个工具是什么、参数是什么execute:一个函数,传 args 进去,返回字符串结果
只要满足这两个东西,注册器扫描到就能用。具体看一个例子:
# 假设这就是 read_file_tools.py 的样子
DEFINITION = {
"id": "read_file",
"name": "read_file",
"description": "读取工作目录下某个文件的完整内容",
"parameters": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
},
}
defexecute(args: dict, **kwargs) -> str:
path = args.get("path", "")
...
return content
DEFINITION 里面的 id 和 name 字段。id 是工具的唯一标识(用来做配置和索引),name 是给模型看的函数名。它们一般来说都是一样的,分开配置也有一些好处,比如以后想给同一个工具配置不同的 name 也能做到。
execute 接受 args: dict 和 kwargs。args 是模型传过来的参数字典,kwargs 是给主流程传上下文用的(比如 session_id、当前 agent 是谁),工具用不到可以忽略。这个签名留着扩展余地。
返回必须是字符串,失败了也是字符串(以 "Error:" 开头),不要 raise 异常。第 04 篇讲过为什么这样做,异常会让 Agent 循环挂掉,而字符串错误可以让模型自己看看错误消息,自己想办法做后续处理。
整个目录结构会变成这样:
backend/app/tools/
├── __init__.py # 注册器
├── list_files_tools.py # 一个工具一个文件
├── read_file_tools.py
├── write_file_tools.py
└── web_search_tools.py # 想加新工具就丢一个文件进来
还有一个约定就是,文件名以 _tools.py 结尾,注册器扫的就是这个后缀。