跳到主要内容

数据截至 (上游 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,要么把控制权交给一个子指标(如嵌一个 GEvalscorechild 二选一,不能都给(: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_metricmeasure,分数由 runner 回填到外层 metric.score)。

也就是说:DAG 可以在树的某些叶子上嵌套 G-Eval——粗的路由用确定性决策树,细的打分在叶子上交给 LLM 裁判。粗细结合。


3.3 它怎么执行:入度计数 + 写死路由

图的执行有两个机制要讲清楚。

机制一:入度计数,保证「父先于子」

DAG 是有向无环图,一个节点可能有多个父节点。要保证「所有父都跑完了,才轮到我」,DeepEval 用了入度(indegree)计数这个经典拓扑排序技巧:

  • 建图时,DeepAcyclicGraph._build_graph 统计出每个节点的 indegree(它有几个父,metrics/dag/graph.py:118)。
  • 执行时,每个节点被某个父「访问」一次,就把自己的剩余入度减 1(_DeepAcyclicGraphRunner._check_remainingmetrics/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

这保证了菱形结构(一个节点被多路汇聚)只会在最后一路到达时执行一次,不重复、不提前。

建图入口在 DeepAcyclicGraphmetrics/dag/graph.py:85),执行则由 _DeepAcyclicGraphRunnermetrics/dag/runner.py:43)从 root_nodes 开始 _visit 往下递归(:90graph.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_matchesmetrics/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._executemetrics/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 叶子上嵌 GEvalVerdictNode(child=GEval(...))

代码地图

主题文件符号
DAG 指标外壳metrics/dag/dag.pyDAGMetricis_valid_dag_from_roots
图容器与执行metrics/dag/graph.pyDeepAcyclicGraphvalidate_root_nodes
图遍历 runnermetrics/dag/runner.py_DeepAcyclicGraphRunner_check_remaining_verdict_matches
节点类型metrics/dag/nodes.pyBaseNodeTaskNodeBinaryJudgementNodeNonBinaryJudgementNodeVerdictNode
入度/路由metrics/dag/nodes.pyincrement_indegreedecrement_indegree_execute
节点输出 schemametrics/dag/schema.pyBinaryJudgementVerdictNonBinaryJudgementVerdictTaskNodeOutput