Skip to content

第2章 架构总览

"好的架构不是把每个组件都设计得完美,而是让组件之间的边界清晰到你可以独立地理解和替换它们。" -- Robert C. Martin

本章要点

  • 一条消息的完整旅程:从用户在终端输入,到最终响应输出,中间经历了入口解析、消息标准化、系统提示词组装、API 流式调用、工具调用、权限检查等十余个环节
  • 六大核心子系统:CLI 入口与启动系统、Query 引擎、工具系统、权限系统、MCP 集成、IDE Bridge,各自承担明确的职责边界
  • Generator 驱动的流式管道:整个 query 循环基于 async function* 实现,数据以 yield 方式逐块流出,天然支持流式渲染和中断恢复
  • 工具即自描述对象:每个工具是一个实现了统一 Tool 接口的对象,包含 schema 定义、权限检查、执行逻辑、并发安全性声明等元信息
  • 特性标志与死代码消除:通过 Bun 的 feature() 宏在编译期裁剪代码分支,外部构建产物不包含内部实验性功能

2.1 跟踪一条消息的完整旅程

理解一个复杂系统最好的方式,不是先看它的类图或模块划分,而是跟踪一个具体的数据流从头到尾走一遍。就像理解一座城市的交通系统,最好的方法是坐一趟公交车走完全程,而不是先研究线路图。

让我们假设用户在终端输入了一句话:

> 帮我写一个快速排序函数

这条消息从键入到最终排序函数出现在屏幕上,会经历怎样的旅程?

第一站:CLI 入口接收输入

一切始于 src/entrypoints/cli.tsx。这是 Claude Code 的真正入口点。它的设计有一个精妙之处——为高频快速路径做了零依赖优化:

typescript
// src/entrypoints/cli.tsx
async function main(): Promise<void> {
  const args = process.argv.slice(2);

  // Fast-path for --version/-v: zero module loading needed
  if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
    console.log(`${MACRO.VERSION} (Claude Code)`);
    return;
  }
  // ...后续才开始加载完整的启动模块
}

当没有命中快速路径时,控制权转移到 src/main.tsx——整个应用的主编排器。main.tsx 的开头就展示了启动性能的极致追求:

typescript
// src/main.tsx (前20行)
import { profileCheckpoint } from './utils/startupProfiler.js';
profileCheckpoint('main_tsx_entry');

import { startMdmRawRead } from './utils/settings/mdm/rawRead.js';
startMdmRawRead(); // 启动 MDM 子进程,与后续 135ms 的 import 并行

import { startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js';
startKeychainPrefetch(); // 并行预取 macOS keychain 中的 OAuth 和 API key

这三条 import 被故意放在最前面,并且每条都立即执行一个副作用函数。注释清楚地解释了原因:MDM(Mobile Device Management)配置读取和 Keychain 凭证预取是耗时的 I/O 操作,通过在模块加载(约 135ms)期间并行执行它们,可以将总启动时间缩短约 65ms。

在交互模式下,main.tsx 经过一系列初始化步骤后,最终调用 launchRepl() 函数启动 React/Ink 终端 UI。Ink 是一个将 React 组件渲染到终端的库,Claude Code 用它构建了完整的终端界面——包括消息展示、进度指示、权限确认对话框等。REPL 组件接收到用户输入后,将其传递给 QueryEnginesubmitMessage() 方法,开始一轮新的对话。

值得一提的是,main.tsx 的初始化过程本身就是一个精心编排的并发流程。它在等待用户认证的同时预取 MCP 配置、加载插件、检查更新、执行配置迁移——所有这些操作通过 Promise.all 并行进行,确保用户看到交互界面的等待时间尽可能短。

第二站:消息标准化

用户的文本字符串 "帮我写一个快速排序函数" 不能直接发送给 API。它需要被包装成 Claude API 能理解的消息格式。这个标准化过程发生在 src/utils/messages.ts

typescript
// src/utils/messages.ts
import { randomUUID } from 'crypto';

// 创建一条用户消息
export function createUserMessage({
  content,
  toolUseResult,
  sourceToolAssistantUUID,
}: {
  content: ContentBlockParam[];
  toolUseResult?: string;
  sourceToolAssistantUUID?: string;
}) {
  return {
    type: 'user' as const,
    uuid: randomUUID(),
    message: { role: 'user' as const, content },
    toolUseResult,
    sourceToolAssistantUUID,
  };
}

每条消息都被赋予一个 UUID。这个 UUID 在后续的工具调用链中起到关键的追踪作用——当一个工具产生的结果需要关联回触发它的助手消息时,sourceToolAssistantUUID 就是连接它们的纽带。

第三站:系统提示词组装

在消息发送到 API 之前,需要构建系统提示词。这不是一个简单的字符串拼接,而是一个涉及多个数据源的聚合过程。src/utils/queryContext.ts 中的 fetchSystemPromptParts() 函数负责此工作:

typescript
// src/utils/queryContext.ts
export async function fetchSystemPromptParts({
  tools, mainLoopModel, additionalWorkingDirectories,
  mcpClients, customSystemPrompt,
}: { ... }): Promise<{
  defaultSystemPrompt: string[];
  userContext: { [k: string]: string };
  systemContext: { [k: string]: string };
}> {
  const [defaultSystemPrompt, userContext, systemContext] =
    await Promise.all([
      customSystemPrompt !== undefined
        ? Promise.resolve([])
        : getSystemPrompt(tools, mainLoopModel,
            additionalWorkingDirectories, mcpClients),
      getUserContext(),
      customSystemPrompt !== undefined
        ? Promise.resolve({})
        : getSystemContext(),
    ]);
  return { defaultSystemPrompt, userContext, systemContext };
}

系统提示词由三部分组成:

  1. 默认系统提示词getSystemPrompt):包含 Claude Code 的核心行为指令,由 src/constants/prompts.ts 构建。它会动态感知当前可用的工具列表、操作系统类型、Git 状态等环境信息。

  2. 用户上下文getUserContext):包括 CLAUDE.md 记忆文件的内容、当前日期等个性化信息。由 src/context.tsgetClaudeMds() 等函数收集。

  3. 系统上下文getSystemContext):包含 Git 状态、分支信息、工作目录等运行时环境信息。

这三者通过 Promise.all 并行获取——又一个性能优化的细节。注意函数签名中的 customSystemPrompt 参数:当用户通过 SDK 提供了自定义系统提示词时,默认的系统提示词和系统上下文都会被跳过。这个设计让 SDK 调用者可以完全控制模型看到的提示词,而不是在默认提示词上叠加。

系统提示词的构建由 src/constants/prompts.ts 中的 getSystemPrompt() 函数完成。它会根据当前可用的工具列表动态生成工具使用说明,根据操作系统类型调整文件路径格式,甚至根据是否处于 Git 仓库中来决定是否包含 Git 相关指令。这不是一个静态的模板,而是一个对运行时环境高度敏感的动态构建过程。

