Claude Code 源码解析
返回目录
工具系统09

计划与工作流

结构化的任务推进

核心洞察

ToolSearch 延迟加载机制让 40+ 工具仅在需要时才进入上下文窗口

学习

概念讲解与核心设计分析

计划与工作流工具概述

除了直接操作文件和代码的工具之外,Claude Code 还有一组"元工具"——它们不直接修改代码,而是帮助 Claude 规划工作、与用户交互、管理任务、加载技能。这些工具构成了 Claude Code 的"工作流引擎",使其从简单的代码生成器进化为一个能自主规划和执行复杂任务的 AI 代理。

Plan 模式:探索与执行的分离

Claude Code 支持在两种模式之间切换:

  • 正常模式(Normal) — Claude 可以使用所有工具,包括写文件、执行命令等有副作用的操作
  • 计划模式(Plan) — Claude 只能使用只读工具(读文件、搜索、Grep),不能修改任何内容

EnterPlanModeTool

当 Claude 遇到复杂任务时,它可以调用 EnterPlanModeTool 进入计划模式:

  • 系统自动禁用所有非只读工具
  • Claude 可以自由探索代码库,理解架构和依赖关系
  • 不用担心误操作——物理上无法执行写操作

ExitPlanModeV2Tool

计划完成后,Claude 调用 ExitPlanModeV2Tool 退出计划模式:

  • 关键特性:allowedPrompts 参数——Claude 可以声明"我打算执行的操作清单"
  • 这个清单显示给用户,让用户在 Claude 开始执行前审查计划
  • 退出后恢复所有工具的可用性

SkillTool:技能加载系统

SkillTool 是 Claude Code 的"能力扩展机制"。技能(Skill)本质上是预定义的提示词模板,可以注入到对话中增强 Claude 在特定领域的表现。

技能来源

  • 用户自定义技能 — 存储在 .claude/skills/ 目录中,用户或团队编写
  • 内置技能 — 与 Claude Code 一起打包发布的 bundled/ 技能

17 个内置技能

Claude Code 自带 17 个内置技能,覆盖常见的开发工作流:

  • batch — 批量处理:对多个文件或目标执行相同操作
  • claudeApi — Claude API 使用指南:帮助生成调用 Anthropic API 的代码
  • claudeInChrome — Chrome 扩展开发辅助
  • debug — 调试辅助:分析错误堆栈、建议调试策略
  • keybindings — 快捷键配置辅助
  • loop — 循环执行:定时重复执行某个操作
  • remember — 记忆管理:将信息写入 CLAUDE.md 以持久化
  • scheduleRemoteAgents — 远程代理调度
  • simplify — 代码简化:审查并简化复杂代码
  • skillify — 技能创建:帮助用户编写新技能
  • stuck — 困境突破:当 Claude 陷入循环时提供新思路
  • updateConfig — 配置更新辅助
  • verify — 验证工具:运行测试和检查以验证变更

技能执行机制

当 Claude 调用 SkillTool 时:

  1. 根据技能名称查找技能定义文件
  2. 读取技能的提示词模板
  3. 将提示词注入到子对话中
  4. 在子对话上下文中执行技能逻辑
  5. 将结果返回主对话

ToolSearchTool:延迟工具发现

并非所有工具都需要在每次对话开始时就发送给 Claude。一些不常用的工具被标记为 shouldDefer: true,表示它们可以被延迟加载:

  • 这些工具在初始的 tools 数组中不出现
  • 当 Claude 需要时,调用 ToolSearchTool 搜索可用工具
  • 搜索基于 searchHint 关键词匹配
  • 找到后,工具被添加到当前会话的工具池中
  • 这减少了每次 API 调用的 token 数(工具定义也消耗 input tokens)

AskUserQuestionTool:结构化用户交互

当 Claude 需要用户输入时,它不是简单地在回复中提问,而是通过 AskUserQuestionTool 发起结构化的交互:

  • question — 向用户展示的问题文本
  • options — 可选项列表(如 ["是", "否", "跳过"]),渲染为按钮
  • multiSelect — 是否允许多选
  • preview — 预览内容(如即将应用的 diff)

这提供了比纯文本对话更丰富的交互体验。

TodoWriteTool:任务跟踪

对于复杂的多步骤任务,Claude 可以使用 TodoWriteTool 创建和管理任务列表:

  • 创建带有 subject 和 description 的任务项
  • 标记任务状态:pending → in_progress → completed
  • 设置任务间的依赖关系(blocks/blockedBy)
  • 任务列表对用户可见,展示当前进度

这使得 Claude 在处理复杂任务时更加透明和有组织。

