第一章:我是如何剖析 Claude Code 整体架构与启动流程的

前几天不是 Claude Code 泄露了吗,存了下来一直也没来的及学习,刚好这几天抽空学习了一下,分享给大家,如有说的不对的地方还望大家纠正,一起学习。

大家好。今天,我们将一起开启探索 Claude Code 源码的旅程。作为 Anthropic 官方推出的 AI 编程助手,Claude Code 的底层架构设计得非常精妙。

接下来,我将带大家深入了解 Claude Code 的生命周期整体架构。学习完这章后,你会对以下内容有清晰的认识:

  1. Claude Code 是如何分层的?(揭秘四大核心层级)
  2. 项目里用到了哪 5 大核心设计模式?
  3. 从我们在终端敲下 claude 命令开始,到出现交互界面,它到底经历了什么?(尤其是它牛逼的"并行预加载"和"Fast-path"性能优化策略)

一、宏观视角:Claude Code 的分层架构

Claude Code 是一个用 TypeScript 和 React 编写的高度模块化应用。第一次看源码时,我发现它的代码组织非常有条理,整体可以清晰地划分为四层

  • 表现层 (Presentation) :你可以把它理解为"外衣"。
    • 比如 REPL.tsxMessages.tsx 这些文件。
    • 它们主要负责和我们用户进行交互,并把复杂的终端 UI 渲染出来(底层用的是自研的类似 Ink 的渲染框架)。
  • 应用层 (Application) :这里是大脑的"前额叶"。
    • 包含 QueryEngine.tscommands.ts
    • 它们负责管理整个对话的生命周期,
    • 以及处理我们输入的斜杠命令(比如 /help/compact)。
  • 服务层 (Services) :这里是"外交部"。
    • 主要文件在 services/ 目录下,比如 api/claude.tsmcp/client.ts
    • 它们负责和外部系统打交道,比如请求大模型 API、建立 MCP(Model Context Protocol)协议连接,
    • 或者处理上下文压缩。
  • 基础设施层 (Infrastructure) :这是最底层的"地基"。
    • 包括了工具接口定义 Tool.ts
    • 钩子系统 hooks.ts
    • 权限校验机制 permissions/
    • 以及全局状态管理 state/

二、精妙绝伦:5 大核心设计模式

在阅读源码的过程中,我总结出了贯穿整个项目的 5 大核心设计模式。理解了它们,你就能把握住 Claude Code 的代码灵魂:

  1. 工具驱动模式 (Tool-Based Pattern)
    • 这是我觉得最精彩的一点!在 Claude Code 里,"万物皆工具"。
    • 不管是执行 Bash 命令、读写文件,还是连接 MCP 服务器,所有的能力全都被抽象成了一个个实现了 Tool 接口的模块。
    • 这种设计让工具的定义、权限校验、执行和渲染形成了统一的闭环。
  2. 依赖注入模式 (Dependency Injection)
    • 每次执行工具时,系统都不会让工具去"硬找"它需要的数据,
    • 而是主动把一个叫 ToolUseContext 的大礼包(包含当前状态、文件缓存等)"注入"给它。
    • 这就避免了代码里到处都是死板的硬编码。
  3. 事件驱动模式 (Event-Driven)
    • 系统里埋了 20 多种生命周期钩子(Hooks),比如 PreToolUse(工具执行前)、PostCompact(上下文压缩后)。
    • 这种事件分发机制,让系统的扩展性变得极其强大。
  4. 观察者模式 (Observer)
    • 系统通过一个 onChangeAppState 函数,允许各个组件像订阅报纸一样,实时监听全局状态(AppState)的变化,一有风吹草动就能立刻做出反应。
  5. 策略模式 (Strategy)
    • 它的权限系统非常灵活。
    • 支持 default(默认询问)、plan(先计划后执行)、auto(自动判断)甚至 acceptEdits(自动接受修改)等多种策略。
    • 系统会根据当前的权限模式采用不同的处理逻辑。

三、庖丁解牛:源码启动流程分析

说完了理论,我们来看看真实的源码。当你按下回车键启动 Claude Code 时,代码是怎么跑起来的?

1. 极速通道:src/entrypoints/cli.tsx (Fast-path 路由)

这个文件是整个程序的物理入口。我发现它的核心秘诀就一个字: 。为了避免终端白屏,它在文件最顶层没有任何大体积的 import 语句。

typescript 复制代码
async function main(): Promise<void> {
  const args = process.argv.slice(2);

  // 【Fast-path 1】:如果你只是想看版本号(--version)
  // 它会瞬间打印并退出,根本不加载外部模块,耗时极短!
  if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
    console.log(`${MACRO.VERSION} (Claude Code)`);
    return;
  }

  // 加载性能探针,用来记录启动耗时
  const { profileCheckpoint } = await import('../utils/startupProfiler.js');
  profileCheckpoint('cli_entry');

  // 【Fast-path 2】:如果你是在运行后台 Session 管理命令 (如 ps, logs)
  if (feature('BG_SESSIONS') && (args[0] === 'ps' || args[0] === 'logs')) {
    // 这里只会按需加载最小化的配置模块
    // ...
  }

  // 兜底逻辑:如果不是上述特殊情况,那就老老实实加载主应用
  const { startCapturingEarlyInput } = await import('../utils/earlyInput.js');
  startCapturingEarlyInput(); // 提前把你的键盘输入存起来,防止卡顿时吞键
  const { main: cliMain } = await import('../main.js');
  await cliMain();
}

