Claude Code 工具延迟加载系统:完整设计与源码分析

基于 Claude Code npm 包 source map 泄露快照 (2026-03-31) 的源码分析。

适用于团队内部分享,理解 AI Agent 工具管理的工程范式。


一、设计哲学

源码注释(src/Tool.ts:438-442)定义了工具延迟加载的核心概念:

When true, this tool is deferred (sent with defer_loading: true) and requires ToolSearch to be used before it can be called.

核心原则:不要把工具注册表当静态配置——把它变成一个可搜索的服务,让模型成为自己的工具发现者。「不发送」比「发送后忽略」更高效:defer_loading 让工具 schema 完全不进入 prompt,而不是靠模型自觉不用。


二、问题:工具膨胀的乘法代价

Claude Code 是一个 agent 系统。每次调用 Claude API,必须把所有可用工具的完整 JSON Schema 发给模型。

Claude Code 面临的现实数据:

指标数量
内置工具44+
MCP 工具(用户可扩展)不限,实际可达 100+
每个工具 schema 大小数百到数千 token
每次对话轮数10-50+

代价是乘法效应:44 个工具的 schema 约占上下文窗口 5%-8%。用户安装 MCP 服务器后,工具数可轻松翻 3-5 倍,schema 占比膨胀到 20-30%,挤压真正的对话和代码内容。

更隐蔽的问题是 prompt cache 稳定性:Claude API 对 prompt 的前缀做 cache。工具列表的任何变动(MCP 服务器连接/断开、工具 schema 更新)都会让整个 cache 失效,导致下一轮调用重新计算全部 token——在长对话中这是巨大的成本。


三、解法架构总览

Claude Code 没有选择简单的「减少工具」或「手动分组」,而是设计了一套三层架构,核心思想是:让模型成为自己的工具发现者

Layer 1: 分类决策 哪些工具始终加载?哪些延迟? isDeferredTool() · shouldDefer · alwaysLoad Layer 2: API 协议 怎样告诉 API「这个工具存在但先不注入 prompt」? defer_loading: true → API 从 prompt 中剥离 schema tool_reference 内容块 → API 动态注入已发现工具的 schema Layer 3: 运行时发现 模型怎么找到需要的工具? ToolSearchTool: select: 精确选择 / 关键词搜索

四、Layer 1: 分类决策 —— 哪些工具该延迟

4.1 核心判定函数

文件:src/tools/ToolSearchTool/prompt.ts

export function isDeferredTool(tool: Tool): boolean {
  // ① 显式 opt-out:alwaysLoad = true 永远不延迟
  //    MCP 工具可通过 _meta['anthropic/alwaysLoad'] 设置
  if (tool.alwaysLoad === true) return false

  // ② MCP 工具一律延迟(用户/工作流特定,无法全局缓存)
  if (tool.isMcp === true) return true

  // ③ ToolSearch 自身不能延迟——鸡蛋问题
  if (tool.name === TOOL_SEARCH_TOOL_NAME) return false

  // ④ Agent 工具在 fork 模式下不延迟(第一轮就需要生成子 agent)
  if (feature('FORK_SUBAGENT') && tool.name === AGENT_TOOL_NAME) {
    if (m.isForkSubagentEnabled()) return false
  }

  // ⑤ Brief 是主要通信通道(KAIROS 模式),不能有额外延迟
  if (BRIEF_TOOL_NAME && tool.name === BRIEF_TOOL_NAME) return false

  // ⑥ SendUserFile 是文件传输通道,同理
  if (SEND_USER_FILE_TOOL_NAME && tool.name === SEND_USER_FILE_TOOL_NAME
      && isReplBridgeActive()) return false

  // ⑦ 最终:看工具自身的 shouldDefer 声明
  return tool.shouldDefer === true
}

设计原则

  1. alwaysLoad 优先级最高 —— 任何工具(包括 MCP 工具)都可以通过此标记强制始终加载
  2. MCP 工具默认延迟 —— MCP 工具是用户安装的外部扩展,Claude Code 控制不了数量
  3. 关键通道工具不延迟 —— ToolSearch、Agent、Brief 等在第一轮就可能被需要
  4. 内置工具按频率分类 —— 高频工具不延迟,低频工具设 shouldDefer: true

4.2 内置工具的分类

通过搜索源码中所有 shouldDefer: true 的声明:

始终加载(高频核心工具):

工具职责
BashToolShell 命令执行
FileReadTool文件读取
FileEditTool文件编辑
FileWriteTool文件创建/覆盖
GlobTool文件模式匹配
GrepTool内容搜索
AgentTool子 agent 生成
SkillTool技能执行
ToolSearchTool工具发现(自身)

延迟加载(shouldDefer: true):

工具searchHint
WebFetchTool(无,但 description 可搜)
WebSearchToolsearch the web for current information
NotebookEditTooledit Jupyter notebook cells (.ipynb)
LSPTool(无)
AskUserQuestionToolprompt the user with a multiple-choice question
TaskCreateTool(无)
TaskGetTool(无)
TaskUpdateTool(无)
TaskListTool(无)
TaskStopTool(无)
TaskOutputToolread output/logs from a background task
SendMessageTool(无)
TeamCreateToolcreate a multi-agent swarm team
TeamDeleteTooldisband a swarm team and clean up
EnterPlanModeTool(无)
ExitPlanModeV2Tool(无)
EnterWorktreeTool(无)
ExitWorktreeTool(无)
TodoWriteTool(无)
ConfigTool(无)
ListMcpResourcesToollist resources from connected MCP servers
ReadMcpResourceToolread a specific MCP resource by URI
CronCreateToolschedule a recurring or one-shot prompt
CronDeleteToolcancel a scheduled cron job
CronListToollist active cron jobs
RemoteTriggerToolmanage scheduled remote agent triggers

关键数据:大多数对话只需要始终加载的 7-9 个核心工具。延迟加载的 25+ 个内置工具加上所有 MCP 工具,只在需要时按需加载。


五、Layer 2: API 协议 —— defer_loading 和 tool_reference

5.1 工具 schema 构建

文件:src/utils/api.ts

每个工具的 schema 通过 toolToAPISchema() 构建。该函数有一个会话级缓存,工具名+schema 作为 key,避免重复序列化:

// 扩展的 BetaTool 类型
type BetaToolWithExtras = BetaTool & {
  strict?: boolean
  defer_loading?: boolean        // ← 延迟加载标记
  cache_control?: { type: 'ephemeral'; scope?: 'global' | 'org' }
  eager_input_streaming?: boolean
}

export async function toolToAPISchema(tool, options): Promise<BetaToolUnion> {
  // ① 会话级 cache:name + description + input_schema + strict + eager_input_streaming
  const cache = getToolSchemaCache()
  let base = cache.get(cacheKey)
  if (!base) {
    base = {
      name: tool.name,
      description: await tool.prompt({ ... }),
      input_schema: zodToJsonSchema(tool.inputSchema),
    }
    cache.set(cacheKey, base)
  }

  // ② 每次请求的 overlay(不修改缓存的 base)
  const schema: BetaToolWithExtras = {
    name: base.name,
    description: base.description,
    input_schema: base.input_schema,
    ...(base.strict && { strict: true }),
  }

  // ③ 按需添加 defer_loading
  if (options.deferLoading) {
    schema.defer_loading = true
  }

  return schema
}

为什么 defer_loading 不放在 cache 里:同一个工具在不同轮次可能需要/不需要延迟(比如被发现后就不再延迟)。将 defer_loading 作为 per-request overlay,避免 cache 被 defer 状态污染。

5.2 API 端行为

当 API 收到一个带 defer_loading: true 的工具 schema:

  1. 从实际 prompt 中剥离 —— 模型看不到完整 schema,不消耗 context token
  2. 工具名仍然可见 —— 模型知道这个工具存在(通过 <system-reminder><available-deferred-tools> 消息)
  3. 无法直接调用 —— 模型必须先通过 ToolSearch 获取完整 schema

5.3 tool_reference 内容块

当 ToolSearchTool 找到匹配工具时,返回特殊的 tool_reference 块:

// ToolSearchTool.mapToolResultToToolResultBlockParam()
if (content.matches.length === 0) {
  return {
    type: 'tool_result',
    tool_use_id: toolUseID,
    content: 'No matching deferred tools found',
  }
}

return {
  type: 'tool_result',
  tool_use_id: toolUseID,
  content: content.matches.map(name => ({
    type: 'tool_reference',     // ← 特殊内容块
    tool_name: name,
  })),
}

API 端行为:收到 tool_reference 后,API 会在当前轮次的模型可见上下文中动态注入对应工具的完整 schema。此后模型就可以正常使用 tool_use 调用该工具了。