BriefTool:文件上传支持

BriefTool 支持用户通过文件上传/附件的方式向 Claude 提供信息:

  • 支持拖拽文件到终端
  • 支持粘贴剪贴板内容
  • 文件内容被转换为对话中的消息

架构

模块关系与设计决策

计划与工作流工具架构

核心模块

工具职责模式影响
EnterPlanModeTool进入计划模式(只读探索)禁用写操作工具
ExitPlanModeV2Tool退出计划模式(带操作清单)恢复所有工具
SkillTool加载和执行技能注入提示词到子对话
ToolSearchTool搜索延迟加载的工具添加工具到当前池
AskUserQuestionTool结构化用户交互暂停等待用户输入
TodoWriteTool任务列表管理展示进度给用户
BriefTool文件附件支持文件内容注入对话

Plan 模式状态机

  • Normal Mode
    • 所有工具可用
    • Claude 调用 EnterPlanModeTool →
  • Plan Mode
    • 仅只读工具可用(Read, Glob, Grep, LSP...)
    • Claude 自由探索代码库
    • Claude 调用 ExitPlanModeV2Tool(allowedPrompts) →
  • Back to Normal Mode
    • 所有工具恢复
    • allowedPrompts 展示给用户
    • Claude 按计划执行

SkillTool 执行流程

  1. 查找技能 → 搜索 .claude/skills/ 和 bundled/ 目录
  2. 读取定义 → 解析技能文件中的提示词模板
  3. 注入上下文 → 将提示词注入子对话的系统提示中
  4. 执行子对话 → 延迟 require(queryLoop) 启动子 QueryLoop
  5. 返回结果 → 子对话的输出作为 ToolResult 返回主对话

工具延迟加载策略

  • 始终加载:BashTool, FileReadTool, FileEditTool, FileWriteTool, GlobTool, GrepTool(核心工具,几乎每次都用)
  • 延迟加载(shouldDefer=true):NotebookEditTool, WebSearchTool, LSPTool 等(按需发现)
  • 触发条件:Claude 调用 ToolSearchTool,或系统检测到相关文件类型

设计决策

为什么 Plan 模式要在工具层面禁用而不是提示词层面?

仅靠提示词"请不要修改文件"是不可靠的——模型可能在复杂推理中忘记这个约束。在工具层面禁用意味着即使 Claude 尝试调用 FileWriteTool,工具系统也会返回错误。这是"能力限制"而非"行为建议",提供了硬性保证。

源码

3 个关键代码示例

01
Plan 模式切换实现
TypeScript
// tools/EnterPlanModeTool.ts — 简化版
export class EnterPlanModeTool implements Tool {
  name = 'EnterPlanMode'

  inputSchema = z.object({
    reason: z.string().optional()
      .describe('进入计划模式的原因'),
  })

  description() {
    return '进入计划模式。在此模式下你只能使用只读工具来探索' +
      '代码库,不能修改任何文件。适用于需要先理解代码结构再动手的场景。'
  }

  async call(
    input: { reason?: string },
    context: ToolUseContext
  ): Promise<ToolResult> {
    // 切换全局模式为 Plan
    context.setAppState(state => ({
      ...state,
      permissionMode: 'plan' as PermissionMode,
    }))

    return {
      data: '已进入计划模式。你现在只能使用只读工具(Read, Glob, Grep, LSP 等)。' +
        '请充分探索代码库,制定完整的实施计划后再退出。',
    }
  }

  isReadOnly() { return true }
  isConcurrencySafe() { return true }
}

// tools/ExitPlanModeV2Tool.ts — 简化版
export class ExitPlanModeV2Tool implements Tool {
  name = 'ExitPlanMode'

  inputSchema = z.object({
    plan: z.string().describe('制定的实施计划'),
    allowedPrompts: z.array(z.string()).optional()
      .describe('计划执行的操作清单,展示给用户审查'),
  })

  async call(
    input: { plan: string; allowedPrompts?: string[] },
    context: ToolUseContext
  ): Promise<ToolResult> {
    // 恢复正常模式
    context.setAppState(state => ({
      ...state,
      permissionMode: 'default' as PermissionMode,
    }))

    // 构建计划摘要
    let summary = '计划已制定:\n\n' + input.plan
    if (input.allowedPrompts?.length) {
      summary += '\n\n计划执行的操作:\n'
      summary += input.allowedPrompts
        .map((p, i) => (i + 1) + '. ' + p)
        .join('\n')
    }

    return {
      data: summary,
      // contextModifier 可以将计划注入后续的系统提示词
      contextModifier: (ctx) => ({
        ...ctx,
        activePlan: input.plan,
      }),
    }
  }

