数据截至 (上游 commit 11e53e8eedb3)
第 3 章 · DAG 确定性指标:把裁判拆成决策树
这章讲什么: 当「让一个 LLM 直接打分」太不可控时,
DAGMetric让你把 评分逻辑画成一棵决策树:每个分叉用 LLM 做一次小分类,但走哪条分支、最终给几分是你写死的。这把「评分」从黑箱变成可读、可复现的流程图。先讲为什么,再讲节点类型,最后讲它怎么执行。
3.1 它要解决的小问题
G-Eval 很强,但它把整个判断交给一次 LLM 调用——你只能描述标准,没法精确控制「如果 A 成立就这么扣分、否则那样」。对于有明确评分规则的场景(比如「标题格式对不对」分几个硬性条件),你想要的是决策树:
标题里有日期吗?→ 没有 → 0 分;有 → 格式对吗?→ 对 → 10 分 / 不对 → 5 分。
每个「问号」可以让 LLM 来答(它擅长判断),但树的结构和叶子的分数由你定。这就是 DAGMetric(Deep Acyclic Graph metric,metrics/dag/dag.py:23)——DeepEval 自称的「图式确定性 LLM 裁判构建器」。
一句话直觉: G-Eval 是「把整张卷子交给一个阅卷老师凭印象打分」;DAG 是「把评分拆成一张是非题判分表,每道是非题让老师判,但每个组合给几分是规则册写死的」。
3.2 四种节点
一棵 DAG 由四种节点拼成(metrics/dag/nodes.py):
| 节点 | 干什么 | 关键约束 |
|---|---|---|
TaskNode | 让 LLM 从用例里抽取/加工出一段中间结果,喂给下游 | :139 |
BinaryJudgementNode | 让 LLM 做一个 yes/no 二元判断 | 必须恰好 2 个子节点,一 True 一 False(:232 的 __post_init__/_validate) |
NonBinaryJudgementNode | 让 LLM 做多选一分类(多于两个分支) | :312 |
VerdictNode | 叶子或中转:要么给一个确定 score,要么把控制权交给一个子指标(如嵌一个 GEval) | score 和 child 二选一,不能都给(:58) |
它们的关系:判断节点(Binary/NonBinary)做分类 → 每个可能的分类结果对应一个 VerdictNode 分支 → VerdictNode 要么落到一个分数(叶子),要么继续接下一层。
┌─────────────────────┐
│ BinaryJudgementNode │ 「标题里有日期吗?」(LLM 判 yes/no)
└───────┬──────┬──────┘
verdict=True verdict=False
┌─────┘ └─────┐
┌────────────┐ ┌────────────┐
│ VerdictNode│ │ VerdictNode│
│ child=下一层│ │ score = 0 │ ← 叶子:确定给 0 分
└─────┬──────┘ └────────────┘
▼
(再接一个判断节点……)
VerdictNode 的双形态是这套设计的关键灵活点(metrics/dag/nodes.py:50):
VerdictNode(verdict=False, score=0)—— 叶子,命中这条分支就定分。VerdictNode(verdict=True, child=GEval(...))—— 命中后把这一支交给一个完整的 G-Eval 子指标去打分(:102起:_build_child_metric会复制一个GEval、_run_child_metric跑measure,分数由 runner 回填到外层metric.score)。
也就是说:DAG 可以在树的某些叶子上嵌套 G-Eval——粗的路由用确定性决策树,细的打分在叶子上交给 LLM 裁判。粗细结合。
3.3 它怎么执行:入度计数 + 写死路由
图的执行有两个机制要讲清楚。
机制一:入度计数,保证「父先于子」
DAG 是有向无环图,一个节点可能有多个父节点。要保证「所有父都跑完了,才轮到我」,DeepEval 用了入度(indegree)计数这个经典拓扑排序技巧:
- 建图时,
DeepAcyclicGraph._build_graph统计出每个节点的indegree(它有几个父,metrics/dag/graph.py:118)。 - 执行时,每个节点被某个父「访问」一次,就把自己的剩余入度减 1(
_DeepAcyclicGraphRunner._check_remaining,metrics/dag/runner.py:54)。 - 只有入度减到 0(所有父都来过了)才真正执行,否则直接 return 等下一个父:
# metrics/dag/runner.py:54 _check_remaining + _visit 开头(入度计数)
def _check_remaining(self, node: Node) -> bool:
self.remaining[node] -= 1
return self.remaining[node] <= 0
# _visit 里:入度没归零就先不动
if not self._check_remaining(node):
return
这保证了菱形结构(一个节点被多路汇聚)只会在最后一路到达时执行一次,不重复、不提前。
建图入口在 DeepAcyclicGraph(metrics/dag/graph.py:85),执行则由 _DeepAcyclicGraphRunner(metrics/dag/runner.py:43)从 root_nodes 开始 _visit 往下递归(:90;graph.py 的 _execute / _a_execute 现在只是调它的薄壳,:144、:155)。DAGMetric.__init__ 还会先调 is_valid_dag_from_roots(...) 检 测有没有环——有环直接报 "Cycle detected in DAG graph."(metrics/dag/dag.py:44)。
机制二:路由由「父的裁决」写死
判断节点跑完,runner 会把 LLM 给的分类结果存进 self.verdicts。访问子 VerdictNode 时,先看自己的 verdict 是否等于父的裁决(_verdict_matches,metrics/dag/runner.py:58),不等就直接 return(这条分支没被选中):
# metrics/dag/runner.py:58 _verdict_matches(路由判断节选)
parent = parents[0] if parents else None
if isinstance(parent, _JUDGEMENT):
return self.verdicts[parent].verdict == node.verdict
# _visit_verdict 里:路由不匹配就不执行
if not self._verdict_matches(node):
return
这就是「确定性」的来源: LLM 只负责回答每个分叉的小问题(yes/no 或选项),但「yes 走哪、no 走哪、走到底给几分」全是图结构写死的。同样的裁决组合,永远得到同样的分数。
3.4 判断节点内部:还是 LLM 裁判
别忘了每个判断节点本身还是一次 LLM 调用。BinaryJudgementNode._execute(metrics/dag/nodes.py:268)做的事:把上游 TaskNode 的输出 + 用例里的相关字段拼成文本,渲染 generate_binary_verdict 模板,让 LLM 返回一个 BinaryJudgementVerdict:
# metrics/dag/nodes.py:268 BinaryJudgementNode._execute(节选)
prompt = self._get_prompt(
"generate_binary_verdict",
template_class="BinaryJudgement", # 借用另一个类的模板
criteria=self.criteria,
text=self._resolve_text(test_case, parents, outputs),
)
self._verdict = generate_with_schema_and_extract(
metric=metric, prompt=prompt, schema_cls=BinaryJudgementVerdict, ...
)
注意 template_class="BinaryJudgement"——这用到了 01 章说的 PromptMixin「借用别的类的模板」能力。
所以 DAG 的本质是:用一串受控的小 LLM 分类,拼出一个确定的打分函数。 每个分类是 LLM(灵活),组合与定分是代码(可控)。
3.5 什么时候用 DAG vs G-Eval
| 你想要 | 用 |
|---|---|
| 一句话描述标准,不想拆逻辑 | GEval |
| 有明确的、分条件的评分规则,要可复现 | DAGMetric |
| 两者都要:粗规则路由 + 细处 LLM 打分 | DAG 叶子上嵌 GEval(VerdictNode(child=GEval(...))) |
代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| DAG 指标外壳 | metrics/dag/dag.py | DAGMetric、is_valid_dag_from_roots |
| 图容器与执行 | metrics/dag/graph.py | DeepAcyclicGraph、validate_root_nodes |
| 图遍历 runner | metrics/dag/runner.py | _DeepAcyclicGraphRunner、_check_remaining、_verdict_matches |
| 节点类型 | metrics/dag/nodes.py | BaseNode、TaskNode、BinaryJudgementNode、NonBinaryJudgementNode、VerdictNode |
| 入度/路由 | metrics/dag/nodes.py | increment_indegree、decrement_indegree、_execute |
| 节点输出 schema | metrics/dag/schema.py | BinaryJudgementVerdict、NonBinaryJudgementVerdict、TaskNodeOutput |