数据截至 (上游 commit 11e53e8eedb3)
第 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 在 :115):
Step 1 — 拆命题。 把 actual_output 拆成一串「陈述句」(statements):
# metrics/answer_relevancy/answer_relevancy.py:261 _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(:238,异步版 _a_generate_verdicts 在 :215)。
Step 3 — 算分。 这是最能体现「分数=命题比例」的一段(_calculate_score 在 :303):
# metrics/answer_relevancy/answer_relevancy.py:303 _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,:190)只挑出verdict == "no"的那些命题来解释为什么扣分——理由由失败项倒推,所以分数和理由必然一致。
这个「statements → verdicts → 比例」三段式,就是 DeepEval RAG 系指标的模板。换个指标(如忠实度),只是把「相不相关」换成「有没有被检索上下文支持」,骨架不变。
2.3 范例二(旗舰):GEval
GEval(metrics/g_eval/g_eval.py:49)是 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_steps(metrics/g_eval/g_eval.py:235):如果你自己传了 evaluation_steps 就跳过这一步(省一次 LLM 调用,见 :236-237),否则现场生成。
然后用「评估步骤 + 测试用例内容」组装真正的打分 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_score(metrics/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_evaluate(metrics/g_eval/g_eval.py:277)的结构是「先试 logprob 加权,失败就退回普通 schema 解析」:
# metrics/g_eval/g_eval.py:277 _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:215 a_measure 里的归一化
self.score = (float(g_score) - self.score_range[0]) / self.score_range_span
self.success = self.is_successful()
G-Eval 的额外灵活性:rubric
你还能传 rubric(一组 Rubric,把分段区间映射到「期望结果」描述,metrics/g_eval/utils.py 的 Rubric),让裁判按打分细则给分;score_range 也随 rubric 变。这让 G-Eval 从「模糊好坏」升级到「按评分量表打分」。
2.4 两个范例的对照
AnswerRelevancyMetric | GEval | |
|---|---|---|
| 打分思路 | 拆命题→二元裁决→比例 | 标准展开成步骤→裁判给分→logprob 加权 |
| 你要提供 | 啥都不用(指标内置 prompt) | 一句 criteria 或自定义 evaluation_steps |
| 分数来源 | 非 no 命题占比 | 连续期望分(归一化到 0~1) |
| 适用 | 固定的、可拆命题的判断 | 任意自定义标准 |
| 精华 | 「理由由失败项倒推」 | 「CoT 评估步 + logprob 去抖」 |
一句话带走: 普通指标靠「拆命题求比例」做到可解释;G-Eval 在此之上靠「logprob 加权」做到连续稳定,并靠「自然语言 criteria」做到无需写代码即可定制。
代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 通用范例(拆命题) | metrics/answer_relevancy/answer_relevancy.py | AnswerRelevancyMetric、_generate_statements、_generate_verdicts、_calculate_score |
| 裁决/陈述 schema | metrics/answer_relevancy/schema.py | Statements、AnswerRelevancyVerdict、Verdicts |
| G-Eval 主体 | metrics/g_eval/g_eval.py | GEval、a_measure、_a_generate_evaluation_steps、_a_evaluate |
| logprob 加权 | metrics/g_eval/utils.py | calculate_weighted_summed_score、get_score_range、Rubric、no_log_prob_support |
| G-Eval prompt | metrics/g_eval/templates/ | generate_evaluation_steps.txt、generate_evaluation_results.txt |
| 统一 schema 调用 | metrics/utils.py | generate_with_schema_and_extract、a_generate_with_schema_and_extract、trimAndLoadJson |