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 的解决方案是:工具不再是静态的"配置文件",而是可动态发现和加载的"插件市场"。
本文将深入剖析这套系统:
- 工具的统一抽象:43 个内置工具如何统一注册
- Tool Search:让 Agent 自己"搜索"需要的工具
- Deferred Loading:按需加载,节省 80% tokens
- 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 // 常用工具,不延迟加载
})
设计亮点:
- 多格式支持:不仅读文本,还能处理图片、PDF、Jupyter Notebook
- 分页机制:大文件只读取指定范围(避免 token 爆炸)
- 缓存优化 :
readFileState缓存避免重复读取 - 权限检查:内置文件路径权限验证
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 仅用于工具定义!
影响:
- Token 浪费:占据 200k 上下文的 14%
- 模型混淆:工具太多,选错概率增加
- 启动延迟:需要加载所有 MCP 服务器
2.2 传统方案的困境
方案 1:全部加载
优点:模型能看到所有工具
缺点:Token 浪费、模型混淆
方案 2:手动选择
优点:减少 token
缺点:用户需要提前知道要用哪些工具(违背 AI 助手的初衷)
方案 3:分组加载
arduino
优点:按场景分组(如"开发工具组"、"办公工具组")
缺点:分组边界模糊、跨组协作困难
2.3 Claude Code 的方案:动态发现 + 延迟加载
核心思路:
- 初始只加载"核心工具"(Read、Bash、ToolSearch 等)
- 其他工具"延迟加载"(deferred tools),只显示名称,不加载 schema
- Agent 通过 ToolSearch 按需搜索工具
- 搜索结果以
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 效率 | 中等(完整列表) | 高(仅增量) |
| 实现复杂度 | 低 | 中(需要扫描历史) |
新版优势:
- KV Cache 友好:attachment 不影响主对话历史的 cache
- 增量更新:连接新 MCP Server 时,只通知新增工具
- 向后兼容: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 设计权衡
优势
- Token 效率:节省 80-92% 工具定义 tokens
- 模型清晰度:工具少,选择更准确
- 可扩展性:MCP 生态无限扩展,不影响核心性能
- 启动速度:不需要等待所有 MCP Server 连接
劣势
- 额外一轮 API 调用:需要先 ToolSearch,再调用真实工具
- 搜索准确性:关键词匹配可能不精确(如模型用了错误关键词)
- 缓存依赖:首次查询较慢(~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 自主(根据任务按需加载)
关键设计原则
- 统一抽象:43 个内置工具统一的 Tool 接口
- 延迟加载:只加载核心工具,其他按需搜索
- 智能搜索:关键词 + 评分,精准匹配工具
- 增量通知: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
相关协议:
- Anthropic Tool Use - Claude API 工具使用
- tool_reference 格式 - 动态工具加载机制