工具共享基础设施

📑 目录

在 Claude Code 的 40+ 个工具背后,存在一套精密的共享基础设施。这套基础设施负责工具间的公共能力复用、Schema 缓存优化、动态工具发现,以及整个工具池的组装与过滤。如果说单个工具是战场上的士兵,那么这些共享模块就是后勤补给线、情报网络和作战指挥部——它们不直接参与"战斗",却决定了整个工具系统能否高效运转。

本文将深入解析 tools/shared/tools/testing/toolSchemaCache.tsToolSearchTool/toolSearch.ts 以及 tools.ts 中的核心逻辑,揭示 Claude Code 如何在复杂的多工具环境中保持高性能与高可维护性。

一、tools/shared/:工具间的公共能力仓库

tools/shared/ 目录是 Claude Code 工具系统中公共逻辑的集中存放地。它的设计理念很明确:避免多个工具重复实现相同的业务逻辑,将通用能力抽象为可复用的模块。目前该目录主要包含两个核心文件:

1.1 gitOperationTracking.ts —— Git 操作统一追踪

当 Claude Code 执行 BashTool 运行 git commitgit pushgh pr create 时,系统需要感知这些操作以更新内部状态、触发分析事件(Analytics)和更新 OTLP 指标。gitOperationTracking.ts 就是负责这一任务的"观测站"。

// src/tools/shared/gitOperationTracking.ts, 行 28-35
function gitCmdRe(subcmd: string, suffix = ''): RegExp {
  return new RegExp(
    `\\bgit(?:\\s+-[cC]\\s+\\S+|\\s+--\\S+=\\S+)*\\s+${subcmd}\\b${suffix}`,
  )
}

这个正则构建函数的设计非常精巧:它不仅匹配 git commit,还能容忍 git -c commit.gpgsign=false commit 这类带有全局选项的命令。通过 \b 单词边界和 (?:\s+-[cC]\s+\S+|\s+--\S+=\S+)* 这段模式,它能够跳过任意数量的 -c key=val--git-dir=path 参数。

detectGitOperation() 函数(约行 145-210)会同时扫描命令字符串和输出内容,返回一个结构化的操作描述对象:

// src/tools/shared/gitOperationTracking.ts, 行 145-155
export function detectGitOperation(
  command: string,
  output: string,
): {
  commit?: { sha: string; kind: CommitKind }
  push?: { branch: string }
  branch?: { ref: string; action: BranchAction }
  pr?: { number: number; url?: string; action: PrAction }
} {

该函数不仅能识别常规的 Git 操作,还支持 gh pr create/edit/merge/comment/close/ready 等 GitHub CLI 操作,甚至能检测通过 curl 调用 REST API 创建 PR 的场景。当检测到 pr created 时,它会自动调用 linkSessionToPR() 将当前 Session 与创建的 PR 关联起来——这对于后续在对话中追踪 PR 状态至关重要。

1.2 spawnMultiAgent.ts —— 多 Agent 生成的共享逻辑

TeammateToolAgentTool 都需要创建新的 Agent 实例,两者的底层逻辑有大量重叠。spawnMultiAgent.ts 将这些公共逻辑抽取出来,提供了统一的 teammate 生成能力。

核心导出函数 spawnTeammate()(约行 690-700)是外部调用的统一入口:

// src/tools/shared/spawnMultiAgent.ts, 行 690-698
export async function spawnTeammate(
  config: SpawnTeammateConfig,
  context: ToolUseContext,
): Promise<{ data: SpawnOutput }> {
  return handleSpawn(config, context)
}

handleSpawn() 内部实现了三种 spawn 模式的选择逻辑:

  1. In-Process 模式:当 isInProcessEnabled() 返回 true 时,在同一个 Node.js 进程中通过 AsyncLocalStorage 创建 teammate
  2. Split-Pane 模式(默认):在 tmux 或 iTerm2 中创建分屏视图,leader 在左、teammates 在右
  3. Separate-Window 模式(Legacy):为每个 teammate 创建独立的 tmux 窗口

模型继承是另一个关键设计点。resolveTeammateModel() 函数处理 inherit 别名:

// src/tools/shared/spawnMultiAgent.ts, 行 70-78
export function resolveTeammateModel(
  inputModel: string | undefined,
  leaderModel: string | null,
): string {
  if (inputModel === 'inherit') {
    return leaderModel ?? getDefaultTeammateModel(leaderModel)
  }
  return inputModel ?? getDefaultTeammateModel(leaderModel)
}

这里有一个值得注意的 bugfix:早期版本中 'inherit' 被直接传递给 --model CLI 参数,导致模型报错"It may not exist or you may not have access"。现在的实现会正确将其解析为 leader 的模型名或 fallback 模型。

buildInheritedCliFlags() 函数则确保子 Agent 继承父级的重要配置——包括权限模式(--dangerously-skip-permissions)、模型选择(--model)、设置路径(--settings)和插件目录(--plugin-dir)。但有一个安全例外:当 plan_mode_required 为 true 时,子 Agent 不会继承 bypass permissions 标志,因为 Plan 模式在安全优先级上高于权限绕过。

二、tools/testing/:工具测试框架

测试框架位于 tools/testing/ 目录,目前核心是一个专门用于测试的工具:TestingPermissionTool

// src/tools/testing/TestingPermissionTool.tsx, 行 1-25
/**
 * This testing-only tool will always pop up a permission dialog when called by
 * the model.
 */
import { z } from 'zod/v4';
import type { Tool } from '../../Tool.js';
import { buildTool, type ToolDef } from '../../Tool.js';
import { lazySchema } from '../../utils/lazySchema.js';

const NAME = 'TestingPermission';
const inputSchema = lazySchema(() => z.strictObject({}));

这个工具的设计目的很明确:验证权限系统的端到端行为。它的关键特性包括:

  • 始终请求权限checkPermissions() 永远返回 { behavior: 'ask', message: 'Run test?' }
  • 仅在测试环境启用isEnabled() 返回 "production" === 'test',即只有在 NODE_ENV=test 时才会被注册到工具池中
  • 无副作用isReadOnly() 返回 true,call() 仅返回成功字符串
// src/tools/testing/TestingPermissionTool.tsx, 行 40-48
async checkPermissions() {
  // This tool always requires permission
  return {
    behavior: 'ask' as const,
    message: `Run test?`
  };
}

tools.ts 中,它的注册条件非常严格:

// src/tools.ts, 行 130
...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []),

