职责概述

解决的问题:模型说"我要读文件 X"或"我要运行命令 Y",但这些意图只是一段文本。需要一个调度系统把模型的文本意图翻译成真实的文件操作、Shell 命令、网络请求,并把执行结果格式化后返回给模型。

应用场景:① 模型请求读取代码文件 → 调度 FileRead 工具 → 返回文件内容 ② 模型请求运行测试 → 调度 Bash 工具 → 返回执行结果 ③ MCP 插件注册的自定义工具(如数据库查询)的统一调度 ④ 多工具并发执行时的读写分离和并发控制。

一句话理解:就像餐厅里的服务员——客人(模型)点菜(工具调用),服务员把单子传给厨房(执行引擎),菜做好了端回去(返回结果)。

架构设计

工具注册层
Tool.ts — Tool 类型定义
tools.ts — 注册表与组装
buildTool() 工厂函数
工具组装层
getAllBaseTools()
getTools(permissionCtx)
assembleToolPool() — 内置 + MCP 合并
filterToolsByDenyRules()
工具执行层
findToolByName() — 查找匹配
tool.call() — 执行入口
validateInput → checkPermissions
ToolResult 处理
工具实现层(30+ 工具)
BashTool / FileEditTool / FileReadTool / FileWriteTool
AgentTool / SkillTool / MCPTool
GrepTool / GlobTool / WebSearchTool / WebFetchTool
SendMessageTool / TaskCreateTool / NotebookEditTool

核心数据流

工具生命周期流程

1. 注册阶段
各工具模块 export 单例对象,由 getAllBaseTools() 汇总(tools.ts:193)。条件工具通过 feature flag 和环境变量动态加入。
2. 过滤与组装
getTools() 过滤 deny 规则和 disabled 工具;assembleToolPool() 合并内置+MCP 工具并按名称排序(确保 prompt cache 稳定性)。
3. API 提交
工具列表作为 API 请求的 tools 参数发送。支持 ToolSearch 延迟加载大型工具集。
4. 模型选择工具
LLM 返回 tool_use block,包含工具名和参数。通过 findToolByName() 匹配(支持 alias 别名)。
5. 校验与权限
validateInput() 验证参数合法性;checkPermissions() 检查工具级权限。详见权限系统章节。
6. 执行与结果
tool.call() 执行具体逻辑,返回 ToolResult<T>。支持 progress 回调、后台运行和结果持久化。

工具注册表架构

getAllBaseTools()
tools.ts:193 — 工具注册表入口
▼ 分支
核心工具(始终加载)
BashTool, FileEditTool, FileReadTool, FileWriteTool, AgentTool, SkillTool, GlobTool, GrepTool, WebSearchTool, WebFetchTool, NotebookEditTool, AskUserQuestionTool, TodoWriteTool, TaskOutputTool, TaskStopTool, EnterPlanModeTool, ExitPlanModeV2Tool, BriefTool, SendMessageTool, ListMcpResourcesTool, ReadMcpResourceTool
条件工具(Feature Flag 控制)
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>
  }
}

设计模式与亮点

Builder 工厂模式
buildTool() 使用类型级 spread 运算符,将 ToolDef(部分定义)补全为完整 Tool。7 个可默认字段(isEnabled, isConcurrencySafe, isReadOnly, isDestructive, checkPermissions, toAutoClassifierInput, userFacingName)都有安全默认值,采用"fail-closed"原则。
核心模式
分层组装管线
工具集通过 getAllBaseTools → getTools → assembleToolPool → getMergedTools 四级管线逐步精炼。每层职责清晰:注册、权限过滤、MCP 合并、最终组装。排序保证 prompt cache 稳定性。
架构
Feature Flag 条件加载
30+ 工具中约 15 个通过 feature() flag 或 process.env 条件加载。使用 require() 懒加载避免循环依赖,实现 dead code elimination。
性能

更多亮点

开发者实践指南

如何添加新工具

