AB
AiBoss站
教程

系统评测 LLM 应用:从评分器设计到 RAG 与 Agent 上线前回归

教程

系统评测 LLM 应用:从评分器设计到 RAG 与 Agent 上线前回归

面向 LLM 应用开发者的评测教程:区分代码评分器、参考答案评分器与 LLM 裁判,写出可解析的评分提示词,用 QAG 与 G-Eval 得到稳定分数,用 Cohen's kappa 校准裁判,再按组件拆解 RAG 与 Agent 的失败原因,并区分上线前离线评测与线上流量评测。

LLM 应用最常见的验证方式是「读几条输出,觉得还行就发布」。问题在于,一次提示词改动、一次模型替换、一次检索策略调整,都可能破坏你从未检查过的行为,而等到用户发现时,你已经说不清是哪次改动造成的。系统化评测要解决的就是这件事:准备测试用例,按写下来的标准给每条输出打分,并在每次改动前后对比分数。它本质上就是常规代码里的回归测试循环,只是适配了「每次运行结果都不完全相同」的输出。

这篇教程面向正在开发或维护 LLM 应用的人:客服问答、文档检索问答、带工具调用的 Agent,以及任何需要判断「新版本是不是比旧版本好」的场景。读完之后,你应该能搭起一条最小可用的评测流水线:定义数据集、选择评分器、写出可解析的裁判提示词、用人工标注校准裁判,并把 RAG 与 Agent 的失败定位到具体组件。文中涉及的具体模型、接口能力、价格与配额,请以各服务商官网当前信息为准。

准备工作

开始之前,先把下面几样东西准备好。它们不涉及任何特定厂商,任何能调用模型的 SDK 都可以。

  • 一个可调用的模型接口:用于充当裁判模型。你需要能设置温度参数,最好还能拿到 token 的对数概率(用于 G-Eval 的期望值计算)。如果接口不暴露对数概率,就走采样平均的近似方案。
  • 一个测试用例集合:每条用例至少包含输入,视评分方式还需要上下文(检索到的文档、政策原文)和参考答案。用例不必多,但要覆盖你真正在意的行为。
  • 一个评分结果存储:哪怕只是一个 JSONL 文件,也要能按时间回看分数。评测的价值一半来自「同一批用例在不同版本上的分数对比」,没有历史记录就无法对比。
  • 人工标注的一小批样本:30 到 50 条即可,用来校准裁判。这是整条流水线里最容易被跳过、也最不该跳过的一步。
  • Python 环境:示例使用标准库的 json,校准部分使用 scikit-learn 的 cohen_kappa_score。

还需要先想清楚一件事:评测与测试不是一回事。评测衡量的是模糊标准下的相对质量——版本 B 是否比版本 A 更好,好在哪些输入上。测试则是用二值检查卡住发布,比如输出必须是合法 JSON、禁用词绝不能出现。如果你把「忠实度分数达到 0.7」当作发布门槛,那么每周都会有人为这个阈值争论。合理的分工是:用评测指导迭代,用测试保护发布。

任何一次评测都由三部分组成:输入数据集、给输出打分的评分器、随时间累积的分数记录。后面的所有内容都建立在这三件东西上。

操作步骤

第一步:理解为什么抽查会失效

LLM 的输出每次运行都可能不同,而「正确」本身依赖上下文。单元测试断言相等,但「鞋子可在 30 天内退货」和「收货后一个月内接受退货」都是对的,任何字符串比较都无法识别这一点。这就是需要评分器的原因。

评分器接收一条测试用例(输入、输出,有时还有上下文或参考答案),返回一个判定或一个数值。按工作方式可以分成三类:

类型打分方式适用场景示例
代码评分器确定性逻辑格式、长度、必需字符串JSON 可解析、字数少于 120
参考答案评分器与已知答案比较只有一个正确答案的问题精确匹配、F1、嵌入相似度
LLM 裁判模型按你的评分标准判断语气、有据可依、有用性「每条论断是否都被上下文支持?」

选择顺序很关键:凡是能用规则表达的,先用代码评分器。它不花钱,而且每次返回相同结果。只有当规则无法表达某个标准时,才引入裁判模型。

