← 返回文章列表

Dify 实战:供应商报价单格式五花八门,AI 怎么知道哪列是单价?

1. 业务场景

做采购的人都懂这个画面:比价前,桌面上一排供应商发来的报价单,格式几乎没有重样的。

华为的报价单是中文表头:「品名,规格,单价(元),数量,交期(天)」。烽火通信的是另一套叫法:「货物名称,规格型号,金额,数目,交付天数」。到了海外供应商,干脆全是英文:「Vendor, Item, Model, Price, Units, Delivery」。

列数不一样,列名不一样,单位还可能混着来。采购想做的只有一件事:把这三份捏成一张对比表,看谁便宜、谁交期快、谁报价有猫腻。

我们做了个采购比价助手,专门干这件事——上传几家报价单,自动吐出一张对比表、一份异常清单、一个「合计最低」的结论。

这里藏着一个看似简单、实际很要命的问题:AI 是怎么知道「金额」这一列代表单价、「数目」代表数量、「Delivery」代表交期的?

2. 场景痛点

先说最直觉的思路:写规则。

既然每家报价单都有列名,那就定义一个映射表——「单价」对应 unit_price、「数量」对应 quantity、「交期」对应 delivery_days,遇到就认出来。

这个思路在两个地方崩掉。

第一,列名组合是无限的。 「单价」「报价」「价格」「金额」「Price」「Unit Price」「单价(元)」……同一语义有几十种写法,更别说还有「含税单价」「出厂价」这种变体。规则表能写 20 条,写不完 200 条,而且每来一家新供应商,就可能冒出一个没见过的叫法——规则永远在补,永远补不完。

第二,规则表没有容错。 认不出「金额」这一列,数据就丢了;更糟的是,如果「金额」其实是总价(数量×单价的结果),规则会把它当成单价,算出来的比价全错——错得还很隐蔽,采购拿着错误对比表去谈价,灾难。

也有人想用列位置硬编码:第 3 列是单价,第 4 列是数量。这更脆——哪家报价单少一列、多一列,后面全部错位,比价结果直接报废。

那怎么办?

3. 方案:语义识别交给 LLM,计算规则交给 code

我们把问题拆成了两层,交给两个不同的执行者:

理由很简单:识别列语义的本质是语言理解。「金额」「数目」「Delivery」指向什么业务含义,人靠语感能判断,LLM 靠语言模型也能判断——这是它的强项,而且是规则写不出来的强项。而一旦语义被识别出来、数据被归一到统一结构,后面的计算(单价×数量、合计、排序、异常阈值)就必须是确定性的——这一步交给代码,一秒都不能错。

这就是我们一贯的分工原则:语义理解(不确定性强)交给 LLM,计算与规则(确定性)交给 code。

LLM 读到的是列名文本,不是列位置。所以列数不同没关系,列名不同也没关系——它按语义找「哪列是单价」,而不是按第几列找。

4. 整体架构

graph TD A["上传多份报价单\n(格式各不相同)"] --> B["文档提取\nCSV/Excel → markdown 表格"] B --> C["合并文本\n加文件分隔符"] C --> D["LLM 语义抽取\n列名 → 统一 JSON 字段"] D --> E["代码计算\n合计/排序/异常判定"] E --> F["LLM 结论\n最低价供应商"] F --> G["输出\n对比表 + 异常 + 结论"]

链路里两个关键节点:

列名差异在 LLM 这一层就被抹平了。代码永远只面对统一结构,不需要知道「金额」还是「Price」。

5. 模块设计

5.1 LLM 抽取:判定标准 + 输出契约

给 LLM 的指令里,除了输出格式,更重要的是判定标准——告诉它「单价」的定义,而不是「单价」的字面:

请逐份提取每份报价单中的商品报价条目,输出 JSON 数组。

每份报价单输出一个对象:

{"supplier": "供应商名称", "items": [{"name": "品名", "spec": "规格",

"unit": "单位", "unit_price": 单价(数字,无则null),

"quantity": 数量(数字,无则null), "delivery_days": 交期天数(数字,无则null)}]}

