为什么生产力指标会惩罚你最出色的工程工作
提交次数和增加的代码行数奖励的是工作量,而非进展。本文将解释为什么删除代码会吃亏,以及在 AI 编写大部分代码的当下,我们应该衡量什么。
大多数软件生产力指标衡量的是错误的东西,而且在机器开始编写代码之前,它们就已经是错误的了。提交次数、增加的代码行数、关闭的工单数以及总代码量,这些都奖励创造更多的东西。但最好的工程工作通常是创造更少的东西。你删掉一个无用的模块,你用一个清晰的服务替换三个纠缠不清的服务,你从根源上修复一个 bug 而不是在其周围打补丁。在体量指标上,这些胜利中的每一个都被记为负数或零。当你为产出付薪时,你得到的就是产出,而产出并不等同于进步。这篇文章是关于哪些指标会颠覆好的工作,为什么 AI 编码会使问题更加尖锐,以及我们应该转而衡量什么。
奖励错误行为的指标
从最常见的仪表盘说起。增加的代码行数、提交次数、燃尽的故事点、每周关闭的工单数、合并的拉取请求。这些指标中的每一项看起来都像是价值的代表,但每一项都在悄悄地奖励着与你期望相反的行为。
设想一个真实的工作场景。一位工程师花了四天时间阅读一个再没人能看懂的子系统,然后删除了六百行代码,并用八十行代码取而代之。现在,这个系统能完成同样的工作,故障模式更少,并且对于下一个接手的人来说是可读的。在仪表盘上,这表现为负五百二十行代码、一次提交,以及一周内没有关闭任何功能工单。按照所有数量指标来看,那位工程师度过了糟糕的一周。而实际上,这是团队中任何人在那个季度里最有价值的一周之一。
现在,再设想另一位工程师,他接了同样的工单,并在现有的一堆东西上又加装了一个新服务。增加了两千行代码,进行了十四次提交,多了一个新的配置文件和一个新的部署目标。仪表盘上一片绿灯。系统现在变得更难理解,并且有更多可能出问题的地方,但这些在数字上都看不出来,而奖励结构刚刚告诉了所有人,这两种方法中哪一种会得到关注。
为什么删除和简化总是吃亏
删除是指标反向作用最明显的例子。删除代码通常是你能对一个系统做的影响最大的事情,因为你删除的每一行代码,都将不再可能产生 bug、拖慢构建、迷惑读者或需要维护。一个在保持其功能的同时不断缩减的代码库,正在变得更健康。然而,没有一个标准指标会将删除计为一项积极贡献。它最多也就是忽略删除操作。通常,它给删除操作打的分数比什么都不做还要低。
修复根本原因同样会吃亏。一个得到真正解决的疑难 bug,在经过两天的调查后,可能只会产生三行代码的变更。而一个掩盖症状的浅层补丁可能会产生五十行代码和三次提交,并在以后再次出问题时生成更多的工单。指标偏爱那个补丁。它也偏爱未来的工单,因为那会是下个月又一个关闭的项目。
说“不”同样会吃亏。一位工程师说服团队放弃开发一个本会成为维护负担的功能,这是做了真正有价值的工作。他们避免了成本。但被避免的成本是看不见的。你没有写的那两千行代码,你没有部署的那个服务,你永远不会收到的那个待命呼叫,都没有对应的条目。指标只能看到存在的东西,所以构建了不必要东西的人,得分会比阻止它的人更高。
这三者背后的模式是相同的。体量指标只能衡量加法,而许多最优秀的工程工作是做减法。
古德哈特定律如何造成其余的损害
有一条法则可以解释为什么情况会随着时间的推移而变得更糟,而不仅仅是保持不准确。当一个衡量标准成为一个目标时,它就不再是一个好的衡量标准。人不是被动的。当一个数字决定了你的评审、你的奖金或你的地位时,你就会开始朝着这个数字努力,而这个数字也就不再能描述现实。
commit-count 目标会教人将一个变更拆分成八个提交。lines-added 文化会教人通过编写更多代码来解决问题,因为奖励在于数量,而不在于清晰度。tickets-closed 目标会教人将工作切分成许多小工单,并避免那些会拉低他们周度计数的、有深度的、单一工单的问题。这些都不需要任何人不诚实。它只需要他们对激励措施做出反应,而每个人都会这么做。
因此,一个数量指标不仅仅是无法捕捉到好的工作。它会主动地训练团队去产出那种得分高的工作,也就是那种注水的、过度构建的、易于计数的工作。这个指标劣化了它本应观察的东西。你最终会得到一个为仪表盘而优化的代码库。
为什么 AI 编码会彻底颠覆代码量指标
当人类手写每一行代码时,以上所述皆为事实。AI 编码助手沿用了这些本就有问题的指标,并剥离了它们仅存的一点意义。
原因是 AI 让代码量的成本几乎为零。一个模型可以在一分钟内生成五百行代码。它可以在午饭前完成十个提交所包含的变更量。如果你的指标奖励增加的代码行数或提交次数,那么现在一个助手就可以自行满足该指标,而坐在它旁边的人类只需稍加思索,就能产出三年前看起来堪称英雄般的数据。产出已经与付出的努力脱钩,并且进一步与价值脱钩。
机器不会做的是决定应该创造什么。在 AI 辅助工程中,稀缺的工作是判断力。正确地框定问题。决定不构建什么。批判性地阅读生成的代码,并丢弃其中错误、不安全或过于臃肿的部分。将模型过度生成的解决方案删减至实际适合系统的版本。这些任务决定了 AI 是让代码库变得更好还是仅仅是更大,而它们没有一项能体现在代码量上。如果说有什么影响的话,那就是做好这些工作反而会降低你的代码行数和提交次数,因为对生成代码的良好审查主要意味着拒绝和精简。
因此,在 AI 时代,代码量指标不仅仅是继续保持其错误性。它们现在奖励那些已变得廉价的东西,而惩罚那些已变得稀缺的东西。一个以产出为衡量标准的团队会用 AI 让自己被代码淹没。而一个以成果为衡量标准的团队会使用同样的 AI,最终用更少的代码完成更多的工作。
取而代之的衡量标准
解决方法是衡量工作的结果,而不是产生它的过程。结果指标更难被操控,因为它们与现实情况的改善挂钩,而不是与看起来忙碌的活动挂钩。
最强的信号是关于系统是否正常工作,以及你能多快地安全地变更它。服务有多可靠。一个变更从想法到生产需要多长时间。部署导致事故的频率有多高。变更被回滚的比例是多少。出现问题时需要多长时间恢复。这些描述了系统的健康状况和工作流,你无法通过堆砌提交来改善它们。你通过真正地改善系统来提升它们,这才是重点。
除此之外,要明确地重视简化工作。将保留行为的删除操作视为一流的贡献,并在评审和晋升中明确指出。计算你拒绝的需求范围,而不仅仅是你交付的需求范围。当有人阻止了一个本会成为负担的功能时,这个决定应该被看到并得到认可,就像一个已交付的功能一样。如果唯一被奖励的动词是“构建”,你就会得到构建行为,包括那些本不应该发生的构建。
以下是通俗的对比。
| 不好的指标(奖励数量) | 更好的指标(奖励结果) |
|---|---|
| 增加的代码行数 | 变更回滚率 |
| 每周提交次数 | 从想法到生产的交付周期 |
| 关闭的工单数 | 每次部署的事故率 |
| 合并的拉取请求数 | 从故障中恢复的时间 |
| 总代码量 | 运行中系统的可靠性 |
| 交付的功能数 | 拒绝的会增加成本的需求范围 |
右栏的指标没有一个可以通过为了写而写更多代码来满足。当你写的代码更少时,它们中的大多数都会改善。这正是你想要的特性。一个好的指标应该让删除、简化和克制看起来和它们实际上一样好。
指标是对话,而非记分牌
最后一点是关于如何使用这一切的。即使是更好的指标,在它们成为目标的那一刻也会变得有害,因为古德哈特定律(Goodhart's law)并不在乎你选择了哪个数字。变更回滚率可以通过从不发布任何有风险的东西来操纵。交付周期可以通过削减评审环节来操纵。任何一个单一的数字,一旦被用作决定薪酬和地位的记分牌,就会被不断优化,直到它失去其原有的意义。
所以,指标的正确作用是作为一个问题的开端,而非一个判断的终点。上升的故障率是去询问“发生了什么变化”的理由,而不是一根大棒。一周的负代码行数是一个提示,让你去看看哪些地方被简化了,而不是一个红叉。数字告诉你该往哪里看。仍然需要人去看。当你跳过这一步,让仪表盘直接给人们打分时,你就会得到那种被填充、被过度构建、为迎合仪表盘而生的工作成果——这是数量指标一直以来都会产生的,而现在,一台乐于为你提供你所要求的任何数量的机器,正以前所未有的速度生成着这些成果。
整件事可以归结为一条指令。在一个机器让“数量”几乎免费的时代,请停止衡量产出了多少。衡量系统是否变得更好,认可那些让系统变得更简单的工作,并将每个数字都视为一个需要讨论的事物,而不是一个需要追逐的目标。少,但做得对,才是真正的工作。指标应该体现出这一点。