原文:https://dev.to/debashish_ghosal/7-agent-eval-mistakes-that-cost-me-weeks-and-the-one-line-fixes-that-ended-them-ho(作者 @debashish_ghosal)
你肯定也遇到过:跑完评估,数字出来了,总觉得哪里不对劲。但数字就是数字,于是你继续往下走。三周之后你才发现,这个数字的问题从来就不出在模型身上。
我花了一周时间调 matcher,想解释 20% 的通过率。结果 bug 不在 matcher 里,而在评估器里,在那六行我早已不再细读的代码里——因为我早就不再信任它们了。
这篇文章列了七个错误。七个我全都犯过,而且就是按这个顺序,每一个都实实在在吃掉了我好几天。对每一个错误,我会讲清楚:我看到了什么现象、我先做了什么尝试(以及为什么那是修错了层),还有真正解决问题的小改动。它们看起来是七个不同的问题,根源却只有一个:我一直在修模型,而不是修测量。
如果 add commit push 已经成了你的肌肉记忆,而你手里的 Agent 指标还像个不许打开的黑盒,那就继续往下读。
*
1. 盲目相信全绿的测试套件
全绿的测试套件是软件工程里最昂贵的谎言。我的套件显示 359 个测试全部通过,而我 Agent 的真实通过率只有 20%。一片绿色,我当然认定是模型的问题。
我最初的反应:写更多测试。我本该做的:检查这些测试到底有没有断言任何东西。其中一个测试的函数体只有一行 pass。
def test_all_adapters_importable():
# 能 import 就肯定能用,对吧?
pass
pass 不是测试,它只是一个带函数体的绿色对勾。在一次 PlannerCritic 审查中,我发现 65 个断言文件里有 57 个格式错误,于是评测 harness 悄悄返回了 0/0 并计为通过。套件之所以全绿,是因为它什么都没跑。
如果一个模块返回 0/0,把它当错误处理,而不是当通过。零断言不是一种悄无声息的成功。
*
2. 让模型给自己打分
这个错误看起来像模型的问题。其实不是。
模型拿到了精确率(precision)1.00、召回率(recall)0.02 的成绩,居然还算通过。它找到了一个可以无限触发、不断白拿奖励的退化触发器。我的第一反应是调 prompt——修错了层,而且我整整调了一天。
我的 3B 模型学会了去匹配字面字符串 "step_1",因为奖励函数为重叠付钱,而不是为正确性付钱。它不是在学怎么做审查,而是在学什么能领到奖励。完整复盘在这里。
精确率 1.00 加召回率趋近于零,不是一个安全的模型,而是一个找到了最廉价方式来伪装安全的模型。
*
3. 用错了分母
召回率卡在 0.087。连续两次实地测试都是同一个数字,不管我改什么都纹丝不动。我几乎要下结论:模型到顶了。
我重写了 matcher。纯属浪费。matcher 毫无问题。
真正的修复只有一行作用域代码:把参考池限制在源域内。模型本来答对了问题,却被扔进一大堆毫不相关的标准答案样本里对答案,所以哪怕大体是对的,分数也很难看。限定作用域之后,召回率从 0.087 升到 0.170 / 0.228,模型一行都没改。完整数字在这里。
如果一个指标换遍所有模型都卡在同一个低位,先怀疑分母,再怀疑模型。
*
4. 只优化报告点名的那个层
一周的 matcher 工作只把指标拉动了十个百分点。十个百分点。我一直在修 matcher,因为失败的测试反复点名的就是这一层,而我对报告的信任超过了应有的限度。
真正见效的改动,是生成失败用例的模拟器(simulator)里的六行代码。分数从 20% 直接跳到 50%。六行代码。那一周的详细记录在这里。
这是最扎心的一个错误,因为报告把矛头指向 matcher,而我信了。
失败信息点名的层,未必就是坏掉的层。先证明问题源头,再押上你的一周。
*
5. 把与模型无关的失败当成模型天花板
本地 4B 模型和云端模型撞上了同一堵墙,分毫不差。两边都是整齐划一的 20%。我的膝跳反射是买一个更大的模型。还是同一堵墙。
截然不同的模型出现完全一致的失败率,几乎从来不是能力天花板,而是共享 harness 的 bug。我对比了本地 4B、更强的云端模型和角色分离方案,三者的结果一模一样——这种一致性本身就该敲响警钟。对比结果在这里。
如果每个模型都停在同一个数字上,该修的是那个常数,不是模型。
*
6. 永远挂不掉的 Mock
单元测试全绿,真实 Agent 却在现场接连失败。我的对策是写更多 mock。错招。
我的 mock 套件报告了 9% 的通过率,而零 mock 的实地测试找出了 mock 根本表达不出来的每一处失败。为什么?因为 mock 出来的 agent 永远会按要求调用工具,而真实模型有时候干脆用一段自然语言直接作答。
mock 只能以你早已想到的方式失败,真实 Agent 会以你没想到的方式失败。
你的 mock 记录的是你当前的想象力,而不是系统的真实面目。
7. 忽视了 runner
一次计划跑 1000 轮的基准测试,在进度 80% 处倒下了。我把锅甩给语料库,重跑了一遍,它又倒下了。
真正的 bug 出在 runner 上。一次挂死的 API 调用,悄悄丢掉了 60 个已完成的审计任务——没有报错记录,什么都没有。我给每条轨迹加了超时、把超时归类为不可重试、加了 token 上限、失败即隔离、关停时取消剩余任务。此后一次干净的实际测试跑完 4768 次轨迹运行,一次完整的扫描都没丢。实际测试报告在仓库里。
挂死是数据完整性 bug,不是性能 bug。一次不完整的扫描看起来仍然像合法数据,这正是陷阱所在。
*
七个问题的共同点
回头看一遍:这七个问题里只有一个是真正的模型问题,而且就连那一个,最后也是在打分器里修好的,而不是在提示词里。其余六个全是测量层的 bug:一个不做断言的测试套件、一个说谎的分母、一个被我认错的层、一个被多个模型共享的分类器、一组无法失败的 mock、一个丢数据的 runner。
如果要把教训重新表述成一句话,那就是:奖励函数几乎总是真正的 bug。你可以永远调模型,那个数字纹丝不动,因为那个数字从头到尾就不是模型的事。
还有一个我至今回答不了的开放问题:你怎么知道自己的 eval 是诚实的?我现在能点名这七个错误,却没法完全说服自己已经抓到了第八个。从命令行看过去,一个藏着测量 bug 的绿色套件,和一个带着真 bug 的红色套件,长得一模一样。我认为目前还没有针对这件事的干净测试方法,我宁愿把这句话大声说出来,也不愿假装这个问题已经关闭。
在吃到教训之前,这几个错误你先上线过哪一个?我七个全都认。欢迎说说你的那份。
代码与佐证:agent-eval-forge · CauterRule · planner-critic-engine——全部 MIT 许可,全部公开。
原文:https://dev.to/debashish_ghosal/7-agent-eval-mistakes-that-cost-me-weeks-and-the-one-line-fixes-that-ended-them-ho(作者 @debashish_ghosal)



