跳到主要内容

第 2 章 · LLM 裁判指标:命题→裁决→打分

这章讲什么: DeepEval 的看家本领。先讲所有 LLM 裁判指标的通用套路,再用两个代表把它讲透:AnswerRelevancyMetric(最干净的「拆命题→裁决」范例)和旗舰的 GEval(多了 CoT 评估步 + logprob 加权这两个精华)。读完这章,你基本理解了 DeepEval 一大半指标。


2.1 通用套路:为什么不直接问 LLM「打几分」

最朴素的做法是把整段输出丢给裁判 LLM,问「这个答案 0~10 分打几分」。问题是:这种打分抖、不可解释——同一段文本问两次给不同分,而且你不知道分数怎么来的。

DeepEval 的解法是把打分拆成三步小问题,每步都好判定:

Step 1 拆命题 Step 2 逐条裁决 Step 3 算分 + 写理由
┌────────────────┐ ┌──────────────────┐ ┌────────────────────┐
│ 把 output 拆成 │ ───► │ 对每条命题问一个 │ ──► │ 分数 = 合格命题/总数│
│ 一串原子陈述句 │ │ yes/no 的小问题 │ │ 再让 LLM 写一句理由 │
└────────────────┘ └──────────────────┘ └────────────────────┘

为什么这样更好: 二元判定(yes/no)比连续打分稳定得多;分数是命题比例,天然可解释(哪几条不合格一目了然);理由是回头看不合格的命题生成的,所以理由和分数对得上。

这个套路在 DeepEval 里出现在大量指标中(答案相关性、忠实度、上下文召回……)。下面先看最干净的一个。


2.2 范例一:AnswerRelevancyMetric

它判断什么: actual_output 里有多少内容是真的在回答 input(不跑题、不灌水)。只需要 input + actual_output 两个字段。

真实实现就是上面三步(metrics/answer_relevancy/answer_relevancy.py,看异步主流程 a_measure:107):

Step 1 — 拆命题。actual_output 拆成一串「陈述句」(statements):

# metrics/answer_relevancy/answer_relevancy.py:254 _generate_statements(节选)
prompt = self._get_prompt(
"generate_statements",
multimodal=multimodal,
actual_output=actual_output,
)
return generate_with_schema_and_extract(
metric=self, prompt=prompt, schema_cls=Statements,
extract_schema=lambda s: s.statements, ...
)

Step 2 — 逐条裁决。 对每条陈述句,问裁判「这句和 input 相关吗」,得到一串 AnswerRelevancyVerdict(每条带 verdict: "yes"|"no"|"idk"reason),见同步的 _generate_verdicts:231,异步版 _a_generate_verdicts:208)。

Step 3 — 算分。 这是最能体现「分数=命题比例」的一段(_calculate_score:296):

# metrics/answer_relevancy/answer_relevancy.py:296 _calculate_score
number_of_verdicts = len(self.verdicts)
if number_of_verdicts == 0:
return 1
relevant_count = 0
for verdict in self.verdicts:
if verdict.verdict.strip().lower() != "no": # "yes" 和 "idk" 都算相关
relevant_count += 1
score = relevant_count / number_of_verdicts # 相关命题的比例
return 0 if self.strict_mode and score < self.threshold else score

几个值得记住的细节:

  • 分数就是「非 no 的命题占比」——干净、可解释。
  • "idk"(说不准)也算相关,只有明确 "no" 才扣分——这是个偏宽容的判定。
  • strict_mode 下,只要没到阈值就直接归零(非黑即白)。
  • 理由(_generate_reason:183)只挑出 verdict == "no" 的那些命题来解释为什么扣分——理由由失败项倒推,所以分数和理由必然一致。

这个「statements → verdicts → 比例」三段式,就是 DeepEval RAG 系指标的模板。换个指标(如忠实度),只是把「相不相关」换成「有没有被检索上下文支持」,骨架不变。


2.3 范例二(旗舰):GEval

GEvalmetrics/g_eval/g_eval.py:45)是 DeepEval 最招牌的指标,源自论文 G-Eval(用 LLM + 思维链做 NLG 评估)。它解决一个更野的需求:让你用一句自然语言描述评分标准,不写任何判定代码,就得到一个稳定指标。

# 示意,非源码:G-Eval 用法——只描述标准
from deepeval.metrics import GEval
from deepeval.test_case import SingleTurnParams

correctness = GEval(
name="Correctness",
criteria="判断 actual_output 在事实上是否与 expected_output 一致",
evaluation_params=[SingleTurnParams.ACTUAL_OUTPUT, SingleTurnParams.EXPECTED_OUTPUT],
)

G-Eval 内部有两个精华,是它比朴素打分强的原因。

精华一:先让 LLM 把标准展开成「评估步骤」(CoT)

你给的 criteria 是一句话。G-Eval 不直接拿它打分,而是先让裁判 LLM 把这句标准展开成 3~4 条具体的评估步骤,再拿这些步骤去逐一对照打分。这就是 G-Eval 论文的核心——用「思维链」让评估更对齐人类。

看真实的展开 prompt(metrics/g_eval/templates/generate_evaluation_steps.txt):

Given an evaluation criteria which outlines how you should judge the
{{ parameters }}, generate 3-4 concise evaluation steps based on the
criteria below. ...
Evaluation Criteria:
{{ criteria }}
...
Example JSON:
{ "steps": <list_of_strings> }

