第二章:架构全景
学习时间: 4 小时 | 难度: ⭐⭐⭐ | 前置: 第一章
学习目标
完成本章后,学员将能够:
- 绘制 Claude Code 的三层架构图
- 描述启动流程的 9 个阶段及其优化策略
- 解释查询循环状态机的 7 种 Continue 类型
- 定位关键源文件并理解其职责
1. 三层架构
Claude Code 采用清晰的三层架构,每层职责分明:
┌─────────────────────────────────────────────────────────────┐
│ Layer 1: 启动引导层 │
│ cli.tsx (320行) → feature flag polyfill + 13条快速路径 │
│ → main.tsx (4683行) → Commander.js + 并行预取 + 初始化 │
└──────────────────────────────┬──────────────────────────────┘
│
┌──────────────────────────────▼──────────────────────────────┐
│ Layer 2: CLI 框架层 │
│ REPL.tsx (5009行) → React/Ink 终端 UI │
│ QueryEngine.ts (1320行) → 会话编排 │
│ query.ts (1732行) → 7状态查询循环 │
└──────────────────────────────┬──────────────────────────────┘
│
┌──────────────────────────────▼──────────────────────────────┐
│ Layer 3: 工具与运行层 │
│ 54 个内置工具 + MCP 外部工具 │
│ claude.ts (3420行) → API 流式通信 │
│ mcp/client.ts (3351行) → MCP 协议 │
│ bootstrap/state.ts (1758行) → 全局状态 │
└─────────────────────────────────────────────────────────────┘
1.1 Layer 1: 启动引导层
核心文件: cli.tsx (320行) + main.tsx (4683行)
启动引导层的首要目标是极致的启动性能。它通过以下策略实现:
| 策略 | 实现 | 效果 |
|---|---|---|
| 快速路径 | 13 条 --version、--daemon 等直接短路 | 零模块加载 |
| Feature Flag DCE | feature('FLAG') 构建时替换为 false | 死代码消除 |
| 并行预取 | MDM + Keychain 与模块加载并行 | I/O 重叠 |
| 延迟加载 | 首帧渲染后才加载非关键数据 | 首屏更快 |
// cli.tsx 快速路径示例
if (args.version) {
process.stdout.write(`${MACRO.VERSION}\n`) // 零模块加载,直接输出
return
}
// Feature Flag polyfill (反编译版)
// 在正式版中,feature() 由 bun:bundle 在构建时内联
function feature(flag: string): boolean {
return false // 所有内部功能禁用
}
1.2 Layer 2: CLI 框架层
核心文件: REPL.tsx (5009行) + QueryEngine.ts (1320行) + query.ts (1732行)
CLI 框架层基于 React + Ink 实现终端 UI,采用组件化架构:
REPL.tsx (主屏幕)
├── PromptInput (用户输入处理)
├── Messages (消息列表 + 虚拟滚动)
├── PermissionDialog (权限确认弹窗)
└── TaskPanel (后台任务面板)
QueryEngine 是会话编排器,负责:
- 管理对话状态(消息列表、工具列表)
- 触发自动压缩(token 接近限制时)
- 维护文件状态快照(readFileState)
- 调用
query()函数执行查询循环
1.3 Layer 3: 工具与运行层
核心文件: claude.ts (3420行) + mcp/client.ts (3351行) + bootstrap/state.ts (1758行)
这一层包含所有运行时基础设施:
- 54 个内置工具:BashTool、FileEditTool、GrepTool、AgentTool 等
- MCP 客户端:8 种传输协议的统一管理
- 全局状态:进程级单例,管理 session ID、CWD、token 计数等
2. 启动流程(9 阶段)
Claude Code 的启动流程经过精心优化,分为 9 个阶段:
阶段 1: cli.tsx 入口
→ polyfill 注入 (feature/MACRO/BUILD_TARGET)
→ 确保运行时环境正确
阶段 2: 快速路径检查
→ 13 条快速路径 (--version, --daemon, --bridge, --worktree 等)
→ 命中则直接返回,不加载 main.tsx
阶段 3: main.tsx → Commander.js 参数解析
→ 解析命令行参数
→ 确定运行模式 (交互/管道/守护进程)
阶段 4: 并行预取
→ startMdmRawRead() — MDM 子进程并行读取
→ startKeychainPrefetch() — macOS keychain 并行预取
→ 与模块加载并行执行
阶段 5: init() 初始化
→ 配置加载 + 环境变量
→ 优雅关闭信号注册
阶段 6: 信任对话框
→ 首次使用时显示安全提示
→ 用户确认后写入配置
阶段 7: 迁移脚本
→ 12 个 migrations/ 脚本
→ 处理配置格式升级
阶段 8: 服务初始化
→ analytics + GrowthBook + 策略限制 + MCP
→ 初始化所有运行时服务
阶段 9: 模式分支
→ 管道模式 (-p): 读 stdin,输出结果
→ 交互模式: launchRepl() 启动 REPL
2.1 关键优化:并行预取
// main.tsx 中的并行预取(概念还原)
const [mdmData, keychainData] = await Promise.all([
startMdmRawRead(), // 企业 MDM 配置
startKeychainPrefetch(), // API key 缓存
loadModules(), // 模块加载
])
这种并行策略确保 I/O 密集操作不会阻塞启动流程。
3. 查询循环状态机
query.ts (1732行) 是整个系统的核心——它实现了 LLM 多轮对话的状态机。
3.1 循环流程
query() 调用
├─ 构建 system prompt (getSystemPrompt, ~900行)
├─ 准备消息 (normalizeMessagesForAPI)
├─ 流式 API 调用 (queryModelWithStreaming, claude.ts:753)
│
└─ 流式事件处理:
├─ message_start → 开始新消息
├─ content_block_start → 开始内容块
├─ delta → 增量文本/工具调用
└─ stop → 检查 stop_reason
│
└─ 7 种 Continue 类型:
├─ tool_use → StreamingToolExecutor → 注入结果 → 循环
├─ end_turn → 返回最终结果
├─ max_output_tokens → 恢复尝试 (最多3次)
├─ prompt_too_long → context-collapse → reactive-compact
├─ error → 重试/报告
└─ interrupt → 用户中断
3.2 7 种 Continue 类型详解
| 类型 | 含义 | 处理策略 |
|---|---|---|
tool_result | 工具执行完成 | 注入结果到消息列表,继续循环 |
max_tokens_recovery | 输出被截断 | 尝试恢复(最多 3 次) |
prompt_too_long_recovery | 上下文过长 | 触发压缩(截断→摘要→升级限制) |
end_turn | 对话自然结束 | 返回最终结果 |
tool_use | 需要执行工具 | 交给 StreamingToolExecutor |
error | API 错误 | 重试或报告给用户 |
interrupt | 用户中断 | 清理并返回 |
3.3 Withhold-then-Recover 错误处理
这是 Claude Code 最精妙的错误处理策略之一:
prompt_too_long 错误发生
↓
第一步: 抑制(不 surface 给用户)
↓
第二步: 尝试 context-collapse
→ 截断最旧的 API round
→ 最多尝试 3 次
↓
第三步: 尝试 reactive-compact
→ fork 子 agent 生成摘要
→ 替换截断的消息
↓
第四步: 升级 max_tokens
↓
第五步: 全部失败才 surface 错误给用户
设计意图: 用户不应该因为临时的上下文长度问题而看到错误消息。系统应该先尝试自动恢复,只有在所有恢复策略都失败时才告知用户。
4. 关键文件索引表
| 文件 | 行数 | 职责 | 验证状态 |
|---|---|---|---|
| src/entrypoints/cli.tsx | 320 | 入口 + polyfill + 快速路径 | ✅ |
| src/main.tsx | 4683 | Commander.js CLI + 初始化 | ✅ |
| src/query.ts | 1732 | 核心查询循环 (7状态机) | ✅ |
| src/QueryEngine.ts | 1320 | 会话编排器 | ✅ |
| src/Tool.ts | 792 | Tool 接口定义 | ✅ |
| src/tools.ts | 389 | 工具注册表 (54个) | ✅ |
| src/commands.ts | 754 | 命令注册表 (~113个) | ✅ |
| src/context.ts | — | 上下文收集 | ✅ |
| src/constants/prompts.ts | ~900 | System prompt 构建 | ✅ |
| src/screens/REPL.tsx | 5009 | 主 REPL 屏幕 | ✅ |
| src/services/api/claude.ts | 3420 | API 客户端 | ✅ |
| src/services/mcp/client.ts | 3351 | MCP 客户端 (8种传输) | ✅ |
| src/services/compact/ | ~1700 | 上下文压缩 | ✅ |
| src/services/tools/StreamingToolExecutor.ts | 530 | 流式工具执行器 | ✅ |
| src/bootstrap/state.ts | 1758 | 全局单例状态 | ✅ |
| src/tools/AgentTool/ | 1397 | 子 Agent 工具 | ✅ |
| src/utils/permissions/ | ~1500 | 权限系统 | ✅ |
| src/utils/claudemd.ts | — | CLAUDE.md 加载 | ✅ |
| src/utils/hooks/ | ~5121 | Hook 系统 (20+事件) | ✅ |
| src/ink/ | 104文件 | Ink 框架 (内部fork) | ✅ |
设计模式
本章涉及的模式:
| 模式 | 定义 | 应用位置 |
|---|---|---|
| Fast-Path + DCE | 多路分发 + 构建时死代码消除 | cli.tsx 13条快速路径 |
| State Machine | 状态转换追踪 + 错误恢复 | query.ts 7种Continue类型 |
| Parallel Prefetch | I/O 密集操作与模块加载并行 | main.tsx 并行预取 |
| Withhold-then-Recover | 错误先抑制,多级恢复策略 | query.ts prompt_too_long 处理 |
源码验证
本章所有架构信息均经过源码验证:
- ✅ 三层架构:cli.tsx / main.tsx / query.ts 职责划分确认
- ✅ 9 阶段启动:逐阶段对照 main.tsx 源码确认
- ✅ 7 种 Continue 类型:query.ts 状态机代码确认
- ✅ 文件行数:所有文件行数误差 < 5%
- ⚠️ 文档声称 main.tsx 500+ 行,实际 4683 行(9倍差距)
思考题
-
为什么 Claude Code 把
cli.tsx和main.tsx分成两个文件? 这种分层对启动性能有什么影响? -
查询循环中的
max_output_tokens恢复最多尝试 3 次,为什么是 3 次而不是其他数字? 这个数字应该如何确定? -
Withhold-then-Recover 策略在什么场景下可能导致问题? 比如,如果错误信息对用户很重要,但被自动恢复掩盖了?
-
如果让你设计一个类似的 Agent 查询循环,你会增加哪些 Continue 类型? 比如「需要用户提供更多信息」?