2. 引擎点火:src/main.tsx (主入口启动与并行预加载)

如果没有命中 Fast-path,程序就会通过 cli.tsx 动态导入 main.tsx 并进入 main()run() 核心函数。

这里的启动流程极其讲究,它并没有傻傻地排队执行任务,而是巧妙利用了 Node.js 的模块加载机制和异步特性来进行并行预加载(Prefetch)

我给大家梳理了从敲下回车到界面渲染的关键节点(这部分代码在 main.tsx 中长达 4000 多行):

  1. 提前捕获用户输入(防吞键) :在正式加载 main.tsx 及其庞大依赖前(位于 cli.tsx 中),系统会提前调用 startCapturingEarlyInput()

    • 因为后续的初始化需要几百毫秒,如果不做拦截,用户手快敲击的键盘输入就会全部丢失。
  2. 顶层副作用抢跑耗时 I/O :一进入 main.tsx(甚至在大部分 import 语句前),程序就利用顶层副作用 (Top-level side-effects)直接触发了 startMdmRawRead()(读取企业 MDM 配置)和 startKeychainPrefetch()(预取系统凭证)。

    • 为什么要这样抢跑?因为像去 Keychain 读密码、发网络请求拿配置,这些都属于"阻塞型 I/O 操作"。
    • 如果用常规的 await 等这些 I/O 做完,再去解析加载好几兆的 JS 源码模块,那你在终端敲下命令后,就会看到好几秒的黑屏(这就是让人头疼的冷启动延迟)。
  3. 加载大模块与 Promise 汇合 :为了解决冷启动延迟,在上述 I/O 操作跑在后台的间隙,Node.js 引擎继续解析并加载 main.tsx 依赖的 Commander、React 以及海量的服务模块(这个过程大约消耗 135 毫秒)。

    • 等模块加载完进入 Commander 的 preAction 钩子时,才利用 Promise.all 等待之前的耗时 I/O 结果。
    • 这就好比你一边在锅里烧水(I/O 操作),一边在案板上切菜(代码解析),水烧开了菜也切好了,时间省了一半!
    • Node.js 单线程是怎么做到并行的?
    • 秘密在于 Node.js 的非阻塞 I/O 模型。
    • 解析 JavaScript 代码(切菜)确实只能由 V8 主线程这一个"厨师"来做,但读取凭证、拉取配置这些耗时的 I/O 操作(烧水)是交由操作系统或底层的 libuv 线程池去完成的。
    • 所以,主线程在触发 I/O 请求后并不会傻等,而是立刻回头去解析并加载 Commander、React 等海量的服务模块(这个过程需要消耗 CPU,约 135 毫秒)。
    • 等到主线程把所有模块解析完,进入 Commander 的 preAction 钩子时,才通过 Promise.all 去"揭开锅盖"看看之前交派给操作系统的 I/O 任务完成了没有。
    • 这样就把 CPU 密集型操作和 I/O 密集型操作完美重叠,大大缩短了启动时间。
  4. 初始化核心组件与扫描扩展 :接着执行 init()(配置优雅退出、初始化 OpenTelemetry 遥测、应用环境变量),

    • 并在主体逻辑中异步扫描加载 GrowthBook 特性开关、MCP 配置、Agents、Skills 和 Plugins,
    • 这一切都尽可能让 I/O 与 CPU 操作重叠。
  5. 挂载 UI 或进入后台 :最后,根据是否传入了 -p/--print 或其他非交互参数,决定是动态引入 src/cli/print.js 并调用 runHeadless() 进入无头模式(常用于脚本管道),

    • 还是调用 launchRepl() 把基于 React/Ink 编写的终端 UI 正式挂载到你的屏幕上。

3. 中枢神经:全局状态管理 (AppState)

在这个过程中,你可能会好奇,这些复杂的 UI、后台任务、甚至是不同的 Agents 之间,数据是怎么共享和流转的? 翻看源码你会发现,Claude Code 并没有引入 Redux 或 Zustand 这样庞大的第三方库,而是基于 React 18 的 useSyncExternalStoreContext,自己手搓了一个极度精简但五脏俱全的响应式状态管理(位于 src/state/AppStateStore.ts)。

我们可以看看它的核心结构(去掉了大量细节后的骨架):

typescript 复制代码
export type AppState = DeepImmutable<{
  settings: SettingsJson;           // 个性化设置与 Feature Flags
  verbose: boolean;                 // Debug 模式开关
  mainLoopModel: ModelSetting;      // 当前交互主循环使用的 AI 模型
  tasks: { [taskId: string]: TaskState }; // 后台执行的任务树(支持嵌套)
  agentNameRegistry: Map<string, AgentId>; // 存活的子代理(Agents)花名册
  toolPermissionContext: ToolPermissionContext; // 工具调用的权限拦截配置
  replBridgeConnected: boolean;     // 远程控制(Web 桥接)的连接状态
  // ... 其他诸如 MCP 状态、终端 UI 焦点等
}>;