这种设计保证了生产环境绝不会暴露测试工具,同时测试环境又能有一个"永远触发权限对话框"的受控目标。端到端测试可以利用它验证:

  • 权限对话框是否正确渲染
  • 用户允许/拒绝后工具是否正确执行/取消
  • 权限规则(alwaysAllow/alwaysDeny)是否按预期生效

三、Schema 缓存:toolSchemaCache.ts

工具 Schema 的渲染代价极高。在 Claude Code 中,所有 40+ 个工具的 JSON Schema 定义合计约 11K tokens,且这些 Schema 被放置在 system prompt 的 position 2 位置——这意味着任何微小的 Schema 变化都会导致整个工具块及下游所有内容的缓存失效。toolSchemaCache.ts 就是为了解决这个问题而存在的。

// src/utils/toolSchemaCache.ts, 行 1-20
import type { BetaTool } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'

// Session-scoped cache of rendered tool schemas. Tool schemas render at server
// position 2 (before system prompt), so any byte-level change busts the entire
// ~11K-token tool block AND everything downstream.
type CachedSchema = BetaTool & {
  strict?: boolean
  eager_input_streaming?: boolean
}

const TOOL_SCHEMA_CACHE = new Map<string, CachedSchema>()

这个缓存有两个核心设计原则:

1. Session 级别的作用域