两类常见指标各有硬伤,需要知道边界。BLEU、ROUGE 这类重叠指标统计与参考答案共享的 n-gram,因此一个正确的改写会得低分。BERTScore 这类基于嵌入的分数通过 token 嵌入之间的余弦相似度处理改写,但它必须有参考答案,而且无法判断语气,也无法判断某条论断是否被来源支持。

第二步:选择裁判的三种设置

LLM 裁判是一个被提示去给另一个模型输出打分的模型。它的任务比生成窄得多:读评分标准与候选答案,输出判定。三种设置覆盖大多数用途:

  • 单点打分(pointwise):裁判按评分标准给一条输出打分,形式可以是 PASS / FAIL 标签,也可以是量表上的数值。
  • 成对比较(pairwise):裁判看到同一输入下的两条输出,选出更好的那条。比较两个提示词或两个模型时通常用这种设置。
  • 参考答案引导(reference-guided):裁判同时拿到标准答案,并据此检查输出。在事实性问题上,裁判自己未必知道真相,这时参考答案能帮上忙。

第三步:写出可用的评分标准提示词

评分标准就是全部产品。一个可用的裁判提示词包含四部分:标准的定义、裁判可以使用的输入、执行步骤、严格的输出格式。像「这个答案好吗」这样模糊的标准只会产生模糊的分数。

下面是一个政策问答机器人的忠实度裁判。它要求裁判先列出答案中的每条事实性论断,再逐条判断政策原文是否支持,最后给出总判定:

import json

RUBRIC = """You are grading a customer-support answer for faithfulness.

Policy: {context}
Question: {input}
Answer: {answer}

Steps:
1. List each factual claim in the Answer.
2. For each claim, decide whether the Policy supports it.
3. Verdict: PASS if every claim is supported, otherwise FAIL.

Respond with JSON only:
{{"claims": [{{"text": "...", "supported": true}}], "verdict": "PASS"}}"""

def judge(case, answer):
    response = call_llm(RUBRIC.format(answer=answer, **case), temperature=0)
    return json.loads(response)

其中 call_llm 是你自己的函数,负责把提示词发给模型并返回文本。这个提示词里有三个设计选择值得注意:

  • 推理在判定之前。论断列表先输出,判定后输出,于是判定以证据为条件,而不是先下结论再补理由。
  • 输出是结构化 JSON。你可以解析它,并在分数看起来不对时把推理过程一并记录下来。
  • 温度为 0。这能降低采样噪声,但无法消除它——浮点非确定性和服务端批处理仍会让边界用例偶尔翻转。

对于翻转会影响结论的标准,把裁判跑奇数次,取多数判定。另外,论断列表本身就能给出分数,不必让模型直接报一个数字。

第四步:用 QAG 把主观打分变成可复现的算术

直接问模型「给这个答案打 1 到 10 分」结果不稳定,因为模型没有稳定的内部量尺。问题—答案生成(QAG)绕开了这一点:裁判只回答封闭的是 / 否问题,算术由你的代码完成。

在上面的例子里,每条论断就是一个是 / 否问题(「政策是否支持这一条?」),忠实度就是:

score = sum(c["supported"] for c in verdict["claims"]) / len(verdict["claims"])

一个包含四条论断、其中三条被支持的答案得 0.75。模型只做分类,而分类是 LLM 执行得最稳定的任务之一;在论断判定相同的前提下,分数完全可复现。多数 RAG 忠实度指标都是这样工作的。

使用 QAG 时要注意一个边界:如果答案里没有任何可拆分的论断(比如纯寒暄或纯建议),分母会为零。这类用例应当在评分器里单独处理,而不是让它抛异常。

第五步:用 G-Eval 处理没有离散论断的主观标准

连贯性、有用性这类标准没有可计数的事实论断,G-Eval 针对的正是它们。它分两个阶段:先用自动思维链(Auto-CoT)提示把高层评测标准拆解成一步步的评测流程;再用填表式生成提示把这些步骤与目标文本组合,生成 1 到 5 分(或 0 到 10 分,取决于所用框架)的分数,并通过输出 token 的概率加权求和来保证稳定性。

它最有特点的地方是最终数值的计算方式:读取分数 token「1」到「5」的概率,返回期望值,也就是概率加权平均。如果模型在 4 分和 5 分之间给出 60 / 40 的分布,得分就是 4.4。这带来更细的分辨率,也减少了模型总往某个偏好数字上塌缩的习惯。