  // 只在 Plan 模式下可用
  isEnabled() {
    return getAppState().permissionMode === 'plan'
  }
}
02
SkillTool 技能加载与执行
TypeScript
// tools/SkillTool.ts — 简化版
import { readdir, readFile } from 'fs/promises'
import { join } from 'path'

interface SkillDefinition {
  name: string
  description: string
  prompt: string
  source: 'user' | 'bundled'
}

export class SkillTool implements Tool {
  name = 'Skill'

  inputSchema = z.object({
    skill: z.string().describe('要执行的技能名称'),
    args: z.string().optional().describe('传给技能的参数'),
  })

  description() {
    return '执行一个预定义的技能。技能提供特定领域的专业能力。'
  }

  async call(
    input: { skill: string; args?: string },
    context: ToolUseContext
  ): Promise<ToolResult> {
    // 1. 查找技能
    const skill = await this.findSkill(input.skill, context)
    if (!skill) {
      const available = await this.listSkills(context)
      return {
        data: '未找到技能 "' + input.skill + '"。可用技能:\n' +
          available.map(s => '  - ' + s.name + ': ' + s.description).join('\n'),
      }
    }

    // 2. 构建技能提示词
    const skillPrompt = this.buildPrompt(skill, input.args)

    // 3. 延迟加载 queryLoop(避免循环依赖)
    const { runSubConversation } = require('../services/queryLoop')

    // 4. 在子对话中执行技能
    const result = await runSubConversation({
      systemPromptModifier: (basePrompt: string) =>
        basePrompt + '\n\n' + skillPrompt,
      messages: context.messages,
      tools: context.options.tools,
      abortSignal: context.abortController.signal,
    })

    return {
      data: result.output,
      contextModifier: skill.contextModifier,
    }
  }

  private async findSkill(
    name: string,
    context: ToolUseContext
  ): Promise<SkillDefinition | null> {
    // 先搜索用户自定义技能
    const userSkillPath = join(context.options.cwd, '.claude/skills', name)
    try {
      const content = await readFile(userSkillPath + '.md', 'utf-8')
      return { name, prompt: content, description: '', source: 'user' }
    } catch {}

    // 再搜索内置技能
    const bundledSkills = await this.getBundledSkills()
    return bundledSkills.find(s =>
      s.name === name || s.name.includes(name)
    ) ?? null
  }

  private async getBundledSkills(): Promise<SkillDefinition[]> {
    // 17 个内置技能
    return [
      { name: 'batch', description: '批量处理多个文件', prompt: '...', source: 'bundled' },
      { name: 'debug', description: '调试辅助', prompt: '...', source: 'bundled' },
      { name: 'simplify', description: '代码简化', prompt: '...', source: 'bundled' },
      { name: 'verify', description: '验证变更', prompt: '...', source: 'bundled' },
      { name: 'stuck', description: '困境突破', prompt: '...', source: 'bundled' },
      // ... 共 17 个
    ]
  }
}
03
ToolSearchTool 延迟发现与 AskUserQuestionTool
TypeScript
// tools/ToolSearchTool.ts — 简化版
export class ToolSearchTool implements Tool {
  name = 'ToolSearch'

  inputSchema = z.object({
    query: z.string().describe('搜索关键词'),
  })

  description() {
    return '搜索可用但未加载的工具。' +
      '某些工具(如 NotebookEdit、WebSearch)' +
      '不是默认加载的,需要通过搜索发现。'
  }

  async call(
    input: { query: string },
    context: ToolUseContext
  ): Promise<ToolResult> {
    // 获取所有延迟加载的工具
    const deferredTools = getAllBaseTools().filter(
      t => t.shouldDefer === true
    )

    // 关键词匹配
    const matches = deferredTools.filter(tool => {
      const searchText = [
        tool.name,
        tool.searchHint ?? '',
        tool.description(),
      ].join(' ').toLowerCase()

      return input.query.toLowerCase().split(/\s+/).some(
        keyword => searchText.includes(keyword)
      )
    })

    if (matches.length === 0) {
      return { data: '未找到匹配 "' + input.query + '" 的工具。' }
    }

    // 将找到的工具添加到当前会话
    context.setAppState(state => ({
      ...state,
      additionalTools: [
        ...(state.additionalTools ?? []),
        ...matches,
      ],
    }))

    return {
      data: '找到 ' + matches.length + ' 个工具:\n' +
        matches.map(t =>
          '  - ' + t.name + ': ' + t.description().slice(0, 100)
        ).join('\n') +
        '\n\n这些工具已添加到当前会话,可以直接使用。',
    }
  }
}

