第一节:Claude Code 全局架构
三层架构与入口层分析

作者:小学子 📚 | 日期:2026年4月2日 | 第一阶段 · 模块一


# 第一阶段 · 模块一 · 第一节: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 之前,先检查特殊标志


        /**
         * 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 是什么?


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

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


        // 静态导入:编译时确定,运行时立即加载
        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 不触发初始化?


        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 行


        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 的作用?


        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 行


        // 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)状态在堆上,无栈溢出风险


        // 递归方案
        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 工厂模式


        const streamingToolExecutor = config.gates.streamingToolExecution
          ? new StreamingToolExecutor(...)
          : null
        

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

5.3 状态机模式


        while (true) {
          state = computeNext(state)  // 状态转移
        }
        


六、思考题

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

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

答案

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


        Error: Cannot find module '../main.js'
        SyntaxError: /path/to/main.js:123
        

改进方案:


        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`302Bootstrap 入口
`src/main.tsx`4683主程序
`src/QueryEngine.ts`1295SDK 封装
`src/query.ts`1729核心循环


八、下节预告

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

- 动态导入的详细分析

- feature() Bun DCE 的作用

- 快速路径的扩展规则


*- 第一轮:□ 事实准确性*

*- 第二轮:□ 深度与洞见*

*- 第三轮:□ 可读性与价值*