第四站:进入 Query 循环

组装完成后,控制权进入 src/query.ts 中的 query() 函数。这是整个系统最核心的函数之一。它是一个 async function*(异步 Generator),这个选择不是偶然的——Generator 天然适合表达"产生一系列结果,中间可以暂停和恢复"的语义:

typescript
// src/query.ts
export async function* query(
  params: QueryParams,
): AsyncGenerator<
  StreamEvent | RequestStartEvent | Message | TombstoneMessage
    | ToolUseSummaryMessage,
  Terminal
> {
  const consumedCommandUuids: string[] = [];
  const terminal = yield* queryLoop(params, consumedCommandUuids);
  for (const uuid of consumedCommandUuids) {
    notifyCommandLifecycle(uuid, 'completed');
  }
  return terminal;
}

queryLoop() 是一个 while(true) 无限循环。每次迭代代表一个"轮次"(turn):发送消息到 API、接收响应、如果响应包含工具调用则执行工具并继续循环;如果响应是纯文本则退出。这种"循环直到模型不再调用工具"的模式是 Agent 系统的经典范式——它让模型可以自主决定需要执行多少步操作才能完成任务,而不是由外部预先指定步骤数。

在循环开始之前,queryLoop 会构建一些重要的辅助设施:通过 buildQueryConfig() 快照一次性的环境配置(避免在循环内重复读取会变化的状态),通过 startRelevantMemoryPrefetch() 预取可能相关的记忆文件(在模型还在生成时就开始准备下一轮可能需要的上下文)。

QueryParams 的定义清楚地展示了一次 query 需要的所有输入:

typescript
// src/query.ts
export type QueryParams = {
  messages: Message[];
  systemPrompt: SystemPrompt;
  userContext: { [k: string]: string };
  systemContext: { [k: string]: string };
  canUseTool: CanUseToolFn;
  toolUseContext: ToolUseContext;
  fallbackModel?: string;
  querySource: QuerySource;
  maxTurns?: number;
  taskBudget?: { total: number };
  // ...
};

第五站:API 调用与流式响应

queryLoop 的每次迭代中,消息被发送到 Claude API。调用发生在 src/services/api/claude.ts 中。调用之前,消息经过关键的预处理:

typescript
// src/query.ts 中的调用
for await (const message of deps.callModel({
  messages: prependUserContext(messagesForQuery, userContext),
  systemPrompt: fullSystemPrompt,
  thinkingConfig: toolUseContext.options.thinkingConfig,
  tools: toolUseContext.options.tools,
  signal: toolUseContext.abortController.signal,
  options: {
    model: currentModel,
    querySource,
    // ...
  },
})) {
  // 处理每个流式事件...
}

prependUserContext 将用户上下文(如 CLAUDE.md 内容)注入到消息序列前部,利用 API 的前缀缓存机制——相同前缀的消息在后续请求中可以命中缓存,大幅降低 Token 消耗。AbortController 的 signal 被传入,使得用户按 Ctrl+C 时可以立即中断 API 调用。这种基于标准 Web API 的取消机制确保了从网络请求到工具执行的整个链路都能被即时中断。

在实际调用 API 之前,query 循环还会依次执行几个上下文管理步骤:应用工具结果预算(applyToolResultBudget,将过大的历史工具结果替换为摘要)、裁剪压缩(snipCompact,移除超出窗口的远古历史)、微压缩(microcompact,内联合并小的冗余内容)、自动压缩(autocompact,在 Token 数接近上限时触发完整的上下文总结)。这些机制确保长对话不会因为上下文溢出而崩溃,同时尽可能保留对当前任务有用的信息。

API 返回的是一个流式响应。每当收到一个 assistant 类型的消息块时,系统检查其中是否包含 tool_use 类型的内容块:

typescript
if (message.type === 'assistant') {
  assistantMessages.push(message);
  const msgToolUseBlocks = message.message.content.filter(
    content => content.type === 'tool_use',
  ) as ToolUseBlock[];
  if (msgToolUseBlocks.length > 0) {
    toolUseBlocks.push(...msgToolUseBlocks);
    needsFollowUp = true; // 标记需要继续循��
  }
}

对于我们的排序函数请求,如果模型决定直接用文本回复(内联给出排序代码),响应会被 yield 出去并最终渲染到终端。但如果模型决定先创建一个文件再写入代码(这在实际使用中更常见,因为代码通常需要保存到文件中才有用),它会返回一个包含 tool_use 块的响应。例如,模型可能会返回一个调用 FileWrite 工具的请求,参数是文件路径 sort.py 和文件内容(排序函数的代码)。

第六站:工具调用与权限检查

当检测到工具调用时,系统进入工具执行阶段。src/services/tools/toolOrchestration.ts 中的 runTools() 负责编排:

typescript
// src/services/tools/toolOrchestration.ts
export async function* runTools(
  toolUseMessages: ToolUseBlock[],
  assistantMessages: AssistantMessage[],
  canUseTool: CanUseToolFn,
  toolUseContext: ToolUseContext,
): AsyncGenerator<MessageUpdate, void> {
  let currentContext = toolUseContext;
  for (const { isConcurrencySafe, blocks } of
    partitionToolCalls(toolUseMessages, currentContext)) {
    if (isConcurrencySafe) {
      // 只读工具可以并发执行
      for await (const update of runToolsConcurrently(
        blocks, assistantMessages, canUseTool, currentContext
      )) { yield { message: update.message, newContext: currentContext }; }
    } else {
      // 非只读工具必须串行执行
      for await (const update of runToolsSerially(
        blocks, assistantMessages, canUseTool, currentContext
      )) { /* ... */ }
    }
  }
}

这里有一个重要的设计决策:工具调用被分为"并发安全"和"非并发安全"两类。读取文件、搜索代码等只读操作可以并行执行以提高效率;而写入文件、执行 shell 命令等有副作用的操作必须串行执行以保证正确性。每个工具通过 isConcurrencySafe() 方法自声明其并发安全性。

在工具真正执行之前,必须通过权限检查。src/hooks/useCanUseTool.tsx 中的 canUseTool 函数是权限系统的入口:

typescript
// src/hooks/useCanUseTool.tsx
export type CanUseToolFn = (
  tool: ToolType,
  input: Input,
  toolUseContext: ToolUseContext,
  assistantMessage: AssistantMessage,
  toolUseID: string,
  forceDecision?: PermissionDecision
) => Promise<PermissionDecision>;

权限检查是一个多阶段流水线。首先查询静态规则——这些规则来自用户配置文件、项目设置、企业策略等多个数据源,规定了哪些工具操作总是允许、总是拒绝或总是需要确认。如果静态规则无法做出判定,且当前处于 Auto 模式,系统会启动一个机器学习分类器来评估操作的安全性。分类器会分析命令的语义(例如 git status 是安全的,而 rm -rf / 是危险的)来做出判断。如果分类器也无法确定,最后才弹出交互式权限确认对话框,由用户做最终决策。只有当 result.behavior === "allow" 时,工具才会被真正执行。

这种多层级的判定管线确保了两个看似矛盾的目标之间的平衡:大多数安全操作能自动通过(用户体验流畅),而危险操作一定会被拦截(安全性有保障)。

第七站:工具执行

假设模型决定调用 FileWrite 工具来创建排序函数文件。工具执行发生在 src/services/tools/toolExecution.ts 中的 runToolUse() 函数。这是一个精心设计的多阶段流水线,每个阶段都有明确的职责和失败处理策略。执行过程经历四个阶段:

  1. PreToolUse 钩子:执行用户在 settings.json 中配置的前置钩子脚本。例如,用户可能配置了一个钩子,在每次文件写入前检查是否符合团队的代码规范。钩子可以选择阻止工具执行,也可以修改工具的输入参数。

  2. 输入校验:调用工具的 validateInput() 方法。这一步检查输入参数的格式是否合法,如文件路径是否在允许的目录内、命令字符串是否超过长度限制等。与权限检查不同,输入校验关注的是"能不能做"(技术可行性),而不是"允不允许做"(安全策略)。

  3. 工具调用:调用工具的 call() 方法,执行实际操作。对于 FileWrite,这意味着将内容写入指定路径的文件。执行过程中,工具可以通过 onProgress 回调报告进度,这些进度信息会实时显示在终端上。

  4. PostToolUse 钩子:执行后置钩子脚本。常见用途包括运行代码格式化工具(如 Prettier)、执行 lint 检查、更新索引等。钩子甚至可以修改工具的输出,例如在文件写入后自动追加版权声明。

工具的执行结果被包装成 ToolResult

typescript
// src/Tool.ts
export type ToolResult<T> = {
  data: T;
  newMessages?: (UserMessage | AssistantMessage | AttachmentMessage)[];
  contextModifier?: (context: ToolUseContext) => ToolUseContext;
};

第八站:结果返回与循环继续

工具执行的结果被转换为 tool_result 类型的用户消息,追加到消息历史中。这是 Claude API 的协议要求——每个 tool_use 块必须有一个对应的 tool_result 块,它们通过共同的 tool_use_id 关联。这种配对机制确保模型能准确知道每次工具调用的结果。

然后 queryLoopwhile(true) 进入下一次迭代:带着包含工具结果的完整消息历史,再次调用 API。模型收到工具执行结果后,可能有几种反应:

  • 任务完成:模型返回一条纯文本响应(如 "我已经为你创建了 sort.py 文件,包含快速排序的实现..."),此时 needsFollowUp 保持 false,循环终止,返回 Terminal { reason: 'end_turn' }

  • 需要更多操作:模型认为任务尚未完成,继续返回工具调用。例如,写完文件后模型可能还想运行测试来验证排序函数的正确性,这会触发 BashTool 来执行 python sort.py

  • 需要用户确认:模型可能通过 AskUserQuestionTool 向用户提问,例如 "你希望排序函数支持自定义比较函数吗?"

整个过程可能经历多个轮次。在我们的例子中,一个典型的执行轨迹可能是:

轮次1: 模型调用 FileWrite 创建 sort.py       -> 工具返回 "文件创建成功"
轮次2: 模型调用 Bash 执行 python sort.py     -> 工具返回执行结果
轮次3: 模型返回文本 "排序函数已创建并验证通过" -> 循环结束

完整旅程图

用户输入: "帮我写一个快速排序函数"
    |
    v
[cli.tsx] CLI 入口 --> [main.tsx] 主编排器
    |
    v
[replLauncher.tsx] REPL 启动 --> React/Ink UI
    |
    v
[messages.ts] createUserMessage() --> 标准化消息
    |
    v
[QueryEngine.ts] submitMessage() --> 会话管理
    |
    v
[queryContext.ts] fetchSystemPromptParts()
    |-- getSystemPrompt()    // 核心行为指令
    |-- getUserContext()      // CLAUDE.md 等
    |-- getSystemContext()    // Git 状态等
    |
    v
[query.ts] query() --> Generator 驱动的循环
    |
    v
[claude.ts] callModel() --> 流式 API 调用
    |
    v
 API 响应包含 tool_use?
    |           |
   否          是
    |           |
    v           v
 yield 文本  [toolOrchestration.ts] runTools()
    |           |
    |           v
    |     [useCanUseTool.tsx] 权限检查
    |           |
    |           v
    |     [toolExecution.ts] runToolUse()
    |           |
    |           v
    |     工具结果 --> tool_result 消息
    |           |
    |           +---> 回到 queryLoop 下一轮迭代
    |
    v
 最终文本输出到终端

下面是这条消息旅程的流程图,展示了从用户输入到最终输出的完整数据流:

2.2 六大核心子系统

了解了消息的完整旅程后,我们把视角拉远,看看整个系统由哪些子系统组成,以及它们之间的职责边界。

下面的架构图展示了六大核心子系统之间的关系与数据流向:

2.2.1 CLI 入口与启动系统

核心文件src/entrypoints/cli.tsxsrc/main.tsxsrc/entrypoints/init.ts

启动系统负责将 Claude Code 从一个静态的代码包变成一个运行中的交互式 Agent。它的职责包括:

  • 快速路径分流cli.tsx 通过检查 process.argv--version--dump-system-prompt 等快速操作提供零依赖路径,无需加载完整运行时。

  • 并行预热main.tsx 在模块导入阶段就启动 MDM 配置读取和 Keychain 凭证预取,利用模块 evaluate 时间做有用的 I/O 工作。

  • 初始化编排init.ts 中的 init() 函数(被 memoize 包装以防重复调用)执行环境检测、配置加载、代理配置、优雅退出处理、遥测初始化等一系列启动任务。

  • 迁移系统main.tsx 包含版本化的迁移逻辑(CURRENT_MIGRATION_VERSION = 11),确保用户的本地配置随版本升级平滑迁移。

  • 模式分流:根据启动参数决定进入交互模式(REPL)还是非交互模式(-p 参数的 headless 执行),或是 MCP 服务器模式、SDK 模式等。

启动系统的设计理念是"越早开始有用的工作越好"。这从第一行代码就能看出来——profileCheckpoint('main_tsx_entry') 不仅是性能监控,更是对启动延迟的严肃承诺。整个启动序列被精心安排成一个瀑布流:网络 I/O 操作(如 API 预连接、凭证获取)在最早的时间点启动,CPU 密集操作(如配置解析、工具初始化)填补 I/O 等待的空隙。