代价是它需要 token 的对数概率,而并非每个 API 都暴露这一信息。拿不到时,可以采样多次判定再取平均作为近似。

选择原则很简单:标准能拆成可核查的事实时用 QAG,拆不了时用 G-Eval。

第六步:做成对比较并消除位置偏差

成对裁判回答「A 还是 B 更好」,避开了绝对量表。但它对答案顺序敏感,所以每次比较都要交换位置跑两遍,只保留两遍一致的结论:

def prefer(case, a, b):
    first = pairwise(case, a, b)    # returns "A" or "B"
    second = pairwise(case, b, a)
    if first == "A" and second == "B":
        return "a"
    if first == "B" and second == "A":
        return "b"
    return "tie"

这里的「平局」意味着裁判的偏好跟着位置走,而不是跟着内容走。把它们计为平局并报告平局率。平局率高说明两条输出确实难以区分,这本身就是一个有用的结论。

第七步:识别并缓解裁判偏差

关于裁判 LLM 的研究记录了几种稳定的偏差,每一种都有对应的缓解手段:

  • 位置偏差:偏好某个固定位置的答案。缓解方式是交换位置。
  • 冗长偏差:偏好更长的答案,即使长出来的部分没有增加信息。缓解方式是在提示词里写明「不要因长度给分」,并在数据集中加入长度匹配的测试用例。
  • 自我偏好偏差:偏好与裁判同属一个模型家族的输出。缓解方式是让裁判来自与被测系统不同的家族。

最重要的缓解手段是与人工标注校准。自己标注 30 到 50 条输出,让裁判跑同一批,然后比较。

这里有个陷阱:当大多数用例都通过时,原始一致率会误导你。如果 90% 的用例是 PASS,一个永远输出 PASS 的裁判与你的原始一致率就是 90%,却什么信息都没提供。Cohen's kappa 会校正这种偶然一致:

from sklearn.metrics import cohen_kappa_score

kappa = cohen_kappa_score(human_labels, judge_labels)

kappa 为 0 表示与随机水平相当,为 1 表示完全一致。通常把 0.6 以上读作实质性一致。kappa 偏低时,去读那些分歧样本,找出裁判误读了什么,然后修改评分标准。给量表的每一档配一个示例输出也有帮助。把标注集当作裁判自身的回归测试,每次改动裁判提示词或更换裁判模型后都重跑一遍。

第八步:按组件评测 RAG 系统

RAG 系统有两个会独立失败的组件:检索器负责找到上下文,生成器负责据此写出答案。分开打分,才能知道该修哪一边。四个常用指标:

  • 上下文精确率:检索到的片段是否相关,最相关的片段是否排在前面?
  • 上下文召回率:检索是否找齐了回答问题所需的全部内容?
  • 忠实度:答案中的每条论断是否都被检索到的上下文支持?
  • 答案相关性:答案是否回应了被问的问题?

组合起来读,诊断价值就出来了。忠实度高而召回率低,说明模型老老实实只用了收到的内容,但收到的太少——该修检索。精确率高而忠实度低,说明文档找得不错,论断却是编的——该修提示词或换模型。

还有一个部署层面的区别:无参考答案的 RAG 指标可以在没有标注答案的情况下计算忠实度与相关性,因此能跑在线上流量上;召回率需要预期答案,只能作为离线指标。

如果完全没有上下文、只想查幻觉,可以用基于采样的方法:对同一个提示生成多个答案再比较。模型确实知道的事实会在多次采样中重复出现,编造的内容则会漂移。代价是每条答案要调用多次模型。

第九步:评测 Agent 与多轮对话

Agent 在 RAG 的基础上增加了工具调用、规划和循环。评测从任务完成度开始:用户的目标达成了吗?一个回复流畅却始终没解决工单的客服 Agent,在本职工作上就是失败的。

当完成度下降时,需要诊断性分数来定位原因。把 Agent 的行为拆成可分别打分的环节:规划是否合理、工具选择是否正确、工具参数是否合法、多轮对话中是否保持了上下文一致性。这样当整体完成度下滑时,你能指出是哪一环退化,而不是笼统地说「Agent 变差了」。

第十步:区分上线前离线评测与线上流量评测

