Claude Code 有 48+ 个内置工具,每个工具都是一个完整的生命周期管理单元。从定义到执行,工具要经过七步管道:查找、解析、验证、钩子、权限、执行、后处理。这个设计使得每个工具都是自描述、自验证、自渲染的——框架不需要了解工具的内部逻辑,只需调用标准接口。
导读:工具不只是函数调用
在很多 AI Agent 框架中,工具只是一个简单的函数:
1 2 3 4
| @tool def read_file(path: str) -> str: with open(path) as f: return f.read()
|
但在 Claude Code 中,工具是一个完整的生命周期管理单元:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| type Tool<Input, Output> = { name: string aliases?: string[] searchHint?: string
isEnabled(): boolean isConcurrencySafe(input): boolean isReadOnly(input): boolean isDestructive(input): boolean
validateInput(input, context) checkPermissions(input, context) call(input, context, ...)
renderToolUseMessage(input) renderToolResultMessage(content) mapToolResultToToolResultBlockParam()
inputSchema: Zod schema maxResultSizeChars: number getToolUseSummary?(input): string }
|
这种设计使得每个工具都是自描述、自验证、自渲染的——框架不需要了解工具的内部逻辑,只需调用标准接口。
一、工具的定义:不只是名称和函数
src/Tool.ts(约 792 行)定义了工具的完整接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| export type Tool<Input, Output, P extends ToolProgressData> = { name: string aliases?: string[] searchHint?: string
isEnabled(): boolean isConcurrencySafe(input): boolean isReadOnly(input): boolean isDestructive?(input): boolean
validateInput?(input, context): Promise<ValidationResult> checkPermissions?(input, context): Promise<PermissionResult> call(input, context, canUseTool, parentMessage, onProgress): Promise<ToolResult<Output>>
renderToolUseMessage?(input): ReactNode renderToolResultMessage?(content): ReactNode renderToolUseProgressMessage?(...): ReactNode mapToolResultToToolResultBlockParam?(...): ToolResultBlockParam
inputSchema: Input outputSchema?: z.ZodType<Output> maxResultSizeChars?: number getToolUseSummary?(input): string shouldDefer?: boolean alwaysLoad?: boolean toAutoClassifierInput?(input): string preparePermissionMatcher?(input): Promise<(pattern: string) => boolean> }
|
buildTool() 函数提供了工具定义的便捷方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export const BashTool = buildTool({ name: 'Bash', description: async (input) => `Execute command: ${input.command}`, inputSchema: z.object({ command: z.string(), timeout: z.number().optional(), }), isConcurrencySafe: (input) => false, isReadOnly: (input) => isReadOnlyCommand(input.command), isDestructive: (input) => isDestructiveCommand(input.command), validateInput: async (input, context) => { }, checkPermissions: async (input, context) => { }, call: async (input, context, canUseTool, parentMessage, onProgress) => { }, })
|
二、工具注册:三阶段流水线
工具的发现和注册分三个阶段(src/tools.ts):
2.1 阶段1:基础工具池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| export function getAllBaseTools(): Tools { return [ FileReadTool, FileEditTool, FileWriteTool, GlobTool, GrepTool, BashTool, PowerShellTool, WebFetchTool, WebSearchTool, AgentTool, TeamCreateTool, SendMessageTool, TaskCreateTool, TaskOutputTool, TodoWriteTool, AskUserQuestionTool, SkillTool, SleepTool, ].filter(tool => { if (tool.name === 'Agent' && !feature('FORK_SUBAGENT')) return false return true }) }
|
2.2 阶段2:过滤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| export function getTools( baseTools: Tools, permissionContext: ToolPermissionContext, options: GetToolsOptions, ): Tools { return baseTools.filter(tool => { if (permissionContext.mode === 'dontAsk' && !tool.isReadOnly?.()) { return false } if (options.isReplMode && !isReplCompatible(tool)) { return false } if (!tool.isEnabled()) { return false } return true }) }
|
2.3 阶段3:MCP 合并
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| export function assembleToolPool( baseTools: Tools, mcpClients: MCPServerConnection[], ): Tools { const mcpTools: Tools = [] for (const client of mcpClients) { if (client.type !== 'connected') continue for (const mcpTool of client.tools) { const name = `mcp__${normalizeNameForMCP(client.name)}__${mcpTool.name}` mcpTools.push(convertMcpToolToTool(name, mcpTool, client)) } } return mergeAndDeduplicate(baseTools, mcpTools) }
|
工具池构建流程图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| getAllBaseTools() │ 48+ 个内置工具 │ + Feature Flag 过滤 │ ▼ getTools() │ 权限模式过滤 │ REPL 模式过滤 │ isEnabled() 过滤 │ ▼ assembleToolPool() │ + MCP 工具 │ 去重(内置优先) │ 排序(缓存稳定性) │ ▼ 最终工具池
|
三、七步执行管道
一次工具调用要经过7 步管道(src/services/tools/toolExecution.ts):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| ┌─────────────────────────────────────────────────────────────┐ │ 工具执行管道 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Step 1: 工具查找 │ │ └─ findToolByName(name) → 支持别名回退 │ │ │ │ Step 2: 输入解析(Zod) │ │ └─ inputSchema.safeParse(input) → 类型验证 │ │ │ │ Step 3: 自定义验证 │ │ └─ tool.validateInput?(input, context) │ │ │ │ Step 4: Pre-Tool 钩子 │ │ └─ runPreToolUseHooks(tool, input, context) │ │ ├─ 退出码 0: 成功,继续 │ │ ├─ 退出码 2: 阻塞,展示错误 │ │ └─ 其他: 展示给用户 │ │ │ │ Step 5: 权限检查 │ │ └─ hasPermissionsToUseTool(tool, input, context) │ │ ├─ behavior: 'allow' → 继续 │ │ ├─ behavior: 'deny' → 返回拒绝 │ │ └─ behavior: 'ask' → 弹出确认对话框 │ │ │ │ Step 6: 实际执行 │ │ └─ tool.call(input, context, canUseTool, ...) │ │ ├─ 成功 → ToolResult │ │ └─ 失败 → ToolError │ │ │ │ Step 7: Post-Tool 钩子 │ │ └─ runPostToolUseHooks(tool, input, result) │ │ │ └─────────────────────────────────────────────────────────────┘
|
3.1 Step 1: 工具查找
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| export function findToolByName( name: string, tools: Tools, ): Tool<unknown, unknown, ToolProgressData> | undefined { const exact = tools.find(t => t.name === name) if (exact) return exact for (const tool of tools) { if (tool.aliases?.includes(name)) { return tool } } if (name.startsWith('mcp__')) { return findMcpTool(name, tools) } return undefined }
|
3.2 Step 2: 输入解析
1 2 3 4 5 6 7 8 9 10 11 12
| const parseResult = tool.inputSchema.safeParse(input) if (!parseResult.success) { const formattedError = formatZodValidationError(parseResult.error) return { type: 'tool_result', content: formattedError, is_error: true, tool_use_id: toolUseId, } } const validatedInput = parseResult.data
|
3.3 Step 3: 自定义验证
某些工具需要额外的验证逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| async validateInput(input, context): Promise<ValidationResult> { if (!await fileExists(input.file_path)) { return { result: false, message: 'File does not exist', errorCode: 1 } } const stats = await stat(input.file_path) if (stats.size > 1_000_000_000) { return { result: false, message: 'File too large', errorCode: 2 } } const readState = context.readFileState.get(input.file_path) if (!readState) { return { result: false, message: 'Must read file before editing', errorCode: 3 } } const currentMtime = (await stat(input.file_path)).mtimeMs if (currentMtime > readState.timestamp) { return { result: false, message: 'File was modified after reading', errorCode: 4 } } return { result: true } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| export async function runPreToolUseHooks( tool: Tool, input: unknown, context: ToolUseContext, ): Promise<HookResult> { const hooks = getHooksForTool(tool.name, context) for (const hook of hooks) { const result = await executeHook(hook, { tool_name: tool.name, tool_input: input, }) if (result.exitCode === 2) { return { blocked: true, message: result.stderr, modifiedInput: parseModifiedInput(result.stdout), } } } return { blocked: false } }
|
3.5 Step 5: 权限检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| export async function hasPermissionsToUseTool( tool: Tool, input: unknown, context: ToolUseContext, ): Promise<PermissionResult> { const denyResult = checkDenyRules(tool.name, input, context) if (denyResult) return { behavior: 'deny', ...denyResult } if (tool.checkPermissions) { const toolResult = await tool.checkPermissions(input, context) if (toolResult.behavior !== 'passthrough') { return toolResult } } if (context.permissionContext.mode === 'bypassPermissions') { return { behavior: 'allow', decisionReason: { type: 'mode' } } } const allowResult = checkAllowRules(tool.name, input, context) if (allowResult) return { behavior: 'allow', ...allowResult } return { behavior: 'ask', message: generatePermissionMessage(tool, input), suggestions: generateSuggestions(tool, input), } }
|
3.6 Step 6: 实际执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const startTime = Date.now() try { const result = await tool.call( validatedInput, context, canUseTool, parentMessage, onProgress, ) const durationMs = Date.now() - startTime addToToolDuration(durationMs) return { type: 'tool_result', content: result.content, tool_use_id: toolUseId, } } catch (error) { const classifiedError = classifyToolError(error) return { type: 'tool_result', content: `Error: ${classifiedError}`, is_error: true, tool_use_id: toolUseId, } }
|
3.7 Step 7: Post-Tool 钩子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| export async function runPostToolUseHooks( tool: Tool, input: unknown, result: ToolResult, context: ToolUseContext, ): Promise<void> { const hooks = getPostToolUseHooks(tool.name, context) for (const hook of hooks) { await executeHook(hook, { tool_name: tool.name, tool_input: input, tool_result: result.content, tool_result_is_error: result.is_error, }) } }
|
四、工具延迟加载
Claude Code 有 48+ 个内置工具。如果每次 API 调用都把所有工具定义发给模型,会浪费大量 token。
4.1 延迟加载设计
1 2 3 4 5 6
| { shouldDefer: true, alwaysLoad: false, searchHint: "notebook" }
|
当模型需要使用延迟加载的工具时:
1 2 3 4 5 6 7 8 9 10 11
| 模型看到: "The following tools are available but deferred: NotebookEdit, ..."
模型调用: ToolSearch({ query: "notebook" })
返回: NotebookEdit 的完整 schema 和使用说明
模型调用: NotebookEdit({ ... })
|
Token 节省:
- 默认情况下,48 个工具的 schema 约 15000 tokens
- 延迟加载后,初始提示词只包含核心工具,约 5000 tokens
- 节省约 66% 的工具相关 token
五、工具结果处理
5.1 结果大小限制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const TOOL_RESULT_PERSIST_THRESHOLD_CHARS = 20_000
async function processToolResultBlock( tool: Tool, result: string, toolUseID: string, ): Promise<ToolResultBlockParam> { if (result.length > TOOL_RESULT_PERSIST_THRESHOLD_CHARS) { const filePath = getToolResultPath(toolUseID) await writeFile(filePath, result) const preview = result.slice(0, 4096) return { content: `${preview}\n\n[Output saved to ${filePath}. Use Read tool to view full output.]`, tool_use_id: toolUseID, } } return { content: result, tool_use_id: toolUseID } }
|
5.2 文件读取缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class FileStateCache { private cache = new Map<string, { timestamp: number content: string offset?: number limit?: number }>() get(path: string) { return this.cache.get(path) } set(path: string, state: { timestamp: number, content: string }) { this.cache.set(path, state) } validate(path: string): boolean { const cached = this.cache.get(path) if (!cached) return false const currentMtime = statSync(path).mtimeMs return currentMtime <= cached.timestamp } }
|
六、关键设计原则
6.1 自描述工具
每个工具通过接口暴露所有必要信息,框架无需了解内部:
1 2 3 4 5 6
| const isReadOnly = tool.isReadOnly?.(input) const isDestructive = tool.isDestructive?.(input)
tool.checkPermissions(input, context)
|
6.2 验证前置
输入验证在权限检查之前:
1
| 输入 → Zod 解析 → 自定义验证 → Pre-Tool 钩子 → 权限检查 → 执行
|
这确保了权限检查不会因为无效输入而触发。
6.3 钩子可扩展
钩子系统允许用户在任何阶段注入自定义逻辑:
1 2 3 4 5 6 7 8 9
| { "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "echo 'Bash called'" }] }] } }
|
七、关键源文件索引
| 文件 |
行数 |
职责 |
src/Tool.ts |
~792 |
Tool 类型定义和构建器 |
src/tools.ts |
~389 |
工具发现和注册 |
src/services/tools/toolExecution.ts |
~1500 |
执行管道 |
src/services/tools/toolOrchestration.ts |
~200 |
并行/串行策略 |
src/services/tools/toolHooks.ts |
~300 |
钩子执行 |
src/utils/toolResultStorage.ts |
~200 |
结果存储 |
src/utils/fileStateCache.ts |
~100 |
文件状态缓存 |
八、总结
Claude Code 的工具系统设计体现了几个核心原则:
- 接口驱动:统一的 Tool 接口,框架无需了解实现
- 管道模式:七步执行管道,每步职责清晰
- 延迟加载:减少初始 token 消耗
- 钩子扩展:用户可在任意阶段注入逻辑
- 结果管理:自动处理大型结果
这个设计使得添加新工具变得简单——只需实现 Tool 接口,框架会自动处理验证、权限、执行和结果处理。
系列文章导航: