模块四:工具系统 | 前置依赖:第 10-13 课 | 预计学习时间:70 分钟
学习目标
完成本课后,你将能够:
- 描述
tools.ts中心注册表如何组织 40+ 种工具的静态导入和条件加载 - 解释 ToolSearchTool 的延迟加载机制(isDeferredTool 判定、select 查询、关键词搜索评分)
- 说明
toolSchemaCache.ts如何通过 schema 缓存避免 API 级别的缓存穿透 - 理解
useMergedTools()和assembleToolPool()如何组装最终的工具池
14.1 tools.ts — 中心注册表
文件位置与角色
tools.ts # 顶层文件,~390 行
这个文件是所有工具的唯一入口。无论是 REPL 主循环、Agent SDK 还是 Coordinator 模式,都通过这个文件获取工具列表。
静态导入 vs 条件导入
tools.ts 使用两种导入策略:
静态导入(始终加载):
import { AgentTool } from './tools/AgentTool/AgentTool.js'
import { BashTool } from './tools/BashTool/BashTool.js'
import { FileEditTool } from './tools/FileEditTool/FileEditTool.js'
import { FileReadTool } from './tools/FileReadTool/FileReadTool.js'
import { FileWriteTool } from './tools/FileWriteTool/FileWriteTool.js'
import { GlobTool } from './tools/GlobTool/GlobTool.js'
import { GrepTool } from './tools/GrepTool/GrepTool.js'
import { TodoWriteTool } from './tools/TodoWriteTool/TodoWriteTool.js'
import { WebFetchTool } from './tools/WebFetchTool/WebFetchTool.js'
import { ToolSearchTool } from './tools/ToolSearchTool/ToolSearchTool.js'
// ...约 20 个静态导入
条件导入(基于 Feature Gate 或环境变量):
// Dead code elimination: conditional import for ant-only tools
const REPLTool =
process.env.USER_TYPE === 'ant'
? require('./tools/REPLTool/REPLTool.js').REPLTool
: null
const SleepTool =
feature('PROACTIVE') || feature('KAIROS')
? require('./tools/SleepTool/SleepTool.js').SleepTool
: null
const cronTools = feature('AGENT_TRIGGERS')
? [
require('./tools/ScheduleCronTool/CronCreateTool.js').CronCreateTool,
require('./tools/ScheduleCronTool/CronDeleteTool.js').CronDeleteTool,
require('./tools/ScheduleCronTool/CronListTool.js').CronListTool,
]
: []
const MonitorTool = feature('MONITOR_TOOL')
? require('./tools/MonitorTool/MonitorTool.js').MonitorTool
: null
const WebBrowserTool = feature('WEB_BROWSER_TOOL')
? require('./tools/WebBrowserTool/WebBrowserTool.js').WebBrowserTool
: null
注意这里使用 require() 而非 import()。Bun 的编译时 feature() 函数会在打包时将未启用的分支完全消除(Dead Code Elimination),使用 require 的条件表达式可以被 Bun 静态分析。
延迟 require 破循环依赖
某些工具存在循环依赖,使用延迟 require 函数解决:
// Lazy require to break circular dependency:
// tools.ts -> TeamCreateTool -> ... -> tools.ts
const getTeamCreateTool = () =>
require('./tools/TeamCreateTool/TeamCreateTool.js').TeamCreateTool
const getTeamDeleteTool = () =>
require('./tools/TeamDeleteTool/TeamDeleteTool.js').TeamDeleteTool
const getSendMessageTool = () =>
require('./tools/SendMessageTool/SendMessageTool.js').SendMessageTool
14.2 getAllBaseTools() — 工具全集
getAllBaseTools() 返回当前环境下所有可能的工具列表:
export function getAllBaseTools(): Tools {
return [
AgentTool,
TaskOutputTool,
BashTool,
// 嵌入式搜索工具时不需要 Glob/Grep
...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
ExitPlanModeV2Tool,
FileReadTool,
FileEditTool,
FileWriteTool,
NotebookEditTool,
WebFetchTool,
TodoWriteTool,
WebSearchTool,
TaskStopTool,
AskUserQuestionTool,
SkillTool,
EnterPlanModeTool,
// Ant-only 工具
...(process.env.USER_TYPE === 'ant' ? [ConfigTool, TungstenTool] : []),
// Feature-gated 工具
...(WebBrowserTool ? [WebBrowserTool] : []),
...(isTodoV2Enabled() ? [TaskCreateTool, TaskGetTool, TaskUpdateTool, TaskListTool] : []),
...(isWorktreeModeEnabled() ? [EnterWorktreeTool, ExitWorktreeTool] : []),
...(SleepTool ? [SleepTool] : []),
...cronTools,
...(MonitorTool ? [MonitorTool] : []),
BriefTool,
...(SendUserFileTool ? [SendUserFileTool] : []),
// ToolSearch 最后加入
...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
]
}
工具的排列顺序并非随意 —— 源码注释中有重要说明:
NOTE: This MUST stay in sync with the Statsig console's system cache config, in order to cache the system prompt across users.
排列顺序影响 API 级别的 prompt cache 命中率。
嵌入式搜索工具
Ant 内部构建版本将 bfs(更快的 find)和 ugrep 嵌入到 Bun 二进制中,此时 Glob 和 Grep 工具不需要:
...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
14.3 getTools() — 过滤后的工具集
getTools() 在 getAllBaseTools() 基础上应用权限过滤和模式过滤:
getAllBaseTools()
│
▼
filterToolsByDenyRules() ─── 移除被权限规则拒绝的工具
│
▼
isEnabled() 过滤 ────────── 移除自身禁用的工具
│
▼
REPL 模式过滤 ───────────── 隐藏被 REPL 工具包裹的原始工具
│
▼
返回最终工具列表
Simple 模式
当设置 CLAUDE_CODE_SIMPLE=true 时,只提供三个核心工具:
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
const simpleTools: Tool[] = [BashTool, FileReadTool, FileEditTool]
// Coordinator 模式额外添加 AgentTool 和 TaskStopTool
if (feature('COORDINATOR_MODE') && coordinatorModeModule?.isCoordinatorMode()) {
simpleTools.push(AgentTool, TaskStopTool, getSendMessageTool())
}
return filterToolsByDenyRules(simpleTools, permissionContext)
}
Deny 规则过滤
export function filterToolsByDenyRules<T extends { name: string; mcpInfo?: ... }>(
tools: readonly T[],
permissionContext: ToolPermissionContext,
): T[] {
return tools.filter(tool => !getDenyRuleForTool(permissionContext, tool))
}
MCP 服务器前缀规则(如 mcp__server)可以一次性移除该服务器的所有工具。
14.4 constants/tools.ts — 工具集合常量
这个文件定义了四个关键的工具名称集合:
ALL_AGENT_DISALLOWED_TOOLS
子 Agent 不能使用的工具:
export const ALL_AGENT_DISALLOWED_TOOLS = new Set([
TASK_OUTPUT_TOOL_NAME, // 防止递归
EXIT_PLAN_MODE_V2_TOOL_NAME, // Plan 模式是主线程抽象
ENTER_PLAN_MODE_TOOL_NAME, // 同上
ASK_USER_QUESTION_TOOL_NAME, // 子 Agent 不应直接提问用户
TASK_STOP_TOOL_NAME, // 需要主线程任务状态
// Ant 用户可以嵌套 Agent
...(process.env.USER_TYPE === 'ant' ? [] : [AGENT_TOOL_NAME]),
])
ASYNC_AGENT_ALLOWED_TOOLS
异步 Agent(后台任务)可以使用的工具白名单:
export const ASYNC_AGENT_ALLOWED_TOOLS = new Set([
FILE_READ_TOOL_NAME,
WEB_SEARCH_TOOL_NAME,
TODO_WRITE_TOOL_NAME,
GREP_TOOL_NAME,
WEB_FETCH_TOOL_NAME,
GLOB_TOOL_NAME,
...SHELL_TOOL_NAMES, // Bash + PowerShell
FILE_EDIT_TOOL_NAME,
FILE_WRITE_TOOL_NAME,
NOTEBOOK_EDIT_TOOL_NAME,
SKILL_TOOL_NAME,
SYNTHETIC_OUTPUT_TOOL_NAME,
TOOL_SEARCH_TOOL_NAME,
ENTER_WORKTREE_TOOL_NAME,
EXIT_WORKTREE_TOOL_NAME,
])
COORDINATOR_MODE_ALLOWED_TOOLS
Coordinator 模式下协调者只能使用管理工具:
export const COORDINATOR_MODE_ALLOWED_TOOLS = new Set([
AGENT_TOOL_NAME, // 创建子 Agent
TASK_STOP_TOOL_NAME, // 停止任务
SEND_MESSAGE_TOOL_NAME, // 发送消息
SYNTHETIC_OUTPUT_TOOL_NAME, // 合成输出
])
这形成了一个清晰的分层:
┌─────────────────────────────────────────────────────┐
│ 工具可用性矩阵 │
├──────────────┬───────┬───────┬───────┬──────────────┤
│ 工具 │ 主线程 │ 子Agent│ 异步 │ Coordinator │
├──────────────┼───────┼───────┼───────┼──────────────┤
│ BashTool │ Yes │ Yes │ Yes │ No │
│ FileReadTool │ Yes │ Yes │ Yes │ No │
│ FileEditTool │ Yes │ Yes │ Yes │ No │
│ AgentTool │ Yes │ Ant │ No │ Yes │
│ AskUserQ │ Yes │ No │ No │ No │
│ EnterPlan │ Yes │ No │ No │ No │
│ TaskStop │ Yes │ No │ No │ Yes │
│ ToolSearch │ Yes │ Yes │ Yes │ No │
└──────────────┴───────┴───────┴───────┴──────────────┘
14.5 ToolSearchTool — 延迟加载机制
为什么需要延迟加载
当工具数量超过一定阈值时(特别是有大量 MCP 工具时),将所有工具的完整 schema 放入提示词会消耗大量 token。ToolSearchTool 允许只将工具名称放入提示词,模型需要时再通过搜索获取完整 schema。
isDeferredTool 判定逻辑
export function isDeferredTool(tool: Tool): boolean {
// 1. 显式 opt-out — alwaysLoad 标记
if (tool.alwaysLoad === true) return false
// 2. MCP 工具始终延迟
if (tool.isMcp === true) return true
// 3. ToolSearch 自身不能延迟(否则无法加载其他工具)
if (tool.name === TOOL_SEARCH_TOOL_NAME) return false
// 4. Fork-first Agent 不延迟(需要第一轮就可用)
if (feature('FORK_SUBAGENT') && tool.name === AGENT_TOOL_NAME) {
if (isForkSubagentEnabled()) return false
}
// 5. Brief 工具不延迟(包含文本可见性契约)
if ((feature('KAIROS') || feature('KAIROS_BRIEF')) && tool.name === BRIEF_TOOL_NAME) {
return false
}
// 6. 工具自身声明 shouldDefer
return tool.shouldDefer === true
}
两种查询模式
ToolSearchTool 支持两种查询方式:
select 模式(精确选取):
query: "select:Read,Edit,Grep"
│
▼
解析为 ["Read", "Edit", "Grep"]
│
▼
在 deferred + loaded 工具中查找
│
▼
返回匹配工具的完整 schema
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.push(tool.name)
}
return buildSearchResult(found, query, deferredTools.length)
}
关键词搜索(模糊匹配):
query: "notebook jupyter"
│
▼
拆分为搜索词 ["notebook", "jupyter"]
│
▼
对每个延迟工具评分
│
├── 工具名精确匹配部分: +10 分(MCP: +12)
├── 工具名包含匹配: +5 分(MCP: +6)
├── searchHint 匹配: +4 分
├── 描述词边界匹配: +2 分
└── 全名后备匹配: +3 分
│
▼
按分数排序,返回 top N
MCP 工具名称解析
MCP 工具名称的格式是 mcp__server__action:
function parseToolName(name: string): { parts: string[]; full: string; isMcp: boolean } {
if (name.startsWith('mcp__')) {
const withoutPrefix = name.replace(/^mcp__/, '').toLowerCase()
const parts = withoutPrefix.split('__').flatMap(p => p.split('_'))
return { parts: parts.filter(Boolean), full: withoutPrefix.replace(/__/g, ' '), isMcp: true }
}
// 普通工具按 CamelCase 拆分
const parts = name.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/_/g, ' ').toLowerCase().split(/\s+/)
return { parts, full: parts.join(' '), isMcp: false }
}
必需词搜索(+前缀)
支持 + 前缀强制要求某个词必须匹配:
for (const term of queryTerms) {
if (term.startsWith('+') && term.length > 1) {
requiredTerms.push(term.slice(1)) // +slack → "slack" 必须匹配
} else {
optionalTerms.push(term)
}
}
查询 +slack send 意味着工具名/描述中必须包含 "slack","send" 用于排序。
tool_reference 返回格式
ToolSearchTool 的结果使用特殊的 tool_reference 格式:
mapToolResultToToolResultBlockParam(content: Output, toolUseID: string) {
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' as const,
tool_name: name,
})),
}
}
tool_reference 告诉 API 服务器展开对应工具的完整 schema 到模型的上下文中。
14.6 toolSchemaCache.ts — Schema 缓存
问题背景
工具 schema 在 API 请求中位于 position 2(system prompt 之前),任何字节级变化都会导致整个 ~11K token 的工具块及所有下游内容的缓存失效。
缓存实现
type CachedSchema = BetaTool & {
strict?: boolean
eager_input_streaming?: boolean
}
const TOOL_SCHEMA_CACHE = new Map<string, CachedSchema>()
export function getToolSchemaCache(): Map<string, CachedSchema> {
return TOOL_SCHEMA_CACHE
}
export function clearToolSchemaCache(): void {
TOOL_SCHEMA_CACHE.clear()
}
缓存位置
toolSchemaCache.ts 被放在一个叶子模块中:
Lives in a leaf module so auth.ts can clear it without importing api.ts (which would create a cycle via plans -> settings -> file -> growthbook -> config -> bridgeEnabled -> auth).
当用户切换账户时(auth.ts),缓存需要被清除,但 auth.ts 不能导入 api.ts(会造成循环依赖),所以缓存独立到叶子模块。
缓存失效场景
以下情况会触发缓存失效:
┌──────────────────────────────────────────────────────┐
│ Schema 缓存失效触发条件 │
├──────────────────────────────────────────────────────┤
│ │
│ 1. 用户登出/切换账户 (clearToolSchemaCache) │
│ 2. MCP 服务器重连(工具列表变化) │
│ 3. 新会话开始 │
│ │
│ 不触发失效的情况(缓存命中): │
│ 1. GrowthBook flag 后台刷新 │
│ 2. tool.prompt() 中的动态内容变化 │
│ 3. 同一会话内的后续请求 │
│ │
└──────────────────────────────────────────────────────┘
14.7 assembleToolPool() 和 useMergedTools()
assembleToolPool — 纯函数
这是组装工具池的共享纯函数,被 REPL 和 runAgent 共同使用:
export function assembleToolPool(
permissionContext: ToolPermissionContext,
mcpTools: Tools,
): Tools {
const builtInTools = getTools(permissionContext)
const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
// 分区排序保证 prompt-cache 稳定性
const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
return uniqBy(
[...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
'name',
)
}
排序策略值得注意:
Sort each partition for prompt-cache stability, keeping built-ins as a contiguous prefix. The server's cache policy places a global cache breakpoint after the last prefix-matched built-in tool; a flat sort would interleave MCP tools into built-ins and invalidate all downstream cache keys.
内置工具和 MCP 工具分别排序后拼接(不是全局混排),确保内置工具形成连续前缀,最大化 prompt cache 命中。
useMergedTools — React Hook
export function useMergedTools(
initialTools: Tools,
mcpTools: Tools,
toolPermissionContext: ToolPermissionContext,
): Tools {
return useMemo(() => {
const assembled = assembleToolPool(toolPermissionContext, mcpTools)
return mergeAndFilterTools(initialTools, assembled, toolPermissionContext.mode)
}, [initialTools, mcpTools, toolPermissionContext])
}
使用 useMemo 确保只在依赖变化时重新计算。mergeAndFilterTools 来自 utils/toolPool.ts,处理额外的初始工具合并。
14.8 工具生命周期全景
从注册到执行的完整生命周期:
┌─────────────────────────────────────────────────────────┐
│ 工具生命周期 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 模块加载 │
│ └── tools.ts 中的 import/require 执行 │
│ └── feature() 编译时决定哪些工具被包含 │
│ │
│ 2. 工具列表构建 │
│ └── getAllBaseTools() → getTools() → assembleToolPool()│
│ └── 权限过滤、模式过滤、MCP 合并 │
│ │
│ 3. Schema 渲染 │
│ └── api.ts 中渲染 tool schema → toolSchemaCache 缓存 │
│ └── isDeferredTool() 决定完整/简化 schema │
│ │
│ 4. 提示词注入 │
│ └── 非延迟工具: 完整 schema + prompt() │
│ └── 延迟工具: 仅名称列表在 system-reminder 中 │
│ │
│ 5. 模型调用 │
│ └── Claude 决定使用某个工具 │
│ └── 如果是延迟工具 → 先调用 ToolSearch 获取 schema │
│ │
│ 6. 工具执行 │
│ └── findToolByName() → validateInput() → │
│ checkPermissions() → call() │
│ │
│ 7. 结果返回 │
│ └── mapToolResultToToolResultBlockParam() → │
│ 追加到 messages → 下一轮 API 调用 │
│ │
└─────────────────────────────────────────────────────────┘
14.9 searchHint 字段
每个工具可以提供一个 searchHint 字段,帮助 ToolSearchTool 更准确地匹配:
// FileReadTool
searchHint: 'read files, images, PDFs, notebooks'
// GlobTool
searchHint: 'find files by name pattern or wildcard'
// GrepTool
searchHint: 'search file contents with regex (ripgrep)'
// FileEditTool
searchHint: 'modify file contents in place'
// FileWriteTool
searchHint: 'create or overwrite files'
// BashTool 没有 searchHint(始终加载,不会被延迟)
searchHint 在评分系统中权重为 +4 分,高于描述匹配(+2)但低于名称精确匹配(+10)。
课后练习
练习 1:工具加载决策树
给定以下条件组合,判断哪些工具会被加载:
CLAUDE_CODE_SIMPLE=true+USER_TYPE=external+ 无 MCP 服务器CLAUDE_CODE_SIMPLE=false+USER_TYPE=ant+feature('KAIROS')=true- Coordinator 模式下的主 Agent vs Worker Agent
练习 2:设计 searchHint
为以下假想工具设计 searchHint:
DatabaseQueryTool— 执行 SQL 查询ImageGenerateTool— 使用 DALL-E 生成图片SlackNotifyTool— 发送 Slack 通知
要求:searchHint 应包含模型最可能搜索的关键词。
练习 3:prompt-cache 影响分析
假设有 20 个内置工具和 30 个 MCP 工具。如果 assembleToolPool() 使用全局混排而非分区排序:
- 当一个新 MCP 工具加入时,最坏情况下多少工具的 schema 需要重新缓存?
- 分区排序如何改善这个问题?
练习 4:ToolSearch 评分计算
计算以下查询对工具 mcp__claude_ai_OKEngine_LARK_MCP__lark_bitable_search_records 的评分:
- query: "lark search"
- query: "+lark bitable"
- query: "select:mcp__claude_ai_OKEngine_LARK_MCP__lark_bitable_search_records"
本课小结
| 要点 | 内容 |
|---|---|
| tools.ts | 中心注册表,静态导入 + feature() 条件导入,getAllBaseTools() 是源头 |
| 工具过滤 | getTools() 应用 deny 规则、isEnabled()、REPL 模式过滤 |
| 工具集合常量 | ALL_AGENT_DISALLOWED (子 Agent 禁用), ASYNC_AGENT_ALLOWED (异步白名单), COORDINATOR_ALLOWED (协调者专用) |
| ToolSearchTool | isDeferredTool 判定,select 精确/关键词模糊两种查询,tool_reference 返回格式 |
| toolSchemaCache | 会话级 schema 缓存,防止 GrowthBook 刷新导致的缓存穿透 |
| assembleToolPool | 分区排序(内置前缀 + MCP 后缀),最大化 prompt-cache 命中率 |
| useMergedTools | React Hook 包装,useMemo 优化重复计算 |
下一课预告
第 15 课:MCP 工具集成 — 我们将深入 Model Context Protocol 的实现。Claude Code 如何发现和连接 MCP 服务器?MCP 工具如何被转换为内部 Tool 接口?多服务器的工具命名空间如何管理?以及 alwaysLoad 元数据标记如何让关键 MCP 工具跳过延迟加载。