// tools/AskUserQuestionTool.ts — 简化版
export class AskUserQuestionTool implements Tool {
  name = 'AskUserQuestion'

  inputSchema = z.object({
    question: z.string().describe('向用户提出的问题'),
    options: z.array(z.string()).optional()
      .describe('选项列表,渲染为按钮'),
    multiSelect: z.boolean().optional()
      .describe('是否允许多选'),
    preview: z.string().optional()
      .describe('预览内容(如 diff)'),
  })

  async call(
    input: {
      question: string
      options?: string[]
      multiSelect?: boolean
      preview?: string
    },
    context: ToolUseContext
  ): Promise<ToolResult> {
    // 等待用户通过 UI 回复
    // 这会暂停 QueryLoop 直到用户输入
    const userResponse = await waitForUserInput({
      prompt: input.question,
      options: input.options,
      multiSelect: input.multiSelect,
      preview: input.preview,
    })

    return {
      data: userResponse.text,
      newMessages: [{
        role: 'user',
        content: userResponse.text,
      }],
    }
  }

  isReadOnly() { return true }
  isConcurrencySafe() { return false }
}

互动

步进式流程演示

互动演示

计划与工作流工具互动解析

第 1 步:为什么需要 Plan 模式?

考虑一个复杂任务:"重构这个项目的认证系统"。如果 Claude 直接开始修改代码,可能会:

  • 在不了解全貌的情况下修改了关键文件
  • 遗漏了某些依赖该模块的地方
  • 做出与项目惯例不一致的设计决策

Plan 模式解决了这个问题:

  1. Claude 先进入 Plan 模式,只能读和搜索
  2. 它浏览认证相关的所有文件,理解架构
  3. 它搜索所有调用认证 API 的地方,了解影响范围
  4. 制定完整计划后退出 Plan 模式
  5. 按计划有序地修改代码

这模拟了优秀程序员的工作方式:先理解,后动手。

第 2 步:Skill 系统的设计哲学

为什么用"注入提示词"而不是"编写代码"来实现技能?

  • 灵活性:任何人都可以用自然语言编写技能,不需要编程知识
  • 组合性:技能可以使用所有已有工具,不需要重新实现功能
  • 可维护性:修改技能只需要编辑一个 .md 文件,不需要重新编译
  • 安全性:技能在同一个权限体系下运行,不会绕过安全检查

第 3 步:延迟工具加载的经济学

每个工具的 JSON Schema 定义大约消耗 200-500 input tokens。42 个工具就是 8400-21000 tokens。

  • 如果用户只需要文件编辑,加载 WebSearchTool 和 NotebookEditTool 是浪费
  • 延迟加载可以将初始工具数减少到 ~20 个,节省约 50% 的工具定义 tokens
  • 在长对话中(每轮都发送工具定义),累积节省可达数万 tokens

第 4 步:AskUserQuestionTool vs 直接在回复中提问

Claude 可以在回复中直接写"请告诉我你想要哪种方案",但 AskUserQuestionTool 提供了更好的体验:

  • 选项按钮:用户可以点击而不是打字,减少输入错误
  • 多选支持:用户可以同时选择多个选项
  • 预览:在用户做决定之前展示变更的效果
  • 结构化:用户的回复被结构化为 ToolResult,而不是需要 Claude 解析的自然语言

第 5 步:TodoWriteTool 的透明性价值

当 Claude 处理一个"把项目从 JavaScript 迁移到 TypeScript"这样的大任务时:

  • Claude 创建 15 个子任务(如"迁移 utils/", "迁移 components/", "更新 tsconfig"...)
  • 用户可以在 UI 中看到任务列表和进度
  • Claude 标记完成的任务,用户知道还剩多少工作
  • 如果会话中断,任务列表帮助 Claude 快速回忆进度

这种透明性建立了用户对 AI 代理的信任——用户始终知道 Claude 在做什么、还要做什么。

关键设计洞察

  • 能力控制 > 行为建议:Plan 模式在工具层面限制能力,而非依赖提示词约束
  • 提示词即代码:Skill 系统用自然语言模板代替代码,降低了扩展门槛
  • 按需加载:延迟工具发现平衡了能力完备性和 token 成本
  • 结构化交互:AskUserQuestionTool 将自然语言对话升级为结构化工具调用
  • 透明执行:TodoWriteTool 让 AI 代理的工作过程对用户可见

相关源文件

tools/EnterPlanModeTool/tools/SkillTool/tools/ToolSearchTool/