1 总体架构
Claude Code 是一个基于大模型的编程 Agent:模型负责决策,它自己这几十万行 TypeScript 负责把决策变成动作------执行工具、管理上下文、守住权限。它有三种交付形态:命令行工具(CLI)、可嵌入的 SDK、MCP 服务。三种形态不是三份代码,来自同一条代码库。
这条代码库整体上分五层:

| 分层 | 职责 | 代表位置 |
|---|---|---|
| 入口 | 每种运行形态一个入口文件 | src/entrypoints/ |
| 装配 | 启动时初始化与组装 | src/main.tsx |
| 引擎 | 驱动会话的主循环与工具协议 | src/query.ts |
| 能力 | 工具、命令、服务 | src/tools/、src/commands/、src/services/ |
| 支撑 | 权限、编排、扩展、界面 | src/utils/permissions/ 等 |
2 运行视图

一次完整的运行分两个阶段。
**启动阶段,每次运行走一遍。**你在终端输入 claude:
- 入口层
cli.tsx补环境、按运行模式分拣; - 控制权交给装配层
main.tsx; main.tsx并行预取系统配置和钥匙串、解析参数、按当前环境拼出命令集;- 挂载终端界面,等待输入。
**会话阶段,每一轮对话走一遍。**你输入一句话:
- 消息进入引擎层
query.ts主循环; - 主循环把消息连同工具清单(装配时注入的那份)发给模型;
- 模型要求调用工具时,执行交给能力层------先过校验、权限、Hook(权限系统在支撑层),再真正执行;
- 执行结果包装成消息回灌对话,回到第 2 步;
- 模型不再要求工具,给出最终回答,流式渲染到终端。
注意:主循环自己不执行任何工具,整个流程里也不存在一个统一调度的「总管」------每一步只和相邻层的接口打交道。
3 入口层:三种形态
入口 = 每种运行形态自己的启动文件。Claude Code 的入口全部位于 src/entrypoints/:
cli.tsx起命令行(可执行入口,包的 bin 指向它)mcp.ts起 MCP 服务sdk/给嵌入方
以最常用的 cli.tsx 为例。它做三件事:补环境、分拣运行模式、交接。前两件是例行准备,值得看的是交接的那一刻:
typescript
// src/entrypoints/cli.tsx
profileCheckpoint('cli_before_main_import');
const {
main: cliMain
} = await import('../main.js');
profileCheckpoint('cli_after_main_import');
await cliMain();
三种形态共用一套核心
看 mcp.ts 文件头的引入:
typescript
// src/entrypoints/mcp.ts
import review from '../commands/review.js'
import type { Command } from '../commands.js'
import { getTools } from '../tools.js'
import { hasPermissionsToUseTool } from '../utils/permissions/permissions.js'
工具清单、命令注册表、权限检查,引的都是命令行模式下的同一批模块。三种形态的差异被拦截在入口文件里;核心模块一套,零改动。
这一层的设计理念是同核多形态:每新增一种形态,就新增一个入口文件;核心一行不动。
为什么不用单入口+参数来区分形态?
替代方案是单入口 + 参数区分形态,但这有两个问题:
- 参数表随形态数量膨胀;
- 每接入一种新宿主都要改核心代码------而改核心,意味着所有既有形态都要回归测试。
「新形态 = 新入口文件」则完全不触碰既有核心模块。这个取舍后面每一层都会再次出现:新增的能力放在外围,不触碰核心。
4 装配层:冷启动与按需加载
main.tsx负责装配:解析命令行参数、挂载终端界面、准备各子系统,把程序带到「可以开始对话」的状态。
装配层要解决一个 CLI 特有的问题:冷启动 。服务端程序启动一次可以运行数月,初始化开销摊得薄;CLI 每次运行都从零启动------你每输入一次 claude,启动开销就重复支付一次。
所以装配的原则是「用多少,装多少」,落到四个机制:
- 并行预取。
main.tsx的文件头,先把启动路径上最慢的两件事先启动,不等结果;后面的 import 继续加载,两件事在后台并行跑:
typescript
// src/main.tsx(文件头,省略性能打点与部分注释)
// These side-effects must run before all other imports:
// 2. startMdmRawRead fires MDM subprocesses (plutil/reg query) so they run in
// parallel with the remaining ~135ms of imports below
// 3. startKeychainPrefetch fires both macOS keychain reads (OAuth + legacy API
// key) in parallel --- ... (~65ms on every macOS startup)
// 译:读系统配置的子进程、两笔钥匙串读取,与下面剩余约 135ms 的 import 并行跑;
// 否则钥匙串会被顺序读,每次 macOS 启动多花约 65ms(数字都来自源码注释)
import { startMdmRawRead } from './utils/settings/mdm/rawRead.js';
startMdmRawRead();
import { ensureKeychainPrefetchCompleted, startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js';
startKeychainPrefetch();
import { feature } from 'bun:bundle';
// ...往下还有 160 多行 import
-
懒加载。
main.tsx里有大量await import。遥测、分析这些模块体量大,启动时又用不上------先不加载,第一次真正用到才加载进内存。 -
**按环境加载。**交互终端、CI、无头环境里可用的命令集不同;加载哪些命令,由启动时按当前环境现查。
-
**编译期裁剪。**光
main.tsx一个文件里就有 61 处feature(...)调用------编译期开关,构建时决定哪些能力进入产物。关闭的功能不打进包里,而不是运行时绕行:死代码不进产物。
四个机制合起来一句话:能推迟的推迟,能排除的排除,两者都做不到的,并行执行。
5 引擎层:主循环、工具协议与会话封装
引擎是驱动整个会话的核心,由三个文件构成,全部直接放在 src/ 根目录、与各子目录平级:
| 文件 | 职责 |
|---|---|
query.ts |
主循环------问模型、执行工具、结果回灌 |
Tool.ts |
工具协议------规定一只工具长什么样 |
QueryEngine.ts |
会话封装------包着 query 循环,管用量统计、中断、给 SDK 用 |
引擎文件里没有工具
打开 query.ts 搜索工具名------BashTool、FileReadTool、GlobTool------一个都找不到。引擎三个文件对 tools/ 目录的引用只有四处,没有一处 import 工具实现------引的全是类型定义和工具名常量。比如判断「刚才执行的是不是等待一秒那只」,比的是工具名,不是工具本身。
工具从哪来?
看主循环请求模型的代码:
typescript
// src/query.ts
for await (const message of deps.callModel({
messages: ...,
systemPrompt: fullSystemPrompt,
tools: toolUseContext.options.tools,
...
toolUseContext.options.tools:工具清单由启动时的装配层注入。引擎面对的永远是「这批工具」,而不是固定的某批工具。
工具的执行也不在引擎里
工具的执行同样不在引擎里,query.ts 把它整体委托给服务层:
typescript
// src/query.ts
import { runTools } from './services/tools/toolOrchestration.js'
引擎认识的不是工具,而是协议
Tool.ts 定义的工具契约,规定一个工具必须提供什么:
typescript
// src/Tool.ts
export type Tool<
Input extends AnyObject = AnyObject,
Output = unknown,
...
> = {
...
call(...): Promise<ToolResult<Output>>
description(...): Promise<string>
readonly inputSchema: Input
...
名称、说明、参数定义、执行函数、权限模型,全部写在工具自己身上,按契约实现;引擎只验收契约,不验收具体工具。
主循环代码量很少,它的能力扩展------新工具、新命令、新服务、新入口------几乎全部在引擎之外实现。
为什么引擎不内置工具清单?
工具清单的特点,一是多、清单很大,二是经常变化。两种设计,代价不同:
- 写死在引擎里:每次变动都要改全库最不能出错的主循环,一处回归问题影响全部能力;
- 放在核心之外、只认协议:加一只工具,引擎一行不改;换掉一半工具,引擎不受影响。
引擎保持稳定,能力层才能持续扩展。
6 能力层:工具、命令、服务
能力 = 模型和用户可用的功能,分三类:
- 工具------模型执行动作用。
src/tools/下的工具目录,一个工具一个目录,目录名即能力名:BashTool、FileReadTool、FileEditTool、GlobTool、AgentTool......参数定义、权限规则、执行函数全部自带------一个工具的全部实现,都在自己的目录里。 - 命令------用户以斜杠调用。
src/commands/下的实现文件,不少命令有专属子目录;注册表不是一张静态表,而是commands.ts里的一个函数,每次调用按当前环境现拼一份命令清单返回。 - 服务------支撑性子系统。
src/services/下的子目录,一个服务一个。
三类能力同构:接入点由引擎和装配层提供,内容由能力模块自带。
这一层的设计理念是自包含模块:新增一个能力 = 新增一个目录 + 注册表加一行,不碰引擎、也不碰其他能力模块。
为什么不按职能设计?
替代方案是按职能设计------参数定义、执行函数、权限规则各集中一个目录,看起来更整齐。但两种设计,代价不同:
- 按职能设计:每加一个能力,要横跨多个目录改多处;复查一个改动,要跨几十个文件核对;
- 自包含:一个能力的增删都在同一个目录内完成------新增不影响其他模块,删除不产生遗留引用。
7 支撑层:权限、编排、扩展、界面
其余代码归入支撑层:让上面四层能够安全运行、承接更大的任务、对外连接的外围系统。
- **
utils/permissions/------权限。**模型的每个动作过三态检查:直接放行 / 直接拒绝 / 必须询问用户。权限是独立的一层,不是散落在各工具里的判断:同一套规则全局一致,可配置、可审计。 - **
coordinator/------多 Agent 编排。**一个上下文装不下的任务,派给多个 Agent:通信、分发、互不干扰。 - **
skills/、plugins/、MCP------扩展。**能力边界不写死在代码里:工作流以文档形式渐进加载,外部工具按协议动态接入。清单永远列不全,所以留扩展点。 - **
components/------界面。**用 React + Ink 渲染终端界面------React 把「画到哪里」交给渲染器:react-dom画到浏览器,Ink 画到终端,同一套组件模型。
另有 bridge/(IDE 与远程桥接)、state/(状态管理)、schemas/(配置校验)等,不逐一举例。
支撑层同样没有总管:这些目录服务整机,但没有任何一个位于所有目录之上做统一调度。
8 设计理念:靠边界,不靠总管
把五层的理念收成一张表:
| 层 | 理念 | 为什么 |
|---|---|---|
| 入口 | 同核多形态 | 新形态 = 新入口文件,核心零改动 |
| 装配 | 用多少装多少 | CLI 每次冷启动,启动开销重复支付 |
| 引擎 | 只认协议,不认工具 | 核心不动,清单常新 |
| 能力 | 自包含模块 | 新增不改动其他模块,删除不留遗留引用 |
| 支撑 | 权限是层,边界留扩展点 | 规则全局一致;清单永远列不全 |
回到 §2 留下的事实:没有总管,靠什么保持秩序?答案就是这五层:**靠边界。**每层只依赖下一层的接口------入口把控制权交给装配,装配把工具清单注入引擎,引擎只认协议,能力按协议实现,支撑在外围提供保障。由此得到的性质是具体的:
- 改一个工具,不触及引擎;
- 加一种形态,不触及核心;
- 替换大半能力,不影响主循环。
这样的秩序,不来自一个什么都管的中央模块,来自一组谁都不越的边界。
9 三条经验,用在你自己的系统上
Claude Code 的设计讲解完了;下面三条,换成你自己搭系统的时候直接能用:
- **新增的能力放外围,不碰核心------开闭原则。**多一种运行形态,加一个入口文件;多一只工具,加一个目录。反过来,「加个新功能要改核心代码」就是该修的信号------核心每被碰一次,所有既有能力都要跟着回归测试。
- **冷启动按「用多少,装多少」------按需加载、懒加载。**能推迟的推迟(懒加载),能排除的排除(编译期裁剪),都做不到的并行执行(预取)。每次都从零启动的程序,启动路径上每一毫秒都在重复支付。
- **靠边界,不靠总管------分层架构、松耦合。**每层只依赖下一层的接口,不让任何一个模块位于所有模块之上做统一调度。检验标准就一条:你的系统敢不敢让人替换掉大半能力?敢,说明边界立住了。
今晚就能试:翻开你手头最大的项目,数一数核心流程文件直接 import 了多少个功能模块------超过一只手,就是这篇说的「总管病」。