22
22 / 50

LLM 评测与质量监控:可运行的回归检查

⏱️ 45分钟

一个 RAG 客服把退款期限从 14 天答成 30 天,即使 JSON 格式正确、引用 ID 也存在,仍然是错误答案。评测需要把格式、事实值、引用和拒答行为分别检查,才能知道改动到底破坏了什么。

本章用一个可离线运行的 Python 示例比较两个版本,输出逐条失败原因,并用退出码阻止回归。退款政策、输入和输出全部是教学用合成数据,不代表 JR Academy 的退款规则,也不是任何模型的实测成绩。 示例不调用模型、不联网、不需要 API key。

1. 先定义可判定的任务

假设应用只做一件事:从已提供的政策中提取退款天数,返回 refund_dayscitationsabstain 三个字段。没有政策时,产品约定返回空值并弃答;这个约定仅适用于本例的“必须依据所给政策回答”任务,并非所有聊天应用都应在没有检索结果时拒答。

用例人工标注的预期要拦截的错误
answerpolicy-v1:14 天引用正确但数值写成 30
changed-policypolicy-v2:7 天数值正确但引用了不存在的文档
no-context空值、无引用、弃答没有依据仍给出退款天数

这三个用例只用于演示。真实评测集还要覆盖你的用户任务、语言、文档版本、冲突来源和线上失败案例;不能凭“达到 50 个样本”认定覆盖足够。保留稳定的回归集,新增挑战集,并留出未参与 prompt 调整的样本,避免只记住测试答案。

2. 运行一个会失败的回归检查

把下面代码保存为 eval_demo.py。需要 Python 3.8 或更高版本,只使用标准库。BASELINECANDIDATE 是手写的固定输出;它们用于验证评分器和发布门禁,不用于评估模型能力。

import argparse
import json
import sys

# Synthetic fixtures: these are NOT model responses or a real refund policy.
CASES = [
    {"id": "answer", "days": 14, "source": "policy-v1"},
    {"id": "changed-policy", "days": 7, "source": "policy-v2"},
    {"id": "no-context", "days": None, "source": None},
]
BASELINE = {
    "answer": {"refund_days": 14, "citations": ["policy-v1"], "abstain": False},
    "changed-policy": {"refund_days": 7, "citations": ["policy-v2"], "abstain": False},
    "no-context": {"refund_days": None, "citations": [], "abstain": True},
}
CANDIDATE = {
    "answer": {"refund_days": 30, "citations": ["policy-v1"], "abstain": False},
    "changed-policy": {"refund_days": 7, "citations": ["missing-doc"], "abstain": False},
    "no-context": {"refund_days": 14, "citations": [], "abstain": False},
}


def grade(case, output):
    required = {"refund_days", "citations", "abstain"}
    if not isinstance(output, dict) or set(output) != required:
        return ["schema"]
    days = output["refund_days"]
    citations = output["citations"]
    if (days is not None and type(days) is not int) or (
        not isinstance(citations, list)
        or any(not isinstance(item, str) for item in citations)
        or type(output["abstain"]) is not bool
    ):
        return ["schema"]
    errors = []
    if days != case["days"]:
        errors.append("value")
    expected_citations = [] if case["source"] is None else [case["source"]]
    if citations != expected_citations:
        errors.append("citations")
    if output["abstain"] != (case["days"] is None):
        errors.append("abstention")
    return errors


def evaluate(outputs):
    return {case["id"]: grade(case, outputs.get(case["id"])) for case in CASES}


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--candidate", choices=["broken", "fixed"], default="broken")
    args = parser.parse_args()
    baseline = evaluate(BASELINE)
    candidate = evaluate(BASELINE if args.candidate == "fixed" else CANDIDATE)
    regressions = [key for key in baseline if not baseline[key] and candidate[key]]
    print(json.dumps({"baseline": baseline, "candidate": candidate,
                      "regressions": regressions}, indent=2))
    # This demo gate protects known passing cases; it is not a safety certification.
    return 1 if regressions else 0


if __name__ == "__main__":
    sys.exit(main())

运行故意带错的版本:

python3 eval_demo.py --candidate broken
echo $?

在 macOS 或 Linux shell 中,最后一行应为 1。JSON 报告中 baseline 三项均为 [];candidate 的 answer["value"]changed-policy["citations"]no-context["value", "abstention"]regressions 包含全部三个 ID。

再验证成功路径:

python3 eval_demo.py --candidate fixed
echo $?

此时 regressions[],退出码为 0fixed 直接复用已知正确的 fixture,并没有修复或调用任何模型。把失败示例放进 CI 时,非零退出码是预期行为;接入真实候选输出后,才能让该步骤判断真实改动。

3. 评分器也会漏判和误判

grade() 拒绝缺字段、额外字段和错误类型;特别用 type(days) is int 避免 Python 把 True 当整数接受。引用检查只允许本用例指定的文档 ID,因此能发现不存在的来源。

