[CC解构] 380K 行代码的骨架

[CC解构] 380K 行代码的骨架

接上回:一个 .map 文件的蝴蝶效应。

打开一个 380K 行的代码库,第一个问题不是"它有什么",而是"它是怎么组织的"。一个好的架构,你看目录就能猜到数据流向;一个差的架构,你读完所有文件也不知道谁调谁。

Claude Code 属于前者。


从入口开始:main.tsx 的前三行

一个命令行工具,入口是 .tsx 文件。但更反直觉的是——在执行任何业务逻辑之前,前三行做了三件副作用:

profileCheckpoint('main_tsx_entry');   // 打点:启动计时
startMdmRawRead();                     // 开始读 MDM 设备管理配置
startKeychainPrefetch();               // 开始读 macOS 钥匙串

这三行代码,说明了三件事关 Claude Code 性质的事:

它在意启动速度。 profileCheckpoint 不是 debug 工具——它是一个生产级的 startup profiler,追踪从进程启动到 UI 渲染每个阶段的耗时。

它在企业环境中运行。 MDM(Mobile Device Management)配置读取——意味着它要兼容企业 IT 管理策略。这不是一个 hacker 的玩具。

它深度集成 macOS。 Keychain prefetch 预读 OAuth token 和 API key,避免后续同步阻塞。注释里写得明白:isRemoteManagedSettingsEligible() 本来会同步读两次钥匙串,共 ~65ms。并行化后,启动快了一截。

接下来的 100 行 import,拉进来的模块涵盖了整个应用:Commander.js(CLI 解析)、React(UI 渲染)、GrowthBook(Feature Flags)、MCP SDK(工具协议)、OAuth(认证)、各种 Tool 和 Command。

main.tsx 是一个调度中心,不是实现中心。 它的 4,683 行主要在做:解析命令行参数 → 初始化配置 → 认证 → 构建工具集 → 启动 REPL 或执行单次命令。真正的业务逻辑,分散在六个层里。


六层架构

读完整个 src/,我把 Claude Code 的架构抽象为六层:

┌─────────────────────────────────────────────┐
│           Layer 6: 表面层 (Surface)          │
│    Terminal UI (Ink) · IDE Bridge · Voice    │
├─────────────────────────────────────────────┤
│           Layer 5: 命令层 (Command)          │
│         50 个 slash commands + CLI           │
├─────────────────────────────────────────────┤
│         Layer 4: 编排层 (Orchestration)      │
│    Agent · Fork · Coordinator · Swarm       │
├─────────────────────────────────────────────┤
│          Layer 3: 工具层 (Tool)              │
│        40+ 个 Tools · Permission · MCP      │
├─────────────────────────────────────────────┤
│         Layer 2: 引擎层 (Engine)             │
│   QueryEngine · Compact · Memory · Context  │
├─────────────────────────────────────────────┤
│          Layer 1: 基础层 (Foundation)        │
│   API Client · Auth · Config · Sandbox      │
└─────────────────────────────────────────────┘

数据从上往下流:用户在 Surface 层输入 → Command 层解析意图 → Orchestration 层决定是否拆分 → Tool 层执行具体操作 → Engine 层调用 LLM → Foundation 层处理网络/认证/沙箱。

结果从下往上回:API 返回流式 token → Engine 解析 tool_use → Tool 层执行并返回 tool_result → Orchestration 层汇总 → Surface 层渲染给用户。

每一层有自己明确的职责边界。让我逐层展开。


Layer 1: 基础层——地基中的地基

API Client (services/api/claude.ts, 3,419 行)

这是跟 Anthropic API 通信的核心。不是一个简单的 fetch 封装——它处理:

  • 流式响应解析:SSE (Server-Sent Events) 逐 token 解析
  • 重试策略:429/5xx 自动重试,指数退避
  • Prompt Cache 管理:getCacheControl() 根据查询来源(main loop / auto mode / compact)决定缓存策略
  • Beta Header 管理:不同功能需要不同的 beta flag
  • Max Output Tokens:根据模型动态决定

一个关键设计:所有对 Claude API 的调用都经过这一层,没有人直接 new Anthropic() 然后 messages.create()。这保证了认证、缓存、重试的一致性。

Auth (utils/auth.ts)

支持四种认证方式:

  • API Key(个人用户)
  • OAuth(Claude.ai 订阅用户)
  • AWS Bedrock
  • GCP Vertex AI

订阅等级(getSubscriptionType())影响功能可用性——比如 Pro 用户可以并行启动更多 Agent。

Config (utils/config.ts)

配置分三层:

  1. Global Config:~/.claude/config.json,跨项目
  2. Project Config:.claude/settings.json,per-repo
  3. MDM/Remote Settings:企业管理下推

三层配置的合并策略不是简单覆盖——某些字段(如 alwaysDeny 权限规则)会向下累积,保证企业策略不会被项目配置绕过。

Sandbox (utils/sandbox/)

沙箱系统控制命令执行的边界:

  • 文件系统:读白名单 + 读黑名单中的白名单、写白名单 + 写白名单中的黑名单
  • 网络:允许的 host 列表 / 拒绝的 host 列表
  • Unix Sockets:单独控制

注意它不是一个简单的 allow/deny——而是四层嵌套规则。比如,你可以禁止读 /etc,但允许读 /etc/hosts。这种精细度是企业部署必需的。


Layer 2: 引擎层——大脑

QueryEngine.ts (1,295 行入口 + query.ts 1,729 行)

这是 Claude Code 的心脏。一个完整的 LLM 交互循环:

用户输入 → 构建消息 → 调用 API → 解析响应
    ↑                                    ↓
    └── tool_result ←── 执行工具 ←── tool_use

QueryEngine 管理的是这个循环的全部复杂性:

  • 消息标准化:normalizeMessagesForAPI() 把内部消息格式转成 Anthropic API 格式(5,512 行的 messages.ts,整个代码库第二大文件)
  • 流式处理:逐 token 接收、累积、渲染、中断
  • Tool Call 调度:解析 tool_use block → 权限检查 → 执行 → 构造 tool_result → 送回下一轮
  • Auto Compact:上下文接近窗口极限时自动压缩(后续专门讲)
  • Token 追踪:实时估算 token 消耗,驱动 UI 上的进度条和压缩决策

一个关键架构选择:QueryEngine 不知道任何具体 Tool 的实现。 它只关心 Tool 接口——给我 name + input,还我 result。这是整个 Tool 系统可扩展的基础。

Context (context.ts + context/)

每一轮对话开始前,Context 层负责组装两大块信息:

System Context(getSystemContext):系统提示词 + 环境信息 + 能力声明。这些信息放在 system message 里,对模型全局生效。

User Context(getUserContext):当前工作目录、CLAUDE.md 内容、Memory 内容、git 状态、项目结构。这些作为 user message 的 attachment 注入。

分离 system/user context 是为了缓存优化——system context 跨会话基本不变(可以缓存),user context 每次不同(需要重新计算)。

Compact (services/compact/)

上下文压缩是 Claude Code 能进行长时间工作的关键。三种策略:

  1. Full Compact:压缩整个对话为一段摘要
  2. Partial Compact (from):只压缩最近的消息,保留早期上下文
  3. Partial Compact (up_to):只压缩早期消息,保留最近上下文

压缩 prompt 是一个 9 段结构化模板(主要请求、技术概念、文件/代码、错误修复、问题解决、所有用户消息、待办、当前工作、下一步),加上 <analysis> 草稿区让模型先整理思路再输出摘要。

自动压缩的触发条件:

effective_window = context_window - 20K (预留输出)
threshold = effective_window - 13K (缓冲区)

超过阈值就自动触发。连续失败 3 次则放弃(这个数字来自生产数据:BQ 里发现 1,279 个 session 有 50+ 次连续失败,浪费了约 250K API calls/day)。

Memory (memdir/)

记忆系统基于文件,有严格的四类分类法:

类型用途示例
user用户偏好"用 bun 不用 npm"
feedback行为反馈"不要自动 commit"
project项目上下文"这个 repo 用 monorepo"
reference参考资料"API 文档在 /docs"

MEMORY.md 是索引(每行 <150 字符),指向具体的记忆文件。索引上限 200 行 / 25KB——超过就截断,并提示用户精简。

长期 session(KAIROS 模式)有不同的记忆策略:追加日志(logs/YYYY/MM/YYYY-MM-DD.md),每晚用 /dream skill 蒸馏到 MEMORY.md。


