ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Dify实战:构建财务报销审核助手,自动检测违规与补全字段

Dify实战:构建财务报销审核助手,自动检测违规与补全字段

[Dify实战] 财务报销审核助手:检测违规、补全字段、生成汇总

先说结论:这个项目做的就是一套基于 Dify 工作流的财务报销审核助手,输入是一段杂乱报销信息(比如微信聊天里转过来的行程说明、发票附言、出差申请),输出是三类东西——是否违规的判定结果、缺什么字段的补全提示、以及按部门/费用类型汇总的报销清单。整套流程靠 Dify 的编排能力串起来,没有写一行业务后端代码,全部在画布上完成。

我大概用了两个下午搭完这套流程,前后改了四版提示词,经历了字段识别错乱、日期格式打架、金额单位混用等一堆坑。这篇就把从需求拆解到工作流配置的完整过程写透,包括每个节点的意图、参数怎么调、提示词怎么收敛、以及遇到问题怎么排查。如果你是第一次用 Dify 做这类文本抽取加审核判断的场景,这篇可以直接当参考手册用。

1. 项目整体设计与思路拆解

1.1 报销审核场景里真正难的是什么

财务报销审核看起来是收票、核对、打款,实际上大量时间耗在信息核对和缺失沟通上。报销人发来一句话“这周去客户那边谈续约,打车花了八十多,住宿七百,麻烦给报了”,里面缺抬头、缺发票类型、缺时间、缺城市,审核人员得反复问;就算信息齐全,还得判断是否符合差旅标准、是否超预算、是否缺附件。这些问题非常分散且高度重复。

我调研过的几个财务系统,通常只有表单录入和审批流,不具备“理解一句自然语言描述并自动提取报销要素”的能力。如果把这件事交给纯大模型对话框,又面临输出格式不可控、结果不稳定、无法跟业务规则挂钩的问题。Dify 的价值在于:把大模型当作“理解引擎”,把节点当作“规则骨架”,让自由文本在受控的流程里被加工成结构化数据。这正是我做这套助手的核心出发点。

1.2 为什么选 Dify 而不是写代码或纯提示词工程

对比过三条路线:

  • 纯 Python 写抽取逻辑:需要维护一堆正则和规则,字段一变就崩,而且没法让业务人员参与调整。
  • 纯提示词工程(单次调用大模型):最快,但每次输出格式都有偏差,金额单位不统一时尤其明显,没有校验环节等于无底洞。
  • Dify 工作流:用节点的编排方式把“抽取—校验—判断—补全—汇总”拆成步骤,每步都有独立提示词和输出 schema,出错时可以单独调试某个节点而不是推翻重来。

我最终选 Dify 还有一个实际原因:它自带调试运行面板,可以逐节点看输入输出,对定位问题是降维打击。写代码调试要打日志看变量,Dify 里点一下节点就能看到实时的 prompt 和返回内容。另一个原因是团队里财务同学可以看懂节点流程,后续调规则不用每次都找我。

1.3 核心目标拆解成可落地的能力项

需求拆解是这套项目里最花时间的部分。我把它拆成了四个能力:

  1. 字段抽取:从非结构化文本中提取报销人、部门、费用类型、金额、日期、城市、事由、是否含发票附件等基础要素
  2. 违规检测:将抽取结果对照差旅标准、费用上限、预算额度等规则,输出违规项和风险等级
  3. 字段补全:识别缺失字段,生成面向报销人的补全问题或默认值建议
  4. 汇总生成:把多条报销记录按部门或费用类型聚合成汇总表,方便财务做日报/周报

这四个能力合在一起,才构成一个“能直接放进工作流”的助手。单独做任何一个都比较简单,合并之后的价值是指数级上升的。

2. 工作流核心配置与节点详解

2.1 工作流整体拓扑