引用 ID 存在不等于引用支持了回答。本例因答案是确定的整数,可以直接和人工标签比对。对于自由文本,需要逐条核对回答中的事实与来源;不能用“出现了引用”代替事实一致性检查。多个有效来源、合理的措辞变化或更复杂的 JSON 类型,都需要重新定义评分规则,而不是照搬这里的精确相等比较。

三个用例全过,也不能证明提示注入防护、隐私保护或生产可靠性。接入模型时,把异常、超时、缺失输出也记录成可见失败;保存实际输出与版本信息,不要用空字符串掩盖错误。重复运行有随机性的任务,并比较同一批输入下的基线与候选结果。

4. 需要 LLM judge 时,先验证它会不会判

格式检查交给代码;开放式回答的事实支持度或任务完成情况,可以增加人工审核或模型评分器。用同一组人工标注案例比较 judge 的判断,分别看“把错误放过”和“把正确拦下”的情况。换 judge 模型或 rubric 后重新校准,保留版本;不要为了日历上的月度任务随意更换评分标准。OpenAI 的评测指南说明了人工校准、明确 rubric 和模型评分偏差。

例如,事实支持度可以定义为:supported(每条事实都有依据)、unsupported(至少一条事实不被来源支持)、uncertain(来源不足或冲突,交给人工)。要求返回对应的回答片段和来源片段,并验证这些片段确实出现在输入中。被评答案和检索文本都是待检查的数据,不能让其中的指令覆盖评分规则。

不要把任意的“4/5”分数直接视为通用发布标准。先定义哪些错误不可接受,再用历史基线、人工判断和任务风险设置门槛。价格较低的 judge 是否可用,也要看它与标注的一致性,而不是只比较模型价格。Anthropic 的评测工程文章讨论了代码、模型和人工评分器各自的限制。

5. 从离线报告接到线上监控

上线记录至少要能关联到具体的应用版本、prompt 版本、模型标识、检索索引版本与请求结果。延迟、token 用量、重试次数和失败类型帮助定位工程问题;任务完成率与经审核的错误案例帮助判断回答质量。不要把 HTTP 200 或点赞率直接当成事实正确率。

日志优先存必要的标识和汇总信息。确实需要保存问题、检索片段或输出时,先处理个人信息与敏感内容,明确访问权限、保留期限及用户授权范围。不要把客户原始数据复制进公开测试仓库。

小流量发布前写清观察窗口、停止条件和回滚版本。候选与基线应比较相同任务类别;流量分布变化时,先分组分析,避免把简单任务增多误认为质量提升。超时或成本超限属于独立的工程门禁,不能用较高的平均质量分抵消。

免费证书

最新免费认证与项目

快速补充简历亮点,提升竞争力。

立即查看 →

6. 练习与验收

  1. grade() 传入缺字段、refund_days=True、重复引用和非对象输出,确认返回失败而非崩溃。
  2. 增加一个“两个来源冲突”的用例,先写出产品期望,再写 fixture 和评分规则。说明何时允许回答、何时转人工。
  3. 接入你自己的已脱敏输出快照,记录数据集、应用、prompt、模型和评分器版本。报告每类失败及具体案例,不只展示一个总分。
  4. 让另一位审核者检查标签与失败解释。若正确答案被拒绝,先修评分规则,再判断候选版本是否真的退步。

本章的验收是:能复现失败与成功退出码,解释每个失败原因,并说清评分器没有覆盖的风险。它不提供任何模型排名或上线安全保证。

📚 相关资源

常见问题

点击问题,查看本章对应的实践答案。

评测集多少样本才够?

没有适用于所有任务的固定数量。本章三个合成用例仅用于演示评分器,不代表真实覆盖率。按用户任务、语言、文档变化与失败类型建立样本,保留稳定回归集、挑战集和未参与 prompt 调整的留出样本。

示例需要 API key 或付费模型吗?

不需要。Python 3.8 或更高版本即可离线运行,全部输入和输出都是手写合成数据。broken 模式报告三个回归并退出 1;fixed 模式复用正确 fixture 并退出 0。这是评分器演示,不是模型实测。

引用 ID 正确就表示没有幻觉吗?

不能。文档存在与文档支持该事实是两件事。示例还核对人工标注的整数值;自由文本需要逐条比对事实与来源。精确匹配也可能误判合理措辞或多个有效引用,需要按任务调整规则。

如何选择 judge 和发布阈值?

先用人工标注校准 judge,分别检查错误放行与正确拦截。定义不可接受的失败,再按任务风险和历史基线设门槛;4/5 不是通用标准。更换 judge 或 rubric 后重新校准并记录版本。

线上应该记录什么?

关联应用、prompt、模型和检索索引版本,记录结果、延迟、token 用量、重试和失败类型。任务完成率与人工复核帮助判断质量;HTTP 200 不等于回答正确。保留文本前需处理敏感信息,并明确权限和保留期限。