资讯动态

我用 Dify + 本地大模型搭了个发票识别 Agent:开发过程全记录 + 完整文件分享

发布时间:2026/10/20 7:49:42 来源:尧图企业网站定制
摘要本文完整记录了一个基于 Dify 工作流 Ollama 本地视觉模型qwen2.5vl:7b搭建的增值税发票识别 Agent 的开发全过程。全本地运行、不联网、不上传、零 API 成本输出结构化 JSON 并附带自动校验报告。文章详细拆解了 8 节点 8 边的工作流设计、开发中踩过的三个最难的坑、12 个版本的演进历程、本地环境两个必配项以及完整 DSL 文件的获取方式。一、先说清楚这个东西是干什么的把增值税发票PDF 或图片丢进去输出结构化 JSON同时给一份自动校验报告。输出的四个字段{ invoice_result: { /* 发票字段结构化结果 */ }, check_report: 校验报告8 条勾稽规则 需人工复核区块, error_count: 0, notice: }听起来简单。但真正做到「能用」而不是「演示时看着挺好」中间踩的坑比想象多得多。这篇文章记录开发过程也把踩过的坑全部写出来后面会讲文件怎么拿。二、为什么选这套技术栈我一开始想做的是「发票信息结构化提取」纯本地。约束很硬需求为什么是硬约束数据不能出本机发票含公司名称、税号、银行账号属于敏感财务信息不能有 API 费用打算长期当工具用按调用次数付费不现实要能离线有时候网不好或者内网环境基于这三条排除了一堆方案云端视觉识别 API—— 直接排除数据出本机传统 OCR 规则解析—— 试过增值税发票版式种类太多规则写不完一张新票种就崩本地 Python 服务 解析库—— 能跑但依赖重、跨平台难、要单独维护进程最后选了dify 编排 ollama 本地推理dify 负责流程编排、可视化调试、文件上传、变量传递这些自己写要几百行ollama 负责本地推理一条命令装模型不碰 Python 环境单模型完成全流程不需要额外的文档解析服务一个关键决策不写 Python 服务很多教程会教你「dify HTTP 节点调本地 Python Flask 服务做后处理」。我没这么做原因是多一个进程就多一处可能挂。多一个依赖Flask/gunicorn/requests就多一处环境问题。我把后处理全部放在 dify 的代码节点里——纯 Python 沙箱执行无状态不用部署调试时直接看输入输出。代价是不能装第三方库但我的后处理只需要 json / re / decimal标准库够了。三、工作流结构开始 → 按文件类型分流 ├─ 图片 ────────────┐ └─ PDF → PDF转图片 ─┤→ 合并图片 → 发票视觉提取 → 清理与排版 → 字段硬校验 → 结束8 个节点8 条边。下面逐个说为什么这么设计。按文件类型分流条件分支用户可能传 PDF也可能直接传图片截图、拍照。走两条路图片分支直接进入合并节点PDF 分支先经过PDF转图片节点社区插件atoy0m0DPI 设 200为什么不做成「统一先转图片」因为图片再转一次会二次压缩、损失清晰度直接用原图更好。合并图片变量聚合器—— 这个节点不能省这是整个工作流里最容易被人误删的一个节点。图片分支的变量叫start.filesPDF 转出来的图片变量叫pdf2img.files两个名字不一样。而 LLM 节点的视觉输入只能绑定一个变量。变量聚合器的作用是归一哪条分支跑了就取哪条的值两个出口接到同一个输入端口。删掉它会怎样PDF 能跑、图片直接挂。发票视觉提取LLM 节点整个流程唯一的模型调用点。模型必须是qwen2.5vl:7b。配置上几个要点temperature 0—— 保证同一张票多次运行结果一致。如果发现输出复读或啰嗦可以微调到 0.05。提示词里写死了输出 JSON 的完整结构包括每个字段的类型和可空性。清理与排版代码节点模型输出的 JSON 不一定干净。这个节点干三件事1. 去污染—— 半角分号、金额带前缀、大写金额前的⊗防伪符号、备注首尾残留的分隔符2. 字段归属纠偏—— 修「购买方地址电话串进销售方电话」这类问题3. 小数规整—— 把超过 2 位的小数截到 2 位下面会讲为什么需要字段硬校验代码节点纯 Python不调模型。8 条规则明细金额合计 不含税金额不含税金额 税额 价税合计逐行 金额 × 税率 税额逐行 数量 × 单价 金额大写金额 ↔ 小写金额 交叉验证税号位数15/17/18/20 位发票号码位数旧版 8 位 / 数电 20 位开票日期格式这里的设计原则是能用确定性代码做的绝不交给模型。金额加法让大模型算等于自找麻烦——它是概率模型同一个输入两次可能给两个结果。代码节点是确定性的同样的输入永远同样的输出。四、开发中最难的三件事难点一金额校验的容差不能设成 0第一版我容差设 0满心欢喜以为能查出所有错。结果一上线全是误报。排查发现是数电票的计税方式。以价税合计 399,000.00 元、税率 13% 的数电专票为例发票上印的不含税金额是按未舍入值算出来的399000 / 1.13 353097.3451327...票面写的是353097.35已舍入到分。那我拿舍入后的数去反算税额353097.35 × 0.13 45902.6555 → 45902.66票面税额是45902.65。差 0.01 元。这不是识别错误是舍入顺序不同造成的正常尾差。任何真实世界的财务系统都会有这个尾差。所以容差必须存在。最后定税额比较固定容差0.02 元数量 × 单价容差随数量放大0.02 数量 × 0.005第二个为什么也要放大因为单价在票面上只印到 2 位小数。数量是 100 的时候0.005 的单价格式化误差会被放大成 0.5 元的偏差固定 0.02 容差会误报。这条经验很通用财务数据的校验规则一定要区分「错误」和「舍入噪声」。容差设 0 的校验规则等于没有校验规则——它只会给你一屏误报然后你就会习惯性地忽略它。难点二模型没有算错是 PDF 文字层本来就有有一个悬案卡了我好几个版本。某张票模型输出的单价是18584.070796460219十位小数。我第一反应是模型在用「金额 ÷ 数量」倒算而且算得很粗糙。但金额和数量都是模型从图上读的它为什么要自己算一遍除非——它在 PDF 文字层里看到了这个值。验证方法把 PDF 的文字层直接抽出来看。结论是确实有。生成这个 PDF 的工具Suwell OFD 转换器把 OFD 源数据原样写入文本流坐标渲染是正常的但文本流的字符顺序是乱的。于是这个 10 位小数的单价就这么原封不动印在了 PDF 里。模型一个字都没错它只是忠实地把票面上的东西读出来了。处置分两步清理节点把超过 2 位的小数规整到 2 位 →18584.07规整产生的误差由0.02 数量 × 0.005的行容差兜住这里想说的是排查 AI 输出的异常第一步永远是确认「异常是不是本来就在输入里」。我那段时间一直在改提示词、换模型参数方向完全错了。难点三绿色但错误的校验报告上面两个坑修完之后工作流跑得很顺校验报告一片绿。但我在实测样本的时候发现一个让我后背发凉的情况。某张票的校验码票面上印的是72535 77524 00873 31371模型输出72525 77524 00873 31371第 4 位3识别成了2。位数正常、格式合法、前缀分组一致、八条校验规则全部通过。而这个码拿去做发票验真是错的。根因是校验码和密码区是长串无语义的数字没有语义约束、没有勾稽关系。OCR 在这里出错自动校验在原理上就不可能发现。密码区更狠96 位字符串错一个字符照样是绿的。所以我做了一件反直觉的事往报告里加「错误」。0.4.71 起校验报告尾部会根据票面类型自动判断非电子发票纸质专票/普票等有校验码和密码区 → 附加「 来源提示」区块把这两个值单独列出来标注「需人工复核」提醒逐位对照原票电子发票含数电票没有这两个区域 → 不提示避免噪音机器编号、保单号、长账号同理。这条经验我写在这里也想提醒看到这篇的人自动校验通过 ≠ 数据正确。校验规则只能验证「字段之间自洽」验证不了「字段本身被读对了」。前者是逻辑后者是感知——模型在这里是概率的没有确定性的兜底手段。五、版本演进12 个版本修了什么这个项目从 v4.1 到现在 0.4.7212 个版本基本都在跟提取错误搏斗。挑几个有代表性的版本修了什么v4.1发票类型括号全/半角随机漂移备注被省略校验容差过紧导致误报v4.2半角分号污染开户行/账号购买方地址电话串进销售方电话单价被无条件补成.00v4.3销售方地址电话漏提取提示词写「同购买方」模型直接跳过该栏v4.4大写金额前的⊗防伪符号导致校验误报v4.5「复核」被当成别名导致「复核人」字段丢失开户行栏位名被当成值填进去规格型号被填成单位v4.6校验报告新增「需人工复核」区块v4.7输出被截断时容错抢救完整发票对象原来整批丢弃新增notice提示丢失张数0.4.71移除 Word 分支新增「来源提示」0.4.72拆除纯透传聚合器视觉节点直连清理排版8 节点 8 边关于 Word 支持一条从来就没成功过的分支早期版本里有 Word 上传通道。从 0.4.71 起我把它整体删了。原因很直接Word 里内嵌的是发票图片。dify 的文档提取器只能抽取 Word 正文文字发票是图片文档提取器看不见dify 原生也没有「把 docx 内嵌图片喂给视觉模型」的能力所以这条通道从来没成功识别过一张发票。用户传 Word 的结果是流程正常结束输出为空。不是故障是这条路从设计上就通不了。既然通不了留着只会让人以为「支持 Word」。删掉一个功能比加一个功能更需要理由。我删它的理由是它在假装自己能工作。版本号命名约定原来的v4.1 → v4.7到 0.4.70 有点乱要记 0.4.70 就是 v4.7。所以改成三段式0.4.XX第二位沿用原 v4 系架构第三位是流水号0.4.70 v4.7内容完全不变只是改名文件状态标记网盘上的文件我做了状态标记只改文件名不改内容、不改内部版本号标记目录含义.releasedify-agents/dsl/已发布、验证过的版本.betadify-agents/beta/

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