这套助手用的是 Dify 的 Chatflow 类型,入口是一个会话框,用户直接输入报销描述,后端开始走节点;也可以用 Workflow 类型做批处理,一次喂入多行文本逐条处理。Chatflow 更适合“财务和报销人对话式使用”,Workflow 更适合批量场景,建议先做 Chatflow,稳定后再加批量版本。

节点链路由以下部分组成:

开始 → 问题理解与预处理 → 字段抽取(LLM) → 规则判定(Code/条件分支) → 违规检测(LLM) → 字段补全(LLM) → 汇总生成(LLM/模板) → 结束

这个链路并非一成不变。实际项目中,字段抽取和违规检测也可以用同一个 LLM 节点完成,但为了便于单点调试和规则升级,拆开不仅更清晰,还能利用不同模型或不同 temperature 参数来处理不同任务。

2.2 入口参数设计

入口参数就是用户最初输入的那段自然语言文本,我这里命名为raw_text。这个参数会同时被问题理解节点和字段抽取节点使用。

入口变量设计表:

变量名类型说明示例
raw_textString原始报销描述本周去上海拜访客户,高铁票553元,住宿两晚共760元,打车46元
default_departmentString默认部门(可选)销售部
current_userString当前操作人张三

入口参数不一定要多,但default_department和current_user这种兜底信息很有用,因为一段报销描述里通常不会写“我是哪个部门的”,这两个参数从登录态或会话上下文取就可以,避免每条都让用户重输。

2.3 节点1:问题理解与预处理

这个节点用的是 LLM 节点,作用不是抽取字段,而是做三件事:

  • 判断用户输入的是不是报销相关信息(可能有人发“你好”“帮我查一下天气”)
  • 如果输入太短或信息量不足,直接输出“无法处理,请补充信息”
  • 标准化文本格式:去除空格、统一中文标点、将括号内容整体保留

判断是否报销相关是最容易忽略的一步。测试时发现,如果不做这个判断,随便输入“帮我写个周报”,系统也会硬解析出一堆字段,非常荒谬。加上这个节点之后,非报销输入被拦截在流程外。

提示词模板参考(节选):

你是报销审核助理。用户会输入一段与报销相关的自然语言。你的任务是: 1. 判断这段内容是否与报销相关,若完全无关,输出"INVALID"; 2. 若相关,输出"VALID"; 3. 若内容过少(比如只有“打车”两个字),输出"NEED_MORE_INFO"; 只输出以上三个词之一,不要输出其他解释。

这里模型建议用便宜快速的版本,比如gpt-4o-mini或者国产轻量模型,因为这个任务的输出非常有限,不需要重模型。实测中,这个节点通常 2 秒内返回。

2.4 节点2:字段抽取 LLM

这是整套流程最关键的节点。目标是把raw_text解析成结构化 JSON,输出到后续节点使用。

输出结构我定义为:

{ "employee_name": "报销人姓名", "department": "部门", "expense_type": "费用类型:交通/住宿/餐饮/办公/其他", "amount": 123.45, "currency": "CNY", "expense_date": "YYYY-MM-DD", "city": "城市", "purpose": "事由", "has_invoice": true, "has_attachment": false, "missing_fields": ["字段名"] }

这个节点的提示词是最难调的,踩过的坑后面专门讲。这里先给一个核心设计原则:所有字段都要给出明确的判定标准,并且要求模型“没有信息就写 null,不要猜”。

我之前犯过一个错误:让模型“推断”报销人所属部门,结果模型给每个城市都编了个部门,导致后面统计全错。后来改成“仅从原文提取,禁止推断”才好。

另一个细节是金额拆分。一条报销描述里经常混合多个费用类型,比如“高铁553元,住宿760元,打车46元”。我的做法是字段抽取阶段先提取总金额和主要费用类型,在汇总节点再按费用类型明细拆分。如果一开始就要求模型一次性输出明细列表,输出格式容易崩,错误率明显上升。

2.5 节点3:规则判定(Code 节点)

字段抽取完,要先做一次“机器规则校验”,再做“大模型违规判断”。机器规则的作用是保证百分百准确的硬性约束不被模型幻觉影响。

我用的是 Code 节点(Python),输入是上一节点的 JSON 输出,定义了这样几条硬规则:

  • 如果amount为 null 或小于等于 0,标记AMOUNT_INVALID
  • 如果expense_date格式不是 YYYY-MM-DD,标记DATE_FORMAT_ERROR
  • 如果has_invoice为 false 且金额大于 200,标记INVOICE_MISSING
  • 如果expense_type为“住宿”且金额大于 800,标记HOTEL_OVER_LIMIT
  • 如果expense_type为“餐饮”且金额大于 300,标记MEAL_OVER_LIMIT

这些规则本身就是业务方给的,财务会告诉你“普通员工住宿上限 800 元,餐饮一天上限 300 元”,不是模型学出来的,是规则表写死的。用 Code 节点而不是让 LLM 做这步判断,是为了保证“超限”这种硬伤必须 100% 被抓住,不允许模型心情不好就放过去。

Code 节点核心逻辑示例:

def main(raw_data: dict) -> dict: errors = [] warning = [] amount = raw_data.get("amount") expense_type = raw_data.get("expense_type") has_invoice = raw_data.get("has_invoice") if amount is None or amount <= 0: errors.append("AMOUNT_INVALID") if expense_type == "住宿" and amount and amount > 800: errors.append("HOTEL_OVER_LIMIT") if expense_type == "餐饮" and amount and amount > 300: errors.append("MEAL_OVER_LIMIT") if has_invoice is False and amount and amount > 200: errors.append("INVOICE_MISSING") return { "errors": errors, "warning": warning, "passed": len(errors) == 0 }

需要说清楚的是:这里amount我定义为每条报销单的总金额,而不是明细金额。如果一条输入里“高铁553元+住宿760元”,按场景拆分更好,但初版为了控制复杂度,先按单条记录处理;如果要支持多条混合描述,需要在字段抽取节点追加一个items数组。

2.6 节点4:违规检测 LLM

Code 节点抓硬伤,LLM 节点用来判断“语义层面”的违规。比如:

  • 事由和费用类型不匹配:“购买办公用品”但费用类型选了“餐饮”
  • 报销描述中提到的出差城市与申请单上的城市不一致
  • 描述中带有“加急费”“找朋友代开”这类可疑措辞

这个节点不用输出太多字段,只输出:

{ "violations": [ {"type": "RULE_VIOLATION", "level": "high", "description": "xxxx"} ], "risk_level": "high/medium/low" }

节点输入是字段抽取结果和硬规则校验结果,提示词里要求模型只关注“逻辑不一致”,不要重复 Code 节点已经判断过的硬规则。这样分工明确,模型输出也更稳定。实测证明,给模型减轻负担后,违规检测的准确率有明显提升——它不需要再计算金额是否超限,只盯着语义矛盾就行。

2.7 节点5:字段补全 LLM

字段补全节点的目标很明确:针对missing_fields里的字段,生成一句自然语言问题,让报销人补充回答。比如:“您提交的报销记录缺少发票信息,请确认是否已取得发票,并补充发票号码。”

这个节点还可以做一步“默认值建议”。比如缺少expense_date,但描述里有“上周五”,LLM 可以计算出具体日期并提示“系统将自动使用 2025-04-11 作为报销日期,如不正确请指出”。这种方式能减少来回沟通的次数,直接给报销人确认即可。

补全节点输出:

{ "questions": ["缺少发票信息,请补充发票号?", "缺少出差城市,请补充是哪个城市?"], "suggested_values": {"expense_date": "2025-04-11"} }

这一步做得好不好,直接影响用户体验。很多财务助手“一问一答”,用户回答完一个字段又追问另一个字段,体验很差。我的做法是:一次性把所有缺失项列出来,统一问,提高效率。

2.8 节点6:汇总生成

