你经历过这种情况。你运行了评估,数字回来了,但总觉得哪里不对劲。但数字就是数字,所以你继续往下走。三周后你发现,那个数字从来就不是模型的问题。

我花了一周时间调一个匹配器,想解释 20% 的通过率。bug 不在匹配器里。它在评估器里,就在那六行代码里——我已经不再读它们了,因为我已经不再信任它们了。

这是一份包含七个错误的清单。这七个错误我全都犯过,而且就是按这个顺序犯的,每一个都实实在在地耗费了我好几天。对于每一个错误,我会告诉你我看到了什么、我最初尝试了什么(以及为什么那是错误的层面),以及真正修复它的小改动。它们看起来像是七个不同的问题。它们只有一个根本原因:我一直在修模型,而不是修测量。

如果add commit push已经是肌肉记忆,但你的智能体指标仍然感觉像是一个你不被允许打开的黑箱,那就继续往下读。


1. 信任一个绿色的测试套件

绿色的测试套件是软件中最昂贵的谎言。我的套件显示 359 个测试通过,而我的智能体真实通过率却停留在 20%。一切都是绿的,所以我当然就假设是模型的问题。

我最初尝试的:加更多测试。我本该做的:检查这些测试到底有没有在断言任何东西。其中一个测试的函数体只有pass。

def test_all_adapters_importable():
    # 它导入成功了,所以它肯定能工作,对吧?    pass

pass不是测试。它是一个带函数体的绿色对勾。在一次 PlannerCritic 审查中,我发现 65 个断言文件里有 57 个格式错误,所以测试框架悄悄返回了 0/0,并把它当作通过。套件之所以是绿的,是因为它什么都没运行。

如果一个模块返回 0/0,把它当作错误,而不是通过。零断言不是悄无声息的成功。


2. 让模型给自己打分

这个问题感觉像是模型的问题。其实不是。

模型精确率得分 1.00、召回率 0.02,却仍然通过了。它找到了一个退化的触发器,可以无限触发并收集功劳。我的第一反应是调提示词。那是错误的层面,我为此调了一整天。

我的 3B 模型学会了匹配字面字符串"step_1"因为奖励函数为重叠付钱,而不是为正确性付钱。它不是在学审查。它是在学什么能拿到钱。完整文章在这里。

精确率 1.00 而召回率接近零并不是一个安全的模型。它是一个找到了最省事的方式来显得安全的模型。


3. 用错误的分母来评分

召回率卡在 0.087。同样的数字,连续两次现场测试,无论我改什么都是如此。我差点就要下结论说这个模型已经到天花板了。

我重建了匹配器。那是白费功夫。匹配器本身没问题。

真正的修复只有一行作用域代码:把参考池限制在源领域内。模型是在一堆不相关的黄金样本里回答正确的问题,所以即使它大体上是对的,看起来也很差。一旦我限定了作用域,召回率从 0.087 升到了 0.170 / 0.228,完全没有改动模型。完整的数字在这里。

如果一个指标在你尝试的每个模型上都卡在同一个低值,先怀疑分母,再怀疑模型。


4. 优化报告所指的那一层

一周的匹配器工作让指标提升了十个点。十个点。我一直在修匹配器,因为那是失败的测试不断指向的那一层,而我过于相信那份报告了。

真正起作用的改动,是生成失败用例的模拟器里的六行代码。分数从 20% 跳到了 50%。六行。我在这里写了关于那一周的事。

这是最让我难受的错误,因为报告把我指向了匹配器,而我相信了它。

失败所指向的那一层,并不总是坏掉的那一层。在花掉一周之前,先证明问题的根源。


5. 把与模型无关的失败当成模型的天花板

一个本地 4B 模型和一个云端模型撞上了完全相同的那堵墙。两者都是统一的 20%。我的本能反应是去买一个更大的模型。还是同一堵墙。

截然不同的模型出现完全相同的失败率,几乎从来都不是能力天花板。这是一个共用的测试框架 bug。我比较了一个本地 4B、一个更好的云端模型,以及角色分离,结果完全相同,这本该是一个警告信号。对比在这里。

如果每个模型都在同一个数字上停滞,那个常数才是要修的东西。不是模型。


6. 无法被打破的 mock

单元测试全绿。真实智能体在现场失败。我写了更多的 mock。错误的做法。

我的 mock 套件报告 9% 通过。零 mock 的现场测试发现了 mock 无法表达的每一个失败。为什么?mock 智能体总是调用它被告知要调用的工具。而真实模型,有时只是用散文作答。

mock 只能以你已经想象到的方式失败。真实智能体以你没想到的方式失败。

你的 mock 是你当前想象力的记录,而不是系统的记录。


7. 忘记 Runner

一个 1,000 次运行的基准测试在 80% 处死掉了。我怪罪语料库。我重新跑了一遍。它又死了。

bug 出在 runner 上。一次挂起的 API 调用丢弃了 60 个已完成的审计,没有错误记录,什么都没有。我加了每条轨迹的超时、不可重试的超时分类、token 上限、失败时隔离,以及关闭时取消。在那之后,一次干净的现场测试跑了 4,768 次轨迹运行,零丢失扫描。现场测试报告在仓库里。

挂起是数据完整性 bug,不是性能 bug。部分扫描看起来仍然像数据,这就是陷阱。


这七个的共同点

回头看。这些里只有一个实际上是模型问题,而且就连那一个也是在评分器里修复的,不是在提示词里。另外六个都是测量 bug:一个没有断言的套件、一个说谎的分母、一个我误判的层、一个跨模型共享的分类器、无法失败的 mock,以及一个丢数据的 runner。

我会这样重新表述这个教训:奖励函数几乎总是真正的 bug。你可以永远调模型,数字也不会动,因为那个数字从一开始就不是模型。

我仍然无法回答的开放问题:你怎么知道你的 eval 是诚实的?我现在能说出这七个错误。我无法完全相信我已经抓住了第八个。一个带有隐藏测量 bug 的绿色套件和一个带有真实 bug 的红色套件,从命令行看是一模一样的。我认为目前还没有一个干净的测试能测出这个,我宁愿把这话说出来,也不愿假装我已经解决了它。

这些里有哪些是你在它教会你之前就已经上线的?我承认全部七个。说说你的。

代码和凭证:agent-eval-forge·CauterRule·planner-critic-engine——全部 MIT,全部公开。