那个评估 harness 第一轮跑起来的时候,12 个输出有 11 个被判了 malformed。
12 个里 11 个。我盯着 scoreboard 看了半天,以为是脚本里除零了——不是。它只是非常坚定地告诉我:你喂给我的东西,我全都不认识。像一个入职第二天就被拉去当面试官的实习生,看谁都像简历造假。
然后你去看那个 malformed 到底是啥,事情就开始往荒诞的方向走了。
同一个标签,下面罩着三种完全不同的死法。第一种是 API 请求压根没送到模型那儿——额度跑光了,钱不够,模型根本没接客。第二种是本地 Ollama 的输出被终端控制序列污染了,ANSI 那堆转义字节就这么堂而皇之地嵌进了 JSON 里,像个不请自来的伴郎混进了婚礼合影。第三种更绝——JSON 本身是好的,合法的,但前面裹了一圈 markdown 的反引号 fence,parser 看到第一个字符是反引号就当场摆烂。
一个词,三个根因,既没有共同症状也没有共同病理。这就像法医在三具尸体上都写了"死于厨房事故",但一个是煤气中毒,一个是菜刀割腕,一个是吃蘑菇噎死的。
你要怎么修?你没法修"malformed",因为它根本不是一个病。你得先把那三具尸体分别验完,然后发现:哦,其实前两具根本不是自然死亡,是当年写捕获代码的我亲手捂死的。
第一个 bug 是用 subprocess 跑 ollama run 然后捕获 stdout——理论上够用了对吧?但 Ollama 的 CLI 是带 TUI 的,那些终端绘制的控制字节也顺着管道一起流出来了,全塞在模型的 JSON 响应里。你解析的时候看到一堆 \x1b[?25l,你还以为是模型在输出某种来自未来的神秘符号。第二个 bug 更丢人:直接对原始文本调用 json.loads,而文本的第一行是三个反引号。就三个反引号。任何一个写过两年代码的人,都应该在看到三个反引号的瞬间闻到 markdown 的味道——但我没有,我选择了信任数据。
修的过程就不展开了,反正就是那套:换成 HTTP API、写了个剥 fence 的函数、单文件改 54 行、commit 49902b2。改完之后重新跑,18 个 case 零 malformed,Sonnet 把 12 个正例全抓回来了。那一刻我差点给这台机器上香——"显灵"了,真的显灵了。
但这个故事如果在这儿结束,我就不写它了。
因为紧接着我发现了一件更瘆人的事:之前那些被标成 malformed 的输出,在评分器眼里并不是"无法判定",而是被静默地转化成了判定。
unparseable 的正例,计成 miss。unparseable 的负例,有可能直接算成 clean pass。
你品品这个逻辑。看不懂正确答案,算你答错了;看不懂反面材料,算你表现良好。这是哪门子评估?这是睁一只眼闭一只眼,而且闭上的那只眼睛比较大、比较会读题。一个声称要评判 AI 有没有错的工具,在自己的盲区里,把"不知道"悄悄换算成了"对/错"——它比很多我见过的人都会无中生有地写年终总结。
所以第二个修复不情不愿地来了:malformed 不再参与任何聚合统计,单独列出来,明晃晃地标着"未评分",但原始输出和诊断原因全部留痕。
我当时在 commit message 里写了句"exclude unscored",写的时候心里想的是:这玩意儿早该这么干了。宁可诚实地显示"我看不懂",好过假装看懂然后给你一个义正词严的分数。机器假装读懂的后果,比人假装读懂可怕多了——人假装读懂至少还会心虚,AI 不会,评分器更像他那个人工智障它爹,它连心虚这个功能都没装。
顺带说一句,这个项目要是当时接了 Sentry,两个 capture bug 都撑不过五分钟。它那个 breadcrumb 里直接记了三样东西:原始输出长度、前 200 字符预览、还有"是不是以 markdown fence 开头"。三样东西像三根手指头,齐刷刷指着案发现场:这边,这边,以及这边。
但你没有接。
你没有接是因为觉得"跑个评测而已用不着上监控",这个想法和"就改一行代码不用写测试"肩并肩,那是程序员史上最大的一对双子坟。我这辈子在里面躺过不少于七次,次次都是自己铲的土。
改完之后测试套件从 38 个涨到 40 个,还有一个预期内的 xfail。一切都长舒了一口气。然后我打开那个 repo,发现作者把修之前的破玩意儿全留在公开提交里了,就差在 README 写一句"请自行前往 master 的早期 commit 瞻仰事故现场,拍照请勿开闪光灯"。
我其实特别理解这个举动。就像出过车祸的人不想修车,但会把残骸摆在院子里——不是为了好看,是为了让你知道:这件事真的发生过,而且发生得异常丢人。
写到最后我想说,那次排查全程里我印象最深的不是哪个 bug,而是"malformed"这个词本身被用到烂的过程。它最初是个技术状态,后来变成一个垃圾桶,最后变成一个我见到就想砸屏幕的字符串。人类的语言就是这么被工作磨损的——你今天说"P0"还心惊肉跳,等你把它挂在嘴边调侃的时候,说明你离麻木不远了。祝我早日回到"看到 P0 还会慌"的状态,那说明我还没把倒霉当日常。
(友情提示:如果哪天你发现自己写的 harness 把一半结果判成 malformed,先别查模型——大概率是你的代码想你了,给点耐心,实在不行就上 Sentry,真的。)