汇总节点负责把多条报销记录聚合成一个简洁的汇总结果。这里我用的是 LLM 节点加模板变量,模板变量里填的是前面节点的输出。

汇总输出文本示例:

【报销汇总单】 报销人:张三 部门:销售部 费用类型:交通 553元、住宿 760元、打车 46元 合计:1359元 风险等级:medium 补全提醒:缺少发票信息(合计超200元,需补充)

这里的模板不是简单字符串拼接,而是把 JSON 转成易读文本再做一次格式化。实测中直接拼 JSON 会又长又乱,转成自然语言模板后,其他人也更容易阅读和转发。

3. 提示词工程与数据处理的精修

3.1 提示词收敛的三个阶段

我前后迭代了四版提示词。第一阶段是“自由发挥”式,让模型随便输出,结果字段名乱飞,一会儿employee_name一会儿name;第二阶段开始增加“字段定义表”,效果明显改善;第三阶段增加“禁止推断”;第四阶段增加“输出格式说明必须合法 JSON”。

一个比较实用的技巧是:给字段定义表的时候,不仅写“字段名”,还写“这个字段在原文中通常长什么样”。例如:

expense_date:报销发生日期,原文可能体现为“4月11日”、“上周三”、“2025-04-11”、“今天”等。

这样模型对日期格式的解析准确率提升明显,因为“上周三”这种表达在普通 JSON Schema 里没有对应关系,必须靠自然语言解释。

3.2 JSON 格式稳定性保障

LLM 节点输出 JSON 经常出问题,常见的问题是:多输出了解释性文字、JSON 里带了注释、字段值带了中文顿号而不是英文逗号。Dify 的 LLM 节点可以在输出变量处定义 JSON 结构,启用“结构化输出”功能就能强制模型按 JSON Schema 输出。

如果没用 Dify 的结构化输出功能,那就在提示词末尾加一条强约束:

只输出 JSON,不要输出 markdown 代码块标记,不要输出任何解释性文字。JSON 必须是合法的,便于机器解析。

实测下来,加上这句能减少 80% 的格式错误。另外建议在提示词里加一个示例(few-shot),一个正面示例加上一个负面示例,效果比十行规则描述都好。

3.3 日期格式与金额单位处理

这是所有字段里最容易翻车的两个。日期方面,我最终的方案是在字段抽取节点的提示词里给定“标准格式转换规则”:

  • 中文数字(四月十一日)转成数字
  • 相对时间(上周三)基于系统当前日期推算
  • 时间范围(4月10日到4月12日)取第一天作为 expense_date

金额方面,必须处理“元/块/毛”的混用,描述里可能出现“八十多”“553块钱”“760元左右”的说法。策略是要求模型输出纯数字,只保留两位小数,null的情况单独判断。这里需要特别强调的是“左右”的处理,我要求模型直接忽略“左右”这类模糊词,按最接近的整数处理,并在missing_fields里不标记为缺失——因为实际报销场景里“左右”并不影响报销单录入,但必须在汇总里体现“以实际发票为准”的提示。

金额单位统一在 Code 节点里再做一次兜底校验:如果amount是字符串且包含“元”“块”,会报AMOUNT_FORMAT_ERROR,再把字符串交给 LLM 节点处理格式转换,避免模型输出“553元”这种带单位的值直接参与后续计算。

3.4 变量作用域与传递设计

Chatflow 里创建自定义变量,中间结果都存进去。我用了三个中间变量:

  • extracted_data:存储字段抽取节点输出的 JSON
  • check_result:存储规则判定和违规检测的结果
  • final_reply:存储最终回复文本

好处是后续加节点时不需要把前面的节点链接全改一遍,直接引用变量即可。改流程的时候特别方便,不用一行行查节点 ID。

4. 实操过程与调试实录

4.1 第一个完整 Demo:从搭建到跑通

