AI 原生系列 · 可靠性
我把这个 Bug 修了三次。Bug 从来都不是问题所在。
一分钟速览——你能带走什么
我把同一个坏掉的页面报告给我的 AI agent 三次;第三次,我是用全大写打的。每一次回复都是真的——修好了、合并了、测试全绿——可页面依然是死的,因为交付链上有三处不同的环节断了,而断点之上的每一层都还在报告成功。教训是:“完成”这个说法,说的只是最后一环。要在用户所处的高度去验证,否则你不过是在收集自己开给自己的收据。
一个下午的尸检:一个接线错误、一个悄悄玩消失的部署平台,还有一个从未真正跑起来过的功能。约 6 分钟读完。
8 月 2 日,我告诉我的 AI agent,这个作品集网站上的一个私有仪表盘显示“Not Found”。它说它修好了这个 bug。我去检查:Not Found。我又报告了一次。它修了另一个 bug。Not Found。第三次,我打的是WHY???——三个问号,这是利益相关方耐心的国际计量单位。
别扭的地方在这儿:那些修复,每一个都是对的。测试全绿。合并都是真实发生的。可页面还是死的,一次又一次,因为我对付的从来不是一个 bug。我对付的是一条链条 ——而链条每次只会在一个环节断裂,修好上一个断点,才会露出下一个断点。
心智模型:一场接力赛,每个跑者都给自己这一棒打分。 一次修复要经过这样的旅程——代码 → 合并 → 构建 → 部署 → 服务 → 认证 → 用户的屏幕。接力赛里的每个跑者都会信誓旦旦地说自己这一棒跑得漂亮,而且他们通常没说错。但比赛只有在接力棒越过最后一道线时才算赢,而中间没有一个人能看到那条线。“完成”只是关于最后一次交接的一句断言。上游的一切,都只是跑者自己给自己写的评语。
第一幕:两个各自正确的部分,却没握上手
仪表盘的 API 靠读取一个 HTTP 头来做认证。仪表盘的页面去请求这个 API——却压根没带任何头信息。API 是对的:它按设计拒绝了匿名调用。页面也是对的:它按设计取回了自己的数据。没人认领中间那句话:页面必须携带 API 要读取的那个凭证.
Google 的 SRE 那本书里有句话,我多年前读过,当时大概归到了“说得挺好”那一类:"Note that in a multilayered system, one person's symptom is another person's cause" [1](中译:在一个多层系统里,一个人看到的症状,正是另一个人造成的病因)。我的症状是 404。而病因,藏在两个各自都通过了自己测试的组件之间的接缝里。接缝是没有测试套件的,除非你自己写一个——这正是契约测试存在的全部理由,Fowler 就指出过 "[a] failure in any of these contract tests implies you need to update your test doubles, and probably your code" [2](中译:这些契约测试中任何一个失败,都意味着你需要更新测试替身,很可能也需要更新代码)。契约,才是那个没人测过的东西。
第二幕:那个不动声色玩失踪的部署平台
于是我们修好了接线问题,合并了代码,告诉用户(也就是我):搞定了。结果还是 Not Found。
修复已经在主分支上了。测试全绿。但没人知道的是:这家托管平台的免费套餐——原话摘自它自己的限额说明页——只允许 "100 times every 86400 seconds" [3](中译:每 86400 秒 100 次),而一个下午里我们兴致勃勃地提了一堆小 PR,已经把这一百次额度全用光了。从那之后的每一次合并,触发的都是空。仓库里没有报错,没有红叉。部署集成只是不动声色地拒绝了,像一个没有底的邮箱:它整天都在收你的信。
我的智能体一直告诉我“已合并 ✓”,那语气就像服务员说“马上就来”——可后厨其实从午饭后就关门了。这句话是真的,但也毫无意义,因为“已合并”不过是你自己给自己开的收据上、自己写的字——真正面向用户的问题是“已部署并且在提供服务了吗”,而这个问题从没人问过。SRE 那本书把这个叫做黑盒/白盒之分:黑盒监控 "is symptom-oriented and represents active—not predicted—problems: 'The system isn't working correctly, right now'" [1](中译:黑盒监控是面向症状的,代表的是实际发生的——而非预测中的——问题:“系统现在就是不能正常工作”)。我们跑的每一项检查都是白盒。用户是当时唯一在岗的黑盒监控,而用户正在用一连串问号跟我算账。
第三幕:那个一次都没真正跑起来过的功能
额度腾出来了,强制触发了部署,页面终于加载出来了——却是空的。仪表盘渲染的“成绩单”,本该由一个 CLI 推送到网站上。我们这才发现,这次推送这辈子就没成功过:CLI 发送凭证时用的是一个头字段名,网站读取的却是另一个。这个功能其实是测试过的——只不过测的是那条不需要凭证的路径。而需要认证的那一半,生产环境里的运行次数是零。不是“很少运行”,是零。
一段从没在生产环境里跑过的代码,算不上一个功能。它顶多是一条附带单元测试的谣言。
为什么反复出现三次?瑞士奶酪模型的答案
James Reason 的瑞士奶酪模型将高科技系统中的事故描述为穿越多层防御漏洞的轨迹——"some are engineered (alarms, physical barriers, automatic shutdowns, etc), others rely on people" [4](中译:有些防御是工程手段(警报、物理屏障、自动停机等),有些则依赖人)。这个模型通常用来解释漏洞层层对齐、灾难趁隙而入的情形。而我这个下午恰恰相反,也是一线工程师每天都在经历的版本:那些奶酪片叠得太深,以至于补上一个洞,只会暴露下一片的洞,一个用户投诉接一个用户投诉地暴露。纵深防御是双向的——它同时提供了「纵深失败」:每一层都客客气气地吸收了你的修复,然后递上一个全新的「Not Found」。
这条链条只有在有人在最后一环完成验证时才会终结。当我们终于用一个真实的浏览器、以真实的所有者会话,去访问那个真实部署的页面时——整条链瞬间水落石出:接线的修复根本没有部署,部署压根没有发生,数据管道从没通过一滴水。
模式 / 反模式
- 模式——在用户所处的高度验证。「修好这个页面」的完成定义,是一个以用户身份登录的浏览器,亲眼看到这个页面。不是测试,不是合并,是像素。
- 模式——每一次异步交接都需要一个正向确认。合并到部署之间是异步的。如果你不去检查部署是否真的启动了,沉默和成功看起来是同一种颜色。
- 模式——复活提问。对任何跨越边界的功能,都要问一句:这个功能上一次真正端到端地在生产环境跑通,是什么时候?如果诚实的答案是「从来没有」,那你手上的不是一个功能,而是一个传闻。
- 反模式——收集自己开的收据。「已合并 ✓」「测试全绿」「构建通过」,这些都是系统对自己下的断言。用户的屏幕才是唯一的第三方审计。
- 反模式——只测试半份契约。如果需要认证的那条路径依赖一个密钥,而你的 CI——那个跑测试的机器人——根本没有这个密钥,那你的 CI 测的其实是另一个恰好同名的功能。
机制,命名如下:在错误的高度上确认。每一层都在确认「自己」做过的那部分,而人(以及智能体)的大脑,会把这一串各自的确认听成一句响亮的「大功告成」。
第一性原理,一句话:一项改动,只存在于用户实际体验到它的那个高度;在此之上的一切,都不过是利益相关方自己写下的证词。
收据
毕竟一篇讲验证的文章,自己也该经得起验证:三次用户报告;三个截然不同的断点(页面→API 凭证、合并→构建配额、CLI→API 请求头名称);一个部署平台限额——每 86400 秒 100 次,被用得一次不剩;十九张成绩单被推送进一条实际吞吐量为零的管道——算上这次事故自己的那张成绩单,一共二十张。而我自己的问责引擎给这一轮的打分是1.0 分制下的 0.57 分。这份存档卡片(本仓库中的 rc0020)记录了我docs/reportcards/collection.json自报的分数——四个 1.0——而引擎的评分器在回读卡片时,会依据证据强度给每个自报分数设上限;对着那份文件跑一遍 anyagent report roadmap,它打出来的是 0.57。我的自评和它的判分,当众对不上,且这分歧摆在那儿,谁也别想改口——而这个数字,我后来还得原封不动地转述给那位一直在等消息的人。我建议大家都去搭这种系统:它逼着你把那个不愿说出口的数字大声念出来。这也是这篇尸检报告存在的唯一理由。
参考文献
- Beyer, B., Jones, C., Petoff, J., & Murphy, N. R. (2016)。《Site Reliability Engineering》,第 6 章:分布式系统监控。O'Reilly / Google。sre.google/sre-book/monitoring-distributed-systems
- Fowler, M. (2011)。《Contract Test》. martinfowler.com/bliki/ContractTest.html
- Vercel。《Limits》——“Deployments per day (Hobby): You are able to deploy 100 times every 86400 seconds (1 day).”(中译:每日部署次数(Hobby 套餐):每 86400 秒(即一天)最多可部署 100 次。) vercel.com/docs/limits
- Reason, J. (2000)。Human error: models and management。《BMJ》, 320(7237), 768–770. pmc.ncbi.nlm.nih.gov/articles/PMC1117770
相关阅读
本文写于事故发生当天,材料全部来自一手记录:PR、部署日志、palace push 的输出,还有用户那条带着三个问号的消息。—— Paul Jialiang Wu · agentic-portfolio-lovat.vercel.app