6. Tool 抽象与工具注册机制

所属分组:工具系统

概述

Claude Code 的"工具系统"是连接大模型推理能力与外部世界(文件系统、Shell、网络、子 Agent 等)的桥梁。整套系统的核心抽象极其精炼——一个名为 Tool 的 TypeScript 类型加上一个 buildTool 工厂函数,便定义了所有工具必须遵循的契约。每一个具体工具(BashTool、FileReadTool、AgentTool 等)都是这一抽象的实现。

工具的注册机制同样简洁:tools.ts 中的 getAllBaseTools() 作为唯一可信源,集中维护当前环境下可用的所有内置工具;getTools() 在权限上下文中过滤这些工具;assembleToolPool() 则把内置工具与 MCP 工具合并成最终下发给模型的工具池。整个设计强调"声明式注册 + 多层过滤 + 分层装配",既保证了可扩展性,又确保权限与缓存策略的稳定。

本文将从 Tool.tstools.tstools/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 Schema:

export type ToolInputJSONSchema = {
  [x: string]: unknown
  type: 'object'
  properties?: {
    [x: string]: unknown
  }
}
// Type for any schema that outputs an object with string keys
export type AnyObject = z.ZodType<{ [key: string]: unknown }>

ToolInputJSONSchema 主要服务于 MCP 工具——MCP 协议本身用 JSON Schema 描述参数,无需先转换成 Zod。AnyObject 则是所有 Zod 入参 schema 的上界,保证工具入参一定是一个对象。

2. Tool 接口定义

Tool 类型是整个工具系统的核心。它是一个泛型类型,三个类型参数分别约束入参 schema、输出类型和进度数据类型:

export type Tool<
  Input extends AnyObject = AnyObject,
  Output = unknown,
  P extends ToolProgressData = ToolProgressData,
> = {
  readonly name: string
  readonly inputSchema: Input
  readonly inputJSONSchema?: ToolInputJSONSchema
  outputSchema?: z.ZodType<unknown>
  // ...
}

接口中方法可大致分为四类:

执行与校验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 为一组常用方法提供"安全失败"默认值:

const TOOL_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) => '',
}

export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
  return {
    ...TOOL_DEFAULTS,
    userFacingName: () => def.name,
    ...def,
  } as BuiltTool<D>
}

注意默认值是"fail-closed"的:isConcurrencySafe 默认 falseisReadOnly 默认 falsetoAutoClassifierInput 默认空字符串(在安全分类器中跳过)。任何工具若想改变这些行为,必须显式覆盖——这种保守默认值策略避免了"忘记实现某个方法就引入安全漏洞"的风险。

4. ToolUseContext:工具调用的运行时上下文

ToolUseContext 是每次 call() 都会接收的上下文对象,是工具访问运行时状态的唯一通道。它包含:

  • options:全局配置,包括 tools(完整工具池)、mcpClientsagentDefinitionsthinkingConfig
  • abortController:用于中断工具执行
  • readFileState:文件状态缓存,用于检测文件是否被外部修改
  • getAppState() / setAppState():访问与修改应用状态
  • setToolJSX:让工具临时接管整个 REPL 的 JSX 渲染(用于全屏交互)
  • messages:当前消息历史
  • agentId / agentType:子 Agent 标识
  • contentReplacementState:工具结果预算与替换状态
  • renderedSystemPrompt:父 Agent 冻结的 system prompt,用于 fork subagent 共享 prompt cache

contextModifier 字段允许非并发安全的工具在执行后修改上下文(例如 BashTool 修改 cwd)。

5. tools.ts 中的注册流程

getAllBaseTools() 是工具注册的"唯一可信源"。它返回一个数组,包含当前环境下所有可能启用的工具:

export function getAllBaseTools(): Tools {
  return [
    AgentTool,
    TaskOutputTool,
    BashTool,
    ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
    ExitPlanModeV2Tool,
    FileReadTool,
    FileEditTool,
    FileWriteTool,
    // ...
    ...(isEnvTruthy(process.env.ENABLE_LSP_TOOL) ? [LSPTool] : []),
    ...(isWorktreeModeEnabled() ? [EnterWorktreeTool, ExitWorktreeTool] : []),
    // ...
    ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
  ]
}

可以看到注册策略有三种形态:

  1. 静态导入:核心工具直接 import,无条件注册(BashTool、FileReadTool 等)。
  2. feature flag 条件注册:通过 feature('XXX') 控制的实验性工具,构建时被打包器死代码消除。
  3. 运行时环境变量注册:如 process.env.USER_TYPE === 'ant'process.env.ENABLE_LSP_TOOL 等,运行时决定是否启用。

部分工具用 require() 懒加载来打破循环依赖(如 TeamCreateToolSendMessageTool)。

6. getTools 与权限过滤

getTools(permissionContext) 是工具进入实际请求的入口。它的处理顺序很重要:

export const getTools = (permissionContext: ToolPermissionContext): Tools => {
  // Simple mode:仅返回 Bash/Read/Edit 等少数工具
  if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
    // ...
  }

  // 排除特殊工具(MCP 资源工具、SyntheticOutputTool)
  const specialTools = new Set([
    ListMcpResourcesTool.name,
    ReadMcpResourceTool.name,
    SYNTHETIC_OUTPUT_TOOL_NAME,
  ])
  const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name))

  // 按 deny 规则过滤
  let allowedTools = filterToolsByDenyRules(tools, permissionContext)

  // REPL 模式下隐藏被 REPL 包装的原始工具
  if (isReplModeEnabled()) { /* ... */ }

  // 按 isEnabled() 过滤
  const isEnabled = allowedTools.map(_ => _.isEnabled())
  return allowedTools.filter((_, i) => isEnabled[i])
}

filterToolsByDenyRules() 使用与运行时权限检查相同的匹配器,意味着 mcp__server 这样的前缀 deny 规则会在模型看到工具列表之前就把整个 server 的工具全部移除——而不仅仅是调用时拒绝。

7. assembleToolPool:内置工具与 MCP 工具的合并

assembleToolPool() 是 REPL 和 runAgent 共用的工具池装配函数:

export function assembleToolPool(
  permissionContext: ToolPermissionContext,
  mcpTools: Tools,
): Tools {
  const builtInTools = getTools(permissionContext)
  const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)

  const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
  return uniqBy(
    [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
    'name',
  )
}

注释中点明了排序的原因:服务端的 claude_code_system_cache_policy 会在最后一个内置工具之后放置全局缓存断点;如果 MCP 工具被插入到内置工具之间,所有下游缓存键都会失效。因此内置工具必须作为连续前缀,按 name 排序后拼接 MCP 工具。uniqBy 保留插入顺序,内置工具在同名冲突时优先。

8. tools/utils.ts 中的消息标记工具

tools/utils.ts 提供两个工具共用的辅助函数:

export function tagMessagesWithToolUseID(messages, toolUseID) {
  if (!toolUseID) return messages
  return messages.map(m => {
    if (m.type === 'user') {
      return { ...m, sourceToolUseID: toolUseID }
    }
    return m
  })
}

export function getToolUseIDFromParentMessage(parentMessage, toolName) {
  const toolUseBlock = parentMessage.message.content.find(
    block => block.type === 'tool_use' && block.name === toolName,
  )
  return toolUseBlock && toolUseBlock.type === 'tool_use'
    ? toolUseBlock.id
    : undefined
}

tagMessagesWithToolUseID 给工具执行期间产生的用户消息打上 sourceToolUseID 标记,让这些消息在工具解析前保持 transient,避免 UI 上出现重复的"is running"消息。这是工具与消息系统协作的一个细微但重要的设计。

9. UI 组件与 prompt 文本分离设计

观察 Tool 接口可以发现一个明显的设计:UI 渲染方法(renderToolUseMessage 等)和模型可见方法(descriptionprompt)被有意放在同一个接口里,但具体工具的实现通常把这两部分拆到不同文件:

  • prompt.ts:负责 prompt()description(),输出供模型阅读的文本
  • UI.tsx:负责 renderToolUseMessage 等渲染方法,输出供用户阅读的 React 节点
  • XxxTool.ts(x):负责 callcheckPermissionsinputSchema 等核心逻辑

这种"模型可见 / 用户可见 / 执行逻辑"三分离的设计带来三个好处:模型 prompt 可以独立调优而不影响 UI;UI 可以在终端不同模式(verbose / condensed / transcript)下复用同一份渲染逻辑;工具的核心逻辑可以在不引入 React 依赖的环境(如 SDK、agent runner)中被复用。

关键设计要点

  1. 单一接口契约:所有工具实现同一个 Tool 接口,无论它是内置 BashTool 还是远程 MCP 工具,对调用方而言完全等价。
  2. fail-closed 默认值buildTool 为并发安全、只读、破坏性等关键属性提供保守默认值,工具开发者必须显式声明才会"放宽"。
  3. 注册即过滤:工具注册分两层——getAllBaseTools 声明全部可能工具,getTools 在权限上下文中过滤;filterToolsByDenyRules 让 deny 规则在工具列表送达模型前就生效。
  4. 缓存友好的工具池排序assembleToolPool 强制内置工具作为连续前缀,避免 MCP 工具插入破坏服务端 prompt cache。
  5. UI 与 prompt 分离:每个工具目录下 prompt.ts / UI.tsx / XxxTool.ts 三件套分工清晰,模型可见文本、用户可见渲染、执行逻辑互不耦合。

与其他模块的关系

  • 权限系统Tool.checkPermissionsToolPermissionContext 协作,具体的 deny/allow 规则匹配在 utils/permissions/permissions.ts 中实现,Tool.preparePermissionMatcher 为带模式(如 Bash(git *))的规则提供细粒度匹配器。
  • 消息系统ToolUseContext.messagesappendSystemMessagetagMessagesWithToolUseID 让工具能向消息流追加内容,同时保持 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 合并四层处理后送达模型。理解这一抽象是阅读后续具体工具实现的前提——接下来的篇章将依次剖析各工具族如何填满这份契约。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