我几乎没有「写」过这套东西。
我的输入长这样——一句策略,加上一段从 CI 日志里复制出来的报错:
「先不用,我每个子应用都没有构建成功,我们先一个一个解下,先是……仓,刚才触发构建的时候日志是……」
「先修改这个仓吧,然后我再把其他仓的具体报错发给你,再根据实际的情况定向改。」
「先 commit 吧,然后我给你下一个子仓的代码路径和报错日志。」
没有方案、没有架构图、没有一行代码。我全程做的事只有两件:给目标、做确认。
最后交回来的是一套 6 个仓库联动的构建体系,把跨仓发版的中位耗时从 12.5 分钟压到 6.85 分钟,而且带一整套自己能证明自己有效的校验。
这套流程叫 Peaks-Loop。下面是我实际怎么用它、它替我省掉了哪三件事、以及它做错了什么。
一、我给的从来不是方案
把上面三句话拆开看,我的输入里其实只有三种东西:
| 我给的东西 | 例子 | 我没有给的东西 |
|---|---|---|
| 一个坐标 | 某个子应用仓的绝对路径 | 该改哪个文件 |
| 一份证据 | 从 CI 上原样粘贴的构建日志 | 报错的根因是什么 |
| 一条策略 | 「先一个一个解下」「根据实际情况定向改」 | 具体怎么改 |
整个 6 仓排查的那一轮,我一共只说了 14 句话,其中 3 次是打断它。
而且这些打断很关键。它一度把两个仓的关系搞错了,我的回复是:
「等下。」
「之前是把 agent 拆分为了 flow 和 agentic 两个仓……也是你刚才的质疑。」
注意这句话的形态:我没有直接指出错误,我只是陈述了一个事实(历史上是怎么拆的),剩下的是它自己去对账。这是我在用这套流程时最常用的一种干预方式——我不纠错,我只补充它不知道的上下文。
还有一次我补了一条约束:
「使用 main 分支的代码。」
一句话,它就换了参照基线。没有解释,没有铺垫,因为它已经知道这个项目的全貌——这就是「不用重新讲背景」的物证。
二、约束:一个必须先讲清楚的前提
要说清它在跟什么较劲,得先讲这个约束。
这是一个 qiankun 架构的微前端平台,拆成 6 个仓库:一个主应用做壳,持路由、登录态与公共布局;5 个子应用各自独立构建、独立发镜像。
关键在于:子应用的构建产物不是运行时从远端加载的,而是在主应用构建时就被烤进主应用镜像。
这条约束一旦成立,后面全是推论——6 个镜像必须在同一次发版里原子地产出,缺一不可。跨仓协作不是优化项,是硬约束。
它带来的第一个代价,就是我最初看到的那一幕:每个子应用都构建不成功。
三、它先做的不是改代码,是先找重复
它没有直接动我贴的报错,而是先把整个项目的构建画像扫了一遍,然后给回一张表。表里有一行很刺眼:
apk add --no-cache git ca-certificates 419.5 s
一个装 git 和根证书的命令,占掉整个构建一半以上的时间。而更关键的是它给出的结论,大意是:
这笔钱为什么要付十遍?
5 个子应用 × 2 个架构腿 = 10 个 job,每个都在自己全新的容器里从头装一遍同样的工具链——同一批与业务毫无关系的包。
当一个成本被乘以 N,先消灭这个乘法,再优化被乘数。 把一个被重复十次的长操作优化掉一半,收益远小于让它只发生一次,而且后者不需要任何算法创新,只需要一次判断。
这就是我最初感受到的差别:我贴的是「这些仓都失败了」,它回答的是「它们为什么都在付同一笔钱」。 前者是现象,后者是结构。
四、它交回来的三层解法

