Claude Code 源码剖析 模块一 · 第一节:Claude Code 宏观架构

第一阶段 · 模块一 · 第一节:Claude Code 全局架构

核心问题

Claude Code 的三层架构是什么?cli.tsx、main.tsx、QueryEngine、query.ts 各自负责什么?它们如何协作?


◇ 本节位置

复制代码
Claude Code 全局架构

┌─────────────────────────────────────────────────────────────────────┐
│  入口层(entrypoints/)                                             │
│                                                                      │
│  cli.tsx ──> main.tsx ──> REPL.tsx (交互模式)                      │
│                     └──> QueryEngine.ts (SDK/headless)              │
└──────────────────────────────┬──────────────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────────────┐
│  查询引擎层                                                          │
│                                                                      │
│  QueryEngine.ts ──> query.ts (核心循环)                             │
│                     ├── callModel() 调用 Claude API                 │
│                     ├── StreamingToolExecutor 并行工具执行           │
│                     └── yield SDKMessage 流式输出                    │
└──────────────────────────────┬──────────────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────────────┐
│  工具/服务/状态层                                                    │
│                                                                      │
│  tools.ts (40+工具) / commands.ts (~80命令) / services/            │
│  context.ts (上下文) / cost-tracker.ts (成本追踪)                   │
└─────────────────────────────────────────────────────────────────────┘

本节内容:三层架构概览与入口层分析

一、入口层:cli.tsx 的 bootstrap 模式

1.1 cli.tsx 的核心职责

源码位置src/entrypoints/cli.tsx 第 28-45 行

cli.tsx 是整个 Claude Code 的程序入口 ,它的核心职责是:在加载完整 CLI 之前,先检查特殊标志

typescript 复制代码
/**
 * Bootstrap entrypoint - checks for special flags before loading the full CLI.
 * All imports are dynamic to minimize module evaluation for fast paths.
 * Fast-path for --version has zero imports beyond this file.
 */
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' || args[0] === '-V')) {
    // MACRO.VERSION is inlined at build time
    console.log(`${MACRO.VERSION} (Claude Code)`);
    return;  // ⚠️ 关键:完全不加载任何模块
  }

  // 其他快速路径:--dump-system-prompt, --daemon, --bg 等

  // 最后:加载完整 CLI
  const { main: cliMain } = await import('../main.js');
  await cliMain();
}

1.2 为什么需要 bootstrap?

问 1:为什么 --version 需要零模块加载?

用户期望 --version 立即响应(毫秒级)。如果加载完整模块:

复制代码
claude --version (静态导入)
    │
    ├── 加载 main.tsx(4683行)
    ├── 加载 QueryEngine.ts(1295行)
    ├── 加载 query.ts(1729行)
    ├── 加载 tools.ts(40+工具)
    ├── 加载 commands.ts(~80命令)
    ├── 加载所有 services/
    └── 打印版本
    │
    └── 耗时:数百毫秒

零模块加载则:

复制代码
claude --version (cli.tsx bootstrap)
    │
    ├── 读取 process.argv
    ├── 匹配 --version
    ├── 打印 MACRO.VERSION
    └── 耗时:<10毫秒

问 2:MACRO.VERSION 是什么?

typescript 复制代码
// MACRO 是 Bun 的编译时内建变量
// 在构建时从 package.json + git describe 注入
console.log(`${MACRO.VERSION} (Claude Code)`);
// 输出:claude-code 2.1.88

问 3:动态导入的本质是什么?

typescript 复制代码
// 静态导入:编译时确定,运行时立即加载
import { main } from '../main.js';  // 立即加载所有模块

// 动态导入:运行时确定,按需加载
const { main } = await import('../main.js');  // 执行到这里才加载

1.3 cli.tsx 的完整快速路径

源码位置src/entrypoints/cli.tsx 第 28-302 行

