AI 写的 CI 流水线跑起来总不稳:缓存键、并发取消、失败重跑怎么定
数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。
多数人把「AI 写的流水线不稳」归给模型不懂 CI,这是归错了因。 模型对主流 CI 平台的语法掌握得相当好,骨架、矩阵、步骤顺序它写得又快又整齐。真正让流水线三天两头出问题的,是三处没法从代码里推出来的东西:缓存键该覆盖哪些输入、并发取消该按什么粒度分组、失败重跑的前提条件成不成立。这三处的答案取决于你的仓库结构、部署方式和团队约定,AI 看不到这些,只能按最常见的写法填一个默认值。默认值在小项目上恰好能跑,在你这儿就未必。
先说本篇和站内两篇相邻文章的分工,免得你翻错地方:如果流水线已经红了、你让 AI 去修但它改一轮好一轮坏,那是 CI 失败 AI 修不动 的场景;如果你关心的是 Agent 侧的缓存复用与重复执行怎么做幂等,看 Agent 缓存与幂等。本篇讲的是更前一步——流水线配置生成的那一刻,哪些该让 AI 写、哪些必须你自己定死,以及定完之后怎么验收。
一、先分因:把「不稳」拆成可判别的现象
流水线不稳是个模糊说法,直接去改配置多半白改。先花十分钟把现象归类。归类的依据只有一条:同样的输入,是不是同样的输出。
- 输入相同、结果相同,但每次都慢 —— 缓存问题。
- 输入相同、结果时对时错 —— 并发或环境干扰。
- 输入相同、重跑一次就好 —— 三种可能:外部基础设施抖动(网络、镜像源)、重跑掩盖了真实缺陷、任务本身不幂等。看失败发生在哪个阶段就能分开,第四节展开。
拿到一次失败,先做三件事:找到这次构建对应的提交号、找到同一提交上一次跑的结果、找到这两次之间流水线配置有没有改过。
# 确认当前构建对应的提交,以及这个提交上还有没有别的分支引用
git rev-parse HEAD
git log --oneline -5
git branch -a --contains HEAD
# 确认流水线配置文件最近的改动,很多"突然不稳"就是这里改过
git log --oneline -10 -- .github .gitlab-ci.yml ci/
下面这张表是按现象倒推成因用的。真实排查里我几乎每次都从这张表往下走。
| 现象 | 大概率成因 | 怎么验证 | 处置动作 |
|---|---|---|---|
| 每次构建都在完整安装依赖,日志里从没出现命中缓存 | 缓存键里混进了每次都变的量(提交号、构建序号、时间戳) | 连续两次跑同一个提交,把键的实际取值打印出来对比 | 把键改成「固定前缀 + 平台标识 + 锁文件摘要」,去掉一切易变量 |
| 缓存命中了,但装出来的依赖版本不对,或产物是旧的 | 键太粗,锁文件已经变了但键没变 | 手动改一行锁文件重跑,看键值是否随之变化 | 让锁文件摘要成为键的一部分,而不是只作为回退前缀 |
| 恢复缓存比重新安装还慢 | 缓存了海量小文件目录或整个工作区 | 打印缓存包大小与恢复步骤耗时 | 只缓存包管理器的下载目录,不缓存已安装树 |
| 同分支连推两次,两个任务同时在跑,机器被占满 | 并发分组表达式没带上分支维度,或压根没开取消 | 连推两个空提交,观察队列里是否并存 | 分组键 = 工作流标识 + 分支引用;开启取消进行中的任务 |
| 部署跑到一半被新提交取消,环境停在中间态 | 对部署类任务也开了自动取消 | 看被取消任务的最后一条日志停在哪一步 | 部署类任务禁止取消,改成排队串行执行 |
| 重跑一次就绿,重跑两次又红 | 用例之间共享状态、抢端口,或依赖当前时间 | 本地随机化用例顺序、单独跑失败用例 | 先定位再重跑,别把重跑当修复手段 |
| 重跑之后下游出现重复数据或重复发布 | 任务有外部副作用且不幂等 | 查下游是否存在两条内容相同的记录 | 给外部写操作加以内容为准的幂等标识 |
分完因再动手。跳过这一步直接让 AI「优化一下流水线」,它会把三处一起改,你连是哪一处生效了都分不清。
二、缓存键:AI 能写骨架,边界必须你定
缓存这件事,AI 生成的配置几乎总能跑通,问题在于它给的键往往要么太宽要么太窄。
太宽是指键里只有一个固定字符串,锁文件改了键也不变,于是新依赖装不进来,或者装了一半和旧缓存混在一起。这类问题最阴险:构建是绿的,产物是错的。太窄是指键里带了提交号或构建序号,每次都是新键,缓存永远命中不了,你只是白白多存了一堆数据。
正确的做法是先想清楚一个问题:这份缓存的内容,由哪些文件唯一决定? 依赖缓存由锁文件决定,编译缓存由源码和工具链版本共同决定,容器层缓存由基础镜像和构建上下文决定。把这些决定因素的摘要拼进键,把不决定内容的东西全部踢出去。
键的结构我一般写成三段:
- 用途前缀(比如
deps、build),方便你按前缀清理。 - 运行环境标识(操作系统、架构、语言主版本),跨平台的缓存不能互相污染。
- 决定内容的文件摘要。
摘要怎么算不重要,重要的是跨机器可复现。你可以在本地先验证一遍:
# 用锁文件内容算摘要,确认同样内容在不同机器上得到同样的值
sha256sum package-lock.json | cut -c1-16
# 多个文件一起决定缓存时,先排序再一起算,避免文件顺序影响结果
find . -name "requirements*.txt" -not -path "./.git/*" | sort | xargs sha256sum | sha256sum
除了主键,通常还要配一组回退前缀:主键没命中时,退而求其次用旧缓存打底,再增量安装。回退前缀的写法是把摘要那一段去掉,只留前两段。这一层 AI 常常漏掉,你补上去能省掉大部分冷启动时间。
验收动作(缺一不可):
- 连跑两次同一提交,第二次日志里必须出现命中,且安装步骤耗时明显下降。
- 改一行锁文件再跑,第一次必须未命中并重新落缓存,第二次必须命中新键。
- 换一个操作系统或架构的任务再跑,不能命中另一平台的缓存。
三条都过了,缓存这块才算定住。构建产物层面还有一类缓存不生效的情况(工具自身的增量判断被破坏),排查路径不同,见 构建缓存不生效。
三、并发取消:粒度是业务决定的,不是语法问题
并发取消的作用是省机器:同一个分支上你连推三次,前两次的构建结果没人看,取消掉就好。AI 默认给的分组表达式通常是「工作流名 + 分支引用」,对纯测试流水线够用。
麻烦出在这条规则不能一刀切。我把任务分成三类,取消策略完全不同:
- 只读任务(跑测试、跑静态检查、构建产物但不发布):可以放心取消,取消了没有副作用。
- 有外部副作用但可重入的任务(推镜像到仓库、上传制品):取消可能留下半截制品,取决于上传是不是原子的。倾向于不取消,或者取消后有清理步骤。
- 部署与发布任务:绝对不能自动取消。取消一个正在滚动更新的部署,你得到的是一半新版本一半旧版本,而且流水线显示的是「已取消」,没人会去看。
所以并发分组的正确写法不是一个表达式,而是按任务类型分成不同的组。测试组按分支分,取消进行中的;部署组按环境分(比如按目标环境名),不取消,排队执行。同一环境的部署天然应该串行,这也顺手解决了两个部署互相覆盖的问题。
合并请求和分支推送还要分开考虑。同一次推送在多数平台上会触发两条流水线:一条针对分支本身,一条针对开着的合并请求。如果两条的分组键都只用分支名,它们会互相取消,表现为其中一条永远停在「已取消」,你以为跑过了、其实只跑了另一条。不同平台给出的分支引用形式不一样,有的两条天然不同、有的完全相同,所以不要凭印象假定,先在自己的平台上把分组键的实际取值打印出来看一眼。要避开的话,分组键里再加上事件类型或工作流标识。
验收动作:连推两个空提交,观察队列。测试任务应该只剩最新那个在跑,部署任务应该排队而不是并行也不是被杀。
# 造两次连续推送用来验证取消行为
# 务必在一条一次性验证分支上做,不要推到会触发真实部署的分支
git switch -c ci-verify-concurrency
git commit --allow-empty -m "ci: verify concurrency group"
git push -u origin ci-verify-concurrency
git commit --allow-empty -m "ci: verify concurrency group 2"
git push
# 验证完删掉本地与远端的验证分支
git switch -
git branch -D ci-verify-concurrency
git push origin --delete ci-verify-concurrency
部署任务的排队行为不好在验证分支上观察(验证分支通常不触发部署),可以退一步只看配置:确认部署任务的分组键与测试任务不同,且没有开启「取消进行中」。
四、失败重跑:重跑不是修复,前提是幂等
「点一下重跑就好了」是流水线腐化最快的路径。重跑本身没有错,错的是把重跑当成常态。
先分清两种失败。基础设施类失败——拉取依赖时网络超时(ETIMEDOUT)、镜像源返回 500、被限流返回 429、连接被重置(ECONNRESET)——这类失败与你的代码无关,重跑是合理处置,甚至应该自动重试。代码或用例类失败——断言不通过、编译错误、时序竞争——重跑只是掷骰子,掷到绿了你就把一个真实缺陷放进了主干。
区分这两类的判据很直接:失败发生在哪个阶段。依赖安装、镜像拉取、制品上传阶段的失败大概率属于前者;进入测试和构建阶段之后的失败大概率属于后者。所以自动重试要限定到具体步骤,而不是整条流水线加一个「失败重试三次」。整条重试的代价是把偶发失败的测试永久藏起来,等它在生产环境爆出来,参考 测试绿了线上还是炸。
重跑的第二个前提是幂等。只要任务对外部世界有写操作——推镜像、发通知、写数据库、调第三方接口——重跑就有产生重复的风险。幂等的做法是给每次操作一个由内容决定的标识,而不是由时间或随机数决定:用提交号或产物摘要做版本标签,下游看到相同标识就跳过。这套思路和分布式任务里的处理是一致的,展开见 重试与幂等。
AI 在这里的能力边界很清楚:它可以帮你把重试逻辑写成配置、把幂等标识拼出来、把清理步骤补齐;但哪些步骤允许自动重试、幂等标识用什么、下游怎么判重,得你给它。你不给,它就按最通用的写法给你加一个全局重试,看起来很省事,实际是埋雷。
五、什么时候别再折腾了
三处都调过一轮还是不稳,继续调的收益会掉得很快。给几个我自己用的止损点:
同一处配置改了三轮以上还没稳定,停下来。 通常说明你排查的方向错了——比如一直在调缓存键,真正的原因是构建机的基础镜像换了版本,导致工具链行为不一致。这时候要做的是对比两次运行的环境信息,而不是继续改键。相关的判别方法见 运行环境不一致。
改动开始影响正确性,立刻回滚。 为了让缓存命中率高一点,把锁文件摘要从键里去掉;为了省机器,给部署任务开了取消——这类改动是拿正确性换速度,不值。回滚点很好找:流水线配置文件应该和代码一起提交,git revert 那一次提交就能回到已知可用的状态。回滚之后再重新设计,不要在错的基础上继续叠补丁。
排查耗时超过它节省的时间,换条路。 缓存优化的收益上限就是安装依赖的那点时间。如果你为了省几分钟已经花了两天,直接放弃缓存、每次全量安装,是完全体面的选择。稳定的慢流水线,比不稳定的快流水线值钱得多。
只在 CI 上出现、本地复现不了的问题,别在 CI 里硬试。 在流水线里靠推提交做二分排查,每次反馈都要等一轮构建,效率极低。把 CI 的运行环境在本地用容器复现一份,在本地二分,定位到了再改回去。做不到本地复现时,先加日志把关键状态打出来,等下一次自然失败时拿数据,不要为了复现盲改配置。
最后一个止损信号:AI 开始反复给你同一个方案的变体。 你换个说法问,它换个写法答,本质没变。这说明它已经把它知道的都给你了,剩下的差距在你的项目特有约定上。此时正确的动作是把仓库结构、部署方式、任务分类整理清楚再问,而不是继续追问。
六、避坑清单
把 AI 生成的配置直接合进主干,不看键的实际取值。 为什么会踩:生成的配置语法正确、结构漂亮,看起来没有可疑的地方,而缓存键是不是有效,从静态阅读根本看不出来。 怎么避:合并前先在一个临时分支上跑两次,看第二次是否命中。命中判断以日志为准,不以配置写法为准。
用提交号或构建序号做缓存键的一部分。 为什么会踩:AI 需要一个「每次不同」的值来保证正确性,提交号是它最容易想到的。逻辑上没错,代价是缓存永不命中。 怎么避:键里只允许出现「决定缓存内容」的量。写完之后逐个问自己:这个量变了,缓存内容会不会变?答案是否,就删掉。
对所有任务用同一套并发取消规则。 为什么会踩:并发配置通常写在工作流顶层,一处生效全局,改成分任务需要多写几行。 怎么避:按「只读 / 有副作用 / 部署」分三档,部署档单独一组且不取消。多写的那几行,能省掉一次半截部署的事故。
给整条流水线加全局失败重试。 为什么会踩:它确实让红色的构建变少了,短期看指标很好看。 怎么避:重试只加在网络相关的具体步骤上,并且把重试次数打进日志。团队要能一眼看出「这次绿是第几次才绿的」。
缓存目录选成了已安装的依赖树。 为什么会踩:缓存安装结果看起来比缓存下载包更省事,一步到位。 怎么避:已安装树包含大量小文件和平台相关的软链接,打包解包的开销经常超过重新安装,而且跨平台恢复容易出错。缓存包管理器的下载目录更安全。
把密钥或凭据带进缓存和日志。 为什么会踩:为了排查方便,顺手把环境变量整个打印出来;或者缓存目录里正好含有登录后写入的凭据文件。 怎么避:打印环境变量时白名单式地只打你要看的几个;缓存路径里排除凭据文件所在目录。这条一旦踩了,代价是轮换所有相关密钥。
在流水线里用当前时间做判断。 为什么会踩:本地写脚本习惯了用日期做标签或做分支判断。 怎么避:CI 机器的时区未必和你一致,跨零点会得到不同结果,重跑同一提交也会得到不同产物。时间只用于日志,不用于逻辑分支。
假定海外托管的 AI 工具或模型可以在国内直连使用。 为什么会踩:文章和示例里看到的用法都默认能连上,配置抄过来就用。 怎么避:几家海外厂商官方对中国大陆有区域限制、不支持直连,流水线里直接调用会以连接失败或拒绝服务的形式出现,且失败点在网络层,日志往往只有超时。市面上存在第三方中转,是否使用需要你自己评估合规与稳定性,这里不做推荐。在流水线里依赖任何外部 AI 服务之前,先想清楚它挂掉时构建应该失败还是跳过。
收束
这三处配置的共同点是:语法归 AI,语义归你。缓存键的语法 AI 写得比你快,但「什么决定了缓存内容」是你的项目知识;并发分组的表达式 AI 一行就给,但「哪些任务不能被取消」是你的部署常识;重试配置 AI 随手加上,但「哪些失败允许重试」是你对系统的判断。把这三条语义先想清楚再让它写,一次就能定住;反过来让它猜,你会在后面几个月里反复调。
合并流水线改动之前,过一遍这份自检:
- 同一提交连跑两次,第二次缓存命中,安装耗时明显下降。
- 改一行锁文件,键值随之变化,缓存重新落盘。
- 换平台的任务不会命中另一平台的缓存。
- 连推两次,测试任务只剩最新一个在跑。
- 部署任务永远不会被自动取消,同一环境串行执行。
- 自动重试只挂在网络相关步骤上,且重试次数进了日志。
- 所有对外写操作都有由内容决定的幂等标识。
- 日志和缓存里没有任何凭据。
- 流水线配置和代码在同一次提交里,随时可以整体回滚。
九条里有一条过不了,就先别合。流水线是团队每天都要看的东西,它一旦让人开始习惯性点重跑,后面所有质量信号都会跟着失效。