OFFICIAL SOURCE ANALYSIS

核心架构

Claude Code 的启动流程、QueryEngine 核心循环、上下文装配机制。

全局架构概览

在深入各模块之前,先建立一个全景视角。Claude Code 的运行时可以分为以下层次:

用户输入层
Terminal UI / IDE Extension
斜杠命令解析
编排层 (QueryEngine)
消息循环
权限拦截
上下文装配
执行层
工具并行/串行调度
MCP 桥接
子进程管理
状态层 (Zustand Store)
设置持久化
Feature Gate
Token 追踪
持久化层
会话磁盘写入
上下文压缩
Transcript 归档

下面我们自顶向下,逐层拆解。

1. 启动流程 — Claude Code 启动时做了什么

当你在终端输入 claude 并按下回车,到你看到交互式提示符,中间发生了远比你想象多的事情。Claude Code 的启动流程经过精心优化,目标是将感知延迟压到最低,同时完成所有必要的初始化工作。

并行 Prefetch 优化

启动的第一个关键设计是:绝不串行等待。以下操作在进程启动后立即并行触发:

进程启动 (t=0ms) │ ├─→ [并行 A] MDM 配置读取 │ 读取管理配置文件(企业部署场景下的托管设备管理) │ 结果:获取组织级别的限制和默认值 │ ├─→ [并行 B] Keychain Prefetch │ 预取认证凭据(API Key / OAuth Token) │ macOS 上调用 security find-generic-password │ 结果:凭据已在内存中,后续 API 调用零等待 │ ├─→ [并行 C] Feature Flag 加载 │ 从缓存或远端获取功能开关状态 │ 结果:决定哪些功能/工具对本次会话可见 │ └─→ [并行 D] 插件/Skill 发现 扫描 ~/.claude/skills/ 和 .claude/skills/ 目录 仅读取 manifest,不执行任何 skill 代码 结果:技能名称和描述已就绪 所有并行操作完成后 → 进入交互循环
为什么 Keychain 要 prefetch?macOS 的 Keychain 访问涉及系统级鉴权弹窗和进程间通信,延迟不可预测(50ms~500ms)。通过在启动时就发起请求,等到用户输入第一条消息并实际需要调用 API 时,凭据大概率已经在内存缓存中了。

重量级模块懒加载

Claude Code 使用 Bun 作为 JavaScript 运行时,但即使 Bun 的启动速度很快,加载所有依赖仍然会拖慢首屏时间。解决方案是懒加载

模块大小/复杂度加载时机为什么懒加载
OpenTelemetry 重量级(含多个 proto 定义) 第一次 API 调用时 遥测不影响核心功能,可以延迟初始化
gRPC 客户端 重量级(含 native binding) MCP 服务连接时 不是所有会话都需要 MCP
Prettier / AST 解析器 中等 首次代码格式化时 只有写入工具需要
PDF 解析库 中等 首次读取 PDF 时 大多数会话不涉及 PDF

关键技巧是:这些模块的 import() 调用在 I/O 等待期间触发。例如,当第一个 API 请求正在等待网络响应时,OpenTelemetry 的加载就可以在同一个事件循环中并行完成。

Feature Flag 与 Dead Code Elimination

Claude Code 在 Bun 构建时使用 Dead Code Elimination(DCE)。Feature flag 的值在构建阶段就被确定,条件分支中不可达的代码会被编译器直接移除。

这意味着什么?如果某个实验性功能的 flag 在构建时是 false,那么相关的所有代码——工具定义、UI 组件、提示模板——都不会出现在最终的 bundle 中。这不是运行时判断,而是编译时裁剪。运行时仍然有一部分 flag 是动态评估的(比如 A/B 测试),但核心功能的开关是静态的。

插件/Skill 发现(Cache-Only 模式)

Skill 发现是启动流程中最巧妙的优化之一。Claude Code 在启动时不会执行任何 skill 的代码,甚至不会完整解析 skill 的内容。它只做两件事:

  1. 扫描目录:遍历 ~/.claude/skills/(全局)和 .claude/skills/(项目级),读取每个 skill 的 manifest 文件
  2. 建立索引:将 skill 名称和简短描述写入内存索引,这个索引会被注入 system prompt,告诉模型有哪些 skill 可用

完整的 skill 内容(详细说明、工具定义、系统提示片段)只有在模型决定使用某个 skill 时才会按需加载。这就是「名字先加载,内容按需」的策略。

