强调untested eval是生产灾难;eval规则的关键不是代码本身,而是必须在已知坏trace上验证它能否检出问题,否则只是运气。
你写了一个新的 eval。它捕获了你刚在生产环境看到的故障。你把它部署为一道门控。恭喜——现在你有一段未测试的代码坐在每次 agent 运行的热路径上,决定什么能发布什么会被阻止。
我们对待 agent eval 就像写它是困难的部分。其实不是。困难的部分是知道你的 eval 真的有区分能力:它在坏的 trace 上亮红,在好的 trace 上亮绿,而不是反过来。一个没有在已标注 trace 上运行过的 eval,就是带着仪表盘的硬币翻转。
这是隐而不宣的灾难。你加了一条规则来捕捉幻觉文件路径。它有个正则表达式的 bug。它什么都匹配不上。每次运行都通过。你的仪表盘是绿的。你感觉很安全。三周后一个客户找到了你的"门控"本该阻止的完全相同的故障,然后你发现这条规则从没触发过一次。
一个绿色的 eval 不是 agent 健康的证据。它是证据表明:要么 agent 是健康的,要么你的 eval 坏了——除非你已经用一条你知道是坏的 trace 来喂它并看它变红,否则你无法分辨。
Eval 是代码。守卫生产环境的代码必须针对测试夹具进行测试。不知怎的,eval 免除了这个责任,这是我见过的最大的虚假信心来源。
这重要的原因与你应该如何优先排序 eval 证据密切相关。不是按成本轴——便宜到昂贵是错的思维模型。应该按独立性轴排序:agent 生成的输出能多容易地伪造这个信号?
Tier 1 —— 外部可观察的证明,agent 无法伪造。有效的 JSON、文件存在于磁盘、编译成功、测试通过、在超时前完成、输出非空。基本真理,无主观成分。
Tier 2 —— 针对 agent 没有编写过的基线的统计信号。输出与任务规范的嵌入相似度、长度和重复检查、diff 是否真的改变了什么。
Tier 3 —— 模型作为评判者。共享基质的观点。一个信号,永远不是判决。
这个排序正是让你的 eval 套件可测试的原因。Tier 1 和 Tier 2 是确定性的,运行成本大约为零,速度很快——这就是为什么它们是你的实时门控:它们可以坐在热路径上阻止一次运行。因为它们是确定性的,你可以把它们钉死在一个固定的 trace 语料库上,每次都得到相同的答案。这就是"测试你的 eval"的意义所在。
Tier 3 做不到。它是非确定性的、计量的、速度慢的,所以它只能离线——它不能活在热路径上,而且不能用相同的清晰方式进行回归测试,因为它不会给你稳定的答案两次。还有更深层的问题:一个模型评判另一个模型的推理是循环的。评判者和被评判者共享一个基质;没有独立的基本真理。所以 Tier 3 只能检查被评判 agent 没有被允许写的工件,即便如此也是"观点,不是证据"。你不能在它上面设门控,也不能假装你可以把它单元测试到可靠性。
实际的结果:发布那 80%。大多数真实故障——陈旧的输出、崩溃、格式错误、幻觉路径、空结果——都被 Tier 1+2 单独捕获,确定性地、免费地。把评判者保留给约 20% 的主观尾部,并大声标记为观点。正是 Tier 1+2 规则——确定性的那些——你可以而且必须在信任前测试。
这个招数很无聊,但行之有效:保持一个标注 trace 的语料库,并断言每个 eval 产生你期望的标签。新规则在它对已知坏的变红、对已知好的变绿之前不能发布。
type Verdict = "pass" | "fail";
interface Trace {
id: string;
output: string;
claimedPaths: string[];
existsOnDisk: (p: string) => boolean;
}
// A Tier 1 eval: no hallucinated file paths. Deterministic, ~$0.
function noHallucinatedPaths(t: Trace): Verdict {
return t.claimedPaths.every((p) => t.existsOnDisk(p)) ? "pass" : "fail";
}
// The fixtures are the point. Each is a trace you ALREADY labeled.
const fixtures: { trace: Trace; expected: Verdict }[] = [
{
trace: {
id: "good-1",
output: "wrote src/index.ts",
claimedPaths: ["src/index.ts"],
existsOnDisk: () => true,
},
expected: "pass",
},
{
trace: {
id: "bad-1",
output: "edited src/does-not-exist.ts",
claimedPaths: ["src/does-not-exist.ts"],
existsOnDisk: () => false,
},
expected: "fail", // if this comes back "pass", your GATE is broken
},
];
for (const { trace, expected } of fixtures) {
const got = noHallucinatedPaths(trace);
if (got !== expected) {
throw new Error(
`Eval regression on ${trace.id}: expected ${expected}, got ${got}`,
);
}
}
bad-1 这个 fixture 是整个游戏。没有它,一个正则表达式的打字错误或一个反转的条件会悄悄发布,你的门控变成装饰。有了它,一个坏的 eval 会在你的 CI 中失败,而不是在你的客户那里失败。
以上一切都假设你有真实的 trace,带着真实的、无法伪造的输入来评分——这正是大多数团队失败的地方。如果你的"trace"是 agent 关于自己运行的总结,你已经把分数学生给了答案钥匙。
这就是为什么这两个部分作为一个单元发布。agent-eval 评分并控制 agent 的输出——这是 tier 学说活着的地方,是漂移和幻觉检查运行的地方,是红色门控阻止运行的地方。但它只能评分它能看到的东西。AgentLens 捕捉 agent 如何到达那里的 trace:每个模型和工具步骤、解析的输入、原始输出,都不是在事后由 agent 编写的来看起来好看。那个 trace 数据就是给 Tier 1+2 提供无法伪造的东西来评分的——而且这就是让你能首先构建标注语料库的原因,因为你在挖掘真实运行而不是从想象中发明 fixture。
agent-eval 告诉你运行失败了。AgentLens 让你重新打开它,看到它出了问题的确切步骤,标注它,把它放进你的 fixture 集,这样漏掉它的 eval 永远不能再漏掉它。一个捕捉 trace;另一个评分。都一个人都无用。
写在墙上:没有 eval 在没有红色 fixture 的情况下到达门控。如果你不能生成一条让规则失败的 trace,你没有写一个 eval——你写了一个注释,碰巧返回"pass"。
在信任你的 eval 来守卫任何东西之前,针对已知坏的 trace 测试它们。确定性的 Tier 1+2 规则让那个测试成为可能;捕捉真实 trace 所以 fixture 是真实的;保持评判者离线和诚实关于是一个观点。那就是门控和没人检查过的绿灯之间的区别。