Claude Code 工具系统深度剖析:从静态注册到动态发现

Claude Code 工具系统深度剖析:从静态注册到动态发现

引言:AI Agent 的"手"有多灵活?

想象这样一个场景:

你让 AI 助手"帮我查询 GitHub issue #123"。传统做法是:开发者提前把 GitHub API 封装成工具,硬编码在 Agent 的工具列表里。但这带来一个致命问题:

如果用户有 50 个服务(GitHub、Slack、Jira、Notion...),难道要把 50×10=500 个工具定义都塞进 Prompt?

  • ❌ Token 消耗爆炸(每个工具 ~200 tokens,共 100k tokens)
  • ❌ 模型混淆(工具太多,容易选错)
  • ❌ 扩展性差(新服务需要重新编译)

Claude Code 的解决方案是:工具不再是静态的"配置文件",而是可动态发现和加载的"插件市场"

本文将深入剖析这套系统:

  1. 工具的统一抽象:43 个内置工具如何统一注册
  2. Tool Search:让 Agent 自己"搜索"需要的工具
  3. Deferred Loading:按需加载,节省 80% tokens
  4. Agent 感知机制:如何让 Agent "看到" 延迟工具列表

一、工具系统的三层架构:定义、注册、执行

1.1 工具定义:JSONSchema + Handler

Claude Code 中的工具不是简单的函数,而是完整的能力描述

typescript 复制代码
// src/Tool.ts

interface Tool {
  name: string                      // 工具名称
  description: string               // 功能描述(让模型知道"何时用")
  input_schema: JSONSchema          // 参数约束(类型、必填项、格式)
  handler: ToolHandler              // 执行逻辑

  // 权限和特性
  canUseTool?: CanUseToolFn        // 权限检查钩子
  isReadOnly?: boolean              // 是否只读(影响并发策略)
  isConcurrencySafe?: boolean       // 能否并发调用

  // 延迟加载相关
  shouldDefer?: boolean             // 是否延迟加载
  alwaysLoad?: boolean              // 是否必须立即加载
  searchHint?: string               // 搜索提示(帮助 ToolSearch 匹配)

  // MCP 相关
  isMcp?: boolean                   // 是否来自 MCP 服务器
}

关键洞察:工具不仅包含"怎么执行"(handler),更重要的是包含"何时用"(description)和"如何约束"(input_schema)。这三者缺一不可。

1.2 实例:FileReadTool

让我们看一个真实工具的完整定义:

typescript 复制代码
// src/tools/FileReadTool/FileReadTool.ts

export const FileReadTool = buildTool({
  name: "Read",

  // 功能描述(让模型知道何时用)
  description: `Reads a file from the filesystem.
  - Can read text files, images (PNG, JPG), PDFs, and Jupyter notebooks
  - Results limited to 2000 lines by default
  - Support offset/limit for large files`,

  // 参数约束
  inputSchema: z.object({
    file_path: z.string().describe("Absolute path to the file"),
    offset: z.number().optional(),
    limit: z.number().optional()
  }),

  // 执行逻辑
  async call(input, context) {
    const { file_path, offset = 0, limit = 2000 } = input

    // 1. 权限检查(是否允许读取此文件?)
    const permission = await checkFilePermission(file_path, context)
    if (!permission.allowed) {
      return { data: { error: permission.reason } }
    }

    // 2. 文件类型检测
    const ext = path.extname(file_path)
    if (['.png', '.jpg'].includes(ext)) {
      return readImage(file_path)  // 图片转 base64
    }
    if (ext === '.pdf') {
      return readPDF(file_path)    // PDF 提取文本
    }

    // 3. 文本文件读取(带缓存)
    if (context.readFileState.has(file_path)) {
      return { data: context.readFileState.get(file_path) }
    }

    const content = await fs.readFile(file_path, 'utf-8')
    const lines = content.split('\n').slice(offset, offset + limit)

    context.readFileState.set(file_path, lines)
    return { data: { content: lines.join('\n') } }
  },

  // 特性标记
  isReadOnly: true,           // 只读工具,可以并发
  isConcurrencySafe: true,    // 多个 Read 调用不冲突
  shouldDefer: false          // 常用工具,不延迟加载
})

设计亮点

  1. 多格式支持:不仅读文本,还能处理图片、PDF、Jupyter Notebook
  2. 分页机制:大文件只读取指定范围(避免 token 爆炸)
  3. 缓存优化readFileState 缓存避免重复读取
  4. 权限检查:内置文件路径权限验证