步骤 1:在 tools/ 下创建目录,使用 buildTool() 定义工具对象。只需提供 nameinputSchemacall()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,
})
步骤 2:在 tools.tsgetAllBaseTools() 数组中注册。如果是条件工具,使用 feature()process.env 守卫,并通过 require() 懒加载避免循环依赖。

工具开发常见模式

架构师决策指南

设计权衡

单例模式 vs 工厂模式
每个工具 export 一个单例对象而非工厂函数。优势:注册简单,类型推断精确。代价:工具实例无法携带请求级状态,必须通过 ToolUseContext 传递。这是一个刻意的设计选择——工具定义是静态的,上下文是动态的。
工具排序与 Prompt Cache
assembleToolPool() 按名称排序工具,内置工具作为连续前缀。这确保 MCP 工具的增减不会影响内置工具的排序位置,从而保持 prompt cache 命中率。排序策略与服务端 claude_code_system_cache_policy 配合工作。

扩展性考量

性能考量

可视化处理拓扑图

工具调度系统是从模型输出到实际执行的完整管线。当 API 流式响应中包含 tool_use 块时,系统依次经过工具查找、输入验证、权限检查、并发/串行执行和结果规范化。buildTool 工厂统一了 60+ 工具的接口,StreamingToolExecutor 实现流式并行执行。

Phase 1 — 工具注册与组装管线
getAllBaseTools — 注册表入口tools.ts:193
核心工具~21 个始终加载
条件工具feature() 守卫
特殊工具test/延迟/元工具
getTools — deny 规则过滤tools.ts:filterToolsByDenyRules
assembleToolPool — 内置 + MCP 合并(按名称排序)tools.ts:345-367 — 保证 prompt cache 稳定
getMergedTools — 最终工具集tools.ts — 发送给 API
Phase 2 — 单次工具调用调度管线
API 流式响应 — tool_use 块到达query.ts:829-835
findToolByName — 工具查找匹配Tool.ts:348-360 — 支持主名 + 别名
validateInput — 输入验证Tool.ts:489-492
无效
返回验证错误{ valid: false, message }
有效 ↓
checkPermissions — 权限检查Tool.ts:500-503 — 默认 allow
allow
→ 执行
deny
→ 拒绝
ask
→ 用户确认
isConcurrencySafe? — 并发安全判定Tool.ts:402 — 默认 false,Read/Glob 返回 true
并发安全 → StreamingToolExecutorquery.ts:561-568 — 并行执行
Tool A
Tool B
Tool C
并行执行,完成即 yield
非安全 → runTools 串行执行query.ts:1382
Tool A → Tool B → Tool C
排队串行,防止状态冲突
tool.call — 实际执行Tool.ts:379-385 — 接收 args + context + onProgress
maxResultSizeChars — 结果大小检查Tool.ts:466 — ReadTool: Infinity
结果 ≤ 阈值直接返回 ToolResult
结果 > 阈值写入磁盘,返回路径预览
normalizeMessagesForAPI — 结果规范化返回query.ts:1395-1400 — tool_result → user message
调度核心约束:从定义到执行全程类型安全——buildTool 工厂通过 TypeScript 类型推断确保 call()checkPermissions() 等方法接收正确类型。默认值机制(TOOL_DEFAULTS)让简单工具只需 4 个必要字段。工具排序保证 prompt cache 稳定性,maxResultSizeChars 防止超大输出淹没上下文。

核心处理流程详解

工具调度系统是 Claude Code 执行力的核心。从模型输出 tool_use 块到实际执行并返回结果,数据经过一条类型安全、权限受控、支持并行的完整管线。buildTool 工厂统一了 60+ 个工具的接口,StreamingToolExecutor 实现了流式并行执行,runTools 提供了批量串行执行的兜底方案。