5.4 Beta Header

使用 defer_loadingtool_reference 需要在 API 请求中附带 beta header:

// claude.ts:1174-1182
const toolSearchHeader = useToolSearch ? getToolSearchBetaHeader() : null
if (toolSearchHeader && getAPIProvider() !== 'bedrock') {
  if (!betas.includes(toolSearchHeader)) {
    betas.push(toolSearchHeader)
  }
}
// 不同 provider 使用不同 header:
// 1P/Foundry: 'advanced-tool-use'
// Vertex/Bedrock: 'tool-search-tool'

六、Layer 3: 运行时发现 —— ToolSearchTool

6.1 工具列表通知

模型需要知道有哪些延迟工具可用。有两种机制(正在从 A 迁移到 B):

机制 A:<available-deferred-tools> 消息注入

// claude.ts:1330-1345
if (useToolSearch && !isDeferredToolsDeltaEnabled()) {
  const deferredToolList = tools
    .filter(t => deferredToolNames.has(t.name))
    .map(formatDeferredToolLine)    // 只返回工具名
    .sort()
    .join('\n')

  if (deferredToolList) {
    messagesForAPI = [
      createUserMessage({
        content: `<available-deferred-tools>\n${deferredToolList}\n</available-deferred-tools>`,
        isMeta: true,
      }),
      ...messagesForAPI,
    ]
  }
}

这种方式在每次 API 调用前注入一条包含所有延迟工具名的合成消息。缺点是每次都发送完整列表,且变化时会破坏 cache。

机制 B:deferred_tools_delta 增量通知(正在推广)

// attachments.ts:836-841
maybe('deferred_tools_delta', () =>
  Promise.resolve(
    getDeferredToolsDeltaAttachment(
      toolUseContext.options.tools,
      toolUseContext.options.mainLoopModel,
      messages,
      scanContext,
    ),
  ),
),

增量机制只通知变化(新增/移除的工具),作为持久化的 attachment 消息存在于对话历史中。渲染为 <system-reminder>

// messages.ts:4178-4182
case 'deferred_tools_delta': {
  const parts: string[] = []
  if (attachment.addedLines.length > 0) {
    parts.push(
      `The following deferred tools are now available via ToolSearch:\n${attachment.addedLines.join('\n')}`,
    )
  }
  // ...
}

6.2 ToolSearchTool 的搜索实现

文件:src/tools/ToolSearchTool/ToolSearchTool.ts

两种查询模式

模式一:精确选择(select: 前缀)

const selectMatch = query.match(/^select:(.+)$/i)
if (selectMatch) {
  const requested = selectMatch[1]!
    .split(',')        // 支持逗号分隔多选
    .map(s => s.trim())
    .filter(Boolean)

  const found: string[] = []
  for (const toolName of requested) {
    // 先在延迟工具中找,再在全量工具中找
    const tool =
      findToolByName(deferredTools, toolName) ??
      findToolByName(tools, toolName)        // 已加载工具也能命中(幂等)
    if (tool && !found.includes(tool.name)) found.push(tool.name)
  }
  return buildSearchResult(found, query, deferredTools.length)
}

使用场景:模型已经知道工具名(从 <system-reminder> 中看到),直接选择。

模式二:关键词搜索

async function searchToolsWithKeywords(query, deferredTools, tools, maxResults) {
  const queryLower = query.toLowerCase().trim()

  // 快速路径:精确名称匹配
  const exactMatch =
    deferredTools.find(t => t.name.toLowerCase() === queryLower) ??
    tools.find(t => t.name.toLowerCase() === queryLower)
  if (exactMatch) return [exactMatch.name]

  // MCP 前缀匹配:mcp__slack → 所有 mcp__slack__* 工具
  if (queryLower.startsWith('mcp__') && queryLower.length > 5) {
    const prefixMatches = deferredTools
      .filter(t => t.name.toLowerCase().startsWith(queryLower))
      .slice(0, maxResults)
      .map(t => t.name)
    if (prefixMatches.length > 0) return prefixMatches
  }

  // 解析查询词(支持 + 前缀表示必选项)
  const queryTerms = queryLower.split(/\s+/).filter(term => term.length > 0)
  const requiredTerms: string[] = []    // +前缀的词
  const optionalTerms: string[] = []    // 普通词

  // 评分搜索
  const scored = await Promise.all(
    candidateTools.map(async tool => {
      const parsed = parseToolName(tool.name)  // 拆解 CamelCase 和 mcp__x__y
      const description = await getToolDescriptionMemoized(tool.name, tools)
      let score = 0
      for (const term of allScoringTerms) {
        // 工具名精确段匹配 — MCP +12 / 普通 +10
        if (parsed.parts.includes(term))
          score += parsed.isMcp ? 12 : 10
        // 工具名部分匹配 — MCP +6 / 普通 +5
        else if (parsed.parts.some(part => part.includes(term)))
          score += parsed.isMcp ? 6 : 5
        // searchHint 匹配 — +4(人工策划的能力短语,信号强于 description)
        if (hintNormalized && pattern.test(hintNormalized))
          score += 4
        // description 匹配 — +2(使用词边界 regex 避免误报)
        if (pattern.test(descNormalized))
          score += 2
      }
      return { name: tool.name, score }
    }),
  )

  return scored
    .filter(item => item.score > 0)
    .sort((a, b) => b.score - a.score)
    .slice(0, maxResults)
    .map(item => item.name)
}

评分体系设计思路

匹配类型分值举例
工具名精确段匹配(MCP)12slackmcp__slack__send_message
工具名精确段匹配(普通)10notebookNotebookEditTool
工具名部分段匹配(MCP)6slacmcp__slack__send_message
工具名部分段匹配(普通)5noteNotebookEditTool
searchHint 词边界匹配4jupyter → searchHint 含 Jupyter
description 词边界匹配2cron → description 含 cron

为什么 MCP 工具分值更高:MCP 工具命名为 mcp__server__action,其 server 段是最关键的区分信息。用户查 slack 时,期望优先命中 Slack 相关的 MCP 工具,而不是 description 中碰巧提到 Slack 的内置工具。

6.3 工具名解析

function parseToolName(name: string) {
  // MCP 工具:mcp__slack__send_message → ["slack", "send", "message"]
  if (name.startsWith('mcp__')) {
    const parts = name.replace(/^mcp__/, '').split('__').flatMap(p => p.split('_'))
    return { parts, full: '...', isMcp: true }
  }

  // 普通工具:NotebookEditTool → ["notebook", "edit", "tool"]
  const parts = name
    .replace(/([a-z])([A-Z])/g, '$1 $2')   // CamelCase 拆分
    .replace(/_/g, ' ')
    .toLowerCase()
    .split(/\s+/)
  return { parts, full: '...', isMcp: false }
}

6.4 性能优化

  1. description 记忆化getToolDescriptionMemoized 按工具名缓存,避免每次搜索重复调用 tool.prompt()
  2. cache 失效检测maybeInvalidateCache 检查延迟工具集合是否变化(MCP 服务器连接/断开)
  3. 预编译正则compileTermPatterns 对所有搜索词一次性编译词边界 regex,避免 tools×terms 次重复编译
  4. 并发评分Promise.all 并行计算所有候选工具的分数

七、完整调用链 —— 从用户输入到 defer_loading

7.1 时序图

调用链时序 用户输入 "帮我读取 Jupyter notebook" query.ts: queryLoop() 主循环每次迭代调用 deps.callModel() query/deps.ts: productionDeps() callModel 绑定到 queryModelWithStreaming claude.ts: queryModelWithStreaming() claude.ts: queryModel() ← useToolSearch 在此计算 ① isToolSearchEnabled(model, tools, ...) modelSupportsToolReference(model)? // haiku → false isToolSearchToolAvailable(tools)? // deny rule → false getToolSearchMode() → 'tst'|'tst-auto'|'standard' ② 预计算 deferredToolNames (Set<string>) for (const t of tools) if (isDeferredTool(t)) deferredToolNames.add(t.name) ③ 安全降级 if (deferredToolNames.size === 0) useToolSearch = false ④ 过滤工具列表 discoveredToolNames = extractDiscoveredToolNames(messages) filteredTools = tools.filter(t => !deferred(t) || discovered(t)) ⑤ 构建 tool schemas toolSchemas = filteredTools.map(t => toolToAPISchema(t, { deferLoading })) ⑥ 注入延迟工具列表(机制 A 或 B) messagesForAPI.unshift(<available-deferred-tools>...) // 或通过 deferred_tools_delta attachment 注入 ⑦ 发送 API 请求 messages: messagesForAPI tools: toolSchemas (含 defer_loading 标记) betas: [..., toolSearchHeader]

7.2 模型视角的交互流程

第 1 轮 模型收到: 系统提示 + 用户消息 工具:Bash, Read, Edit, Glob, Grep, Agent, Skill, ToolSearch (+ NotebookEdit 等标记 defer) <system-reminder> deferred: NotebookEditTool, WebFetchTool, ... 模型思考: 需要编辑 notebook,NotebookEditTool 在 deferred 列表里 模型输出: tool_use: ToolSearch query: "select:NotebookEditTool" ToolSearch 执行 返回 tool_result: content: [{ type: "tool_reference", tool_name: "NotebookEditTool" }] API 收到 tool_reference 后: → 动态注入 NotebookEditTool 完整 schema → 模型现在可以看到并调用它 第 2 轮 模型现在可以正常使用 NotebookEditTool 模型输出: tool_use: NotebookEditTool notebook_path: "./analysis.ipynb" cell_index: 3

7.3 query.ts 中 attachment 注入时机

deferred_tools_delta 作为 attachment 在查询循环中注入:

// query.ts:1580-1590
// 在每轮工具执行结束后、准备下一轮 API 调用前
for await (const attachment of getAttachmentMessages(
  null,
  updatedToolUseContext,
  null,
  queuedCommandsSnapshot,
  [...messagesForQuery, ...assistantMessages, ...toolResults],
  querySource,
)) {
  yield attachment
  toolResults.push(attachment)    // 注入到消息流中
}

getAttachmentMessages 内部会调用 getDeferredToolsDeltaAttachment,计算与上一次通知的 diff。


八、与上下文压缩的协同 —— 防止工具「失忆」

8.1 问题

Claude Code 的长对话使用多种压缩策略(auto-compact、reactive-compact、snip)来管理上下文。压缩会删除/合并历史消息。但 tool_reference 块就藏在那些消息里——如果被删了,之前发现的工具在后续轮次就不会被包含在 filteredTools 中。

8.2 解法:compact boundary 快照

// toolSearch.ts:545-592
export function extractDiscoveredToolNames(messages: Message[]): Set<string> {
  const discoveredTools = new Set<string>()

  for (const msg of messages) {
    // ① compact 边界消息携带压缩前的发现集合
    if (msg.type === 'system' && msg.subtype === 'compact_boundary') {
      const carried = msg.compactMetadata?.preCompactDiscoveredTools
      if (carried) {
        for (const name of carried) discoveredTools.add(name)
      }
      continue
    }

    // ② 扫描用户消息中的 tool_result → tool_reference
    if (msg.type !== 'user') continue
    const content = msg.message?.content
    if (!Array.isArray(content)) continue

    for (const block of content) {
      if (isToolResultBlockWithContent(block)) {
        for (const item of block.content) {
          if (isToolReferenceWithName(item)) {
            discoveredTools.add(item.tool_name)
          }
        }
      }
    }
  }
  return discoveredTools
}

流程

  1. 压缩发生前,当前已发现的工具集合被快照到 compactMetadata.preCompactDiscoveredTools
  2. 压缩后,旧的 tool_reference 消息被删除,但 compact boundary 消息保留了快照
  3. extractDiscoveredToolNames 同时从 boundary 快照和残存的 tool_reference 中重建发现集合

8.3 cache 稳定性保护

// claude.ts:1461-1467
// defer_loading 工具不参与 prompt cache 检测的 hash 计算
const toolsForCacheDetection = allTools.filter(
  t => !('defer_loading' in t && t.defer_loading),
)

原理defer_loading: true 的工具 schema 被 API 从实际 prompt 中剥离,不影响 cache key。如果把它们算进 hash,每次 MCP 服务器连接/断开都会误触发 cache 失效检测。

同样,assembleToolPool() 中的工具排序也考虑了 cache 稳定性:

