本篇回答四个问题:敲下命令的一瞬间程序先做什么?为什么查版本号只要 0.1 秒?几百个命令参数怎么管?整个程序共享的信息放在哪?
2.1 敲下命令的一瞬间:程序起床后做的第一件事
你在终端敲下启动命令、按下回车,到屏幕上出现对话界面,这中间大约 1 秒钟。这 1 秒里程序做了几十件事,但第一件事特别反直觉:它做的不是"准备对话",而是"校准钟表"。
一个必须最先做的小动作:性能垫片
程序的第一行有效代码(注意,真的是第一行,在所有其他代码之前),是导入一个叫"性能垫片"的小东西。垫片(shim)这个词你可以理解成"补丁"或"适配层"------在原有东西上面垫一层,让它变得合用。
这个垫片做的事很简单:统一"计时"功能。程序里到处都在掐表------这个网络请求花了多少毫秒、那段渲染用了多久、一次对话总共耗时多少。JavaScript 本身提供了计时功能,但不同环境(浏览器、Bun、某些被阉割过的运行环境)的计时功能精度和行为不一样。垫片在最开头把计时器换成一个统一、高精度的版本。
为什么非要是第一行?打个比方:你要给一栋大楼装统一的标准时钟,必须在任何人上班打卡之前装好。如果有人 9 点整已经用旧钟打了卡,你 9 点 01 分才换上新钟,那之前的打卡记录就全乱了。程序也是一样:别的代码一旦在垫片装好之前就用过计时功能,它们拿到的就是"旧钟"的数据,而且这些数据会一直留在程序里,长对话跑下来误差会累积到很难看的程度。
所以项目里有一条铁律:这个导入语句必须排在文件最顶上,连自动整理代码顺序的工具都不许动它(代码里专门写了注释告诉整理工具"跳过这里")。
起床后的待办清单
校准完钟表,程序接下来按顺序做这些事(这就是"引导序列",bootstrap 这个词原指靴子上的拉环,引申为"自己把自己拉起来"的开机自举过程):
- 看命令行参数:你是来正常聊天的,还是查版本、还是要跑后台模式?(下一章细讲)
- 找到工作目录:你在哪个文件夹里启动的它,它就管哪个文件夹;同时记住"项目根目录"是哪。
- 读配置:你的个人设置(主题、模型选择、权限偏好)存在配置文件里,启动时读进来。
- 检查登录状态:看看你有没有登录、用的是哪家服务。
- 准备全局小本本:初始化整个程序共用的那些信息(2.4 讲)。
- 决定穿哪件外套:是显示对话界面,还是进入"被程序调用/被编辑器调用"的无界面模式。
这里有个值得学的设计:初始化工作被包在一个"只执行一次"的壳子里(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 启动时记住的"全局小本本":整个程序共享的信息放哪
程序跑起来后,有一些信息到处都要用:当前在哪个文件夹、这轮对话花了多少钱、用的哪个模型、现在是不是交互模式、会话编号是什么......这些信息放哪?
最容易想到的办法是:放在一个全局对象里,谁要用谁来拿。但全局变量在小程序里是便利,在大项目里是灾难------任何人任何地方都能改,出了问题你根本不知道是谁改的。
项目的做法是:确实有一个全局小本本,但对它的读写立了规矩。
小本本上记什么
这个小本本(代码里就是一个集中的状态文件)上记的东西分几类:
- 位置信息:启动时的目录、当前工作目录、项目根目录。这三个是分开的------因为运行中可以切换工作目录,但"项目是哪个"不能变(历史记录、记忆文件都挂在项目根上)。
- 累计数字:总共花了多少钱、网络请求累计耗时、工具执行累计耗时、加了多少行代码、删了多少行。这些数字从启动开始只增不减。
- 模型信息:当前用的模型、启动时的模型(留底,切换后还能查"本来用的是啥")。
- 模式开关:是不是交互模式、是否开启了某些实验功能。
特别说明:小本本的文件顶上写着一句警告,大意是"别再往这里加东西了,对全局状态要手下留情"。因为每加一个全局字段,就多一份"谁都能改、出问题难查"的风险。加之前必须想清楚:这东西真的是全局都要用的吗?能不能局部解决?
读写规矩
规矩有三条:
- 不直接伸手进本本改。本本对外提供一对对"取值/设值"的小函数(取当前目录、设当前目录、累加花费......),所有读写都通过这些函数。这样以后想加"改值时记个日志"之类的统一处理,改一个地方就行。
- 深层的小文件不许直接碰本本。项目有自定义的检查规则:藏在很深目录里的工具文件如果想读全局信息,会被警告"请走正规渠道"。这防止了"随便哪个角落的代码都能篡改全局"。
- 界面状态不在这个本本上。这是下一篇要讲的重要分工------用户在输入框里打了一半的字、界面主题、弹窗状态,这些变得极快(敲一个字就变一次)的东西,放在另一处专门给界面用的状态里(React 的状态管理)。全局本本上只放"低频、重要、和界面无关"的东西。
打个比方:全局小本本像公司的人事档案室------每个人都可以查"公司全称是什么""报销额度多少",但只有人事部(专门的函数)能改,而且改了要留痕。而员工桌面上贴的便利贴(输入框草稿、界面高亮状态),不属于档案室,天天换也没人管。
为什么界面状态要分开,这里先埋个伏笔,第 8 篇讲数据管理时会完整解释:核心原因是两者"变化频率"和"使用者"完全不同------档案室的资料一天不变一次,桌面便利贴一秒变十次;档案室给全程序用,便利贴只有界面关心。混在一起,要么界面卡顿,要么全局状态被高频刷新污染。
本篇小结
- 程序的第一个动作是校准计时器(性能垫片),必须在所有代码之前,晚了数据就不准。
- 查版本号快,是因为入口处有快速通道:简单业务在全套家当加载之前就办完走人。
- 几百个命令参数靠一个登记系统(Commander)集中管理,主文件五千行是故意"摊平"的结果,为了好查。
- 全局信息集中在一个小本本上,但只许通过专门的函数读写,并且严格控制往里加东西;界面状态另有去处。
下一篇进入全书最核心的部分:你按下回车之后,"AI 想一句、做一步、再想一句"这个圈到底是怎么转起来的。