Dify 高级实验(07):智能审批——如何让机器自动完成流程审批?
1. 业务场景
先讲一个我们实际遇到的场景。
一家中型公司的财务部,每天收到几十条报销申请。员工在 OA 里写一段话:「报销 5000 元差旅费,市场部王五 E1001」——然后这笔钱就要经过「领导审批 → 财务审核」两道人工关卡。金额大的要领导签字,金额小的领导也得逐条看;审批标准全凭领导个人判断,同样的金额有的领导批有的领导卡,员工私底下抱怨「报销像开盲盒」。财务总监算过一笔账:一笔小额报销从提交到打款平均要走 3 天,其中 90% 的时间花在等人审批上。
我们第一次听到这个需求时,第一反应也是「审批流嘛,OA 里配个条件分支就有了」。后来把规则一条条列出来才发现——金额四档、招待费特殊规则、按部门路由,这些判断维度全是能写死的确定性逻辑;真正卡住自动化的不是「判定」,而是「读懂一句话里的金额和事由」。这一步,正好是参数提取器的活。
这不是个例。任何「规则可描述的流程审批」都是这个模式:报销审批、请假审批、采购审批、工单分级、风控拦截——金额、类型、职级这些判断维度都是确定的,完全可以用规则描述,却偏偏靠人一层层手工流转,慢且标准不统一。
2. 场景痛点
这个流程的痛点,在财务和员工身上体现得最直接:
- 审批慢:小额报销也要排队等领导审批,3 天的流转里 90% 是等待时间——员工垫资周期长,影响积极性,紧急支出甚至被卡住。
- 标准不统一:审批全凭领导个人把握,同样的报销金额不同领导批法不同——员工摸不清规则,财务也难以解释「为什么他批了我不批」。
- 人工判断易漏:招待费超 500 元即使小额也该走审批,这种特殊规则靠人记,漏一次就是合规风险——规则越多,人越记不住。
- 流程不可追溯:每笔审批谁批的、按什么规则批的没有沉淀——审计时说不清,出了合规问题只能翻聊天记录。
本质上,审批的瓶颈不是「该不该批」,而是「规则明确的判断不该让人逐条做」——金额档位、费用类型、特殊规则都是确定性逻辑,机械化执行正是代码的强项。
3. 方案:为什么是这套规则路由范式
Dify 工作流里的「参数提取 → 代码规则引擎判定 → IF/ELSE 三路分支」链路,把审批从「人等判断」变成「机器秒级判定」。
选它的理由,我们实际对比过:
- 参数提取器把自然语言变结构化:员工用一句话提交报销,PE 自动抽出金额、费用类型、部门、事由、员工编号五个字段——不用改 OA 表单,现有提交流程零改造;
- 规则引擎代码化,标准统一可审计:金额四档(无效/小额/中额/大额)+ 特殊费用类型规则全部写进代码节点,同一套规则对所有人生效,判定结果可追溯;
- 三路互斥分支,各分支独立收尾:自动通过 / 需审批 / 转人工各走各的结束节点,输出不同结果——小额秒批、大额转总监,天然符合企业实际。
这篇文章我们就用它搭一个「报销审批机器人」:员工提交报销申请文本,机器按规则自动判定走哪条审批路径。
4. 整体架构
链路很清晰:入口收报销文本 → 提取结构化字段 → 规则引擎判定 → 三路分支收尾。规则引擎是这个架构的决策核心——所有审批标准集中在一个代码节点里,改规则只改一处,全流程生效。
5. 模块设计
5.1 参数提取器(pe_extract)
注意 PE 的查询字段名是
query(与知识库/分类器的
query_variable_selector
相反),reasoning_mode: function_call,五个参数声明:
- data:
instruction: |
从以下报销申请内容中提取字段信息。注意:
- amount 是报销金额(数字,单位元)
- expense_type 是费用类型(差旅/办公/招待/设备,如无法归类填"其他")
- department 是所属部门
- reason 是报销事由(简要概括)
- employee_id 是员工编号(如 E1001)
- 如果某个字段没有明确信息,填 null 而不是编造
parameters:
- description: 报销金额(元)
name: amount
required: true
type: number
- description: 费用类型(差旅/办公/招待/设备)
name: expense_type
required: true
type: string
# department / reason / employee_id 同理……
query: [start, expense_request]
reasoning_mode: function_call
title: 参数提取
type: parameter-extractor5.2 审批规则引擎(cd_rules)
入参先 float() 防御转换(PE 提取的 number
到代码节点可能是字符串),输出 action 供分支路由:
def main(amount, expense_type, department):
amount = float(amount) if amount else 0
result = {"amount": amount, "action": "", "approver": "", "reason": ""}
if amount <= 0:
result["action"] = "reject"; result["reason"] = "金额无效"
elif amount < 1000:
result["action"] = "auto_approve"; result["reason"] = "小额自动通过"
elif amount < 10000:
result["action"] = "need_approval"; result["approver"] = "部门经理"; result["reason"] = "需部门经理审批"
else:
result["action"] = "human_review"; result["approver"] = "财务总监"; result["reason"] = "大额需财务总监审批"
# 特殊规则:招待费即使小额也需审批
if expense_type == "招待" and amount > 500:
result["action"] = "need_approval"; result["approver"] = "部门经理"
return result5.3 三路分支(cond_route)
字符串比较用 is(不是
=/==);case_human 是 OR
条件,同时覆盖 human_review 与
reject;所有被边引用的 case_id 必须在 cases
中定义:
- data:
cases:
- case_id: case_auto
conditions:
- comparison_operator: is
value: auto_approve
variable_selector: [cd_rules, action]
logical_operator: and
- case_id: case_need
conditions:
- comparison_operator: is
value: need_approval
variable_selector: [cd_rules, action]
logical_operator: and
- case_id: case_human
conditions:
- comparison_operator: is
value: human_review
variable_selector: [cd_rules, action]
- comparison_operator: is
value: reject
variable_selector: [cd_rules, action]
logical_operator: or
title: 审批路由分支
type: if-else5.4 三个独立结束节点
多 End 的 variable
必须唯一:result_auto / result_need /
result_human,各取本分支输出:
- data:
outputs:
- type: string
value_selector: [lm_approve, text]
variable: result_auto
title: 结束(自动通过)
type: end6. 运行验证
| 输入(expense_request) | 期望行为 | 实测 |
|---|---|---|
| 报销 500 元交通费,市场部张三 E1001 | action=auto_approve,LLM 生成「已自动通过」通知 | 与预期一致 |
| 报销 5000 元差旅费,销售部李四 E1002 | action=need_approval,通知部门经理审批 | 与预期一致 |
| 报销 50000 元设备采购,技术部王五 E1003 | action=human_review,生成审批单并模拟推送,审批人财务总监 | 与预期一致 |
| 报销 800 元招待客户,市场部赵六 E1004 | 特殊规则生效:action=need_approval(而非自动通过) | 与预期一致 |
7. 实战坑
| 坑 | 现象 | 修复 |
|---|---|---|
多个 End 节点 variable
重名 |
三个结束节点全叫 result
时校验脚本查不出(双脚本全绿),运行结果互相覆盖 |
每个 End 用唯一 variable(result_auto/result_need/result_human),validate_dsl.py 已加跨节点检查(dify103_07 实测教训) |
IF/ELSE 字符串比较用
=/== |
Pydantic 校验报错(字符串只支持
is/contains 等),旧 DSL 迁移后运行才炸 |
字符串一律 is;cases
必须定义所有被边引用的 case_id,否则该分支 UI 无连线(dify02_02
迁移实测) |
| 下游引用 PE 输出用错字段 | 引用 pe_extract.text
取不到值——校验器对 PE 只认 parameters 里声明的 name;PE 的
text 输出也不能作为 end/selector 来源 |
只引用 parameters 里声明的
name(amount/expense_type/employee_id);要全量 JSON 用代码节点
json.dumps 打包(dify08_01 实证) |
| 代码节点入参类型不符 | PE 提取的 number
传进代码是字符串,"12000" > 10000 报
TypeError: '>' not supported between instances of 'str' and 'int' |
代码内
float(amount) if amount else 0 防御转换;同时 main()
参数名必须与 variables 的 variable 名一致(按名传参) |
8. 实验文档及源码获取
- 实验文档(完整操作步骤):DIFY-103-07:智能审批工作流.md
- 源码(可直接导入):dify103_07_智能审批工作流.yml
文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。
- Dify 高级实验(01):智能文档处理——如何把非结构化文档变成结构化数据?
- Dify 高级实验(02):对话式 BI 分析——如何让业务人员用自然语言直接查数?
- Dify 高级实验(03):智能客服——如何让 AI 自动应答并把疑难问题转人工?
- Dify 高级实验(04):RAG 增强问答——如何让知识库问答更准还能多轮追问?
- Dify 高级实验(05):多步推理——如何把复杂问题拆解成子问题逐个击破?
- Dify 高级实验(06):自动化报告——如何让系统定时自动生成周报?
- Dify 高级实验(07):智能审批——如何让机器自动完成流程审批?
- Dify 高级实验(08):智能告警——如何用 AI 替代固定阈值监控?
- Dify 高级实验(09):测试用例生成——如何让 AI 自动产出测试用例?
- Dify 高级实验(10):综合实战——如何从零搭建一个企业级智能客服平台?