Layer 3: 工具层——手和脚

Tool 抽象 (Tool.ts, 792 行)

所有工具共享一个接口。核心定义:

export type Tool = {
  name: string
  description: string | (() => string)
  inputSchema: ToolInputJSONSchema
  
  // 执行
  call(input: unknown, context: ToolUseContext): Promise<ToolResult>
  
  // 权限
  isReadOnly: () => boolean
  needsPermissions: (input: unknown) => boolean
  
  // Auto Mode 分类器输入
  toAutoClassifierInput(input: unknown): unknown
  
  // 验证
  validateInput(input: unknown): ValidationResult
}

关键方法:

  • call:执行工具,返回结果
  • isReadOnly:只读工具跳过权限检查
  • needsPermissions:细粒度判断特定输入是否需要确认
  • toAutoClassifierInput:给安全分类器看的精简输入——不是把整个 input 丢给分类器,而是每个 Tool 自己决定暴露什么

这个 toAutoClassifierInput 特别值得注意。它解决了一个现实问题:分类器不需要看到完整的文件内容来判断一个 Write 操作是否安全——它只需要看路径和摘要。每个 Tool 自己做投影,比一个通用的裁剪逻辑更精确。

ToolUseContext

这是贯穿整个系统的"环境对象"——每次 Tool 调用都携带:

  • messages: 完整对话历史
  • abortController: 中断信号
  • options.tools: 当前可用工具集
  • options.mcpClients: MCP 服务器连接
  • options.agentDefinitions: 可用 Agent 定义
  • readFileState: 文件状态缓存
  • getAppState/setAppState: React 状态访问

整个应用通过这一个 Context 把状态传递到每个角落——而不是全局变量或 singleton。这是可测试性的基础。

40+ 个具体工具

粗略分类:

类别工具行数亮点
文件操作FileRead, FileWrite, FileEdit, Glob, Grep, NotebookEditFileEdit 有完整的 diff 语义
命令执行Bash, REPLbashParser.ts 4,436 行,bashSecurity.ts 2,592 行
搜索WebSearch, WebFetch, ToolSearchToolSearch 搜索可用工具本身
AgentAgent, SendMessage, TaskStop, TaskCreate/Get/List/UpdateAgent 系统核心
特殊Skill, SyntheticOutput, Brief, Sleep, MonitorSkill 调用项目级别的 skill 定义
MCPMCP (动态注册)每个 MCP server 的工具动态加入

Bash 工具是最复杂的单个工具。 它有自己的 AST 解析器(2,679 行),安全检查器(2,592 行),权限系统(2,621 行),加起来超过 7,800 行——比很多独立项目还大。因为 Bash 能做任何事,所以安全检查要覆盖任何事。

Permission 系统

四种权限模式:

模式行为
default危险操作需要确认
plan只能读和分析,不能修改
bypassPermissions全部自动允许(需要用户启用)
auto用 LLM 分类器自动判断

auto 模式是最有意思的——它调用另一个 LLM(yoloClassifier)来判断当前操作是否安全。后面会有一整篇专门拆解这个分类器。


Layer 4: 编排层——指挥中心

这是 Claude Code 从"单 Agent"进化到"多 Agent 系统"的关键层。

三种编排模式

1. 标准 Agent(subagent_type 指定)

主线程 ──→ Agent("code-reviewer", prompt) ──→ 独立子进程
                                                    ↓
主线程 ←── <task-notification> ──────────────── 完成通知

子 Agent 从零开始,没有父对话的上下文。需要在 prompt 里给完整信息。

2. Fork(不指定 subagent_type)

主线程 ──→ Fork(directive) ──→ 子进程(继承全部对话上下文)
                                    ↓
主线程 ←── 完成通知 ──────────── 共享 prompt cache

Fork 是 Claude Code 最优雅的设计之一。子进程继承父的系统提示和全部对话历史,prompt cache 直接命中——这意味着启动一个 Fork 几乎没有额外的 API 成本(cache 命中后只需为新的 directive 付费)。

Fork child 收到的第一条消息是一段硬编码的"你是 fork 进程"指令,包含 10 条不可违反的规则。其中最重要的两条:

