AIGC标识 那次 DirectMail 事故之后,我们给 AI 编码加了几条规则

前一阵,我们的云端认证服务第一次真实发信就失败了。验证码发不出去,云端日志里是阿里云 DirectMail 的一句 InvalidReplyToAddress。看起来像个小问题,排查下来居然花了一晚上。

第一次是配置。回信地址的环境变量写成了显示名格式,DirectMail 对这个字段的字符集要求极严,直接拒收。删掉它,又加了启动期校验,以为完事了。没完。

第二次是运行时。配置删了,错误一字不变。查下来,FC 的常驻实例根本不感知配置变更,实例用的是出生时的快照,唯一的办法是重新部署,强制实例重建。

第三次部署。重新部署四个字,连环踩了四个坑:本机没有 zip 命令,用 tar -a 打出来的是个伪装成 zip 的 tar,用 od 打开文件头才看出来;函数要自带 Node 运行时;Windows 打的 zip 没有执行位,得显式把 create_system 设成 Unix;pnpm 的软链布局被 zip 打平,模块解析直接断链。一个个填平才往前走。

第四次根因,最离谱。部署修好,还是失败。拿生产凭据做了五组对照实验,结果出乎意料:这个参数必填,而且只认布尔值;文档里写的 0/1,网关根本不认。而代码里传的恰恰是 0/1——SDK 的构造函数签名太松,条件 spread 一绕,TypeScript 全程没吭声。

修复很简单:参数构造抽成纯函数,返回布尔,配契约测试。assert/strict 之下 0 不等于 false,谁再改回数值写法,测试直接红。真正让人不舒服的,是回头翻 git 历史看到的东西。

正确答案曾经存在过

有人问:排查为什么不从一开始就做 API 实验?翻完 git 历史发现,实现的时候恰恰就是从 API 开始的。真凭据,真收件箱,冒烟成功,写的还是正确的布尔。

第二天早上一轮 review 加重构,把跑通的布尔改成了 0/1,注释写着「API 规格为必填 0/1 数值」。这轮提交的验证记录是 89 个测试全绿。一周后它在生产上炸了。

也就是说,测试全绿的那一刻,正好是 bug 进门的那一刻。正确答案只在世上活了一晚。排查的时候,眼前所有文本都是自洽的:注释引用规格,文档写 0/1,错误信息指向回信地址。唯一知道真相的那次冒烟,在仓库里没留下任何可读的痕迹。

为什么会这样

这轮改动是 AI 参与完成的。但先别急着怪 AI。看它的输入:代码写布尔,文档写 0/1,两处对不上。它消解矛盾的默认办法是改代码向文档看齐,因为「文档是对的」是训练语料教给它的先验,这个先验一百次里有九十九次是对的。错的那一次,被我们赶上了。

更要命的是,那次跑通的冒烟从来没变成任何资产。结论只存在于当晚的会话记录里,仓库里的测试全走 fake transport,从没见过真实请求长什么样。下一轮改动,不管是人还是 AI,都读不到会话记录,只读得到仓库。而在仓库里,一句随口编的注释和一条实测的结论,长得一模一样。TypeScript 本来有机会拦,松签名放它过去了。

链条就是这样:经验没固化,文本权威乘虚而入,编译器失声,全绿的测试把 bug 护送上路。AI 编码只是让这条链转得更快——重构更快,对齐更勤,经验蒸发得也更利索。

我们加的规则

在 AI 编码里,散文没有约束力。你记得什么、在会话里说过什么、跑通过哪次冒烟,对下一轮改动都不存在。规则只有写成 AI 和流水线绕不过去的形式,才算规则。下面是我们现在的清单:

1.接外部 API 之前,先写探针。探针脚本常驻仓库,用真凭据对参数形态做对照实验,跑通了再写业务代码。这次定位根因的五组实验只花了 20 分钟,但脚本是事故当晚现搭的——如果接入时就写好,这次事故不会发生。

2.文档和 SDK 类型打架时,不许二选一,做实验。这次文档说 0/1,SDK 类型说 boolean。遇到这种矛盾,唯一正确的动作是把探针跑起来,没有「先信文档」或「先信类型」的选项。

3.实测结论写成测试,别写成注释。外部 API 的参数构造抽成带显式返回类型的纯函数,参数形态用契约测试锁死。现在谁改回 0/1,类型过不了,测试也会红。散文会被按上游文档纠正,断言只会大声失败。

4.动外部调用点的改动,提交信息必须写实证依据。请求参数形态、端点、鉴权方式,动了就要写明什么时候实测过、哪个探针、哪条测试。禁止以「对齐文档」为由改行为。这条写进了 CLAUDE.md,人审和 AI 审同一个标准。这次的 bug 就是 review 放进门的,门禁当时只看文本符合度。

5.写结论时,把验证方式一起写上。「API 规格为必填 0/1」那条注释没有任何来源,后来所有人都把它当成验证过的事实。现在的规矩是结论带出处:何时实测、怎么验证。猜的就写是猜的。

6.全绿不等于对,关键路径发布前真冒烟。那 89 个测试全走 fake transport,参数形态错了也绿。现在发布清单里有一条:部署之后真实发一封邮件,确认收件箱收到,不是看一眼响应码就算。

7.排查第三方契约类错误时,探针并行跑。参数非法、格式不符这类错误一出现,实验和查配置同时进行,别等本地假设穷尽完再想起来。AI 尤其需要流程强制这一步——怀疑文档等于怀疑它的训练数据,不逼它,它不会主动走这一步。

这套规则不铺到每个端点。只对用户可感知的关键路径做契约化防御,认证、支付、发信这类。防过头和不防,错的程度差不多。

结尾

排查记录最后写了一句:文档会骗人,对照实验不会。这事过后想补半句——实验的结论要是不固化成测试,它活不过下一轮重构。

想让 AI 编码可靠,先问一个问题:你的规则,是 AI 绕不过去的形式,还是它随时会按文档改掉的形式?测试、探针、提交证据、发布清单,算前者。注释和口头叮嘱,都不算。

后记

规则还有一半,这篇没写。测试、探针、提交证据,这些都属于硬知识:被强制执行,失败了会大声报错。但另一类知识进不了 CI——教训、红旗、当时的判断依据。它们管的是注意力,决定 AI 动手之前注意到什么。文档和 SDK 类型打架、FC 的实例拿的是快照,这两条要是事先就知道,四层洋葱能少走一半。而这类知识,项目里的 CLAUDE.md 装不下——它们对所有用 DirectMail 的项目、所有跑在 FC 上的部署都成立。它们应该跟着人走,不是跟着仓库走。

这就是我们在 Molio 里想解决的问题:一个本地优先的知识库加 AI runtime,把研发经验沉淀下来,让 AI 动手之前先从积累里拿到上下文,而不是靠训练记忆或者当次会话的聊天记录。这次事故算是它逆否命题的一个注脚:不积累的知识会持续蒸发,并且持续计费。这次的账单是一晚排查加一次生产故障。

顺便说,这篇文章本身就是积累的受益者——两篇复盘早就沉淀在知识库里,写的时候直接翻出来用,没靠任何人回忆。项目开源,如果你也被经验蒸发坑过,可以来看看,顺手点个 star 更好:https://github.com/zhuzhaoyun/Molio

posted @ 2026-08-24 22:11  AI闲人  阅读(2)  评论(0)    收藏  举报