设计一个可扩展的工具系统 —— 从 Claude Code 的 40+ 工具学习架构模式

3 阅读6分钟

设计一个可扩展的工具系统 —— 从 Claude Code 的 40+ 工具学习架构模式

Claude Code 源码泄露技术解析系列 · 第 4 篇
深入解析生产级 AI 工具的平台化架构设计


引言

Claude Code 的核心能力之一是其丰富的工具系统——从文件操作到代码执行,从网络请求到数据库查询,超过 40 个内置工具构成了一个强大的 AI 编程助手。

但更重要的是其架构设计:如何设计一个可扩展、可维护、安全的工具系统?如何让新工具的添加变得简单?如何确保工具执行的权限控制?

本文将从 Claude Code 的源码出发,深度解析工具系统的架构模式。

本文你将学到

  • 工具接口设计(Tool Interface)
  • 权限门控模式(Permission Gate)
  • 工具注册与发现机制
  • 工具执行的沙箱隔离
  • 实战:实现一个支持插件的 CLI 框架

一、工具接口设计

1.1 核心接口定义

// src/tools/types.ts
import { ZodSchema } from 'zod';

export interface ToolContext {
  cwd: string;
  env: Record<string, string>;
  signal: AbortSignal;
  user?: User;
}

export interface ToolResult<T = any> {
  success: boolean;
  data?: T;
  error?: string;
  display?: string; // 人类可读的输出
}

export interface Tool<TInput = any, TOutput = any> {
  /** 工具唯一标识 */
  name: string;
  
  /** 人类可读的描述,用于 LLM 理解工具用途 */
  description: string;
  
  /** 输入参数的 JSON Schema(使用 Zod 定义) */
  inputSchema: ZodSchema<TInput>;
  
  /** 所需权限列表 */
  permissions: Permission[];
  
  /** 是否允许 LLM 自动调用(无需用户确认) */
  autoApprove?: boolean;
  
  /** 执行工具的核心逻辑 */
  execute(input: TInput, context: ToolContext): Promise<ToolResult<TOutput>>;
}

export type Permission = 
  | { type: 'fs:read'; path?: string }
  | { type: 'fs:write'; path?: string }
  | { type: 'fs:execute'; path?: string }
  | { type: 'network'; domain?: string }
  | { type: 'process'; command?: string }
  | { type: 'env'; variable?: string };

1.2 使用 Zod 定义输入 Schema

import { z } from 'zod';

// 读取文件工具
const ReadFileInputSchema = z.object({
  path: z.string().describe('要读取的文件路径'),
  encoding: z.enum(['utf-8', 'binary']).optional().default('utf-8'),
});

type ReadFileInput = z.infer<typeof ReadFileInputSchema>;

// 写入文件工具
const WriteFileInputSchema = z.object({
  path: z.string().describe('要写入的文件路径'),
  content: z.string().describe('文件内容'),
  mode: z.number().optional().describe('文件权限模式'),
});

1.3 工具描述优化

工具描述直接影响 LLM 的理解和调用准确性:

const tool: Tool = {
  name: 'read_file',
  
  // ❌ 不好的描述:太简略
  // description: 'Read a file',
  
  // ✅ 好的描述:详细、包含使用场景
  description: `读取指定路径的文件内容。

适用场景:
- 查看源代码文件内容
- 读取配置文件
- 分析日志文件

注意事项:
- 大文件(>1MB)可能被截断
- 二进制文件返回 base64 编码
- 需要 fs:read 权限`,

  inputSchema: ReadFileInputSchema,
  permissions: [{ type: 'fs:read' }],
  
  async execute(input, context) {
    // ...
  }
};

二、权限门控模式

2.1 权限检查中间件

// src/tools/permissionGate.ts
export class PermissionGate {
  private rules: PermissionRule[] = [];
  
  addRule(rule: PermissionRule) {
    this.rules.push(rule);
  }
  
