核心架构
Claude Code 的启动流程、QueryEngine 核心循环、上下文装配机制。
全局架构概览
在深入各模块之前,先建立一个全景视角。Claude Code 的运行时可以分为以下层次:
下面我们自顶向下,逐层拆解。
1. 启动流程 — Claude Code 启动时做了什么
当你在终端输入 claude 并按下回车,到你看到交互式提示符,中间发生了远比你想象多的事情。Claude Code 的启动流程经过精心优化,目标是将感知延迟压到最低,同时完成所有必要的初始化工作。
并行 Prefetch 优化
启动的第一个关键设计是:绝不串行等待。以下操作在进程启动后立即并行触发:
重量级模块懒加载
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 的值在构建阶段就被确定,条件分支中不可达的代码会被编译器直接移除。
false,那么相关的所有代码——工具定义、UI 组件、提示模板——都不会出现在最终的 bundle 中。这不是运行时判断,而是编译时裁剪。运行时仍然有一部分 flag 是动态评估的(比如 A/B 测试),但核心功能的开关是静态的。
插件/Skill 发现(Cache-Only 模式)
Skill 发现是启动流程中最巧妙的优化之一。Claude Code 在启动时不会执行任何 skill 的代码,甚至不会完整解析 skill 的内容。它只做两件事:
- 扫描目录:遍历
~/.claude/skills/(全局)和.claude/skills/(项目级),读取每个 skill 的 manifest 文件 - 建立索引:将 skill 名称和简短描述写入内存索引,这个索引会被注入 system prompt,告诉模型有哪些 skill 可用
完整的 skill 内容(详细说明、工具定义、系统提示片段)只有在模型决定使用某个 skill 时才会按需加载。这就是「名字先加载,内容按需」的策略。
启动性能数据(典型值)
| 阶段 | 耗时 | 说明 |
|---|---|---|
| 进程启动 + Bun 初始化 | ~30ms | Bun 的冷启动远快于 Node.js |
| 核心模块加载 | ~50ms | 不含懒加载模块 |
| 并行 Prefetch(最慢路径) | ~200ms | 通常是 Keychain 或 Feature Flag |
| Skill 发现 | ~20ms | 仅扫描 manifest |
| UI 渲染首帧 | ~100ms | Ink (React) 组件挂载 |
| 总计到可交互 | ~300ms | 取决于 Keychain 响应速度 |
2. QueryEngine — 核心循环引擎
QueryEngine 是 Claude Code 的心脏。它不是一个简单的「发请求、收回复」的函数——它是一个有状态的异步生成器,管理着从用户输入到模型响应再到工具执行的完整循环。
submitMessage() — Async Generator 设计
核心入口 submitMessage() 是一个 async generator(async function*),这意味着它不是一次性返回所有结果,而是逐条 yield 消息。每一个 yield 的值代表一个可以立即渲染到 UI 的事件:
这个设计的好处是流式渲染——UI 层不需要等待整个对话轮次结束就可以开始显示内容。用户看到的「逐字打印」效果就来自于这些连续的 text_delta yield。
消息处理管线
当用户输入一条消息后,它会经过一条完整的处理管线:
processUserInput 阶段详解
processUserInput 是一个前置处理器,它在消息发送到模型之前做以下事情:
| 处理项 | 说明 |
|---|---|
| 斜杠命令检测 | 如果输入以 / 开头,直接分发到对应的命令处理器,不经过模型。例如 /clear 直接清空对话历史。 |
| 附件处理 | 检测输入中是否包含文件路径或 URL 引用,如果有,将文件内容或 URL 抓取结果作为附件注入消息。 |
| 模型选择 | 根据当前设置确定使用哪个模型(默认 / fast mode / 用户指定),以及对应的 API endpoint。 |
| 图片处理 | 如果消息包含图片,压缩并编码为 base64,转换为 Claude 能理解的多模态内容格式。 |
| 上下文窗口检查 | 预估本次请求的 token 量,如果接近上下文窗口上限,触发 auto-compact(详见后文)。 |
工具调用循环
这是 QueryEngine 中最关键的逻辑:当模型的响应中 stop_reason 为 "tool_use" 时,引擎不会停下来,而是继续循环。
这意味着一次用户输入可以触发多轮模型调用。例如,用户说「帮我找到并修复这个 bug」,模型可能先调用 Grep 搜索相关文件,再调用 Read 阅读代码,然后调用 Edit 修改文件,每一步都是一次完整的 API 往返。
end_turn;2) 有最大工具调用轮次限制;3) token 耗尽会自动停止;4) 用户随时可以按 Esc 中断。
崩溃恢复机制
Claude Code 有一个重要的设计决策:消息在 API 响应返回之前就写入磁盘。
具体来说,每次调用 API 之前,当前的完整消息历史(包括刚刚追加的用户消息和工具结果)都会被序列化并写入本地文件系统。这意味着即使进程在 API 调用期间崩溃(网络断开、系统休眠、意外终止),下次启动时对话状态依然完整——最多丢失模型正在生成的那一条回复。
Permission Wrapping
在工具执行之前,每一次工具调用都会经过权限系统的拦截。这不是一个简单的 yes/no 检查,而是一个完整的决策链:
权限决策的优先级顺序
- Deny Rules(最高优先级):如果工具/路径匹配了 deny 规则,直接拒绝,没有任何覆盖机制
- Allow Rules:如果匹配了 allow 规则(用户之前批准过或在配置中预设),自动放行
- ReadOnly 工具:只读工具(如 Read、Grep、Glob)在大多数权限模式下自动放行
- PreToolUse Hooks:用户配置的自定义钩子可以在此阶段拦截或修改工具调用
- 交互式确认:如果以上都没有明确决定,弹出权限确认界面让用户选择
Token 追踪和用量统计
QueryEngine 内部维护着精确的 token 计数器。每次 API 调用返回时,响应头中的 usage 信息会被提取并累加:
| 统计项 | 用途 |
|---|---|
| input_tokens(本轮) | 计算本轮上下文大小,判断是否需要压缩 |
| output_tokens(本轮) | 计算模型生成量,影响费用估算 |
| cache_creation_input_tokens | Prompt caching 命中分析 |
| cache_read_input_tokens | 衡量缓存效率 |
| 累计 input/output tokens | 整个会话的总用量,显示在状态栏 |
| cost 估算 | 基于模型价格和 token 数量计算实时费用 |
3. 上下文装配 — 模型每次调用时看到什么
每次调用 Claude API 时,发送的不是一条简单的消息,而是一个精心组装的上下文包。理解这个组装过程,对于写出有效的 CLAUDE.md 和优化工具使用至关重要。
System Prompt 的组成
Claude Code 的 system prompt 不是一段固定文本。它是多个来源动态拼接的结果:
各部分的详细内容
核心人格与行为规则
这是 Claude Code 内置的基础指令,定义了 Claude 在 CLI 环境下应该如何行为:使用工具而非猜测、在修改文件前先阅读、遵循项目代码风格等。这部分用户不可修改。
CLAUDE.md 内容
Claude Code 会从多个位置查找并合并 CLAUDE.md 文件:
~/.claude/CLAUDE.md— 全局指令,适用于所有项目- 从 git 仓库根目录到当前工作目录路径上每一层的
CLAUDE.md .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:按需注入策略
这是一个精妙的两阶段策略:
- 第一阶段(始终加载):Skill 名称 + 一行描述出现在 system prompt 中,MCP 工具名出现在工具列表中。成本极低(几十个 token)。
- 第二阶段(按需加载):当模型决定使用某个 skill 或 MCP 工具时,才加载完整的提示模板、参数 schema、使用示例等。这些内容可能有数千个 token。
这种设计让模型知道有哪些能力可用(不会遗漏),但又不会在每次调用时浪费大量 token 在不相关的工具定义上。