Chapter 01
Chapter 01

工具系统基础

Claude 有 40+ 工具,它怎么知道该用哪个、能用哪个?
学习目标:
  • 理解 Tool 接口和 ToolUseContext 的核心类型定义
  • 掌握 getTools() 如何根据特性门控和用户类型过滤工具
  • 理解工具权限模型:ToolPermissionContext 的三种规则
  • 了解 Agent 工具过滤逻辑:ALL_AGENT_DISALLOWED_TOOLS

场景:你输入了一条命令……

场景:你刚在终端输入了 claude "帮我修这个 bug",Claude 秒回说要先读文件再跑测试……

小白

我看到 Claude 能读文件、写文件、跑命令、搜索代码……这些能力是写死的还是可以扩展的?

架构师

都是可扩展的。每个工具是一个实现了 Tool 接口的对象,注册在 tools.ts 里。就像瑞士军刀——统一的插槽接口,但每个工具头功能不同。

小白

那 Claude 怎么知道什么时候该用哪个工具?总不能 40 多个工具全塞给 API 吧?

场景追踪:从输入到工具调用

当你输入 claude "帮我修这个 bug" 并按下回车……
graph LR A["用户输入"] -- "解析参数" --> B["main.tsx"] B -- "构建权限" --> C["getTools()"] C -- "Tools[]" --> D["System Prompt"] D -- "API 调用" --> E["Claude API"] E -- "tool_use" --> F["checkPermissions → call()"]
关键洞察:工具列表在 API 调用前就已经确定——getTools() 根据权限上下文动态过滤,而不是把所有工具都发给模型。

Tool 接口:一个工具长什么样?

小白

Tool 接口看着有 30 多个方法,写一个新工具岂不是要实现一大堆东西?

架构师

不用。核心只有 4 个:call(执行)、inputSchema(输入校验)、checkPermissions(权限检查)、prompt(给模型的说明文字)。

小白

那其他 26 个方法呢?

架构师

大多是 UI 渲染和安全相关的,而且有 buildTool() 这个工厂函数帮你填好安全的默认值——就像新电脑出厂预装了防火墙,你只需要改你关心的设置。

Tool 接口核心方法

Tool.ts:362-504
export type Tool<
  Input extends AnyObject = AnyObject,
  Output = unknown,
> = {
  call(
    args: z.infer,
    context: ToolUseContext,
    canUseTool: CanUseToolFn,
    parentMessage: AssistantMessage,
    onProgress?: ToolCallProgress,
  ): Promise>

  readonly inputSchema: Input

  checkPermissions(
    input: z.infer,
    context: ToolUseContext,
  ): Promise

  prompt(options: {
    getToolPermissionContext: () => Promise
    tools: Tools
    agents: AgentDefinition[]
  }): Promise

  readonly name: string
  isReadOnly(input: z.infer): boolean
  isDestructive?(input: z.infer): boolean
}

call 是执行入口——接收解析后的参数和完整上下文。

checkPermissionscall 之前被调用,决定是否放行(allow/deny/ask)。

prompt 生成给模型看的工具使用说明,每次对话构建 system prompt 时调用。

面试要点:Interface + Factory 模式。统一接口使得 40+ 工具可以统一注册、统一权限检查、统一 UI 渲染。

buildTool() — 安全默认值工厂

Tool.ts:757-792
const TOOL_DEFAULTS = {
  isEnabled: () => true,
  isConcurrencySafe: (_input?: unknown) => false,
  isReadOnly: (_input?: unknown) => false,
  isDestructive: (_input?: unknown) => false,
  checkPermissions: (
    input: { [key: string]: unknown },
    _ctx?: ToolUseContext,
  ): Promise =>
    Promise.resolve({
      behavior: 'allow',
      updatedInput: input
    }),
}

export function buildTool(
  def: D
): BuiltTool {
  return {
    ...TOOL_DEFAULTS,
    userFacingName: () => def.name,
    ...def,
  } as BuiltTool
}

注意 isConcurrencySafe 默认 false——假设不安全(fail-closed)。

isReadOnly 也默认 false——假设会写入。

简单的展开运算符 ...def 覆盖默认值,开发者只需定义差异部分。

设计原则:Fail-closed——不确定就按最保守的来。宁可牺牲并发性能,也不冒数据竞争的风险。

工具过滤:三层滤网

小白

getAllBaseTools() 返回全部 40+ 个工具,可普通用户应该看不到内部工具吧?

架构师

对,getTools() 像个三层滤网:第一层是编译时 feature() 门控,代码直接不打包;第二层是 USER_TYPE 运行时过滤内部工具;第三层是 filterToolsByDenyRules,根据用户权限配置再筛。

小白

像餐厅菜单系统——完整菜单是 getAllBaseTools,但根据会员等级和过敏禁忌,每桌客人看到的菜单不一样?

架构师

完美的比喻!而且还有简单模式短路——CLAUDE_CODE_SIMPLE 为 true 时,直接只给 Bash、Read、Edit 三个工具。

getTools() 动态过滤管线

tools.ts:271-327
export const getTools = (
  permissionContext: ToolPermissionContext
): Tools => {
  // Simple mode: only Bash, Read, Edit
  if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
    const simpleTools: Tool[] = [
      BashTool, FileReadTool, FileEditTool
    ]
    return filterToolsByDenyRules(
      simpleTools, permissionContext
    )
  }

  const tools = getAllBaseTools().filter(
    tool => !specialTools.has(tool.name)
  )
  let allowedTools = filterToolsByDenyRules(
    tools, permissionContext
  )

  // REPL mode filter
  if (isReplModeEnabled()) {
    /* ... filter REPL_ONLY_TOOLS ... */
  }

  // Final: isEnabled() runtime check
  const isEnabled = allowedTools.map(_ => _.isEnabled())
  return allowedTools.filter((_, i) => isEnabled[i])
}

