接上回:一个 .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)
配置分三层:
- Global Config:
~/.claude/config.json,跨项目 - Project Config:
.claude/settings.json,per-repo - 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_useblock → 权限检查 → 执行 → 构造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 能进行长时间工作的关键。三种策略:
- Full Compact:压缩整个对话为一段摘要
- Partial Compact (from):只压缩最近的消息,保留早期上下文
- 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, NotebookEdit | FileEdit 有完整的 diff 语义 |
| 命令执行 | Bash, REPL | bashParser.ts 4,436 行,bashSecurity.ts 2,592 行 |
| 搜索 | WebSearch, WebFetch, ToolSearch | ToolSearch 搜索可用工具本身 |
| Agent | Agent, SendMessage, TaskStop, TaskCreate/Get/List/Update | Agent 系统核心 |
| 特殊 | Skill, SyntheticOutput, Brief, Sleep, Monitor | Skill 调用项目级别的 skill 定义 |
| MCP | MCP (动态注册) | 每个 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 库?
- 状态管理:LLM 交互天然是异步、流式的,React 的声明式状态管理比命令式
stdout.write好用太多 - 组件复用:权限确认弹窗、进度条、markdown 渲染……组件化后可以在不同场景复用
- 测试性:React 组件可以脱离终端独立测试
- 团队技能:前端工程师可以直接上手
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-01 | 380K 行代码的骨架 |
| 第 2 篇 | 2026-04-02 | 灵魂工程:System Prompt 的七层设计 |
| 第 3 篇 | 2026-04-03 | 40 个工具的统一抽象 |
| ... | ... | 持续更新 |

![[CC解构] 380K 行代码的骨架](/claude-code-architecture-overview/cover.jpg)