缓存是一个模块级全局的 Map,在单次 Session 内持续有效。这意味着:

  • GrowthBook 功能开关在中途翻转(如 tengu_tool_peartengu_fgts不会导致已缓存的 Schema 失效
  • MCP 服务器重连后,新工具的 Schema 会走首次渲染路径,而已缓存的 built-in 工具不受影响

2. 显式清除接口

// src/utils/toolSchemaCache.ts, 行 16-20
export function getToolSchemaCache(): Map<string, CachedSchema> {
  return TOOL_SCHEMA_CACHE
}

export function clearToolSchemaCache(): void {
  TOOL_SCHEMA_CACHE.clear()
}

clearToolSchemaCache() 被设计为 auth.ts 等叶子模块可以安全调用,而不需要引入 api.ts(否则会因为 plans→settings→file→growthbook→config→bridgeEnabled→auth 的依赖链产生循环导入)。

这个简单的两函数接口背后蕴含着深刻的架构考量:缓存策略宁可保守也不要激进。因为一次不必要的缓存失效意味着 11K tokens 的重新传输——这在 latency 敏感的场景下代价极高。

四、工具搜索:ToolSearchTool

当 MCP 服务器接入后,Claude Code 的工具数量可能从 40 个暴涨到上百个。如果将所有工具的完整 Schema 都塞进 system prompt,上下文会被迅速耗尽。ToolSearchTool 就是解决这个问题的动态加载机制。

4.1 核心机制:Deferred Loading

isDeferredTool() 函数(位于 prompt.ts)定义了哪些工具需要延迟加载:

// src/tools/ToolSearchTool/prompt.ts, 行 55-75
export function isDeferredTool(tool: Tool): boolean {
  // Explicit opt-out via _meta['anthropic/alwaysLoad']
  if (tool.alwaysLoad === true) return false

  // MCP tools are always deferred (workflow-specific)
  if (tool.isMcp === true) return true

  // Never defer ToolSearch itself
  if (tool.name === TOOL_SEARCH_TOOL_NAME) return false

  // Fork-first experiment: Agent must be available turn 1
  if (feature('FORK_SUBAGENT') && tool.name === AGENT_TOOL_NAME) {
    // ...
  }

  return tool.shouldDefer === true
}

延迟加载遵循三条规则:

  1. MCP 工具默认延迟加载:它们是工作流特定的,不同用户接入的 MCP 服务器各不相同
  2. 显式 opt-out:通过 alwaysLoad: true_meta['anthropic/alwaysLoad'] 可以强制预加载
  3. 核心工具永不延迟ToolSearchTool 自身、AgentTool(在 fork 实验下)、BriefTool 等通信通道必须立即可用

4.2 搜索实现

ToolSearchTool.ts 实现了两种查询模式:

精确选择模式select:ToolNameA,ToolNameB

// src/tools/ToolSearchTool/ToolSearchTool.ts, 行 210-235
const selectMatch = query.match(/^select:(.+)$/i)
if (selectMatch) {
  const requested = selectMatch[1]!
    .split(',')
    .map(s => s.trim())
    .filter(Boolean)

  const found: string[] = []
  const missing: string[] = []
  for (const toolName of requested) {
    const tool =
      findToolByName(deferredTools, toolName) ??
      findToolByName(tools, toolName)
    if (tool) {
      if (!found.includes(tool.name)) found.push(tool.name)
    } else {
      missing.push(toolName)
    }
  }

关键词搜索模式:支持自然语言查询

// src/tools/ToolSearchTool/ToolSearchTool.ts, 行 245-260
const matches = await searchToolsWithKeywords(
  query,
  deferredTools,
  tools,
  max_results,
)

searchToolsWithKeywords() 的评分算法(约行 125-200)非常精巧:

匹配类型MCP 工具得分普通工具得分说明
名称部分精确匹配1210mcp__slack__send 中的 slack
名称部分包含匹配65send_message 中的 send
完整名称包含33兜底匹配
searchHint 匹配44人工标注的能力短语
描述词边界匹配22基于 \bterm\b 的正则匹配

这种多层级的评分机制确保了模型搜索 "slack" 时,Slack MCP 工具会排在最前面,而不是某个描述里偶然提到 "slack" 的 built-in 工具。

搜索结果以 tool_reference 块的形式返回给模型:

// src/tools/ToolSearchTool/ToolSearchTool.ts, 行 275-290
mapToolResultToToolResultBlockParam(content: Output, toolUseID: string) {
  return {
    type: 'tool_result',
    tool_use_id: toolUseID,
    content: content.matches.map(name => ({
      type: 'tool_reference' as const,
      tool_name: name,
    })),
  } as unknown as ToolResultBlockParam
}

Anthropic API 会在服务端将这些 tool_reference 展开为完整的工具定义,模型无需额外的往返即可调用这些工具。

4.3 阈值自动判断

toolSearch.ts 中的 isToolSearchEnabled() 函数是决定是否启用延迟加载的"大脑"。它支持三种模式:

  • tst(Tool Search Tool):始终启用延迟加载
  • tst-auto:仅当延迟工具的描述文本超过上下文窗口的 10%(可配置)时才启用
  • standard:完全禁用延迟加载
// src/utils/toolSearch.ts, 行 95-125
export function getToolSearchMode(): ToolSearchMode {
  // Kill switch: disable beta features for proxy gateways
  if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS)) {
    return 'standard'
  }

  const value = process.env.ENABLE_TOOL_SEARCH
  // auto:N syntax: auto:0 = always, auto:100 = never
  const autoPercent = value ? parseAutoPercentage(value) : null
  if (autoPercent === 0) return 'tst'
  if (autoPercent === 100) return 'standard'
  if (isAutoToolSearchMode(value)) return 'tst-auto'

  if (isEnvTruthy(value)) return 'tst'
  if (isEnvDefinedFalsy(value)) return 'standard'
  return 'tst' // default: always defer MCP and shouldDefer tools
}

