Skip to content
Published on

评估驱动开发里,最先要校准的是评判者

分享
Authors

评判者的分数涨了,客服工单也跟着涨了

把提示词改了一次,内部评测面板上的忠实度分数从 0.82 涨到 0.90。上线了。第二周,转人工的工单变多了。打开日志一看,回答更长、更客气,也更自信地错着。

这种时候多数团队会得出「评测做得不够」的结论,然后把评测集做大。100 条扩到 500 条,五个维度拆成二十个。但真正的原因不是评测集的大小。是那个被交去打分的模型在给又长又客气的回答加分,而从来没有人核实过这件事。把评测集扩大五倍,只会把那份偏差量得精确五倍。

评估驱动开发与 TDD 的决定性分歧点

Airbnb 工程团队在 2026 年 7 月 28 日发布的 Eval-driven development 主张,评估应当被当作一等的工程学科,而不是附带工作。到这里为止,口号和 TDD 是一样的。

不同之处在于裁判。TDD 的裁判是 assert,这个裁判不会错。评估驱动开发的裁判有相当一部分是 LLM,而这个裁判会错。而且不是随机地错,是沿着特定方向一贯地错:长度、格式、自信程度、对同门模型的偏好。

于是流程多出一步。先写测试还不够,必须先确认这个测试是不是在用一把直的尺子量

评判者不是代码,而是一个上线的模型

接受这个视角之后,对待评判者的方式就变了。评判者提示词不是配置文件,而是一件交付物。它需要版本号,改动时需要接受回归检查,用了哪个模型、什么温度跑的,都要和结果一起存下来。

Airbnb 那篇文章提出的三层结构在这里才有了意义。确定性的代码检查是第一层,评判者模型是第二层,人是第三层。关键不在于分了层这件事本身,而在于第三层持续审计第二层这个关系。人不会去看所有输出,而是去看评判者与人之间偏离了多少。

第一层放什么同样重要。JSON schema 违规、禁用词、没有出处的断言、响应长度上限,凡是能用规则表达的都应该在第一层拦掉。这不是成本问题,而是信任问题。把规则能抓住的失败交给评判者,评判者一旦判错,那个失败就通过了。把能确定性判定的事交给一个概率性的判定者,永远是亏本买卖。

用 50 到 100 条黄金集校准评判者的流程

Airbnb 的文章把评判者校准归纳为五步。先造黄金集,再让评判者在上面跑一遍,量一致度,把不一致的地方拆开看,然后定期重新校准。推荐的规模是「50 到 100 条黄金集」,标注则建议从「由主题专家标注的 20 到 100 行」开始。

这些数字看起来偏小是有原因的。黄金集不是量模型性能的尺子,而是量评判者性能的尺子。检验一把尺子并不需要很多样本,需要的是里面必须掺着失败案例。在只收集了好答案的黄金集上,一个什么都抓不住的评判者也能拿满分。

同一篇文章的真实案例里,最初做出来的忠实度评判者与人的一致度是 78%。把评分标准打磨一遍、加进几个示例之后升到 88%,这才被投入到大规模运行中。

为什么不能照单全收「一致率 88%」这个数字

这里还需要再进一步。简单一致率会在标签分布倾斜时被虚高。在一场 90% 都合格的评测里,一个无条件盖「合格」章的评判者同样能拿到 90% 的一致率。所以必须看剔除了偶然一致之后的指标,Airbnb 的文章也明确说要用 Cohen kappa 或 Krippendorff alpha。

"""评判者校准:不要只看一致率,要连 kappa 一起看。"""
from collections import Counter
from typing import Sequence


def cohens_kappa(human: Sequence[str], judge: Sequence[str]) -> float:
    assert len(human) == len(judge) and human, "需要成对的标签"
    n = len(human)
    po = sum(h == j for h, j in zip(human, judge)) / n  # 观测一致率
    hc, jc = Counter(human), Counter(judge)
    pe = sum((hc[k] / n) * (jc[k] / n) for k in set(hc) | set(jc))  # 偶然一致率
    return 1.0 if pe == 1 else (po - pe) / (1 - pe)