一个有趣的细节是调试保护机制。main.tsx 在外部构建中会检测是否有调试器附加,如果检测到就直接退出。这防止了通过 --inspect 标志附加调试器来逆向分析或绕过安全检查的行为,体现了生产级安全意识。

2.2.2 Query 引擎

核心文件src/QueryEngine.tssrc/query.tssrc/query/config.tssrc/query/deps.ts

Query 引擎是整个系统的心脏。它管理着一个对话的完整生命周期,核心抽象是 QueryEngine 类:

typescript
// src/QueryEngine.ts
export class QueryEngine {
  private config: QueryEngineConfig;
  private mutableMessages: Message[];
  private abortController: AbortController;
  private totalUsage: NonNullableUsage;
  private readFileState: FileStateCache;

  async *submitMessage(
    prompt: string | ContentBlockParam[],
    options?: { uuid?: string; isMeta?: boolean },
  ): AsyncGenerator<SDKMessage, void, unknown> {
    // ...
  }
}

QueryEngine 的设计有几个关键特征:

一,它是有状态的。 一个 QueryEngine 实例对应一个对话。mutableMessages 数组在多次 submitMessage() 调用之间持续积累,实现多轮对话。readFileState 缓存已读文件的内容,避免重复读取。

二,它与渲染解耦。 QueryEngine 通过 AsyncGenerator 向外 yield 事件,不直接操作 UI。这使得同一套 query 逻辑可以同时服务交互式 REPL 和非交互式 SDK/headless 模式。

三,它封装了上下文管理。 自动压缩(auto-compact)、微压缩(microcompact)、裁剪压缩(snip-compact)等上下文窗口管理策略都集成在 query 循环内部,对上层调用者透明。

QueryEnginequery() 函数的关系是一种清晰的分层:QueryEngine 负责会话级状态管理和外部接口,而 query() 函数实现了单轮对话的核心循环逻辑(API 调用 -> 工具执行 -> 继续/终止)。QueryEngine.submitMessage() 内部调用 query() 并负责消息的前后处理——包括用户输入的预处理(如斜杠命令解析、附件注入)和轮次完成后的后处理(如对话记录持久化、使用量统计更新)。

这种分层的价值在 SDK 模式下尤为明显:SDK 调用者只需创建一个 QueryEngine 实例并反复调用 submitMessage(),而不需要关心系统提示词组装、上下文窗口管理、权限状态追踪等复杂的内部机制。QueryEngine 成为了一个完整的、有状态的对话 API。

2.2.3 工具系统

核心文件src/Tool.tssrc/tools.tssrc/tools/ 目录、src/services/tools/

工具系统是 Claude Code 区别于普通聊天机器人的核心能力。Tool 类型定义了一个工具必须实现的完整契约:

typescript
// src/Tool.ts (简化)
export type Tool<Input, Output, P> = {
  readonly name: string;
  readonly inputSchema: Input;
  call(args, context, canUseTool, parentMessage, onProgress?):
    Promise<ToolResult<Output>>;
  description(input, options): Promise<string>;
  isConcurrencySafe(input): boolean;
  isEnabled(): boolean;
  isReadOnly(input): boolean;
  checkPermissions(input, context): Promise<PermissionResult>;
  validateInput?(input, context): Promise<ValidationResult>;
  maxResultSizeChars: number;
  // ...
};

这个接口的设计值得仔细品味:

  • isConcurrencySafe(input):注意参数是 input 而不是无参——同一个工具在不同输入下可能有不同的并发安全性。例如 BashTool 执行 ls 是安全的,但执行 rm 就不是。

  • isReadOnly(input):同理,只读性取决于具体输入。这个方法被工具编排器用来决定并发策略。

  • maxResultSizeChars:工具结果超过此阈值时,会被持久化到磁盘文件,Claude 只收到一个摘要和文件路径。设为 Infinity 的工具(如 FileRead)表示结果永不持久化——因为持久化一个文件读取结果会导致"读文件->保存到文件->读文件"的死循环。

  • checkPermissionsvalidateInput:两者职责不同。validateInput 判断输入格式是否合法(如路径是否存在),checkPermissions 判断操作是否被允许(如是否在允许的目录内)。

工具的注册在 src/tools.tsgetAllBaseTools() 函数中:

typescript
// src/tools.ts
export function getAllBaseTools(): Tools {
  return [
    AgentTool, TaskOutputTool, BashTool,
    ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
    FileReadTool, FileEditTool, FileWriteTool,
    NotebookEditTool, WebFetchTool, TodoWriteTool,
    WebSearchTool, SkillTool, EnterPlanModeTool,
    // 条件工具...
    ...(isWorktreeModeEnabled()
      ? [EnterWorktreeTool, ExitWorktreeTool] : []),
    ...(isToolSearchEnabledOptimistic()
      ? [ToolSearchTool] : []),
    // ...
  ];
}

工具列表是动态组合的。哪些工具可用取决于运行时环境(是否有嵌入式搜索工具)、用户配置(是否启用 Worktree 模式)、特性标志(是否启用 ToolSearch)等条件。最终,getTools() 函数还会通过 filterToolsByDenyRules() 根据权限规则进一步过滤。

工具执行的编排层位于 src/services/tools/ 目录下,包含四个关键文件:

文件职责
toolOrchestration.ts批量工具调用的编排,分区并发/串行
StreamingToolExecutor.ts流式执行器,在 API 响应流式到达时就开始执行工具
toolExecution.ts单个工具的完整执行流程(校验->权限->执行->钩子)
toolHooks.tsPreToolUse / PostToolUse 钩子的运行

StreamingToolExecutor 是一个特别巧妙的优化。传统的做法是等 API 完整响应后再开始执行工具,但 StreamingToolExecutor 在 API 响应还在流式传输时就开始执行已收到的工具调用:

typescript
// src/services/tools/StreamingToolExecutor.ts
export class StreamingToolExecutor {
  addTool(block: ToolUseBlock, assistantMessage: AssistantMessage): void {
    // 工具到达时立即加入队列并尝试执行
  }
  getCompletedResults(): MessageUpdate[] {
    // 返回已完成的工具结果,保持顺序
  }
}

2.2.4 权限系统

核心文件src/hooks/useCanUseTool.tsxsrc/utils/permissions/permissions.tssrc/utils/permissions/ 目录

权限系统保护用户免受 AI 误操作的风险。它的核心是 src/utils/permissions/permissions.ts 中的 hasPermissionsToUseTool() 函数,以及 useCanUseTool Hook。

权限系统的架构是一个多层判定管线,以下流程图展示了完整的权限决策链路:

权限上下文 ToolPermissionContext 定义了权限系统的完整状态:

typescript
// src/Tool.ts
export type ToolPermissionContext = DeepImmutable<{
  mode: PermissionMode;
  additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>;
  alwaysAllowRules: ToolPermissionRulesBySource;
  alwaysDenyRules: ToolPermissionRulesBySource;
  alwaysAskRules: ToolPermissionRulesBySource;
  isBypassPermissionsModeAvailable: boolean;
  isAutoModeAvailable?: boolean;
  shouldAvoidPermissionPrompts?: boolean;
  // ...
}>;

注意 DeepImmutable 类型包装——权限上下文是深度不可变的。修改权限状态不是原地修改,而是通过 setAppState 产生新的状态对象。这防止了权限状态被意外篡改。

src/utils/permissions/ 目录下的文件形成了一个完整的权限子系统:

文件职责
PermissionMode.ts定义权限模式(default / plan / auto / bypass)
PermissionRule.ts权限规则的类型定义
PermissionResult.ts权限判定结果的类型
permissions.ts核心权限判定逻辑
permissionsLoader.ts从配置文件加载权限规则
bashClassifier.tsBash 命令的安全分类器
denialTracking.ts拒绝次数追踪(超过阈值时降级提示)
pathValidation.ts文件路径安全校验

2.2.5 MCP 集成

核心文件src/services/mcp/client.tssrc/services/mcp/config.tssrc/services/mcp/types.ts

MCP(Model Context Protocol)是 Anthropic 提出的标准协议,允许 Claude Code 连接外部工具服务器。MCP 集成子系统的职责是将外部 MCP 服务器提供的工具无缝融入 Claude Code 的工具系统。

src/services/mcp/client.ts 是 MCP 客户端的核心实现,它使用 @modelcontextprotocol/sdk 与 MCP 服务器通信,支持三种传输方式:

typescript
// src/services/mcp/client.ts (import 部分即可看出)
import { StdioClientTransport } from
  '@modelcontextprotocol/sdk/client/stdio.js';
import { SSEClientTransport } from
  '@modelcontextprotocol/sdk/client/sse.js';
import { StreamableHTTPClientTransport } from
  '@modelcontextprotocol/sdk/client/streamableHttp.js';
  • Stdio 传输:MCP 服务器作为子进程启动,通过标准输入/输出通信
  • SSE 传输:通过 HTTP Server-Sent Events 连接远程服务器
  • StreamableHTTP 传输:更新的 HTTP 传输方案,支持双向流式通信

MCP 工具在 Claude Code 中被封装为标准的 Tool 对象(通过 MCPTool 类),与内建工具使用完全相同的权限检查和执行流程。工具名称遵循 mcp__<serverName>__<toolName> 的命名约定,通过 mcpInfo 字段保留原始的服务器名和工具名。这种封装的优雅之处在于:模型无需知道一个工具是内建的还是来自 MCP 服务器——它们在工具列表中的表现完全一致,使用相同的调用协议。

src/services/mcp/config.ts 负责从多个配置源(全局配置、项目配置、企业策略配置)解析和合并 MCP 服务器配置。企业可以通过策略配置来限制哪些 MCP 服务器是被允许的,防止用户连接到未经审查的外部服务。

除了工具之外,MCP 还支持资源(Resources)和命令(Commands)。资源是 MCP 服务器暴露的只读数据源,如数据库查询结果或 API 端点的响应。Claude Code 通过 ListMcpResourcesToolReadMcpResourceTool 让模型可以发现和读取这些资源。命令则扩展了 Claude Code 的斜杠命令系统,允许 MCP 服务器注册自定义的交互指令。

2.2.6 IDE Bridge

核心文件src/bridge/ 目录

IDE Bridge 子系统使 Claude Code 能够与 IDE(如 VS Code)协同工作。它不是一个简单的 API 调用层,而是一个完整的会话管理和消息中继系统。

src/bridge/bridgeMain.ts 是 Bridge 的入口,其导入就展示了它的复杂性:

typescript
// src/bridge/bridgeMain.ts
import { createBridgeApiClient } from './bridgeApi.js';
import { createCapacityWake } from './capacityWake.js';
import { createTokenRefreshScheduler } from './jwtUtils.js';
import { createSessionSpawner } from './sessionRunner.js';
import { getTrustedDeviceToken } from './trustedDevice.js';

Bridge 目录下的文件各司其职:

文件职责
bridgeApi.ts与 Bridge 服务器的 HTTP API 通信
bridgeConfig.tsBridge 连接配置
bridgeMessaging.ts消息的序列化与传输
bridgePermissionCallbacks.ts权限请求在 IDE 端的处理
sessionRunner.tsClaude Code 会话进程的生命周期管理
capacityWake.ts容量唤醒机制(远程会话的按需启动)
jwtUtils.tsJWT Token 的刷新调度
trustedDevice.ts设备信任认证
inboundMessages.ts来自 IDE 的入站消息处理
inboundAttachments.ts来自 IDE 的附件处理

Bridge 的设计采用了"会话即进程"模型:每个 IDE 标签页或工作区对应一个 Claude Code 进程,通过 Bridge 服务器进行消息中继。这种设计的好处是进程隔离——一个会话的崩溃不会影响其他会话。同时,每个会话的文件状态缓存、权限上下文、对话历史都是独立的,不会产生跨会话的状态污染。

Bridge 的消息流是双向的:从 IDE 到 Claude Code 的方向传递用户输入、文件变更通知、诊断信息;从 Claude Code 到 IDE 的方向传递流式文本输出、文件编辑操作、权限确认请求。bridgePermissionCallbacks.ts 特别值得关注——它实现了权限确认 UI 从终端向 IDE 的迁移,让用户可以直接在 IDE 的弹窗中审批或拒绝工具操作,而不是切换到终端窗口。

2.3 源码目录结构导航

理解一个大型项目的第一步是建立空间感。以下是 src/ 目录下每个关键目录的用途,作为你后续深入阅读的导航地图:

src/
|-- main.tsx                 # 主编排器,CLI 参数解析和启动逻辑
|-- query.ts                 # 核心 query 循环 (Generator)
|-- QueryEngine.ts           # 会话级 query 引擎
|-- Tool.ts                  # Tool 类型定义和工具相关类型
|-- tools.ts                 # 工具注册表 (getAllBaseTools)
|-- commands.ts              # 斜杠命令注册
|-- context.ts               # 系统上下文和用户上下文收集
|-- cost-tracker.ts          # API 调用成本追踪
|
|-- entrypoints/             # 入口点
|   |-- cli.tsx              # CLI 主入口 (快速路径分流)
|   |-- init.ts              # 初始化逻辑 (环境检测, 配置加载)
|   |-- sdk/                 # SDK 入口 (程序化调用)
|   |-- mcp.ts               # MCP 服务器模式入口
|
|-- tools/                   # 工具实现 (~30 个工具)
|   |-- BashTool/            # Shell 命令执行
|   |-- FileReadTool/        # 文件读取
|   |-- FileEditTool/        # 文件编辑 (基于差异)
|   |-- FileWriteTool/       # 文件写入
|   |-- GrepTool/            # 内容搜索 (ripgrep)
|   |-- GlobTool/            # 文件路径匹配
|   |-- AgentTool/           # 子 Agent 启动
|   |-- MCPTool/             # MCP 工具封装
|   |-- WebFetchTool/        # URL 内容获取
|   |-- WebSearchTool/       # 网页搜索
|   |-- ToolSearchTool/      # 延迟加载的工具搜索
|   |-- SkillTool/           # 技能执行
|   |-- NotebookEditTool/    # Jupyter Notebook 编辑
|   |-- LSPTool/             # Language Server Protocol 集成
|   |-- shared/              # 工具间共享逻辑
|   +-- ...
|
|-- services/                # 服务层
|   |-- api/                 # Claude API 调用层
|   |-- mcp/                 # MCP 客户端和配置
|   |-- tools/               # 工具编排和执行 (orchestration)
|   |-- compact/             # 上下文压缩 (auto/micro/snip/reactive)
|   |-- analytics/           # 遥测和分析
|   |-- lsp/                 # LSP 服务器管理
|   |-- oauth/               # OAuth 认证
|   |-- plugins/             # 插件管理
|   |-- policyLimits/        # 企业策略限制
|   |-- tips/                # 提示建议
|   +-- ...
|
|-- bridge/                  # IDE Bridge
|   |-- bridgeMain.ts        # Bridge 入口
|   |-- bridgeApi.ts         # Bridge API 客户端
|   |-- sessionRunner.ts     # 会话进程管理
|   +-- ...
|
|-- hooks/                   # React Hooks (权限, 状态)
|   |-- useCanUseTool.tsx    # 工具权限判定 Hook
|   +-- toolPermission/      # 权限判定子模块
|
|-- utils/                   # 工具函数 (最大的目录)
|   |-- permissions/         # 权限系统核心逻辑
|   |-- messages.ts          # 消息创建和标准化
|   |-- api.ts               # API 辅助函数
|   |-- queryContext.ts       # 系统提示词组装
|   |-- model/               # 模型选择和配置
|   |-- settings/            # 设置系统
|   |-- hooks.ts             # 用户自定义钩子执行
|   |-- Shell.ts             # Shell 命令执行
|   |-- tokens.ts            # Token 计数和估算
|   |-- git.ts               # Git 操作
|   +-- ...
|
|-- components/              # React/Ink UI 组件
|-- screens/                 # 顶层屏幕 (REPL 等)
|-- state/                   # 应用状态管理
|   |-- AppStateStore.ts     # AppState 类型定义
|   |-- store.ts             # 状态存储
|   +-- ...
|
|-- constants/               # 常量定义
|   |-- prompts.ts           # 系统提示词模板
|   |-- tools.ts             # 工具相关常量
|   +-- ...
|
|-- types/                   # 类型定义
|   |-- message.ts           # 消息类型
|   |-- permissions.ts       # 权限类型
|   +-- ...
|
|-- coordinator/             # 多 Agent 协调器
|-- skills/                  # 内建技能
|-- plugins/                 # 插件系统
|-- migrations/              # 配置迁移脚本
|-- bootstrap/               # 启动状态管理
|-- query/                   # query 子模块
|   |-- config.ts            # query 配置快照
|   |-- deps.ts              # query 依赖注入
|   |-- stopHooks.ts         # 停止钩子处理
|   +-- tokenBudget.ts       # Token 预算管理
+-- ...

2.4 核心架构模式

在六大子系统和目录结构的背后,有几个贯穿整个代码库的架构模式。理解它们,你就能预测新代码应该放在哪里,以及现有代码为什么要那样组织。

2.4.1 Generator 驱动的流式管道

Claude Code 最核心的架构模式是使用 JavaScript 的 async function*(异步 Generator)构建数据管道。从最内层的 API 流式响应,到最外层的终端渲染,整个链路都是 Generator 驱动的。

以下时序图展示了 Generator 管道中各层之间的 yield 传递过程:

以下是原始的层次结构示意:

claude.ts (API Stream)
  --> query.ts (queryLoop)
    --> QueryEngine.ts (submitMessage)
      --> REPL UI 渲染

每一层都是一个 async function*,通过 yield 向上层传递事件,通过 yield* 将内层 Generator 的输出透传。这种模式带来了三个关键好处:

一,天然流式。 用户能实时看到 Claude 的输出,而不是等到完整响应才显示。文本、思考过程、工具调用进度都能实时渲染。

二,惰性求值。 Generator 是拉模型(pull-based)——只有消费者请求下一个值时,生产者才会执行到下一个 yield。这意味着如果用户中断(Ctrl+C),已经排队但尚未消费的消息不会被处理,避免了不必要的计算。

三,组合性。 Generator 可以通过 yield* 进行组合。query() 函数将内部的 queryLoop() Generator 透传出去,同时在完成时做清理工作。这比回调嵌套或 Promise 链更清晰。

typescript
// 组合示例:query 函数透传 queryLoop 的所有输出
export async function* query(params: QueryParams) {
  const consumedCommandUuids: string[] = [];
  const terminal = yield* queryLoop(params, consumedCommandUuids);
  // 清理工作只在循环正常结束时执行
  for (const uuid of consumedCommandUuids) {
    notifyCommandLifecycle(uuid, 'completed');
  }
  return terminal;
}

2.4.2 工具即自描述对象

以下类图展示了 Tool 接口的核心结构以及典型工具的实现关系:

传统的工具系统通常是"注册函数+配置文件"的模式,但 Claude Code 的工具是自描述对象——每个工具包含了运行它所需的全部元信息。以 BashTool 为例:

typescript
// src/tools/BashTool/BashTool.tsx (结构示意)
export const BashTool: Tool = buildTool({
  name: 'Bash',
  inputSchema: z.object({
    command: z.string(),
    timeout: z.number().optional(),
  }),
  async call(args, context, canUseTool, parentMessage, onProgress) {
    // 执行 shell 命令...
  },
  async description(input, options) {
    return `执行命令: ${input.command}`;
  },
  isConcurrencySafe(input) {
    // 根据命令内容判断是否可以并发
    return isReadOnlyCommand(input.command);
  },
  isReadOnly(input) { /* ... */ },
  isEnabled() { return true; },
  async checkPermissions(input, context) { /* ... */ },
  async validateInput(input, context) { /* ... */ },
  maxResultSizeChars: 100_000,
});

