← 返回文章列表

Dify 企业级实验(08):人机协同审批流——机器预审与人工确认如何配合?

1. 业务场景

先讲一个我们实际遇到的场景。

报销审批,财务的日常:员工提交报销单,财务一张张人工审——金额超没超限、发票格式对不对、是不是重复报销。一天几十单,财务被淹没在重复劳动里。

我们第一次接这类需求时,第一反应也是「要么全自动、要么全人工」。真正动手才发现——两头都是坑:纯人工低效,纯自动不可靠;生产系统的答案在中间:规则优先、LLM 补位。AI 判机器能判的,人判机器判不了的。

全自动又不敢:AI 全判,误放行一笔违规报销就是财务风险。纯人工低效,纯自动不可靠。

这不是个例。任何审批/风控场景都是这个模式:机器预审提效,人工兜底负责。

2. 场景痛点

这个流程的痛点,在财务身上体现得最直接:

本质上,「规则优先、LLM 补位」——确定性校验用规则(0 Token 且可解释),LLM 只做规则判断不了的部分

3. 方案:为什么是规则优先、LLM 补位

Dify 的 code 节点 + LLM + 外部状态存储,正好实现「规则拦截 → LLM 评估 → 分流处理」的审批状态机。选它的理由:

这篇文章我们就用它搭一个「报销预审应用」:规则拦截 + LLM 风险评估 + 自动放行或转人工。

4. 整体架构

graph TD start["开始:expense_id/amount/category/invoice_no/applicant/remark"] read_status["读取审批状态(http/KV)"] validate["规则校验(code:金额/重复/发票)"] rule_ok{"规则是否通过"} end_reject["结束(驳回:输出原因列表)"] llm_risk["LLM 风险评估"] risk_level{"风险等级分流"} auto_pass["自动通过(code)"] write1["写入状态(http upsert)"] end_auto["结束"] manual["转人工(code,状态=待审)"] write2["写入状态(http upsert)"] end_manual["结束"] start --> read_status --> validate --> rule_ok rule_ok -- "否" --> end_reject rule_ok -- "是" --> llm_risk --> risk_level risk_level -- "低" --> auto_pass --> write1 --> end_auto risk_level -- "中/高" --> manual --> write2 --> end_manual

链路很清晰:读审批状态 → 规则校验 → 通过后 LLM 风险评估 → 低风险自动通过 / 中高风险转人工。规则优先、LLM 补位是这条链的关键设计。

5. 模块设计

5.1 开始变量

start 变量expense_id(文本)、amount(数字)、category(下拉:差旅/餐饮/办公/其他)、invoice_no(文本)、applicant(文本)、remark(段落,选填)。

5.2 规则校验(确定性拦截)

规则校验 code 节点:进入 LLM 之前先用确定性规则拦截,返回 rule_pass("true"/"false")与原因列表:

def main(expense_id: str, amount, category: str, invoice_no: str, applicant: str, remark: str, state_body: str) -> dict:

    import re, json

    try:

        amt = float(amount or 0)

    except Exception:

        amt = 0

    reasons = []

    if amt > 5000:

        reasons.append("金额超上限(5000 元)")

    if not re.search(r"^(INV|FP)\d{6,}$", str(invoice_no or "")):

        reasons.append("发票号格式不正确(INV/FP 开头+6 位以上数字)")

    # 重复检测:读 KV 中该 expense_id 的历史记录

    # 已通过 → "重复报销(该单已通过)";待审/审批中 → "该单正在审批中"

    rule_pass = "true" if not reasons else "false"

    return {"rule_pass": rule_pass, "reasons": ";".join(reasons) if reasons else "规则校验通过",

            "amount": str(amt)}

5.3 规则分支(if_rule)

if_rule 分支判断 cd_rules.rule_pass is "true"(字符串比较,必须精确 "true")。

5.4 LLM 风险评估

LLM 风险评估(规则通过后才调用,只输出三个字之一):

system: |

  你是报销审批预审员。根据报销单信息评估风险等级,只输出三个字之一:低、中、高。

  判定规则:金额接近上限(4000 以上)、备注含敏感或模糊描述(加急/特批/代报)、

  发票信息不完整 → 中或高;常规合规报销 → 低。

5.5 风险分支(if_risk)

if_risk 分支判断 lm_risk.text is 低 → 自动通过;否则(中/高)转人工。

5.6 状态持久化

状态持久化:审批状态按 expense_id upsert 到 KV(dify104_08_approvals),自动通过写 status=已通过,转人工写 status=待审,并记录 risk 与时间——工作流重启状态不丢,也为重复检测提供依据。

6. 运行验证

输入 预期 结果
合规单(800 元,发票合规) 规则通过 → LLM 低风险 → 自动通过 状态=已通过
超金额单(9000 元) 规则拒绝,不进 LLM 驳回,原因=金额超上限(5000 元)
备注含「加急特批」 LLM 高风险 → 转人工 状态=待审,推送审批任务
已通过单重复提交 拒绝 重复提交被拦截(重复报销)
驳回后修改重提 重新预审(状态重置) 走完整预审流程

环境:Dify 1.16.1(Docker Compose),模型 DeepSeek deepseek-v4-flash。DSL 导入发布通过,Service API 验证全分支。

7. 实战坑

现象 修复
code 节点沙箱禁写文件 审批状态写文件报 PermissionError(/tmp) 状态存 KV 模拟服务(dify104-kv,172.19.0.50:8123),按 expense_id upsert,生产换 Redis/DB,拓扑不变
code 字符串状态判断 elif rule_pass: 对 "false" 恒真,规则拒绝的单也进了 LLM if_rule 比较必须 rule_pass is "true"(交付说明实测)
规则和 LLM 边界不清 规则能判的(金额/重复/格式)也走 LLM,又贵又不可解释 规则优先 0 Token 且可解释,LLM 只评估非结构化风险(104-05 成本思想)
状态无持久化 工作流实例结束状态丢失,重复检测失效 状态 upsert 到外部 KV,重启不丢(实测)
校验结果不消费 reasons 算出来了但分支没引用,全部放行 if_else 必须消费 rule_pass 再分流(102-20 实测)
驳回无重提路径 用户被卡死,只能重新填单 状态机允许 已驳回 → 待审(重提),禁止 已驳回 → 已通过 非法跳转(104-09 状态机思想)

8. 实验文档及源码获取

文章聚焦核心配置与采坑点,完整分步操作与人工审批衔接说明见实验文档原文。

联系我

15088711270

手机端点击号码可直接拨打 · 桌面端可复制

微信二维码

扫码加微信 · 备注「门户」更快通过