发布前跑离线评测:用固定数据集,对每个候选版本计算全套分数,与当前线上版本对比。这一步的作用是拦截回归——某个指标下降就阻止发布,或者至少要求解释。

发布后跑线上评测:在真实流量上计算那些不需要标注答案的指标,比如忠实度、答案相关性、格式合法率。线上评测能发现离线数据集覆盖不到的真实输入分布,也能捕捉模型服务端变化带来的漂移。

两者共用同一套评分器定义,但数据来源和门槛不同。离线评测可以要求严格,线上评测更适合做监控与告警。

一个完整示例

下面把前面的部件串成一个能跑通的最小流程:一个政策问答机器人,用 QAG 风格的忠实度裁判打分,并用人工标注校准。

第 1 步,准备用例。每条用例包含问题、检索到的政策原文、模型答案,以及(校准时才需要的)人工标签。

cases = [
    {
        "input": "Can I return shoes after 40 days?",
        "context": "Returns are accepted within 30 days of delivery.",
        "answer": "Yes, you can return shoes within 30 days of delivery.",
        "human": "PASS",
    },
    {
        "input": "Can I return shoes after 40 days?",
        "context": "Returns are accepted within 30 days of delivery.",
        "answer": "Yes, returns are accepted for 60 days.",
        "human": "FAIL",
    },
]

第 2 步,定义裁判并计算分数。复用第三步的 RUBRIC 与 judge 函数,把论断判定折算成 0 到 1 的忠实度:

def faithfulness(case):
    verdict = judge(case, case["answer"])
    claims = verdict["claims"]
    if not claims:
        return None
    return sum(c["supported"] for c in claims) / len(claims)

results = []
for case in cases:
    score = faithfulness(case)
    results.append({"input": case["input"], "score": score})
    print(case["input"], score)

第一条用例的答案把「30 天」正确复述,论断被支持,分数为 1.0;第二条把 30 天说成 60 天,论断不被支持,分数为 0.0。

第 3 步,把分数转成标签并校准。设定一个判定阈值(这里用 1.0,即所有论断都必须被支持),然后与人工标签比较:

from sklearn.metrics import cohen_kappa_score

human_labels = [c["human"] for c in cases]
judge_labels = ["PASS" if r["score"] == 1.0 else "FAIL" for r in results]

print(cohen_kappa_score(human_labels, judge_labels))

第 4 步,迭代。如果 kappa 偏低,逐条查看分歧样本:是裁判把常识当成了论断?是政策原文里的例外条款被忽略?据此修改 RUBRIC 的步骤或判定条件,然后重跑第 2、3 步。标注集就是裁判的回归测试,每次改动都要重跑。

第 5 步,接入发布流程。把同一套评分器跑在完整数据集上,记录每个版本的分数。改动提示词、换模型、调检索参数之后都重跑一次,对比分数变化。对必须卡死的项(合法 JSON、禁用词)另写二值测试,作为发布门槛。

注意事项

  • 温度设为 0 不等于确定性。浮点非确定性与服务端批处理仍会让边界用例偶尔翻转。对翻转敏感的标准,跑奇数次取多数判定。
  • G-Eval 依赖对数概率。不是每个 API 都暴露 token 对数概率;拿不到时只能采样多次取平均作为近似,精度会下降。
  • 成对比较必须交换位置。只跑一遍会把位置偏差当成质量差异。交换后不一致的结论应计为平局,并报告平局率。
  • 原始一致率会骗人。类别分布不均衡时,一个恒定输出的裁判也能拿到很高的一致率。用 Cohen's kappa 校正偶然一致。
  • 召回率需要参考答案。它只能作为离线指标;忠实度与相关性可以在无标注的情况下跑在线上流量上。
  • 采样查幻觉有成本。每条答案要调用多次模型,成本随采样数线性增长。
  • 不要用评测分数当发布门槛。模糊标准的阈值会引发无休止的争论。用评测指导迭代,用二值测试保护发布。
  • 裁判与被测系统尽量不同家族。同家族会引入自我偏好偏差。
  • 数据集要包含长度匹配的用例。否则冗长偏差会伪装成质量提升。
  • 文中涉及的模型能力、接口参数、价格与配额均可能变化,请以各服务商官网当前信息为准。