这种设计的优势是工具的添加和删除不需要修改编排层代码。要添加一个新工具,只需要创建一个实现 Tool 接口的对象,然后在 tools.ts 的数组中注册即可。编排层通过接口契约(isConcurrencySafeisReadOnlycheckPermissions 等方法)自动适配新工具的行为。

更进一步,ToolSearchToolshouldDefer 属性实现了工具的延迟加载。在 ToolSearch 模式下,不常用的工具以精简的 schema 发送给模型(defer_loading: true),只有当模型通过 ToolSearch 明确请求时才发送完整 schema。这显著减少了上下文窗口的占用。

2.4.3 权限上下文栈

权限系统不是一个简单的 "allow/deny" 判定器,而是一个具有上下文感知能力的分层系统。权限规则来自多个源,优先级从高到低为:

[最高] 企业策略 (policySettings)
          |
[次高] 项目设置 (projectSettings)
          |
[中等] 用户设置 (userSettings)
          |
[较低] 本地设置 (localSettings)
          |
[最低] 会话临时授权 (session)

每个源可以定义 alwaysAllowalwaysDenyalwaysAsk 三类规则。规则匹配采用工具名+参数模式的方式,例如 Bash(git *) 表示允许所有以 git 开头的 Bash 命令。

权限上下文的不可变性(DeepImmutable)保证了规则的修改是显式的、可追踪的。整个状态通过 setAppState 函数式更新,产生新的快照,而不是原地修改。

ToolUseContext 中的 toolDecisions Map 记录了每次权限判定的结果,用于审计和调试:

typescript
toolDecisions?: Map<string, {
  source: string;
  decision: 'accept' | 'reject';
  timestamp: number;
}>;

2.4.4 特性标志与死代码消除

Claude Code 的源码中大量使用了 feature() 宏:

typescript
import { feature } from 'bun:bundle';

const reactiveCompact = feature('REACTIVE_COMPACT')
  ? require('./services/compact/reactiveCompact.js')
  : null;

const coordinatorModeModule = feature('COORDINATOR_MODE')
  ? require('./coordinator/coordinatorMode.js')
  : null;

feature() 不是运行时检查。它是 Bun 的编译期宏——在构建时,Bun 的打包器会根据构建配置将 feature('XYZ') 替换为 truefalse。替换后,未命中的分支成为死代码,被打包器完全消除。

这个模式在 src/tools.ts 中尤为密集:

typescript
const SleepTool = feature('PROACTIVE') || feature('KAIROS')
  ? require('./tools/SleepTool/SleepTool.js').SleepTool
  : null;

const cronTools = feature('AGENT_TRIGGERS')
  ? [require('./tools/ScheduleCronTool/CronCreateTool.js').CronCreateTool,
     /* ... */]
  : [];

这带来了两个好处:

一,产物精简。 外部构建不包含内部实验性功能的代码,减小了包体积。

二,边界清晰。 一个特性的所有代码都被 feature() 守卫包裹,你可以通过搜索特性名快速找到它影响的所有代码。

值得注意的是,process.env.USER_TYPE === 'ant' 也起到了类似的作用,将 Anthropic 内部专用功能(如 REPLToolTungstenTool)与外部版本隔离。

2.4.5 依赖注入与可测试性

src/query/deps.ts 展示了 Claude Code 的依赖注入策略。query() 函数的外部依赖(如 API 调用、UUID 生成、自动压缩)不是硬编码的,而是通过 QueryDeps 接口注入:

typescript
// src/query/deps.ts (推断结构)
export type QueryDeps = {
  callModel: (...) => AsyncGenerator<...>;
  uuid: () => string;
  autocompact: (...) => Promise<...>;
  microcompact: (...) => Promise<...>;
  // ...
};

export function productionDeps(): QueryDeps {
  return {
    callModel: /* 真实的 API 调用 */,
    uuid: randomUUID,
    autocompact: /* 真实的压缩逻辑 */,
    // ...
  };
}

在测试中,可以注入 mock 依赖来隔离测试 query 循环的逻辑,而无需真正调用 API。这种模式比全局 mock(如 jest.mock)更精确、更可维护。

2.5 架构全景图

将上述所有内容综合起来,以下是 Claude Code 的架构全景图:

+------------------------------------------------------------------+
|                      用户 (终端 / IDE)                            |
+------------------------------------------------------------------+
       |                        ^                       |
       | 用户输入               | 流式输出              | IDE 操作
       v                        |                       v
+------------------+   +------------------+   +------------------+
|  CLI 入口        |   |  React/Ink UI    |   |  IDE Bridge      |
|  cli.tsx         |   |  REPL 组件        |   |  bridgeMain.ts   |
|  main.tsx        |   |  components/      |   |  sessionRunner   |
+------------------+   +------------------+   +------------------+
       |                        ^                       |
       | 初始化/参数解析        | yield SDKMessage       |
       v                        |                       v
+------------------------------------------------------------------+
|                    QueryEngine (会话管理)                          |
|  QueryEngine.ts: submitMessage() -> AsyncGenerator<SDKMessage>    |
+------------------------------------------------------------------+
       |
       | 消息 + 系统提示词 + 工具上下文
       v
+------------------------------------------------------------------+
|                    Query 循环 (核心引擎)                           |
|  query.ts: queryLoop() -> while(true) { ... yield ... }          |
|                                                                   |
|  +-------------------+  +------------------+  +----------------+ |
|  | 系统提示词组装      |  | 上下文管理        |  | Token 预算     | |
|  | queryContext.ts    |  | autocompact      |  | tokenBudget   | |
|  | prompts.ts        |  | microcompact     |  |               | |
|  | context.ts        |  | snipCompact      |  |               | |
|  +-------------------+  +------------------+  +----------------+ |
+------------------------------------------------------------------+
       |                        ^
       | API 请求               | 流式响应
       v                        |
+------------------------------------------------------------------+
|                    API 调用层                                      |
|  services/api/claude.ts: callModel()                              |
|  模型选择 | 重试 | 降级 | 缓存                                    |
+------------------------------------------------------------------+
       |
       | tool_use 块
       v
+------------------------------------------------------------------+
|                    工具编排层                                      |
|  services/tools/toolOrchestration.ts: runTools()                  |
|  services/tools/StreamingToolExecutor.ts                          |
|                                                                   |
|  并发分区: [只读工具并行] [写入工具串行]                            |
+------------------------------------------------------------------+
       |                        |
       v                        v
+-------------------+  +------------------+
|  权限系统          |  |  工具执行         |
|  useCanUseTool    |  |  toolExecution   |
|  permissions.ts   |  |  toolHooks.ts    |
|                   |  |                  |
|  静态规则匹配     |  |  PreToolUse 钩子  |
|  分类器判定       |  |  validateInput   |
|  交互式确认       |  |  tool.call()     |
|                   |  |  PostToolUse 钩子 |
+-------------------+  +------------------+
       |                        |
       v                        v