// tools.ts: assembleToolPool()
// 内置工具排序后作为连续前缀
// MCP 工具排序后追加
// uniqBy 保证内置工具在名称冲突时优先
const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
return uniqBy(
  [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
  'name',
)

为什么不 flat sort:API 的 cache breakpoint 设在最后一个内置工具之后。如果 flat sort 让 MCP 工具插入到内置工具之间,内置工具的 cache 前缀就被破坏了。


九、模式选择与降级

9.1 三种运行模式

文件:src/utils/toolSearch.ts

ENABLE_TOOL_SEARCH模式行为
未设置(默认)tst始终延迟 MCP + shouldDefer 工具
true / auto:0tst同上
auto / auto:N(N=1-99)tst-auto延迟工具 token 超过上下文窗口 N% 才启用
false / auto:100standard不延迟,全量发送

9.2 自动模式的阈值检查

async function checkAutoThreshold(tools, getToolPermissionContext, agents, model) {
  // ① 优先用精确 token 计数(通过 API 计算,按工具集合缓存)
  const deferredToolTokens = await getDeferredToolTokenCount(
    tools, getToolPermissionContext, agents, model
  )
  if (deferredToolTokens !== null) {
    const threshold = contextWindow * (percentage / 100)  // 默认 10%
    return { enabled: deferredToolTokens >= threshold }
  }

  // ② 降级:字符数估算(1 token ≈ 2.5 字符)
  const chars = await calculateDeferredToolDescriptionChars(tools, ...)
  const charThreshold = threshold * 2.5
  return { enabled: chars >= charThreshold }
}

9.3 多级安全降级链

安全降级链 检查 1: CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS? → true → 强制 standard(kill switch) 检查 2: 模型支持 tool_reference? → Haiku → false → 回退 standard 检查 3: 第三方 API 代理? ANTHROPIC_BASE_URL 非一方 & 未显式设置 → 禁用 但 ENABLE_TOOL_SEARCH=true → 用户显式声明代理支持 → 保持启用 检查 4: ToolSearchTool 被 deny rule 禁用? → isToolSearchToolAvailable() === false → 回退 检查 5: 没有可延迟的工具 & 没有待连接的 MCP 服务器? → 回退 standard(无事可做)

9.4 isToolSearchEnabled 的完整决策链

export async function isToolSearchEnabled(model, tools, getToolPermissionContext, agents) {
  // 模型不支持 → false
  if (!modelSupportsToolReference(model)) return false

  // ToolSearch 被禁用 → false
  if (!isToolSearchToolAvailable(tools)) return false

  const mode = getToolSearchMode()
  switch (mode) {
    case 'tst':       return true
    case 'standard':  return false
    case 'tst-auto':
      const { enabled } = await checkAutoThreshold(tools, ...)
      return enabled
  }
}

关键点:这个函数每次 API 调用都重新执行(在 queryModel() 内部),而不是一次性决策。这意味着:


十、deferred_tools_delta 增量通知

10.1 动机

MCP 服务器可能在对话过程中动态连接/断开。<available-deferred-tools> 每次发完整列表,变化时破坏 cache。增量通知只传 diff。

10.2 实现

文件:src/utils/toolSearch.ts

export function getDeferredToolsDelta(tools, messages, scanContext) {
  // ① 从历史 attachment 消息重建已通知集合
  const announced = new Set<string>()
  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)
  }

  // ② 计算当前延迟工具集合
  const deferred = tools.filter(isDeferredTool)
  const deferredNames = new Set(deferred.map(t => t.name))
  const poolNames = new Set(tools.map(t => t.name))

  // ③ 计算 diff
  const added = deferred.filter(t => !announced.has(t.name))
  const removed: string[] = []
  for (const n of announced) {
    if (deferredNames.has(n)) continue
    // 如果工具仍在 pool 中但不再 deferred → 变成了直接加载 → 不报 removed
    if (!poolNames.has(n)) removed.push(n)
  }

  if (added.length === 0 && removed.length === 0) return null
  return { addedNames, addedLines, removedNames }
}

边界情况:如果一个工具从 deferred 变成了 always-load(比如 feature flag 改变),它仍然可用,只是加载方式变了。此时不应该通知模型「工具被移除了」,所以只有「从 pool 中完全消失」的工具才报 removed。

10.3 消息渲染

// messages.ts
case 'deferred_tools_delta': {
  const parts: string[] = []
  if (attachment.addedLines.length > 0) {
    parts.push(
      `The following deferred tools are now available via ToolSearch:\n${attachment.addedLines.join('\n')}`,
    )
  }
  if (attachment.removedNames.length > 0) {
    parts.push(
      `The following deferred tools are no longer available:\n${attachment.removedNames.join('\n')}`,
    )
  }
  // 渲染为 <system-reminder> 消息
}

十一、完整数据流图

