深入拆解 Claude Code 源码(一):7 万行 TypeScript 揭秘全球最强 AI 编程助手的内部架构
深入拆解 Claude Code 源码(一):7 万行 TypeScript 揭秘全球最强 AI 编程助手的内部架构
系列:深入拆解 Claude Code 源码 | 第 1 篇 / 共 8 篇
关键词:Claude Code, AI 编程助手, 源码分析, TypeScript, 架构设计
写在前面
2024 年底,Anthropic 发布了 Claude Code —— 一个直接运行在终端里的 AI 编程助手。不同于 Cursor、Copilot 这类 IDE 插件,Claude Code 选择了一条更"极客"的路线:它就是一个 CLI 工具,你敲 claude 回车,它就开始帮你写代码。
但你有没有想过,当你在终端里输入一句话,到 Claude 帮你改好代码、跑完测试、提交 PR,这中间到底发生了什么?
我花了大量时间研究了 Claude Code v2.1.88 的恢复源码(从 npm 包的 source map 中提取),约 7 万行 TypeScript,1884 个文件。这篇文章是这个系列的开篇,我将带你从全局视角俯瞰整个系统的架构,让你对这个"最强 AI 编程助手"有一个完整的认知地图。
一、一句话理解 Claude Code
如果只能用一句话概括 Claude Code 的本质,我会说:
Claude Code 是一个基于 React + Ink 的终端应用,通过 Anthropic Messages API 实现流式对话,内置 35+ 工具让 AI 能直接操作你的文件系统和命令行。
拆开来看,它解决了三个核心问题:
- 怎么和 AI 对话? → 流式 API 通信 + 消息历史管理
- 怎么让 AI 操作电脑? → 工具系统(读写文件、执行命令、搜索代码…)
- 怎么在终端里做出好看的界面? → React + Ink 渲染引擎
二、关键数字
在深入架构之前,先用一组数字感受一下这个工程的规模:
| 维度 | 数字 | 说明 |
|---|---|---|
| 源码总行数 | ~70,000 | TypeScript,从 source map 恢复 |
| 源文件数 | 1,884 | src/ 目录下的 .ts/.tsx 文件 |
| 组件文件 | 389 | components/ 目录 |
| React Hooks | 80+ | hooks/ 目录 |
| 斜杠命令 | 77 | commands/ 目录,每个命令一个文件 |
| 内置工具 | 35+ | tools/ 目录,含条件加载工具 |
| 后端服务 | 20+ | services/ 子系统 |
| 状态管理文件 | ~35 | Zustand store + selectors + context |
| Ink 渲染层(fork) | 76+ | ink/ 目录,深度定制 |
| 工具最大并发数 | 10 | CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY |
| Fast Path 分支 | 12+ | cli.tsx 中的快速路径 |
| Feature Flags | 30+ | bun:bundle 编译时宏 |
| 类型定义文件 | ~50 | types/ + entrypoints/sdk/ |
| vendor 原生模块 | 4 | audio-capture, image-processor, modifiers-napi, url-handler |
三、全局架构图
先看一张鸟瞰图:
┌─────────────────────────────────────────────────────┐
│ 用户终端 (Terminal) │
│ ┌───────────────────────────────────────────────┐ │
│ │ React + Ink 渲染层 (389 组件) │ │
│ │ App → Messages → PromptInput → StatusLine │ │
│ └──────────────────┬────────────────────────────┘ │
│ │ │
│ ┌──────────────────▼────────────────────────────┐ │
│ │ REPL 主循环 │ │
│ │ screens/REPL.tsx → QueryEngine.ts │ │
│ └──────────────────┬────────────────────────────┘ │
│ │ │
│ ┌──────────────────▼────────────────────────────┐ │
│ │ 核心引擎层 │ │
│ │ query.ts (API 调用) │ ContextManager │ │
│ │ messageState.ts │ tokenBudget.ts │ │
│ └────┬───────────┬────────────┬─────────────────┘ │
│ │ │ │ │
│ ┌────▼────┐ ┌────▼─────┐ ┌───▼──────┐ │
│ │ 35+ 工具 │ │ 77 命令 │ │ 20+ 服务 │ │
│ │ Bash │ │ /commit │ │ API 客户端│ │
│ │ FileEdit│ │ /review │ │ MCP 集成 │ │
│ │ Agent │ │ /compact │ │ OAuth │ │
│ │ Grep │ │ /mcp │ │ 插件系统 │ │
│ └─────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 支撑系统:状态管理 / Hooks / 类型 / 常量 │ │
│ │ Zustand Store │ 80+ Hooks │ Feature Flags │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│ │
▼ ▼
Anthropic Messages API MCP Servers
(流式对话) (外部工具扩展)
四、目录结构速览
整个 src/ 目录可以分为 8 大模块:
| 模块 | 路径 | 文件数 | 一句话说明 |
|---|---|---|---|
| 入口与启动 | entrypoints/, main.tsx, replLauncher.tsx |
~15 | 从命令行到 REPL 的启动链路 |
| 核心引擎 | query.ts, QueryEngine.ts, ContextManager/ |
~20 | 对话循环、API 调用、上下文管理 |
| 命令系统 | commands/ |
77 | /commit, /review, /mcp 等斜杠命令 |
| 工具系统 | tools/ |
~35 | 文件读写、Shell 执行、Agent 调度 |
| 服务层 | services/ |
20+ 子系统 | API 客户端、MCP、OAuth、插件、分析 |
| 组件系统 | components/ |
389 | React + Ink 终端 UI |
| 状态与 Hooks | hooks/, state/, context/ |
100+ | Zustand 状态管理、80+ React Hooks |
| 其他系统 | skills/, tasks/, bridge/, vim/, ink/ |
200+ | 技能、任务、远程控制、Vim 模式、渲染引擎 |
五、一次完整对话的生命周期
让我们跟踪一次用户输入到 AI 回答的完整流程。这是理解整个架构最好的方式 —— 沿着数据流走一遍,所有模块的关系就清晰了。
数据流全景图
用户输入 "帮我修复这个 bug"
│
▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ PromptInput │────▶│ REPL.tsx │────▶│ processUser │
│ (React 组件) │ │ (主循环) │ │ Input() │
└──────────────┘ └──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ QueryEngine │ │ 消息构建 │
│ .submitMsg() │◀────│ UserMessage │
└──────┬───────┘ └──────────────┘
│
▼
┌──────────────┐ ┌──────────────┐
│ query() │────▶│ Anthropic API│
│ (流式请求) │ │ (stream:true)│
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 流式响应处理 │◀────│ SSE 事件流 │
│ yield 事件 │ │ text/tool_use│
└──────┬───────┘ └──────────────┘
│
┌───────────┴───────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 纯文本回复 │ │ tool_use 请求 │
│ → 渲染到终端 │ │ → 工具执行 │
└──────────────┘ └──────┬───────┘
│
┌──────────┴──────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 并发安全工具 │ │ 串行敏感工具 │
│ Read/Glob/ │ │ Bash/Edit/ │
│ Grep │ │ Write │
└──────┬───────┘ └──────┬───────┘
└──────────┬──────────┘
▼
┌──────────────┐
│ ToolResult │
│ → 追加到消息 │
│ → 再次调 API │
└──────┬───────┘
│
▼
┌──────────────┐
│ 循环直到 AI │
│ 不再请求工具 │
└──────────────┘
Step 1: CLI 启动 — cli.tsx 的快速路径分发
一切从 src/entrypoints/cli.tsx 的 main() 函数开始。这是一个精心设计的快速路径分发器 —— 所有 import 都是动态加载的,目的是让 --version 这类简单命令零开销执行:
// src/entrypoints/cli.tsx — 完整 main() 函数骨架(302 行中的核心逻辑)
async function main(): Promise<void> {
const args = process.argv.slice(2);
// 最快路径:--version,零 import,直接输出
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
console.log(`${MACRO.VERSION} (Claude Code)`);
return; // 直接退出,不加载任何其他模块
}
// 加载启动性能分析器
const { profileCheckpoint } = await import('../utils/startupProfiler.js');
profileCheckpoint('cli_entry');
// 快速路径:--dump-system-prompt(ant-only,feature flag 控制)
if (feature('DUMP_SYSTEM_PROMPT') && args[0] === '--dump-system-prompt') {
const { enableConfigs } = await import('../utils/config.js');
enableConfigs();
const { getSystemPrompt } = await import('../constants/prompts.js');
const prompt = await getSystemPrompt([], model);
console.log(prompt.join('\n'));
return;
}
// 快速路径:Chrome MCP 服务器
if (process.argv[2] === '--claude-in-chrome-mcp') {
const { runClaudeInChromeMcpServer } = await import(
'../utils/claudeInChrome/mcpServer.js'
);
await runClaudeInChromeMcpServer();
return;
}
// ... 还有 10+ 个快速路径:daemon、bridge、bg sessions、templates 等 ...
// 最终路径:加载完整 CLI(最慢,但功能最全)
const { main: cliMain } = await import('../main.js');
await cliMain();
}
void main(); // 顶层调用,整个应用从这里启动
这个设计的精髓在于:--version 只需要 1 行代码、0 个额外 import;而正常启动需要加载 4700 行的 main.tsx。Feature flag feature('DUMP_SYSTEM_PROMPT') 是 Bun 的编译时宏,在外部构建版本中会被整个 if 块消除(dead code elimination),这就是为什么说它是"ant-only"功能。
Step 2: 用户输入 → 消息构建
用户在终端输入 “帮我修复这个 bug”,按回车。PromptInput 组件捕获输入,提交到 REPL 主循环。
REPL 主循环(screens/REPL.tsx)接收输入,构建 UserMessage:
// src/types/message.ts — 消息类型定义
type UserMessage = {
type: 'user'
role: 'user'
content: ContentBlock[] // 文本 + 图片 + 工具结果
uuid: string
timestamp: number
toolUseResult?: string // 工具执行结果(如果是工具回传)
}
type AssistantMessage = {
type: 'assistant'
role: 'assistant'
message: { content: ContentBlock[] } // API 原始响应
costUSD: number
durationMs: number
uuid: string
timestamp: number
apiError?: string // 如 'max_output_tokens'
}
Step 3: 系统提示词拼装
constants/system.ts 中的 getCLISyspromptPrefix() 拼装系统提示词,包含:
- 基础指令
- 当前日期、工作目录
- Git 状态
- 已安装的技能描述
- 工具使用规范
Step 4: QueryEngine 发起查询
QueryEngine 是对话循环的核心。它是一个有状态的类,每个实例对应一个会话:
// src/QueryEngine.ts — QueryEngine 类的核心结构
export class QueryEngine {
private config: QueryEngineConfig
private mutableMessages: Message[] // 消息历史(可变)
private abortController: AbortController // 中止控制器
private permissionDenials: SDKPermissionDenial[]
private totalUsage: NonNullableUsage // 累积 token 用量
private readFileState: FileStateCache // 文件状态缓存
private discoveredSkillNames = new Set<string>()
constructor(config: QueryEngineConfig) {
this.config = config
this.mutableMessages = config.initialMessages ?? []
this.abortController = config.abortController ?? createAbortController()
this.readFileState = config.readFileCache
this.totalUsage = EMPTY_USAGE
}
// 每次用户输入调用一次 submitMessage,返回一个 AsyncGenerator
async *submitMessage(
prompt: string | ContentBlockParam[],
options?: { uuid?: string; isMeta?: boolean },
): AsyncGenerator<SDKMessage, void, unknown> {
// 1. 构建系统提示词
const { defaultSystemPrompt, userContext, systemContext } =
await fetchSystemPromptParts({ tools, mainLoopModel, ... });
// 2. 处理用户输入(解析 slash 命令、图片、粘贴文本等)
// 3. 调用 query() 进入 API 循环
// 4. yield 每个流式事件给上层渲染
}
}
Step 5: query() — API 调用的核心循环
query.ts 是与 Anthropic API 通信的核心。它用一个 AsyncGenerator 实现了流式 + 工具调用循环:
// src/query.ts — query() 函数签名和核心循环
export async function* query(
params: QueryParams,
): AsyncGenerator<
| StreamEvent // 流式文本片段
| RequestStartEvent // API 请求开始
| Message // 完整消息
| TombstoneMessage // 消息删除标记
| ToolUseSummaryMessage, // 工具使用摘要
Terminal // 返回值:终态
> {
const consumedCommandUuids: string[] = []
// 核心:委托给 queryLoop
const terminal = yield* queryLoop(params, consumedCommandUuids)
// 通知所有消费的命令完成
for (const uuid of consumedCommandUuids) {
notifyCommandLifecycle(uuid, 'completed')
}
return terminal
}
// queryLoop 内部的 while(true) 循环:
// 1. 构建 API 请求参数(messages, tools, system prompt)
// 2. 调用 Anthropic SDK 的流式 API
// 3. 逐个 yield 流式事件(文本、工具调用)
// 4. 如果有 tool_use → 执行工具 → 结果追加到 messages → 继续循环
// 5. 如果 AI 不再请求工具 → break,返回 Terminal
queryLoop 内部维护了一个可变的 State 对象,跟踪循环间的上下文:
// src/query.ts — 循环状态
type State = {
messages: Message[]
toolUseContext: ToolUseContext
autoCompactTracking: AutoCompactTrackingState | undefined
maxOutputTokensRecoveryCount: number // max_output_tokens 恢复次数
hasAttemptedReactiveCompact: boolean // 是否尝试过响应式压缩
maxOutputTokensOverride: number | undefined
pendingToolUseSummary: Promise<ToolUseSummaryMessage | null> | undefined
stopHookActive: boolean | undefined
turnCount: number // 当前轮次
transition: Continue | undefined // 上一次循环继续的原因
}
这个 while(true) 循环是 Claude Code 的心跳 —— 每一次迭代代表一次"AI 思考 → 输出 → 可能执行工具 → 继续思考"的完整周期。
Step 6: 工具执行 — 并发与串行的安全边界
当 AI 决定使用工具(比如 BashTool),query.ts 会调用 toolOrchestration.ts 中的 runTools()。这是整个工具系统的调度中心:
// src/services/tools/toolOrchestration.ts — 工具调度核心
export async function* runTools(
toolUseMessages: ToolUseBlock[],
assistantMessages: AssistantMessage[],
canUseTool: CanUseToolFn,
toolUseContext: ToolUseContext,
): AsyncGenerator<MessageUpdate, void> {
let currentContext = toolUseContext
// 关键:partitionToolCalls 将工具调用分为并发安全/必须串行的批次
for (const { isConcurrencySafe, blocks } of partitionToolCalls(
toolUseMessages,
currentContext,
)) {
if (isConcurrencySafe) {
// 并发执行只读工具(Read、Glob、Grep 等)
for await (const update of runToolsConcurrently(
blocks, assistantMessages, canUseTool, currentContext,
)) {
yield { message: update.message, newContext: currentContext }
}
} else {
// 串行执行写操作(Bash、FileEdit、FileWrite 等)
for await (const update of runToolsSerially(
blocks, assistantMessages, canUseTool, currentContext,
)) {
if (update.newContext) currentContext = update.newContext
yield { message: update.message, newContext: currentContext }
}
}
}
}
partitionToolCalls 的逻辑非常巧妙 —— 它将工具调用按顺序分组,连续的只读工具合并为一个并发批次,写操作各自独立:
// src/services/tools/toolOrchestration.ts — 分区逻辑
function partitionToolCalls(
toolUseMessages: ToolUseBlock[],
toolUseContext: ToolUseContext,
): Batch[] {
return toolUseMessages.reduce((acc: Batch[], toolUse) => {
const tool = findToolByName(toolUseContext.options.tools, toolUse.name)
const parsedInput = tool?.inputSchema.safeParse(toolUse.input)
const isConcurrencySafe = parsedInput?.success
? (() => {
try {
return Boolean(tool?.isConcurrencySafe(parsedInput.data))
} catch {
return false // 解析失败,保守处理为非并发安全
}
})()
: false
// 如果当前工具并发安全,且上一个批次也是并发安全的 → 合并
if (isConcurrencySafe && acc[acc.length - 1]?.isConcurrencySafe) {
acc[acc.length - 1]!.blocks.push(toolUse)
} else {
acc.push({ isConcurrencySafe, blocks: [toolUse] })
}
return acc
}, [])
}
例如,如果 AI 连续请求 [Glob, Grep, Read, Bash, Read, Grep],分区结果是:
- 批次 1(并发):
[Glob, Grep, Read] - 批次 2(串行):
[Bash] - 批次 3(并发):
[Read, Grep]
每个工具执行前经过完整的生命周期:
validateInput()— 输入验证(Zod schema)checkPermissions()— 权限检查(plan/auto/bypass 模式)runPreToolUsesHooks()— PreToolUse Hookcall()— 实际执行runPostToolUseHooks()— PostToolUse Hook
Step 7: 状态管理 — 34 行的 Zustand Store
Claude Code 的状态管理核心是一个极简的自实现 store,只有 34 行,比 Zustand 本身还简单:
// src/state/store.ts — 完整实现(34 行)
type Listener = () => void
type OnChange<T> = (args: { newState: T; oldState: T }) => void
export type Store<T> = {
getState: () => T
setState: (updater: (prev: T) => T) => void
subscribe: (listener: Listener) => () => void
}
export function createStore<T>(
initialState: T,
onChange?: OnChange<T>,
): Store<T> {
let state = initialState
const listeners = new Set<Listener>()
return {
getState: () => state,
setState: (updater: (prev: T) => T) => {
const prev = state
const next = updater(prev)
if (Object.is(next, prev)) return // 引用相等则跳过
state = next
onChange?.({ newState: next, oldState: prev })
for (const listener of listeners) listener()
},
subscribe: (listener: Listener) => {
listeners.add(listener)
return () => listeners.delete(listener)
},
}
}
这个 store 的设计哲学是:最小化抽象。没有 middleware、没有 devtools、没有 immer —— 就是一个带订阅的可变容器。Object.is 的浅比较确保了不会触发无意义的重渲染。
Step 8: 结果回传与循环
工具结果作为 ToolResultMessage 追加到消息历史,再次调用 API,形成工具调用闭环:
用户输入 → AI 回复(含 tool_use) → 工具执行 → 结果回传 → AI 继续回复 → ...
这个循环会持续到 AI 不再请求工具为止。query.ts 中的 while(true) 循环就是这个闭环的实现 —— 每次迭代检查 AI 的响应是否包含 tool_use 块,如果有就执行工具、将结果追加到消息历史、继续下一轮迭代。
Step 9: 渲染输出
React + Ink 层将最终结果渲染到终端:
Messages.tsx → MessageRow.tsx → 各种内容组件(代码高亮、Diff、表格…)
每一次流式事件都会触发 React 状态更新,Ink 的双缓冲渲染器只更新变化的字符,所以你在终端里看到的是实时逐字流出的效果。
六、关键源文件速查
以下是理解 Claude Code 架构最关键的 20+ 个源文件,按重要性排序:
| 文件路径 | 行数 | 用途 | 关键导出 |
|---|---|---|---|
src/entrypoints/cli.tsx |
302 | CLI 入口,快速路径分发 | main() |
src/main.tsx |
~4,700 | Commander.js CLI 定义,编排中心 | main() (cliMain) |
src/QueryEngine.ts |
~1,300 | 对话引擎,消息历史管理 | QueryEngine 类 |
src/query.ts |
~1,730 | API 查询层,流式循环 | query(), QueryParams |
src/commands.ts |
754 | 命令注册表,77 个 slash 命令 | getSlashCommandToolSkills() |
src/Tool.ts |
792 | 工具接口定义 | Tool, Tools, ToolUseContext |
src/tools.ts |
389 | 工具注册中心 | getAllBaseTools(), getTools() |
src/state/store.ts |
34 | 状态管理核心 | createStore(), Store<T> |
src/context.ts |
190 | 会话上下文管理 | getSystemContext(), getUserContext() |
src/cost-tracker.ts |
~324 | API 成本追踪 | addToTotalSessionCost(), formatTotalCost() |
src/setup.ts |
~200 | 会话设置 | setup() |
src/replLauncher.tsx |
22 | REPL 启动桥接 | launchRepl() |
src/entrypoints/init.ts |
341 | 系统初始化 | init(), initializeTelemetryAfterTrust() |
src/entrypoints/mcp.ts |
197 | MCP 服务器模式 | startMCPServer() |
src/services/tools/toolOrchestration.ts |
~189 | 工具并发调度 | runTools(), partitionToolCalls() |
src/services/api/claude.ts |
— | Anthropic API 客户端 | accumulateUsage(), updateUsage() |
src/services/compact/autoCompact.ts |
— | 自动压缩 | calculateTokenWarningState() |
src/utils/messages.ts |
— | 消息创建工具 | createUserMessage(), normalizeMessagesForAPI() |
src/utils/systemPrompt*.ts |
— | 系统提示词构建 | getSystemPrompt() |
src/components/App.tsx |
— | 根组件 | React 组件树入口 |
src/components/Messages.tsx |
— | 消息列表渲染 | 消息渲染组件 |
src/ink/ |
76+ | Ink 渲染引擎 fork | 双缓冲、字素感知 |
src/hooks/useCanUseTool.tsx |
— | 工具权限门控 | useCanUseTool() |
src/state/AppState.tsx |
— | 应用状态定义 | AppState 类型 |
七、技术栈全景
| 技术 | 用途 | 备注 |
|---|---|---|
| TypeScript | 主语言 | ~7 万行,strict 模式 |
| React + Ink | 终端 UI | Ink 是 React 的终端渲染器,Anthropic fork 了 76+ 文件深度定制 |
| React Compiler | 自动优化 | _c() 缓存函数,免手写 memo/useCallback |
| Commander.js | CLI 参数解析 | 在 main.tsx 中定义 50+ 参数,使用 @commander-js/extra-typings 获得类型安全 |
| Zustand | 状态管理 | AppState.tsx + 自实现的 34 行 createStore() |
| Zod v4 | Schema 验证 | 工具输入、设置、Hook 响应的验证,配合 zodToJsonSchema 用于 MCP |
| Anthropic SDK | API 通信 | 流式 Messages API,支持 thinking blocks 和 tool_use |
| Bun | 运行时+打包 | bun:bundle feature flags 实现编译时死代码消除 |
| Yoga | 布局引擎 | Ink 内部的 Flexbox 实现,处理终端字符的布局计算 |
| MCP | 工具扩展协议 | Model Context Protocol,支持 stdio 和 HTTP 传输 |
| OpenTelemetry | 遥测 | 延迟加载 instrumentation,attributed counter 工厂 |
| GrowthBook | Feature flag 服务 | 运行时 A/B 测试,与编译时 feature() 互补 |
| Lodash-es | 工具库 | memoize 用于初始化函数的单次执行保证 |
| strip-ansi | 终端处理 | 清理 ANSI 转义码用于日志和存储 |
| randomUUID | ID 生成 | 来自 crypto 模块,用于消息和任务 ID |
技术栈选择的深层逻辑
为什么用 React + Ink 而不是 blessed/ink-text-input?
Claude Code 的 UI 复杂度远超一般 CLI 工具 —— 389 个组件文件意味着它需要组件化、状态管理、条件渲染这些 React 生态的核心能力。用传统的 blessed 库写 389 个"屏幕"是不可想象的维护噩梦。
为什么 fork Ink 而不是直接用?
原版 Ink 的渲染器是"全量重绘"模式,对于 Claude Code 这种高频更新场景(流式文本逐字输出)性能不够。Anthropic 的 fork 做了:
- 双缓冲渲染 — 维护前后两帧 buffer,只 diff 变化的字符
- CharPool 字符池复用 — 避免 GC 压力
- HyperlinkPool — 超链接对象复用
- 字素感知 — 正确处理 emoji 和 CJK 字符的宽度计算
为什么自实现 store 而不是直接用 Zustand?
34 行的 createStore() 比 Zustand 轻量 10 倍。Claude Code 的状态管理需求其实很简单 —— 主要是 AppState 的读写和订阅。自实现意味着零依赖、零抽象泄漏、完全可控。Zustand 的 create() 在内部做的事情和这个 34 行实现本质相同,但多了一层 middleware/plugin 抽象,对于 CLI 场景是不必要的开销。
为什么用 AsyncGenerator 而不是 Promise/EventEmitter?
query() 返回 AsyncGenerator 而不是 Promise,这是一个关键设计选择。AsyncGenerator 天然支持:
- 流式消费 —
yield逐个产出事件,上层可以实时处理 - 背压控制 — 消费者可以控制生产者的节奏
- 优雅取消 —
generator.return()可以干净地终止循环 - 组合 —
yield*可以嵌套组合多个 generator
如果用 Promise,要么等到整个响应完成才返回(延迟太高),要么用 EventEmitter(类型不安全、取消困难)。
八、几个令人惊叹的设计决策
1. Feature Flag 双层架构:编译时 + 运行时
Claude Code 的 feature flag 系统是两层的:
编译时:feature('FLAG_NAME') 来自 bun:bundle,是 Bun 的编译时宏。在构建阶段,未启用的 flag 对应的代码块会被完全消除(dead code elimination),产出的二进制中不包含任何相关代码。
// src/entrypoints/cli.tsx — 编译时 feature flag 的典型用法
import { feature } from 'bun:bundle';
// 整个 if 块在外部构建中被完全消除
if (feature('DUMP_SYSTEM_PROMPT') && args[0] === '--dump-system-prompt') {
// ... ant-only 的系统提示词导出功能 ...
return;
}
// feature() 与 require() 配合实现条件模块加载
const coordinatorModeModule = feature('COORDINATOR_MODE')
? require('./coordinator/coordinatorMode.js')
: null;
运行时:GrowthBook 服务提供运行时的 feature flag,支持 A/B 测试和渐进发布。编译时 flag 控制"这个版本有没有这个功能",运行时 flag 控制"这个功能对这个用户开不开启"。
这种双层架构让 Anthropic 可以:
- 从同一份源码产出不同功能集的二进制(内部版 vs 外部版)
- 在运行时精细控制功能的灰度发布
- 进行 A/B 测试来验证新功能的效果
2. 整个终端 UI 是一个 React 应用
没错,你在终端里看到的每一个字符、每一种颜色、每一个 spinner,都是 React 组件通过 Ink 渲染出来的。components/ 目录有 389 个文件,包含完整的组件树、设计系统、主题切换、甚至模糊搜索选择器。
3. Ink 是一个 fork 而不是依赖
Anthropic 没有直接用 npm 上的 Ink,而是 fork 了一份放在 src/ink/(76+ 文件),做了大量优化:
- 双缓冲渲染 — 只更新变化的字符
- CharPool 字符池复用 — 避免重复分配
- HyperlinkPool — 超链接复用
- 字素感知 — 正确处理 emoji 和 CJK 字符
4. 工具可以并发执行,但有安全边界
toolOrchestration.ts 会将工具调用分为两类:
- 并发安全:Read、Glob、Grep 等只读操作可以并行
- 必须串行:Bash、FileEdit、FileWrite 等写操作必须排队
最大并发数由 CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 环境变量控制(默认 10)。
5. 推测执行:AI 的"预判"
Claude Code 有一个实验性的推测执行系统(services/PromptSuggestion/speculation.ts,992 行),它会在等待用户输入时,预判用户可能的下一步操作并提前执行,结果放在 overlay 目录里。如果猜对了,直接使用,节省响应时间。
6. 动态 import 的极致运用
Claude Code 对 await import() 的使用到了极致。cli.tsx 中的每一个快速路径都使用动态 import,确保只加载必要的模块:
// cli.tsx 中的模式:每个快速路径都是 "用到才加载"
if (args[0] === 'daemon') {
const { enableConfigs } = await import('../utils/config.js');
enableConfigs();
const { daemonMain } = await import('../daemon/main.js');
await daemonMain(args.slice(1));
return; // 不会加载 main.tsx 的 4700 行
}
这意味着 claude --version 和 claude daemon 的启动时间差异巨大 —— 前者几乎零开销,后者需要加载完整 CLI。
7. 权限系统:三种模式的门控
工具执行前必须通过权限检查。useCanUseTool hook 实现了三种权限模式:
| 模式 | 行为 | 场景 |
|---|---|---|
plan |
只读,不执行任何写操作 | 安全审查模式 |
auto |
自动批准安全操作,危险操作需确认 | 默认模式 |
bypass |
跳过所有权限检查 | CI/CD 自动化 |
九、init.ts — 启动时的 19 步初始化
在 cli.tsx 加载完 main.tsx 之后,init.ts 的 init() 函数会执行一系列初始化操作。这个函数用 lodash-es/memoize 包装,确保整个会话只执行一次:
init() 执行顺序(19 步):
│
├─ 1. enableConfigs() — 启用配置系统
├─ 2. applySafeConfigEnvVars() — 应用安全环境变量
├─ 3. applyExtraCACerts() — 额外 CA 证书(TLS 握手前)
├─ 4. setupGracefulShutdown() — 优雅关闭处理
├─ 5. initialize1PEventLogging() — 第一方事件日志(异步)
├─ 6. populateOAuthAccountInfo() — OAuth 账户信息(异步)
├─ 7. initJetBrainsDetection() — JetBrains IDE 检测(异步)
├─ 8. detectCurrentRepository() — GitHub 仓库检测(异步)
├─ 9. initRemoteManagedSettings() — 远程管理设置(条件)
├─ 10. initPolicyLimits() — 策略限制加载(条件)
├─ 11. recordFirstStartTime() — 记录首次启动时间
├─ 12. configureGlobalMTLS() — 全局 mTLS 配置
├─ 13. configureGlobalAgents() — 全局 HTTP 代理
├─ 14. preconnectAnthropicApi() — 预连接 API(TCP+TLS 预热)
├─ 15. initUpstreamProxy() — CCR 上游代理(条件)
├─ 16. setShellIfWindows() — Windows shell 设置
├─ 17. registerCleanup(LSP) — LSP 管理器清理注册
├─ 18. registerCleanup(teams) — 团队清理注册
└─ 19. ensureScratchpadDir() — scratchpad 目录(条件)
注意步骤 14:preconnectAnthropicApi() 会在后台预建立到 Anthropic API 的 TCP+TLS 连接,这样当用户第一次输入时不需要等待连接建立。这种"预热"策略把首次请求的延迟降低了 100-200ms。
步骤 5-10 都是异步的,意味着它们不会阻塞主流程 —— 配置加载完成后立即开始异步初始化遥测、OAuth、IDE 检测等,利用了 Node.js 的事件循环并发。
十、模块间依赖关系
entrypoints ──→ main.tsx ──→ REPL.tsx
│
┌───────────┼───────────┐
▼ ▼ ▼
QueryEngine Commands Components
│ │ │
▼ │ │
query.ts │ │
│ │ │
┌───────┼───────┐ │ │
▼ ▼ ▼ ▼ ▼
Tools Services Hooks ◄──── State/Context
│ │
▼ ▼
MCP Servers OAuth
十一、系列目录
本系列共 8 篇,按模块从入口到输出逐层深入:
| 篇 | 标题 | 核心内容 |
|---|---|---|
| 1 | 全面拆解 Claude Code 架构 ← 你在这里 | 全局鸟瞰、技术栈、生命周期 |
| 2 | CLI 启动流程深度解析 | cli.tsx → main.tsx → REPL 的完整链路 |
| 3 | 核心对话引擎 | QueryEngine、流式处理、上下文管理 |
| 4 | 77 个斜杠命令的设计艺术 | 三种命令类型、插件迁移模式 |
| 5 | 35 个内置工具的瑞士军刀 | buildTool 模式、并发安全、权限检查 |
| 6 | 20 个后端服务的微服务架构 | MCP、OAuth、推测执行、记忆同步 |
| 7 | 终端里的 React 渲染引擎 | 389 组件 + Ink fork 的双缓冲渲染 |
| 8 | 状态管理、技能、Vim 与类型系统 | 收尾篇,串联剩余模块 |
下篇预告
第二篇:CLI 启动流程深度解析
当你在终端敲下
claude并按下回车,到看到那个闪烁的光标,中间只经过了大约 100 毫秒。但这 100 毫秒里发生了什么?cli.tsx如何快速判断是--version还是正常启动?main.tsx的 4700 行 Commander.js 定义做了哪些初始化?REPL 是如何被"点燃"的?下一篇,我们将逐行跟踪这 100ms 的启动链路。
标签:
Claude CodeAI 编程助手源码分析TypeScript架构设计AnthropicCLI 工具
更多推荐




所有评论(0)