1.3 工具注册表:统一的能力目录

所有工具在启动时注册到全局注册表:

typescript 复制代码
// src/constants/tools.ts

function getAllTools(): Tool[] {
  return [
    // 文件操作(4 个)
    FileReadTool,
    FileEditTool,
    FileWriteTool,
    GlobTool,

    // 代码搜索(2 个)
    GrepTool,
    LSPTool,

    // 命令执行(2 个)
    BashTool,
    PowerShellTool,

    // 网络请求(2 个)
    WebFetchTool,
    WebSearchTool,

    // 多代理(3 个)
    AgentTool,
    TaskCreateTool,
    TaskUpdateTool,

    // MCP 集成(2 个)
    MCPTool,
    ListMcpResourcesTool,

    // 元工具(1 个)
    ToolSearchTool,  // ← 用于搜索其他工具!

    // ... 共 43 个内置工具
  ]
}

工具分类

类别 数量 代表工具 作用
文件操作 7 Read, Edit, Write, Glob, Grep 代码库探索和修改
命令执行 2 Bash, PowerShell 运行测试、安装依赖
网络请求 2 WebFetch, WebSearch 获取最新信息
多代理 5 Agent, Task, Brief 子任务分发
元能力 3 ToolSearch, AskUser, EnterPlanMode 工作流控制

二、核心挑战:如何避免"工具爆炸"?

2.1 问题定义

假设你配置了 10 个 MCP 服务器,每个提供 10 个工具:

复制代码
10 个服务器 × 10 个工具 = 100 个工具
100 个工具 × 200 tokens/工具 = 20,000 tokens

加上 43 个内置工具:

ini 复制代码
43 × 200 = 8,600 tokens
总计:28,600 tokens 仅用于工具定义!

影响

  1. Token 浪费:占据 200k 上下文的 14%
  2. 模型混淆:工具太多,选错概率增加
  3. 启动延迟:需要加载所有 MCP 服务器

2.2 传统方案的困境

方案 1:全部加载

复制代码
优点:模型能看到所有工具
缺点:Token 浪费、模型混淆

方案 2:手动选择

复制代码
优点:减少 token
缺点:用户需要提前知道要用哪些工具(违背 AI 助手的初衷)

方案 3:分组加载

arduino 复制代码
优点:按场景分组(如"开发工具组"、"办公工具组")
缺点:分组边界模糊、跨组协作困难

2.3 Claude Code 的方案:动态发现 + 延迟加载

核心思路

  1. 初始只加载"核心工具"(Read、Bash、ToolSearch 等)
  2. 其他工具"延迟加载"(deferred tools),只显示名称,不加载 schema
  3. Agent 通过 ToolSearch 按需搜索工具
  4. 搜索结果以 tool_reference 形式返回,API 自动加载完整 schema

三、ToolSearchTool:Agent 的"工具搜索引擎"

3.1 设计哲学

ToolSearchTool 本身也是一个工具 ,但它的特殊之处在于:它的输出不是数据,而是其他工具的 schema

typescript 复制代码
// Agent 对话示例
User: "帮我查询 Slack #engineering 频道的消息"

// Agent 第 1 步:搜索工具
Assistant: [调用 ToolSearchTool]
  tool_use: {
    query: "slack message",
    max_results: 5
  }

// ToolSearch 返回(tool_reference 格式)
tool_result: {
  type: "tool_result",
  content: [
    { type: "tool_reference", tool_name: "mcp__slack__send_message" },
    { type: "tool_reference", tool_name: "mcp__slack__list_channels" },
    { type: "tool_reference", tool_name: "mcp__slack__get_messages" }
  ]
}

// API 自动注入这些工具的完整 schema
// Agent 第 2 步:现在可以直接调用了
Assistant: [调用 mcp__slack__get_messages]
  tool_use: {
    channel: "#engineering",
    limit: 10
  }

关键机制

  • tool_reference 是 Anthropic API 的特性,告诉 API"这个工具现在需要了,请加载它"
  • API 自动从服务端拉取工具的完整 schema,注入到后续 context
  • Agent 无需关心加载细节,就像工具一直在那里

3.2 搜索算法:关键词匹配 + 评分排序

typescript 复制代码
// src/tools/ToolSearchTool/ToolSearchTool.ts

async function searchToolsWithKeywords(
  query: string,
  deferredTools: Tools,
  tools: Tools,
  maxResults: number
): Promise<string[]> {

  // Step 1: 快速路径 - 精确匹配
  const exactMatch = deferredTools.find(t =>
    t.name.toLowerCase() === query.toLowerCase()
  )
  if (exactMatch) {
    return [exactMatch.name]  // 直接返回
  }

  // Step 2: MCP 工具前缀匹配
  // 查询 "mcp__slack" 会匹配所有 Slack 工具
  if (query.startsWith('mcp__')) {
    const matches = deferredTools.filter(t =>
      t.name.toLowerCase().startsWith(query)
    )
    if (matches.length > 0) {
      return matches.slice(0, maxResults).map(t => t.name)
    }
  }

  // Step 3: 关键词分词
  const queryTerms = query.toLowerCase().split(/\s+/)

  // Step 4: 必需项/可选项分离
  const requiredTerms = queryTerms
    .filter(t => t.startsWith('+'))
    .map(t => t.slice(1))
  const optionalTerms = queryTerms
    .filter(t => !t.startsWith('+'))

  // Step 5: 预过滤(必需项必须全部匹配)
  let candidates = deferredTools
  if (requiredTerms.length > 0) {
    candidates = candidates.filter(tool => {
      const parsed = parseToolName(tool.name)  // 拆分工具名
      const description = getToolDescription(tool)
      return requiredTerms.every(term =>
        parsed.parts.includes(term) ||      // 名称包含
        description.includes(term) ||        // 描述包含
        tool.searchHint?.includes(term)      // 提示包含
      )
    })
  }

  // Step 6: 评分排序
  const scored = await Promise.all(
    candidates.map(async tool => {
      const parsed = parseToolName(tool.name)
      const description = await getToolDescription(tool)

      let score = 0
      for (const term of [...requiredTerms, ...optionalTerms]) {
        // 名称精确匹配(最高权重)
        if (parsed.parts.includes(term)) {
          score += parsed.isMcp ? 12 : 10  // MCP 工具权重更高
        }
        // 名称部分匹配
        else if (parsed.parts.some(part => part.includes(term))) {
          score += parsed.isMcp ? 6 : 5
        }
        // searchHint 匹配(人工标注,高信号)
        else if (tool.searchHint?.includes(term)) {
          score += 4
        }
        // 描述匹配(词边界匹配,避免误报)
        else if (/\\b${term}\\b/.test(description)) {
          score += 2
        }
      }

      return { name: tool.name, score }
    })
  )

  // Step 7: 返回 Top-N
  return scored
    .filter(item => item.score > 0)
    .sort((a, b) => b.score - a.score)
    .slice(0, maxResults)
    .map(item => item.name)
}

评分权重设计

匹配类型 权重 示例
工具名精确匹配(MCP) 12 "slack" → mcp__slack__send_message
工具名精确匹配(普通) 10 "read" → FileReadTool
工具名部分匹配(MCP) 6 "send" → mcp__slack__send_message
工具名部分匹配(普通) 5 "edit" → FileEditTool
searchHint 匹配 4 "jupyter" → NotebookEditTool
描述词边界匹配 2 "message" → description 中的 "send message"

为什么 MCP 工具权重更高?

  • MCP 工具通常是特定服务(如 Slack、GitHub),名称信号很强
  • 普通工具名称更通用(如 Read、Bash),需要结合描述判断

3.3 工具名称解析:处理两种命名风格

typescript 复制代码
// MCP 工具命名: mcp__server__action
// 示例: mcp__slack__send_message → ["slack", "send", "message"]

// 普通工具命名: CamelCase
// 示例: FileReadTool → ["file", "read", "tool"]

function parseToolName(name: string): {
  parts: string[]
  full: string
  isMcp: boolean
} {
  if (name.startsWith('mcp__')) {
    // MCP 工具:移除前缀,按双下划线和单下划线拆分
    const withoutPrefix = name.replace(/^mcp__/, '').toLowerCase()
    const parts = withoutPrefix.split('__').flatMap(p => p.split('_'))
    return {
      parts: parts.filter(Boolean),
      full: withoutPrefix.replace(/__/g, ' ').replace(/_/g, ' '),
      isMcp: true
    }
  }

  // 普通工具:CamelCase 拆分
  const parts = name
    .replace(/([a-z])([A-Z])/g, '$1 $2')  // CamelCase → 空格
    .replace(/_/g, ' ')
    .toLowerCase()
    .split(/\s+/)
    .filter(Boolean)

  return {
    parts,
    full: parts.join(' '),
    isMcp: false
  }
}

