深入解构Claude Code - 第 2 篇 · 启动的秘密

本篇回答四个问题:敲下命令的一瞬间程序先做什么?为什么查版本号只要 0.1 秒?几百个命令参数怎么管?整个程序共享的信息放在哪?


2.1 敲下命令的一瞬间:程序起床后做的第一件事

你在终端敲下启动命令、按下回车,到屏幕上出现对话界面,这中间大约 1 秒钟。这 1 秒里程序做了几十件事,但第一件事特别反直觉:它做的不是"准备对话",而是"校准钟表"。

一个必须最先做的小动作:性能垫片

程序的第一行有效代码(注意,真的是第一行,在所有其他代码之前),是导入一个叫"性能垫片"的小东西。垫片(shim)这个词你可以理解成"补丁"或"适配层"------在原有东西上面垫一层,让它变得合用。

这个垫片做的事很简单:统一"计时"功能。程序里到处都在掐表------这个网络请求花了多少毫秒、那段渲染用了多久、一次对话总共耗时多少。JavaScript 本身提供了计时功能,但不同环境(浏览器、Bun、某些被阉割过的运行环境)的计时功能精度和行为不一样。垫片在最开头把计时器换成一个统一、高精度的版本。

为什么非要是第一行?打个比方:你要给一栋大楼装统一的标准时钟,必须在任何人上班打卡之前装好。如果有人 9 点整已经用旧钟打了卡,你 9 点 01 分才换上新钟,那之前的打卡记录就全乱了。程序也是一样:别的代码一旦在垫片装好之前就用过计时功能,它们拿到的就是"旧钟"的数据,而且这些数据会一直留在程序里,长对话跑下来误差会累积到很难看的程度。

所以项目里有一条铁律:这个导入语句必须排在文件最顶上,连自动整理代码顺序的工具都不许动它(代码里专门写了注释告诉整理工具"跳过这里")。

起床后的待办清单

校准完钟表,程序接下来按顺序做这些事(这就是"引导序列",bootstrap 这个词原指靴子上的拉环,引申为"自己把自己拉起来"的开机自举过程):

  1. 看命令行参数:你是来正常聊天的,还是查版本、还是要跑后台模式?(下一章细讲)
  2. 找到工作目录:你在哪个文件夹里启动的它,它就管哪个文件夹;同时记住"项目根目录"是哪。
  3. 读配置:你的个人设置(主题、模型选择、权限偏好)存在配置文件里,启动时读进来。
  4. 检查登录状态:看看你有没有登录、用的是哪家服务。
  5. 准备全局小本本:初始化整个程序共用的那些信息(2.4 讲)。
  6. 决定穿哪件外套:是显示对话界面,还是进入"被程序调用/被编辑器调用"的无界面模式。

这里有个值得学的设计:初始化工作被包在一个"只执行一次"的壳子里(memoize,"记忆化"------第一次调用真的执行,之后再调用直接返回上次的结果)。为什么?因为启动逻辑分散在很多文件里,A 文件可能需要"配置已加载",B 文件也需要,谁也不确定自己是不是第一个用的。包上这层壳之后,大家只管喊一声"我要配置",程序保证配置只加载一次,无论多少人喊、什么时候喊。

打个比方:办公室里谁都可能需要会议室钥匙,与其规定"必须前台先开门大家才能用",不如配一把"谁第一个到谁开门,门开后所有人直接进"的机制。 memoize 就是这把锁。


2.2 为什么查个版本号只要 0.1 秒:快速通道设计

先做个实验想象:你敲 --version(查版本号),程序瞬间就回了,比如 0.1 秒。而正常启动进入对话界面要将近 1 秒。差的这 0.9 秒花在哪了?

答案是:查版本号时,程序 99% 的代码根本没有被加载

一个关于"加载代码"的冷知识

很多人以为程序启动 = 把所有代码读进内存。现代程序确实大体如此:你 import(导入)一个文件,它连带导入的文件、文件导入的文件......像滚雪球一样全部加载。一个大型程序,哪怕你只用其中一个小功能,启动时也可能把几万个文件全读进来------读文件、解析语法、编译成可执行格式,都要时间。

这个项目正常启动时,要加载对话循环、六十多个工具、终端界面库、模型对接层......雪球非常大。