启动性能数据(典型值)
阶段耗时说明
进程启动 + Bun 初始化~30msBun 的冷启动远快于 Node.js
核心模块加载~50ms不含懒加载模块
并行 Prefetch(最慢路径)~200ms通常是 Keychain 或 Feature Flag
Skill 发现~20ms仅扫描 manifest
UI 渲染首帧~100msInk (React) 组件挂载
总计到可交互~300ms取决于 Keychain 响应速度

2. QueryEngine — 核心循环引擎

QueryEngine 是 Claude Code 的心脏。它不是一个简单的「发请求、收回复」的函数——它是一个有状态的异步生成器,管理着从用户输入到模型响应再到工具执行的完整循环。

submitMessage() — Async Generator 设计

核心入口 submitMessage() 是一个 async generatorasync function*),这意味着它不是一次性返回所有结果,而是逐条 yield 消息。每一个 yield 的值代表一个可以立即渲染到 UI 的事件:

submitMessage("帮我重构这个函数") │ yield → { type: "user_message", content: "帮我重构这个函数" } │ yield → { type: "assistant_start", model: "claude-sonnet-4-..." } │ yield → { type: "text_delta", text: "我来看看这个函数..." } yield → { type: "text_delta", text: "的结构。首先..." } │ yield → { type: "tool_use", name: "Read", input: { file_path: "..." } } │ ← 工具执行(可能耗时几秒) │ yield → { type: "tool_result", output: "文件内容..." } │ yield → { type: "text_delta", text: "好的,我建议..." } │ yield → { type: "turn_complete", usage: { input: 2340, output: 890 } }

这个设计的好处是流式渲染——UI 层不需要等待整个对话轮次结束就可以开始显示内容。用户看到的「逐字打印」效果就来自于这些连续的 text_delta yield。

消息处理管线

当用户输入一条消息后,它会经过一条完整的处理管线:

用户输入
processUserInput
系统提示合并
query() 循环
响应/工具调用
processUserInput 阶段详解

processUserInput 是一个前置处理器,它在消息发送到模型之前做以下事情:

处理项说明
斜杠命令检测 如果输入以 / 开头,直接分发到对应的命令处理器,不经过模型。例如 /clear 直接清空对话历史。
附件处理 检测输入中是否包含文件路径或 URL 引用,如果有,将文件内容或 URL 抓取结果作为附件注入消息。
模型选择 根据当前设置确定使用哪个模型(默认 / fast mode / 用户指定),以及对应的 API endpoint。
图片处理 如果消息包含图片,压缩并编码为 base64,转换为 Claude 能理解的多模态内容格式。
上下文窗口检查 预估本次请求的 token 量,如果接近上下文窗口上限,触发 auto-compact(详见后文)。

工具调用循环

这是 QueryEngine 中最关键的逻辑:当模型的响应中 stop_reason"tool_use" 时,引擎不会停下来,而是继续循环

query() 循环内部: while (true) { response = await callAPI(messages) if (response.stop_reason === "end_turn") { // 模型认为任务完成,退出循环 break } if (response.stop_reason === "tool_use") { // 提取所有工具调用 toolCalls = extractToolCalls(response) // 执行工具(可能并行,可能串行) results = await executeTools(toolCalls) // 将结果追加到 messages[] messages.push(response, ...results) // 继续循环——带着工具结果再次调用模型 continue } }

这意味着一次用户输入可以触发多轮模型调用。例如,用户说「帮我找到并修复这个 bug」,模型可能先调用 Grep 搜索相关文件,再调用 Read 阅读代码,然后调用 Edit 修改文件,每一步都是一次完整的 API 往返。

循环是否会无限运行?不会。有多重保护:1) 模型最终会返回 end_turn;2) 有最大工具调用轮次限制;3) token 耗尽会自动停止;4) 用户随时可以按 Esc 中断。

崩溃恢复机制

Claude Code 有一个重要的设计决策:消息在 API 响应返回之前就写入磁盘

具体来说,每次调用 API 之前,当前的完整消息历史(包括刚刚追加的用户消息和工具结果)都会被序列化并写入本地文件系统。这意味着即使进程在 API 调用期间崩溃(网络断开、系统休眠、意外终止),下次启动时对话状态依然完整——最多丢失模型正在生成的那一条回复。

Permission Wrapping

在工具执行之前,每一次工具调用都会经过权限系统的拦截。这不是一个简单的 yes/no 检查,而是一个完整的决策链:

工具调用请求
Allow Rules
Deny Rules
PreToolUse Hooks
用户确认/自动放行
执行
权限决策的优先级顺序
  1. Deny Rules(最高优先级):如果工具/路径匹配了 deny 规则,直接拒绝,没有任何覆盖机制
  2. Allow Rules:如果匹配了 allow 规则(用户之前批准过或在配置中预设),自动放行
  3. ReadOnly 工具:只读工具(如 Read、Grep、Glob)在大多数权限模式下自动放行
  4. PreToolUse Hooks:用户配置的自定义钩子可以在此阶段拦截或修改工具调用
  5. 交互式确认:如果以上都没有明确决定,弹出权限确认界面让用户选择

Token 追踪和用量统计

QueryEngine 内部维护着精确的 token 计数器。每次 API 调用返回时,响应头中的 usage 信息会被提取并累加:

统计项用途
input_tokens(本轮)计算本轮上下文大小,判断是否需要压缩
output_tokens(本轮)计算模型生成量,影响费用估算
cache_creation_input_tokensPrompt caching 命中分析
cache_read_input_tokens衡量缓存效率
累计 input/output tokens整个会话的总用量,显示在状态栏
cost 估算基于模型价格和 token 数量计算实时费用

3. 上下文装配 — 模型每次调用时看到什么

每次调用 Claude API 时,发送的不是一条简单的消息,而是一个精心组装的上下文包。理解这个组装过程,对于写出有效的 CLAUDE.md 和优化工具使用至关重要。

System Prompt 的组成

Claude Code 的 system prompt 不是一段固定文本。它是多个来源动态拼接的结果:

核心人格与行为规则
CLAUDE.md 内容(全局 + 项目级 + 父目录链)
Skill 描述索引
MCP 工具名列表
Auto Memory 索引
活跃规则 (Rules)
各部分的详细内容

核心人格与行为规则

这是 Claude Code 内置的基础指令,定义了 Claude 在 CLI 环境下应该如何行为:使用工具而非猜测、在修改文件前先阅读、遵循项目代码风格等。这部分用户不可修改。

CLAUDE.md 内容

Claude Code 会从多个位置查找并合并 CLAUDE.md 文件:

  1. ~/.claude/CLAUDE.md — 全局指令,适用于所有项目
  2. 从 git 仓库根目录到当前工作目录路径上每一层的 CLAUDE.md
  3. .claude/CLAUDE.md — 项目级指令

所有找到的文件会按顺序拼接。如果内容重复或冲突,后面的覆盖前面的。

Skill 描述索引

启动时扫描的 skill manifest 信息会在这里列出,格式类似:

「可用 skill:lumi-url-to-markdown(抓取网页内容转 Markdown)、pdf(PDF 文件处理)...」

注意:这里只有名字和一行描述,完整的 skill 提示内容在模型决定使用时才会注入。

MCP 工具名列表

当前连接的 MCP 服务器提供的工具列表。同样只列名字和简短说明,完整 schema 按需加载。

Auto Memory 索引

如果用户启用了记忆功能,之前对话中积累的关键事实和偏好会作为索引注入 system prompt,帮助模型在跨会话间保持一致性。

对话历史:累积的 messages[]

这是 API 调用中最大的组成部分。messages[] 数组包含了从会话开始到当前的所有消息——用户消息、模型回复、工具调用和工具结果。

为什么上下文会越来越大?每一轮对话都会向 messages[] 追加新内容。如果你在一个长会话中反复读取大文件、执行复杂搜索,上下文可以很快膨胀到数十万 token。这就是为什么 auto-compact 机制如此重要(详见第 6 节)。

工具定义:动态过滤

发送给模型的工具列表不是固定的 42 个。每次 API 调用前,工具列表会经过多重过滤:

过滤条件效果
Deny Rules被禁用的工具完全从列表中移除,模型看不到它们
Feature Flags未启用的实验性工具不会出现
Simple Mode简化模式下隐藏高级工具,降低认知负载
MCP 连接状态只展示当前已连接的 MCP 服务器提供的工具
ToolSearch 延迟发现一些不常用的工具通过 ToolSearch 按需暴露,初始不在列表中

技能和 MCP:按需注入策略

这是一个精妙的两阶段策略:

  1. 第一阶段(始终加载):Skill 名称 + 一行描述出现在 system prompt 中,MCP 工具名出现在工具列表中。成本极低(几十个 token)。
  2. 第二阶段(按需加载):当模型决定使用某个 skill 或 MCP 工具时,才加载完整的提示模板、参数 schema、使用示例等。这些内容可能有数千个 token。

这种设计让模型知道有哪些能力可用(不会遗漏),但又不会在每次调用时浪费大量 token 在不相关的工具定义上。