命令体系架构

📑 目录

本系列文章深入解析 Claude Code 的源码架构,帮助你理解现代 AI Agent 的设计哲学与工程实践。

一、命令系统概览

Claude Code 的核心交互方式是通过斜杠命令(slash commands)驱动的。从源码规模来看,整个命令系统是一个精心设计的分层架构:

  • src/commands.ts(约 754 行,25KB):命令注册中心,负责所有命令的加载、过滤和查询
  • src/types/command.ts:命令类型系统的完整定义
  • src/commands/ 目录:70+ 个独立命令模块,每个命令一个文件夹
  • src/skills/ 目录:动态技能加载系统
  • 插件/MCP 扩展:外部命令的动态注入

1.1 命令 vs 工具:两种交互范式

在 Claude Code 的架构中,命令(Command)工具(Tool) 是两个不同层级的概念:

维度命令(Command)工具(Tool)
触发方式用户主动输入 /command-name由 LLM 在推理过程中自动调用
执行主体本地 JavaScript/TypeScript 代码通常是 shell 命令或文件操作
结果返回直接输出到终端或修改 UI 状态结果注入到对话上下文供 LLM 参考
典型示例/clear, /exit, /themeBash, Read, Edit
生命周期解析 → 验证 → 执行 → 清理由 LLM 决策链自动管理

命令系统是用户与 Claude Code 之间的"控制面板",而工具系统则是 Claude Code 与外部环境之间的"手脚"。这种分离设计使得用户可以通过命令直接控制应用状态(如清空对话、切换主题),而工具则专注于完成具体的编码任务。

1.2 95+ 命令的规模与分类

commands.tsCOMMANDS 函数(第 258-346 行)可以看到,内置命令按功能可分为以下几类:

// src/commands.ts (第 258-346 行)
const COMMANDS = memoize((): Command[] => [
  addDir, advisor, agents, branch, btw, chrome, clear, color,
  compact, config, copy, desktop, context, contextNonInteractive,
  cost, diff, doctor, effort, exit, fast, files, heapDump, help,
  ide, init, keybindings, installGitHubApp, installSlackApp, mcp,
  memory, mobile, model, outputStyle, remoteEnv, plugin, pr_comments,
  releaseNotes, reloadPlugins, rename, resume, session, skills,
  stats, status, statusline, stickers, tag, theme, feedback,
  review, ultrareview, rewind, securityReview, terminalSetup,
  upgrade, extraUsage, extraUsageNonInteractive, rateLimitOptions,
  usage, usageReport, vim,
  // ... 条件编译的命令
])

这些命令覆盖了:会话管理(session, resume, exit)、代码操作(commit, review, diff)、系统配置(theme, config, model)、开发辅助(doctor, skills, hooks)以及实验性功能(通过 feature() 条件编译注入)。

flowchart TD
    subgraph 用户输入层
        A[用户输入 /command]
        B[自动补全提示]
    end

    subgraph 命令解析层
        C[识别斜杠命令]
        D[参数分割]
        E[查找命令定义]
    end

    subgraph 命令执行层
        F[类型: local]
        G[类型: local-jsx]
        H[类型: prompt]
    end

    subgraph 结果处理层
        I[文本输出]
        J[UI 渲染]
        K[Prompt 展开]
    end

    A --> C
    B --> C
    C --> D
    D --> E
    E --> F
    E --> G
    E --> H
    F --> I
    G --> J
    H --> K
    K --> L[LLM 处理]

二、命令注册机制

2.1 命令定义格式:三种类型

Claude Code 的命令系统支持三种核心命令类型,定义在 src/types/command.ts(第 15-85 行):

1. local 类型:本地函数命令

// src/types/command.ts (第 56-61 行)
type LocalCommand = {
  type: 'local'
  supportsNonInteractive: boolean
  load: () => Promise<LocalCommandModule>
}

local 命令是最简单的类型,它返回纯文本结果。典型代表是 /cost 命令:

// src/commands/cost/index.ts
const cost = {
  type: 'local',
  name: 'cost',
  description: 'Show the total cost and duration of the current session',
  get isHidden() {
    if (process.env.USER_TYPE === 'ant') return false
    return isClaudeAISubscriber()
  },
  supportsNonInteractive: true,
  load: () => import('./cost.js'),
} satisfies Command

2. local-jsx 类型:React UI 命令

// src/types/command.ts (第 102-109 行)
type LocalJSXCommand = {
  type: 'local-jsx'
  load: () => Promise<LocalJSXCommandModule>
}

local-jsx 命令会渲染 Ink(基于 React 的终端 UI 框架)组件。例如 /exit 命令:

// src/commands/exit/index.ts
const exit = {
  type: 'local-jsx',
  name: 'exit',
  aliases: ['quit'],
  description: 'Exit the REPL',
  immediate: true,
  load: () => import('./exit.js'),
} satisfies Command

3. prompt 类型:Prompt 展开命令

// src/types/command.ts (第 34-54 行)
export type PromptCommand = {
  type: 'prompt'
  progressMessage: string
  contentLength: number
  argNames?: string[]
  allowedTools?: string[]
  model?: string
  source: SettingSource | 'builtin' | 'mcp' | 'plugin' | 'bundled'
  async getPromptForCommand(
    args: string,
    context: ToolUseContext,
  ): Promise<ContentBlockParam[]>
}

prompt 命令是最强大的类型,它将用户输入展开为完整的 Prompt 内容发送给 LLM。/commit 命令就是一个典型例子:

// src/commands/commit.ts (第 15-85 行)
const command = {
  type: 'prompt',
  name: 'commit',
  description: 'Create a git commit',
  allowedTools: [
    'Bash(git add:*)',
    'Bash(git status:*)',
    'Bash(git commit:*)',
  ],
  contentLength: 0,
  progressMessage: 'creating commit',
  source: 'builtin',
  async getPromptForCommand(_args, context) {
    const promptContent = getPromptContent()
    // ... 展开为完整 Prompt
  }
}

2.2 自动注册 vs 手动注册

Claude Code 采用了混合注册策略

手动注册(静态导入):所有核心内置命令在 commands.ts 中通过显式 import 导入并注册到 COMMANDS() 数组中。这种方式的优点是:

  • 编译时确定依赖关系
  • Tree-shaking 友好
  • 明确的命令可见性控制

自动注册(动态加载):技能(skills)、插件(plugins)和 MCP 命令通过运行时动态发现:

// src/commands.ts (第 449-471 行)
const loadAllCommands = memoize(async (cwd: string): Promise<Command[]> => {
  const [
    { skillDirCommands, pluginSkills, bundledSkills, builtinPluginSkills },
    pluginCommands,
    workflowCommands,
  ] = await Promise.all([
    getSkills(cwd),
    getPluginCommands(),
    getWorkflowCommands ? getWorkflowCommands(cwd) : Promise.resolve([]),
  ])

  return [
    ...bundledSkills,
    ...builtinPluginSkills,
    ...skillDirCommands,
    ...workflowCommands,
    ...pluginCommands,
    ...pluginSkills,
    ...COMMANDS(),  // 内置命令放在最后
  ]
})

这里使用了 lodash-es/memoize 对加载结果进行缓存,避免每次查询命令时都重新执行磁盘 I/O 和动态导入。

2.3 命令元数据体系

每个命令都携带丰富的元数据,定义在 CommandBase 接口中(src/types/command.ts 第 127-162 行):

export type CommandBase = {
  availability?: CommandAvailability[]    // 权限控制
  description: string                      // 命令描述
  hasUserSpecifiedDescription?: boolean   // 用户自定义描述
  isEnabled?: () => boolean               // 动态启用检查
  isHidden?: boolean                      // 是否在帮助中隐藏
  name: string                            // 命令名称
  aliases?: string[]                      // 别名
  argumentHint?: string                   // 参数提示
  whenToUse?: string                      // 使用场景说明
  version?: string                        // 版本
  disableModelInvocation?: boolean        // 禁止模型调用
  userInvocable?: boolean                 // 用户是否可调用
  loadedFrom?: 'commands_DEPRECATED' | 'skills' | 'plugin' | 'bundled' | 'mcp'
  kind?: 'workflow'                       // 工作流标记
  immediate?: boolean                     // 立即执行(绕过队列)
  isSensitive?: boolean                   // 参数是否脱敏
}

这些元数据为命令系统提供了强大的可扩展性。例如:

  • availability:控制命令对不同用户群体的可见性(claude-ai 订阅者 vs console API 用户)
  • isEnabled:通过 feature flag 动态控制命令是否可用
  • isHidden:隐藏内部命令或环境专属命令
  • loadedFrom:追踪命令来源,用于优先级排序和来源标注
flowchart LR
    subgraph 命令来源
        A1[内置命令
COMMANDS()] A2[打包技能
bundledSkills] A3[目录技能
skillDirCommands] A4[插件命令
pluginCommands] A5[MCP 命令
mcpCommands] A6[工作流
workflowCommands] end subgraph 过滤层 B1[availability
权限检查] B2[isEnabled
功能开关] B3[isHidden
可见性过滤] end subgraph 查询接口 C1[getCommands] C2[getSkillToolCommands] C3[getSlashCommandToolSkills] end A1 --> B1 A2 --> B1 A3 --> B1 A4 --> B1 A5 --> B1 A6 --> B1 B1 --> B2 B2 --> B3 B3 --> C1 C1 --> C2 C1 --> C3

三、参数解析

3.1 参数传递的设计哲学

Claude Code 的命令参数解析采用了极简设计:用户输入 /command arg1 arg2 后,从第一个空格后的所有内容作为单个字符串传递给命令处理器。

// 伪代码示意参数解析流程
function parseCommandInput(input: string): { name: string; args: string } {
  const trimmed = input.trim()
  if (!trimmed.startsWith('/')) return null

  const spaceIndex = trimmed.indexOf(' ')
  if (spaceIndex === -1) {
    return { name: trimmed.slice(1), args: '' }
  }

  return {
    name: trimmed.slice(1, spaceIndex),
    args: trimmed.slice(spaceIndex + 1).trim(),
  }
}

这种设计的优势在于:

  • 灵活性:命令自己决定如何解析参数,可以使用任何解析库
  • 简单性:不需要维护复杂的参数 schema
  • 可扩展性:支持复杂的多行参数和特殊格式

3.2 argNames 与参数绑定

对于 prompt 类型的命令,可以通过 argNames 声明预期的参数名:

// src/types/command.ts (第 38 行)
argNames?: string[]  // 参数名称列表

这些参数名主要用于:

  1. 自动补全提示:向用户展示期望的参数格式
  2. Token 估算contentLength 字段帮助系统估算命令展开后的 Prompt 长度
  3. 参数校验:部分命令在 getPromptForCommand 中对参数进行验证

3.3 参数验证模式

由于参数以原始字符串传递,验证通常在命令内部完成。以 /init 命令为例:

// src/commands/init.ts (简化示意)
async function getPromptForCommand(args: string, context: ToolUseContext) {
  // args 可能为空,表示使用默认行为
  // 命令内部根据 args 内容选择不同的初始化策略
  if (args.includes('--minimal')) {
    return generateMinimalPrompt()
  }
  return generateFullPrompt()
}

3.4 可选参数和默认值

Claude Code 的命令系统没有强制的参数 schema,但遵循以下约定:

模式示例说明
无参数/clear直接执行,不需要额外输入
可选参数/model gpt-4参数可选,省略时使用默认值
多参数/skill-name arg1 arg2以空格分隔,由命令自行分割
标志参数/command --flag命令内部解析 flags

四、命令生命周期

Claude Code 的命令生命周期可以分为四个阶段:解析、验证、执行、清理。

4.1 解析阶段:从输入到命令对象

当用户在终端输入 /cost 并按下回车时,系统执行以下解析流程:

// src/commands.ts (第 688-718 行)
export function findCommand(
  commandName: string,
  commands: Command[],
): Command | undefined {
  return commands.find(
    _ =>
      _.name === commandName ||
      getCommandName(_) === commandName ||
      _.aliases?.includes(commandName),
  )
}

export function getCommand(commandName: string, commands: Command[]): Command {
  const command = findCommand(commandName, commands)
  if (!command) {
    throw ReferenceError(
      `Command ${commandName} not found. Available commands: ${commands
        .map(_ => {
          const name = getCommandName(_)
          return _.aliases ? `${name} (aliases: ${_.aliases.join(', ')})` : name
        })
        .sort((a, b) => a.localeCompare(b))
        .join(', ')}`
    )
  }
  return command
}

解析阶段的核心逻辑:

  1. 提取命令名:从输入中分割出 / 后的命令名称
  2. 匹配别名:支持通过 aliases 数组匹配(如 /exit/quit 是同一个命令)
  3. 模糊匹配:通过 getCommandName 处理可能的名称变体
  4. 错误提示:命令不存在时,返回完整的可用命令列表

4.2 验证阶段:权限与可用性检查

在命令执行前,系统会进行多层验证:

第一层:Availability(权限验证)

// src/commands.ts (第 417-445 行)
export function meetsAvailabilityRequirement(cmd: Command): boolean {
  if (!cmd.availability) return true
  for (const a of cmd.availability) {
    switch (a) {
      case 'claude-ai':
        if (isClaudeAISubscriber()) return true
        break
      case 'console':
        if (!isClaudeAISubscriber() && !isUsing3PServices() &&
            isFirstPartyAnthropicBaseUrl()) return true
        break
    }
  }
  return false
}

第二层:isEnabled(功能开关)

// src/types/command.ts (第 136 行)
isEnabled?: () => boolean  // 动态启用检查

许多命令通过 isEnabled 钩子与 feature flag 系统集成。例如条件编译的命令只有在特定 feature 开启时才可见。

第三层:Remote/Bridge 安全过滤

// src/commands.ts (第 619-665 行)
export const REMOTE_SAFE_COMMANDS: Set<Command> = new Set([
  session, exit, clear, help, theme, color, vim, cost, usage, copy,
  btw, feedback, plan, keybindings, statusline, stickers, mobile,
])

export const BRIDGE_SAFE_COMMANDS: Set<Command> = new Set([
  compact, clear, cost, summary, releaseNotes, files,
])

当 Claude Code 运行在远程模式(--remote)或通过 Bridge 接收移动端指令时,只有白名单中的命令才会被暴露,防止本地敏感操作被远程触发。

4.3 执行阶段:三种执行路径

根据命令类型的不同,执行阶段有三条路径:

路径 A:local 命令

// 执行流程示意
const module = await command.load()  // 懒加载实现
const result = await module.call(args, context)
// result: { type: 'text', value: '...' } |
//         { type: 'compact', compactionResult: ... } |
//         { type: 'skip' }

local 命令通过 load() 动态导入实现模块,然后调用 call() 函数执行。结果是一个 LocalCommandResult 对象,可以是文本、压缩结果或跳过标记。

路径 B:local-jsx 命令

// 执行流程示意
const module = await command.load()
const reactNode = await module.call(onDone, context, args)
// reactNode 是 Ink 组件,由 React 渲染到终端

local-jsx 命令返回 React 元素,由 Ink 框架渲染为终端 UI。onDone 回调用于命令完成后通知系统继续处理。

路径 C:prompt 命令

// 执行流程示意
const contentBlocks = await command.getPromptForCommand(args, context)
// contentBlocks 直接注入到对话上下文,发送给 LLM

prompt 命令不直接产生可见输出,而是生成 Prompt 内容块,扩展当前对话上下文。这是 Claude Code 最独特的命令类型——它将用户的简单指令转换为详细的 LLM 提示词。

4.4 清理阶段:缓存失效与状态更新

命令执行完成后,系统可能需要清理缓存:

// src/commands.ts (第 523-538 行)
export function clearCommandMemoizationCaches(): void {
  loadAllCommands.cache?.clear?.()
  getSkillToolCommands.cache?.clear?.()
  getSlashCommandToolSkills.cache?.clear?.()
  clearSkillIndexCache?.()
}

export function clearCommandsCache(): void {
  clearCommandMemoizationCaches()
  clearPluginCommandCache()
  clearPluginSkillsCache()
  clearSkillCaches()
}