示例

sql 复制代码
parseToolName("mcp__slack__send_message")
→ {
  parts: ["slack", "send", "message"],
  full: "slack send message",
  isMcp: true
}

parseToolName("FileReadTool")
→ {
  parts: ["file", "read", "tool"],
  full: "file read tool",
  isMcp: false
}

3.4 查询模式:支持三种查询方式

模式 1:直接选择(select:)
typescript 复制代码
query: "select:Read,Edit,Grep"

// 结果:直接返回这些工具(如果存在)
matches: ["Read", "Edit", "Grep"]

适用场景:模型已经知道确切需要哪些工具(如压缩后恢复工具列表)

模式 2:关键词搜索
typescript 复制代码
query: "notebook jupyter"

// 搜索逻辑:
// - "notebook" 匹配 NotebookEditTool(名称)
// - "jupyter" 匹配描述中的 "Jupyter notebooks"
//
// 结果:
matches: ["NotebookEdit"]

适用场景:模型根据任务猜测需要的工具

模式 3:必需项查询(+前缀)
typescript 复制代码
query: "+slack send"

// 搜索逻辑:
// - "slack" 是必需项(+ 前缀),必须出现
// - "send" 是可选项,用于排序
//
// 只返回包含 "slack" 的工具,按 "send" 相关性排序
matches: [
  "mcp__slack__send_message",    // score: 12 + 6 = 18
  "mcp__slack__send_file",        // score: 12 + 6 = 18
  "mcp__slack__list_channels"     // score: 12 + 0 = 12
]

适用场景:模型知道服务(如 Slack),但不确定具体操作

3.5 缓存优化:避免重复计算

typescript 复制代码
// src/tools/ToolSearchTool/ToolSearchTool.ts

// 工具描述的 memoization
const getToolDescriptionMemoized = memoize(
  async (toolName: string, tools: Tools): Promise<string> => {
    const tool = findToolByName(tools, toolName)
    return tool.description  // 调用 tool.prompt() 生成完整描述
  },
  (toolName: string) => toolName  // 以工具名为 key
)

// 缓存失效检测
let cachedDeferredToolNames: string | null = null

function maybeInvalidateCache(deferredTools: Tools): void {
  const currentKey = deferredTools.map(t => t.name).sort().join(',')

  if (cachedDeferredToolNames !== currentKey) {
    // 工具列表变化了(如 MCP 服务器新连接),清空缓存
    getToolDescriptionMemoized.cache.clear()
    cachedDeferredToolNames = currentKey
  }
}

为什么需要缓存?

  • 工具描述生成较慢(需要渲染 prompt 模板)
  • 同一次搜索中,多个工具的描述会被重复读取
  • 缓存可以将搜索时间从 ~500ms 降低到 ~50ms

何时失效?

  • MCP 服务器新连接时(新工具加入)
  • 用户修改工具配置时

四、延迟加载(Deferred Loading):核心优化策略

4.1 什么是"延迟加载"?

传统加载

yaml 复制代码
API Request:
  system_prompt: "..."
  tools: [
    { name: "Read", description: "...", parameters: {...} },     ← 8600 tokens
    { name: "Edit", description: "...", parameters: {...} },
    { name: "Bash", description: "...", parameters: {...} },
    { name: "mcp__slack__send", description: "...", parameters: {...} },
    ... (共 143 个工具)
  ]
  messages: [...]

延迟加载

yaml 复制代码
API Request:
  system_prompt: "..."
  tools: [
    { name: "Read", description: "...", parameters: {...} },     ← 1500 tokens
    { name: "ToolSearch", description: "...", parameters: {...} }
  ]
  available_deferred_tools: [                                    ← 300 tokens
    "Edit", "Bash", "mcp__slack__send", ...
  ]
  messages: [...]

Token 对比

  • 传统:8600 tokens(工具定义)
  • 延迟:1800 tokens(核心工具 + 延迟工具名称)
  • 节省:~80%

4.2 延迟规则:哪些工具必须立即加载?

typescript 复制代码
// src/tools/ToolSearchTool/prompt.ts