tst-auto 模式的阈值检查优先使用精确的 token 计数 API(通过 countToolDefinitionTokens()),当 API 不可用时回退到字符数启发式(CHARS_PER_TOKEN = 2.5)。

五、工具池组装:assembleToolPool

工具池组装是整个工具系统的"总装车间"。Claude Code 的工具来源很复杂:built-in 工具、MCP 动态工具、Feature Flag 条件工具、环境变量开关工具等。assembleToolPool() 负责将这些来源统一合并为模型最终看到的工具列表。

5.1 从 getAllBaseTools 到 getTools

getAllBaseTools()tools.ts 约行 160-210)是所有可能被注册的工具的"全量清单":

// src/tools.ts, 行 160-210
export function getAllBaseTools(): Tools {
  return [
    AgentTool,
    TaskOutputTool,
    BashTool,
    ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
    ExitPlanModeV2Tool,
    FileReadTool,
    // ... 约 40 个工具
    ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
  ]
}

注意几个条件注册的例子:

  • hasEmbeddedSearchTools() 为 true 时(原生构建内置了 bfs/ugrep),GlobToolGrepTool 被省略
  • process.env.USER_TYPE === 'ant' 控制的内部工具只在 Anthropic 内部环境出现
  • feature('AGENT_TRIGGERS') 等 Feature Flag 控制实验性工具

getTools() 在此基础上进行两层过滤:

  1. filterToolsByDenyRules():根据权限上下文中的 deny 规则剔除工具(包括 mcp__server 前缀规则可以批量屏蔽某个 MCP 服务器的所有工具)
  2. isEnabled():每个工具的自检逻辑(如 TestingPermissionTool 只在 NODE_ENV === 'test' 时启用)

5.2 assembleToolPool:最终的合并与去重

// src/tools.ts, 行 280-310
export function assembleToolPool(
  permissionContext: ToolPermissionContext,
  mcpTools: Tools,
): Tools {
  const builtInTools = getTools(permissionContext)

  // Filter out MCP tools that are in the deny list
  const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)

  // Sort each partition for prompt-cache stability, keeping built-ins as a
  // contiguous prefix. The server's claude_code_system_cache_policy places a
  // global cache breakpoint after the last prefix-matched built-in tool.
  const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
  return uniqBy(
    [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
    'name',
  )
}

这里有三个关键设计决策:

分区排序(Partition Sort)

工具数组被分为 built-in 和 MCP 两个分区,各自按名称排序后再拼接。这保证了 built-in 工具始终位于数组的前缀位置。为什么这很重要?因为 Anthropic 服务端的 claude_code_system_cache_policy 在最后一个前缀匹配的 built-in 工具后放置全局缓存断点。如果 flat sort 将 MCP 工具穿插进 built-in 之间,每次 MCP 工具变化都会破坏所有下游内容的缓存键。

去重优先级(Deduplication Priority)

uniqBy([...builtInTools, ...allowedMcpTools], 'name') 使用 lodash 的 uniqBy,它保留首次出现的元素。由于 built-in 工具排在 MCP 工具前面,这意味着 built-in 工具在命名冲突时具有更高的优先级——这是有意为之的安全设计。

MCP 过滤前置

filterToolsByDenyRules() 在 MCP 工具进入组装流程之前就将其过滤掉。这确保了被 deny 规则屏蔽的 MCP 工具既不会出现在 system prompt 中,也不会被 ToolSearchTool 搜索到——模型从头到尾都不知道这些工具的存在。

5.3 mergeAndFilterTools:REPL 环境的二次加工

flowchart TD
    A[getAllBaseTools] --> B[getTools]
    B -->|过滤 deny 规则 + isEnabled| C[builtInTools]
    D[MCP 服务器] --> E[mcpTools]
    E -->|filterToolsByDenyRules| F[allowedMcpTools]
    C --> G[assembleToolPool]
    F --> G
    G -->|分区排序 + uniqBy| H[assembledPool]
    I[initialTools] --> J[mergeAndFilterTools]
    H --> J
    J -->|分区排序 + uniqBy| K[mergedPool]
    K -->|COORDINATOR_MODE?| L[applyCoordinatorToolFilter]
    L --> M[最终工具列表]

