06-Claude Code的Tool抽象与工具注册机制

📅 2026/7/17 3:06:35
06-Claude Code的Tool抽象与工具注册机制
6. Tool 抽象与工具注册机制所属分组工具系统概述Claude Code 的工具系统是连接大模型推理能力与外部世界文件系统、Shell、网络、子 Agent 等的桥梁。整套系统的核心抽象极其精炼——一个名为Tool的 TypeScript 类型加上一个buildTool工厂函数便定义了所有工具必须遵循的契约。每一个具体工具BashTool、FileReadTool、AgentTool 等都是这一抽象的实现。工具的注册机制同样简洁tools.ts中的getAllBaseTools()作为唯一可信源集中维护当前环境下可用的所有内置工具getTools()在权限上下文中过滤这些工具assembleToolPool()则把内置工具与 MCP 工具合并成最终下发给模型的工具池。整个设计强调声明式注册 多层过滤 分层装配既保证了可扩展性又确保权限与缓存策略的稳定。本文将从Tool.ts、tools.ts、tools/utils.ts三个核心文件出发剖析工具抽象的形状、注册流程的关键函数以及 UI 组件与 prompt 文本分离的设计哲学。源码位置[Tool.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/Tool.ts)[tools.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools.ts)[tools/utils.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/utils.ts)核心实现分析1. ToolInputJSONSchema 与 AnyObject 类型工具的入参 schema 是工具与模型之间的契约。Claude Code 既支持标准的 Zod schema也兼容 MCP 工具直接提供的 JSON SchemaexporttypeToolInputJSONSchema{[x:string]:unknowntype:objectproperties?:{[x:string]:unknown}}// Type for any schema that outputs an object with string keysexporttypeAnyObjectz.ZodType{[key:string]:unknown}ToolInputJSONSchema主要服务于 MCP 工具——MCP 协议本身用 JSON Schema 描述参数无需先转换成 Zod。AnyObject则是所有 Zod 入参 schema 的上界保证工具入参一定是一个对象。2. Tool 接口定义Tool类型是整个工具系统的核心。它是一个泛型类型三个类型参数分别约束入参 schema、输出类型和进度数据类型exporttypeToolInputextendsAnyObjectAnyObject,Outputunknown,PextendsToolProgressDataToolProgressData,{readonlyname:stringreadonlyinputSchema:InputreadonlyinputJSONSchema?:ToolInputJSONSchema outputSchema?:z.ZodTypeunknown// ...}接口中方法可大致分为四类执行与校验call()是工具真正的执行入口validateInput()在权限检查之前做语义校验checkPermissions()决定是否需要向用户请求许可isReadOnly()/isDestructive()/isConcurrencySafe()描述工具的副作用特征影响并发与权限策略。模型可见信息description()返回发送给模型的工具描述文本prompt()返回附加到 system prompt 的工具使用指引searchHint/shouldDefer/alwaysLoad控制工具在 ToolSearch 延迟加载机制中的行为。UI 渲染renderToolUseMessage()渲染工具调用气泡输入侧renderToolResultMessage()渲染工具结果气泡renderToolUseProgressMessage()渲染运行中进度renderToolUseRejectedMessage()/renderToolUseErrorMessage()分别处理拒绝与错误情形renderGroupedToolUse()处理并行工具调用的分组渲染。辅助元数据userFacingName()/getToolUseSummary()/getActivityDescription()/toAutoClassifierInput()等为 UI、spinner、安全分类器提供短摘要。值得注意的细节是maxResultSizeChars当工具结果超过此字符数时会被持久化到磁盘模型只收到带文件路径的预览。Read工具被设为Infinity避免读文件→存盘→再读的循环。3. buildTool 工厂函数与默认值策略buildTool()是所有工具导出的统一通道。它通过TOOL_DEFAULTS为一组常用方法提供安全失败默认值constTOOL_DEFAULTS{isEnabled:()true,isConcurrencySafe:(_input?:unknown)false,isReadOnly:(_input?:unknown)false,isDestructive:(_input?:unknown)false,checkPermissions:(input,_ctx?)Promise.resolve({behavior:allow,updatedInput:input}),toAutoClassifierInput:(_input?:unknown),userFacingName:(_input?:unknown),}exportfunctionbuildToolDextendsAnyToolDef(def:D):BuiltToolD{return{...TOOL_DEFAULTS,userFacingName:()def.name,...def,}asBuiltToolD}注意默认值是fail-closed的isConcurrencySafe默认falseisReadOnly默认falsetoAutoClassifierInput默认空字符串在安全分类器中跳过。任何工具若想改变这些行为必须显式覆盖——这种保守默认值策略避免了忘记实现某个方法就引入安全漏洞的风险。4. ToolUseContext工具调用的运行时上下文ToolUseContext是每次call()都会接收的上下文对象是工具访问运行时状态的唯一通道。它包含options全局配置包括tools完整工具池、mcpClients、agentDefinitions、thinkingConfig等abortController用于中断工具执行readFileState文件状态缓存用于检测文件是否被外部修改getAppState()/setAppState()访问与修改应用状态setToolJSX让工具临时接管整个 REPL 的 JSX 渲染用于全屏交互messages当前消息历史agentId/agentType子 Agent 标识contentReplacementState工具结果预算与替换状态renderedSystemPrompt父 Agent 冻结的 system prompt用于 fork subagent 共享 prompt cachecontextModifier字段允许非并发安全的工具在执行后修改上下文例如 BashTool 修改 cwd。5. tools.ts 中的注册流程getAllBaseTools()是工具注册的唯一可信源。它返回一个数组包含当前环境下所有可能启用的工具exportfunctiongetAllBaseTools():Tools{return[AgentTool,TaskOutputTool,BashTool,...(hasEmbeddedSearchTools()?[]:[GlobTool,GrepTool]),ExitPlanModeV2Tool,FileReadTool,FileEditTool,FileWriteTool,// ......(isEnvTruthy(process.env.ENABLE_LSP_TOOL)?[LSPTool]:[]),...(isWorktreeModeEnabled()?[EnterWorktreeTool,ExitWorktreeTool]:[]),// ......(isToolSearchEnabledOptimistic()?[ToolSearchTool]:[]),]}可以看到注册策略有三种形态静态导入核心工具直接import无条件注册BashTool、FileReadTool 等。feature flag 条件注册通过feature(XXX)控制的实验性工具构建时被打包器死代码消除。运行时环境变量注册如process.env.USER_TYPE ant、process.env.ENABLE_LSP_TOOL等运行时决定是否启用。部分工具用require()懒加载来打破循环依赖如TeamCreateTool、SendMessageTool。6. getTools 与权限过滤getTools(permissionContext)是工具进入实际请求的入口。它的处理顺序很重要exportconstgetTools(permissionContext:ToolPermissionContext):Tools{// Simple mode仅返回 Bash/Read/Edit 等少数工具if(isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)){// ...}// 排除特殊工具MCP 资源工具、SyntheticOutputToolconstspecialToolsnewSet([ListMcpResourcesTool.name,ReadMcpResourceTool.name,SYNTHETIC_OUTPUT_TOOL_NAME,])consttoolsgetAllBaseTools().filter(tool!specialTools.has(tool.name))// 按 deny 规则过滤letallowedToolsfilterToolsByDenyRules(tools,permissionContext)// REPL 模式下隐藏被 REPL 包装的原始工具if(isReplModeEnabled()){/* ... */}// 按 isEnabled() 过滤constisEnabledallowedTools.map(__.isEnabled())returnallowedTools.filter((_,i)isEnabled[i])}filterToolsByDenyRules()使用与运行时权限检查相同的匹配器意味着mcp__server这样的前缀 deny 规则会在模型看到工具列表之前就把整个 server 的工具全部移除——而不仅仅是调用时拒绝。7. assembleToolPool内置工具与 MCP 工具的合并assembleToolPool()是 REPL 和 runAgent 共用的工具池装配函数exportfunctionassembleToolPool(permissionContext:ToolPermissionContext,mcpTools:Tools,):Tools{constbuiltInToolsgetTools(permissionContext)constallowedMcpToolsfilterToolsByDenyRules(mcpTools,permissionContext)constbyName(a:Tool,b:Tool)a.name.localeCompare(b.name)returnuniqBy([...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),name,)}注释中点明了排序的原因服务端的claude_code_system_cache_policy会在最后一个内置工具之后放置全局缓存断点如果 MCP 工具被插入到内置工具之间所有下游缓存键都会失效。因此内置工具必须作为连续前缀按 name 排序后拼接 MCP 工具。uniqBy保留插入顺序内置工具在同名冲突时优先。8. tools/utils.ts 中的消息标记工具tools/utils.ts提供两个工具共用的辅助函数exportfunctiontagMessagesWithToolUseID(messages,toolUseID){if(!toolUseID)returnmessagesreturnmessages.map(m{if(m.typeuser){return{...m,sourceToolUseID:toolUseID}}returnm})}exportfunctiongetToolUseIDFromParentMessage(parentMessage,toolName){consttoolUseBlockparentMessage.message.content.find(blockblock.typetool_useblock.nametoolName,)returntoolUseBlocktoolUseBlock.typetool_use?toolUseBlock.id:undefined}tagMessagesWithToolUseID给工具执行期间产生的用户消息打上sourceToolUseID标记让这些消息在工具解析前保持 transient避免 UI 上出现重复的is running消息。这是工具与消息系统协作的一个细微但重要的设计。9. UI 组件与 prompt 文本分离设计观察Tool接口可以发现一个明显的设计UI 渲染方法renderToolUseMessage等和模型可见方法description、prompt被有意放在同一个接口里但具体工具的实现通常把这两部分拆到不同文件prompt.ts负责prompt()、description()输出供模型阅读的文本UI.tsx负责renderToolUseMessage等渲染方法输出供用户阅读的 React 节点XxxTool.ts(x)负责call、checkPermissions、inputSchema等核心逻辑这种模型可见 / 用户可见 / 执行逻辑三分离的设计带来三个好处模型 prompt 可以独立调优而不影响 UIUI 可以在终端不同模式verbose / condensed / transcript下复用同一份渲染逻辑工具的核心逻辑可以在不引入 React 依赖的环境如 SDK、agent runner中被复用。关键设计要点单一接口契约所有工具实现同一个Tool接口无论它是内置 BashTool 还是远程 MCP 工具对调用方而言完全等价。fail-closed 默认值buildTool为并发安全、只读、破坏性等关键属性提供保守默认值工具开发者必须显式声明才会放宽。注册即过滤工具注册分两层——getAllBaseTools声明全部可能工具getTools在权限上下文中过滤filterToolsByDenyRules让 deny 规则在工具列表送达模型前就生效。缓存友好的工具池排序assembleToolPool强制内置工具作为连续前缀避免 MCP 工具插入破坏服务端 prompt cache。UI 与 prompt 分离每个工具目录下prompt.ts/UI.tsx/XxxTool.ts三件套分工清晰模型可见文本、用户可见渲染、执行逻辑互不耦合。与其他模块的关系权限系统Tool.checkPermissions与ToolPermissionContext协作具体的 deny/allow 规则匹配在utils/permissions/permissions.ts中实现Tool.preparePermissionMatcher为带模式如Bash(git *)的规则提供细粒度匹配器。消息系统ToolUseContext.messages、appendSystemMessage、tagMessagesWithToolUseID让工具能向消息流追加内容同时保持 UI 一致性。MCP 服务services/mcp/下的客户端把远程 MCP 工具适配成Tool接口通过mcpInfo字段标识再由assembleToolPool合并入工具池。Agent 系统AgentTool通过ToolUseContext.options.agentDefinitions获取子 Agent 定义子 Agent 又通过createSubagentContext派生出新的ToolUseContext。ToolSearch 延迟加载shouldDefer/alwaysLoad字段配合ToolSearchTool实现工具 schema 的按需加载节省 token。小结Claude Code 的工具系统用一份精炼的Tool接口 一个buildTool工厂 一组注册/过滤/装配函数支撑起了 Bash、文件、搜索、Agent、MCP、Web 等数十种工具。它的核心思想是统一契约 安全默认 分层过滤所有工具实现同一接口所有工具经过默认值补全、deny 规则过滤、isEnabled 过滤、MCP 合并四层处理后送达模型。理解这一抽象是阅读后续具体工具实现的前提——接下来的篇章将依次剖析各工具族如何填满这份契约。