对应代码 _a_generate_evaluation_stepsmetrics/g_eval/g_eval.py:229):如果你自己传了 evaluation_steps 就跳过这一步(省一次 LLM 调用,见 :230-231),否则现场生成。

然后用「评估步骤 + 测试用例内容」组装真正的打分 prompt(generate_evaluation_results.txt),要 LLM 返回 {score, reason}

精华二:用 logprob 把离散分变连续分(去抖动的关键)

这是 G-Eval 最巧的一招。裁判 LLM 被要求返回一个整数分(如 0~10),但整数分太粗、还抖:同一段文本,模型可能这次输出 7、下次输出 8。

G-Eval 的解法:不只看模型最终吐的那个数字,而是看它在那个位置上各候选数字的概率分布(log probabilities),做期望。 8 分概率 0.6、7 分概率 0.3、9 分概率 0.1 → 加权得 7.7。这样分数连续、稳定

真实实现 calculate_weighted_summed_scoremetrics/g_eval/utils.py,搜该符号):

# metrics/g_eval/utils.py calculate_weighted_summed_score(核心逻辑节选)
min_logprob = math.log(0.01) # 过滤 <1% 概率的候选
for token_logprob in score_logprobs.top_logprobs:
if token_logprob.logprob < min_logprob: # 太低概率,丢
continue
if not token_logprob.token.isdecimal(): # 不是数字 token,丢
continue
linear_prob = math.exp(token_logprob.logprob) # log 概率 → 线性概率
token_score = int(token_logprob.token)
token_linear_probability[token_score] = \
token_linear_probability.get(token_score, 0) + linear_prob
# 期望 = Σ(分数 × 概率) / Σ概率
weighted_summed_score = sum_of_weighted_scores / sum_linear_probability

几个工程细节,全是为了稳:

  • 只保留 ≥1% 概率的数字 token,把噪声候选剔掉。
  • 只取 isdecimal() 的 token,避免把别的字符当分数。
  • 调模型时要 top_logprobs(默认 20,见 GEval.__init__top_logprobs)。

降级路径很重要: 不是所有模型都给 logprob。_a_evaluatemetrics/g_eval/g_eval.py:271)的结构是「先试 logprob 加权,失败就退回普通 schema 解析」:

# metrics/g_eval/g_eval.py:271 _a_evaluate(控制流骨架)
try:
if no_log_prob_support(self.model): # 模型不支持 logprob
raise AttributeError("log_probs unsupported.")
res, cost = await self.model.a_generate_raw_response(prompt, top_logprobs=self.top_logprobs)
data = trimAndLoadJson(res.choices[0].message.content, self)
score, reason = data["score"], data["reason"]
try:
return calculate_weighted_summed_score(score, res), reason # 加权连续分
except (KeyError, AttributeError, TypeError, ValueError):
return score, reason # 加权失败就退回离散分
except AttributeError:
# 退回:直接按 schema 拿离散 score/reason
return await a_generate_with_schema_and_extract(metric=self, prompt=prompt, ...)

注意有两层 try/except:外层 except AttributeError 兜住「模型根本不支持 logprob」(退回 schema 解析);内层把 calculate_weighted_summed_score 单独包起来,万一加权计算本身出问题,也能退回那个离散 score

最后归一化。 拿到的 g_score 在 score_range(默认 (0,10),见 get_score_range)里,再线性映射到 0~1:

# metrics/g_eval/g_eval.py:210 a_measure 里的归一化
self.score = (float(g_score) - self.score_range[0]) / self.score_range_span
self.success = self.score >= self.threshold

G-Eval 的额外灵活性:rubric

你还能传 rubric(一组 Rubric,把分段区间映射到「期望结果」描述,metrics/g_eval/utils.pyRubric),让裁判按打分细则给分;score_range 也随 rubric 变。这让 G-Eval 从「模糊好坏」升级到「按评分量表打分」。


2.4 两个范例的对照

AnswerRelevancyMetricGEval
打分思路拆命题→二元裁决→比例标准展开成步骤→裁判给分→logprob 加权
你要提供啥都不用(指标内置 prompt)一句 criteria 或自定义 evaluation_steps
分数来源非 no 命题占比连续期望分(归一化到 0~1)
适用固定的、可拆命题的判断任意自定义标准
精华「理由由失败项倒推」「CoT 评估步 + logprob 去抖」

一句话带走: 普通指标靠「拆命题求比例」做到可解释;G-Eval 在此之上靠「logprob 加权」做到连续稳定,并靠「自然语言 criteria」做到无需写代码即可定制。


代码地图

主题文件符号
通用范例(拆命题)metrics/answer_relevancy/answer_relevancy.pyAnswerRelevancyMetric_generate_statements_generate_verdicts_calculate_score
裁决/陈述 schemametrics/answer_relevancy/schema.pyStatementsAnswerRelevancyVerdictVerdicts
G-Eval 主体metrics/g_eval/g_eval.pyGEvala_measure_a_generate_evaluation_steps_a_evaluate
logprob 加权metrics/g_eval/utils.pycalculate_weighted_summed_scoreget_score_rangeRubricno_log_prob_support
G-Eval promptmetrics/g_eval/templates/generate_evaluation_steps.txtgenerate_evaluation_results.txt
统一 schema 调用metrics/utils.pygenerate_with_schema_and_extracta_generate_with_schema_and_extracttrimAndLoadJson