mergeAndFilterTools()(位于 toolPool.ts)是 REPL 路径特有的二次加工步骤:

// src/utils/toolPool.ts, 行 35-55
export function mergeAndFilterTools(
  initialTools: Tools,
  assembled: Tools,
  mode: ToolPermissionContext['mode'],
): Tools {
  // Merge initialTools on top - they take precedence in deduplication.
  const [mcp, builtIn] = partition(
    uniqBy([...initialTools, ...assembled], 'name'),
    isMcpTool,
  )
  const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
  const tools = [...builtIn.sort(byName), ...mcp.sort(byName)]

  if (feature('COORDINATOR_MODE') && coordinatorModeModule) {
    if (coordinatorModeModule.isCoordinatorMode()) {
      return applyCoordinatorToolFilter(tools)
    }
  }
  return tools
}

initialTools 通常来自 REPL 启动时传入的 props(如 --tools 命令行参数指定的额外工具)。它们通过相同的 uniqBy 机制获得最高优先级——因为它们排在数组最前面。

当 Coordinator Mode 启用时,applyCoordinatorToolFilter() 会将工具列表严格限制为:

// src/constants/tools.ts, 行 75-80
export const COORDINATOR_MODE_ALLOWED_TOOLS = new Set([
  AGENT_TOOL_NAME,
  TASK_STOP_TOOL_NAME,
  SEND_MESSAGE_TOOL_NAME,
  SYNTHETIC_OUTPUT_TOOL_NAME,
])

这意味着在 Coordinator 模式下,Worker Agent 只能使用 Agent 管理、任务控制和消息发送工具——所有的文件操作、Shell 执行等"危险"工具都被强制移除。

六、Deferred Tools Delta:跨轮次的状态同步

当工具搜索启用时,还有一个精妙的状态同步机制:getDeferredToolsDelta()(位于 toolSearch.ts,约行 340-400)。

它的作用是在对话的每一轮之间追踪"哪些延迟加载的工具已经被宣布给模型"。通过扫描消息历史中的 deferred_tools_delta attachment,它可以计算出:

  • Added:本轮新出现的延迟工具(如新连接的 MCP 服务器)
  • Removed:从工具池中彻底消失的延迟工具(MCP 服务器断开)
// src/utils/toolSearch.ts, 行 340-360
export function getDeferredToolsDelta(
  tools: Tools,
  messages: Message[],
  scanContext?: DeferredToolsDeltaScanContext,
): DeferredToolsDelta | null {
  const announced = new Set<string>()
  // Scan prior deferred_tools_delta attachments
  for (const msg of messages) {
    if (msg.type !== 'attachment') continue
    if (msg.attachment.type !== 'deferred_tools_delta') continue
    for (const n of msg.attachment.addedNames) announced.add(n)
    for (const n of msg.attachment.removedNames) announced.delete(n)
  }
  // ... diff logic
}

scanContext 参数是一个调用点标识符(如 attachments_mainattachments_subagentcompact_full 等),它使得开发者可以在 BigQuery 中精确区分不同调用路径的统计特征——这对于定位跨轮次状态同步的 bug 至关重要。

七、总结

Claude Code 的工具共享基础设施是一个精心设计的分层系统:

层级模块职责
公共能力层tools/shared/Git 操作追踪、多 Agent 生成共享
质量保障层tools/testing/受控的测试工具,仅在测试环境暴露
缓存优化层toolSchemaCache.tsSession 级 Schema 缓存,避免 11K tokens 的重复渲染
动态发现层ToolSearchTool/ + toolSearch.tsDeferred loading、关键词搜索、阈值自动判断
总装集成层tools.ts + toolPool.ts工具池组装、去重、权限过滤、Coordinator 模式裁剪

这些基础设施不直接面向用户,却是 Claude Code 能够在 40+ built-in 工具 + 任意数量 MCP 工具的复杂环境下保持高性能、高可维护性的关键。特别是 Schema 缓存的分区排序策略、ToolSearch 的延迟加载机制,以及工具池组装的去重优先级设计,都体现了"缓存稳定性"和"安全边界"在工程实践中的深度考量。

理解这些底层支撑,不仅有助于我们阅读 Claude Code 的源码,也能为设计自己的 AI Agent 工具系统提供宝贵的架构参考。