设计一个可扩展的工具系统 —— 从 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 |
扩展性设计
- ✅ 插件式架构:动态加载外部工具
- ✅ 分类管理:按功能组织工具
- ✅ 权限策略:可配置的安全规则
- ✅ 统一接口:所有工具遵循相同模式