"Do NOT spawn sub-agents; execute directly." "Your response MUST begin with 'Scope:'."

防递归、保证输出格式。简单有效。

3. Coordinator 模式

Coordinator 模式下,Claude Code 变成一个纯调度器——自己不写代码,只分析问题、分配 Worker、综合结果、跟用户沟通。

这是一套完整的多 Agent 编排协议:

  • Worker 通过 <task-notification> XML 报告完成/失败
  • Coordinator 用 SendMessage 继续已有 Worker(复用上下文)
  • Coordinator 用 TaskStop 停止走偏的 Worker
  • 并行是核心原则——"Parallelism is your superpower"

Coordinator 的系统提示词里有详细的"何时 spawn fresh vs. continue"决策表,以及"永远不要说 based on your findings"的硬性规则(必须自己综合理解后写具体指令)。

Swarm 后端

多 Agent 的物理执行有三种后端:

  • tmux:每个 teammate 一个 pane,可以看到实时输出
  • iTerm:macOS 专属,用 iTerm tab
  • in-process:进程内运行,最轻量

Layer 5: 命令层——用户意图的翻译器

commands.ts(754 行)注册了约 50 个 slash 命令。不是所有命令都同等重要——按使用频率粗分:

高频核心:

  • /compact — 手动触发上下文压缩
  • /clear — 清空对话
  • /model — 切换模型
  • /config — 修改配置

工作流:

  • /commit — 智能 git commit
  • /commit-push-pr — commit + push + 创建 PR
  • /review — 代码 Review
  • /simplify — 审视自己的改动
  • /verify — 验证改动是否正确

Agent 相关:

  • /fork — 手动 fork 子 Agent
  • /agents — 管理 Agent 定义

系统:

  • /status — 查看 token 使用量
  • /sandbox — 管理沙箱规则
  • /share — 分享会话

命令层的设计哲学:每个命令都是一个 "macro"——把多步操作封装成一键触发。比如 /commit 不是简单地执行 git commit,而是:检查 diff → 分析变更 → 生成 commit message → 处理 hook 失败 → 验证结果。


Layer 6: 表面层——人类接口

Terminal UI (React + Ink)

这是我觉得 Claude Code 最有 engineering taste 的决策之一:用 React 来渲染终端 UI。

Ink 是一个把 React 组件树渲染到终端的库。Claude Code 有 389 个组件文件,104 个 hooks——这是一个正经的 React 应用,只不过输出是 ANSI 字符而不是 HTML。

为什么选 React 而不是 blessed/inquirer 之类的传统终端 UI 库?

  1. 状态管理:LLM 交互天然是异步、流式的,React 的声明式状态管理比命令式 stdout.write 好用太多
  2. 组件复用:权限确认弹窗、进度条、markdown 渲染……组件化后可以在不同场景复用
  3. 测试性:React 组件可以脱离终端独立测试
  4. 团队技能:前端工程师可以直接上手

Claude Code 甚至自己实现了一个 Yoga Layout 引擎的 TypeScript 版本(native-ts/yoga-layout/, 2,578 行)——Flexbox 布局,在终端里。

IDE Bridge (bridge/, 31 个文件)

VS Code 和 JetBrains 的集成不是通过嵌入终端实现的——而是一个双向通信协议:

IDE Extension ←→ Bridge Protocol ←→ Claude Code 核心

bridgeMain.ts(2,999 行)实现了这个协议。IDE 可以:发送用户输入、接收 AI 响应、同步文件变更、展示权限确认 UI、显示 diff 预览。

这意味着 Claude Code 的核心是与 UI 解耦的——终端、VS Code、JetBrains 都只是不同的 Surface。

Voice (voice/)

语音输入支持。隐藏在 VOICE_MODE feature flag 后面,外部构建看不到实现。但从导入关系看,它跟 Whisper API 集成。


一次完整的请求流转

让我用一个具体例子串起整个架构:

用户输入: "帮我修复 src/auth.ts 里的 null pointer bug"

