架构总览
把前面所有子系统重新拼回一张完整地图:从启动、状态、查询、工具、服务,到最终 UI 渲染的全链路视角。
前面几十课分别拆开讲了启动、系统提示词、工具、MCP、技能、状态与 UI。这一课要做相反的事:把这些局部机制重新接回一张整体图,让你能从一次用户输入出发,顺着代码把整套系统跑通。
main.tsx → setup.ts → QueryEngine.ts →
query.ts → tools.ts → Tool.ts →
bootstrap/state.ts → state/AppStateStore.ts →
replLauncher.tsx → screens/REPL.tsx →
services/api/ → services/mcp/
你可以把 Claude Code 看成六层串接的系统:启动层决定会话如何建立,UI 层承接交互,状态层保存长期上下文,查询层驱动与模型的往返,工具层执行副作用,服务层负责 API / MCP / 压缩等外部连接。
main.tsx, setup.ts, entrypoints/init.ts — 进程启动、设置、迁移、会话连接
replLauncher.tsx, screens/REPL.tsx, components/App.tsx — 墨迹渲染的交互式终端界面
state/AppStateStore.ts, bootstrap/state.ts, state/store.ts — 不可变的 AppState + 全局单例状态
QueryEngine.ts, query.ts — 对话生命周期、系统提示组装、API 流循环
tools.ts, Tool.ts, tools/*/ — 功能注册表:Bash、文件 I/O、代理、搜索、MCP、技能
services/api/, services/mcp/, services/compact/ — Anthropic API 客户端、MCP 连接、压缩
该 ASCII 图映射了每个主要组件及其与其他组件的关系。 自上而下地阅读:用户的击键向下流过每一层,直到到达 API,然后响应通过工具和 UI 向上冒泡。
现在让我们跟踪单个用户消息 - 比如说 "refactor this function" — 贯穿整个堆栈。 每个编号的步骤都映射到源树中的实际代码。
attach images, memory files
build UserMessage PUI-->>QE: { messages, shouldQuery, allowedTools } QE->>QE: fetchSystemPromptParts()
assemble system + user + system context QE->>SS: recordTranscript(messages) [BEFORE API call] QE->>Q: query({ messages, systemPrompt, canUseTool, ... }) Q->>API: POST /v1/messages (streaming SSE) API-->>Q: message_start API-->>Q: content_block_delta (text streaming) Q-->>REPL: yield AssistantMessage (partial) REPL->>U: renders streaming text API-->>Q: content_block (tool_use: Bash) Q->>Q: canUseTool() permission check Q->>T: BashTool.call({ command: "..." }) T-->>Q: yield BashProgress (live output) Q-->>REPL: yield ProgressMessage REPL->>U: renders tool progress T-->>Q: ToolResult { output: "..." } Q->>API: POST /v1/messages (tool_result appended) API-->>Q: next assistant turn API-->>Q: stop_reason: end_turn Q-->>QE: Terminal result QE->>SS: recordTranscript (final) QE-->>REPL: yield SDKResultMessage REPL->>U: final render complete
QueryEngine.ts:450 用于解释此设计决策的评论。
启动顺序经过精心编排,以最大限度地缩短首次渲染时间。 三类工作尽早并行进行:
后台输入/输出
startMdmRawRead() — MDM 策略 plutil 子流程。
startKeychainPrefetch() — macOS 钥匙串读取。
两者都会在 135ms 模块评估完成之前触发。
网络预热
preconnectAnthropicApi() 在用户键入任何内容之前建立到 API 端点的 TCP 连接,因此第一个 API 请求不支付 TCP 握手成本。
延迟预取
startDeferredPrefetches() — 用户/git 上下文、提示、模型功能、文件计数、更改检测器。 跑步 after 绘制,这样就不会阻止提示。
会话接线
captureHooksConfigSnapshot() 必须追赶 setCwd() 但在任何查询之前。 钩子配置被读取一次并冻结,因此会话中文件修改无法注入新的钩子。
插件缓存
getCommands() and loadPluginHooks() 作为后台任务预取。 它们填充第一次查询时消耗的缓存,而不会阻塞渲染路径。
配置升级
runMigrations() checks migrationVersion against CURRENT_MIGRATION_VERSION=11 并仅运行所需的模型字符串/设置架构迁移。
为什么 main.tsx 静态导入所有内容但仍然感觉很快?
所有静态导入的约 135ms 模块评估成本与
startMdmRawRead() and startKeychainPrefetch() 在任何导入完成之前,子进程调用会在文件的最顶部触发。 当 JavaScript 完成评估模块图时,两个子进程调用都已分派到操作系统。
像 OpenTelemetry (~400KB) 和 gRPC (~700KB) 这样的重型模块是通过动态延迟加载的 import() inside init() 仅当实际需要遥测时,它们才不会触及关键路径。
React 和 Ink 也很懒: launchRepl() in replLauncher.tsx
only import()s App.tsx and REPL.tsx 在通话时间。 在无头模式下(-p),这些根本不会被加载。
QueryEngine (在后来的重构中引入)提取什么是整体的 ask() 函数到拥有完整对话生命周期的类中。 每个对话会话存在一个实例。
说明发动机拥有
class QueryEngine {
private config: QueryEngineConfig // tools, commands, mcpClients, model, ...
private mutableMessages: Message[] // full conversation history (grows each turn)
private abortController: AbortController // shared with all tools in this session
private permissionDenials: SDKPermissionDenial[] // accumulated for SDK result
private totalUsage: NonNullableUsage // token counts across all turns
private readFileState: FileStateCache // snapshot of files read this session
private discoveredSkillNames: Set<string> // skills seen in this turn (for telemetry)
private loadedNestedMemoryPaths: Set<string> // CLAUDE.md files already injected
}
SubmitMessage() — 回合生命周期
每次致电 submitMessage() 是一个异步生成器
SDKMessage 事件。 每回合的顺序:
| Step | Code | Purpose |
|---|---|---|
| 1. 处理斜杠命令 | processUserInput() | 处理/commands,构建UserMessage数组,确定shouldQuery |
| 2. 持久化用户消息 | recordTranscript(messages) | 在 API 之前写入磁盘,以便终止中请求可恢复 |
| 3. 组装系统提示 | fetchSystemPromptParts() | 组合默认 + 自定义 + 内存 + 协调器上下文 |
| 4.加载技能+插件 | getSlashCommandToolSkills() | 无头的仅缓存加载; 全面刷新互动 |
| 5.收益系统初始化 | buildSystemInitMessage() | SDK 调用者接收工具、命令、代理的列表 |
| 6.进入查询循环 | query() | 流式API调用+工具执行直到end_turn |
| 7. 产量结果 | SDKResultMessage | 最终成本、使用情况、permission_denials、stop_reason |
query() 循环——迭代剖析
// query.ts — simplified loop skeleton
async function* queryLoop(params) {
let state = { messages, turnCount: 1, autoCompactTracking, ... }
const config = buildQueryConfig() // snapshot env/statsig state
while (true) {
// 1. Optionally start skill/job prefetch (async, consumes settled results only)
// 2. Send streaming API request via deps.sendRequest()
for await (const event of streamEvents) {
yield event // passes text deltas directly to REPL
if (event.type === 'tool_use') collectToolUse(event)
}
// 3. Check stop reason
if (stopReason === 'end_turn') return 'success'
// 4. Execute tools (StreamingToolExecutor — parallel where possible)
for await (const result of runTools(toolUseBlocks, canUseTool, context)) {
yield result
}
// 5. Token budget / compact checks → may compact and continue
// 6. Append tool_results to messages, increment turnCount, loop
}
}
克劳德可以调用的每项能力都是 Tool。 工具系统故意扁平化——没有工具层次结构,只有注册表功能
getAllBaseTools() in tools.ts 返回权威列表。
工具接口(Tool.ts)
// Simplified Tool interface
interface Tool {
name: string // must be stable (used in Statsig cache key)
description: string // injected into system prompt
inputSchema: ZodSchema // validation before call()
isEnabled(): boolean // feature-gate / env check
call(input, context: ToolUseContext): // async generator
AsyncGenerator<ToolProgressData, ToolResult>
renderToolResult(result, context): React.ReactNode // Ink UI rendering
}
ToolUseContext — 工具通向世界的窗口
每个工具调用都会收到一个 ToolUseContext 将工具可能需要的所有内容捆绑在一起,而不将其耦合到全局状态:
| Property | Type | Purpose |
|---|---|---|
messages | Message[] | 完整的对话历史记录 |
mainLoopModel | ModelSetting | 当前子代理生成模型 |
tools | Tools | 可用工具集(供AgentTool向下传递) |
mcpClients | MCPServerConnection[] | 活动 MCP 连接 |
agentDefinitions | AgentDefinitionsResult | 自定义代理配置 |
abortController | AbortController | 共享中止信号(Ctrl-C 传播) |
readFileState | FileStateCache | 读取的文件快照(用于比较/撤消) |
setAppState | Setter<AppState> | 工具可以改变 UI 状态(例如 TodoWriteTool) |
handleElicitation | ElicitFn | MCP URL 获取(OAuth 流) |
功能门控工具
许多工具是有条件包含的 feature() 标志(Bun 捆绑时死代码消除)或环境变量。 这使工具列表对于 Anthropic 的提示缓存键保持确定性:
// tools.ts — feature gate pattern
const SleepTool =
feature('PROACTIVE') || feature('KAIROS')
? require('./tools/SleepTool/SleepTool.js').SleepTool
: null
// getAllBaseTools() filters nulls from the array
// NOTE: this list is synced to Statsig console for prompt cache invalidation
Claude Code 有一个两层状态模型。 了解使用哪一层来做什么对于理解代码库至关重要。
bootstrap/state.ts
进程生命周期常量:sessionId、cwd、projectRoot、模型、身份验证令牌、遥测仪表、挂钩注册表。 明确不是 React 存储。 文件中的注释警告:“请勿在此处添加更多状态”。
state/AppStateStore.ts
DeepImmutable<AppState> — UI 需要的一切:消息、mcpClients、权限上下文、推测状态、设置、任务列表、代理定义、文件历史记录。 通过不可更改地更新 setAppState(prev => ...).
关键设计原则: bootstrap/state.ts 是模块级单例(普通 JS 对象),而 AppState 是 React 上下文。 这种分离意味着查询引擎和工具可以在不导入 React 的情况下访问会话身份,而 UI 可以根据任何 AppState 更改进行反应式重新渲染。
// bootstrap/state.ts — the singleton shape (partial)
type State = {
originalCwd: string
projectRoot: string
totalCostUSD: number
totalAPIDuration: number
cwd: string
modelUsage: { [modelName: string]: ModelUsage }
mainLoopModelOverride: ModelSetting | undefined
isInteractive: boolean
sessionId: SessionId
sdkBetas: BetaMessageStreamParams['betas']
hookRegistry: RegisteredHookMatcher[]
meter: Meter | undefined
tokenBudgetInfo: { remainingTokens: number; ... }
// ... ~40 more fields, all process-lifetime
}
// state/AppStateStore.ts — the React state shape (partial)
type AppState = DeepImmutable<{
settings: SettingsJson
mainLoopModel: ModelSetting
toolPermissionContext: ToolPermissionContext
messages: Message[]
mcpClients: MCPServerConnection[]
agentDefinitions: AgentDefinitionsResult
speculation: SpeculationState
fileHistory: FileHistoryState
plugins: LoadedPlugin[]
tasks: TaskState | null
// ... ~50 more fields
}>
Claude Code 中的“会话”是具有唯一 UUID 的持久对话。 会话作为 JSONL 转录文件存储在
~/.claude/projects/<cwd-hash>/<session-id>.jsonl.
会话生命周期
// Startup: generate or restore session ID
getSessionId() // reads bootstrap/state.ts
registerSession() // registers in concurrent sessions tracking
countConcurrentSessions() // used for display in status bar
// During conversation:
recordTranscript(messages) // enqueues write (lazy 100ms JSONL flush)
flushSessionStorage() // forced flush (EAGER_FLUSH env / cowork)
cacheSessionTitle() // first user message → title for resume UI
// Resume path (--continue / --resume):
loadTranscriptFromFile() // reads JSONL back into Message[]
processResumedConversation() // validates + replays into initial messages
recordTranscript()
对于助理消息,呼叫是即发即忘的,但对于用户消息则需要等待。 这是故意的——评论 QueryEngine.ts:727 解释说等待助理写入会阻塞流生成器,从而阻止
message_delta 处理中的事件。
MCP(模型上下文协议)服务器连接为 MCPServerConnection 里面装着的物体 AppState.mcpClients。 它们在第一个查询之前初始化并传递到每个 ToolUseContext.
MCP 工具是 not in getAllBaseTools() - 它们在会话启动时与基本工具一起动态添加
getMcpToolsCommandsAndResources()。 这就是 MCP 工具名称可能与基本工具名称冲突的原因:重复数据删除发生在加载时。
每个工具调用都会经过 canUseTool() 执行前。 这个单一功能是所有权限决策的架构瓶颈。
// The permission gate — called by query.ts before runTools()
const wrappedCanUseTool: CanUseToolFn = async (
tool, input, toolUseContext, assistantMessage, toolUseID, forceDecision
) => {
const result = await canUseTool(tool, input, toolUseContext, ...)
if (result.behavior !== 'allow') {
// Track for SDK result reporting
this.permissionDenials.push({ tool_name, tool_use_id, tool_input })
}
return result
}
| 权限模式 | Behavior | 配置通过 |
|---|---|---|
default | 向用户询问不在允许列表中的任何工具 | 正常 CLI 启动 |
auto | 自动允许安全工具,阻止危险 | --permission-mode auto |
bypass | 无需询问即可允许所有工具 | --dangerously-skip-permissions |
| 始终允许规则 | 每个工具的允许列表(来自设置 + 会话) | 用户在会话期间接受 |
当对话增长到足以威胁模型的上下文窗口时,Claude Code 会触发自动压缩。 这对用户来说是透明的。
代币门槛
calculateTokenWarningState() 将当前上下文标记计数与模型的上下文窗口进行比较。 在 ~80% 填充时,自动压缩触发器。
buildPostCompactMessages()
将对话发送给 Claude,并附有摘要提示。 返回单个紧凑摘要消息以及任何保留的最近消息。
剪断压实
功能门控替代方案: snipCompact.ts 产生一个 compact_boundary 系统消息。 SDK 路径在内存中截断; REPL 保留完整的回滚和按需项目。
500k 延续
checkTokenBudget() 处理单个 API 响应超过 max_output_tokens 的情况。 它会自动继续显示“请继续”,直到响应完成。
钩子允许用户在特定的生命周期点注入 shell 命令或回调。 它们配置在 settings.json 并在启动时捕获一次(不可变快照模式)。
| 挂钩式 | 触发时 | 可以拦截吗? |
|---|---|---|
PreToolUse | 在任何工具执行之前 | 是的 - 可以拒绝该工具 |
PostToolUse | 任何工具完成后 | No |
PreCompact | 上下文压缩之前 | No |
PostCompact | 压实完成后 | No |
Stop | 当克劳德输出stop_reason=end_turn时 | 是的——可以继续 |
Notification | 任何助理通知事件 | No |
FileChanged | 观察磁盘上修改的文件 | No |
SessionStart | 在新会话中第一次查询之前 | 是 - 延迟第一个查询 |
captureHooksConfigSnapshot() 必须运行 after setCwd()
and before 任何查询。 快照后,会话的钩子配置将被冻结。 这可以防止恶意项目修改 settings.json
会话中期注入以当前权限执行的钩子命令。
代码库有两个不同的共享运行时路径 QueryEngine 但它们的 UI 和启动行为有显着差异:
| Aspect | 交互式(默认) | 无头(-p / --print) |
|---|---|---|
| UI | Ink/React 终端渲染 | 仅 stdout 文本输出 |
| 信任对话框 | 首次启动时显示 | 跳过(隐式信任) |
| 会议记录 | 在 API 调用之前等待 | Fire-and-forget |
| 反应进口 | 满载 | 从未进口过 |
| 插件预取 | 设置期间的背景 | 跳过(isBareMode()) |
| 延迟预取 | 第一次渲染后运行 | 完全跳过 |
| 查询引擎路径 | REPL → ask() | print.ts → QueryEngine.submitMessage() |
| 入口点标签 | cli | sdk-cli |
isBareMode() 返回 true 时 --print/-p 是活跃的。 代码库广泛使用此标志来跳过所有仅限交互的工作。 这也是 SDK 调用者所依赖的标志来获得可预测的低延迟执行。
The AgentTool 启用递归执行:Claude 可以生成子代理,每个子代理都有自己的 QueryEngine 实例和受限工具集。
在集群模式下(ENABLE_AGENT_SWARMS=true),代理通过 Unix Domain Socket (UDS) 消息服务器进行通信,该服务器于 setup.ts。 每个代理注册 TeamCreateTool 并可以通过以下方式将消息发送回协调器 SendMessageTool.
一切都在一起 - 从冷启动到流响应的完整时间表:
1. 异步生成器线程
从 API 到 UI 的整个数据流是一个异步生成器链。
query() yields StreamEvents, QueryEngine.submitMessage()
yields SDKMessages,并且 REPL 消耗它们。 这使得真正的流式传输无需回调或事件总线。
2. 死代码消除通过 feature()
Bun 的捆绑时间 feature('FLAG_NAME') 从编译的二进制文件中完全删除禁用的功能分支。 这意味着每个构建的工具列表都是确定的(对于 Anthropic 的提示缓存键很重要),并且禁用的功能会增加零运行时开销。
3. 缓存预热以应对延迟
关键路径(系统提示、工具、命令、模型功能)均在设置/启动期间并行预热。 当用户提交第一个提示时,几乎所有昂贵的 I/O 都已经完成。 模式:触发异步工作,丢弃承诺,并记录/缓存结果。
4. 不可变的AppState + 可变的引导/状态
React 状态是不可变的(DeepImmutable)以防止意外突变并启用 React 的变更检测。 但是会话级常量(cwd、sessionId、model)存在于一个普通的模块单例中,该单例故意不是 React 状态——这些值由查询引擎深处的非 React 代码访问。
5. 的 isBareMode() 快速路径
每一个昂贵的启动操作都由 if (!isBareMode())。 此单个标志(无头运行时为 true)会跳过 React、Ink、UDS 消息传递、插件预取、延迟预取和所有仅交互式设置。 无头执行几乎变成纯粹的计算。
6. 并行子流程投资
代码库不是顺序 I/O,而是尽早触发子进程和异步操作,并让它们与 JavaScript 执行并行运行。
startMdmRawRead() and startKeychainPrefetch() 两者都会在 135ms 模块图完成评估之前触发。 当使用其结果的代码运行时,它们通常已经完成。
顶点要点
- 引导在设计上是并行的。 MDM 读取、钥匙串读取、TCP 预热和命令预取在需要消除顺序 I/O 成本之前全部启动。
- QueryEngine 是对话所有者。 每个对话一个实例。 它在所有回合中保存消息历史记录、令牌使用情况、文件缓存和中止控制器。
- 查询循环是一个纯异步生成器。 每条消息(文本增量、工具进度、工具结果)都会流经
yield从API到UI。 没有回调,没有事件总线。 - 工具是一个平面注册表。
getAllBaseTools()intools.ts是唯一的事实来源。 出于提示缓存目的,该列表在每个构建中都是稳定的。 - 两个状态层服务于不同的主人。
bootstrap/state.ts(单例)用于查询引擎;AppStateStore.ts(反应)UI。 - 权限是一个单一的瓶颈。
canUseTool()在每个工具执行之前调用。 所有三种权限模式(默认、自动、旁路)都流经它。 - 脚本是在 API 调用之前编写的。 这确保即使进程在请求过程中终止,会话也可以恢复。
- 无头模式在架构上是独特的。
isBareMode()删除 React、Ink、UDS、插件和所有推迟的工作。 SDK 调用者的开销几乎为零。 - 功能门是捆绑时的,而不是运行时的。
feature('FLAG')是在构建时由 Bun 消除的死代码。 二进制文件中确实不存在禁用的功能。 - MCP 服务器是一流的对等服务器。 他们的工具、命令和资源与内置工具集成到相同的注册表中,并通过相同的注册表传递。
ToolUseContext.
知识检查
bootstrap/state.ts 和 React 的 AppState 最大的架构差异是什么?课程完成
您已完成 Claude Code 源代码课程的全部 50 课。 现在,您对 Claude Code 的工作原理有了一个完整的心智模型 - 从第一次击键到最终呈现的标记。 这些知识是贡献、扩展或简单地深入理解有史以来最复杂的人工智能编码工具之一的基础。