但查版本号需要什么?只需要一个写死的版本字符串,别的什么都不需要。

快速通道:在收费站之前就放行

项目的做法是:在程序最最开头(大门入口处),先检查你是不是来办"简单业务"的。代码里大致是这样一串判断:

arduino 复制代码
你是来查版本号的吗?      → 是 → 打印版本号,立刻退出(不加载任何其他代码)
你是来导出系统提示的吗?  → 是 → 走专门的小路,办完就退出
你是来跑编辑器模式的吗?  → 是 → 走编辑器那条路
你是来跑后台 worker 的吗?→ 是 → 走后台那条路
......(一共约 14 条这样的快速通道)
都不是?
   → 好,你是来正常聊天的,这才把"全套家当"加载起来

打个比方:这就像高速公路收费站前的"ETC 专用道"。查版本号这种业务,等于一辆只送一封信的摩托车,不需要开进服务区、不需要卸货检查,收费站远远地招个手就放行了。只有"正常聊天"这辆大货车,才需要开进完整服务区------装载全部功能。

这个设计带来的好处是实打实的:

  • :简单业务 0.1 秒办完。
  • 省内存:不加载的代码不占内存。查版本号时内存占用只有正常模式的几十分之一。
  • :简单业务不依赖复杂环境,比如没登录、配置文件坏了,也照样能查版本号------这对排查问题特别重要("程序出问题了?先 --version 看看它还能不能说话")。

这里藏着一个通用的工程智慧:把"判断要做什么"放在"加载做事的家当"之前。绝大多数命令行程序、甚至很多服务器程序,都值得这样检查一遍:有没有一些请求是"轻量级"的?能不能让它们在全套系统启动之前就被处理掉?


2.3 几百个命令参数是怎么管理的

这个工具的命令行参数非常多:选模型、设权限模式、指定工作目录、切换输出格式、进入各种特殊模式......加起来有几百个选项。如果用 if (参数 === '--xxx') 这样一个个手写判断,代码会变成一团几百个 if 缠在一起的毛线球,没人敢改。

项目用了一个叫 Commander 的成熟库来管这件事。你可以把它理解成一个"总服务台登记系统":每个选项在开张前先到服务台登记------我叫什么名字、缩写是什么、要不要带参数、帮助说明怎么写、被选中了要干什么。

登记长这样(示意):

css 复制代码
登记一个选项:
  名字:--model
  缩写:-m
  需要带一个参数:模型名
  说明:选择使用哪个 AI 模型
  触发后:把模型名记下来

登记一个选项:
  名字:--version
  说明:显示版本号
  触发后:走快速通道打印版本

登记完之后,解析用户输入这件麻烦事全由库代办:用户写 --model sonnet,库自动认出"model 选项,值是 sonnet";用户把选项顺序写乱、用缩写、拼错,库都能处理并给出友好的错误提示和帮助文本。

为什么主文件有五千多行

你可能会听到一个吓人的数字:登记这些命令的主文件有五千多行。这是不是说明代码写得烂?恰恰相反,这是"摊平"的结果。

设想另一种写法:把每个命令的处理逻辑拆到几十个小文件里,主文件只留薄薄一层调度。看起来优雅,但你要查"--model 到底能接受哪些值、和 --effort 有没有冲突"时,得在几十个文件之间跳来跳去。

而现在的做法是:所有命令行契约(程序和用户之间的约定)都摊在同一个文件里,像一面贴满信封的墙。五千行里大部分是一行行的"登记信息",密度很低、一眼能扫。想知道程序支持哪些参数?看这一个文件就够了。

这是一个很实在的设计取舍:"集中、冗长、好查" vs "分散、优雅、难找"。对于"命令行参数"这种东西------它是程序对外的完整契约清单,很少改动但经常被查阅------集中摊平比拆碎更合用。第 12 篇讲设计取舍时还会提到这个例子。


2.4 启动时记住的"全局小本本":整个程序共享的信息放哪

程序跑起来后,有一些信息到处都要用:当前在哪个文件夹、这轮对话花了多少钱、用的哪个模型、现在是不是交互模式、会话编号是什么......这些信息放哪?

最容易想到的办法是:放在一个全局对象里,谁要用谁来拿。但全局变量在小程序里是便利,在大项目里是灾难------任何人任何地方都能改,出了问题你根本不知道是谁改的。

