前天下午五点半,arXiv 上挂出一篇 2611.06219,标题叫 A Definition of Catastrophic Performance。作者三人:Henrik Marklund、Alex Infanger、Benjamin Van Roy。v1 的提交时间戳是 UTC 09:30:13,提交人 Alex Infanger。
我点开纯属巧合。标题里那个词我这半年在群里看到太多次了——模型回一句脏话是“灾难性”,prompt injection 让 agent 发出去一封邮件也是“灾难性”。一个词用到这个密度,基本就废了。
这篇想干的事很朴素:把尺子换掉。
不再拿“有没有帮上忙”当尺子,改用两个参考值夹住:一个是 baseline value,即一个智能体都不部署时你能拿到的价值;另一个是 orthogonal value,即和人类偏好基本无关的那些轨迹能拿到的价值,作者说它通常远低于 baseline。
判词就一句:灾难性性能,指这个智能体抵消了人类活动攒下来的收益。
作者给的例子朴素得有点好笑——人类过去的活动把环境状态改好了,你从这个状态出发丢一个 agent 下去,本来想加速后续进展,结果它把“已经改好了”这件事给抹平了。
我第一反应是这门槛定得真高。高得好。
orthogonal value 是全文最不显眼的一刀。如果只拿 baseline 当参照,“比不部署更差”就叫灾难,那这个定义等于“负收益”的同义词,一点信息量都没有——负收益天天有,用不着单开一个词。加上 orthogonal 之后才变了味:它在“什么都不干”和“乱干一通”之间划了一条线,让“抵消”有了个可比的坐标。你得先证明这比乱干还差,才配叫灾难。
我的读法是,作者想堵的是“反正 agent 干了点事,总比闲着强”那种和稀泥。按这个框架,闲着是有价值的,而且不低。
有一处我不确定:orthogonal value 一般情况下怎么估,摘要层面我没看到答案,正文里有没有估计方法我也没细读。但这不影响我给这个定义投一票,理由在下面。
我一直烦“灾难”这个词的通胀。通胀本身不是措辞难看的问题,是真出事的时候没人听得见。
更偷懒的是拿任务成功率代替这套判断。成功率的参照系是工单,工单是写工单的人写的,而写工单的人跟你一样,不知道那段代码为什么长这样。用成功率当尺子,等于让一个不了解历史的人给历史打分。
这个定义至少逼你回答一个平时没人问的问题:这个 PR 我根本不合,会怎样?
大部分时候答案是“也不会怎样,原来的样子能跑”。那你就得解释清楚,你合进去的这一坨比原来那坨好在哪。
我之所以对这篇有感觉,是因为在我这个量级上,抵消天天发生,只是从来没人这么叫。
去年有一段代码:
// upstream 有 ~1s 的最终一致性窗口,这里的 sleep 不是遗留垃圾
await sleep(800);
const fresh = await cache.get(key);
那个 800 是两年前一次脏读事故换来的。用户改完昵称,刷新还是旧的,客服一天收了四十多条工单。查了整整一个下午,最后定位到上游 CDN 的收敛窗口。加上这行 sleep,事故没了。注释写了半句,没写事故本身。
后来我让 agent 顺手“清理一下这个接口的性能问题”。
- // upstream 有 ~1s 的最终一致性窗口,这里的 sleep 不是遗留垃圾
- await sleep(800);
const fresh = await cache.get(key);
P99 从 900ms 掉到 120ms,监控一片绿,我还挺高兴。
第二天脏读回来了:
[2026-09-1xT02:11:47Z] GET /profile uid=... served stale name (cache age 812ms)
[2026-09-1xT02:11:52Z] GET /profile uid=... served stale name (cache age 790ms)
标准的一次抵消。它完美达成了我给它的目标——降低这个接口的延迟,同时把两年前用四十多张工单换来的东西给抹掉了。按论文的框架,这个改动的价值低于 baseline value:不部署它,接口 900ms,但是对的;部署了它,120ms,但是错的。
画外音:实习生没有心理负担,因为它不知道这 800ms 是怎么来的。这不是它的问题,是我没把这段历史写进 prompt。但话说回来,就算我写了,它大概率也会在心里给这段历史贴个“历史包袱”的标签。
这里有个我越想越觉得要命的不对称。
agent 帮上忙的时候,收益是渐进的、可回滚的——延迟降了,出问题把 diff revert 掉,世界回到原样。
抵消反过来。它删掉的东西往往没有任何备份:不在版本历史里,不在注释里,不在文档里,只在某个人的脑子里,或者在一次没写会议纪要的讨论结论里。你甚至不知道自己丢了什么,直到三天后工单开始堆。
“没帮上忙”是可恢复的,“抵消”是不可恢复的。这两个不该用同一个词,也不该走同一套流程。
这也是我为什么觉得“AI 写的代码要比手写的审得更严”不是洁癖。手写的人至少知道自己在删什么——他可能判断错了,但他做过判断。生成的那一方没做过判断,它在做模式匹配,而“看起来像遗留垃圾”和“是遗留垃圾”之间,隔着一次线上事故。
我现在的做法很土。prompt 里加三行,任何带“优化/清理/重构”字样的任务都带上:
在给 diff 之前,先列三行:
1. 这段代码里有哪些看起来可以删、但可能承载了历史决策的东西;
2. 每一处标出来,说明你为什么认为它可以删;
3. 如果删错了,最早会在什么信号上暴露。
不万能。它列出来的东西我经常一条条否掉,说“这个真能删”。但它把“我不知道历史”这件事从隐性变成显性了,review 时间大概省了一半——我不再需要从零重建上下文,只需要反驳它。
第三行最有用。它逼我们俩一起想“怎么发现错了”,而不是“怎么证明对了”。这算是我这半年攒下最不性感、但最好用的一条经验 😅。
开头我说门槛定得高,高得好,当时的意思是“别什么都叫灾难”。写到这儿我想往前推一步:正因为大部分 agent 事故够不上这条线,它才有用。
够不上,说明它们是普通 bug。普通 bug 走普通流程:复现、修、加一条回归测试,把这次的教训钉进代码里——就像那行 sleep 该配的那半句注释。
够不上的东西一旦被叫成灾难,处理流程就跟着变形:从修 bug 变成讨论原则,从加测试变成写一篇“我们对 AI 风险的立场”。这种替换我见过太多次,代价是下一个撞上同一个坑的人什么也没得到。
这定义还有个我绕不过去的地方:baseline value 是“不部署任何智能体时”的价值。现实里没有这个选项。你不部署这个 agent,也会部署那个;都不部署,人肉干;人肉也不干,这事就烂在那儿。对照组是虚构的。
所以它给的不是一把尺子,是一句判词模板。事后你可以拿它问两遍——这比什么都不干更差吗?这比乱干更差吗?问完你会发现,难的从来不是判定,是估那两个参考值。
但那也比现在强。现在我至少有个能写进复盘第一行的话:这次改动的 baseline 是什么。写不出来,就别合。
以后凡是任务描述里带“优化”“清理”“简化”这三个词的,我先写这一行,写不出来就把 prompt 退回去重写。这条规则是那行 800ms 的 sleep 换来的。不贵,但我也不想再交一次。