  async check(
    tool: Tool,
    input: any,
    context: ToolContext
  ): Promise<PermissionCheckResult> {
    for (const permission of tool.permissions) {
      const result = await this.checkPermission(permission, input, context);
      if (!result.granted) {
        return {
          granted: false,
          reason: result.reason,
          permission
        };
      }
    }
    return { granted: true };
  }
  
  private async checkPermission(
    permission: Permission,
    input: any,
    context: ToolContext
  ): Promise<{ granted: boolean; reason?: string }> {
    switch (permission.type) {
      case 'fs:read':
        return this.checkFsRead(permission, input, context);
      case 'fs:write':
        return this.checkFsWrite(permission, input, context);
      case 'process':
        return this.checkProcess(permission, input, context);
    }
  }
  
  private checkFsRead(
    permission: Permission,
    input: any,
    context: ToolContext
  ): { granted: boolean; reason?: string } {
    const requestedPath = input.path;
    const resolvedPath = path.resolve(context.cwd, requestedPath);
    
    // 检查是否在允许的路径范围内
    const allowedPaths = this.getAllowedReadPaths(context);
    const isAllowed = allowedPaths.some(
      p => resolvedPath.startsWith(p)
    );
    
    if (!isAllowed) {
      return {
        granted: false,
        reason: `路径 ${resolvedPath} 不在允许读取的范围内`
      };
    }
    
    return { granted: true };
  }
}

2.2 用户确认流程

// src/tools/userConfirmation.ts
export async function requireUserConfirmation(
  tool: Tool,
  input: any,
  context: ToolContext
): Promise<boolean> {
  // 如果工具标记为自动批准,跳过确认
  if (tool.autoApprove) {
    return true;
  }
  
  // 构建确认提示
  const prompt = buildConfirmationPrompt(tool, input);
  
  // 等待用户输入
  const answer = await promptUser(prompt);
  return answer === 'yes' || answer === 'y';
}

function buildConfirmationPrompt(tool: Tool, input: any): string {
  const lines = [
    `⚠️  工具执行确认`,
    ``,
    `工具:${tool.name}`,
    `描述:${tool.description.split('\n')[0]}`,
    ``,
    `参数:`,
  ];
  
  for (const [key, value] of Object.entries(input)) {
    lines.push(`  ${key}: ${truncate(String(value), 50)}`);
  }
  
  lines.push(``, `是否继续?(y/n)`);
  return lines.join('\n');
}

2.3 权限策略配置

// 用户可配置的权限策略
interface PermissionPolicy {
  // 自动批准的工具列表
  autoApproveTools: string[];
  
  // 禁止的工具列表
  deniedTools: string[];
  
  // 文件系统访问范围
  fsAllowList: {
    read: string[];
    write: string[];
    execute: string[];
  };
  
  // 网络访问规则
  networkRules: {
    allowedDomains: string[];
    deniedDomains: string[];
  };
  
  // 进程执行规则
  processRules: {
    allowedCommands: RegExp[];
    deniedCommands: RegExp[];
  };
}

// 默认策略(安全优先)
const defaultPolicy: PermissionPolicy = {
  autoApproveTools: ['read_file', 'list_directory', 'search_code'],
  deniedTools: [],
  fsAllowList: {
    read: [process.cwd()],
    write: [process.cwd()],
    execute: []
  },
  networkRules: {
    allowedDomains: [],
    deniedDomains: ['*.internal', 'localhost']
  },
  processRules: {
    allowedCommands: [/^npm\s/, /^yarn\s/, /^pnpm\s/],
    deniedCommands: [
      /^rm\s+-rf\s+\//,
      /^dd\s+/,
      /mkfs/
    ]
  }
};

三、工具注册与发现

3.1 工具注册表

// src/tools/registry.ts
export class ToolRegistry {
  private tools: Map<string, Tool> = new Map();
  private categories: Map<string, string[]> = new Map();
  