+------------------------------------------------------------------+
|                    工具实现层                                      |
|  tools/BashTool   | tools/FileEditTool | tools/AgentTool | ...   |
+------------------------------------------------------------------+
       |                                           |
       v                                           v
+-------------------+                    +------------------+
|  MCP 集成         |                    |  系统资源         |
|  services/mcp/    |                    |  文件系统/Shell   |
|  MCPTool 封装     |                    |  Git/网络         |
|  多传输协议       |                    |                  |
+-------------------+                    +------------------+

以下状态机图展示了 query 循环在处理一次完整对话时的状态转换:

数据流向说明

  1. 用户输入从顶部进入,经过 CLI 入口到达 QueryEngine
  2. QueryEngine 调用 query 循环,循环内先组装系统提示词和上下文管理
  3. 带着完整消息历史调用 API,流式接收响应
  4. 如果响应包含 tool_use,进入工具编排层
  5. 工具编排层根据并发安全性分组,通过权限系统检查后执行工具
  6. 工具可以访问文件系统、Shell、网络,也可以通过 MCP 调用外部服务
  7. 工具结果回到 query 循环,触发下一轮迭代
  8. 最终的文本响应通过 Generator 的 yield 链回到 UI 层渲染

2.6 设计决策分析

为什么用 Generator 而不是 EventEmitter 或 RxJS?

Claude Code 完全可以用 EventEmitter(on('message', handler))或 RxJS(stream.pipe(map(...)).subscribe(...))来实现流式管道。但它选择了 Generator,原因在于:

控制反转的方向。 EventEmitter 是推模型——生产者主动推送事件,消费者被动接收。Generator 是拉模型——消费者主动拉取下一个值。在 Claude Code 的场景中,拉模型更合适:UI 渲染的速度决定了处理的速度,如果 UI 来不及渲染,Generator 自然暂停生产者,形成隐式的背压(backpressure)。

类型安全。 AsyncGenerator<StreamEvent | Message, Terminal> 的类型签名清楚地表达了:yield 产出的值的类型(StreamEvent | Message)和最终 return 值的类型(Terminal)。这比 EventEmitter 的字符串事件名或 RxJS 的复杂类型体操更直观。

生命周期管理。 Generator 的 return() 方法和 finally 块提供了确定性的资源清理机制。当用户中断操作时,for await...of 循环退出会自动触发 Generator 的清理逻辑,无需手动管理订阅的取消。

为什么工具系统没有使用类继承?

Tool 是一个 TypeScript 类型(接口),而不是一个抽象类。每个工具是一个普通对象或通过 buildTool() 工厂函数创建的对象,没有继承关系。

这个决策反映了组合优于继承的原则。工具之间的差异是在各个维度上独立变化的——有的工具有 isDestructive 方法,有的没有;有的工具是 MCP 工具(isMcp),有的是 LSP 工具(isLsp)。如果使用类继承,要么需要一个包含所有方法的庞大基类(大部分方法在大部分子类中是空实现),要么需要复杂的 mixin 组合。

使用接口+可选方法的模式,每个工具只需实现它实际需要的方法。buildTool() 工厂函数可以为缺失的可选方法提供默认实现,在保持灵活性的同时减少样板代码。

为什么权限检查在 React Hook 中?

useCanUseTool 是一个 React Hook,这看起来有些奇怪——权限检查不是纯逻辑吗?为什么要绑定到 React 的渲染循环?

原因在于权限确认可能需要 UI 交互。当静态规则无法判定时,系统需要向用户展示权限确认对话框,等待用户点击"允许"或"拒绝"。这个交互过程需要更新 UI 状态(显示对话框、更新确认队列),而 React 的状态管理是通过 Hook 驱动的。

useCanUseTool 接收 setToolUseConfirmQueuesetToolPermissionContext 作为参数,这使得权限确认的 UI 更新与组件的渲染周期对齐。非交互场景(如 SDK 模式)则使用不同的 handler 实现(handleSwarmWorkerPermissionhandleCoordinatorPermission),跳过 UI 环节。

为什么启动阶段有副作用 import?

main.tsx 开头的三个带副作用的 import 违反了通常的 "import 应该是无副作用的" 原则。代码中甚至有专门的 ESLint disable 注释:

typescript
// eslint-disable-next-line custom-rules/no-top-level-side-effects
startMdmRawRead();

这是一个有意识的权衡。这些 import 副作用执行的是 I/O 操作(进程启动、Keychain 读取),它们的执行时间与后续 import 的模块加载时间重叠。如果等到所有 import 完成后再调用这些函数,用户会多等 65-135ms。在 CLI 工具中,启动延迟是用户体验的关键指标,这个权衡是值得的。

代码中的注释非常详细地解释了每个副作用的原因和时序关系,这是一种负责任的做法——当你必须违反常规时,确保后来者能理解你为什么这样做。

2.7 本章小结

本章我们完成了对 Claude Code 架构的全景巡览。让我们回顾关键要点:

一条消息的旅程 揭示了系统的运行时行为:从 CLI 入口,经过消息标准化、系统提示词组装、query 循环、API 调用、工具编排、权限检查、工具执行,最终回到终端。这不是线性的流水线,而是一个可能多次迭代的循环——每次工具调用都会触发新一轮 API 请求,直到模型认为任务完成。

六大子系统 构成了系统的静态骨架:CLI 入口负责启动和模式分流,Query 引擎管理会话状态,工具系统提供可扩展的能力抽象,权限系统保障安全边界,MCP 集成连接外部世界,IDE Bridge 实现与开发环境的协同。

四个架构模式 是理解代码组织的钥匙:Generator 流式管道使数据在各层间自然流动,工具自描述对象使系统可扩展而无需修改编排层,权限上下文栈实现了多源规则的分层聚合,特性标志与死代码消除在一套代码库中支撑了多种构建产物。

理解了架构全景之后,你应该已经对 Claude Code 的整体设计有了清晰的心智模型。当你在后续章节中深入某个子系统时,可以随时回到本章的全景图来定位自己的位置,理解当前子系统与其他部分的关系。

在接下来的章节中,我们将深入每个子系统的内部实现。第3章将聚焦 CLI 启动过程的细节——从进程启动到第一个用户提示符出现之间的每一个步骤。第4章将深入 Query 引擎的循环机制,包括上下文窗口管理的多种策略。第5章将解析流式响应的处理管道。每一章都会像本章跟踪消息旅程那样,以具体的代码路径为线索,揭示设计决策背后的工程思考。

建议读者在阅读后续章节时保持源码编辑器打开,对照本章给出的文件路径自行探索。阅读源码不是线性的——你会不断在文件之间跳转,而本章建立的导航地图正是为此而设。

基于 VitePress 构建