提取规则:

1. 供应商名称:取报价单中的公司名/供应商名;没有明确名称则用「供应商N」

2. 单价/数量/交期:从表格中识别;单价是单个商品的价格(去掉货币符号和单位);

   数量是采购数量;交期是供货天数

3. 商品行就是表格数据行;表头行、说明行、合计行不提取

4. 无法识别的字段用 null,不要编造

注意第 2 条:「单价是单个商品的价格,去掉货币符号和单位」——这是语义定义,不是字面匹配。有了这条,「金额」这一列到底是不是单价,LLM 会结合上下文判断:如果表格里没有单独的数量列和总价列,「金额」大概率就是单价;如果「单价」「数量」「总价」三列并存,「金额」就是总价,不能当单价。

第 4 条是底线:识别不了就输出 null,禁止编造。 后面代码层会对 null 字段标异常,数据缺失会被看见,而不是被悄悄算错。

5.2 代码计算:规则硬承载

LLM 输出的统一 JSON 进入代码节点,做四件事:

  1. 计算:金额 = 单价 × 数量;每个供应商的合计 = 所有条目金额之和

  2. 排序:按合计找出最低价供应商

  3. 异常判定:同品名内单价超均价 3 倍 → 标异常;缺单价/缺数量 → 标异常;交期超 30 天 → 标异常

  4. 生成对比表:markdown 表格,一行一个条目

异常判定是确定性规则,不进 LLM——「超均价 3 倍」这个阈值必须每次都一样地执行,不能今天判 3 倍、明天判 2.5 倍。

6. 运行验证

我们用三份「故意刁难」的报价单做了实测——三份的列名风格完全不同:

报价单 表头 识别结果
华为 品名,规格,单价(元),数量,交期(天) 常规中文,单价/数量/交期全部正确
烽火通信 货物名称,规格型号,金额,数目,交付天数 「金额」认出是单价、「数目」=数量、「交付天数」=交期,全部正确
Juniper Vendor, Item, Model, Price, Units, Delivery 全英文,Price=单价、Units=数量、Delivery=交期,全部正确

识别之后,计算也全部准确:

供应商 品名 单价 数量 金额
华为 交换机 1000.00 2 2000.00
华为 光模块 150.00 10 1500.00
烽火通信 核心交换机 8500.00 1 8500.00
烽火通信 光模块 260.00 8 2080.00
Juniper Switch 7800.00 2 15600.00
Juniper Optics 300.00 6 1800.00

合计最低:华为 3500 元——与人工计算一致。三份格式完全不同的报价单,一张表比价,结论可靠。

可靠性靠三重保障兜底:

  1. 语义判定标准——LLM 按「单价是单个商品的价格」的定义去认列,不是查字面

  2. null 不编造——识别不了的字段输出 null,代码层标异常,缺数据可见、不会被悄悄算错

  3. 计算确定性——归一后的所有计算和规则判定在代码层,LLM 的语义错误不会扩散成算错

诚实边界:如果列名完全没有语义线索(比如就叫「列1」「列2」,表里也没有能推断含义的示例值),LLM 也可能认错——这是任何语义识别方案的天花板,包括人在内(人看「列1」也不知道是啥)。但真实世界的报价单不会这样,供应商都会用正常的业务词。

7. 总结

识别「哪列是单价」不是一个解析问题,是一个理解问题。 解析问题可以用规则,理解问题只能交给语言模型——然后用确定性代码把理解的结果锁死。

这个分工可以泛化到所有「格式各异的文件 → 统一结构」的场景:简历初筛(每家简历格式不同)、发票核对(各平台票据版式不同)、工单归类(各渠道话术不同)——都是「LLM 理解语义,code 执行规则」的同构问题。

本文基于真实项目交付经验撰写(Dify 1.16.x 环境)。文中数据均来自我们自己的实测记录(三种不同格式报价单的识别与计算结果),不构成任何平台的官方结论。

联系我

15088711270

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

微信二维码

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