文章目录
- [1 先给整个架构定个性](#1 先给整个架构定个性)
-
- [1.1 整条代码库分五层](#1.1 整条代码库分五层)
- [2 跑起来分两个阶段](#2 跑起来分两个阶段)
-
- [2.1 启动阶段:冷启动的全套流程](#2.1 启动阶段:冷启动的全套流程)
- [2.2 会话阶段:每轮对话的循环](#2.2 会话阶段:每轮对话的循环)
- [3 入口层:一套核心,三种皮肤](#3 入口层:一套核心,三种皮肤)
-
- [3.1 核心设计:同核多形态](#3.1 核心设计:同核多形态)
- [3.2 为啥不搞单入口+参数?](#3.2 为啥不搞单入口+参数?)
- [4 装配层:把冷启动开销摁死](#4 装配层:把冷启动开销摁死)
-
- [4.1 CLI的天然痛点:次次冷启动](#4.1 CLI的天然痛点:次次冷启动)
- [4.2 四个省时间的小技巧](#4.2 四个省时间的小技巧)
-
- [4.2.1 并行预取](#4.2.1 并行预取)
- [4.2.2 懒加载](#4.2.2 懒加载)
- [4.2.3 按环境加载](#4.2.3 按环境加载)
- [4.2.4 编译期裁剪](#4.2.4 编译期裁剪)
- [5 引擎层:最稳定的甩手掌柜](#5 引擎层:最稳定的甩手掌柜)
-
- [5.1 引擎里没有任何工具实现](#5.1 引擎里没有任何工具实现)
- [5.2 工具从哪来?装配层注入](#5.2 工具从哪来?装配层注入)
- [5.3 工具执行也不在引擎里](#5.3 工具执行也不在引擎里)
- [5.4 引擎只认协议,不认工具](#5.4 引擎只认协议,不认工具)
- [5.5 为啥不把工具写死在引擎里?](#5.5 为啥不把工具写死在引擎里?)
- [6 能力层:每个模块都是个体户](#6 能力层:每个模块都是个体户)
-
- [6.1 三类能力](#6.1 三类能力)
- [6.2 设计理念:自包含模块](#6.2 设计理念:自包含模块)
- [6.3 为啥不按职能拆分?](#6.3 为啥不按职能拆分?)
- [7 支撑层:外围的隐形保障](#7 支撑层:外围的隐形保障)
-
- [7.1 权限系统:独立的一层](#7.1 权限系统:独立的一层)
- [7.2 多Agent编排](#7.2 多Agent编排)
- [7.3 扩展点:能力不写死](#7.3 扩展点:能力不写死)
- [7.4 终端界面](#7.4 终端界面)
- [8 核心设计哲学:靠边界,不靠总管](#8 核心设计哲学:靠边界,不靠总管)
-
- [8.1 没有总管,靠边界维持秩序](#8.1 没有总管,靠边界维持秩序)
- [9 三个可直接抄的经验](#9 三个可直接抄的经验)
-
- [9.1 新增能力放外围,别碰核心](#9.1 新增能力放外围,别碰核心)
- [9.2 冷启动场景,坚持「用多少装多少」](#9.2 冷启动场景,坚持「用多少装多少」)
- [9.3 靠边界,不靠总管](#9.3 靠边界,不靠总管)

P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看, 传送门https://blog.csdn.net/HHX_01
1 先给整个架构定个性
Claude Code本质上就是个「大模型拍板,代码干活」的编程Agent。模型负责想想要干啥,那几十万行TypeScript负责把想法落地------调工具、管上下文、守权限。
它有三种交付形态:命令行CLI、可嵌入SDK、MCP服务。别以为是三套代码,人家全是同一条代码库编译出来的。
1.1 整条代码库分五层
从外到内一共五层,每层职责边界划得清清楚楚,就像个分工明确的小公司。
| 分层 | 核心职责 | 对应代码位置 |
|---|---|---|
| 入口层 | 每种运行形态一个启动入口 | src/entrypoints/ |
| 装配层 | 启动初始化、按需组装能力 | src/main.tsx |
| 引擎层 | 会话主循环、工具协议定义 | src/query.ts |
| 能力层 | 工具、命令、服务的具体实现 | src/tools/、src/commands/、src/services/ |
| 支撑层 | 权限、编排、扩展、终端界面 | src/utils/permissions/ 等 |
2 跑起来分两个阶段
一次完整的运行分两大阶段,启动阶段每次运行走一遍,会话阶段每轮对话走一遍。
2.1 启动阶段:冷启动的全套流程
你在终端敲下claude回车,程序就开始启动流程。
先是入口层cli.tsx补环境、判断运行模式;然后把控制权交给装配层main.tsx。
装配层并行拉配置、读密钥、解析参数,按当前环境拼出可用的命令集,最后挂载终端界面,等你输入。
整个过程就像你早上到公司:先打卡开机,后台偷偷登微信钉钉,等你接杯水坐下来,电脑刚好能用。
2.2 会话阶段:每轮对话的循环
你输入一句话,就进入会话主循环。
消息先到引擎层主循环,主循环把你的消息+工具清单一起发给大模型。
模型说要调用工具,就交给能力层执行------先过权限校验、钩子逻辑,再真正跑工具。
执行结果包装成消息塞回对话里,再发给模型,直到模型说不用工具了,给出最终回答流式渲染到终端。
重点:主循环自己啥工具都不执行,全程只和相邻层的接口打交道,没有啥都管的「大总管」。
3 入口层:一套核心,三种皮肤
入口层就是每种运行形态自己的启动文件,全放在src/entrypoints/目录下。
cli.tsx起命令行,mcp.ts起MCP服务,sdk/给第三方嵌入用。
3.1 核心设计:同核多形态
三种形态用的是完全同一套核心模块。入口文件把形态差异全部拦截在外,核心代码一行都不用改。
比如MCP的入口文件里,工具清单、命令注册表、权限检查,引的全是命令行模式下的同一批模块。
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'
说白了就是:新增加一种运行形态,只需要加一个新的入口文件就行,核心代码碰都不用碰。
3.2 为啥不搞单入口+参数?
肯定有人会说,搞这么多入口干嘛,整个单入口加个参数区分形态不行吗?
还真不行。首先参数表会随着形态越来越多越变越长,最后变成屎山。
更关键的是,每加一种新形态都要改核心代码,改核心就意味着所有老形态都要回归测试。
就像你家玄关,本来只是进门的地方,你非要让它兼顾快递柜、鞋柜、杂物间,最后加个新功能,整个玄关都得砸了重装。
4 装配层:把冷启动开销摁死
装配层的核心就是main.tsx,负责把程序从0组装到「可以对话」的状态。
4.1 CLI的天然痛点:次次冷启动
CLI工具和服务端不一样。服务端启动一次跑半年,初始化开销摊薄到可以忽略。
CLI每次运行都从零开始,你每敲一次命令,启动开销就重复付一次。那每多100毫秒,都是在消耗用户的耐心。
所以装配层的核心原则就是:用多少,装多少。
4.2 四个省时间的小技巧
为了把冷启动时间压到最低,人家用了四个机制,全是干货。
4.2.1 并行预取
文件头先把启动路径上最慢的两件事给启动了------读系统配置的子进程、钥匙串读取,不等结果,后台先跑着。
后面的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)
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
4.2.2 懒加载
遥测、分析这些体量大、启动又用不上的模块,先不加载。
什么时候第一次真正用到,什么时候才加载进内存。用不上的,永远不占资源。
4.2.3 按环境加载
交互终端、CI流水线、无头环境,能用的命令本来就不一样。
启动的时候按当前环境查一遍,该加载啥加载啥,没用的一律不装。
4.2.4 编译期裁剪
光main.tsx一个文件里就有61处编译期开关,构建的时候就决定哪些能力打进包里。
关闭的功能直接不进产物,连死代码都没有,比运行时判断高效多了。
四个机制合起来就是一句话:能推迟的推迟,能排除的排除,都不行的就并行跑。
5 引擎层:最稳定的甩手掌柜
引擎是整个会话的核心驱动,一共三个文件,全放在src根目录,地位一目了然。
| 文件 | 核心职责 |
|---|---|
query.ts |
主循环:问模型、执行工具、结果回灌 |
Tool.ts |
工具协议:规定一个工具长啥样 |
QueryEngine.ts |
会话封装:管统计、中断、给SDK用 |
5.1 引擎里没有任何工具实现
你去query.ts里搜BashTool、FileReadTool这些工具名,一个都搜不到。
引擎对工具目录的引用全是类型定义和常量,连一个工具实现都没import。
说白了,引擎根本不知道具体有啥工具,它只知道「有一批工具」。
5.2 工具从哪来?装配层注入
主循环请求模型的时候,工具清单是从上下文里拿的,是启动的时候装配层注进来的。
typescript
// src/query.ts
for await (const message of deps.callModel({
messages: ...,
systemPrompt: fullSystemPrompt,
tools: toolUseContext.options.tools,
...
引擎面对的永远是「这批工具」,至于这批工具具体是啥,引擎不关心。
5.3 工具执行也不在引擎里
不光工具定义不在引擎里,连执行都不在。主循环把工具执行整体委托给了服务层。
typescript
// src/query.ts
import { runTools } from './services/tools/toolOrchestration.js'
5.4 引擎只认协议,不认工具
Tool.ts定义了工具的契约:名字、说明、参数 schema、执行函数、权限模型,一个工具必须满足这些。
typescript
// src/Tool.ts
export type Tool<
Input extends AnyObject = AnyObject,
Output = unknown,
...
> = {
...
call(...): Promise>
description(...): Promise
readonly inputSchema: Input
...
只要你按契约实现,你塞啥工具进来引擎都能用。你换掉一半工具,引擎一行代码都不用改。
5.5 为啥不把工具写死在引擎里?
工具清单两大特点:一是多,二是经常变。
写死在引擎里,每次加工具改工具都要动主循环------全库最不能出错的地方。出一个bug,所有能力全崩。
放在外面只认协议,加工具引擎不用动,换工具引擎也不用动。核心稳了,外围才能随便折腾。
6 能力层:每个模块都是个体户
能力层就是模型和用户能用的所有功能,分三类:工具、命令、服务。
6.1 三类能力
工具 :给模型执行动作用的,在src/tools/下,一个工具一个目录,目录名就是能力名。参数、权限、执行逻辑全在自己目录里。
命令 :用户用斜杠调用的,在src/commands/下。注册表不是静态表,每次调用按当前环境现拼。
服务 :支撑性子系统,在src/services/下,一个服务一个目录。
6.2 设计理念:自包含模块
新增一个能力,就是新增一个目录,再加一行注册,不碰引擎,也不碰其他能力模块。
删除一个能力,直接删目录就行,不会留下一堆没人用的遗留引用。
6.3 为啥不按职能拆分?
肯定有人觉得,把所有参数定义放一个目录,所有执行函数放一个目录,看起来更整齐。
整齐是整齐了,加一个新能力,你得横跨好几个目录改好几处。查一个bug,得翻几十个文件。
就像传统工厂,做个产品要跑好几个车间,加个新产品,每个车间都得改工艺。出了问题,冲压说焊接的锅,焊接说装配的问题。
自包含就像个体户,从谈单到交付全自己扛,加新业务直接开个新店,互不干扰。
7 支撑层:外围的隐形保障
剩下的代码都归支撑层,负责让上面四层安全运行、承接大任务、对外连接。
7.1 权限系统:独立的一层
模型的每个动作都要过三态检查:直接放行、直接拒绝、必须问用户。
权限是独立的一层,不是散落在每个工具里的判断。同一套规则全局一致,可配置,可审计。
总比每个工具自己写权限判断强,不然哪天哪个工具漏写了,直接把你系统删了都有可能。
7.2 多Agent编排
一个上下文装不下的大任务,就派给多个Agent去做。通信、分发、隔离,全靠编排模块管。
7.3 扩展点:能力不写死
skills、plugins、MCP,都是扩展点。能力边界不写死在代码里,工作流可以渐进加载,外部工具按协议动态接入。
毕竟能力永远列不全,留好口子比啥都强。
7.4 终端界面
用React+Ink渲染终端界面。React负责逻辑,渲染器负责画到哪。react-dom画浏览器,Ink画终端,同一套组件模型。
支撑层同样没有总管,这些目录各干各的,没有谁凌驾于谁之上做统一调度。
8 核心设计哲学:靠边界,不靠总管
把五层的设计理念收一下,就是一张表。
| 层级 | 核心理念 | 底层原因 |
|---|---|---|
| 入口 | 同核多形态 | 新形态=新入口文件,核心零改动 |
| 装配 | 用多少装多少 | CLI次次冷启动,启动开销重复付 |
| 引擎 | 只认协议,不认工具 | 核心不动,能力常新 |
| 能力 | 自包含模块 | 新增不影响其他,删除不留遗留 |
| 支撑 | 权限成层,边界留扩展 | 规则全局一致;能力永远列不全 |
8.1 没有总管,靠边界维持秩序
整个系统里,没有一个啥都管的中央模块。每层只依赖下一层的接口,谁都不越界。
入口把控制权交装配,装配把工具清单注引擎,引擎只认协议,能力按协议实现,支撑在外围兜底。
改一个工具,碰不到引擎;加一种形态,碰不到核心;换掉大半能力,主循环该咋跑咋跑。
这种秩序,从来不是靠一个大权在握的总管管出来的,是靠大家都守边界守出来的。
9 三个可直接抄的经验
看完人家的设计,别光觉得厉害,这三条直接能用到你自己的项目里。
9.1 新增能力放外围,别碰核心
这就是开闭原则的极致体现。多一种运行形态,加个入口文件;多一个工具,加个目录。
反过来,如果你加个新功能,必须改核心代码,那就是架构出问题了。
核心每被碰一次,所有既有能力都得跟着回归测试,成本高到离谱。
9.2 冷启动场景,坚持「用多少装多少」
只要是每次都要从零启动的场景,启动路径上的每一毫秒都值得抠。
能推迟的推迟(懒加载),能排除的排除(编译期裁剪),都做不到的就并行执行(预取)。
别上来就把所有东西都加载进来,用户可能只用一个功能,你把全家桶都装上,纯纯浪费。
9.3 靠边界,不靠总管
分层架构、松耦合,说起来都是老生常谈,做起来难。
检验标准就一条:你的系统敢不敢让人替换掉大半能力?敢,说明边界立住了;不敢,那就是得了「总管病」。
今晚就能验证:打开你手头最大的项目,数数核心流程文件直接import了多少个功能模块。
超过一只手的数?恭喜你,总管病确诊了。
P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看,传送门https://blog.csdn.net/HHX_01