第一步在 Dify 里创建空白 Chatflow 应用。入口变量配置成功后,依次添加 LLM 节点做问题理解、字段抽取、规则判定、违规检测、字段补全,最后模板节点生成汇总。每个节点的输入输出都在流程面板上直接连线,非常直观。

第一次跑通大概用了 40 分钟。期间最大的问题是变量类型不匹配——字段抽取节点输出的是一个 Object,但规则判定节点输入时手误选了 String,导致程序一直报“类型错误”。排查时逐个节点点开看输入输出,才发现是连线连错了节点。Dify 的连线非常灵活,但这也意味着容易不小心连错,我的建议是连接完一条线就立刻跑一次测试,别等全部连完再跑。

4.2 测试样例与结果复盘

选了三条典型样例做测试,从简到难:

样例一(完整信息):

张三,销售部,4月11日去上海拜访客户,高铁553元,住宿760元,打车46元,发票已开

样例二(缺字段):

这周四出差去杭州,住宿两晚花了七百多,高铁票三百多,还没拿到发票

样例三(违规信息):

项目聚餐,四个人吃了880元,在一家高档餐厅,有发票

初次测试结果:样例一全对,样例二日期解析成了“undefined”,样例三没有触发餐饮超限规则,因为 Code 节点写的是“餐饮金额大于 300 报错”,但实际逻辑应该判断“人均是否超限”,四个人880人均220,确实没超。

这两个问题很有代表性。

日期解析问题出在提示词没有明确说明“上个月”“这周四”应该怎么算。修正提示词后,加了一句“相对日期请基于当前系统时间推算,当前日期是 {today}”,并让模型输出“未提及时返回 null”。

餐饮超限问题是规则定义太死。后来跟财务确认,餐饮报销通常按人均,而不是一餐总额。于是 Code 节点新增了person_count字段,如果存在“N个人”的表达,就用总额除以人数再对比标准。

修正后的规则:

if expense_type == "餐饮" and amount and person_count and (amount / person_count) > 100: errors.append("MEAL_PER_PERSON_OVER_LIMIT")

顺便再分享一个调试思路:Code 节点的输出不要只返回结果,最好把计算中间量也带出来,比如“人均金额=220,限额=100”。这样后续问题排查时能直接看到为什么判定违规,不用再反向推导。我现在所有 Code 节点都会返回debug_info字段。

4.3 调优后的最终效果

修正之后重新跑三个样例,结果全部符合预期。样例二被成功判断为缺字段,并给出补全问题;样例三被判断为人均餐饮超标,风险等级为高。

接下来测试了一批批量数据(100条),把 ChatGPT 生成的模拟报销文本逐一走接口调用。最终字段抽取的准确率大概在 90% 以上,剩下的 10% 基本集中在“金额单位混乱”和“一句话里包含多个费用明细”的场景。后者目前建议在输入规范层面引导用户分条填写,而不是靠模型硬拆。

5. 常见问题与排查技巧实录

5.1 问题速查表

现象可能原因解决方式
模型输出 JSON 带注释或解释文字提示词约束不够强启用 Dify 结构化输出;提示词写明“只输出 JSON”
金额字段是字符串带单位字段抽取提示词没有明确“输出数字类型”在提示词加“amount 为 float 类型,不要带货币符号”
日期显示 undefined相对日期(“上周三”“这周四”)无换算规则在字段抽取提示词中注入当前日期
硬规则不触发规则定义粒度过粗,比如餐饮未考虑人数细化 Code 节点逻辑,增加人数/天数等参与计算
汇总模板乱入 JSON 原始内容模板变量类型直接引用了 Object在汇总节点前加一个模板/文本处理节点,将 Object 转成易读文本
一条描述里多个费用类型识别不全字段抽取阶段只输出总额增加 items 数组结构,按费用类型拆分
非报销内容被解析出字段缺少意图拦截加上问题理解节点,输入无关内容直接输出 INVALID
变量类型不匹配报错连线连错或手动选了错误类型逐个查看节点输入输出;建议连接完一条线就测试一次
报销人名字和部门名相似导致抽取混淆模型无法区分谁是报销人谁是部门在字段定义中加入“报销人通常是人名,部门通常是公司内设部门名”

