Dify 企业级实验(08):人机协同审批流——机器预审与人工确认如何配合?
1. 业务场景
先讲一个我们实际遇到的场景。
报销审批,财务的日常:员工提交报销单,财务一张张人工审——金额超没超限、发票格式对不对、是不是重复报销。一天几十单,财务被淹没在重复劳动里。
我们第一次接这类需求时,第一反应也是「要么全自动、要么全人工」。真正动手才发现——两头都是坑:纯人工低效,纯自动不可靠;生产系统的答案在中间:规则优先、LLM 补位。AI 判机器能判的,人判机器判不了的。
全自动又不敢:AI 全判,误放行一笔违规报销就是财务风险。纯人工低效,纯自动不可靠。
这不是个例。任何审批/风控场景都是这个模式:机器预审提效,人工兜底负责。
2. 场景痛点
这个流程的痛点,在财务身上体现得最直接:
- 纯人工低效:合规单据也占用财务时间,一天几十单,审单审到麻木,真正需要判断的单据反而没精力看。
- 纯自动不可靠:AI 全判,误放行一笔违规报销就是真金白银的损失,责任还说不清。
- 规则和 LLM 边界不清:金额超限、重复报销这种确定性校验也走 LLM,又贵又不可解释,审单依据说不出口。
- 驳回无路径:单据驳回后员工不知道错在哪、改完怎么重提,流程卡死。
本质上,「规则优先、LLM 补位」——确定性校验用规则(0 Token 且可解释),LLM 只做规则判断不了的部分。
3. 方案:为什么是规则优先、LLM 补位
Dify 的 code 节点 + LLM + 外部状态存储,正好实现「规则拦截 → LLM 评估 → 分流处理」的审批状态机。选它的理由:
- 规则优先:金额/重复/发票格式用 code 校验,0 Token、可解释、可测试;
- LLM 只补位:只评估备注里的非结构化风险,输出低/中/高三档;
- 状态闭环:自动通过/转人工/驳回重提,状态持久化到 KV,重启不丢。
这篇文章我们就用它搭一个「报销预审应用」:规则拦截 + LLM 风险评估 + 自动放行或转人工。
4. 整体架构
链路很清晰:读审批状态 → 规则校验 → 通过后 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. 实验文档及源码获取
- 实验文档:DIFY-104-08:人机协同审批流——机器预审与人工确认.md
- 源码一(报销预审):dify104_08_01_报销预审.yml
- 源码目录:dify-104/dsl
文章聚焦核心配置与采坑点,完整分步操作与人工审批衔接说明见实验文档原文。
- Dify 企业级实验(01):多应用编排——如何让多个 Dify 应用协同完成一条业务链?
- Dify 企业级实验(02):跨应用状态传递——多轮对话的状态如何跨应用不丢?
- Dify 企业级实验(03):事件驱动流水线——Webhook 与定时触发如何组成异步处理链?
- Dify 企业级实验(04):性能优化实战——长流程从 60 秒到秒回有哪些手段?
- Dify 企业级实验(05):Token 成本控制——AI 应用省钱改造怎么做?
- Dify 企业级实验(06):可观测性体系——日志埋点与监控告警如何落地?
- Dify 企业级实验(07):安全与合规——全链路脱敏与权限分级怎么做?
- Dify 企业级实验(08):人机协同审批流——机器预审与人工确认如何配合?
- Dify 企业级实验(09):复杂业务状态机——订单状态流转与非法跳转防护?
- Dify 企业级实验(10):知识库持续更新闭环——数据飞轮怎么转起来?
- Dify 企业级实验(11):企业 API 工具化——如何把客户系统封装成 Dify 工具?
- Dify 企业级实验(12):外部系统集成——第三方系统如何通过 Dify API 双向编排?