1. 模型输出 — tool_use 块到达
API 流式响应中包含 type: 'tool_use' 的 content block。query.ts:829-835 提取所有 tool_use 块推入 toolUseBlocks 数组,并标记 needsFollowUp = true 表示需要执行工具。如果启用 StreamingToolExecutor(query.ts:561-568),每个 tool_use 块立即通过 addTool()(L842)入队执行。
2. 工具查找 — findToolByName
Tool.ts:358-360findToolByName() 在工具列表中查找匹配的工具。支持主名称和别名匹配(通过 toolMatchesName(),L348-353,检查 tool.name 和 tool.aliases)。这对工具重命名场景(如旧名兼容)至关重要。
3. 输入验证 — validateInput
工具调用前先验证输入。Tool.ts:489-492 定义了可选的 validateInput() 方法,用于在权限检查之前校验参数合法性(如文件路径是否存在、参数格式是否正确)。验证失败返回 { valid: false, message: '...' },跳过后续所有处理。
4. 权限检查 — checkPermissions
每个工具定义了 checkPermissions(input, context) 方法(Tool.ts:500-503)。默认实现(TOOL_DEFAULTS,L762-766)直接返回 { behavior: 'allow' }。复杂工具如 BashTool 会解析命令内容,匹配子命令规则,检查沙箱可用性。返回三种行为:allow(直接放行)、deny(拒绝执行)、ask(需用户确认)。
5. 工具执行 — call()
Tool.ts:379-385 定义了核心的 call(args, context, canUseTool, parentMessage, onProgress) 方法。这是工具的实际执行入口,接收解析后的参数、工具上下文、权限检查函数和进度回调。执行结果类型 ToolResult<T>(L321-336)支持正常输出、错误输出和元数据。
6. 结果持久化 — maxResultSizeChars
Tool.ts:466maxResultSizeChars 控制大结果处理策略。当工具输出超过此阈值时,结果写入磁盘文件,模型只收到文件路径预览。ReadTool 设为 Infinity(L466 的注释解释了原因:Read 的结果持久化会创建 Read→file→Read 的死循环)。BashTool 等工具使用有限阈值,避免巨大的命令输出淹没上下文。
7. 进度回调 — onProgress
Tool.ts:384onProgress 回调(类型 ToolCallProgress<P>,L338-340)允许工具在执行过程中实时推送进度信息。这在长时间运行的工具中(如 BashTool 执行耗时命令、AgentTool 等待子 agent)特别重要——用户能看到实时反馈而非等待黑盒。
8. 结果规范化与返回
query.ts:1395-1400 通过 normalizeMessagesForAPI() 将工具结果消息转换为 API 兼容格式(tool_result 类型的 user message)。过滤确保只有 user 类型的消息被推入 toolResults 数组,用于下一轮 API 调用。这完成了"模型请求→工具执行→结果返回→模型继续"的完整闭环。
工具调度系统的核心约束是"类型安全从定义到执行":buildTool 工厂通过 TypeScript 类型推断确保每个工具的 call()checkPermissions()isReadOnly() 等方法接收正确类型的参数。默认值机制(TOOL_DEFAULTS)让简单工具只需定义 nameinputSchemacalldescription,复杂工具可以覆盖每个可选方法。

设计精华

1. buildTool 工厂 — 零样板工具定义

Tool.ts:783-792buildTool() 是整个工具系统的基石。它通过运行时展开 { ...TOOL_DEFAULTS, ...def } 为工具定义填充默认值,同时在类型层面通过 BuiltTool<D>(L735-741)精确计算最终类型。这意味着 60+ 个工具中,简单工具只需 4 个必要字段(name、inputSchema、call、description),复杂工具可以选择性覆盖任何默认行为。

buildTool Factory Pattern
默认值 + 展开覆盖,类型安全的工具定义
// 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 Alias for Backward Compatibility
查找逻辑同时匹配主名和别名
// 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))
}
别名系统与权限规则紧密配合——用户可能在 settings.json 中写了 "OldToolName(*)": "allow"。即使工具改名为 NewToolName,只要旧名注册在 aliases 中,权限规则仍然生效。这避免了工具重命名导致的"用户权限突然丢失"问题。

3. searchHint — 延迟加载工具的发现机制

