动态工具注册与插件化架构

——CogitoAgent开发实战(第10篇)

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

在这里插入图片描述


📌 从一个问题开始

假设你要给AI加一个新工具——rename(重命名文件)。

在初版代码里,你需要改三个地方:

  1. file.js 里写 rename 函数
  2. file.jsexport 里加上 rename
  3. Agent.jsexecuteToolswitch 里加一个 case 'rename'

三步不算多,但如果你要加100个工具呢?如果其他人想贡献工具,每次都要改 Agent.js 呢?

问题来了:工具越多,executeToolswitch 就越长。每加一个工具,就要改一次核心代码。

这就违反了开闭原则(对扩展开放,对修改关闭)。


一、传统方案: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 加工具变成三步

现在加一个工具只需要:

  1. 在工具文件里写函数
  2. index.js 里导出
  3. 在注册表里加一条配置

不需要修改 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 工具与注册表的完全解耦

理想状态:

  1. 开发者在 tools/ 目录下新建一个文件,写工具函数
  2. index.js 中导出
  3. 程序自动扫描所有导出,生成注册表

这样,加工具就只需要写函数 + 导出两步,连注册表都不用手动改。


五、设计启示

5.1 从“硬编码”到“声明式”

方面 switch-case 注册表
加工具 executeTool 加一条配置
参数处理 分散在各 case 集中在注册表
危险标记 散落各处 集中列表
工具列表 手动维护 自动生成

5.2 插件化架构的雏形

注册表模式是插件化架构的第一步:

  1. 定义接口:工具函数必须遵守 { success, data/error } 契约
  2. 声明元数据:工具的参数数量、是否需要JSON解析等
  3. 自动发现:扫描导出,自动注册

有了这三步,外部开发者可以在不修改核心代码的情况下添加新工具。这就是插件化。

5.3 什么时候该重构?

信号 说明
同一个函数超过50行 功能太多,该拆分
同一个文件超过300行 职责太多,该拆分
加一个功能要改3个以上文件 耦合太紧,该解耦
同类代码重复出现3次以上 该抽象了

CogitoAgent 的 Agent.js 从800行拆分到多个文件,正是遵循了这些信号。


六、小结

这一篇讲了工具注册与调度系统:

阶段 方案 问题
初版 switch-case 每次加工具改核心代码
改进 注册表模式 加工具只改配置,不动核心
进阶 自动生成注册表 加工具只需写函数+导出

核心设计原则

  1. 声明式优于命令式(注册表 vs switch-case)
  2. 配置与逻辑分离(工具定义与执行分离)
  3. 面向接口编程(工具函数遵守统一契约)
  4. 自动发现优于手动注册

下一篇预告:测试体系与工程化

我们将深入测试模块,看看:

  • 如何用Jest测试ES Module项目
  • 安全验证的测试用例设计
  • 模拟与断言的最佳实践

如果这篇文章对你有帮助,欢迎 ⭐Star 支持一下开源项目!

👉 https://gitee.com/cnt-code/cogito-agent 👈

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