def calibration_report(rows):
    """rows: [(input_id, human_label, judge_label), ...]"""
    human = [r[1] for r in rows]
    judge = [r[2] for r in rows]
    k = cohens_kappa(human, judge)
    po = sum(h == j for h, j in zip(human, judge)) / len(rows)
    # 只有连不一致的方向一起看,才抓得住偏差
    lenient = sum(h == "fail" and j == "pass" for h, j in zip(human, judge))
    strict = sum(h == "pass" and j == "fail" for h, j in zip(human, judge))
    return {
        "agreement": round(po, 3),
        "kappa": round(k, 3),
        "judge_too_lenient": lenient,   # 漏掉的失败 —— 代价最高的错误
        "judge_too_strict": strict,
        "verdict": "ship" if k >= 0.6 and lenient == 0 else "recalibrate",
    }


rows = (
    [(f"g{i}", "pass", "pass") for i in range(80)]
    + [(f"b{i}", "fail", "pass") for i in range(8)]   # 评判者漏掉的失败
    + [(f"c{i}", "fail", "fail") for i in range(12)]
)
print(calibration_report(rows))
# {'agreement': 0.92, 'kappa': 0.694, 'judge_too_lenient': 8, ..., 'verdict': 'recalibrate'}

一致率高达 92% 却判定为需要重新校准,原因就在最后一行。人认定为失败的 20 条里,评判者放过了 8 条。把这个评判者原样接进 CI,团队从此就永远看不到那 8 条所代表的那类失败了。

3 到 5 个评判者好过 20 到 30 个

照搬 Airbnb 文章的说法:3 到 5 个校准良好的评判者,胜过 20 到 30 个夹杂噪声的评判者。这句话与其读成「把评测维度减少」,不如读成「只造你负担得起校准成本的那些维度」才更准确。

维护一个评判者的成本,不是一坨提示词。50 到 100 条黄金集、标注上投入的专家时间、失败类型一变就要做的重新校准,以及评判者提示词变动时用来确认回归的流水线,这些合起来才是一整套。负担不起这一套的维度,不如别造。因为未经校准的评判者不只是有不如无,它是一个指错方向的指南针。

智能体没法只靠最终答案来打分

一进到智能体,轴又多了一根。同一篇文章把智能体评估归纳为横跨三个维度:最终输出、中间推理步骤,以及工具调用与参数。

原因很简单。最终答案对了这件事,说明不了这次运行能否复现。一次搜索空转了三轮、碰巧撞上缓存答案的运行,和一次在首个调用里就用正确参数调了正确工具的运行,会拿到一样的分数,却是完全不同的两套系统。前者下周就会垮。

实务里的做法是把工具调用的名称与参数归一化后存成轨迹,并至少分开统计三项:多余的工具调用次数、因参数错误导致的重试次数,以及同样的输入再喂一次时轨迹变化了多少。第三项尤其重要。如果同一个问题每次跑出来的轨迹差别都很大,那这个智能体还处在不知道该怎么做的状态,眼下的成功率也没有任何理由在下次上线后依然成立。

用评判者给轨迹打分时,评分标准要按流程写,而不是按结果写。不是问「这是不是一个好答案」,而是问「在这一步调这个工具有没有必要」。流程类的问题答案短,依据又都留在日志里,所以同一个评判者在流程问题上与人的一致度会明显高于结果问题。

什么该做成门禁,什么只放在看板上

最后是与发布的衔接。把所有指标都做成 CI 门禁,流水线会长期红灯;一个都不做,评测就成了装饰。这条分界线这样划比较实用。

性质例子放置方式
确定性的,违规即事故禁用表达、个人信息泄露、schema 违规CI 门禁,哪怕一条失败也拦下
基于评判者且已校准忠实度、指令遵循CI 门禁,按相对基线的下滑幅度判定
基于评判者但校准未完成语气、有用程度仅放看板观察,禁止拦截
对分布敏感响应长度、工具调用次数只报警,按趋势判断

另外必须持续从生产环境取样。Airbnb 的文章写明,他们每天对去标识化后的真实流量抽样 5%。评测集从造出来那天起就开始变旧,所以如果没有一条让新的失败类型流进黄金集的路径,几个月后你只会反复量那些老问题。

评估驱动开发的第一件事不是造评测集,而是用数字知道你现在用的这个评判者与人偏离了多少。不知道那个数字,看板上打出来的所有分数都还没有任何意义。

参考资料