Tool.ts:378searchHint 是一句简短的能力描述(3-10 词),用于 ToolSearch 的关键词匹配。当工具被标记为 shouldDefer: true(L442)时,其完整 schema 不会出现在初始 API 请求中——模型需要先调用 ToolSearch 工具找到它,然后再调用。alwaysLoad(L449)则标记"必须在首轮就出现的工具"。

Deferred Tool Loading via searchHint
减少初始 prompt 体积,按需加载工具 schema
// Tool.ts:378-449 — 延迟加载相关字段
searchHint?: string // 3-10 词,关键词匹配用
readonly shouldDefer?: boolean // 延迟加载标记
readonly alwaysLoad?: boolean // 强制首轮加载

// 示例:NotebookEdit 的 searchHint 是 'jupyter notebook cell'
// 模型搜索 'jupyter' 就能找到它,无需在每轮都发送完整 schema
延迟加载是 prompt engineering 的经典权衡:60+ 个工具的完整 schema 约 15000 token,每轮 API 调用都发送它们既浪费 token 又增加延迟。通过延迟加载,初始请求只包含 ~20 个核心工具和所有工具的 searchHint,模型需要时再动态加载。MCP 工具可通过 _meta['anthropic/alwaysLoad'] 选择退出延迟加载。

4. 结果持久化阈值 — 上下文保护

Tool.ts:459-466maxResultSizeChars 是防止工具输出淹没上下文的关键机制。当结果字符数超过阈值时,内容写入磁盘文件,模型只收到路径预览。这个设计有两个精妙之处:一是 ReadTool 设为 Infinity(避免 Read→file→Read 循环),二是阈值由工具自身定义而非全局配置——不同工具的"过大"标准不同。

Per-Tool Result Size Threshold
工具自治的结果大小控制
// 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: 较小阈值,文件列表通常不大
ReadTool 的 Infinity 设定是一个反直觉但正确的设计。如果 Read 工具的结果被持久化为文件("文件内容太长,已保存到 /tmp/xxx"),模型下次需要读取文件时会再次调用 Read,Read 又返回持久化路径...形成死循环。Read 自身通过 limit 参数控制输出大小,不需要外部持久化机制介入。

5. isConcurrencySafe — 并行执行安全标记

Tool.ts:402isConcurrencySafe(input) 方法标记工具是否可以安全并行执行。默认返回 false(Tool.ts:759),只读工具(如 Read、Glob)返回 true。StreamingToolExecutor 使用此标记决定是否并行调度工具——两个安全的工具可以同时运行,不安全的工具排队串行执行。

isConcurrencySafe 接收 input 参数而非无参调用,是因为安全性可能取决于具体参数。例如 BashTool 执行 git status(只读)是安全的,但执行 rm -rf(写入)不安全。这种"参数级"的安全判断比"工具级"更精确。

Agent 实践借鉴 — 客服 Agent 工具系统设计

场景映射:客服 Agent 的工具长什么样?

客服 Agent 不像 CC 那样有 60+ 个工具,但 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~793Tool 类型定义、buildTool 工厂、findToolByName、ToolResult 类型
tools.ts~390工具注册表、getAllBaseTools、getTools、assembleToolPool、getMergedTools
tools/BashTool/BashTool.tsx~1144Shell 命令执行工具,最复杂的内置工具
tools/AgentTool/AgentTool.tsx~1398子 agent 调度工具,支持同步/异步/teammate 模式
tools/AgentTool/runAgent.ts~974Agent 执行引擎,MCP 初始化、消息过滤
tools/AgentTool/forkSubagent.ts~211Fork 子 agent 机制,消息构建与 worktree 通知
tools/FileEditTool/FileEditTool.ts~626文件编辑工具,支持 sed 模拟和精确 diff
tools/FileReadTool/目录文件读取工具
tools/FileWriteTool/目录文件写入工具
tools/SkillTool/SkillTool.ts~1109Skill 执行工具,支持 fork 和远程 skill
tools/MCPTool/MCPTool.ts~78MCP 工具外壳,实际逻辑在 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)