  /** 注册一个工具 */
  register(tool: Tool, category?: string): void {
    if (this.tools.has(tool.name)) {
      throw new Error(`工具 ${tool.name} 已注册`);
    }
    this.tools.set(tool.name, tool);
    
    if (category) {
      const tools = this.categories.get(category) || [];
      tools.push(tool.name);
      this.categories.set(category, tools);
    }
  }
  
  /** 获取单个工具 */
  get(name: string): Tool | undefined {
    return this.tools.get(name);
  }
  
  /** 获取所有工具 */
  getAll(): Tool[] {
    return Array.from(this.tools.values());
  }
  
  /** 按分类获取工具 */
  getByCategory(category: string): Tool[] {
    const names = this.categories.get(category) || [];
    return names.map(name => this.tools.get(name)).filter(Boolean) as Tool[];
  }
  
  /** 获取工具的 LLM 描述(用于 prompt) */
  getToolDescriptions(): string {
    const tools = this.getAll();
    return tools.map(tool => {
      const schema = zodToJsonSchema(tool.inputSchema);
      return `## ${tool.name}\n${tool.description}\n\n输入参数:\n${JSON.stringify(schema, null, 2)}`;
    }).join('\n\n');
  }
}

3.2 工具分类

// src/tools/categories.ts
export const TOOL_CATEGORIES = {
  FILESYSTEM: 'filesystem',
  CODE: 'code',
  SEARCH: 'search',
  PROCESS: 'process',
  NETWORK: 'network',
  UTILITY: 'utility'
} as const;

// 注册内置工具
function registerBuiltinTools(registry: ToolRegistry) {
  // 文件系统工具
  registry.register(new ReadFileTool(), TOOL_CATEGORIES.FILESYSTEM);
  registry.register(new WriteFileTool(), TOOL_CATEGORIES.FILESYSTEM);
  registry.register(new DeleteFileTool(), TOOL_CATEGORIES.FILESYSTEM);
  registry.register(new ListDirectoryTool(), TOOL_CATEGORIES.FILESYSTEM);
  
  // 代码工具
  registry.register(new RunCodeTool(), TOOL_CATEGORIES.CODE);
  registry.register(new FormatCodeTool(), TOOL_CATEGORIES.CODE);
  
  // 搜索工具
  registry.register(new SearchCodeTool(), TOOL_CATEGORIES.SEARCH);
  registry.register(new SearchFilesTool(), TOOL_CATEGORIES.SEARCH);
  
  // 进程工具
  registry.register(new ExecuteCommandTool(), TOOL_CATEGORIES.PROCESS);
  
  // 网络工具
  registry.register(new FetchUrlTool(), TOOL_CATEGORIES.NETWORK);
}

3.3 动态工具加载

// src/tools/loader.ts
import { readdir, readFile } from 'fs/promises';
import { pathToFileURL } from 'url';

export async function loadPlugins(pluginDir: string): Promise<Tool[]> {
  const tools: Tool[] = [];
  
  try {
    const files = await readdir(pluginDir);
    
    for (const file of files) {
      if (!file.endsWith('.tool.js') && !file.endsWith('.tool.ts')) {
        continue;
      }
      
      const filePath = path.join(pluginDir, file);
      const moduleUrl = pathToFileURL(filePath).href;
      const module = await import(moduleUrl);
      
      if (module.default && isValidTool(module.default)) {
        tools.push(module.default);
      }
    }
  } catch (error) {
    console.error('加载插件失败:', error);
  }
  
  return tools;
}

function isValidTool(obj: any): obj is Tool {
  return (
    typeof obj === 'object' &&
    typeof obj.name === 'string' &&
    typeof obj.description === 'string' &&
    typeof obj.execute === 'function' &&
    obj.inputSchema !== undefined
  );
}

四、工具执行与沙箱隔离

4.1 工具执行器

// src/tools/executor.ts
export class ToolExecutor {
  constructor(
    private registry: ToolRegistry,
    private permissionGate: PermissionGate,
    private logger: ToolLogger
  ) {}
  
