跳到主要内容

第 3 章 · DAG 确定性指标:把裁判拆成决策树

这章讲什么: 当「让一个 LLM 直接打分」太不可控时,DAGMetric 让你把评分逻辑画成一棵决策树:每个分叉用 LLM 做一次小分类,但走哪条分支、最终给几分是你写死的。这把「评分」从黑箱变成可读、可复现的流程图。先讲为什么,再讲节点类型,最后讲它怎么执行。


3.1 它要解决的小问题

G-Eval 很强,但它把整个判断交给一次 LLM 调用——你只能描述标准,没法精确控制「如果 A 成立就这么扣分、否则那样」。对于有明确评分规则的场景(比如「标题格式对不对」分几个硬性条件),你想要的是决策树

标题里有日期吗?→ 没有 → 0 分;有 → 格式对吗?→ 对 → 10 分 / 不对 → 5 分。

每个「问号」可以让 LLM 来答(它擅长判断),但树的结构和叶子的分数由你定。这就是 DAGMetric(Deep Acyclic Graph metric,metrics/dag/dag.py:22)——DeepEval 自称的「图式确定性 LLM 裁判构建器」。

一句话直觉: G-Eval 是「把整张卷子交给一个阅卷老师凭印象打分」;DAG 是「把评分拆成一张是非题判分表,每道是非题让老师判,但每个组合给几分是规则册写死的」。


3.2 四种节点

一棵 DAG 由四种节点拼成(metrics/dag/nodes.py):

节点干什么关键约束
TaskNode让 LLM 从用例里抽取/加工出一段中间结果,喂给下游:267
BinaryJudgementNode让 LLM 做一个 yes/no 二元判断必须恰好 2 个子节点,一 True 一 False(:407__post_init__
NonBinaryJudgementNode让 LLM 做多选一分类(多于两个分支):529
VerdictNode叶子或中转:要么给一个确定 score,要么把控制权交给一个子指标(如嵌一个 GEvalscorechild 二选一,不能都给(:67

它们的关系:判断节点(Binary/NonBinary)做分类 → 每个可能的分类结果对应一个 VerdictNode 分支 → VerdictNode 要么落到一个分数(叶子),要么继续接下一层。

┌─────────────────────┐
│ BinaryJudgementNode │ 「标题里有日期吗?」(LLM 判 yes/no)
└───────┬──────┬──────┘
verdict=True verdict=False
┌─────┘ └─────┐
┌────────────┐ ┌────────────┐
│ VerdictNode│ │ VerdictNode│
│ child=下一层│ │ score = 0 │ ← 叶子:确定给 0 分
└─────┬──────┘ └────────────┘

(再接一个判断节点……)

VerdictNode 的双形态是这套设计的关键灵活点(metrics/dag/nodes.py:57):

  • VerdictNode(verdict=False, score=0) —— 叶子,命中这条分支就定分。
  • VerdictNode(verdict=True, child=GEval(...)) —— 命中后把这一支交给一个完整的 G-Eval 子指标去打分(:89 起:它会复制一个 GEval、跑 measure、把分数回填到外层 metric.score)。

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


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

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

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

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

  • 建图时,每个子节点的 _indegree 记录它有几个父(increment_indegreemetrics/dag/nodes.py:51)。
  • 执行时,每个节点被某个父「访问」一次,就把自己的入度减 1(decrement_indegree)。
  • 只有入度减到 0(所有父都来过了)才真正执行,否则直接 return 等下一个父:
# metrics/dag/nodes.py BinaryJudgementNode._execute 开头
decrement_indegree(self)
if self._indegree > 0:
return # 还有父没跑完,先不动

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

建图入口在 DeepAcyclicGraphmetrics/dag/graph.py:27),从 root_nodes 开始 _execute / _a_execute 往下递归(:58:65)。DAGMetric.__init__ 还会先调 is_valid_dag_from_roots(...) 检测有没有环——有环直接报 "Cycle detected in DAG graph."metrics/dag/dag.py:36)。

机制二:路由由「父的裁决」写死

判断节点跑完,会把 LLM 给的分类结果存进 self._verdict。子 VerdictNode 执行时,先看自己的 verdict 是否等于父的裁决,不等就直接 return(这条分支没被选中):

# metrics/dag/nodes.py:88 VerdictNode._execute(路由判断节选)
if isinstance(self._parent, (NonBinaryJudgementNode, BinaryJudgementNode)):
if self._parent._verdict.verdict != self.verdict:
return # 父没选这条分支,我不执行

这就是「确定性」的来源: LLM 只负责回答每个分叉的小问题(yes/no 或选项),但「yes 走哪、no 走哪、走到底给几分」全是图结构写死的。同样的裁决组合,永远得到同样的分数。


3.4 判断节点内部:还是 LLM 裁判

别忘了每个判断节点本身还是一次 LLM 调用BinaryJudgementNode._executemetrics/dag/nodes.py:441)做的事:把上游 TaskNode 的输出 + 用例里的相关字段拼成文本,渲染 generate_binary_verdict 模板,让 LLM 返回一个 BinaryJudgementVerdict

# metrics/dag/nodes.py:460 BinaryJudgementNode._execute(节选)
prompt = self._get_prompt(
"generate_binary_verdict",
template_class="BinaryJudgement", # 借用另一个类的模板
criteria=self.criteria,
text=text,
)
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
节点类型metrics/dag/nodes.pyBaseNodeTaskNodeBinaryJudgementNodeNonBinaryJudgementNodeVerdictNode
入度/路由metrics/dag/nodes.pyincrement_indegreedecrement_indegree_execute
节点输出 schemametrics/dag/schema.pyBinaryJudgementVerdictNonBinaryJudgementVerdictTaskNodeOutput