这里的精妙之处在于两点:

  1. DeepImmutable 深度只读 :在 TypeScript 层面锁死了状态的可变性,任何状态更新都必须通过 useSetAppState 返回的函数去派发一个新对象。
  2. 细粒度的重渲染控制 :通过自定义的 useAppState(selector) 钩子,组件只会订阅它关心的那一部分状态。比如只有在模型切换时,顶部的状态栏才会重新渲染,绝不波及庞大的对话历史列表。

甚至当 Claude Code 派生出子代理(Forked Agent)去解决某个复杂的子任务时,它也会利用类似 parentContext.setAppState 的机制,把子代理的状态巧妙地"冒泡"同步回主应用的 AppState 里,让你在终端能实时看到子任务的进度条。

四、动手实践:加一个自定义的 Fast-path 命令

光说不练假把式。假设我们现在想给 Claude Code 加一个真正的 Fast-path 命令:claude --health,用来极速检查本地网络和 API 连通性,且不能有一丝一毫的启动卡顿。

按照我们刚才学到的原理,我们需要直接修改 src/entrypoints/cli.tsxmain() 函数,把它插在加载庞大的 main.tsx 之前:

typescript 复制代码
// 打开 src/entrypoints/cli.tsx,在 main() 函数中,处理完 --version 之后添加:

async function main(): Promise<void> {
  const args = process.argv.slice(2);
  
  // 现有的 --version fast-path...
  
  // 🟢 我们的实战代码:极速健康检查 Fast-path
  if (args.length === 1 && (args[0] === '--health' || args[0] === '-h')) {
    // 动态引入最小依赖,绝不提前加载 React 或大文件
    const { profileCheckpoint } = await import('../utils/startupProfiler.js');
    profileCheckpoint('health_check_start');
    
    console.log('🔍 正在极速检查连通性...');
    try {
      // 仅加载发起请求所需的极简模块
      const { fetch } = await import('../utils/fetch.js');
      const start = Date.now();
      const res = await fetch('https://api.anthropic.com/v1/health', { 
        method: 'GET',
        signal: AbortSignal.timeout(3000)
      });
      
      if (res.ok) {
        console.log(`✅ Anthropic API 畅通无阻 (延迟: ${Date.now() - start}ms)`);
        process.exit(0);
      }
      throw new Error(`HTTP ${res.status}`);
    } catch (err) {
      console.log('❌ 无法连接到 API,请检查网络或代理设置。');
      process.exit(1);
    }
  }

  // 现有的其他 Fast-path 和最终兜底加载 main.tsx 的逻辑...

为什么这段代码能快到飞起?

  1. 0 成本的入口拦截 :我们在 cli.tsx 顶层直接判断 argv,在 Node.js 刚启动不到 20ms 的时候就劫持了流程。
  2. 纯动态的 import() :只加载了 startupProfiler.jsfetch.js,完全绕过了 Commander 解析器、React 渲染引擎以及成百上千个功能模块。
  3. 干净利落的 process.exit(0):办完事直接杀进程,不留任何后遗症。

这就是 Claude Code 处理诸如 claude ps (查看后台任务)、claude environment-runner 等内置极速命令的真实套路!


五、小结

今天先到这,我们已经学到了很多。Claude Code 源码的旅程刚刚开始。后边会继续.... 一键三连,关注不迷路。


六、推荐阅读

如果你想更深入地理解本章涉及的技术细节,我强烈建议你看看这些资料:

相关推荐
竹林8182 小时前
从“连接失败”到丝滑登录:我用 ethers.js v6 搞定 MetaMask 钱包连接的全过程
前端·javascript
树獭叔叔2 小时前
Claude Code 工具系统深度剖析:从静态注册到动态发现
后端·aigc·openai
oi..2 小时前
《Web 安全入门|XSS 漏洞原理、CSP 策略与 HttpOnly 防护实践》
前端·网络·测试工具·安全·web安全·xss
UXbot2 小时前
2026年AI全链路产品开发工具对比:5款从创意到上线一站式平台深度解析
前端·ui·kotlin·软件构建·swift·原型模式
云边云科技_云网融合2 小时前
详解Token经济:智能时代的价值标尺与产业全链路重构
人工智能·aigc·token
树獭叔叔2 小时前
Claude Code 的上下文管理:多层渐进式压缩架构深度解析
后端·aigc·openai
AI攻城狮2 小时前
Anthropic 开源了 Claude 的 Agent Skills 仓库:文档技能的底层实现全公开了
人工智能·云原生·aigc
一拳不是超人2 小时前
前端工程师也要懂的服务器部署知识:从 Nginx 到 CI/CD
服务器·前端
AlkaidSTART2 小时前
TanStack Query 技术指南:异步状态管理核心实践
前端·react.js