06-Claude Code的Tool抽象与工具注册机制
6. Tool 抽象与工具注册机制
所属分组:工具系统
概述
Claude Code 的"工具系统"是连接大模型推理能力与外部世界(文件系统、Shell、网络、子 Agent 等)的桥梁。整套系统的核心抽象极其精炼——一个名为 Tool 的 TypeScript 类型加上一个 buildTool 工厂函数,便定义了所有工具必须遵循的契约。每一个具体工具(BashTool、FileReadTool、AgentTool 等)都是这一抽象的实现。
工具的注册机制同样简洁:tools.ts 中的 getAllBaseTools() 作为唯一可信源,集中维护当前环境下可用的所有内置工具;getTools() 在权限上下文中过滤这些工具;assembleToolPool() 则把内置工具与 MCP 工具合并成最终下发给模型的工具池。整个设计强调"声明式注册 + 多层过滤 + 分层装配",既保证了可扩展性,又确保权限与缓存策略的稳定。
本文将从 Tool.ts、tools.ts、tools/utils.ts 三个核心文件出发,剖析工具抽象的形状、注册流程的关键函数,以及 UI 组件与 prompt 文本分离的设计哲学。
源码位置
- [Tool.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/Tool.ts)
- [tools.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools.ts)
- [tools/utils.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/utils.ts)
核心实现分析
1. ToolInputJSONSchema 与 AnyObject 类型
工具的入参 schema 是工具与模型之间的契约。Claude Code 既支持标准的 Zod schema,也兼容 MCP 工具直接提供的 JSON Schema:
export type ToolInputJSONSchema = {
[x: string]: unknown
type: 'object'
properties?: {
[x: string]: unknown
}
}
// Type for any schema that outputs an object with string keys
export type AnyObject = z.ZodType<{ [key: string]: unknown }>
ToolInputJSONSchema 主要服务于 MCP 工具——MCP 协议本身用 JSON Schema 描述参数,无需先转换成 Zod。AnyObject 则是所有 Zod 入参 schema 的上界,保证工具入参一定是一个对象。
2. Tool 接口定义
Tool 类型是整个工具系统的核心。它是一个泛型类型,三个类型参数分别约束入参 schema、输出类型和进度数据类型:
export type Tool<
Input extends AnyObject = AnyObject,
Output = unknown,
P extends ToolProgressData = ToolProgressData,
> = {
readonly name: string
readonly inputSchema: Input
readonly inputJSONSchema?: ToolInputJSONSchema
outputSchema?: z.ZodType<unknown>
// ...
}
接口中方法可大致分为四类:
执行与校验:call() 是工具真正的执行入口;validateInput() 在权限检查之前做语义校验;checkPermissions() 决定是否需要向用户请求许可;isReadOnly() / isDestructive() / isConcurrencySafe() 描述工具的副作用特征,影响并发与权限策略。
模型可见信息:description() 返回发送给模型的工具描述文本;prompt() 返回附加到 system prompt 的工具使用指引;searchHint / shouldDefer / alwaysLoad 控制工具在 ToolSearch 延迟加载机制中的行为。
UI 渲染:renderToolUseMessage() 渲染工具调用气泡(输入侧);renderToolResultMessage() 渲染工具结果气泡;renderToolUseProgressMessage() 渲染运行中进度;renderToolUseRejectedMessage() / renderToolUseErrorMessage() 分别处理拒绝与错误情形;renderGroupedToolUse() 处理并行工具调用的分组渲染。
辅助元数据:userFacingName() / getToolUseSummary() / getActivityDescription() / toAutoClassifierInput() 等为 UI、spinner、安全分类器提供短摘要。
值得注意的细节是 maxResultSizeChars:当工具结果超过此字符数时,会被持久化到磁盘,模型只收到带文件路径的预览。Read 工具被设为 Infinity,避免"读文件→存盘→再读"的循环。
3. buildTool 工厂函数与默认值策略
buildTool() 是所有工具导出的统一通道。它通过 TOOL_DEFAULTS 为一组常用方法提供"安全失败"默认值:
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: (_input?: unknown) => false,
isReadOnly: (_input?: unknown) => false,
isDestructive: (_input?: unknown) => false,
checkPermissions: (input, _ctx?) =>
Promise.resolve({ behavior: 'allow', updatedInput: input }),
toAutoClassifierInput: (_input?: unknown) => '',
userFacingName: (_input?: unknown) => '',
}
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
return {
...TOOL_DEFAULTS,
userFacingName: () => def.name,
...def,
} as BuiltTool<D>
}
注意默认值是"fail-closed"的:isConcurrencySafe 默认 false,isReadOnly 默认 false,toAutoClassifierInput 默认空字符串(在安全分类器中跳过)。任何工具若想改变这些行为,必须显式覆盖——这种保守默认值策略避免了"忘记实现某个方法就引入安全漏洞"的风险。
4. ToolUseContext:工具调用的运行时上下文
ToolUseContext 是每次 call() 都会接收的上下文对象,是工具访问运行时状态的唯一通道。它包含:
options:全局配置,包括tools(完整工具池)、mcpClients、agentDefinitions、thinkingConfig等abortController:用于中断工具执行readFileState:文件状态缓存,用于检测文件是否被外部修改getAppState()/setAppState():访问与修改应用状态setToolJSX:让工具临时接管整个 REPL 的 JSX 渲染(用于全屏交互)messages:当前消息历史agentId/agentType:子 Agent 标识contentReplacementState:工具结果预算与替换状态renderedSystemPrompt:父 Agent 冻结的 system prompt,用于 fork subagent 共享 prompt cache
contextModifier 字段允许非并发安全的工具在执行后修改上下文(例如 BashTool 修改 cwd)。
5. tools.ts 中的注册流程
getAllBaseTools() 是工具注册的"唯一可信源"。它返回一个数组,包含当前环境下所有可能启用的工具:
export function getAllBaseTools(): Tools {
return [
AgentTool,
TaskOutputTool,
BashTool,
...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
ExitPlanModeV2Tool,
FileReadTool,
FileEditTool,
FileWriteTool,
// ...
...(isEnvTruthy(process.env.ENABLE_LSP_TOOL) ? [LSPTool] : []),
...(isWorktreeModeEnabled() ? [EnterWorktreeTool, ExitWorktreeTool] : []),
// ...
...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
]
}
可以看到注册策略有三种形态:
- 静态导入:核心工具直接
import,无条件注册(BashTool、FileReadTool 等)。 - feature flag 条件注册:通过
feature('XXX')控制的实验性工具,构建时被打包器死代码消除。 - 运行时环境变量注册:如
process.env.USER_TYPE === 'ant'、process.env.ENABLE_LSP_TOOL等,运行时决定是否启用。
部分工具用 require() 懒加载来打破循环依赖(如 TeamCreateTool、SendMessageTool)。
6. getTools 与权限过滤
getTools(permissionContext) 是工具进入实际请求的入口。它的处理顺序很重要:
export const getTools = (permissionContext: ToolPermissionContext): Tools => {
// Simple mode:仅返回 Bash/Read/Edit 等少数工具
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
// ...
}
// 排除特殊工具(MCP 资源工具、SyntheticOutputTool)
const specialTools = new Set([
ListMcpResourcesTool.name,
ReadMcpResourceTool.name,
SYNTHETIC_OUTPUT_TOOL_NAME,
])
const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name))
// 按 deny 规则过滤
let allowedTools = filterToolsByDenyRules(tools, permissionContext)
// REPL 模式下隐藏被 REPL 包装的原始工具
if (isReplModeEnabled()) { /* ... */ }
// 按 isEnabled() 过滤
const isEnabled = allowedTools.map(_ => _.isEnabled())
return allowedTools.filter((_, i) => isEnabled[i])
}
filterToolsByDenyRules() 使用与运行时权限检查相同的匹配器,意味着 mcp__server 这样的前缀 deny 规则会在模型看到工具列表之前就把整个 server 的工具全部移除——而不仅仅是调用时拒绝。
7. assembleToolPool:内置工具与 MCP 工具的合并
assembleToolPool() 是 REPL 和 runAgent 共用的工具池装配函数:
export function assembleToolPool(
permissionContext: ToolPermissionContext,
mcpTools: Tools,
): Tools {
const builtInTools = getTools(permissionContext)
const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
return uniqBy(
[...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
'name',
)
}
注释中点明了排序的原因:服务端的 claude_code_system_cache_policy 会在最后一个内置工具之后放置全局缓存断点;如果 MCP 工具被插入到内置工具之间,所有下游缓存键都会失效。因此内置工具必须作为连续前缀,按 name 排序后拼接 MCP 工具。uniqBy 保留插入顺序,内置工具在同名冲突时优先。
8. tools/utils.ts 中的消息标记工具
tools/utils.ts 提供两个工具共用的辅助函数:
export function tagMessagesWithToolUseID(messages, toolUseID) {
if (!toolUseID) return messages
return messages.map(m => {
if (m.type === 'user') {
return { ...m, sourceToolUseID: toolUseID }
}
return m
})
}
export function getToolUseIDFromParentMessage(parentMessage, toolName) {
const toolUseBlock = parentMessage.message.content.find(
block => block.type === 'tool_use' && block.name === toolName,
)
return toolUseBlock && toolUseBlock.type === 'tool_use'
? toolUseBlock.id
: undefined
}
tagMessagesWithToolUseID 给工具执行期间产生的用户消息打上 sourceToolUseID 标记,让这些消息在工具解析前保持 transient,避免 UI 上出现重复的"is running"消息。这是工具与消息系统协作的一个细微但重要的设计。
9. UI 组件与 prompt 文本分离设计
观察 Tool 接口可以发现一个明显的设计:UI 渲染方法(renderToolUseMessage 等)和模型可见方法(description、prompt)被有意放在同一个接口里,但具体工具的实现通常把这两部分拆到不同文件:
prompt.ts:负责prompt()、description(),输出供模型阅读的文本UI.tsx:负责renderToolUseMessage等渲染方法,输出供用户阅读的 React 节点XxxTool.ts(x):负责call、checkPermissions、inputSchema等核心逻辑
这种"模型可见 / 用户可见 / 执行逻辑"三分离的设计带来三个好处:模型 prompt 可以独立调优而不影响 UI;UI 可以在终端不同模式(verbose / condensed / transcript)下复用同一份渲染逻辑;工具的核心逻辑可以在不引入 React 依赖的环境(如 SDK、agent runner)中被复用。
关键设计要点
- 单一接口契约:所有工具实现同一个
Tool接口,无论它是内置 BashTool 还是远程 MCP 工具,对调用方而言完全等价。 - fail-closed 默认值:
buildTool为并发安全、只读、破坏性等关键属性提供保守默认值,工具开发者必须显式声明才会"放宽"。 - 注册即过滤:工具注册分两层——
getAllBaseTools声明全部可能工具,getTools在权限上下文中过滤;filterToolsByDenyRules让 deny 规则在工具列表送达模型前就生效。 - 缓存友好的工具池排序:
assembleToolPool强制内置工具作为连续前缀,避免 MCP 工具插入破坏服务端 prompt cache。 - UI 与 prompt 分离:每个工具目录下
prompt.ts/UI.tsx/XxxTool.ts三件套分工清晰,模型可见文本、用户可见渲染、执行逻辑互不耦合。
与其他模块的关系
- 权限系统:
Tool.checkPermissions与ToolPermissionContext协作,具体的 deny/allow 规则匹配在utils/permissions/permissions.ts中实现,Tool.preparePermissionMatcher为带模式(如Bash(git *))的规则提供细粒度匹配器。 - 消息系统:
ToolUseContext.messages、appendSystemMessage、tagMessagesWithToolUseID让工具能向消息流追加内容,同时保持 UI 一致性。 - MCP 服务:
services/mcp/下的客户端把远程 MCP 工具适配成Tool接口(通过mcpInfo字段标识),再由assembleToolPool合并入工具池。 - Agent 系统:
AgentTool通过ToolUseContext.options.agentDefinitions获取子 Agent 定义,子 Agent 又通过createSubagentContext派生出新的ToolUseContext。 - ToolSearch 延迟加载:
shouldDefer/alwaysLoad字段配合ToolSearchTool实现工具 schema 的按需加载,节省 token。
小结
Claude Code 的工具系统用一份精炼的 Tool 接口 + 一个 buildTool 工厂 + 一组注册/过滤/装配函数,支撑起了 Bash、文件、搜索、Agent、MCP、Web 等数十种工具。它的核心思想是"统一契约 + 安全默认 + 分层过滤":所有工具实现同一接口,所有工具经过默认值补全、deny 规则过滤、isEnabled 过滤、MCP 合并四层处理后送达模型。理解这一抽象是阅读后续具体工具实现的前提——接下来的篇章将依次剖析各工具族如何填满这份契约。
更多推荐



所有评论(0)