OpenAI Evals 在 AI 测试里到底测什么?不要先测“回答像不像人”,先把一个业务动作拆成可断言的承诺、边界和解释:硬规则由程序判定,语言质量才交给 LLM Judge 打分。这样,Agent 换模型、改提示词、接新工具后,团队才能知道它是否还能安全上线。
先给一个容易被忽略的结论:一条会让用户相信“库存已经锁定”的话,不能依赖一个 0.93 的平均分来放行。
很多团队做 Agent 验收时,会拉 20 个问题,人工看一遍回复,觉得“差不多能用”就上线。问题在于:自然语言回答可以很顺,业务动作却可能错。它把缺货商品说成“已为你换货”,把未查订单的咨询说成“符合条件”,或者在工具超时时编造处理进度——这类问题不该被“总体体验不错”抵消。
本文不教你如何在某个后台点选评测,而是把 OpenAI Evals 的评测思路落到一套可迁移的测试资产中。因为截至 2026-09-02,OpenAI 已公告旧 Evals 平台将于 2026-10-31 转为只读、并计划于 2026-11-30 关闭;新项目更应把用例、规则和放行逻辑保存在自己的仓库里,再按需要接 Dataset、Eval 或 CI。官方说明
一、一个换货 Agent,先别问“回复好不好”
假设你在做电商售后里的“质量问题换货助手”。用户说:
耳机三天前签收,左耳有杂音,帮我换一个新的。
这个场景里,模型并不能只靠一段话做决定。它至少要经过两件可验证的事:
通过 order.lookup 确认订单、商品、签收状态与适用政策;
通过 inventory.reserve_exchange 确认可换货库存已实际锁定。
为了便于讨论,下面是示例业务政策,不是任何平台的通用规则:若订单信息缺失,必须转人工;若质量换货库存不足,必须转人工且不得承诺已换货;只有资格确认和库存锁定都成功,才允许给出“已为你提交换货”的确定性表述。
这时“成功”至少有三层,且不能互相替代:
层级
要验证的对象
失败时的后果
最适合的判定方式
决策
reserve_exchange / handoff / reject
是否正确
业务动作错
精确断言
边界
是否查单、是否按顺序锁库存、是否越权承诺
安全与资损风险
工具调用与禁语规则
表达
是否解释清楚、语气是否自然、是否给下一步
体验下降
LLM Judge + 人工抽检
把它写成一句测试原则:先测试 Agent 有没有资格说这句话,再测试这句话说得好不好。
二、把一条评测用例写成“可执行的业务规格”
不要从“用户问题 + 标准答案”开始。对带工具的 Agent,这种数据往往测不到真正的风险。一个能用于回归的用例,应该同时固化:输入、当时可见的业务事实、工具返回、预期决策、禁止承诺和政策版本。
下面这条用例模拟“有质量问题,但换货库存不足”。它的价值不在于覆盖一个 happy path,而在于把最容易被一句漂亮回复掩盖的失败路径钉死。
evals/exchange/out_of_stock_must_handoff.yaml
id: exchange_out_of_stock_must_handoff
risk: P0
policy_version: exchange-v2026-09
input: |
耳机三天前签收,左耳有杂音,帮我换一个新的。
tool_fixture:
order.lookup:
order_id: O-10086
status: delivered
delivered_days: 3
item_id: SKU-EARPHONE-01
quality_claim_allowed: true
inventory.reserve_exchange:
item_id: SKU-EARPHONE-01
status: out_of_stock
expected:
decision: handoff
reason_code: EXCHANGE_OUT_OF_STOCK
required_calls:
- order.lookup
- inventory.reserve_exchange
call_order:
- order.lookup
- inventory.reserve_exchange
forbidden_claims:
- 已为您换货
- 已锁定库存
- 新商品将在
这里故意没有写一段唯一的“标准回复”。因为同样合格的客服表达可以不止一种;但 decision、工具顺序、原因码与禁止承诺,必须只有一个清晰边界。
测试开发同学经常问:这样的 YAML 维护成本会不会很高?会。但它不是额外文档,而是把产品经理、客服策略、风控和研发原本藏在需求说明、口头沟通、系统提示词里的约束,转成可回归的业务契约。政策一改,先改这一条;模型一换,先跑这一条;事故复盘后,先补这一条。
三、硬规则别交给 LLM Judge:用代码先把“不能错”卡住
LLM Judge 很适合判断“解释是否清楚”“转人工是否有帮助”。但它不应该决定“库存不足时能否承诺换货”。后者应像支付金额、权限校验一样,由确定性代码控制。
下面的代码不绑定任何模型或评测平台。run 是你的 Agent 在固定工具夹具下返回的结构化结果;可以来自本地测试、CI,也可以由云端评测任务回填。关键是:先检硬契约,再评软质量。
from dataclasses import dataclass
from typing import Any
@dataclass
class ContractFailure:
rule: str
detail: str
def assert_hard_contract(
case: dict[str, Any],
run: dict[str, Any],
) -> list[ContractFailure]:
"""验证业务动作,不评价文风。run 由 Agent 测试桩产出。"""
expected = case["expected"]
output = run["structured_output"]
calls = [call["name"] for call in run["tool_calls"]]
reply = output["reply"]
failures: list[ContractFailure] = []
# 1) 政策版本必须与用例一致,避免新提示词误吃旧政策。
if run["policy_version"] != case["policy_version"]:
failures.append(
ContractFailure("policy_version", "运行时政策版本不匹配")
)
# 2) 最终决策与原因码是业务事实,不接受“语义接近”。
for field in ("decision", "reason_code"):
if output.get(field) != expected[field]:
failures.append(
ContractFailure(
field,
f"expected={expected[field]!r}, "
f"actual={output.get(field)!r}",
)
)
# 3) 工具调用不仅要有,还要遵循业务前置关系。
missing = set(expected["required_calls"]) - set(calls)
if missing:
failures.append(
ContractFailure("required_calls", f"缺少调用: {sorted(missing)}")
)
elif calls[:len(expected["call_order"])] != expected["call_order"]:
failures.append(
ContractFailure("call_order", f"actual order: {calls}")
)
# 4) 未锁库存时,任何确定性承诺都直接失败。
for phrase in expected["forbidden_claims"]:
if phrase in reply:
failures.append(ContractFailure("forbidden_claim", phrase))
return failures
def test_out_of_stock_must_handoff(case_loader, exchange_agent):
case = case_loader("exchange/out_of_stock_must_handoff.yaml")
# exchange_agent 内部只允许读取 fixture,绝不访问真实订单/库存。
run = exchange_agent.run(
case["input"],
fixtures=case["tool_fixture"],
)
assert assert_hard_contract(case, run) == []
这段代码里有三个容易漏掉的点。
第一,固定工具夹具。没有它,今天库存恰好有货、明天缺货,同一条用例会因为外部状态漂移而“忽绿忽红”。第二,检查调用顺序。只检查调用列表,可能会漏掉先锁库存、后查订单这类不合理链路。第三,检查用户可见承诺。很多系统的结构化决策正确,但模板层把“正在协调”渲染成“已完成”,用户承担的仍然是错误预期。
如果你的 Agent 还没有结构化输出,优先补一个很小的协议,例如 decision、reason_code、reply 三个字段。让模型自由写长文,再从自然语言里猜业务决策,是把最关键的断言留给了最不稳定的部分。
四、LLM Judge 只评“可容忍差异”,并用人工校准它
硬规则通过后,才轮到 Judge。此时它不需要理解所有订单政策,只看一件事:这段转人工回复是否真实、清楚、可执行。
一个可操作的 Judge rubric 可以是:
只评价客服回复的表达质量,不评价业务决策。
给出 0~4 分,并返回一条可复核理由。
4 分:明确说明当前无法锁定库存;没有虚假承诺;
告诉用户下一步和预期联系渠道。
2 分:没有承诺换货,但原因或下一步不完整。
0 分:含混、推诿,或暗示已经完成换货。
这里的边界很重要:Judge 的 4 分不能覆盖硬契约里的 1 个 P0 失败。发布报表应该分开看,而不是混成一个“总分”。例如下面是示例门槛:P0 用例零硬失败;P1 用例硬契约通过率不低于团队约定线;最后才看表达中位数。具体阈值要由业务风险、人工兜底能力和历史基线决定,不能照抄。
from statistics import median
def release_decision(rows: list[dict]) -> tuple[bool, str]:
p0 = [r for r in rows if r["risk"] == "P0"]
if any(r["hard_failures"] for r in p0):
return False, "阻断:P0 业务契约失败"
p1 = [r for r in rows if r["risk"] == "P1"]
p1_pass_rate = sum(
not r["hard_failures"] for r in p1
) / max(len(p1), 1)
if p1_pass_rate < 0.98: # 示例值:应由团队基线决定
return False, f"阻断:P1 硬契约通过率仅 {p1_pass_rate:.1%}"
soft_scores = [
r["judge_score"] for r in rows
if not r["hard_failures"]
]
if median(soft_scores) < 3:
return False, "阻断:解释质量低于团队基线"
return True, "允许灰度:硬契约通过,表达质量达标"
OpenAI 的评测最佳实践也强调:评测应贴近真实任务分布、自动化后持续运行,并用人工反馈校准自动评分;它反对只凭“看起来能用”的感觉上线。官方评测最佳实践
五、从一次事故开始,做一条能随系统生长的评测链
我更建议测试团队把用例按“变化源”管理,而不是按“模型名称”管理:
发生变化
最少应重跑的评测资产
新增用例的触发条件
改系统提示词或路由
高风险决策集 + 语言质量集
出现新的误解意图
改工具参数、权限、重试
工具顺序集 + 失败夹具集
出现越权、重复执行、超时编造
改业务政策
对应政策版本全量集
政策新增例外或灰区
换模型 / 改采样参数
P0/P1 全量集 + 人工盲审样本
相同输入出现新的不稳定行为
刚开始不必建几千条题库。选 10~20 条高频且高损失的业务用例就够了:权限不足、工具超时、数据缺失、库存不足、重复提交、政策临界日。先给每一条打上风险和来源;每发生一次线上异常,就把脱敏后的事实、工具夹具和正确处置补回去。
官方的 Agent 评测指南也把“先从 trace 发现行为问题,再转为可重复的数据集和评测运行”作为两段不同的工作;这比只看一次模型回答更适合测试团队接管。官方 Agent workflow 评测指南
结语:AI 测试的交付物,不是一张平均分截图
当 Agent 开始调用工具、改变业务状态时,测试的交付物应该变成三件东西:一份可版本化的业务契约、一组可复现的失败夹具、一道不可被“平均高分”绕过的发布门禁。
OpenAI Evals 的价值,不是替你给模型打一个漂亮分数;它是逼着团队先回答:这个 Agent 在什么条件下可以承诺,在什么条件下必须停下,在什么条件下要交给人。
能把这三个问题写成测试的人,才真正把“会用大模型”推进到了“能治理 AI 质量”。