复制代码
cli.tsx main()
    │
    ├── --version / -v / -V
    │     └── console.log(MACRO.VERSION)  ← 零模块加载
    │
    ├── --dump-system-prompt
    │     └── 加载 config.js + prompts.js
    │
    ├── --daemon
    │     └── 加载 daemon/main.js
    │
    ├── --bg / ps / logs / attach / kill
    │     └── 加载 cli/bg.js
    │
    ├── --new / --list / --reply
    │     └── 加载 cli/handlers/templateJobs.js
    │
    └── [其他命令]
          └── 加载 main.js
                └── 启动完整 CLI

二、主程序层:main.tsx

2.1 main.tsx 的角色

源码位置src/main.tsx

main.tsx 是一个 4683 行的大文件,包含:

  • Commander.js 程序定义
  • 所有 action handler(claude [prompt]claude --resume 等)
  • 子命令处理(claude mcp install
  • 初始化逻辑(init()

2.2 preAction 钩子:惰性初始化

问 1:为什么 --help 不触发初始化?

typescript 复制代码
async function run(): Promise<CommanderCommand> {
  const program = new CommanderCommand();

  // preAction 钩子:在真正执行命令之前触发
  program.hook('preAction', async () => {
    await Promise.all([
      ensureMdmSettingsLoaded(),
      ensureKeychainPrefetchCompleted()
    ]);
    await init();  // 只有执行实际命令才触发
  });

  // 注册 CLI 选项
  program
    .name('claude')
    .option('-p, --print', '...')
    .option('--model <model>', '...')
    // ... 80+ 个选项
    .action(async (prompt, options) => {
      await launchRepl(...);  // 实际执行业务逻辑
    });

  return program.parse();
}
用户输入 Commander.js 行为 preAction 触发?
claude --help 直接显示帮助 ✗ 不触发
claude "hello" 执行 action ✓ 触发
claude --version cli.tsx 已处理 ✗ 不触发

问 2:init() 函数的职责是什么?

init() 是 Claude Code 的核心初始化函数,负责任务:

  • 加载用户配置(~/.claude/)
  • 检查 API key 和认证状态
  • 初始化 MCP 客户端
  • 加载插件和技能

三、查询引擎层:QueryEngine vs query()

3.1 两层分离的设计

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│  QueryEngine.ts (SDK/headless 封装)                                │
│                                                                      │
│  class QueryEngine {                                                │
│    private mutableMessages: Message[]  // 会话状态                  │
│    private permissionDenials: SDKPermissionDenial[]  // 权限记录   │
│    private totalUsage: NonNullableUsage  // 使用量追踪              │
│                                                                      │
│    async *submitMessage(prompt, options) {                          │
│      // 1. 包装 canUseTool,记录权限拒绝                            │
│      // 2. 调用 query() 核心循环                                    │
│      // 3. session 持久化                                           │
│      // 4. yield SDK 格式消息                                       │
│    }                                                                │
│  }                                                                  │
└──────────────────────────────┬──────────────────────────────────────┘
                               │
                               │ 调用
                               ▼
┌─────────────────────────────────────────────────────────────────────┐
│  query.ts (核心循环)                                                │
│                                                                      │
│  while (true) {  // 第 307 行                                       │
│    // 1. 状态解构                                                   │
│    // 2. 调用 Claude API(流式)                                     │
│    // 3. 检查是否需要工具调用                                       │
│    // 4. 执行工具(StreamingToolExecutor 并行)                    │
│    // 5. 状态转移,继续循环                                         │
│  }                                                                  │
└─────────────────────────────────────────────────────────────────────┘

3.2 QueryEngine 的职责

源码位置src/QueryEngine.ts 第 184-220 行

typescript 复制代码
export class QueryEngine {
  // 会话状态
  private mutableMessages: Message[] = []
  private permissionDenials: SDKPermissionDenial[] = []
  private totalUsage: NonNullableUsage = EMPTY_USAGE

  async *submitMessage(
    prompt: string | ContentBlockParam[],
    options?: { uuid?: string; isMeta?: boolean },
  ): AsyncGenerator<SDKMessage, void, unknown> {
    // 1. 包装 canUseTool,记录权限拒绝
    const wrappedCanUseTool: CanUseToolFn = async (tool, input, toolUseContext) => {
      const result = await canUseTool(tool, input, toolUseContext)
      if (!result.allowed) {
        this.permissionDenials.push({ tool, input, toolUseID })
      }
      return result
    }

    // 2. 调用核心循环
    for await (const message of query({
      messages: this.mutableMessages,
      canUseTool: wrappedCanUseTool,
      // ...
    })) {
      // 3. session 持久化
      if (options?.persistSession && message.type === 'assistant') {
        await recordTranscript(this.mutableMessages)
      }
      // 4. yield SDK 格式消息
      yield toSDKMessage(message)
    }
  }
}

问 1:为什么需要 QueryEngine 封装?

问题 原因
query() 是纯函数 状态通过参数传递,不能自己管理 session
SDK 需要持久化 session 跨调用持久化
权限拒绝需要记录 SDK 用户可能需要查询被拒绝的工具

问 2:wrappedCanUseTool 的作用?

typescript 复制代码
const wrappedCanUseTool: CanUseToolFn = async (tool, input, toolUseContext) => {
  const result = await canUseTool(tool, input, toolUseContext)  // 原始权限检查
  if (!result.allowed) {
    this.permissionDenials.push({ tool, input, toolUseID })  // 记录拒绝
  }
  return result
}
  • 包装原始的 canUseTool
  • 添加记录权限拒绝的副作用
  • SDK 可以通过 permissionDenials 查询哪些工具被拒绝

3.3 query.ts 的核心循环

源码位置src/query.ts 第 307 行

typescript 复制代码
// eslint-disable-next-line no-constant-condition
while (true) {
  // 1. 状态解构:从 state 中读取最新状态
  let { toolUseContext } = state
  const {
    messages,
    turnCount,
    autoCompactTracking,
    // ...
  } = state

  // 2. 初始化:每轮循环的临时状态
  const assistantMessages: AssistantMessage[] = []
  const toolResults: (UserMessage | AttachmentMessage)[] = []
  let needsFollowUp = false

  // 3. 调用 Claude API(流式)
  for await (const message of deps.callModel({
    messages: prependUserContext(messagesForQuery, userContext),
    systemPrompt: fullSystemPrompt,
    tools: toolUseContext.options.tools,
    signal: toolUseContext.abortController.signal,
    // ...
  })) {
    // 处理每个流式消息
    if (message.type === 'assistant') {
      assistantMessages.push(message)
      // 检查是否有 tool_use blocks
      if (hasToolUseBlocks(message)) {
        needsFollowUp = true
      }
    }
  }

  // 4. 检查退出条件
  if (!needsFollowUp) {
    return { reason: 'completed' }  // 没有工具调用,正常完成
  }

  // 5. 执行工具
  const toolUpdates = streamingToolExecutor
    ? streamingToolExecutor.getRemainingResults()
    : runTools(toolUseBlocks, ...)

  for await (const update of toolUpdates) {
    if (update.message) {
      toolResults.push(update.message)
    }
  }

  // 6. 状态转移:更新 state,继续循环
  state = {
    messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
    turnCount: nextTurnCount,
    // ...
  }
} // while (true) 结束

问:为什么用 while(true) 而不是递归?

方案 问题
递归 长时间会话会栈溢出(每轮调用累积栈帧)
while(true) 状态在堆上,无栈溢出风险
typescript 复制代码
// 递归方案
async function query(state) {
  const nextState = computeNext(state)
  return query(nextState)  // ⚠️ 每次递归累积一个栈帧
}

// while(true) 方案
while (true) {
  state = computeNext(state)  // ✅ 状态在堆上
}

四、工具/服务/状态层

4.1 工具系统

源码位置src/tools.ts

Claude Code 内置 40+ 工具:

工具 作用
Read 读取文件
Write 写入文件
Bash 执行 shell 命令
Glob 文件模式匹配
Grep 文本搜索
WebSearch 网络搜索
... ...

4.2 命令系统

源码位置src/commands.ts

Claude Code 支持 ~80 个斜杠命令:

命令 作用
/help 显示帮助
/clear 清空会话
/compact 压缩上下文
/model 切换模型
... ...

五、设计模式总结

5.1 责任链模式

复制代码
cli.tsx (bootstrap)
    ↓
main.tsx (程序入口)
    ↓
QueryEngine (SDK 封装)
    ↓
query (核心循环)

每层只负责自己的职责,下一层不知道上一层的存在。

5.2 工厂模式

typescript 复制代码
const streamingToolExecutor = config.gates.streamingToolExecution
  ? new StreamingToolExecutor(...)
  : null

根据条件创建不同的执行器。

5.3 状态机模式

typescript 复制代码
while (true) {
  state = computeNext(state)  // 状态转移
}

六、思考题

思考题 1:动态导入的风险

问题 :如果 main.js 加载失败,用户的体验是什么?如何改进?

答案

用户会看到难以理解的错误:

复制代码
Error: Cannot find module '../main.js'
SyntaxError: /path/to/main.js:123

改进方案:

typescript 复制代码
try {
  const { main: cliMain } = await import('../main.js');
  await cliMain();
} catch (error) {
  exitWithError(
    `Failed to load Claude Code. This may be caused by a corrupted installation.\n` +
    `Try reinstalling: npm install -g @anthropic-ai/claude-code\n` +
    `Error: ${error.message}`
  );
}

思考题 2:三层 vs 两层

问题:如果只有 cli.tsx → query(两层),有什么问题?

答案

  1. session 无法持久化:query() 每次调用都是新的会话
  2. SDK 模式无法实现:SDK 需要管理会话状态
  3. 权限拒绝无法追踪:无法记录用户拒绝了哪些工具
  4. 代码耦合:CLI 逻辑和核心循环混在一起

思考题 3:while(true) 的退出条件

问题:query.ts 的 while(true) 循环有哪些退出条件?

答案

退出条件 触发场景
{ reason: 'completed' } 没有工具调用,正常完成
{ reason: 'max_turns' } 达到最大轮数限制
{ reason: 'aborted_streaming' } 用户中断
{ reason: 'model_error' } API 调用错误
{ reason: 'stop_hook_prevented' } stop hook 阻止执行

七、延伸阅读

文件 行数 核心内容
src/entrypoints/cli.tsx 302 Bootstrap 入口
src/main.tsx 4683 主程序
src/QueryEngine.ts 1295 SDK 封装
src/query.ts 1729 核心循环

八、下节预告

下一节我们将深入 cli.tsx 的 bootstrap 机制

  • 动态导入的详细分析
  • feature() Bun DCE 的作用
  • 快速路径的扩展规则

相关推荐
一直会游泳的小猫2 小时前
everything-claude-code-使用指南
plugin·ecc·claude code·claude plugin
温九味闻醉2 小时前
人工智能应用作业1:PPO强化学习算法
人工智能·算法
安科士andxe2 小时前
实践指南|安科士SFP-10/25G-LR-S-I光模块部署与运维技巧
运维·人工智能·5g
AI360labs_atyun2 小时前
我在命令行里养了只电子宠物,还顺便学会了Claude Code
人工智能·科技·学习·ai·宠物
dydm_131282 小时前
笔尖下的奇迹:当AI实时绘画“撞见”未来教育
人工智能
ai产品老杨2 小时前
异构计算时代的视频底座:基于 X86/ARM 与 GPU/NPU 的边缘云协同架构解析
arm开发·架构·音视频
CanCanCanedFish2 小时前
快速解决OpenCode配置第三方API
人工智能·ai
波动几何2 小时前
IntelGrid — 9 层工具架构的 AI Agent 框架
人工智能
lcjt2 小时前
RTX5060+ubuntu22.04尝试宇树G1踩坑
人工智能