第一层:共享什么,以及拒绝共享什么。
它造了一个只装工具的镜像:node 基础镜像 + git + 根证书 + 包管理器。形态上很朴素,但有一条刻意的自我限制——只贡献工具,不含任何应用代码、依赖或构建缓存。
这不是洁癖,是故障模式的选择。如果基础镜像里混进了业务依赖,它一旦过期,构建出来的就是错的东西,而错的东西通常不报错,它会安静地产出一个看起来正常的错误产物。反过来,只装工具的镜像过期了,最坏是「多装了一遍」——一个慢的问题,不是错的问题。
共享的边界,应该由「失效时有多难被发现」决定,而不是由「能共享多少」决定。
第二层:契约,优先选择不需要协商的那种。
它第一版想做内容哈希:镜像内容变了,引用名就变。听起来更严谨。但这个方案被推翻了,理由很值得记:内容哈希要求 6 个仓库各自独立算出同一个值,而它们的一个输入是分支路径——在打 tag 的流水线上这个值压根不存在。结果就是:一个依赖分支名的引用,变成了只有主应用才有能力正确推导的东西,而子应用推错时的报错是「manifest unknown」——看起来像网络故障,实际是逻辑错误。
最后的解法是:引用名就是发布 tag 本身。 主应用在 5 个子应用仓库里创建同名 tag,子应用的 tag 变量与主应用的逐字节相同。既然两边拿到同一个字符串,就不需要「约定一个算法」——字符串本身就是契约。
不需要协商的东西,不会协商失败。
第三层:编排里的三个取舍。
广播为什么是串行——它先试了并发,实测拿到大量 5xx,于是改回串行,总耗时以秒计;而上一版用固定间隔的 sleep 错开请求,为此付出过八分钟的纯等待。八分钟的 sleep 曾是这条流水线最昂贵的部分之一,问题不是慢,而是它在等一个想象中的对方。
等待阶段为什么不同步镜像——因为运行环境里每个 job 有独立守护进程,「拉下来预热」的缓存活不到下一个 job。一个活不过使用者的缓存,不是缓存。
主应用为什么全程不拉源码——它通过构建上下文直接引用子应用的镜像。于是主应用不需要子应用的源码、不需要它们的仓库权限、也不关心它们用什么构建工具。产物是唯一的接口。
五、我不用自己写验证
这是我最想说的一点,也是这套流程真正替我改变工作方式的地方。
我以前对校验的态度是「要么不做,要么做不彻底」。原因很现实:写一个永远会通过的检查,比不写更糟——它给你一种被保护了的错觉。
它交回来的东西是这样的:一份约 60 项检查的校验套件,外加 25 个「故意写坏」的输入——把关键步骤挪到错误位置、让镜像引用依赖分支名、让等待逻辑接受「未找到」为成功……然后要求校验必须把这些全部抓住。 一条抓不住的,就判定为失败。
它留下的原话是:
一个在它自己命名的 bug 上无法失败的检查,是装饰品。
这套机制不是一次成型的。校验断言的规模在每轮复核里被迫升级——从最初 30 项检查配 9 个负向对照,一路长到 60 多项配 25 个。而其中一半以上的增长,是复核环节发现「检查其实抓不到它声称要抓的东西」之后被迫补的。
让我印象最深的一条断言,原文是:
传了 ≠ 送到了。
背景是:有两个 job 需要在依赖声明里写上基础镜像 job。声明写是写了,但两条腿都静默地回退到了默认镜像,白白多花了几百秒去拉一个不该拉的镜像。参数存在不等于依赖成立——这条检查断言的是实际行为,不是声明文本。
我自己在别的场合也栽过一次同类跟头,所以对这句话有非常具体的体感:「我配了」和「它生效了」之间隔着一整个宇宙。
六、我不用重新讲背景
回到开头那 14 句话。为什么我敢只贴一个路径加一段日志?
因为上下文不在我脑子里,在它的工作区里。
它自己扫项目、自己写诊断文档、自己把结论沉淀成可复用的记录。六个月后我再回到这个仓库,不需要重新回忆「当时为什么把 base 镜像设计成只装工具」——那条判断已经作为一条「设计决策」写在文档里,并且标注了「用户确认,不得更改」。
这也是我认为这套流程最被低估的一部分:它不只是帮你把这件事做完,它把「为什么这么做」留下来了。
而且沉淀是有门槛的——不是所有东西都值得留。只有那些「下次遇到还会踩」的教训才会被写进长期记忆。这次留下的其中一条,恰好是关于我自己的:
报告「这个检查通过了」之前,必须先确认那个信号在失败时真的会变红。
七、它做错了什么:7 轮返工
如果这篇文章只写「它一次做对了」,那它就是广告。
真实情况是:这一件事跑了 7 轮,设计方案被推翻两次,其中一次还是我主导的推倒重来。9 次角色派发(需求侧 5 次、复核侧 4 次),从最早到最晚横跨将近四个半小时。
更值得写的是复核环节是怎么工作的——它独立复算每一个数字,并且在文档开头明确写着:下面每一条都是我自己跑的,不是从设计文档里抄的。
于是出现了这样的记录:
- 第 2 轮,复核直接推翻了设计文档的核心论断:
「设计文档 §9.1(c) 的论断被证伪。」
第 3 轮,它找到了断言方式本身的缺陷:
「断言的对象是 job 自己写的那段文字,而不是它交给 docker 的 argv。」
也就是说,检查在校验「它的自我描述」而不是「它的实际行为」。一个报告自己没问题的检查。
- 第 5 轮,我拍板换掉了整个方案:用 tag 取代内容哈希,同时砍掉依赖预填充,并当场接受了代价——每个版本都要冷构建一次,子应用多花约 48 秒。我当时的判断是:那 419 秒的收益与这个预填充无关,必须完整保留。
- 第 6 轮,复核给出 FAIL。但它同时说清了一件事:成品代码是对的,漏的是校验覆盖。它甚至复现出一个假成功——探针返回 500 时,流程把它当成了「已发布」。
还有一次是它自己的锅,它写在了提交信息里:
上一版只做了语法检查……这是我的回归。
这 7 轮不是失败的证据,恰恰是这套流程有效的证据。 因为那个最危险的东西——一个「看起来很绿但其实永远不会红」的检查——是被复核环节逐轮挖出来的,不是被运气避开的。
八、6.85 分钟是怎么来的
最后说结果,而且要说清它是怎么测的。