function isDeferredTool(tool: Tool): boolean {
  // Rule 1: 明确标记 alwaysLoad 的工具(优先级最高)
  if (tool.alwaysLoad === true) return false

  // Rule 2: MCP 工具默认延迟(工作流特定)
  if (tool.isMcp === true) return true

  // Rule 3: ToolSearch 本身不能延迟(模型需要它来加载其他工具)
  if (tool.name === TOOL_SEARCH_TOOL_NAME) return false

  // Rule 4: Agent 工具不延迟(多代理编排的核心)
  if (tool.name === AGENT_TOOL_NAME) return false

  // Rule 5: Brief 工具不延迟(通信渠道,模型必须看到规则)
  if (tool.name === BRIEF_TOOL_NAME) return false

  // Rule 6: 明确标记 shouldDefer 的工具
  return tool.shouldDefer === true
}

默认不延迟的工具(共 13 个):

typescript 复制代码
[
  // 核心文件操作(高频使用)
  "Read", "Edit", "Write", "Glob", "Grep",

  // 命令执行(高频使用)
  "Bash", "PowerShell",

  // 元能力(工作流控制)
  "ToolSearch", "Agent", "AskUserQuestion",
  "EnterPlanMode", "ExitPlanMode",

  // 通信渠道
  "Brief"
]

默认延迟的工具(共 30+ 个):

typescript 复制代码
[
  // 低频文件操作
  "NotebookEdit", "LSP",

  // 网络请求(按需使用)
  "WebFetch", "WebSearch",

  // 任务管理(并非所有任务都需要)
  "TaskCreate", "TaskUpdate", "TaskGet",

  // MCP 工具(工作流特定)
  "mcp__slack__*", "mcp__github__*", ...
]

4.3 实际效果:Token 节省数据

场景 工具总数 立即加载 延迟加载 Token(立即) Token(延迟) 节省
纯本地开发 43 13 30 8,600 1,800 79%
+ 3 个 MCP 73 13 60 14,600 2,100 86%
+ 10 个 MCP 143 13 130 28,600 2,400 92%

关键洞察:MCP 工具越多,延迟加载的收益越明显。

4.4 核心机制:Agent 如何"看到"延迟工具列表

你可能会问:ToolSearch 本身没有调用大模型,那 Agent 是如何知道有哪些延迟工具可以搜索的?

答案是:Claude Code 通过两种机制将延迟工具列表传递给 Agent

机制 1:旧版 - <available-deferred-tools> 前置块(已淘汰)
typescript 复制代码
// src/services/api/claude.ts:1330-1344

// 在消息列表最前面插入工具列表
if (useToolSearch && !isDeferredToolsDeltaEnabled()) {
  const deferredToolList = tools
    .filter(t => deferredToolNames.has(t.name))
    .map(tool => tool.name)  // 只返回工具名,不含 schema
    .sort()
    .join('\n')

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

Agent 看到的内容

arduino 复制代码
User (meta): <available-deferred-tools>
WebSearch
NotebookEdit
TaskStop
mcp__slack__send_message
mcp__slack__list_channels
mcp__github__create_issue
</available-deferred-tools>

User: 帮我发送 Slack 消息到 #general 频道

问题

  • ❌ 每次 MCP 工具池变化,整个列表重新生成
  • ❌ 列表在消息最前面,变化会导致 KV Cache 全部失效
  • ❌ 无法增量更新
机制 2:新版 - deferred_tools_delta Attachment(当前方案)

核心思路 :不传递完整列表,只传递增量变化(delta)。

typescript 复制代码
// src/utils/toolSearch.ts:646-670

export function getDeferredToolsDelta(
  tools: Tools,
  messages: Message[],
): DeferredToolsDelta | null {

  // 1. 扫描历史消息,重建"已通知"列表
  const announced = new Set<string>()
  for (const msg of messages) {
    if (msg.attachment?.type === 'deferred_tools_delta') {
      for (const n of msg.attachment.addedNames) announced.add(n)
      for (const n of msg.attachment.removedNames) announced.delete(n)
    }
  }

  // 2. 计算当前延迟工具列表
  const deferred: Tool[] = tools.filter(isDeferredTool)
  const deferredNames = new Set(deferred.map(t => t.name))

  // 3. 计算增量
  const added = deferred.filter(t => !announced.has(t.name))
  const removed = [...announced].filter(n => !deferredNames.has(n))

  // 4. 无变化 → 返回 null(不生成 attachment)
  if (added.length === 0 && removed.length === 0) return null

  return {
    addedNames: added.map(t => t.name),
    removedNames: removed
  }
}

转换为 <system-reminder> 消息src/utils/messages.ts:4178-4192):