1. [Surface] Ink REPL 捕获输入
2. [Command] 不是 slash 命令,进入 QueryEngine
3. [Engine] 组装 system prompt + user context + 历史消息
4. [Foundation] 调用 Anthropic API,流式接收响应
5. [Engine] 解析到 tool_use: FileRead { path: "src/auth.ts" }
6. [Tool] FileRead.call() → 读取文件内容
7. [Tool] isReadOnly() = true → 跳过权限检查
8. [Engine] 把 tool_result 送回 API,继续流式响应
9. [Engine] 解析到 tool_use: FileEdit { path: "src/auth.ts", ... }
10. [Tool] FileEdit.isReadOnly() = false → 需要权限
11. [Tool] 根据 permission mode:
    - default → 弹出确认框
    - auto → yoloClassifier 判断 → allow
    - bypass → 直接执行
12. [Surface] 用户确认(或自动通过)
13. [Tool] FileEdit.call() → 写入修改
14. [Engine] 把 tool_result 送回 API
15. [Engine] 模型返回文本总结
16. [Surface] 渲染 markdown 格式的回复
17. [Engine] 检查 token 使用量 → 未达压缩阈值 → 不压缩

如果模型决定这个任务太复杂,需要先做 research:

5'. [Engine] 解析到 tool_use: Agent { prompt: "Research auth.ts..." }
6'. [Orchestration] 判断:Fork 还是新 Agent?
    - Fork → 继承上下文,省 API 成本
    - 新 Agent → 独立执行,更干净
7'. [Orchestration] 启动子进程/tmux pane
8'. [Tool] 子 Agent 独立工作……
9'. [Orchestration] <task-notification> 回传结果
10'. [Engine] 主线程收到结果,继续推理

关键设计决策总结

通读架构后,几个设计决策特别值得记住:

1. 编译时死代码消除

import { feature } from 'bun:bundle'

const voiceModule = feature('VOICE_MODE')
  ? require('./voice/index.js')
  : null

这不是运行时条件分支——Bun 的 bundler 在编译时就把 feature('VOICE_MODE') 解析为 true/false,然后 tree-shaking 掉死分支。外部构建里,VOICE_MODE 是 false,整个 voice 模块连代码都不会出现在产物中。

这让 Anthropic 可以在同一个代码库里维护内部版和外部版,而不用分支管理。

2. Prompt Cache 是一等公民

几乎每个架构决策都在考虑"会不会破坏 prompt cache":

  • 系统提示分静态/动态两部分(静态部分跨用户缓存)
  • Agent 列表从 tool description 移到 attachment message(避免 MCP 连接变化破坏缓存)
  • Fork 子进程共享父的 prompt cache(因为继承了完整上下文)
  • 沙箱配置去重后再注入 prompt(避免重复路径导致不同用户的 prompt 不一致)
  • Auto Mode 分类器的两个阶段共享 cache(Stage 2 命中 Stage 1 的缓存前缀)

在 LLM 产品中,prompt cache 直接决定了成本和延迟。Claude Code 团队显然把这一点内化到了每一个设计选择里。

3. 安全不是事后补丁

Sandbox、Permission、yoloClassifier、Undercover Mode——这些不是"产品做完了加个安全层",而是从 Layer 1 就开始设计的。Tool 接口里 toAutoClassifierInput 的存在说明了这一点:每个 Tool 从诞生之初就要考虑"我该怎么向安全系统暴露自己"。

4. 最大单文件启示

代码库里最大的文件不是 QueryEngine——是 cli/print.ts(5,594 行),非交互模式的输出渲染。第二大是 messages.ts(5,512 行),消息格式转换。

这说明什么?LLM 产品最复杂的部分不是调 API,而是两端的 I/O——把人类输入变成模型能理解的格式,把模型输出变成人类能看的格式。


下一篇

架构骨架摊开了。下一篇,我们钻进 Claude Code 最核心的软件工程:系统提示词的七层设计。

不是"你是 helpful assistant"那种——而是一本完整的行为手册,包含代码哲学、安全原则、甚至连"什么时候该用 heredoc 写 commit message"都规定了。这是我见过的、对"如何用自然语言编程一个 AI Agent"最深入的回答。


📚 CC解构系列

篇目日期主题
序2026-03-31一个 .map 文件的蝴蝶效应
第 1 篇2026-04-01380K 行代码的骨架
第 2 篇2026-04-02灵魂工程:System Prompt 的七层设计
第 3 篇2026-04-0340 个工具的统一抽象
......持续更新

Comments