出口单证齐套与一致性体检
定位与服务边界
这个 Skill 只做一件事——在把一批出口单据交给货代、报关行、银行或客户之前,把单证之间对不上的地方一次挑干净。
它不做报关申报、不算税、不查汇率、不解释贸易术语的法律含义。出口单证事故里有很大一类根本不是「哪张单错了」,
而是两张单各自都没错,但彼此对不上:发票金额 46,800、投保单写 46,000;提单 118 件、装箱单 120 件;
产地证 HS 写 10 位、发票写 7 位。放行前没人交叉核一遍,这些就是退单或事后被追责的起点。
它也不判断商业条款是否合理,不替代货代、报关行、银行或律师的审核。它只做确定性的字符串与数值比对。
三分钟上手
第一步:把单据放进一个目录
支持文本类:.txt .md .csv .json .ini .properties。
pdf / xlsx 不解析——先把内容导成文本(或另存为 .txt)再跑。
第二步:先跑 demo,看清它长什么样
python scripts/doc_audit.py --demo --out ./out
会生成一套示例单证(含刻意埋进去的问题)并出报告。用它先知道报告长什么样,再换成自己的单据。
第三步:跑自己的单据
python scripts/doc_audit.py --path ./单证目录 --out ./out
指定必须齐备的单证类型(可选,但建议):
python scripts/doc_audit.py --path ./单证目录 --out ./out --require invoice,packing_list,bill_of_lading,origin_certificate
单据字段叫法跟内置别名不一样时,传一份映射(不要改脚本):
python scripts/doc_audit.py --path ./单证目录 --out ./out --field-map my_fields.json
my_fields.json 长这样:
{
"seller_ref": ["客户参考号", "cust ref", "客户号"],
"no_of_packages": ["箱数", "总箱量"]
}
打印一份模板看格式:python scripts/doc_audit.py --template
第四步:接进流水线(可选)
python scripts/doc_audit.py --path ./单证目录 --out ./out --fail-on-block
有阻断级问题时退出码返回 2,可以直接当 CI / 交付前卡点。
输出契约
命令结束时 stdout 两行:
单证 5 份,问题:阻断 2 / 警告 2 / 提示 0;跳过 0 份
已出报告:C:/out/单证齐套与一致性报告-20261003-094414.md
产出两个文件(文件名带时间戳,多次跑不会互相覆盖):
| 文件 | 用途 |
|---|---|
单证齐套与一致性报告-<时间戳>.md |
给人看。结论先行 → 单证清单 → 单证内问题 → 跨单证比对 → 字段识别情况 |
单证齐套与一致性报告-<时间戳>.json |
给程序看。counts + 每份单证的 kv 与 issues + cross + skipped |
问题分三级,含义固定:
| 级别 | 含义 | 处理 |
|---|---|---|
| 阻断 | 必然被退单 / 拒付 / 卡放行,或单证自相矛盾 | 交单前必须改 |
| 警告 | 双方口径可能不同,不一定是错 | 人工确认一次即可 |
| 提示 | 类型未识别、字段未覆盖等元信息 | 完善配置用 |
报告章节编号按实际内容动态生成:某章没有内容就不出现,编号不会出现断档。
退出码封闭为三个值:
| 码 | 含义 |
|---|---|
| 0 | 体检完成(无论有没有发现问题) |
| 2 | 参数 / 数据错误(路径不存在、JSON 非法、--require 含未知类型、字段映射结构错) |
| 3 | 环境错误(输出目录不可写、报告写入失败) |
--fail-on-block 且存在阻断时,退出码为 2。
检查项清单
单证内(逐份)
- 必填字段缺失——按单证类型判定(发票要有发票号/总额/币种/开票日/发货方/收货方/品名/HS 编码;提单要有起运港/目的港/件数/毛重/收货人/提单日……)
- 同一逻辑字段在同一份单证里出现两个不同值 → 阻断(最常见也最致命:两页纸自己跟自己打架)
- 金额 / 毛重 / 净重 / 体积 / 件数为零或为负
- 净重大于毛重;净重占毛重低于 50% 时给警告(多半是毛重漏填了整柜或整托)
- 币种符号与 ISO 代码混用;多种符号 / 多种币种代码并列(
RMB与CNY会先归一再比,否则比不出来) - 占位符残留(
TODOTBDFIXMEXXX待补充待定***)——否定语境自动降级 - 残留的
**加粗标记(排版没改完) - UTF-8 BOM 残留
- 跨单证日期逻辑:开票日晚于装运日(警告);装运日晚于最迟装运期(阻断)
跨单证
- 发票号、合同号、币种、件数、毛重、净重、体积、起运港、目的港、收货人、柜号、HS 编码、付款方式、贸易术语 —— 同一字段值不一致
分级规则(这一条是有讲究的):
| 字段 | 级别 | 原因 |
|---|---|---|
| 币种、发票号、柜号、起运港、目的港、HS 编码、毛重、净重 | 阻断 | 对不上必然被退单 / 拒付 |
| 件数、体积、合同号、付款方式、贸易术语 | 警告 | 拼箱并箱时提单按箱计、装箱单按小件计,本就该不同;判阻断会天天误报 |
齐套性
--require指定的单证类型是否齐备- 单证类型识别不出来的,会在报告里单列,提示用
--field-map补别名
触发与不触发
会触发
- 「这批出口单据帮我查一下」「发票和提单对不上」
- 「报关资料体检」「交单前自检」「单证齐套性」
- 「装箱单和提单件数不一样」
- 「产地证 HS 编码和发票写的不一样」
- 「单据有没有漏填字段」
不会触发
| 场景 | 为什么不做 | 该用谁 |
|---|---|---|
| 实际报关申报、税费计算 | 需要海关编码与汇率,本 Skill 明确不内置 | 报关行 / 单一窗口 |
| 汇率换算、汇率风险 | 同上 | 银行 / 财务 |
| 贸易术语的法律含义与风险分配 | 需要合同全文与法律判断 | 律师 / 合同审查 |
| 合同条款体检 | 属另一个 Skill | overseas-contract-clause-audit |
| 译文与单位本地化 | 属另一个 Skill | overseas-localization-audit |
| 识别单证造假 | 这是取证问题,不是格式问题 | 专业机构 / 司法途径 |
边界原则:认不出的字段就报「未识别」,绝不猜。 猜错一次比漏报一次危险得多——
它会让使用者以为已经检查过了。
常见错误与应对
| 症状 | 真实原因 | 怎么办 |
|---|---|---|
| 报告说「字段未识别到」,但你明明写了 | 你的字段叫法不在别名表里 | --template 看格式,写一份 --field-map 传进去,不要改脚本 |
| 必填字段报缺失,但单据上就是有 | 同上,多半是别名问题 | 同上;确属真缺才改单据 |
| 「跨单证不一致:件数」老报错 | 拼箱/并箱下本就不同 | 这是警告不是阻断,确认口径一致即可;要消掉就在两份单据上统一口径 |
| 币种一处 USD 一处 RMB,没报币种冲突 | RMB 归一到 CNY 后确实一致 |
这是正确行为;真要区分币种请改单据 |
| 目录里 pdf 全部被跳过 | 脚本不解析二进制 | 导出为 txt 再跑 |
| 提单日期识别不出来 | 提单日格式太怪(如 26 SEP 2026 手写体),或是 01/09/2026 这类 DD/MM 与 MM/DD 分不清的写法 |
归一不了就手改成 2026-09-26;歧义写法本工具故意不猜,见「支持的日期写法」 |
| 中文日期被报成「日期逻辑倒挂」,但顺序明明是对的 | 旧版把没归一化的日期按字符串比大小,会把 9 月比成晚于 10 月 | v1.0.1 已修:先归一化再比对;任一边读不出来就不判(判不出来优于判错) |
| 报了个看不懂的字段名 | 键名被包含匹配兜到了 | 看报告「字段识别情况」章;把 --field-map 里写更精确的叫法提高优先级 |
| 输出目录写不进去 | 权限问题 | 退出码 3,换个目录 |
| 同一批单据跑了两次,报告堆了两份 | 文件名带时间戳,故意不覆盖 | 按时间戳挑最新的那份 |
支持的日期写法
日期先归一化成 YYYY-MM-DD 再参与跨单证比对。认不出来的写法一律不参与比对——
按字符串比大小会把 9 月比成晚于 10 月,凭空造一条假告警,比判不出来更糟。
| 写法 | 支持 | 例 |
|---|---|---|
2026-09-26 / 2026/09/26 |
✅ | 中英文单证通用 |
2026.09.26 |
✅ | 需前后不接数字,避免把版本串误读成日期 |
2026年9月26日 |
✅ | 中文单证最常见写法(v1.0.1 新增) |
26 Sep 2026 |
✅ | 英文单证 |
01/09/2026 |
❌ 不猜 | DD/MM 与 MM/DD 无法区分,两种读法都合理 |
2027-02-30 |
❌ 判为读不出来 | 真实日历没有这天;闰年 2-29 按真实日历判定 |
异常与兜底
| 情况 | 行为 |
|---|---|
| 目录里一个可检查的文件都没有 | 报一条阻断「无可检查单证」,说明支持哪些扩展名 |
| 路径不存在 | 退出码 2,stderr 打印原因,不创建任何目录 |
| 某份单证读取失败 | 进 skipped 清单,单条失败不中断,其余单证照常体检 |
| 单证内容是二进制乱码 | 编码兜底链 utf-8-sig → utf-8 → gbk → utf-16,全失败则按 utf-8 替换字符解析,不会崩 |
--field-map 不是合法 JSON |
退出码 2,明确指出是读取失败还是解析失败 |
--field-map 结构不是 {字段: [叫法]} |
退出码 2,打印期望结构 |
--require 含未知单证类型 |
退出码 2,并列出全部可选类型(顺带防路径注入:../.. 这类值会被拒) |
--demo 与 --path 同时给 |
退出码 2,不静默丢掉其中一个 |
--fail-on-block + 有阻断 |
退出码 2,可直接当流水线卡点 |
--help / 参数写错 |
退出码 0 / 2,被顶层兜底显式捕获(SystemExit 是 BaseException,except Exception 抓不到) |
| 报告写入失败 | 退出码 3 |
自检:python scripts/doc_audit.py --selftest —— 80 项断言,覆盖编码兜底顺序、键名归一、别名映射不乱抢、
否定式上下文、数值合理性、跨单证分级、章节编号连续性、错误路径退出码封闭。
实战案例
案例一:五份单证,找出 2 个真问题
需求:客户说「系统里 HS 编码过不了」,但五份单证都在手上,不知道问题在哪。
选型:全量目录扫描 + --require 齐套校验。
数据:示例单证五份(发票、装箱单、提单、产地证、保单),埋在同一个目录。
命令:
python scripts/doc_audit.py --path ./单证 --out ./out --require invoice,packing_list,bill_of_lading,origin_certificate,insurance
实际产出(已跑通,真实输出):
单证 5 份,问题:阻断 2 / 警告 2 / 提示 0;跳过 0 份
| 级别 | 查出什么 | 处置 |
|---|---|---|
| 阻断 | 产地证 HS 8415100000 vs 发票/装箱单 8415.1 |
按目标国税则位数统一 |
| 阻断 | 发票第 16 行 TODO: 补保险单号 |
补填或删除该行 |
| 警告 | 提单 118 件 vs 装箱单 120 件 | 拼箱口径不同,确认即可 |
| 警告 | 开票日 09-28 晚于提单日 09-26 | 属倒挂,商务上要解释 |
关键点:客户报的「HS 编码过不了」是跨单证问题,任何单份单证单独看都没错——
所以只审一份单据的工具在这里必然失手。
案例二:字段叫法不一样,不改脚本怎么接入
需求:车间的出厂单一律写「箱数」「总箱量」,没有一份写 No. of Packages。
错误做法:改脚本别名表。结果脚本一升级就丢,别的项目也跟着受影响。
正确做法:外部映射。
{"no_of_packages": ["箱数", "总箱量", "装箱数量"]}
python scripts/doc_audit.py --path ./单证 --out ./out --field-map my_fields.json
--field-map 的键会合并进内置别名表(用户叫法优先),不是替换。
这样脚本保持零修改、可复用,配置随单据走。
已验证清单
以下都是本机实跑结果,不是设计意图:
--selftest80/80 通过(2026-10-05 由 67 增至 80)- 中文日期
2026年9月1日归一为2026-09-01;带空格的2026 年 12 月 31 日同样可解析 - 点分日期
2026.09.01可解析;版本串v1.2.3不会被误读成日期 - 不存在的日期
2027-02-30/2027-02-29判为读不出来;闰日2028-02-29按真实日历可用 - 开票 9-01 / 装运 10-01(中文写法)不再误报「日期逻辑倒挂」(v1.0.1 修)
- 开票 10-01 / 装运 9-01(中文写法)仍能正确报出倒挂——修的是假告警,不是把检查关掉
- 任一边日期没归一化时不参与比对,不造假告警
- 错误路径 11 条退出码全部正确(无参数 / 缺 out / demo 与 path 互斥 / 路径不存在 / require 未知类型 / require 路径注入 / field-map 不存在 / field-map 结构错 / 单文件 / template / help)
--fail-on-block在有阻断时返回 2--demo跑出 5 份示例单证,稳定产出 2 阻断 + 2 警告- 报告章节编号连续无断档(无跳过文件时不会出现缺「五」)
- 跨单证提示里文件名正确渲染(曾因键值用反渲染成空括号,已修)
- 币种
RMB与CNY归一后可识别为同一币种 contract文件名不会被短词co误判成产地证(短词走词边界匹配)- 否定语境中的
XXX不误报为占位符残留;否定词超出 10 字窗口则照报 - 编码兜底
utf-8-sig排第一(否则带 BOM 的文件会把 U+FEFF 留在串首,后续键名匹配全部偏移) - 单份单证内同名字段矛盾、数值为零、净重>毛重均能检出