typescript 复制代码
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 (their MCP server disconnected). Do not search for them --- ToolSearch will return no match:\n${attachment.removedNames.join('\n')}`,
    )
  }

  return wrapMessagesInSystemReminder([
    createUserMessage({ content: parts.join('\n\n'), isMeta: true }),
  ])
}

Agent 看到的完整流程

xml 复制代码
=== 第 1 轮(启动时)===
System: [ToolSearch 工具定义]
        Deferred tools appear by name in <system-reminder> messages.

<system-reminder>
The following deferred tools are now available via ToolSearch:
WebSearch
NotebookEdit
TaskStop
</system-reminder>

User: 帮我查询天气
Assistant: [使用核心工具完成任务]

=== 第 2 轮(连接 Slack MCP Server)===
<system-reminder>
The following deferred tools are now available via ToolSearch:
mcp__slack__send_message
mcp__slack__list_channels
mcp__slack__get_user_info
</system-reminder>

User: 发送消息到 Slack
Assistant: [调用 ToolSearch]
          <tool_use>
            <name>ToolSearch</name>
            <query>slack send</query>  ← Agent 自己抽取关键词
          </tool_use>

=== 第 3 轮(Slack 工具加载完成)===
User: <tool_result>
      [tool_reference: mcp__slack__send_message]
      </tool_result>

对比两种机制

特性 旧版(前置块) 新版(Delta Attachment)
位置 消息最前面 <system-reminder>
更新策略 完整列表重新生成 仅发送增量变化
KV Cache ❌ 变化导致全部失效 ✅ 保留(attachment 不影响 cache)
Token 效率 中等(完整列表) 高(仅增量)
实现复杂度 中(需要扫描历史)

新版优势

  1. KV Cache 友好:attachment 不影响主对话历史的 cache
  2. 增量更新:连接新 MCP Server 时,只通知新增工具
  3. 向后兼容:Agent 扫描历史 attachment 可重建完整列表

ToolSearch 提示信息

ToolSearch 工具的 description 会告诉 Agent 延迟工具在哪里:

typescript 复制代码
// src/tools/ToolSearchTool/prompt.ts:39-41

return deltaEnabled
  ? 'Deferred tools appear by name in <system-reminder> messages.'
  : 'Deferred tools appear by name in <available-deferred-tools> messages.'

这样 Agent 就知道:

  • ✅ 有哪些延迟工具可以搜索(从 <system-reminder> 中看到名称)
  • ✅ 如何生成合理的 query(根据工具名称抽取关键词)
  • ✅ ToolSearch 会返回什么(tool_reference 格式的工具 schema)

五、完整流程:从用户请求到工具执行

让我们用一个完整示例串联所有机制:

场景:用户要求发送 Slack 消息

css 复制代码
┌─────────────────────────────────────────────────────────────┐
│ User: "在 #engineering 频道发送消息说开发完成了"              │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ Step 1: Claude Code 构建 API 请求                           │
│                                                              │
│ {                                                            │
│   system: "You are Claude Code...",                         │
│   tools: [                                                   │
│     { name: "Read", ... },        // 13 个核心工具           │
│     { name: "ToolSearch", ... }                             │
│   ],                                                         │
│   available_deferred_tools: [                               │
│     "mcp__slack__send_message",   // 100+ 个延迟工具(仅名称)│
│     "mcp__slack__list_channels",                            │
│     ...                                                      │
│   ],                                                         │
│   messages: [                                                │
│     { role: "user", content: "在 #engineering..." }        │
│   ]                                                          │
│ }                                                            │
│                                                              │
│ Token 消耗:1,800 tokens(工具部分)                         │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ Step 2: API 返回(模型决策)                                 │
│                                                              │
│ {                                                            │
│   role: "assistant",                                         │
│   content: [                                                 │
│     {                                                        │
│       type: "tool_use",                                      │
│       name: "ToolSearch",                                    │
│       input: {                                               │
│         query: "slack send message",                         │
│         max_results: 5                                       │
│       }                                                      │
│     }                                                        │
│   ]                                                          │
│ }                                                            │
│                                                              │
│ 模型思考:"我需要发送 Slack 消息,但 Slack 工具不在加载列表,  │
│           先用 ToolSearch 搜索一下"                          │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ Step 3: Claude Code 执行 ToolSearch                          │
│                                                              │
│ 1. 提取查询:"slack send message"                           │
│ 2. 分词:["slack", "send", "message"]                       │
│ 3. 在 deferred tools 中搜索:                                │
│    - mcp__slack__send_message: score = 12 + 6 + 2 = 20      │
│    - mcp__slack__send_file: score = 12 + 6 + 0 = 18         │
│    - mcp__slack__list_channels: score = 12 + 0 + 2 = 14     │
│ 4. 返回 Top 3                                                │
│                                                              │
│ 执行时间:~50ms(有缓存)                                     │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ Step 4: 返回搜索结果(tool_reference 格式)                  │
│                                                              │
│ {                                                            │
│   role: "user",                                              │
│   content: [                                                 │
│     {                                                        │
│       type: "tool_result",                                   │
│       tool_use_id: "toolu_123",                              │
│       content: [                                             │
│         { type: "tool_reference", tool_name: "mcp__slack__send_message" },│
│         { type: "tool_reference", tool_name: "mcp__slack__send_file" },   │
│         { type: "tool_reference", tool_name: "mcp__slack__list_channels" }│
│       ]                                                      │
│     }                                                        │
│   ]                                                          │
│ }                                                            │
│                                                              │
│ 关键:tool_reference 告诉 API "这些工具需要加载了"             │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ Step 5: API 自动加载工具 schema(服务端)                    │
│                                                              │
│ API 收到 tool_reference 后:                                 │
│ 1. 从 Claude Code 拉取工具完整定义                           │
│ 2. 注入到当前 context                                        │
│ 3. 模型现在可以看到完整的 mcp__slack__send_message schema    │
│                                                              │
│ 新增 Token:~600 tokens(3 个工具的完整定义)                │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ Step 6: API 再次返回(调用 Slack 工具)                      │
│                                                              │
│ {                                                            │
│   role: "assistant",                                         │
│   content: [                                                 │
│     {                                                        │
│       type: "tool_use",                                      │
│       name: "mcp__slack__send_message",                      │
│       input: {                                               │
│         channel: "#engineering",                             │
│         text: "开发完成了 🎉"                                │
│       }                                                      │
│     }                                                        │
│   ]                                                          │
│ }                                                            │
│                                                              │
│ 模型思考:"现在我有 Slack 工具了,可以直接调用"                │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ Step 7: Claude Code 转发到 MCP Server                        │
│                                                              │
│ MCPServerManager.call('mcp__slack__send_message', {         │
│   channel: "#engineering",                                   │
│   text: "开发完成了 🎉"                                      │
│ })                                                           │
│   ↓                                                          │
│ Slack MCP Server.call('send_message', ...)                  │
│   ↓                                                          │
│ Slack API                                                    │
│   ↓                                                          │
│ 返回:{ ok: true, ts: "1234567890.123456" }                 │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ Step 8: 返回最终结果                                         │
│                                                              │
│ {                                                            │
│   role: "user",                                              │
│   content: [                                                 │
│     {                                                        │
│       type: "tool_result",                                   │
│       tool_use_id: "toolu_456",                              │
│       content: "Message sent successfully to #engineering"   │
│     }                                                        │
│   ]                                                          │
│ }                                                            │
│   ↓                                                          │
│ Assistant: "✅ 已在 #engineering 频道发送消息"                │
└─────────────────────────────────────────────────────────────┘

总 Token 消耗分析

阶段 Token 消耗 说明
传统方案(全部加载) 28,600 143 个工具 × 200 tokens
延迟加载方案
初始加载(13 个核心工具) 1,800 节省 26,800 tokens
ToolSearch 调用 150 查询 + 结果
按需加载(3 个 Slack 工具) 600 只加载需要的
总计 2,550 节省 91%

七、设计权衡与未来展望

7.1 设计权衡

优势
  1. Token 效率:节省 80-92% 工具定义 tokens
  2. 模型清晰度:工具少,选择更准确
  3. 可扩展性:MCP 生态无限扩展,不影响核心性能
  4. 启动速度:不需要等待所有 MCP Server 连接
劣势
  1. 额外一轮 API 调用:需要先 ToolSearch,再调用真实工具
  2. 搜索准确性:关键词匹配可能不精确(如模型用了错误关键词)
  3. 缓存依赖:首次查询较慢(~500ms),需要缓存优化

7.2 实际效果数据

测试场景:100 个 MCP 工具环境

指标 传统方案 延迟加载 改进
初始 Token 20,000 1,800 ↓ 91%
首次工具调用延迟 0ms +500ms ↑ 500ms
缓存后调用延迟 0ms +50ms ↑ 50ms
工具选择准确率 87% 92% ↑ 5%

关键洞察

  • Token 节省远大于延迟代价(500ms vs 18k tokens = $0.18)
  • 工具少反而提升准确率(模型不会在 100 个工具中选错)

7.3 未来优化方向

1. 智能预加载
typescript 复制代码
// 根据对话上下文预测可能需要的工具
async function predictTools(messages: Message[]): Promise<string[]> {
  const lastUserMessage = messages.findLast(m => m.role === 'user')

  // 如果提到 "Slack",预加载所有 Slack 工具
  if (/slack/i.test(lastUserMessage.content)) {
    return deferredTools.filter(t => t.name.includes('slack'))
  }

  // 如果提到 "GitHub",预加载 GitHub 工具
  if (/github/i.test(lastUserMessage.content)) {
    return deferredTools.filter(t => t.name.includes('github'))
  }

  return []
}
2. 语义搜索
typescript 复制代码
// 用 embedding 替代关键词匹配
async function searchToolsWithEmbedding(
  query: string,
  deferredTools: Tools
): Promise<string[]> {
  const queryEmbedding = await embed(query)

  const scores = await Promise.all(
    deferredTools.map(async tool => {
      const toolEmbedding = await embed(tool.description)
      const similarity = cosineSimilarity(queryEmbedding, toolEmbedding)
      return { name: tool.name, score: similarity }
    })
  )

  return scores
    .sort((a, b) => b.score - a.score)
    .slice(0, 5)
    .map(item => item.name)
}
3. 工具使用模式学习
typescript 复制代码
// 记录工具使用频率
const toolUsageStats = {
  "mcp__slack__send_message": 120,  // 用过 120 次
  "mcp__github__create_issue": 50,
  "mcp__notion__create_page": 5
}

// 高频工具自动提升为"核心工具"
function shouldPromoteToCoreTools(tool: Tool): boolean {
  return toolUsageStats[tool.name] > 100  // 用超过 100 次自动提升
}

总结:从静态到动态的范式转变

Claude Code 的工具系统代表了 AI Agent 设计的一次范式转变:

传统范式:静态配置

复制代码
开发者决定 → 硬编码工具列表 → Agent 只能用这些工具

问题

  • ❌ 扩展性差(新工具需要重新编译)
  • ❌ Token 浪费(所有工具都加载)
  • ❌ 用户受限(只能用开发者预设的工具)

新范式:动态发现

复制代码
Agent 根据任务 → 搜索需要的工具 → 按需加载 → 执行任务

优势

  • ✅ 无限扩展(工具插件化)
  • ✅ Token 高效(节省 80-92%)
  • ✅ Agent 自主(根据任务按需加载)

关键设计原则

  1. 统一抽象:43 个内置工具统一的 Tool 接口
  2. 延迟加载:只加载核心工具,其他按需搜索
  3. 智能搜索:关键词 + 评分,精准匹配工具
  4. 增量通知:Delta Attachment 机制保持 KV Cache 有效性

工程价值

对于 AI 应用开发者:

  • Token 成本优化:节省 80-92% 工具定义 tokens
  • 用户体验提升:工具少,模型选择更准确
  • 可扩展性:新工具接入无需修改核心代码

对于 Agent 研究:

  • 工具管理新思路:从"配置"到"发现"
  • 元能力设计:ToolSearch 是"工具的工具"
  • 增量状态管理:Delta 机制在保持上下文效率的同时传递工具列表

参考资料

源码文件

  • src/Tool.ts - 工具系统核心类型(792 行)
  • src/tools/ToolSearchTool/ToolSearchTool.ts - ToolSearch 实现(472 行)
  • src/tools/ToolSearchTool/prompt.ts - 延迟加载规则(122 行)
  • src/utils/toolSearch.ts - Delta 增量机制(getDeferredToolsDelta)
  • src/utils/messages.ts - Attachment 转换为 system-reminder

相关协议

相关推荐
IT_陈寒17 分钟前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro1 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax2 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH2 小时前
Koa和Express的区别
后端
MariaH2 小时前
Koa框架的使用
后端
luckdewei3 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某4 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy4 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom4 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
用户5191495848454 小时前
OpenSSL PKCS#12 PBMAC1 堆栈缓冲区溢出漏洞 (CVE-2025-11187) 分析与验证
人工智能·aigc