工具调度系统
30+ 工具的注册、匹配、分发与执行机制——Claude Code 的核心能力层
职责概述
解决的问题:模型说"我要读文件 X"或"我要运行命令 Y",但这些意图只是一段文本。需要一个调度系统把模型的文本意图翻译成真实的文件操作、Shell 命令、网络请求,并把执行结果格式化后返回给模型。
应用场景:① 模型请求读取代码文件 → 调度 FileRead 工具 → 返回文件内容 ② 模型请求运行测试 → 调度 Bash 工具 → 返回执行结果 ③ MCP 插件注册的自定义工具(如数据库查询)的统一调度 ④ 多工具并发执行时的读写分离和并发控制。
一句话理解:就像餐厅里的服务员——客人(模型)点菜(工具调用),服务员把单子传给厨房(执行引擎),菜做好了端回去(返回结果)。
架构设计
核心数据流
工具生命周期流程
getAllBaseTools() 汇总(tools.ts:193)。条件工具通过 feature flag 和环境变量动态加入。getTools() 过滤 deny 规则和 disabled 工具;assembleToolPool() 合并内置+MCP 工具并按名称排序(确保 prompt cache 稳定性)。tools 参数发送。支持 ToolSearch 延迟加载大型工具集。tool_use block,包含工具名和参数。通过 findToolByName() 匹配(支持 alias 别名)。validateInput() 验证参数合法性;checkPermissions() 检查工具级权限。详见权限系统章节。tool.call() 执行具体逻辑,返回 ToolResult<T>。支持 progress 回调、后台运行和结果持久化。工具注册表架构
tools.ts:193 — 工具注册表入口
BashTool, FileEditTool, FileReadTool, FileWriteTool, AgentTool, SkillTool, GlobTool, GrepTool, WebSearchTool, WebFetchTool, NotebookEditTool, AskUserQuestionTool, TodoWriteTool, TaskOutputTool, TaskStopTool, EnterPlanModeTool, ExitPlanModeV2Tool, BriefTool, SendMessageTool, ListMcpResourcesTool, ReadMcpResourceTool
REPLTool (ant-only), TungstenTool (ant-only), ConfigTool (ant-only), SuggestBackgroundPRTool (ant-only), WebBrowserTool, LSPTool, EnterWorktreeTool/ExitWorktreeTool, WorkflowTool, SleepTool, CronCreateTool/CronDeleteTool/CronListTool, RemoteTriggerTool, MonitorTool, PushNotificationTool, PowerShellTool
TestingPermissionTool (仅 test 环境), ToolSearchTool (延迟加载), TaskCreateTool/TaskGetTool/TaskUpdateTool/TaskListTool (TodoV2), OverflowTestTool, CtxInspectTool, VerifyPlanExecutionTool, SnipTool, ListPeersTool, TeamCreateTool/TeamDeleteTool (Swarm)
关键类型与接口
Tool 类型定义(Tool.ts:362-695)
export type Tool<
Input extends AnyObject = AnyObject,
Output = unknown,
P extends ToolProgressData = ToolProgressData,
> = {
// 身份标识
readonly name: string
aliases?: string[] // 别名(向后兼容重命名)
searchHint?: string // ToolSearch 关键词提示
// 核心方法
call(args, context, canUseTool, parentMessage, onProgress?): Promise<ToolResult<Output>>
description(input, options): Promise<string>
prompt(options): Promise<string | SystemPrompt>
readonly inputSchema: Input // Zod schema
outputSchema?: z.ZodType<unknown>
// 生命周期钩子
validateInput?(input, context): Promise<ValidationResult>
checkPermissions(input, context): Promise<PermissionResult>
preparePermissionMatcher?(input): Promise<(pattern: string) => boolean>
backfillObservableInput?(input): void
// 行为属性
isConcurrencySafe(input): boolean // 默认 false
isEnabled(): boolean // 默认 true
isReadOnly(input): boolean // 默认 false
isDestructive?(input): boolean // 默认 false
interruptBehavior?(): 'cancel' | 'block' // 默认 'block'
// MCP 相关
isMcp?: boolean
mcpInfo?: { serverName: string; toolName: string }
readonly shouldDefer?: boolean // 延迟加载标记
readonly alwaysLoad?: boolean // 首轮必须加载
// 结果处理
maxResultSizeChars: number // 超出时写入磁盘
}
buildTool 工厂函数(Tool.ts:783-792)
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?) => '',
userFacingName: (_input?) => '',
}
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
return {
...TOOL_DEFAULTS,
userFacingName: () => def.name,
...def,
} as BuiltTool<D>
}
工具查找与匹配(Tool.ts:348-360)
export function toolMatchesName(
tool: { name: string; aliases?: string[] },
name: string,
): boolean {
return tool.name === name || (tool.aliases?.includes(name) ?? false)
}
export function findToolByName(tools: Tools, name: string): Tool | undefined {
return tools.find(t => toolMatchesName(t, name))
}
工具组装管线(tools.ts:345-367)
export function assembleToolPool(
permissionContext: ToolPermissionContext,
mcpTools: Tools,
): Tools {
const builtInTools = getTools(permissionContext)
const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
// 按名称排序确保 prompt cache 稳定性
// 内置工具优先(uniqBy 保留插入顺序)
const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
return uniqBy(
[...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
'name',
)
}
ToolResult 类型(Tool.ts:321-336)
export type ToolResult<T> = {
data: T
newMessages?: (UserMessage | AssistantMessage | AttachmentMessage | SystemMessage)[]
contextModifier?: (context: ToolUseContext) => ToolUseContext
mcpMeta?: {
_meta?: Record<string, unknown>
structuredContent?: Record<string, unknown>
}
}
设计模式与亮点
buildTool() 使用类型级 spread 运算符,将 ToolDef(部分定义)补全为完整 Tool。7 个可默认字段(isEnabled, isConcurrencySafe, isReadOnly, isDestructive, checkPermissions, toAutoClassifierInput, userFacingName)都有安全默认值,采用"fail-closed"原则。getAllBaseTools → getTools → assembleToolPool → getMergedTools 四级管线逐步精炼。每层职责清晰:注册、权限过滤、MCP 合并、最终组装。排序保证 prompt cache 稳定性。feature() flag 或 process.env 条件加载。使用 require() 懒加载避免循环依赖,实现 dead code elimination。更多亮点
- 别名系统:
aliases字段支持工具重命名时向后兼容,toolMatchesName()统一匹配主名和别名。 - ToolSearch 延迟加载:当工具总数超过阈值时,
ToolSearchTool作为元工具被启用。其他工具标记shouldDefer: true,模型需先通过 ToolSearch 查找才能调用,减少 prompt token 消耗。 - 结果持久化:
maxResultSizeChars限制结果大小,超出时自动写入磁盘文件,避免超大输出撑爆上下文窗口。 - progress 回调:
onProgress参数支持工具执行过程中的实时进度更新(如 BashTool 的行输出、AgentTool 的 shell 进度)。 - 并发安全标记:
isConcurrencySafe()标识哪些工具可以并行执行。非并发安全工具通过contextModifier修改上下文,确保状态隔离。 - 中断行为:
interruptBehavior()控制用户发送新消息时工具的行为——'cancel'停止并丢弃结果,'block'继续运行让新消息等待。
开发者实践指南
如何添加新工具
tools/ 下创建目录,使用 buildTool() 定义工具对象。只需提供 name、inputSchema、call()、description() 和 prompt(),其他方法有安全默认值。
// tools/MyTool/MyTool.ts
import { buildTool, type ToolDef } from '../../Tool.js'
export const MyTool = buildTool({
name: 'MyTool',
async prompt() { return '...' },
async description() { return 'My custom tool' },
get inputSchema() {
return z.object({ query: z.string() })
},
async call(input, context) {
return { data: { result: 'done' } }
},
// 可选覆盖
isReadOnly: () => true,
isConcurrencySafe: () => true,
})
tools.ts 的 getAllBaseTools() 数组中注册。如果是条件工具,使用 feature() 或 process.env 守卫,并通过 require() 懒加载避免循环依赖。
工具开发常见模式
- 权限检查:覆盖
checkPermissions()实现工具级权限逻辑(如 BashTool 的子命令规则匹配)。 - 输入验证:覆盖
validateInput()在权限检查前拦截非法输入,返回{ result: false, message, errorCode }。 - 路径操作:实现
getPath()让权限系统识别工具操作的文件路径。 - 权限匹配器:实现
preparePermissionMatcher()为 Hook 的if条件提供精确匹配(如Bash(git *)的 glob 匹配)。 - 可观察输入补全:实现
backfillObservableInput()为 SDK 流、transcript、Hook 添加派生字段。
架构师决策指南
设计权衡
每个工具 export 一个单例对象而非工厂函数。优势:注册简单,类型推断精确。代价:工具实例无法携带请求级状态,必须通过
ToolUseContext 传递。这是一个刻意的设计选择——工具定义是静态的,上下文是动态的。
assembleToolPool() 按名称排序工具,内置工具作为连续前缀。这确保 MCP 工具的增减不会影响内置工具的排序位置,从而保持 prompt cache 命中率。排序策略与服务端 claude_code_system_cache_policy 配合工作。
扩展性考量
- MCP 工具集成:MCP 工具通过
assembleToolPool()合并,使用mcpInfo字段标识来源。filterToolsByDenyRules()支持mcp__server前缀的批量拒绝规则。 - 工具数量增长:当工具超过约 20 个时,ToolSearch 延迟加载机制启动。新工具应设置
searchHint以优化关键词匹配。 - 工具间通信:
ToolResult.contextModifier允许工具修改后续工具的上下文(仅非并发安全工具)。SkillTool使用此机制修改getAppState注入子 agent 的隔离状态。
性能考量
- 工具 Schema 序列化:每个工具的
inputSchema(Zod schema)在 API 调用时序列化为 JSON Schema。MCP 工具支持inputJSONSchema直接传递 JSON Schema,避免 Zod → JSON 转换开销。 - 延迟加载:约 15 个条件工具通过
require()动态加载,避免启动时解析无用模块。 - 结果持久化:
maxResultSizeChars阈值控制大结果写入磁盘。FileReadTool 设为Infinity(自身已有 limit),避免 Read→file→Read 循环。
◈ 可视化处理拓扑图
工具调度系统是从模型输出到实际执行的完整管线。当 API 流式响应中包含 tool_use 块时,系统依次经过工具查找、输入验证、权限检查、并发/串行执行和结果规范化。buildTool 工厂统一了 60+ 工具的接口,StreamingToolExecutor 实现流式并行执行。
buildTool 工厂通过 TypeScript 类型推断确保 call()、checkPermissions() 等方法接收正确类型。默认值机制(TOOL_DEFAULTS)让简单工具只需 4 个必要字段。工具排序保证 prompt cache 稳定性,maxResultSizeChars 防止超大输出淹没上下文。⇉ 核心处理流程详解
工具调度系统是 Claude Code 执行力的核心。从模型输出 tool_use 块到实际执行并返回结果,数据经过一条类型安全、权限受控、支持并行的完整管线。buildTool 工厂统一了 60+ 个工具的接口,StreamingToolExecutor 实现了流式并行执行,runTools 提供了批量串行执行的兜底方案。
type: 'tool_use' 的 content block。query.ts:829-835 提取所有 tool_use 块推入 toolUseBlocks 数组,并标记 needsFollowUp = true 表示需要执行工具。如果启用 StreamingToolExecutor(query.ts:561-568),每个 tool_use 块立即通过 addTool()(L842)入队执行。Tool.ts:358-360 的 findToolByName() 在工具列表中查找匹配的工具。支持主名称和别名匹配(通过 toolMatchesName(),L348-353,检查 tool.name 和 tool.aliases)。这对工具重命名场景(如旧名兼容)至关重要。Tool.ts:489-492 定义了可选的 validateInput() 方法,用于在权限检查之前校验参数合法性(如文件路径是否存在、参数格式是否正确)。验证失败返回 { valid: false, message: '...' },跳过后续所有处理。checkPermissions(input, context) 方法(Tool.ts:500-503)。默认实现(TOOL_DEFAULTS,L762-766)直接返回 { behavior: 'allow' }。复杂工具如 BashTool 会解析命令内容,匹配子命令规则,检查沙箱可用性。返回三种行为:allow(直接放行)、deny(拒绝执行)、ask(需用户确认)。Tool.ts:379-385 定义了核心的 call(args, context, canUseTool, parentMessage, onProgress) 方法。这是工具的实际执行入口,接收解析后的参数、工具上下文、权限检查函数和进度回调。执行结果类型 ToolResult<T>(L321-336)支持正常输出、错误输出和元数据。Tool.ts:466 的 maxResultSizeChars 控制大结果处理策略。当工具输出超过此阈值时,结果写入磁盘文件,模型只收到文件路径预览。ReadTool 设为 Infinity(L466 的注释解释了原因:Read 的结果持久化会创建 Read→file→Read 的死循环)。BashTool 等工具使用有限阈值,避免巨大的命令输出淹没上下文。Tool.ts:384 的 onProgress 回调(类型 ToolCallProgress<P>,L338-340)允许工具在执行过程中实时推送进度信息。这在长时间运行的工具中(如 BashTool 执行耗时命令、AgentTool 等待子 agent)特别重要——用户能看到实时反馈而非等待黑盒。query.ts:1395-1400 通过 normalizeMessagesForAPI() 将工具结果消息转换为 API 兼容格式(tool_result 类型的 user message)。过滤确保只有 user 类型的消息被推入 toolResults 数组,用于下一轮 API 调用。这完成了"模型请求→工具执行→结果返回→模型继续"的完整闭环。buildTool 工厂通过 TypeScript 类型推断确保每个工具的 call()、checkPermissions()、isReadOnly() 等方法接收正确类型的参数。默认值机制(TOOL_DEFAULTS)让简单工具只需定义 name、inputSchema、call 和 description,复杂工具可以覆盖每个可选方法。★ 设计精华
1. buildTool 工厂 — 零样板工具定义
Tool.ts:783-792 的 buildTool() 是整个工具系统的基石。它通过运行时展开 { ...TOOL_DEFAULTS, ...def } 为工具定义填充默认值,同时在类型层面通过 BuiltTool<D>(L735-741)精确计算最终类型。这意味着 60+ 个工具中,简单工具只需 4 个必要字段(name、inputSchema、call、description),复杂工具可以选择性覆盖任何默认行为。
// Tool.ts:757-792 — 工厂实现
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(def: D): BuiltTool {
return {
...TOOL_DEFAULTS,
userFacingName: () => def.name, // 默认使用工具名
...def, // 工具定义覆盖默认值
} as BuiltTool
}
BuiltTool<D> 的类型计算(L735-741)使用 Omit<D, DefaultableToolKeys> & ToolDefaults 模式——先移除工具定义中的可默认字段,再与默认类型交叉。这确保了如果工具定义提供了 checkPermissions,类型系统使用工具定义的签名而非默认签名,实现精确的类型推导。2. 工具别名系统 — 向后兼容重命名
Tool.ts:371 的可选 aliases 字段允许工具注册一个或多个别名。toolMatchesName()(L348-353)在查找时同时匹配主名称和所有别名。这使得工具可以在不破坏已保存会话和用户权限规则的情况下重命名——旧名称作为别名继续有效,新代码统一使用新名称。
// Tool.ts:348-360
export function toolMatchesName(tool, name): boolean {
return tool.name === name || tool.aliases?.includes(name) === true
}
export function findToolByName(tools: Tools, name: string): Tool | undefined {
return tools.find(tool => toolMatchesName(tool, name))
}
"OldToolName(*)": "allow"。即使工具改名为 NewToolName,只要旧名注册在 aliases 中,权限规则仍然生效。这避免了工具重命名导致的"用户权限突然丢失"问题。3. searchHint — 延迟加载工具的发现机制
Tool.ts:378 的 searchHint 是一句简短的能力描述(3-10 词),用于 ToolSearch 的关键词匹配。当工具被标记为 shouldDefer: true(L442)时,其完整 schema 不会出现在初始 API 请求中——模型需要先调用 ToolSearch 工具找到它,然后再调用。alwaysLoad(L449)则标记"必须在首轮就出现的工具"。
// Tool.ts:378-449 — 延迟加载相关字段
searchHint?: string // 3-10 词,关键词匹配用
readonly shouldDefer?: boolean // 延迟加载标记
readonly alwaysLoad?: boolean // 强制首轮加载
// 示例:NotebookEdit 的 searchHint 是 'jupyter notebook cell'
// 模型搜索 'jupyter' 就能找到它,无需在每轮都发送完整 schema
_meta['anthropic/alwaysLoad'] 选择退出延迟加载。4. 结果持久化阈值 — 上下文保护
Tool.ts:459-466 的 maxResultSizeChars 是防止工具输出淹没上下文的关键机制。当结果字符数超过阈值时,内容写入磁盘文件,模型只收到路径预览。这个设计有两个精妙之处:一是 ReadTool 设为 Infinity(避免 Read→file→Read 循环),二是阈值由工具自身定义而非全局配置——不同工具的"过大"标准不同。
// Tool.ts:459-466
/**
* Maximum size in characters for tool result before it gets persisted to disk.
* Set to Infinity for tools whose output must never be persisted (e.g. Read,
* where persisting creates a circular Read→file→Read loop).
*/
maxResultSizeChars: number
// BashTool: 有限阈值,大输出写磁盘
// ReadTool: Infinity,自身已有 limit 参数控制输出
// GlobTool: 较小阈值,文件列表通常不大
limit 参数控制输出大小,不需要外部持久化机制介入。5. isConcurrencySafe — 并行执行安全标记
Tool.ts:402 的 isConcurrencySafe(input) 方法标记工具是否可以安全并行执行。默认返回 false(Tool.ts:759),只读工具(如 Read、Glob)返回 true。StreamingToolExecutor 使用此标记决定是否并行调度工具——两个安全的工具可以同时运行,不安全的工具排队串行执行。
input 参数而非无参调用,是因为安全性可能取决于具体参数。例如 BashTool 执行 git status(只读)是安全的,但执行 rm -rf(写入)不安全。这种"参数级"的安全判断比"工具级"更精确。◈ Agent 实践借鉴 — 客服 Agent 工具系统设计
场景映射:客服 Agent 的工具长什么样?
客服 Agent 不像 CC 那样有 60+ 个工具,但 10-15 个核心工具的管理同样有挑战:
- 工具清单:查订单(queryOrder)、查物流(trackLogistics)、退单(createRefund)、改地址(updateAddress)、发优惠券(sendCoupon)、查知识库(searchFAQ)、转人工(transferToAgent)、查客户信息(queryCustomer)、创建工单(createTicket)。约 10-15 个核心工具。
- 动态扩展:新接入了微信支付渠道,需要加一个"查微信退款状态"的工具。不能让其他工具受影响。
- 渠道权限差异:在线客服坐席有"发优惠券"工具(客户在聊天窗口里直接领),电话客服坐席没有(电话里没法发二维码)。不同渠道看到的工具集不同。
- 高危操作:"退单"和"改地址"是写操作,需要坐席确认。"查订单"和"查知识库"是只读操作,可以自动放行。
借鉴 CC:buildTool 工厂 + 统一生命周期
CC 的 buildTool() 工厂让简单工具只需 4 个字段(name、inputSchema、call、description),复杂工具选择性覆盖。每个工具走 validateInput → checkPermissions → call → ToolResult 的标准管线。客服工具完全可以用同样的模式:
// CC 的 buildTool 工厂(简化)
const TOOL_DEFAULTS = {
isEnabled: () => true,
isReadOnly: () => false,
isConcurrencySafe: () => false,
checkPermissions: (input, _ctx) =>
Promise.resolve({ behavior: 'allow', updatedInput: input }),
};
function buildTool<D>(def: D): Tool {
return { ...TOOL_DEFAULTS, userFacingName: () => def.name, ...def };
}
// 简单工具:只需 name + schema + call + description
const GlobTool = buildTool({
name: 'Glob',
inputSchema: z.object({ pattern: z.string() }),
async call(input) { return { data: glob.sync(input.pattern) }; },
async description() { return 'Find files by pattern'; },
});
// 复杂工具:覆盖权限检查
const BashTool = buildTool({
name: 'Bash',
inputSchema: z.object({ command: z.string() }),
async call(input, ctx) { /* ... */ },
async description(input) { return `Run: ${input.command}`; },
async checkPermissions(input, ctx) {
return isSafeCommand(input.command)
? { behavior: 'allow' }
: { behavior: 'ask', message: `Allow: ${input.command}` };
},
});
客服 Agent 怎么改:统一工具接口
照搬 CC 的 buildTool 工厂模式,但把 CC 的文件系统相关概念(isReadOnly 对应文件写入、isConcurrencySafe 对应并行执行)替换为客服场景的概念(isReadOnly 对应写操作如退单、requiresConfirmation 对应需要坐席确认):
// 客服工具统一接口 — 借鉴 CC 的 buildTool 模式
interface CustomerServiceTool<Input, Output> {
// 必须实现(4 个字段,照搬 CC)
readonly name: string;
readonly inputSchema: ZodSchema<Input>;
call(args: Input, ctx: ServiceContext): Promise<ToolResult<Output>>;
description(input: Input): Promise<string>;
// 可选覆盖(有安全默认值)
validate?(input: Input): Promise<ValidationResult>;
checkPermission?(input: Input, ctx: PermContext): Promise<PermResult>;
isReadOnly(input: Input): boolean; // 默认 false
requiresConfirmation(input: Input): boolean; // 默认 true(fail-safe)
allowedChannels?: ChannelType[]; // 默认所有渠道
}
// 默认值:fail-safe 原则(与 CC 的 fail-closed 对应)
const CS_TOOL_DEFAULTS = {
isReadOnly: () => false,
requiresConfirmation: () => true, // 默认需要确认(退单/改地址等高危操作)
checkPermission: (input, _ctx) =>
Promise.resolve({ behavior: 'allow' }),
allowedChannels: undefined, // undefined = 所有渠道都可用
};
function buildCSTool<D extends Partial<CustomerServiceTool<any, any>>>(def: D) {
return { ...CS_TOOL_DEFAULTS, ...def };
}
// ===== 使用示例 =====
// 简单工具:查订单(只读,不需要确认,4 个字段搞定)
const queryOrder = buildCSTool({
name: 'queryOrder',
inputSchema: z.object({ orderId: z.string() }),
async call(input, ctx) {
const order = await ctx.orderService.query(input.orderId);
return { data: order };
},
async description(input) { return `查询订单 ${input.orderId}`; },
isReadOnly: () => true, // 覆盖:只读
requiresConfirmation: () => false, // 覆盖:不需要坐席确认
});
// 复杂工具:退单(写操作,需要坐席确认,需要参数校验)
const createRefund = buildCSTool({
name: 'createRefund',
inputSchema: z.object({
orderId: z.string(),
reason: z.string(),
amount: z.number().positive(),
}),
async call(input, ctx) {
const refund = await ctx.refundService.create({
orderId: input.orderId,
reason: input.reason,
amount: input.amount,
});
return { data: refund };
},
async description(input) { return `为订单 ${input.orderId} 创建退款`; },
// 不覆盖 isReadOnly → 默认 false(写操作)
// 不覆盖 requiresConfirmation → 默认 true(需要坐席确认)
// 覆盖参数校验:退款金额不能超过订单金额
async validate(input, ctx) {
const order = await ctx.orderService.query(input.orderId);
if (input.amount > order.totalAmount) {
return { valid: false, message: '退款金额不能超过订单金额' };
}
if (order.status === 'refunded') {
return { valid: false, message: '该订单已退款,不能重复退款' };
}
return { valid: true };
},
});
// 渠道限定工具:发优惠券(只有在线客服有)
const sendCoupon = buildCSTool({
name: 'sendCoupon',
inputSchema: z.object({ customerId: z.string(), couponId: z.string() }),
async call(input, ctx) {
await ctx.couponService.send(input.customerId, input.couponId);
return { data: { success: true } };
},
async description(input) { return `发送优惠券 ${input.couponId}`; },
allowedChannels: ['online'], // 只有在线渠道可用
isReadOnly: () => false,
requiresConfirmation: () => true,
});
工具执行生命周期:validate → permission → execute
// 客服工具执行管线 — 照搬 CC 的三步生命周期
async function executeCSTool(
tool: CustomerServiceTool<any, any>,
rawInput: unknown,
ctx: ServiceContext
): Promise<ToolResult> {
// Step 1: validate — 校验参数(退单金额不能超限、订单不能重复退款)
if (tool.validate) {
const validation = await tool.validate(rawInput, ctx);
if (!validation.valid) {
return { data: { error: validation.message } };
}
}
// Step 2: checkPermission — 检查权限(这个坐席是否有权退单)
const parsedInput = tool.inputSchema.parse(rawInput);
const perm = await (tool.checkPermission?.(parsedInput, ctx.permCtx)
?? Promise.resolve({ behavior: 'allow' as const }));
if (perm.behavior === 'deny') {
return { data: { error: '权限不足', reason: perm.reason } };
}
if (perm.behavior === 'ask' || tool.requiresConfirmation(parsedInput)) {
// 需要坐席确认(退单、改地址等高危操作)
const confirmed = await ctx.askAgent(
`确认执行 ${tool.name}?参数:${JSON.stringify(parsedInput)}`
);
if (!confirmed) {
return { data: { error: '坐席已拒绝' } };
}
}
// Step 3: call — 实际执行
const result = await tool.call(parsedInput, ctx);
return result;
}
按渠道/角色过滤工具集 + 动态注册
// 工具过滤 — 借鉴 CC 的 assembleToolPool 模式,但更简单
// CC 用的是 getAllBaseTools → getTools → assembleToolPool → getMergedTools 四级管线
// 客服场景工具少(10-15 个),只需要两级:注册 + 过滤
// 工具注册表(全局,启动时加载)
const toolRegistry: Map<string, CustomerServiceTool<any, any>> = new Map();
function registerTool(tool: CustomerServiceTool<any, any>) {
toolRegistry.set(tool.name, tool);
}
// 启动时注册所有工具
registerTool(queryOrder);
registerTool(trackLogistics);
registerTool(createRefund);
registerTool(updateAddress);
registerTool(sendCoupon);
registerTool(searchFAQ);
registerTool(transferToAgent);
registerTool(queryCustomer);
registerTool(createTicket);
// 按渠道/角色过滤工具集
function getToolsForChannel(channel: ChannelType, role: AgentRole): Tool[] {
return Array.from(toolRegistry.values()).filter(tool => {
// 渠道过滤:allowedChannels 未定义 = 所有渠道可用
if (tool.allowedChannels && !tool.allowedChannels.includes(channel)) {
return false;
}
// 角色过滤:初级坐席不能退单
if (role === 'junior' && tool.name === 'createRefund') {
return false;
}
return true;
});
}
// 动态注册新工具(新接入业务系统时)
// 借鉴 CC 的 MCP 动态工具模式,但不需要运行时发现
// 客服场景的工具数量有限,用简单的注册机制即可
function registerExternalTool(systemName: string, toolDef: ExternalToolDef) {
const tool = buildCSTool({
name: `${systemName}__${toolDef.name}`,
inputSchema: convertSchema(toolDef.inputSchema),
async call(input, ctx) {
return await ctx.externalCall(systemName, toolDef.name, input);
},
async description() { return toolDef.description; },
allowedChannels: toolDef.allowedChannels,
});
registerTool(tool);
}
// 使用示例:新接入微信支付渠道
registerExternalTool('wechat_pay', {
name: 'queryRefundStatus',
description: '查询微信退款状态',
inputSchema: { refundId: { type: 'string', required: true } },
allowedChannels: ['online', 'ticket'],
});
// 注册后,指定渠道的坐席自动获得此工具,无需重启
落地清单
- 统一工具接口 + 默认值:用
buildCSTool()工厂,简单工具 4 个字段搞定。默认值要 fail-safe(默认需要确认、默认是写操作),确保新增工具不会意外绕过安全检查。 - validate → execute 生命周期:每个工具执行前先校验参数(退单金额不能超限),再检查权限(坐席是否有权操作),最后才执行。三层防护,任何一层都可以短路返回。
- 工具过滤:按渠道(在线/电话/工单)和角色(初级/高级坐席)过滤可用工具。不是所有坐席都看到所有工具。
- MCP 动态工具合并:CC 用 MCP 协议在运行时动态发现和合并工具。客服场景只有 10-15 个工具,用简单的注册表 + 过滤就够了,不需要运行时发现协议。
- ToolSearch 延迟加载:CC 工具超过 20 个时用 ToolSearch 按需加载 schema。客服工具数量有限,全部加载到 prompt 里也没多少 token,不需要这个机制。
- 结果持久化到磁盘:CC 的
maxResultSizeChars是为文件内容等大输出设计的。客服工具返回的是订单/退款等结构化数据,体积可控,不需要写磁盘。 - 工具别名:CC 用 aliases 做向后兼容。客服工具数量少,直接改名就行,不需要别名机制。
- LLM 幻觉出不存在的工具名:LLM 可能在对话中说"我来帮你使用 cancelOrder",但你根本没有这个工具(只有 createRefund)。解法:工具列表是 prompt 的一部分,LLM 只能调用列表中存在的工具。如果调用了不存在的工具,返回错误消息"工具 cancelOrder 不存在,请使用 createRefund"。
- 跳过 validate 直接执行:LLM 传入退款金额 -100 元,工具直接执行了,财务数据出问题。validate 层必须在 call 之前拦截非法参数。
- 渠道过滤放在前端而不是后端:前端隐藏了"发优惠券"按钮,但 API 层没做校验。客户抓包后直接调 API 发优惠券。工具过滤必须在执行层(后端)做,前端只是锦上添花。
- 动态注册不通知 LLM:运行时注册了新工具,但没有更新 LLM 的工具列表。LLM 不知道新工具的存在。解法:注册新工具后,下一轮对话时更新 system prompt 中的工具描述。
代码索引
| 文件 | 行数 | 说明 |
|---|---|---|
Tool.ts | ~793 | Tool 类型定义、buildTool 工厂、findToolByName、ToolResult 类型 |
tools.ts | ~390 | 工具注册表、getAllBaseTools、getTools、assembleToolPool、getMergedTools |
tools/BashTool/BashTool.tsx | ~1144 | Shell 命令执行工具,最复杂的内置工具 |
tools/AgentTool/AgentTool.tsx | ~1398 | 子 agent 调度工具,支持同步/异步/teammate 模式 |
tools/AgentTool/runAgent.ts | ~974 | Agent 执行引擎,MCP 初始化、消息过滤 |
tools/AgentTool/forkSubagent.ts | ~211 | Fork 子 agent 机制,消息构建与 worktree 通知 |
tools/FileEditTool/FileEditTool.ts | ~626 | 文件编辑工具,支持 sed 模拟和精确 diff |
tools/FileReadTool/ | 目录 | 文件读取工具 |
tools/FileWriteTool/ | 目录 | 文件写入工具 |
tools/SkillTool/SkillTool.ts | ~1109 | Skill 执行工具,支持 fork 和远程 skill |
tools/MCPTool/MCPTool.ts | ~78 | MCP 工具外壳,实际逻辑在 mcpClient.ts 中覆盖 |
tools/SendMessageTool/SendMessageTool.ts | ~918 | 多 agent 消息通信工具 |
tools/GrepTool/ | 目录 | 代码搜索工具(ripgrep) |
tools/GlobTool/ | 目录 | 文件模式匹配工具 |
tools/WebSearchTool/ | 目录 | Web 搜索工具 |
tools/WebFetchTool/ | 目录 | URL 内容获取工具 |
tools/NotebookEditTool/ | 目录 | Jupyter Notebook 编辑工具 |
tools/ToolSearchTool/ | 目录 | 工具搜索元工具(延迟加载场景) |
tools/TaskCreateTool/~TaskUpdateTool/ | 目录 | TodoV2 任务管理工具集 |
tools/REPLTool/ | 目录 | REPL 工具(ant-only,包装 Bash/Read/Edit) |