  async execute(
    toolName: string,
    input: any,
    context: ToolContext
  ): Promise<ToolResult> {
    const tool = this.registry.get(toolName);
    
    if (!tool) {
      return {
        success: false,
        error: `未知工具:${toolName}`
      };
    }
    
    // 1. 验证输入
    const validation = tool.inputSchema.safeParse(input);
    if (!validation.success) {
      return {
        success: false,
        error: `输入验证失败:${validation.error.message}`
      };
    }
    
    // 2. 权限检查
    const permissionResult = await this.permissionGate.check(
      tool,
      validation.data,
      context
    );
    
    if (!permissionResult.granted) {
      this.logger.log('permission_denied', { tool: toolName });
      return {
        success: false,
        error: `权限拒绝:${permissionResult.reason}`
      };
    }
    
    // 3. 用户确认(如果需要)
    const confirmed = await requireUserConfirmation(tool, validation.data, context);
    if (!confirmed) {
      return { success: false, error: '用户取消操作' };
    }
    
    // 4. 执行工具
    this.logger.log('executing', { tool: toolName });
    const startTime = Date.now();
    
    try {
      const result = await Promise.race([
        tool.execute(validation.data, context),
        createTimeout(context.signal)
      ]);
      
      const duration = Date.now() - startTime;
      this.logger.log('completed', { tool: toolName, duration });
      
      return result;
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : '未知错误'
      };
    }
  }
}

4.2 沙箱隔离

// src/tools/sandbox.ts
import { spawn } from 'child_process';
import { tmpdir } from 'os';

export interface SandboxOptions {
  timeout: number;
  maxMemory: number;
  allowedPaths: string[];
  networkDisabled: boolean;
}

export class Sandbox {
  constructor(private options: SandboxOptions) {}
  
  async executeCommand(
    command: string,
    args: string[],
    cwd: string
  ): Promise<{ stdout: string; stderr: string; code: number }> {
    // 创建隔离的临时目录
    const sandboxDir = await this.createSandboxDir();
    
    return new Promise((resolve, reject) => {
      const proc = spawn(command, args, {
        cwd,
        env: this.createRestrictedEnv(),
        stdio: ['pipe', 'pipe', 'pipe'],
      });
      
      let stdout = '';
      let stderr = '';
      
      proc.stdout.on('data', (data) => { stdout += data; });
      proc.stderr.on('data', (data) => { stderr += data; });
      
      // 超时处理
      const timer = setTimeout(() => {
        proc.kill('SIGKILL');
        reject(new Error('命令执行超时'));
      }, this.options.timeout);
      
      proc.on('close', (code) => {
        clearTimeout(timer);
        resolve({ stdout, stderr, code: code || 0 });
      });
      
      proc.on('error', reject);
    });
  }
  
  private createRestrictedEnv(): NodeJS.ProcessEnv {
    const safeEnv: NodeJS.ProcessEnv = {};
    const allowedVars = ['PATH', 'HOME', 'USER', 'LANG'];
    for (const key of allowedVars) {
      if (process.env[key]) safeEnv[key] = process.env[key]!;
    }
    return safeEnv;
  }
  
  private async createSandboxDir(): Promise<string> {
    const dir = await mkdtemp(join(tmpdir(), 'sandbox-'));
    return dir;
  }
}

五、实战:实现插件式 CLI 框架

5.1 项目结构

my-plugin-cli/
├── src/
│   ├── index.ts           # 入口
│   ├── types.ts           # 类型定义
│   ├── registry.ts        # 工具注册表
│   ├── executor.ts        # 工具执行器
│   ├── permission.ts      # 权限系统
│   └── tools/             # 内置工具
│       ├── read-file.ts
│       ├── write-file.ts
│       └── run-command.ts
├── plugins/               # 用户插件目录
│   └── custom.tool.ts
├── package.json
└── tsconfig.json

5.2 完整实现

// src/index.ts
#!/usr/bin/env bun
import { ToolRegistry } from './registry';
import { ToolExecutor } from './executor';
import { PermissionGate } from './permission';

