零 LLM 调用,零 OCR 调用,零成本。
目标是证明架构的核心假设:
大部分发票根本不需要模型就能 100% 准确地解析出来。
14 个测试全过(stdlib unittest,零依赖)
样本发票 7 张:
解析路径分布 : {'L1_XML': 4, 'L1_OFD': 1, 'L2_PDF': 1, 'L3_OCR': 1}
免模型解析率 : 6/7 = 86%
自动通过率 : 4/7 = 57%
LLM/OCR 调用 : 0 次,成本 ¥0
其中被拦下来的那几张,正是应该被拦下来的:
| 样本 | 结果 |
|---|---|
| 数电票 XML(英文标签) | ✅ 自动通过 |
| 数电票 XML(拼音标签) | ✅ 自动通过 —— 候选标签名机制兼容不同开票平台 |
| 金额被篡改(100+6 却写 160) | ❌ 拦截 AMOUNT_MISMATCH —— 勾稽抓住了 |
| OFD(内嵌结构化数据) | ✅ 自动通过 |
| 重复上传 | ❌ 拦截 DUP_FILE + DUP_INVOICE + DUP_SUSPECT |
| 扫描件图片 | ❌ 明确标记 NEEDS_OCR —— 不假装能处理 |
| PDF 文本层 | ✅ 自动通过 |
第三行是重点:金额被改成一个"看起来很合理"的数字,勾稽校验把它揪出来了。 这正是将来 OCR 抠错一位、LLM 抄错数字时会发生的事 —— 校验层是整个系统的安全网。
# P0 是零依赖的。解析 PDF 才需要装东西:
pip install pdfplumber
# 跑测试
python -m unittest discover -s tests -v
# 造样本发票(可选,需要 reportlab 才能造 PDF 样本)
python tests/make_fixtures.py
# 跑管线
python -m invoice.cli run tests/fixtures/python -m invoice.cli dump 你的发票.xml
python -m invoice.cli dump 你的发票.pdf
⚠️ parsers/xml_parser.py里的FIELD_CANDIDATES是一组候选标签名。 不同开票平台的数电票 XML 标签名不一致 —— 我没有猜你的票用哪一套。 dump 会把真实标签打出来,补进候选列表即可。补完 L1 就是无损的。样本里特意放了一张"拼音标签"的票来验证这个机制:同一套代码,
InvoiceNumber和Fphm都能解析。你的票大概率是这两套之一。
# 整个文件夹丢进去
python -m invoice.cli run ./我的发票/
# 带抬头校验
python -m invoice.cli run ./我的发票/ --company "你的公司" --tax-id 91xxxxxxxx
# JSON 输出,喂给下游
python -m invoice.cli run ./我的发票/ --json > out.json免模型解析率。 它越高,说明你越不需要 OCR 和 LLM。
如果你的打车发票这个数接近 100%,就直接证明了: "图片 → OCR → 大模型提字段"那套方案在这些票上做的一切, 都是在花钱把 100% 的准确率降下来。
跑不通的那几张(标记 NEEDS_OCR 的),才是 P1 真正要解决的输入。
invoice/
sniff.py L0 格式嗅探(看 magic bytes,不看扩展名)
parsers/
xml_parser.py L1 数电票 XML —— 无损,免费,准确率 100%
ofd_parser.py L1 OFD(zip 容器,取内嵌结构化数据)
pdf_parser.py L2 PDF 文本层 + 正则 —— 不做 OCR
validate.py 规则校验层 —— 金额勾稽、税率、税号、抬头、去重
config.py 税率表(带生效日期)+ 租户配置
models.py 统一发票模型(全 Decimal)
pipeline.py L0 路由 → 解析 → 校验 → 置信度路由
cli.py 命令行
tests/
test_validate.py 14 个测试
make_fixtures.py 造样本发票
1. 金额永远是 Decimal,不是 float。
0.1 + 0.2 != 0.3 在财务场景是会出事的。
2. 校验层发现问题时,永远不尝试修正。 修正是人的活。系统的职责是保证"错误不会安静地通过"。
3. 抠不到的字段留空,不猜。 留空 → 进人工复核。猜 → 一个看起来很合理的错误答案。后者危险得多。
4. 金额勾稽是整个系统的地基。
不含税 + 税额 = 价税合计 (硬等式,不成立就拦截)
不含税 × 税率 ≈ 税额 (容差内,允许分位进舍)
Σ 明细行金额 = 不含税合计 (容差内)
将来接了 OCR 和 LLM,是这一层把它们的错误抓出来的。 没有它,识别准确率再高也只是"看起来对"。
5. 税率表是数据,不是代码。
config.py 里带生效日期。政策一变,改配置不改代码。
6. P0 不用 pydantic。 纯解析 + 校验用不上它。零依赖 = clone 下来就能跑。 到 P2 上 FastAPI 时再引入 —— 那时它才真正有价值(请求体校验)。
⚠️ FIELD_CANDIDATES的标签名需要你用真实发票 dump 后确认⚠️ PDF 正则是脆的,换版式可能失配 —— 但勾稽会兜住,不会静默出错⚠️ _extract_parties靠"购方在前、销方在后"的顺序假设,不总成立- ❌ OFD 纯版式解析(无内嵌数据时)→ P1
- ❌ XML 数字签名验签 → 接财政部电子凭证工具包,别自己造
- ❌ OCR、LLM 科目分类、税局查验 → P1 / P3 / P4
- 把你的打车发票丢进一个文件夹,跑
dump对标签名 - 跑
run,看免模型解析率能到多少 - 跑不通的票拎出来 —— 那才是 P1 的输入