5.2 调试工具的使用技巧

Dify 的调试功能比较完善。对话流界面右侧可以实时查看每个节点输入输出,我调 Prompt 时都是开两个页签同时修改。另一个好用的功能是“运行历史”,可以回看之前某一轮跑的完整链路,不用重新输入一遍。遇到抽风的时候,直接翻历史比对上一轮和这一轮的差异,比一步步重跑快很多。

5.3 踩过的三个最坑的点

第一,不要在同一个节点里既让模型抽取字段又判违规。这两个任务的优化方向完全不同——抽取追求“忠实原文”,违规判断追求“业务逻辑合理性”。合在一起,模型容易自我矛盾,而且抽取出错时你无法判断是抽取问题还是判断问题。拆开后,哪个节点错了就只调哪个节点的提示词,效率高得多。

第二,不要盲目追求复杂的 prompt 模板。草稿版本我把所有规则都塞进一个提示词,变量引用一塌糊涂,调试时根本不知道是哪个环节出了问题。后来把规则拆到 Code 节点,提示词只留任务描述和输出格式,清爽得多。复杂逻辑交给代码,自然语言只做表达。

第三,“补全字段”不要只靠模型,也要有规则兜底。比如has_invoice字段,模型可能不确定,但规则可以判断“金额大于 200 却没有发票标记”就强制触发补全。模型给出“无法判断”时,规则应当接管,而不是让模型硬猜一个结果。

6. 配套资源与扩展方向

6.1 资源包内容说明

这套项目的配套资源包含三类:

  • 工作流 DSL 文件:可在 Dify 中直接导入,包含全部节点配置和变量定义,导入后按实际业务改一下部门名单和审批人配置即可用
  • 提示词模板集合:每个节点的稳定版提示词,带参数说明和多版本对比,方便按自己的场景改写
  • 字段映射配置表:把业务字段(报销人、费用类型、金额、日期等)与模型输出字段的映射关系整理成表格,方便对接内部财务系统

团队内部试用时,财务同事最关心的是“违规规则能不能改”,我将所有规则统一收敛到 Code 节点和少数几个变量里,强调过“只改配置不改流程”的原则。

6.2 已探明的后续扩展方向

这套流程目前是 Chatflow 单条处理模式,再往下扩展有三个方向:

一是批量导入模式。把入口从会话框改成文件上传,解析 Excel 后逐行走工作流,并在最后追加一个总汇总节点。这个对财务月末集中审核非常有用,可以把一整天的工作量缩短到几分钟。

二是知识库辅助审核。把公司差旅制度、审批流程 SOP 上传到 Dify 知识库,作为审核节点的参考资料,让模型引用具体条款来自动判断“超限”“不合规”。这里对知识库分块策略要求较高,每个块最好对应一个完整条款,避免截断导致上下文混乱。

三是与审批系统联动。通过 Webhook 将最终审核结果推进到内部审批流,Dify 直接把结果 POST 出去。做这个之前建议先确认企业审批系统的接口文档,避免在接口规范上踩坑。

6.3 小结之外的个人体会

用 Dify 搭这套财务报销审核助手之后,最大的感触是:这类“自然语言理解+规则校验”的场景,真正的难点不在模型能力,而在流程设计和字段定义。你花在拆流程、定字段、写 Code 规则上的时间,远多于调提示词的时间。把业务规则说清楚、把输入输出边界定死,比追求一个“万能提示词”重要得多。

如果你也想做类似的东西,我的建议是先找财务要一份真实的报销单模板和差旅标准,哪怕只有两页纸,也比自己拍脑袋设计十个字段强。流程搭完先用三个月的历史数据做回放测试,跑通后再开放给业务使用,起步不要急,慢慢迭代就是了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进