工具注册表 (tools.ts) getAllBaseTools() + assembleToolPool() (内置 44+ 工具 + MCP 工具) isDeferredTool() 分类 始终加载 ~9 工具 延迟加载 ~25+ 所有 MCP 工具 API 请求构建 (claude.ts) 始终加载工具:完整 schema,无 defer_loading 延迟加载工具:schema + defer_loading: true (API 从 prompt 剥离) + ToolSearchTool 完整 schema 模型响应 看到 <system-reminder> 中的延迟工具列表 需要某个延迟工具? → 调用 ToolSearchTool query: "select:X" 或 "keyword" ToolSearchTool 执行 搜索算法: ① 精确名称匹配 ② MCP 前缀匹配 ③ + 必选词过滤 ④ 评分排序 name: 10-12 分 | hint: 4 分 | desc: 2 分 返回 tool_reference: [{ type: "tool_reference", tool_name: "X" }] API 处理 tool_reference 动态注入工具 X 的完整 schema 到模型可见上下文 下一轮 API 调用 extractDiscoveredToolNames(messages) → 从历史 tool_reference + compact boundary 重建已发现集合 filteredTools 包含: ① 全部始终加载工具 ② ToolSearchTool ③ 已发现的延迟工具 (未发现的延迟工具不发送)

十二、关键源码文件清单

src/tools/ToolSearchTool/
├── ToolSearchTool.ts             # 搜索实现(评分算法、精确选择、关键词搜索)
├── prompt.ts                     # isDeferredTool() 分类逻辑、工具列表格式化
└── constants.ts                  # TOOL_SEARCH_TOOL_NAME 常量

src/utils/
├── toolSearch.ts                 # 模式选择(tst/tst-auto/standard)、阈值检查、
│                                 # extractDiscoveredToolNames()、getDeferredToolsDelta()
├── api.ts                        # toolToAPISchema()(schema 构建 + defer_loading overlay)
├── attachments.ts                # getDeferredToolsDeltaAttachment()(增量通知)
└── messages.ts                   # deferred_tools_delta attachment 渲染

src/services/api/
└── claude.ts                     # queryModel()(useToolSearch 决策 + 工具过滤 + schema 构建)

src/
├── Tool.ts                       # Tool 接口(shouldDefer、alwaysLoad、searchHint 定义)
├── tools.ts                      # getAllBaseTools()、assembleToolPool()(工具注册 + 排序)
├── query.ts                      # 主循环中 attachment 注入时机
└── query/deps.ts                 # 依赖注入(callModel 绑定到 queryModelWithStreaming)

十三、可借鉴的设计模式

设计模式实现方式解决的问题适用场景
工具二分法shouldDefer + isDeferredTool()高频工具零延迟,低频工具不浪费 token任何工具数 > 15 的 Agent
API 级声明defer_loading: true工具存在但不占用 context需要 API 支持
模型自主发现ToolSearchTool + tool_reference零人工干预的按需加载插件/MCP 生态
评分排序搜索name/hint/desc 多层权重模糊查询也能找到正确工具大量外部工具
增量通知deferred_tools_delta动态工具池变化不破坏 cacheMCP 服务器动态连接
compact 快照preCompactDiscoveredTools压缩不丢失已发现状态长对话 Agent
排序保证 cache内置前缀 + MCP 后缀排序工具变化不破坏内置工具 cache混合工具源
多级降级模型/代理/kill switch/deny rule生产环境兼容性用户面向的 Agent
per-request overlaydefer_loading 不入 schema cache同一工具不同轮次可切换状态动态工具状态
每轮重新决策isToolSearchEnabledqueryModel 内调用适应运行时变化(模型切换、MCP 连接)多模型/多 agent

十四、核心洞察

  1. 不要把工具注册表当静态配置 —— 把它变成可搜索的服务,让模型成为自己的工具发现者

  2. 「不发送」比「发送后忽略」更高效 —— defer_loading 让工具 schema 完全不进入 prompt,而不是靠模型自觉不用

  3. cache 稳定性是隐藏的性能瓶颈 —— 工具列表变化导致的 cache 失效,在长对话中的 token 浪费远超工具 schema 本身的开销

  4. 增量优于全量 —— 从 <available-deferred-tools>(全量列表)到 deferred_tools_delta(增量通知)的演进,体现了对 cache 稳定性的持续优化

  5. 安全降级是生产必需品 —— 五层降级确保不管什么环境(Haiku 模型、第三方代理、用户禁用)都能正常工作,只是失去优化


文件来源:Claude Code npm 包 source map 泄露快照 (2026-03-31)

分析日期:2026-04-07