项目的做法是:确实有一个全局小本本,但对它的读写立了规矩

小本本上记什么

这个小本本(代码里就是一个集中的状态文件)上记的东西分几类:

  • 位置信息:启动时的目录、当前工作目录、项目根目录。这三个是分开的------因为运行中可以切换工作目录,但"项目是哪个"不能变(历史记录、记忆文件都挂在项目根上)。
  • 累计数字:总共花了多少钱、网络请求累计耗时、工具执行累计耗时、加了多少行代码、删了多少行。这些数字从启动开始只增不减。
  • 模型信息:当前用的模型、启动时的模型(留底,切换后还能查"本来用的是啥")。
  • 模式开关:是不是交互模式、是否开启了某些实验功能。

特别说明:小本本的文件顶上写着一句警告,大意是"别再往这里加东西了,对全局状态要手下留情"。因为每加一个全局字段,就多一份"谁都能改、出问题难查"的风险。加之前必须想清楚:这东西真的是全局都要用的吗?能不能局部解决?

读写规矩

规矩有三条:

  1. 不直接伸手进本本改。本本对外提供一对对"取值/设值"的小函数(取当前目录、设当前目录、累加花费......),所有读写都通过这些函数。这样以后想加"改值时记个日志"之类的统一处理,改一个地方就行。
  2. 深层的小文件不许直接碰本本。项目有自定义的检查规则:藏在很深目录里的工具文件如果想读全局信息,会被警告"请走正规渠道"。这防止了"随便哪个角落的代码都能篡改全局"。
  3. 界面状态不在这个本本上。这是下一篇要讲的重要分工------用户在输入框里打了一半的字、界面主题、弹窗状态,这些变得极快(敲一个字就变一次)的东西,放在另一处专门给界面用的状态里(React 的状态管理)。全局本本上只放"低频、重要、和界面无关"的东西。

打个比方:全局小本本像公司的人事档案室------每个人都可以查"公司全称是什么""报销额度多少",但只有人事部(专门的函数)能改,而且改了要留痕。而员工桌面上贴的便利贴(输入框草稿、界面高亮状态),不属于档案室,天天换也没人管。

为什么界面状态要分开,这里先埋个伏笔,第 8 篇讲数据管理时会完整解释:核心原因是两者"变化频率"和"使用者"完全不同------档案室的资料一天不变一次,桌面便利贴一秒变十次;档案室给全程序用,便利贴只有界面关心。混在一起,要么界面卡顿,要么全局状态被高频刷新污染。


本篇小结

  • 程序的第一个动作是校准计时器(性能垫片),必须在所有代码之前,晚了数据就不准。
  • 查版本号快,是因为入口处有快速通道:简单业务在全套家当加载之前就办完走人。
  • 几百个命令参数靠一个登记系统(Commander)集中管理,主文件五千行是故意"摊平"的结果,为了好查。
  • 全局信息集中在一个小本本上,但只许通过专门的函数读写,并且严格控制往里加东西;界面状态另有去处。

下一篇进入全书最核心的部分:你按下回车之后,"AI 想一句、做一步、再想一句"这个圈到底是怎么转起来的。

相关推荐
月月大王的3D日记1 小时前
Three.js 入门系列(9):从零搭一座“闹鬼小屋”
前端·javascript
开开心心就好2 小时前
电子教鞭工具支持画框写字插图片功能齐全
android·开发语言·前端·javascript·人工智能·pdf·html
Dovis(誓平步青云)2 小时前
拍视频前先把镜头想清楚:做一个分镜取景辅助器
android·java·服务器·javascript·人工智能
2601_962071573 小时前
Java进阶(vue基础)
前端·javascript·vue.js
研☆香3 小时前
数组方法 splice讲解 拓展
开发语言·前端·javascript
————A3 小时前
Agent 文件处理中间件
开发语言·javascript·ecmascript
Patrick_Wilson3 小时前
Superpowers 与 Codex Harness:冲突分析与治理提示词
agent·ai编程·cursor
ITresearchGuest3 小时前
LangChain JS 入门:快速搭建前端 AI 开发环境
前端·javascript·langchain
小磊哥er3 小时前
深入解构Claude Code - 第 1 篇 · 先认识它
javascript·ai编程