当用户添加新技能、安装插件或修改配置时,需要调用这些清理函数使命令列表重新加载。

4.5 错误处理

Claude Code 的命令系统采用了防御性编程策略:

  1. 加载失败容错getSkills() 中每个 Promise 都有独立的 .catch() 处理,即使技能加载失败也不会影响其他命令
  2. 运行时异常捕获getSlashCommandToolSkills 在异常时返回空数组而不是抛出错误
  3. 命令不存在提示getCommand() 抛出详细的 ReferenceError,包含所有可用命令的排序列表
// src/commands.ts (第 586-603 行)
export const getSlashCommandToolSkills = memoize(
  async (cwd: string): Promise<Command[]> => {
    try {
      const allCommands = await getCommands(cwd)
      return allCommands.filter(/* ... */)
    } catch (error) {
      logError(toError(error))
      logForDebugging('Returning empty skills array due to load failure')
      return []  // 返回空数组而不是抛错
    }
  },
)

五、斜杠命令系统

5.1 斜杠命令的识别机制

Claude Code 的 REPL(Read-Eval-Print Loop)在每一轮输入循环中检查用户输入是否以 / 开头:

用户输入: /clear
         ↑
         斜杠前缀触发命令模式

用户输入: 帮我写一个函数
         ↑
         普通文本,发送到 LLM

这种设计使得命令系统和自然语言输入共存于同一个输入框中,用户不需要在不同的界面之间切换。

5.2 自动补全与提示

Claude Code 提供了命令自动补全功能,基于 getCommands() 返回的命令列表:

// 命令描述格式化(src/commands.ts 第 728-754 行)
export function formatDescriptionWithSource(cmd: Command): string {
  if (cmd.type !== 'prompt') return cmd.description

  if (cmd.kind === 'workflow') {
    return `${cmd.description} (workflow)`
  }

  if (cmd.source === 'plugin') {
    const pluginName = cmd.pluginInfo?.pluginManifest.name
    if (pluginName) return `(${pluginName}) ${cmd.description}`
    return `${cmd.description} (plugin)`
  }

  if (cmd.source === 'bundled') {
    return `${cmd.description} (bundled)`
  }

  return `${cmd.description} (${getSettingSourceName(cmd.source)})`
}

这个函数为自动补全下拉框提供格式化的命令描述,标注命令来源(内置、插件、打包技能等),帮助用户区分同名命令。

5.3 与主事件循环的交互

命令系统与主事件循环的交互遵循以下时序:

┌─────────────────────────────────────────────────────────────┐
│                     REPL 主事件循环                          │
├─────────────────────────────────────────────────────────────┤
│  1. 接收用户输入                                             │
│     └─> 输入以 "/" 开头?                                     │
│         ├─ 是 → 进入命令处理分支                               │
│         └─ 否 → 作为自然语言发送给 LLM                         │
│                                                              │
│  2. 命令处理(命令路径)                                       │
│     └─> 解析命令名和参数                                       │
│         └─> 调用 findCommand() 查找定义                        │
│             └─> 检查 availability / isEnabled                  │
│                 └─> 根据 type 分发执行:                        │
│                     ├─ local    → 调用 module.call()           │
│                     ├─ local-jsx → 渲染 Ink 组件               │
│                     └─ prompt   → 展开 Prompt 发送给 LLM       │
│                                                              │
│  3. 自然语言处理(LLM 路径)                                   │
│     └─> 构建消息历史                                          │
│         └─> 调用 LLM API                                      │
│             └─> 解析 LLM 响应(可能包含工具调用)               │
│                 └─> 执行工具 → 返回结果给 LLM                  │
│                     └─> 循环直到响应完成                        │
└─────────────────────────────────────────────────────────────┘

5.4 immediate 命令:绕过队列的特殊处理

某些命令(如 /exit)标记了 immediate: true,这意味着它们不需要等待当前 LLM 请求完成:

// src/commands/exit/index.ts
const exit = {
  type: 'local-jsx',
  name: 'exit',
  aliases: ['quit'],
  description: 'Exit the REPL',
  immediate: true,  // 立即执行
  load: () => import('./exit.js'),
} satisfies Command

