多供应商报价单横向比价与口径对齐
定位与服务边界
这个 Skill 只做一件事——在动手比价之前,先判断这几张报价单的价格数字到底能不能直接比。
拿到 3 家报价,第一件该问的不是「谁便宜」,而是「这三个数字是同一种东西吗」。
币种不同、贸易术语不同、计量单位不同、含税含运不同——任何一项不同,数字就没有可比性。
92 美元/台 FOB 不含税,和 658 元/套 EXW 含税含运,放在一张表里按大小排序,
得出的结论不只是粗糙,而是方向可能反。
它不换算汇率、不算到岸价、不判断哪家划算。它只回答一个问题:这几个数字能不能比。
到岸成本测算(含关税、运费、汇率、退税率)是另一件事。
三分钟上手
第一步:把报价单导出成文本,放进同一个目录
支持 .txt .md .csv .json .ini .properties。pdf / xlsx 不解析,先导出成文本。
第二步:先跑 demo,看清它长什么样
python scripts/quote_cmp.py --demo --out ./out
生成三份口径故意各不相同的示例报价单,并出报告。用它先看懂矩阵怎么读。
第三步:跑自己的报价单
python scripts/quote_cmp.py --path ./报价单目录 --out ./out
加上对照日期,才能查报价是否已过期:
python scripts/quote_cmp.py --path ./报价单目录 --today 2026-10-03 --out ./out
各家字段叫法不一样时传映射(不要改脚本):
python scripts/quote_cmp.py --path ./报价单目录 --out ./out --map my_fields.json
打印映射模板:python scripts/quote_cmp.py --template
输出契约
报价单 3 份(跳过 0),问题:阻断 6 / 警告 5 / 提示 0
已出报告:C:/out/报价单比价对齐报告-20261003-100058.md
同一批报价单不给 --today 时会少 1 条阻断(阻断 5 / 警告 5),因为过期检查被跳过。
引用产出数字时先确认有没有传对照日期。
产出两个文件(文件名带时间戳):
| 文件 | 用途 |
|---|---|
报价单比价对齐报告-<时间戳>.md |
给人看。结论先行 → 横向对齐矩阵 → 问题清单 → 比价之前必须先定的事 |
报价单比价对齐报告-<时间戳>.json |
给程序看。结构见下表 |
JSON 产物字段(给程序消费,逐字段固定)
顶层:generated_at / input / today / counts / quotes / issues
| 路径 | 类型 | 含义 |
|---|---|---|
counts |
对象 | block / warn / info / quotes 四个计数 |
quotes[] |
数组 | 每份报价单一个对象:rel(相对路径)+ kv(字段字典,值一律字符串) |
issues[] |
数组 | 每个问题一个对象,字段固定为下表六个 |
issues[] 元素字段(顺序即下面这个顺序,与内部元组一一对应):
| # | 字段 | 类型 | 含义 |
|---|---|---|---|
| 1 | scope |
字符串 | 归属。单份报价单问题是其相对路径;跨报价单的口径问题是「全文」 |
| 2 | level |
字符串 | 阻断 / 警告 / 提示 |
| 3 | cat |
字符串 | 问题类别,如「口径不一致:币种」「单价×数量≠总价」 |
| 4 | line |
整数 | 行号;跨报价单的问题为 0 |
| 5 | snippet |
字符串 | 现场证据原文 |
| 6 | fix |
字符串 | 建议动作 |
> 内部实现里问题项是一个 6 元组,序列化成 JSON 时转成上面六个命名字段的对象;
> 两者一一对应,不会出现第 4 个字段有时是行号、有时是别的东西的情况。
横向对齐矩阵(报告第二章)
| 供应商 | 币种 | 单位 | 单价 | 数量 | 总价 | 贸易术语 | 含税 | 含运 | MOQ | 交期 | 付款 | 有效期至 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ALPHA TRADING LLC | USD | pcs | 92.00 | 500 | 46000.00 | FOB | 不含税 | 不含运 | 200 | 30 days | 30% T/T… | 2026-10-20 |
| BRAVO INDUSTRIAL CO | CNY | sets | 658.00 | 500 | 329000.00 | EXW | 含税 | 含运 | 500 | 45 days | 100% T/T… | 2026-09-30 |
| CHARLIE SUPPLY SDN BHD | USD | pcs | 118.50 | 500 | 59250.00 | DDP | 不含税 | 含运 | 100 | 20 days | 50% T/T… | 2026-10-25 |
问题分三级:
| 级别 | 含义 | 处理 |
|---|---|---|
| 阻断 | 口径不一致,或该报价单自身有硬伤 | 先对齐口径,再谈价格 |
| 警告 | 条件不同但可能各有道理 | 人工折算后比较 |
| 提示 | 元信息不全 | 补问供应商 |
退出码封闭为三个值:0 完成 / 2 参数或数据错误 / 3 输出目录不可写。
--fail-on-block 且存在阻断时返回 2。
检查项清单
一、口径不一致(阻断级 —— 这些不一致意味着单价不能直接比)
| 字段 | 为什么是阻断 |
|---|---|
| 币种 | 不同币种换算需另算,且汇率波动会改变「谁更便宜」的结论 |
| 贸易术语 | EXW 与 DDP 的价格差可能大于两家之间的价差本身 |
| 计量单位 | pcs / set / carton 混用时,单价差一个数量级 |
| 是否含税 | 含税价与不含税价直接比会系统性偏袒,结论方向可能反 |
| 是否含运 | 含运价与出厂价不可直接比 |
这一层是本 Skill 的核心价值。 它不阻止你比价,而是先告诉你「现在还不能比」。
二、条件不一致(警告级 —— 需人工折算)
数量(单价可比、总价不可比)、最小起订量、交期(要按时间价值折算)、
付款方式(预付比例影响现金流)、包装方式(影响运费与破损率)、质保期(应折进成本)。
三、报价单自身问题
- 缺关键字段:供应商、币种、单价、数量、贸易术语 —— 缺就没法参与比价
- 同一字段自相矛盾:同一份单子里单价出现两个值
- 单价 × 数量 ≠ 总价:偏差 > 5% 报阻断,1%~5% 报警告
- 报价已过期(需
--today)→ 阻断 - 报价临近过期(≤ 7 天)→ 警告
- 无报价有效期 → 警告
- 待定内容残留(
TODO待定待确认)—— 否定语境自动降级
触发与不触发
会触发
- 「这几家报价怎么选」「比价」「询价单对比」
- 「报价单体检」「供应商报价横向对齐」
- 「单价不可比」「这几家口径不一样」
不会触发
| 场景 | 为什么不做 | 该用谁 |
|---|---|---|
| 到岸成本、关税、运费测算 | 需要税率与汇率,脚本明确不内置 | overseas-landed-cost-quote |
| 汇率换算与锁价 | 同上 | 银行 / 财务 |
| 供应商资质、产能、涉诉与征信 | 需要外部数据 | 尽调 / 第三方背调 |
| 判定「哪家最划算」 | 口径不统一时这个问题不成立;统一后还需要战略判断 | 先用本 Skill 对齐口径,再自己判断 |
| 合同条款审查 | 属另一个 Skill | overseas-contract-clause-audit |
| 出口单证一致性 | 属另一个 Skill | overseas-shipping-docs-audit |
边界原则:三家单价不同不是问题,本 Skill 也不会把它报成问题。
问题在于「这三个数字是不是同一种东西」。
扫描范围与漏检边界
工具一定会把它的盲区写出来,否则「报告 0 问题」会被误读成「这份报价单没问题」。
会读的
- 文本文件:
.txt.md.markdown.csv.json.ini.properties - 键值行(
字段: 值)、markdown 表格行、字段 - 值三种写法 - 编码兜底顺序固定为
utf-8-sig→utf-8→gbk→utf-16
不读的(会以「跳过」计数,不会静默吞掉)
- pdf / docx / xlsx / 图片等二进制;伪装成
.txt的二进制会被当文本扫,但不会崩溃 __pycache__.git.svnnode_modules等目录
判不了的(本工具明确不做,也不是缺陷)
| 边界 | 为什么判不了 |
|---|---|
| 只按单价高低排序、判定哪家最划算 | 口径统一后仍需要税费、运费与战略判断,属人工决策 |
31/12/2026 这类 DD/MM 与 MM/DD 歧义日期 |
两种读法都合理,猜错比判不出来更糟,一律不解析并报警告 |
| 同一份单子里阶梯价哪个台阶生效 | 缺你的实际采购量,属商务谈判 |
| 含税与否该由谁承担 | 脚本只能查有没有声明,查不出税负归属 |
| 报价真实性与供应商履约能力 | 需要外部数据,属尽调 |
支持的有效期写法:2026-12-31 2026/12/31 2026.12.31 2026年12月31日 30 Sep 2026。
不存在的日期(如 2027-02-30)会被判为读不出来,而不是当成合法日期继续算——闰年 2 月 29 日会按真实日历判定。
_counts 全为 0 ≠ 这份报价单可以直接下单,只代表上面这些检查项没命中。
常见错误与应对
| 症状 | 真实原因 | 怎么办 |
|---|---|---|
| 报「计量单位不一致」,但三家都写的 pcs | 某份单子的「包装方式」里含 carton 之类词 | 本 Skill 只从显式 Unit 行或数量/单价取单位,不扫全文;若确实没写单位,用 --map 补 |
| 报「是否含税」全部未声明 | 单子只写价格不写含税 | 向供应商问清,或在合同里写死 |
| 单价 92.00 显示成 92 | 输入写的是 92,本 Skill 保留原小数位 | 检查是否导出时丢了尾数 |
| 报「单价×数量≠总价」 | 可能是阶梯价、含税含运、或单位搞错 | 按提示核对;确认是阶梯价就在备注里写明 |
| 报「报价已过期」 | 给了 --today 且有效期已过 |
要求重新出价;这是阻断项,不建议用过期报价决策 |
| 明明写了有效期,却报「无报价有效期」 | 写法是 31/12/2026 这类 DD/MM 与 MM/DD 分不清的格式 |
本工具故意不猜:改成 2026-12-31 / 2026.12.31 / 2026年12月31日 任一写法即可 |
有效期写成 2027-02-30(不存在的日期) |
真实日历里没有这天 | 判为读不出来并报警告,不会拿它去算剩余天数;找供应商确认正确日期 |
| 字段「未识别到」 | 叫法不在别名表 | --template 看格式,写 --map 传进去,不要改脚本 |
| 目录里 pdf 全被跳过 | 不解析二进制 | 导出为 txt 再跑 |
币种显示成 USD/CAD 这类多值 |
单子里并列写了两个币种 | 本身就不该并列,拆单或问清 |
异常与兜底
| 情况 | 行为 |
|---|---|
| 路径不存在 | 退出码 2,不创建任何目录 |
| 目录内没有可检查的文本报价单 | 退出码 2,并列出支持的扩展名 |
| 某份报价单读取/解析失败 | 进「跳过」计数,其余照常比对 |
| 所有报价单都解析失败 | 退出码 2,打印每份的失败原因 |
--today 格式不是 YYYY-MM-DD |
退出码 2 |
--today 是非法日期(如 2026-13-45) |
退出码 2(格式对但日期不存在也要拒) |
--map 不是合法 JSON / 结构不是 {字段: [叫法]} |
退出码 2 |
| 「含税」与「不含税」同时出现 | 否定词优先,读作「不含税」——判反比不判更糟 |
| 金额保留原有小数位 | 输入 92.00 不会被压成 92,避免报告里看着像整数 |
| 某份报价单的有效期是写错的不存在日期 | 判为「有效期无法解析」提示,该份降级、其余照常比对,不打断整批 |
--out 指向一个已存在的文件(不是目录) |
退出码 3,不覆盖该文件 |
| 输出目录写不进去 | 退出码 3 |
--help / 参数写错 |
退出码 0 / 2,被顶层兜底显式捕获(SystemExit 是 BaseException) |
自检:python scripts/quote_cmp.py --selftest —— 83 项断言,覆盖小数位保留、
「不含税」与「含税」必须能区分、单位不被包装描述污染、五类口径不一致必须检出、
条件差异为警告、单价×数量算术分级、报价过期判定、问题项结构不变式。
实战案例
案例一:三家报价,92 / 658 / 118.5 —— 谁最便宜?
需求:拿到三家报价,要选一家。
选型:全量目录扫描 + 横向口径对齐。
数据:三份报价单,各 500 台/套,币种、术语、单位、含税含运各不相同。
命令:
python scripts/quote_cmp.py --path ./报价单 --today 2026-10-03 --out ./out
实际产出(已跑通,真实输出):
报价单 3 份(跳过 0),问题:阻断 6 / 警告 5 / 提示 0
| 级别 | 查出什么 | 为什么这是阻断 |
|---|---|---|
| 阻断 | 口径不一致:币种 | CNY vs USD,没换算不能排序 |
| 阻断 | 口径不一致:贸易术语 | EXW / FOB / DDP 三种,价差可能全在条款里 |
| 阻断 | 口径不一致:计量单位 | pcs vs sets |
| 阻断 | 口径不一致:是否含税 | BRAVO 含税,另两家不含税 |
| 阻断 | 口径不一致:是否含运 | 两家含运,一家不含 |
| 阻断 | 报价已过期 | BRAVO 有效期至 2026-09-30,已过期 |
| 警告 | MOQ / 交期 / 付款 / 包装 / 质保 五项条件不同 | 需人工折算 |
关键点:如果直接按数字排序,658 会被当成最贵;实际上它含税含运、
单位是套、还过期了。先对齐口径再比价,顺序会完全不一样。
这 6 条阻断就是本 Skill 存在的理由。
案例二:单价×数量≠总价,抓到抄错的数
需求:一份报价单,单价 100、数量 500,总价写 60,000。
选型:同单据内的算术自洽校验。
实际产出(已跑通):
- **[阻断] 单价×数量≠总价**:100.00 × 500 = 50000.00,但总价写的是 60000.00(差 20.0%)
关键点:偏差分级是刻意的——超过 5% 报阻断,1%~5% 只报警告。
因为报价里确实常见「单价按含税价、总价按不含税价」这种口径差异,
一刀切报阻断会让人学会忽略这条告警。
已验证清单
以下都是本机实跑结果,不是设计意图:
--selftest83/83 通过(2026-10-05 由 73 增至 83)- 中文日期
2026年12月31日解析为2026-12-31;带空格的2026 年 12 月 31 日同样可解析 - 点分日期
2026.12.31可解析;版本串v1.2.3不会被误读成日期 - 不存在的日期
2027-02-30/2026-02-30/2027-02-29一律判为读不出来(不再放行到下游) - 闰日
2028-02-29按真实日历判为可用 31/12/2026这类歧义写法判不出来,报告会提示改用无歧义格式(刻意不猜)- 有效期为不存在日期时,该份降级为提示、其余报价单照常比对(原为整轮抛 ValueError 中断)
- 错误路径 13 条退出码全部正确(无参数 / 缺 out / demo 与 path 互斥 / 路径不存在 / today 格式错 / today 非法日期 / map 不存在 / map 结构错 / 空目录 / 单目录 / template / help / fail-on-block)
--fail-on-block在有阻断时返回 2--demo稳定产出 3 份报价单:不带--today时 5 阻断 + 5 警告(不查过期,报告会写「对照日期:(未指定,过期检查已跳过)」);带--today 2026-10-03或更晚时 6 阻断 + 5 警告(多出的一条是 BRAVO 有效期 2026-09-30 已过期)- 上述两个数字均 2026-10-04 实跑复核;引用本 Skill 的产出数字时必须同时说明有没有给
--today,否则会差 1 条阻断 - 五类口径不一致(币种/贸易术语/计量单位/含税/含运)全部能被检出并判为阻断
- 「不含税」读作 False、「含税」读作 True、未声明读作 None
- 计量单位不被
Packing: Standard export carton污染 - 金额保留原有小数位:92.00 不变 92、658.00 不变 658、1.125 不变 1.125
- 币种归一:
RMB→CNY;USD / CAD并列时取第一个 - 中文大写金额不参与本 Skill(那是合同体检的判据),金额一律走阿拉伯数字
- 单价×数量偏差 20% 报阻断;相等时不报
- 报价过期报阻断、未过期不报、无有效期报警告
- 比价矩阵真实带上三家单价(92.00 / 658.00 / 118.50)
- 报告章节编号连续无断档(4 章),含对齐矩阵与「不换算汇率」边界声明
- 报告行内无裸三反引号;产物中无 Unicode 替换字符(曾出现过 2 个 U+FFFD,已清除并加扫描)
- 问题项结构一致为 6 元组