跳到主要内容
一万两千六百小时,和那些看起来对的 diff

一万两千六百小时,和那些看起来对的 diff

阿舟
阿舟

· 阅读约 6 分钟

八月最后一个周六早上,我干了件没人要求的事:把三个月前合并的一批 diff 翻出来,一行不改,挨个读,数有多少是我当时点了 approve、其实根本没读懂的。

读到第四十个我停了。

起因是三天前晚上刷到的一条新闻。法国西南部巴约讷的法院判了个案子。索菲·莱诺,59 岁,1989 到 2019 年在法航做乘务员,后来是乘务长,职业生涯累计飞了一万两千六百小时,一半以上是夜航。她得了乳腺癌,要求认定成职业病。法院认了。理由不是哪一趟航班,是三样叠加:宇宙辐射、2000 年之前的机舱二手烟、长期夜班。

我盯着这个结构看了很久。不是盯辐射,是盯法院的归因方式。它压根没说"你这次得病是因为你飞了哪一趟"。朴茨茅斯大学那位教授说得很清楚——没法确定某个具体病例是辐射引起的,现行防护指南的假设无非是"任何小剂量都可能有风险,剂量越低风险越低",就一个很模糊的假设。

法院还是判了。它认的不是单次归因,是一段累积暴露。

我坐不住了。

那两行

那个周六早上,我把那批 diff 从头翻了一遍。挑两个当时形状最规矩的给你看。

第一个是 agent 加的配置读取。我当时的 review 意见写的是"合理,加上兜底更稳":

def load_config(path):
    try:
        return json.loads(open(path).read())
    except Exception:
        return {}

这个 except 到现在吞掉了多少种错误,我不知道——吞掉就是吞掉,日志里什么都不留。最后发现它,是因为另一个八竿子打不着的功能读到了空配置,行为跟默认值撞在一起,表现成一个完全不像配置问题的现象。

画外音:这看起来是整个文件里最安全的两行。它做的唯一一件事,是让错误消失。

第二个更短:

query = query.limit(1000)

一行。省得一次拉太多,看着挺克制,是懂事的实习生才顺手加的东西。超过一千条的数据从那一刻起被静默截断,截得彬彬有礼,连个 warning 都没有。

这两处的共同点:单看,都对。

真凶不是哪一行

后来我想明白的事有点难看——我审 diff 的方式,不知道从哪天起变了。

一周三十来个 agent 生成的 diff,前五个我逐行读。从第六个开始,我读的是形状:改了几个文件、增删多少行、有没有碰生产配置、有没有动数据库。我承认,我最先看的就是这一行:

6 files changed, 142 insertions(+), 8 deletions(-)

142 增 8 删,形状正常,过。上面那两行,就藏在某个 142 里。

这跟一个飞了二十年的人对"这一趟不会有事"的手感,是一回事。不是不负责,是暴露量太大之后的自我保护——你没法对第三十次相同的东西,保持第一次的警觉。物理上做不到。

然后我看到哈佛医学院八月那份研究,愣了一下。他们排了 500 多种职业,空乘人员的辐射相关癌症死亡率第一,6.9%;飞行员第二,6.7%。核技术员排第 12。

核技术员。胸口挂剂量计、进机房要签字、安全手册厚得能砸死人的那个岗位,排第 12。第一名是一个端咖啡、发毯子、看起来跟"辐射"两个字毫无关系的职业。

原因不复杂:从巴黎出发的很多长途航线从北极附近绕,地球别处的磁场能挡一部分宇宙射线,极地那块挡不住,强度就上来了。一趟没事,一万两千六百小时是另一回事。

我一度认真考虑过去买个剂量计挂胸口——扯远了。我又不是空乘,一年飞那几趟,剂量在统计上根本不显著,报道里明说的。我想说的是,人面对"看不见的累积",第一反应永远是去买个能读数的仪表,而不是改自己的行为。

我犯过的每个要命错,都不是那些看着危险的改动。

改索引、删字段、动权限、跑迁移——这些我审得极严,严到自己都嫌烦,每个都要 dry-run,每个都要回滚方案。真正漏过去的,全是看着最安全的:一个兜底、一个默认值、一个静默截断、一行"顺手加个保护"。它们不危险,它们只是让危险变安静。

但我那个类比站不住

写到这我得刹一下车。

我一度想把"技术债等于慢性辐射"这个比喻直接收尾,写完觉得差点意思。辐射是真的在身体里累积,细胞不跟你商量,中间也没有 review 环节。代码债不是。它最终会在某一刻以一个具体的 bug 爆出来——而爆出来是好事,是反馈,是白给的信息。

是这样吗?我纠结了一会。

后来觉得只对了一半。爆是会爆,但爆的位置不指向病灶。上面那个 except 吞掉的错误,三个月后在下游某个模块以"数据对不上"的形式爆,中间隔着三层调用、两个服务、一次数据同步。我真正在衰减的不是代码质量,是我把"谁干的"揪出来的能力。

累积暴晒的不是风险,是归因成本。

这个说法没"技术债会癌变"那么顺口,也没那么吓人,但我留着它,因为它是准的。

扯远了,回正题。那条新闻里还有一句我抄下来的。莱诺的律师说,这是法国第一次把乳腺癌确认为空乘的职业风险,后面类似索赔有路可走了。一万两千六百小时,最后换来一句"这是可以被认定的"。

这个我不评价。公共议题上我没那资格。我只是把它存下来了。

现在的做法

规矩就一条:不再按"这个 diff 看起来对不对"审,改成按周算累积暴露。

每周五下午抽一小时,不碰新代码,只干一件事——把这周 agent 生成的所有"让错误消失"的代码挑出来单列。就四类:宽泛的异常捕获、静默截断、有默认值的取值、兜底分支。

这四类的共同性质是测试抓不到。一个 except 吞掉的错误,不会让任何断言失败,它只会让本该红的测试继续绿着。你给实习生写一批测试让它自己跑,它跑过了——跑过的是它自己那套裁判标准。这个坑我踩过不止一次,这次又踩。

工具丑得可以,就一条 grep:

git log --since="7 days ago" -p -- '*.py' \
  | grep -nE '^\+.*(except|or \{\}|or \[\]|get\([^,)]+,|limit\()' \
  | grep -viE 'test|mock'

误报一堆,但它至少能把这批东西从 142 行里拽到我眼前。拽到眼前,我一定逐行读。

我纠结过要不要把它做成自动化脚本,跑完给个数。后来放弃了。自动化的风险是,我又开始读形状——它告诉我"本周 17 处",我就开始关心这个数比上周多了还是少了,而不是关心这 17 处到底干了什么。宁可丑,宁可手动。

附带一条:这四类代码,现在在我这里的风险等级,跟改生产库是一个级别。不是因为它危险,是因为它让危险变得安静。

那批 diff 我最后翻完了。47 个是我点过 approve 但没读懂的。改了 2 个。剩下的还在那儿。

本期缴税:一次"应该没事",换一次"果然有事"。下次见。

阿舟
阿舟

写代码写到一半开始怀疑人生,靠 AI 工具续命,顺手把踩过的坑都记下来。

查看主页 →