DeepEval 是给 LLM 应用打分的框架。它不训练模型,也不替你跑 RAG 或 Agent——应用已经答完,它只负责判这一次答得好不好。
原理就一句:另开一个 LLM 当裁判,按提示词给出结构化判断;Python 把这些判断聚成 0–1 分,再和阈值比较。
项目常把自己比成 Pytest。Pytest 是 Python 最常用的测试运行器:你写 test_*.py,里面 assert 条件,跑 pytest 就会执行全部用例,失败标红。Go 里对上的是标准库 testing 和 go test:TestXxx(t *testing.T) 里 got != want 就 t.Fatal。DeepEval 像它们,只因为最后一步同样是断言——assert_test 失败就抛错,所以能挂进测试套件。中间那一步并不像 got == want:开放文本没法精确比对,才把「像不像、有没有编造」交给裁判模型。
整条链因此是:装箱 → 裁判判断 → 算术 → 断言。下面按源码把这四步看完。
一次打分怎么走
应用已经产出一次结果
↓
塞进 LLMTestCase
↓
Metric.measure():填模板 → 调裁判 LLM → 解析 JSON
↓
_calculate_score():计数或归一化,得到 0–1
↓
score >= threshold → 通过,否则断言失败
先装箱:评的是这一次交互
要判「这一次」,就得把这一次的材料放进同一个对象。定义在 deepeval/test_case/llm_test_case.py:
class LLMTestCase(BaseModel):
input: str
actual_output: Optional[str] = None # 实际回答
expected_output: Optional[str] = None # 金标,可选
retrieval_context: Optional[List] = None # RAG 检索到的片段
tools_called: Optional[List] = None # Agent 实际调了哪些工具
expected_tools: Optional[List] = None # 期望调哪些工具
框架默认不会去调你的应用。你先跑完,再把字段填上。多轮对话用 ConversationalTestCase,道理一样,只是装的是一整段对话。
后面的指标是按字段工作的。例如忠实度要从检索文档里抽「事实」,retrieval_context 为空就会直接报错——不是调度写错了,是盒子里缺材料。
再调度:跑完指标,做一次是否通过
入口在 deepeval/evaluate/evaluate.py。evaluate() 批量打分只汇总;assert_test() 在失败时抛 AssertionError,相当于 Go 测试里的 t.Fatal:
def assert_test(test_case=None, metrics=None, ...):
test_result = execute_test_cases([test_case], metrics, ...)[0]
if not test_result.success:
raise AssertionError(...)
「算不算通过」不在调度里另写一套逻辑,而在指标基类 deepeval/metrics/base_metric.py:
def is_successful(self) -> Optional[bool]:
if self.error is not None:
self.success = False
else:
self.success = self.score >= self.threshold # 默认 0.5
return self.success
到这里,Pytest / go test 那一层已经结束了:有用例、有失败即红。真正决定分数的,是每个 Metric.measure()。
指标:裁判给出判断,Python 做成比例
多数指标目录长得一样:templates/*.txt 是问裁判的提示词,schema.py 规定 JSON 长什么样,_calculate_score() 把解析结果变成一个数。
忠实度(Faithfulness)把这条原理写得很直。它要回答的是:模型的回答有没有超出检索文档。faithfulness.py 里分四步,每步都是一次(或一批)裁判调用,最后才做除法:
self.truths = self._generate_truths(retrieval_context, multimodal) # 文档里的事实
self.claims = self._generate_claims(actual_output, multimodal) # 回答里的主张
self.verdicts = self._generate_verdicts(multimodal) # 每条主张是否被事实支持
self.score = self._calculate_score()
def _calculate_score(self) -> float:
if not self.verdicts:
return 1
ok = sum(1 for v in self.verdicts if v.verdict.strip().lower() != "no")
return ok / len(self.verdicts) # 没被判成矛盾的主张占比
答案相关性(Answer Relevancy)也是「相关条数 / 总条数」。算法没有隐藏模型,有设计的是提示词怎么拆:先抽、再判、再计数。换裁判模型或改提示词,同一个用例分数会变;threshold=0.5 只是「过线」的约定,不是测出来的物理量。
仓库里还有两类不走裁判的尺子,避免和上面混在一起:字符串是否一致用 ExactMatchMetric;metrics/ragas.py 则是直接调用 RAGAS 库。Agent 是否调对工具、对话是否跑题,绝大多数仍是「提示词 + JSON + 聚合」,和忠实度同一原理,只是问的问题不同。
于是会碰到下一问:忠实度问的问题是写死的;业务上要评「语气对不对」「有没有按话术来」时,尺子从哪来?
G-Eval:标准改由你写,裁判仍按同一套办法判
没有 G-Eval 时,上面那个问题其实没有现成出口。内置指标问的都是固定问题:忠实度只看回答有没有和检索文档打架,答案相关性只看有没有答到用户的问。退货政策换一种说法、客服语气硬不硬、有没有按话术来——这些都不在它们的题面里。剩下两条路更窄:ExactMatchMetric 要求字面一致,「30 天内免费退货退款」和「30 天无理由全额退款」会直接判 0;或者自己继承 BaseMetric,从提示词、JSON schema 到 _calculate_score() 全写一遍。
G-Eval 就是把这第三步收成一个现成指标:你只提供 criteria(还可以指定裁判该看哪些字段),其余仍走 LLM-as-a-judge。实现在 g_eval.py,方法来自论文 Liu et al., 2023。
还是「回答对不对」这件事,有了它就不用自己写裁判流水线:
from deepeval.test_case import LLMTestCase, SingleTurnParams
from deepeval.metrics import GEval
correctness = GEval(
name="正确性",
criteria="对照期望输出,判断实际回答在事实上是否正确。",
evaluation_params=[
SingleTurnParams.ACTUAL_OUTPUT,
SingleTurnParams.EXPECTED_OUTPUT,
],
threshold=0.5,
)
test_case = LLMTestCase(
input="这双鞋不合适怎么办?",
actual_output="30 天内可以免费退货退款。",
expected_output="我们提供 30 天无理由全额退款。",
)
correctness.measure(test_case)
# 分数、理由写在 correctness.score / .reason / .success 上
裁判被要求看的只有实际输出和期望输出;criteria 就是尺子。跑起来时并不是对着这段话直接吐一个整数。论文还要求一步 Chain-of-Thought:先把 criteria 展开成 3–4 条可执行的评分步骤,再按步骤打 0–10 分(再归一化到 0–1)。若接口能返回 token 的 logprobs,还会用分数这个 token 的概率做加权,减轻「只输出 7 或 8」带来的抖动。
你写的 criteria
→ 生成 3–4 条 evaluation steps
→ 按 steps 打分
这两步对应两份提示词,由 _get_prompt() 渲染:
| 文件 | 作用 |
|---|---|
metrics/g_eval/templates/generate_evaluation_steps.txt |
把 criteria 变成 steps[] |
generate_evaluation_results.txt |
按 steps 出分,并写 reason |
def _generate_evaluation_steps(self, multimodal: bool) -> List[str]:
if self.evaluation_steps: # 你自己传入步骤,就跳过生成
return self.evaluation_steps
prompt = self._get_prompt(
"generate_evaluation_steps",
criteria=self.criteria,
parameters=...,
)
return generate_with_schema_and_extract(...) # 从 JSON 里取出 steps
构造 GEval 时如果已经传入 evaluation_steps,第一步不会再调模型——论文也更推荐你自己写清步骤,而不是每次让裁判现编。
所以 G-Eval 并没有另起一套打分哲学:它只是把「问什么」从写死的忠实度,换成你提供的标准,并用评分步骤把这次提问变得可重复一点。
