为什么 AI 项目会失败?

它们失败的原因,和每一个软件项目一直失败的原因完全相同,只有两个。AI 并没有引入新的失败模式 — 它只是改变了旧失败模式的速率常数,而且是以最糟糕的组合方式。

要回答 AI 项目为什么失败,我们首先得回答任何项目为什么会失败。有一个根本性的答案,它由两部分组成。两部分都无关工具、方法论或人才。两者都是结构性的,而且在模型写下第一行代码很久之前就已经成立了。

机制一:每一次交接都是有损信道

一个项目是一连串的传递。意图变成需求。需求变成设计。设计变成 code。code 变成一个部署的系统。每一步都是人、产物和工具之间的一次传递 — 而每一次传递都会损失保真度。

这不是一个关于混乱的比喻。这是一个有噪信道的寻常行为。如果每个阶段以概率 p 如实传递,那么跨 n 个阶段的保真度是 pⁿ。误差不叠加。它们是乘性复合的。

五个阶段各 90% 的保真度,最终只剩 59%。不是因为有人粗心 — 而是因为在没有校正的情况下,串行传递就是会这样。

它的经验版本比大多数在职程序员都老。Lehman 的软件演化第二定律,1974 年提出,此后被反复证实:随着系统演化,其复杂性会增加,除非有工作专门去降低它。关键的是最后那个从句。退化是默认的。秩序才是需要付出代价的东西。

信息论也告诉了我们补救的办法,并且精确地命名了它:纠错码。你不是靠"更小心地传递"来修复一个有噪信道的。你是靠在每个阶段添加冗余和检查来修复它。在软件里,这些检查就是关卡 — types、测试、review、CI、分阶段上线。而编码理论对那个关键性质说得很明确:校正必须在每一个阶段发生。只在最后做一次检查,无法挽回已经在四个未校正的传递里复合起来的误差。

这就是关卡的全部理由,也是为什么一个只在发布时做 review 的项目,无论那次 review 多好,都会失败。

机制二:复杂性活在边上

第二个机制根本不是关于误差的。它是关于一个误差一旦存在,会代价多大。

取一个有 n 个组件的系统。如果任意组件都可以与任意其他组件交互,可能的接口数是 n(n−1)/2 — 二次的。加一个组件,就加了 n 个新的潜在交互。

architecture 就是防止这件事的纪律。它的工作是限定每个组件的度数:这个模块只和那三个说话,不和其他任何一个。这样交互数就是 O(n) 而不是 O(n²),而 — 这才是关键的部分 — 任何改动的爆炸半径,都被它所在的组件及其声明的邻居所限定。

这才是好 architecture 的真正定义。不是分层,不是图,不是模式。有界的爆炸半径。一个你能把后果枚举出来的改动。

没有它,每一次改动都可能是全局的。你无法对一个修改做推理,因为你无法枚举它触碰到的东西。系统变成了一个小的改动产生不成比例且不可预测效果的系统 — 不是因为有什么神秘的东西,而是因为交互图很稠密,而没有人能把它整个装在脑子里。

两个机制是相乘的

这是通常没人说出来的部分,而这才是全部要点:

damage  ≈  (errors introduced)  ×  (blast radius per error)

关卡攻击第一项。architecture 攻击第二项。单独任何一个都不够。

对一个乱麻做完美的 review 仍然是混乱:你的误差很少,但每一个都传得到处都是。有着美丽 architecture 却满是未 review 误差的项目仍然失败:每个误差都被圈住了,但太多了。项目靠把两个项都压住而存活,而在放弃任何一个时失败。

这也解释了一个常见、否则很令人困惑的观察:一个项目可以看起来很健康很长时间,然后迅速崩塌。这两项独立运动,而乘积才是要你命的东西。一个团队可以败坏自己的 architecture 一整年,同时靠强 review 把误差数压低 — 然后失去两个资深 reviewer,跌下悬崖。

一个健康的项目长什么样

每个阶段都做校正。不是只在最后设一道关卡。需求对照意图 review,设计对照需求 review,code 对照设计 review,部署对照测试。每个阶段抓住自己那一类误差,因为每个阶段引入的误差类别不同 — 也因为第二阶段未校正的误差,会被下游的一切所放大。

把 architecture 当作耦合控制。显式的、被强制的边界。声明自己依赖什么的模块。窄到可以枚举的接口。成功的标准不是优雅;而是你能回答"这个改动影响什么?"而不用读整个系统。

两件事都做,误差仍然会进来 — 它们总是会 — 但它们以受控的速率进来,并且待在它们落下的地方。

一个正在失败的项目长什么样

没有有效的关卡。需求阶段引入的误差活过设计、活过实现,在生产里被发现,在那里修复成本高几个数量级,而且裹着建在它们之上的所有东西的后果一起到来。

没有 architecture。耦合无人管理地增长。每一次改动都变成一次集成事件。估算变得没有意义,因为一个改动的爆炸半径在你做它之前是未知的。团队发展出遗留代码库那种典型的习得性无助:没人知道什么会坏,所以没有仪式就不碰任何东西。

终态是一个其复杂性超过任何人推理能力的系统。注意在那一刻真正失败的到底是什么:不是 code。是规约。再也没有人能说出什么是正确行为,这意味着没人能判断一个改动对不对。

现在加上 AI

AI 没有创造第三个机制。它改变了速率常数 — 而且是以正好最糟糕的组合方式。

它抬高了误差数,主要通过体量。按行算,在一个定义良好的窄任务上,一个好模型和一个胜任的工程师是有竞争力的。那不是问题。问题是,十倍的 code 在相同速率下包含十倍的缺陷,而另一端的 review 能力没有增长。

它改变了误差分布,这是被低估的那一半。人类的误差倾向于大声地失败:code 编译不过,明显的情况崩溃,reviewer 看到奇怪的东西。AI 的误差能 compile、读起来很地道、遵循局部约定、能过一遍浏览。它们是貌似合理的。为抓住人类失败模式而设计的关卡,与它们匹配得很差。

我要讲一个具体例子,来自我仔细评估过的一个系统。它有一个静态检查,整个工作就是在模块之间强制执行声明的边界 — 哪个模块被允许依赖哪个。这个检查比较了关系里错的那一边。它本该问的是依赖方模块是否持有许可,它问的却是被依赖方那一个。

后果是彻底的:它接受了每一个被禁止的依赖,并拒绝了几个合法的。

它之所以存活,是因为它看起来是工作的。它吐出格式良好、听起来合理的错误 — 数量够多、够具体,以至于一个 reviewer 会断定这个关卡是有效的。只有写一个故意的违规、看着它 compile 过,失败才变得可见。下游的一切都继承了一个并不存在的保证。

一个不关门的关卡比没有关卡更糟,因为它把未经验证的工作变成了看起来已验证的工作。这是 AI 时代特征性的缺陷,而且它不是一个编码错误。它是一个验证错误。

它做局部优化而没有全局视图。这是最危险的性质。一个 agent 追逐它面前的目标。它不持有三周前在另一个子系统里确立的 architecture 原则。它会让编译器满意、让测试通过、把 ticket 关掉 — 而如果通向那里最便宜的路径增加了耦合或退化了一个全局性质,它就走那条路径,因为它的目标里没有任何东西惩罚它。

具体地,来自我测量过的一个对性能敏感的系统:一次改动引入了一个优化,跳过系统证明自己不需要的工作。优化是对的。但它还在每个请求上跑它的启动成本 — 包括那些没什么可跳过的请求,它们付了成本却没收到任何好处。

那个系统自己的性能套件里最便宜的操作变慢了 大约六倍,把一个之前领先的结果变成了全套里最差的。然后它在之后几十次提交里都存活了下来,因为每一个在场的关卡都仍然通过。没有任何分配。没有新的外部效果被执行。唯一退步的东西是时间 — 而没有任何东西测量时间。

那就是针对一个全局约束的贪心优化,而构造上,这就是一个无人看管的 agent 会做的事。注意人类 reviewer 很可能也抓不住它:改动孤立地看是对的,而且它在孤立地看时确实是对的。缺陷只存在于改动与工作负载之间的关系中。

为什么更多算力救不了它

直觉是:既然 AI 造成了这个烂摊子,更多的 AI 能清理它。算术不同意。

每次改动的生成成本大致是常数。产出一个改动的成本,在系统有十个模块还是一万个模块时大致相同。

验证成本随耦合增长。要知道一个改动是对的,你必须考虑它和什么交互。在一个 architecture 良好的系统里那是一个有界集合。在一个密集耦合的系统里那是每次改动 O(n),而在项目生命周期里累积是 O(n²)

所以生成线性增长,而验证二次增长,差距随每一次提交拉大。越过某个阈值之后,你产出改动的速度快于任何算力能确认它们是对的 — 而正确性不是一个你能采样得到的东西,因为失败就在你没有枚举的那些交互里。

在那道墙后面还有一道更硬的墙。当复杂性超过任何人说明系统应该做什么的能力时,更多的生成产生更多不可验证的 code。你无法从一个未规约的系统里测出来,因为测试编码的就是一份规约,而你不再有规约。在那一刻,算力不仅仅是无效 — 它在主动地加速崩塌。

什么才真的有效

处方直接由此而来,而且不是"更仔细地 review"。人类 review 不随生成体量增长;那是算术,不是态度。

  • 关卡必须是机器执行的。如果一个检查需要人去读 code,它就不是关卡 — 它是一个愿望。types、contracts、capabilities 声明、依赖规则、property tests、benchmarks-as-gates。任何一台机器能在每次提交时不经询问就强制执行的东西。
  • architecture 必须是机器强制的。一张图不是 architecture;它是关于 architecture 的一个声明。architecture 是一条构建拒绝越过的边界。如果你的模块依赖关系活在一个 wiki 页面里,它们已经错了,只是没人知道。如果它们活在一个编译器会检查的清单里,它们就不会悄悄漂移 — 而一个局部优化的 agent 不能增加耦合,因为构建会拦住它。
  • 守卫没有别的东西测量的那些性质。每个项目都用"它能 compile 吗"和"测试过吗"做关卡。几乎没有项目用"延迟退步了吗"、"依赖图多了一条边吗"、"二进制大小跳了吗"、"这个模块获得了一个它以前没有的 capabilities 吗"做关卡。那些正是局部优化会退化的性质,也正是会静默失败的那些。
  • 并且验证那些验证者。什么也不强制的关卡,是这整个模型里代价最高的缺陷,因为它在损害方程里让一项失效,却显得像是把它压住了。像测你的 code 一样测你的关卡:写出违规,确认构建拒绝它。如果你从未见过你的 architecture 检查失败,你不知道它有效。

令人不舒服的结论

这些没有一件是新的。Brooks 和 Parnas 和 Lehman 几十年前就描述过这些机制,而回答它们的纪律 — 在每个阶段设关卡、限制耦合 — 从那以后就为人所知,只是执行得不均匀。

AI 改变的是不做这件事的余地。一个手写 code 的团队,生成误差的速度慢到平庸的关卡大体上还撑得住,而耦合增长的速度慢到 architecture 的疏忽要花好几年才致命。这两样都是余裕,而余裕来自人类的慢。它没了。

所以在 AI 时代失败的项目,会以完全传统的理由失败,在压缩的时间线上,并且 — 这才是该让你担心的部分 — 拿着比失败项目有史以来更好看的中间产物。code 会读起来很好。测试会过。关卡会报绿。

直到有人去测量。

我们为什么构建 claim layer。上面的处方 — 机器执行的关卡、机器强制的 architecture、守卫没有别的东西测量的性质 — 就是 SuperJ 的 claim layer 的设计规约。capabilities 是一个机器执行的关卡("这个包不能做 net"),architecture 是机器强制的分层("这个包不能 import sj.http"),noalloc 守卫没有别的东西测量的性质("这条热路径不能分配"),而 superj capabilities --test 就是验证验证者那一步。四条处方,各一个机制。Written by AI. Read by human.

← 所有文章 尝试 SuperJ →