动态工具注册与插件化架构 ——CogitoAgent开发实战(十)
动态工具注册与插件化架构
——CogitoAgent开发实战(第10篇)
📖 本文是专栏的第十篇。前九篇我们讲了AI的各种能力——思考、文件、联网、终端、记忆、代码、Git、浏览器。但所有能力的背后,有一个“骨架”把它们串在一起——工具系统。这一篇,我们深入工具注册与调度机制,看看如何从“硬编码”走向“插件化”。

📌 从一个问题开始
假设你要给AI加一个新工具——rename(重命名文件)。
在初版代码里,你需要改三个地方:
- 在
file.js里写rename函数 - 在
file.js的export里加上rename - 在
Agent.js的executeTool的switch里加一个case 'rename'
三步不算多,但如果你要加100个工具呢?如果其他人想贡献工具,每次都要改 Agent.js 呢?
问题来了:工具越多,executeTool 的 switch 就越长。每加一个工具,就要改一次核心代码。
这就违反了开闭原则(对扩展开放,对修改关闭)。
一、传统方案:switch-case 的困境
1.1 初版代码
// Agent.js - 初版
async function executeTool(tool, args) {
switch (tool) {
case 'ls': return await ls(args[0]);
case 'read': return await read(args[0]);
case 'copy': return await copy(args[0], args[1]);
case 'mkdir': return await mkdir(args[0]);
// ... 100+ 个 case
default: return { success: false, error: `未知工具: ${tool}` };
}
}
随着工具增加,这个 switch 越来越长。每次加工具,都要修改 Agent.js,增加了:
- 维护成本:修改核心文件的风险
- 合并冲突:多人同时加工具时冲突
- 测试成本:每次修改都要重新测试
1.2 更严重的问题:参数处理不一致
case 'ls': return await ls(args[0]);
case 'create': return await create(args[0], args.slice(1).join(','));
case 'search': return await search(args.join(',').trim());
case 'splitTask': return await splitTask(args[0], JSON.parse(args[1]));
每个工具的参数处理方式都不同,散落在 case 分支里。没有统一规则,新人加工具时容易出错。
二、解决方案:注册表模式
2.1 注册表的定义
把工具的所有“元信息”集中在一起,用声明式配置代替命令式逻辑。
// registry.js
const TOOL_REGISTRY = {
// 文件操作
ls: { fn: tools.ls, argCount: 1 },
read: { fn: tools.read, argCount: 1 },
copy: { fn: tools.copy, argCount: 2 },
mkdir: { fn: tools.mkdir, argCount: 1 },
create: { fn: tools.create, argCount: 2, customArgs: true },
// Git
gitReset: { fn: tools.gitReset, argCount: 2 },
gitPush: { fn: tools.gitPush, argCount: 3 },
// 任务管理
splitTask: { fn: tools.splitTask, argCount: 2, parseJson: [false, true] },
// ...
};
2.2 注册表的结构
| 字段 | 类型 | 说明 |
|---|---|---|
fn |
函数 | 工具的实际执行函数 |
argCount |
数字 | 参数数量(用于验证) |
customArgs |
布尔 | 是否需要自定义参数处理 |
parseJson |
数组 | 哪些参数需要JSON解析 |
2.3 执行逻辑的统一
有了注册表,executeTool 变成通用的:
async function executeTool(toolName, args) {
const registry = TOOL_REGISTRY[toolName];
if (!registry) {
return { success: false, error: `未知工具:${toolName}` };
}
// 危险操作确认
if (isConfirmEnabled() && isDangerousOperation(toolName)) {
const confirmed = await requestConfirmation(toolName, args);
if (!confirmed) {
return { success: false, error: '用户拒绝执行此危险操作' };
}
}
// 参数处理
let processedArgs = args;
// JSON 解析
if (registry.parseJson) {
processedArgs = args.map((arg, i) => {
if (registry.parseJson[i] && typeof arg === 'string') {
try { return JSON.parse(arg); } catch { return arg; }
}
return arg;
});
}
// 特殊参数处理
if (toolName === 'create') {
processedArgs = [args[0], args.slice(1).join(',')];
} else if (toolName === 'search') {
processedArgs = [args.join(',').trim()];
}
// 执行
try {
return await registry.fn(...processedArgs);
} catch (error) {
return { success: false, error: error.message };
}
}
2.4 加工具变成三步
现在加一个工具只需要:
- 在工具文件里写函数
- 在
index.js里导出 - 在注册表里加一条配置
不需要修改 Agent.js 的任何代码。
三、注册表带来的额外能力
3.1 工具列表查询
function getToolNames() {
return Object.keys(TOOL_REGISTRY);
}
AI 可以通过 /tools 命令查看所有可用工具。
3.2 危险操作标记
const DANGEROUS_OPERATIONS = new Set([
'gitPush', 'gitReset', 'gitBranchDelete',
'executeCode', 'executeFile',
'deleteData', 'dropTable',
'clearTasks', 'clearMemory'
]);
function isDangerousOperation(toolName) {
return DANGEROUS_OPERATIONS.has(toolName);
}
危险操作列表独立维护,执行前自动触发确认机制。
3.3 按类别分组
function getToolsByCategory() {
return {
file: ['ls', 'read', 'copy', 'mkdir', 'create'],
git: ['gitInit', 'gitClone', 'gitAdd', 'gitCommit', 'gitPush', ...],
task: ['createTask', 'getTasks', 'updateTask', ...],
// ...
};
}
3.4 工具输出截断
不同类型的工具输出长度不同:
const TOOL_OUTPUT_LIMITS = {
ls: 5000,
gitLog: 10000,
executeCode: 50000,
read: 20000,
default: 10000
};
注册表中可以声明每个工具的输出限制,避免上下文爆炸。
四、进阶:自动生成注册表
4.1 元数据定义
既然注册表是“声明式配置”,为什么不能自动生成?
// tools/index.js - 新增
const TOOL_METADATA = {
ls: { argCount: 1 },
read: { argCount: 1 },
copy: { argCount: 2 },
// ...
};
function generateToolRegistry() {
const registry = {};
for (const [name, metadata] of Object.entries(TOOL_METADATA)) {
const fn = toolExport[name];
if (typeof fn === 'function') {
registry[name] = { fn, ...metadata };
}
}
return registry;
}
4.2 工具与注册表的完全解耦
理想状态:
- 开发者在
tools/目录下新建一个文件,写工具函数 - 在
index.js中导出 - 程序自动扫描所有导出,生成注册表
这样,加工具就只需要写函数 + 导出两步,连注册表都不用手动改。
五、设计启示
5.1 从“硬编码”到“声明式”
| 方面 | switch-case | 注册表 |
|---|---|---|
| 加工具 | 改 executeTool |
加一条配置 |
| 参数处理 | 分散在各 case | 集中在注册表 |
| 危险标记 | 散落各处 | 集中列表 |
| 工具列表 | 手动维护 | 自动生成 |
5.2 插件化架构的雏形
注册表模式是插件化架构的第一步:
- 定义接口:工具函数必须遵守
{ success, data/error }契约 - 声明元数据:工具的参数数量、是否需要JSON解析等
- 自动发现:扫描导出,自动注册
有了这三步,外部开发者可以在不修改核心代码的情况下添加新工具。这就是插件化。
5.3 什么时候该重构?
| 信号 | 说明 |
|---|---|
| 同一个函数超过50行 | 功能太多,该拆分 |
| 同一个文件超过300行 | 职责太多,该拆分 |
| 加一个功能要改3个以上文件 | 耦合太紧,该解耦 |
| 同类代码重复出现3次以上 | 该抽象了 |
CogitoAgent 的 Agent.js 从800行拆分到多个文件,正是遵循了这些信号。
六、小结
这一篇讲了工具注册与调度系统:
| 阶段 | 方案 | 问题 |
|---|---|---|
| 初版 | switch-case |
每次加工具改核心代码 |
| 改进 | 注册表模式 | 加工具只改配置,不动核心 |
| 进阶 | 自动生成注册表 | 加工具只需写函数+导出 |
核心设计原则:
- 声明式优于命令式(注册表 vs switch-case)
- 配置与逻辑分离(工具定义与执行分离)
- 面向接口编程(工具函数遵守统一契约)
- 自动发现优于手动注册
下一篇预告:测试体系与工程化
我们将深入测试模块,看看:
- 如何用Jest测试ES Module项目
- 安全验证的测试用例设计
- 模拟与断言的最佳实践
如果这篇文章对你有帮助,欢迎 ⭐Star 支持一下开源项目!
更多推荐




所有评论(0)