四层过滤管线:Simple 短路 → deny rules → REPL 过滤 → isEnabled 运行时检查。

filterToolsByDenyRules 读取用户权限配置,移除被禁止的工具。

Pipeline Filter 模式:每层过滤独立负责一个维度,层层递进,最终输出最小化工具集。面试时可以讨论这种分层策略的可维护性优势。

权限三叉戟:ToolPermissionContext

每次工具调用都经过三叉戟检查:Allow(快速通过)、Deny(直接拒绝)、Ask(需要确认)

ToolPermissionContext 类型定义

Tool.ts:123-138
export type ToolPermissionContext =
  DeepImmutable<{
    mode: PermissionMode
    alwaysAllowRules: ToolPermissionRulesBySource
    alwaysDenyRules: ToolPermissionRulesBySource
    alwaysAskRules: ToolPermissionRulesBySource
    isBypassPermissionsModeAvailable: boolean
    shouldAvoidPermissionPrompts?: boolean
    prePlanMode?: PermissionMode
  }>

三组规则的类型都是 ToolPermissionRulesBySource——按来源分组(用户配置、CLAUDE.md、hook 等)。

这使得规则可追溯——调试权限问题时能定位"这条规则是谁设的"。

DeepImmutable 包裹确保权限上下文不可变,防止工具执行过程中篡改权限。

面试要点:Immutable context + 按来源分组 = 安全 + 可审计。

Agent 工具过滤:不同角色不同权限

场景切换:Claude 启动了一个子 Agent 来处理子任务……

小白

子 Agent 能用所有工具吗?感觉有些工具(比如 AskUserQuestion)对子 Agent 没意义?

架构师

不只是没意义——还很危险。像公司门禁:正式员工有全部权限,实习生不能进机房,外包限制更多。有四组常量控制不同角色的工具访问。

Agent 工具过滤常量

constants/tools.ts:36-112
// 所有 Agent 禁用的工具
export const ALL_AGENT_DISALLOWED_TOOLS =
  new Set([
    TASK_OUTPUT_TOOL_NAME,
    EXIT_PLAN_MODE_V2_TOOL_NAME,
    ENTER_PLAN_MODE_TOOL_NAME,
    ASK_USER_QUESTION_TOOL_NAME,
    TASK_STOP_TOOL_NAME,
  ])

// Coordinator 只有 4 个工具
export const COORDINATOR_MODE_ALLOWED_TOOLS =
  new Set([
    AGENT_TOOL_NAME,
    TASK_STOP_TOOL_NAME,
    SEND_MESSAGE_TOOL_NAME,
    SYNTHETIC_OUTPUT_TOOL_NAME,
  ])

ALL_AGENT_DISALLOWED_TOOLS:所有子 Agent 都不能用的工具。AskUserQuestion 在这里,因为子 Agent 没有 UI 通道。

COORDINATOR_MODE_ALLOWED_TOOLS 只有 4 个——Coordinator 只负责调度,不直接操作文件。

RBAC 模式:基于角色的访问控制,通过 Set 常量实现。关注点分离——调度层只管调度,执行层只管执行。

场景追踪:子 Agent 工具裁剪

当 Claude 启动一个子 Agent 来处理子任务时……
graph LR A["AgentTool.call()"] -- "spawn" --> B["filterToolsForAgent()"] B -- "移除" --> C["DISALLOWED set"] B -- "交集" --> D["ALLOWED set"] D --> E["Agent 可用工具集"]
关键洞察:工具过滤是 Agent 生命周期的一部分——在 Agent 创建时就已经确定了它能用什么工具,而不是在每次调用时检查。

如果没有这些限制会怎样?

小白

如果子 Agent 能用所有工具会怎样?

架构师

两个灾难:(1) 子 Agent 调用 AskUserQuestion 会永远挂起,因为它没有 UI 通道接收用户回复;(2) 子 Agent 嵌套创建 Agent 可能形成无限递归,吃光资源。

小白

所以 ALL_AGENT_DISALLOWED_TOOLS 其实是安全护栏,不是功能限制?

架构师

正是如此。整个工具系统的设计哲学就是 fail-closed——从 buildTool 的默认值到 Agent 的工具裁剪,都是"不确定就禁止"。

快速检验

buildTool()isConcurrencySafe 的默认值是什么?为什么?

快速检验

COORDINATOR_MODE_ALLOWED_TOOLS 只包含 4 个工具,Coordinator 模式的设计意图是什么?

深入思考

ToolPermissionContext 中三组规则的类型都是 ToolPermissionRulesBySource。为什么要"按来源分组"而不是简单的规则列表?

本章总结

  • Tool 接口:统一的工具抽象,核心是 call / inputSchema / checkPermissions / prompt 四剑客
  • buildTool():用 fail-closed 默认值避免安全遗漏,30+ 方法只需覆盖几个
  • getTools() 过滤管线:编译时 feature() → 运行时 USER_TYPE → 权限 deny rules → isEnabled
  • 权限三叉戟:ToolPermissionContext 的 allow/deny/ask 三组规则控制安全级别
  • Agent 工具过滤:四组 Set 常量实现基于角色的访问控制,防止递归和 UI 挂起
下一章预告:Chapter 02 — 核心对话循环。工具准备好了,query.ts 如何驱动"API 调用 → 工具执行 → 再次调用"的闭环?

这一章体验如何?

可选反馈: