今年我把几乎整条流水线都自动化了,谁劝我改回去我跟谁急。一个智能体写大部分改动的初稿,第二个智能体审这份 diff——它比我周五下午五点认真得多,审到第四百份也不会走神。测试自己跑,类型检查、lint、预发部署、对着一个接近真实的数据库做冒烟检查,全自动。等一个改动走到我面前时,已经有五个独立的自动化步骤看过它、点了头。它确实比我三年前那套手摇版本更好,而打字从来不是我会怀念的那部分。
所以某个迭代里,我做了那件看起来顺理成章的事。我盯着一个带着四个绿色勾、审查意见也干净利落的合并请求,心想:我的手为什么还按在合并键上?我自己点这一下,到底加了什么四个自动化的"是"还没挣到的东西?于是我把最后一步也交了出去。生成、审查、测试、预发、合并,闭环。大约九天里,它感觉像未来。
然后其中一次合并,在凌晨两点把一个付了钱的客户锁在了他自己的账号外面。我用一通电话、在生产环境里、付出了不小的代价,才搞明白这条流水线里哪一步我绝不该交出去。它不是大多数人会猜的那一步,而是几乎所有人最先自动化的那一步。
那份 diff 很干净。问题恰恰就出在这。
那是个写路径。它在真正把数据落库之前,就先确认了请求——把"收到了"回给了客户端。如果你干这行有些年头,胃这会儿应该已经沉下去了,因为你认得这个 bug。它干净、地道、有类型、有测试,读起来完美无缺,直到某次重试撞在错误的瞬间:确认发出去了,保存没落地,系统于是对一件从未发生过的事确信不疑。
写它的智能体写了它。审查的智能体读了它、批准了它——我得替审查智能体说句公道话,它很能干:那个迭代里它抓到了两个我可能会漏掉的真问题。它只是没抓到这一个,因为这一个看起来不像 bug,看起来像完成品。测试是绿的,因为测试测的是顺利路径,而这条代码在顺利路径上恰好是对的。预发没问题,因为没人在预发里对着错误的毫秒重试过请求。五个自动化的"是",每一个在技术上都是诚实的,都在看那份 diff,没有一个在看凌晨两点。
它自动合并了。它自动部署了。在一个普通的糟糕夜晚,一次重试撞了上来,确认发出去了,写入没发生,一个真金白银付我们钱的真人被锁在了自己的账号外面,日志里没有任何东西能证明他来过。那通电话我到现在还能感觉到——不是因为 bug 有多离奇,而是因为在那条漂亮的闭环里,从头到尾没有任何一个时刻,是那个会接到电话的人看过这东西、并且认领了把它发出去这件事。
两件事都叫"审查",但它们不是一回事
一个被锁在门外的客户教会我的区分,就是这篇文章的全部。
我们用"审查"这一个词,指代两件完全不同的事。
第一件事是判断代码:它对不对、地不地道、有没有处理边界情况、命名是否合理。这是一项技能,是拿海量既有代码做模式匹配,加上对系统的心理模型。而这恰恰是今年变便宜的那类技能。一个好的审查智能体做这件事不知疲倦、始终如一,凌晨三点、第四百份 diff 也一样,不会像人类资深工程师那样因为疲劳而在周五下午的 PR 上挥手放行。这件事你绝对应该自动化。我做了,这是对的。
第二件事是认领这次合并:接受它现在要面向真实用户了,如果它错了,算我的。这不是技能,没有模式可匹配。它是一个具体的、可问责的主体,把自己的名字押在一个不可逆的、对外的决定上,并在事情变糟时吞下后果。
二十年来这两件事是焊在一起的,因为审代码的人就是会被半夜叫醒的人,所以我们从来不必注意到它们是两回事。自动化把它们撕开了。机器现在能把第一件事做得比我好,第二件事它根本做不了——不是因为它不够聪明,而是因为第二件事跟智力无关。它关乎问责,而问责需要一个能真正承担后果的东西。
你可以自动化判断。你无法自动化问责。
一个合并了坏 diff、搞挂了生产的智能体,没法被半夜叫醒,没法感到抱歉,没法把凌晨两点那通电话的重量带进它下一次决策里,因为它做的不是它有利害关系的决定——它只是在以无人能及的量级产出自信的输出,无论那份 diff 是杰作还是定时炸弹,都用同样平稳的语调签字放行。无限的吞吐,零问责。合并本该是整条流水线里责任停下的地方,而我把它交给了唯一一个没有责任可停的参与者。
我们守错了那个按钮
接下来这部分才该让你不安,因为一旦你看见了,你到处都能看见。
看看团队本能地保留人工的是什么,又乐于自动化的是什么。
他们把人工留在代码审查上。"每一份 diff 都得有人读。"他们给合并请求加上强制的人工批准,他们争论审查覆盖率,他们因为有人眼看过那些代码行而感到安全。然后他们给绿色通过配上自动合并、给生产配上持续部署,还觉得自己挺聪明,因为那部分不过是管道。
这恰恰是反的。
代码审查是可逆的那一步。如果人在审查里漏了什么,什么都还没发生。你可以再审一遍。你可以评论。你可以在预发里抓住它。什么都还没跨进真实世界。它是整条流水线里最可挽回的一步。
热门跟贴