评估 AI 智能体:如何构建 evals 并衡量可靠性,而不是凭感觉
AI 智能体评估是指:对一组固定的真实任务定期运行,通过自动检查来判定结果并统计指标,而不是依赖几次成功会话的印象。关键在于不仅要衡量 pass@k(k 次尝试中至少一次成功),还要衡量 pass^k(k 次尝试全部成功),因为稳定性才决定能否放心让智能体自主工作。
既然看得出智能体在工作,为什么还要评估
恰恰是“看得出”最不可靠。智能体一周都没有在修改提示词后卡住——这是修好了,还是这周只做了简单任务?一位工程师确信新模板改变了一切,另一位看的是同一份日志,却没发现任何差别。两人的判断都只在没有计算的范围内成立。
评估(evals)用数字代替感觉。你选定一组固定的任务,运行智能体,自动检查结果,并比较修改前后的指标。这样,“是否变好了”的争论就由表格来决定,而不是由权威来决定。
没有评估,对 Harness 的任何改动——提示词、AGENTS.md、工具集、模型——都是盲目的赌注。有了评估,它就变成一次有可测量结果的实验。
为什么“感觉”会骗人
对智能体工作的印象存在三种系统性误差,它们都会导向错误的确信。
- 幸存者偏差。成功的运行会被记住并展示给同事,失败的运行则被重跑、被遗忘。只根据成功运行算出的可靠性,并不能说明智能体真正在哪里出错。
- 样本太小。即使对一个只有一半成功率的智能体,连续三次成功也很正常。概率是 0.5³ = 12.5%,也就是大约每八组里就有一组。
- 笃定的语气。智能体说“完成了,所有测试都通过”的语气,无论真假都一样笃定。模型的自信没有经过校准:回答的语气不是正确性的信号。
由此得出一条规则:凡是没有记录、没有重新计算的,都视为未测量。
pass@k 与 pass^k:容易混淆的两个指标
智能体具有非确定性:同一个任务重复运行,可能成功,也可能失败。因此,要对一个任务运行多次,并计算以下两个指标之一。
pass@k:k 次尝试中至少一次成功
这个指标来自代码生成的评估。它回答的问题是:“如果给系统多次尝试的机会,它原则上能否解决这个任务?”对于人从多个结果中挑选最好的一个,或者存在可靠的自动检查能够过滤掉失败结果的场景,这是一个诚实的指标。
pass^k:k 次尝试全部成功
这个指标回答的是另一个问题:“智能体解决这个任务的稳定性如何?”它描述的是无人值守的工作场景:智能体会运行很多次,每一次失败都要花费金钱或时间。
两者的差距非常大。假设智能体单次解决某任务的概率为 70%,且各次尝试相互独立:
| k | pass@k = 1 − 0.3^k | pass^k = 0.7^k |
|---|---|---|
| 1 | 70% | 70% |
| 3 | 97.3% | 34.3% |
| 5 | 99.8% | 16.8% |
| 10 | ~100% | 2.8% |
按 pass@5 看,智能体几乎完美;按 pass^5 看,它并不可靠。两个数字都是正确的,只是描述的事情不同。如果你打算让智能体自主工作,应该关注 pass^k,因为它更接近你在实际工作中会看到的情况。
实际计算方法
每个任务运行 n 次(例如 5 次),记录成功次数 c。对于 pass^k,近似做法是估计单次成功概率 p = c / n,然后取 p 的 k 次方。对于任务集,则对各任务取平均。
import json
from statistics import mean
# results.jsonl: {"task": "add-pagination", "runs": [true, true, false, true, true]}
rows = [json.loads(line) for line in open("results.jsonl")]
def pass_at_k(runs, k):
p = sum(runs) / len(runs)
return 1 - (1 - p) ** k
def pass_pow_k(runs, k):
p = sum(runs) / len(runs)
return p ** k
for k in (1, 3, 5):
print(k,
round(mean(pass_at_k(r["runs"], k) for r in rows), 3),
round(mean(pass_pow_k(r["runs"], k) for r in rows), 3))
当 n 较小时,用 p 的 k 次方来估计只是粗略的,但比较 Harness 的不同版本时,通常已经足够。更重要的是:比较之间,n 和任务集都不能改变。
如何搭建评估任务集
任务集是最重要的资产。在糟糕的任务上得到的好指标,衡量的是错误的东西。
使用你自己的真实任务
像 SWE-bench 这样的公开基准对于比较模型很有用,但它们的任务是经过筛选的:描述清楚,测试现成。在你的仓库里,需求往往模糊,有些地方甚至没有测试,所以基准上的分数几乎总是高于智能体在你这里的实际表现。最好的任务来源是已关闭的工单,以及日志中智能体真实失败的记录。
任务集的构成
- 起步 20–50 个任务。太少,噪声会吞掉版本之间的差异;太多,每次运行成本高昂,最终没人愿意运行。
- 类型多样:新功能、修复 bug、重构、修改他人模块的任务,以及带陷阱的任务(正确答案是“不能这样做”或“需要澄清”)。
- 每个在工作中失败的任务都变成一个测试。这样任务集会随着你对智能体弱点的了解而增长,回归问题也不会被悄悄放过。
- 固定的初始状态:确定的提交、锁定的依赖、一致的环境。否则你测量的是环境的漂移,而不是智能体。
任务的格式
每个任务包括:描述、初始状态,以及可以在无人参与下检验的成功标准。
id: add-pagination
repo_ref: 3f2c1ab
prompt: |
为 GET /payments 添加分页:参数 limit(默认 20,最大 100)
和 cursor。响应中包含 next_cursor。
check:
- pytest tests/test_payments_pagination.py
- mypy app/
limits:
max_changed_files: 6
注意,成功标准就是用命令写下的“完成定义”(Definition of Done)。如果你无法为某个任务写出成功标准,那就说明这个任务对智能体来说描述也不明确。
自动检查还是 LLM 评判
结果既可以用代码检查,也可以用另一个模型检查。两种方式各有适用范围。
| 自动检查(代码) | LLM 评判 | |
|---|---|---|
| 检查什么 | 测试、类型、linter、E2E、diff 大小、禁止修改的文件 | 是否符合需求、可读性、风格、解释的质量 |
| 确定性 | 完全确定 | 不确定:每次运行的评分都会波动 |
| 成本 | 几乎免费 | 每个任务多一次模型调用 |
| 主要风险 | 检查不全面:测试通过了,但功能不对 | 评判者会认可那些自信但错误的方案 |
规则:先用代码,评判者作为补充
凡是能够确定性检查的,都应确定性地检查。只有对代码无法表达的属性,才加入 LLM 评判,而且绝不能用它来替代测试。
如何让 LLM 评判者有用
- 与生成者使用不同的上下文。评判者不应看到智能体的推理过程,只应看到任务、标准和最终的 diff。否则它会重复同样的错误。这就是 N 版本思想:用两个独立的视角代替一个。
- 用评分细则代替“按十分制打分”。使用具体的是/否问题,例如“是否校验了 limit > 100”“是否修改了 app/payments 之外的文件”。
- 用人工标注进行校准。手动标注 20–30 个结果,与评判者的结论对比。如果一致率低,应该修改评分细则,而不是更加信任评判者。
- 错误的不对称性。错误地判定“完成”比错误地判定“未完成”代价更高:前者会流入生产环境,后者只需重跑一次。因此,要把评判者设置成在有疑问时拒绝。
除了成功率之外还应统计什么
任务成功率只是表层。要弄清楚到底该修哪里,还需要经典工程学中的可靠性指标。
- MTBF:智能体平均能完成多少步或多少任务才卡住。它说明智能体多久出一次问题。
- MTTR:故障之后恢复到可工作状态所需的时间。缩短 MTTR 几乎总是比提高 MTBF 更便宜:良好的进度文件和可复现的环境能把恢复时间缩短数倍。
- 影响半径:一个失败的步骤会破坏多少代码。可以通过 diff 大小限制和工作副本隔离来约束。
- 误报“完成”的比例:智能体宣称成功而检查却失败的任务。这是最危险的指标:它直接说明智能体的报告能不能相信。
- 成本与时间:每个任务消耗的 token 和分钟数。如果一个 Harness 把成功率提高了 5%,却让成本翻倍,它未必是赢家。
失败日志:来自日常工作的评估
并不是每个团队都能马上搭建一个运行器。最小化的评估方案是一份三列的失败日志。
| 任务 | 结果 | 层 |
|-------------------------|------------------------------------|--------------|
| /payments 分页 | “完成”,2 个测试失败 | 验证 |
| 修复 webhook 超时 | 30 分钟花在 pip install 上 | 环境 |
| 重构 refunds | 使用了旧的 ORM 语法 | 上下文 |
“层”指智能体可能出错的五个位置之一:任务规格、上下文、执行环境、验证反馈、状态管理。记录二十条之后,真正的瓶颈就会显现出来,而且几乎从来不是你原先以为的那个地方。日志中的每一行,都是评估任务集的候选。
为什么强大的模型也救不了你
对智能体失败的第一反应,往往是换一个更贵的模型。有时确实有用,但更常见的情况是:弥合基准与现实之间差距的,不是模型,而是它周围的环境。
如果智能体花半小时搭建环境,那是执行环境层的问题,旗舰模型同样会花半小时。如果测试失败时它仍然说“完成”,那是验证层的问题:它没有一个能说出真相的命令。如果它的代码风格与项目不符,那是因为没有给它项目的约定。这些问题都与模型的智力无关。
评估能让你诚实地检验这一点。用当前模型和更贵的模型各跑一遍任务集;再用当前模型,分别跑一遍有和没有带验证命令的 AGENTS.md。通常,后一种改动带来的提升比前一种更大,而且零成本。但不要只凭这句话就相信它:正是为了这类比较,才需要任务集。
分步计划:一周内完成智能体评估
- 第 1 天。建立失败日志,每次智能体出错都记录下来,并标明所在的层。
- 第 2 天。从已关闭的工单和日志中挑选 20 个任务。为每个任务固定提交,并用命令写下成功标准。
- 第 3 天。编写运行器:干净的工作副本,用任务描述启动智能体,运行检查,把结果写入 JSONL。
- 第 4 天。每个任务运行 3–5 次。计算 pass@1、pass^3、误报“完成”的比例以及平均成本。
- 第 5 天。在 Harness 中只改动一样东西,然后重新运行。只有在任务集相同、重复次数相同的情况下,结果才可比较。
每次只改变一个变量。如果在同一次实验中同时更换模型、提示词和工具,你将无法知道究竟是哪一项起了作用。
评估智能体时的常见错误
- 每个任务只运行一次。智能体的非确定性会让这样的结果变成抽签。至少重复三次。
- 比较之间任务集发生了变化。同时添加新任务并修改提示词,得到的数字就无法比较。
- 智能体能看到验证测试。如果测试文件放在仓库里,智能体可能会让代码去迎合测试,或者直接修改测试。应把隐藏的检查放在工作副本之外,并检查智能体是否改动了测试文件。
- 用智能体的报告代替验证。模型回复中的“所有测试都通过”不是结果。结果是运行器执行命令后的返回码。
- 只看 pass@k。它在演示中是漂亮的数字,却是无人值守工作的糟糕预测。
总结
评估 AI 智能体,就是对一组固定的真实任务进行自动检查,并在每次修改 Harness 后重新计算指标。pass@k 说明智能体能做到什么,pass^k 说明能否放心让它自主工作。自动检查提供事实;带有评分细则、独立上下文的 LLM 评判者则在代码无能为力的地方进行补充。按五个层分类的失败日志告诉你首先该修什么,而通常那不是模型。可靠性指标和诊断循环在课程的实际项目中有详细讲解。
常见问题
智能体评估需要多少个任务?
起步通常 20–50 个真实任务就够了,每个任务有自动化的成功标准,并重复运行 3–5 次。任务太少,无法区分改进与噪声;任务太多,成本高到没人愿意运行。
能否只用公开基准来评估智能体?
不能。公开基准比较的是模型在经过筛选的任务上的表现,这些任务有现成的测试;而你仓库中的需求和测试完全不同。要就自己的 Harness 做决策,需要自己的任务集。
pass@1 与 pass^1 有什么区别?
没有区别:只尝试一次时,两个指标都等于成功运行的比例。它们从 k 大于 1 开始才会出现差异。
LLM 评判者能否与智能体使用同一个模型?
可以,前提是它的上下文是独立的:它只看到任务、评分细则和最终的 diff,看不到智能体的推理。但它的结论仍然需要与人工标注进行核对。
应该多久运行一次评估?
每次 Harness 发生明显改动后都应运行:提示词、说明文件、工具集、模型都算。也可以定期按计划运行任务集,以便发现环境和依赖的漂移。
课程“Harness Engineering: инженерия надёжности AI-агентов”结合实践详细讲解了这一主题。
- Метрики надёжности AI-агента: MTBF, MTTR, радиус поражения и закон Литтла
- Почему AI-агент ломается в проде: пять слоёв отказа и диагностическая петля