Dify:审核返工闭环——Judge 节点、校验与人工环节
一、蓝本:检查 → 局部返工 → 重新验收
课程的反例:世界规则写明"太空站无法获得外部救援",某结局却写"救援飞船赶到并接走了所有人"——结构完全合法,语义公然违规。两层验收:Validator(代码)查结构、字段、连接(选项指向存在的场景);Reviewer(模型)查语义冲突,产出"问题文件 + 原文 + 规则 + 建议"四件套报告。Reviewer 用 needs_revision / approved 表态;返工只改被点名的文件;maxRepairs = 2,超限停为 needs_human_review,配三重护栏(第七节对照)。
04 篇的失败支路只做了"记录并终止"——runReviewLoop 被砍的那一半,本篇补上。
二、总装:把闭环画上画布
场景生成子流程 → 代码节点 Validator(结构/字段/连接;不过则进返工支路)
→ LLM 节点 Reviewer(Judge)→ verdict + issues(quote 引用原文)
→ 代码节点校验评审报告(verdict 合法 / 结论一致 / quote 命中;无效则重评或转人工)
→ 条件分支:approved → 下游(变量聚合 / 终检)
needs_revision → 归并受影响场景 → 返工任务单
→ 循环(Loop)/迭代只重做点名的 → 达上限(≈ maxRepairs = 2)→ 转人工
先把一件事说死:Dify 没有现成的"审核返工闭环"——"Judge 节点"不是平台节点类型,而是"LLM 节点当 Judge 用"的简称;校验、评审、归并、返工、上限、转人工全是普通节点拼的,这一整套要自己搭。
三、第一层 Validator:代码节点查结构、字段、连接
场景契约沿用课程场景 JSON:id / title / content 非空、ending 布尔、choices 数组(非结局场景每项含 nextSceneId、结局场景 choices 为空)。04 篇 outline 的 scenes 只有 title / summary,本篇补一条自定约定:每项再带 id——连接检查需要 ID 当基准。
import json
def main(scene_text: str, known_scene_ids: str) -> dict:
# known_scene_ids:大纲里全部场景 ID,逗号分隔(连接检查的基准)
known = {s.strip() for s in known_scene_ids.split(",") if s.strip()}
try:
scene = json.loads(scene_text)
except json.JSONDecodeError as e:
return {"valid": False, "errors": f"不是合法 JSON:{e}"}
if not isinstance(scene, dict):
return {"valid": False, "errors": "场景必须是 JSON 对象"}
errors = []
for field in ("id", "title", "content"): # ① 字段非空
if not isinstance(scene.get(field), str) or not scene[field].strip():
errors.append(f"缺 {field} 或为空")
if not isinstance(scene.get("ending"), bool): # ② 类型合法
errors.append("ending 必须是布尔值")
choices = scene.get("choices")
if not isinstance(choices, list):
return {"valid": False, "errors": "choices 必须是数组"}
if scene.get("ending") is False and not choices:
errors.append("非结局场景必须提供选项")
if scene.get("ending") is True and choices:
errors.append("结局场景不应再提供选项")
for i, ch in enumerate(choices): # ③ 连接:选项指向存在的场景
nxt = str(ch.get("nextSceneId", "")).strip()
if not nxt or nxt not in known:
errors.append(f"第 {i + 1} 个选项缺 nextSceneId 或指向不存在的场景:{nxt}")
return {"valid": len(errors) == 0, "errors": ";".join(errors)}
课程原话的分工:能直接比较的交给代码,需要理解含义的交给模型——"救援飞船"那行字结构合法,Validator 必然放行,别指望它查语义。
四、第二层 Reviewer:LLM 节点当 Judge
提示词要点:
-
给足输入:世界规则(worldRules,必要时加人物档案)、受检场景(id + content)、检查要求、输出格式。
-
职责划界:结构、字段、连接由程序查,你只查语义——有没有违反世界规则、与其他设定冲突。
-
四件套:每条问题给 sceneId(问题文件)、quote(问题原文,逐字摘录)、violatedRule(违反哪条规则)、fixDirection(修改方向)。
-
反幻觉条款:quote 必须逐字出现在场景正文里,禁止凭印象转述;无依据的推测不进报告。
-
输出严格 JSON:
{"verdict": "approved" 或 "needs_revision", "issues": [...]}。
对应关系:verdict ↔ needs_revision / approved;四件套 ↔ 课程报告的"问题文件 / 原文 / 规则 / 建议";quote 逐字引用 ↔ "quote 必须命中原文,防模型编造"。再顺手用 03 篇的伏笔:场景生成与 Reviewer 两个 LLM 节点配不同模型——对应课程"答题与评分模型各自独立的 Prompt 和消息"。
Judge 的卷也要被查
课程第十章的提醒原样平移:模型给的结论,本身也是一份需要被检查的输出。Judge 后面再接一个代码节点(JSON 兜底同 Validator,略),查三件事:verdict 合法;结论与问题列表一致(approved 必须对应空 issues);每条 quote 逐字出现在对应场景正文里。对应课程 validateReview() 的口径,只是"问题指向的文件属于当前场景"换成"sceneId 在受检列表里"(violatedRule / fixDirection 非空同理,略)。
import json
def main(report_text: str, scenes_text: str) -> dict:
# scenes_text:受检场景数组,形如 [{"id": "...", "content": "..."}]
report, scenes = json.loads(report_text), json.loads(scenes_text)
content_of = {s.get("id"): s.get("content", "") for s in scenes}
errors = []
verdict = report.get("verdict") # ① verdict 只认两个值
issues = report.get("issues") if isinstance(report.get("issues"), list) else []
if verdict not in ("approved", "needs_revision"):
errors.append(f"verdict 非法:{verdict}")
if verdict == "approved" and issues: # ② 结论与问题列表一致
errors.append("approved 但 issues 非空")
if verdict == "needs_revision" and not issues:
errors.append("needs_revision 但没指出问题")
for i, issue in enumerate(issues): # ③ 引用命中:quote 防编造
sid, quote = issue.get("sceneId"), str(issue.get("quote", ""))
if sid not in content_of:
errors.append(f"第 {i + 1} 条指向未知场景:{sid}")
elif not quote.strip() or quote not in content_of[sid]:
errors.append(f"第 {i + 1} 条缺 quote 或未命中 {sid} 原文")
return {"valid": len(errors) == 0, "errors": ";".join(errors)}
校验不过的报告走"评审无效"支路(重新评审或转人工),绝不能当 approved 放行——这是"Judge 会错"在画布上的落点。
五、返工闭环:只返工出问题的场景
课程精髓是局部返工:从报告归并出受影响文件,同一文件多个问题只返工一次。Dify 上先归并、再循环——归并就一行(ids = sorted({i["sceneId"] for i in json.loads(report_text)["issues"] if i.get("sceneId")})),输出受影响场景列表。
两种拼法:
-
迭代版:归并出的 scene_ids 当迭代输入,迭代体 = "场景生成子流程 + Validator",只带问题报告重做点名的场景。
-
循环(Loop)版:把"生成 → 校验 → 评审"包进循环体,approved 即退出;退出条件与上限配置以官方文档为准。
返工输入是一张返工任务单,照课程四段式拼:要做什么(修复被点名的问题)、依据什么(世界规则 + 原场景 + 问题报告)、改到哪里(只改 title / content,保留 id、ending 与分支关系)、怎样算完成(问题消失且不引入新问题——由下一轮验收判定)。
两道保险的平移:
-
修改前后对比(checkRepairScope):返工前场景存变量快照,返工后逐字段比对"其余字段保持不变"。零件有、成品无。
-
次数上限(maxRepairs = 2):循环上限设 2,或计数变量 + 条件分支;超限走转人工支路——结束节点输出 needs_human_review 状态(对应课程同名状态),可用 HTTP 请求节点推送待审材料。交给人三样:最后一代产物、最后一份评审报告、已返工次数。
六、人工审批:interrupt() 的替代
课程用 interrupt() 卡在大纲与场景之间:大纲一锤定音,错了返工代价最大。Dify 无原生等价物(02 差距清单③),两种搭法。
做法一:拆两段工作流(批产)。 工作流 A:brief → 世界观 → 校验 → 大纲 → 校验 → 结束(输出 world + outline),导出推送给人审;工作流 B:开始输入 = 确认后的 world_json + outline_json,接着跑场景迭代、审核闭环、终检。人从"流程中的暂停点"变成"第二段的触发者"(02 篇原话);代价是两段之间状态自己管——大纲显式传参,或走外部存储中转。
做法二:Chatflow 轮次闸门(人在场)。 Chatflow 每轮用户消息进来链路重走一遍、会话变量跨轮保留(03 篇)。本轮:生成候选大纲 → 存会话变量 → 直接回复"请确认或提修改意见";下一轮:条件分支看用户答复(用户消息由平台以系统级变量提供,变量名以官方文档为准)——"通过"则取大纲走场景支路,有意见则带意见返工。平台没有"流程中途等人",但"一轮结束等用户说话"天然是暂停点:闸门从"暂停"变形为"轮次边界"。
七、三重护栏对照:对应物与粒度差距
| 课程护栏 | 课程实现 | Dify 对应 | 粒度差距 |
|---|---|---|---|
| 写入权限 | writeFiles 白名单,其余 deny(路径级、运行时拦截) | 子流程只声明约定输出变量;不挂多余工具;敏感操作收进固定参数节点 | 路径级退成工具 / 接口级;"这次调用的参数想写哪"拦不住 |
| 修改前后对比 | checkRepairScope() 自动比对"其余文件保持不变" | 代码节点存快照再逐字段比对(第五节) | 零件有、成品无 |
| 返工次数上限 | maxRepairs = 2,超限停为 needs_human_review | 循环上限或计数变量 + 条件分支 + 转人工支路 | 同上,自己搭 |
平移检查清单
| 课程机制 | 这一篇搭出来的 Dify 形态 |
|---|---|
| Validator(结构 / 字段 / 连接) | 代码节点:字段非空、类型合法、nextSceneId 指向存在的场景 |
| Reviewer(语义冲突,四件套) | LLM 节点当 Judge:verdict + issues,quote 引用原文,与生成端分开配模型 |
| validateReview(quote 命中、结论一致) | 校验评审报告的代码节点 |
| runReviewLoop:归并 + 只返工点名的 + maxRepairs = 2 / needs_human_review | 归并 scene_ids;迭代 / 循环只重做点名的;上限 2,超限转 needs_human_review 并通知 |
| interrupt() 卡在大纲与场景之间 | 拆两段工作流 / Chatflow 轮次闸门 |
如果只记 3 句话
-
两层验收原样平移:代码节点守"形",LLM 节点守"意";而 Judge 的报告还要再过一个代码节点——模型给的结论也是需要被检查的输出。
-
返工闭环自己搭:归并受影响场景 → 只重做被点名的 → 上限 2 次 → 超限转人工(needs_human_review)。
-
人工闸门没有原生等价物:批产拆两段工作流,人当第二段触发者;对话场景拿 Chatflow 轮次边界当闸门。
一个自检题
Reviewer 返回 verdict: needs_revision,但校验代码节点发现第 2 条问题的 quote 并没有出现在该场景正文里。这时该走哪条支路?为什么既不能当"场景有问题"去返工,也不能当 approved 放行?
参考答案: 走"评审无效"支路——重新评审(可换模型、收紧提示词),仍无效转人工。quote 未命中说明这份报告本身不可信:要么 Reviewer 编造了引用,要么输出已错乱,needs_revision 随之失去依据。课程口径:程序检查能确认"报告格式和原文引用",但模型判断可能出错——报告先过结构与引用校验,才配影响流程走向。拿无效报告触发返工,是让编造内容指挥返工;反向当 approved 放行,是把"评审失效"冒充"评审通过"——两头都错。
承上启下
闭环补全,流水线能自己"检查 → 返工 → 收口"了。还剩最后一问:改一次提示词、换一个模型、调一条分支,是修复还是退步?下一篇(全章最后一篇)把第十章的评测整套搬到 Dify——三件套、两层评分、Trace、同题双跑、发布检查——并回答"何时用 Dify、何时写代码",给一条收尾的学习路线。
- 点赞
- 收藏
- 关注作者
评论(0)