这种设计确保了用户可以随时退出应用,即使 LLM 正在处理一个长时间运行的请求。

5.5 动态技能注入

Claude Code 支持在会话运行过程中动态发现新技能:

// src/commands.ts (第 476-517 行)
export async function getCommands(cwd: string): Promise<Command[]> {
  const allCommands = await loadAllCommands(cwd)
  const dynamicSkills = getDynamicSkills()

  const baseCommands = allCommands.filter(
    _ => meetsAvailabilityRequirement(_) && isCommandEnabled(_)
  )

  if (dynamicSkills.length === 0) return baseCommands

  // 去重:只添加不存在的动态技能
  const baseCommandNames = new Set(baseCommands.map(c => c.name))
  const uniqueDynamicSkills = dynamicSkills.filter(
    s => !baseCommandNames.has(s.name) &&
         meetsAvailabilityRequirement(s) && isCommandEnabled(s)
  )

  // 将动态技能插入到内置命令之前
  const builtInNames = new Set(COMMANDS().map(c => c.name))
  const insertIndex = baseCommands.findIndex(c => builtInNames.has(c.name))

  return [
    ...baseCommands.slice(0, insertIndex),
    ...uniqueDynamicSkills,
    ...baseCommands.slice(insertIndex),
  ]
}

动态技能的优先级高于内置命令,这意味着用户可以通过自定义技能覆盖默认行为。这种设计为 Claude Code 提供了极强的可扩展性。

六、设计亮点总结

回顾 Claude Code 的命令体系架构,以下几个设计决策值得特别关注:

6.1 懒加载(Lazy Loading)

几乎所有命令都通过 load: () => import('./xxx.js') 实现懒加载。这使得启动时间不受命令数量影响——只有实际使用的命令才会被加载到内存中。对于 /init 这类大型 Prompt 命令(其 Prompt 模板超过 200 行),懒加载的收益尤为明显。

6.2 Memoization 缓存策略

loadAllCommandsgetSkillToolCommandsgetSlashCommandToolSkills 都使用了 lodash-es/memoize 缓存。但设计者非常谨慎地处理了缓存失效:

// 清除缓存的多层设计
loadAllCommands.cache?.clear?.()      // 清除命令加载缓存
getSkillToolCommands.cache?.clear?.()  // 清除技能工具缓存
clearSkillIndexCache?.()               // 清除搜索索引缓存
clearPluginCommandCache()              // 清除插件命令缓存
clearSkillCaches()                     // 清除技能目录缓存

6.3 分层过滤架构

命令从加载到最终呈现给用户,经历了多层过滤:

  1. 来源层:静态导入 → 动态技能 → 插件 → MCP → 工作流
  2. 权限层availability 检查用户身份
  3. 功能层isEnabled 检查 feature flag
  4. 环境层:Remote/Bridge 安全白名单
  5. 用途层getSkillToolCommands 筛选模型可调用的技能

6.4 错误隔离

每个动态命令源(技能目录、插件、工作流)都有独立的错误处理。即使某个插件加载失败,其他命令仍然可以正常工作。这种"容错优先"的设计理念确保了系统的稳定性。

结语

Claude Code 的命令体系架构展现了一个成熟 AI Agent 的工程化设计:

  • 规模上:95+ 命令通过清晰的类型系统和注册机制有序管理
  • 性能上:懒加载和缓存策略保证了启动速度和运行时效率
  • 安全上:多层权限过滤和环境隔离确保了不同场景下的安全边界
  • 扩展性上:插件、技能、MCP、工作流四条扩展路径覆盖了从个人到企业的需求

对于正在构建 AI Agent 的开发者而言,Claude Code 的命令系统设计提供了一个优秀的参考范本:将用户意图(命令)与模型能力(工具)解耦,通过类型安全的中间层实现灵活而可控的交互架构。


参考资料

  • Claude Code GitHub 仓库(基于公开源码分析)
  • src/commands.ts — 命令注册中心
  • src/types/command.ts — 命令类型定义
  • src/commands/ — 各命令实现模块