async function main() {
  // 初始化注册表
  const registry = new ToolRegistry();
  
  // 注册内置工具
  registerBuiltinTools(registry);
  
  // 加载用户插件
  const plugins = await loadPlugins('./plugins');
  for (const plugin of plugins) {
    registry.register(plugin, 'plugin');
  }
  
  // 初始化权限系统
  const permissionGate = new PermissionGate();
  permissionGate.loadPolicy('./policy.json');
  
  // 初始化工具执行器
  const executor = new ToolExecutor(registry, permissionGate, consoleLogger);
  
  // 处理 LLM 工具调用
  const context: ToolContext = {
    cwd: process.cwd(),
    env: process.env as Record<string, string>,
    signal: AbortSignal.timeout(30000)
  };
  
  // 示例:执行工具
  const result = await executor.execute('read_file', {
    path: './package.json'
  }, context);
  
  console.log(JSON.stringify(result, null, 2));
}

main().catch(console.error);

5.3 示例插件

// plugins/weather.tool.ts
import { z } from 'zod';
import type { Tool } from '../src/types';

const WeatherTool: Tool = {
  name: 'get_weather',
  description: '获取指定城市的天气信息',
  inputSchema: z.object({
    city: z.string().describe('城市名称'),
    unit: z.enum(['celsius', 'fahrenheit']).optional().default('celsius')
  }),
  permissions: [{ type: 'network', domain: 'api.weather.com' }],
  
  async execute(input) {
    try {
      const response = await fetch(
        `https://api.weather.com/weather/${input.city}?unit=${input.unit}`
      );
      const data = await response.json();
      
      return {
        success: true,
        data,
        display: `${input.city} 天气:${data.condition}, ${data.temp}°${input.unit === 'celsius' ? 'C' : 'F'}`
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : '获取天气失败'
      };
    }
  }
};

export default WeatherTool;

六、最佳实践

6.1 工具设计原则

单一职责:每个工具只做一件事

// ❌ 不好:一个工具做太多
const BadTool = {
  name: 'file_ops',
  description: '文件操作(读/写/删除/移动)',
  // ...
};

// ✅ 好:每个操作一个工具
const ReadFileTool = { name: 'read_file', ... };
const WriteFileTool = { name: 'write_file', ... };
const DeleteFileTool = { name: 'delete_file', ... };

幂等性:重复执行相同操作应产生相同结果

// ✅ 好的设计
const WriteFileTool = {
  async execute({ path, content }) {
    // 如果文件已存在且内容相同,不执行写入
    const existing = await readFile(path);
    if (existing === content) {
      return { success: true, data: { unchanged: true } };
    }
    await writeFile(path, content);
    return { success: true };
  }
};

错误处理:提供清晰的错误信息

// ✅ 好的错误处理
try {
  await readFile(path);
} catch (error) {
  if (error.code === 'ENOENT') {
    return { success: false, error: `文件不存在:${path}` };
  }
  if (error.code === 'EACCES') {
    return { success: false, error: `权限不足,无法读取:${path}` };
  }
  throw error;
}

6.2 安全建议

  • 最小权限原则:工具只申请必需的权限
  • 输入验证:使用 Zod 严格验证所有输入
  • 路径规范化:始终解析和验证文件路径
  • 超时控制:所有工具执行必须有超时
  • 日志审计:记录所有工具调用

七、总结

核心架构组件

组件职责关键类
工具接口定义工具契约Tool, ToolResult
注册表工具发现与管理ToolRegistry
权限门控安全检查PermissionGate
执行器工具调用编排ToolExecutor
沙箱隔离执行Sandbox

扩展性设计

  • ✅ 插件式架构:动态加载外部工具
  • ✅ 分类管理:按功能组织工具
  • ✅ 权限策略:可配置的安全规则
  • ✅ 统一接口:所有工具遵循相同模式

延伸学习


系列导航