我调了流水线 API,把改造前后每一条成功流水线的时长逐条拉出来(列表接口不返回时长,只能逐条取)。改造前 9 条、改造后 5 条,取中位数:12.5 分钟 → 6.85 分钟。
三点必须说清楚:
- 主口径用中位数,不用平均值。 改造前那 9 条里有一次 5 分钟的离群值,它把旧方案的平均值拽低了。也就是说用平均值比较,反而对旧方案更有利。中位数对离群值不敏感,两侧才是同一把尺子。
- 样本很小。 9 条和 5 条,都不足以支撑任何统计结论。它的意义是「方向明确」,不是「精确到小数点」。
- 最后一层图里的明细,比总数更有价值。 把耗时拆开,你会发现主应用自己干的活不到 80 秒,而将近 79% 的时间在等——等 5 个子应用把镜像推上来。
这就引出一个不太舒服的结论:继续优化主应用的构建,收益已经接近零。 真正的杠杆在最慢的那个子应用手里。
瓶颈迁移了,而且换了性质。 迁移前瓶颈是本地计算,你能用缓存、并行、换机器;迁移后瓶颈是跨仓库的等待,你能用的工具只剩下协议设计和激励对齐——而后者不是技术问题。
九、我没有交出去的部分
「我只管给目标和确认」听起来很轻松,但「确认」这两个字里有实打实的判断。
上面第 5 轮那次推倒重来就是我做的。它当时给了几个方案,我需要判断的是:哪个代价我承受得起。 用 tag 取代哈希,代价是每个版本冷构建一次——我要评估的是,那个构建能不能在 CI 的硬超时里跑完。这是业务与基础设施的取舍,不是代码问题。
同样,第 6 轮我拍板用 HTTP 接口做镜像探测,也是我给的原话:
「用
/v2/<repo>/manifests/<tag>这个 HTTP 接口做探测;鉴权用 CI/CD 的DOCKER_USER/DOCKER_PASSWORD。」
「请求成功代表推送成功了。」
它负责把「能不能做到」算清楚,我负责决定「值不值得做」。 这条线我一次都没让它越过去。
所以准确的描述不是「它替我做完了」,而是:它把工作量吃掉了,把决策留给了我。 它甚至把这些决策逐条记录成「用户确认,不得更改」——包括我接受的代价。
十、想抄的话,怎么跟它说话
如果你也想试,这几条是我的实操体会:
1. 一次给一个坐标,不要一次给一堆。 我 14 句话里每次只谈一个仓。一次丢 6 个仓进去,它会给我一份均衡但浅的答卷;一个一个来,它每个都能挖到底。
2. 给它证据,不要给它结论。 我贴的是原始日志,不是「我猜是缓存问题」。结论一旦由我给出,它就只能顺着走;证据给它,它才可能找到我没想到的那一层。 那个 419 秒的重复,就是它从证据里挖出来的,不是我说出来的。
3. 给策略,不要给方案。 「先一个一个解下」「根据实际情况定向改」是我的原话。我管节奏和优先级,它管实现路径。
4. 纠错时补事实,不补判断。 我说的是「之前是把 agent 拆成了两个仓」,不是「你搞错了」。补事实让它自己重算,比直接下判断更能让它修正整个推理链。
5. 验收标准要写在它必须能自证的地方。 这套流程里最值钱的一条硬约束是:证明你的校验抓得住它要防的 bug——拿改坏之前的版本跑一遍,必须复现失败。 这条纪律不是我提的,但它是这套流程敢把验证交给它的前提。
6. 每次停下来问你的那个点,认真答。 它会在「这个代价你接不接受」这种地方停下来。这些时刻不是它在偷懒,而是它把最贵的那部分判断标了出来——跳过这些点,等于放弃了整套流程里唯一不可替代的环节。
回到标题那句话。
「用自然语言重写一套跨仓构建」听起来像个噱头,但它的实际含义比噱头朴素、也比噱头重要:
我做的事从「设计并实现一套构建体系」,变成了「描述目标、提供证据、在关键代价上拍板」。前两件事它做得比我快,第三件事它替我保住了——因为它把每一个需要拍板的代价都摊开写在了明处,而不是替我默默选一个。
这套系统最值得说的不是那个 45%。
而是:每个数字都被实际测过,每条限制都被写在明处,每次取舍都能追到是谁拍的板——包括那些并不好看的。