进阶 11 分钟金融

财务:提取 factures

直接回答

要在本地提取发票数据,请使用 Ollama 提供视觉模型服务(Qwen 3.5 9B,6.6 GB,或 Gemma 4),强制模型的回答遵循指定的 JSON 模式,然后通过代码验证:未税金额加增值税等于含税总额,以及 SIRET 编号和日期。未通过验证的发票交由人工复核。自 2026 年 9 月起,收到的结构化电子发票无需使用 AI 读取:该流程适用于普通 PDF 和扫描件。

手动输入发票效率低下且容易出错,而将会计文件交给在线服务则面临隐私问题。本地视觉模型可读取页面内容,模板定义输出格式,确定性校验机制过滤错误。本指南完整展示了从PDF分类到会计导入的全流程,并说明自2026年9月起电子发票带来的变化。

作者: Mohamed Meguedmi·更新于 2026-09-30·已在 Windows、macOS 和 Linux 上测试

#自动化的内容及可靠性水平

我们希望从供应商发票的 PDF 中提取结构化 JSON(供应商、编号、日期、不含税金额、增值税金额、含税金额、明细项),以便批量导入会计软件。由 Ollama 提供推理服务的视觉模型直接读取页面图像,无需另行进行 OCR。让这套流程可靠的规则可以用一句话概括:模型负责读取,代码负责校验。提取结果只有通过算术和标识符校验后才能导入,所有未通过校验的结果都会进入人工复核队列。

本指南针对一个具体场景:一家事务所或中小企业每月收到数百张发票,在本地电脑或小型服务器上进行处理。这里不承诺任何准确率数值:准确率取决于您的供应商、扫描质量和模型。下文介绍了如何使用您自己的发票样本进行测量。

#电子发票:2026年和2027年将发生哪些变化

本地 AI 套件

只需 1 小时,即可在您的电脑上拥有专属的免费 ChatGPT — LM Studio、Ollama、Open WebUI、您的文档,无需云端。

  • 在线空间,终身可用
  • PDF + 文件
  • 30 天内退款

在构建发票读取流程之前,需要先了解这项改革对收到的发票有什么影响。根据 impots.gouv.fr,自 2026 年 9 月 1 日起,大型企业和中等规模企业必须通过获批准的平台开具发票,所有企业也都必须具备接收电子发票的能力。根据税务部门的实用指南,中小企业、超小型企业和微型企业(micro-entreprises)的开票义务自 2027 年 9 月 1 日起生效。

这在实际应用中有两方面的影响。一方面,您收到的发票中,越来越多将直接以结构化形式送达,无需人工智能读取。另一方面,Factur-X 是法国和德国共同制定的混合发票标准,可在同一个文件中包含可供阅读的 PDF 和用于自动化处理的 XML 数据,并支持多种数据配置规范。当 PDF 内嵌这类 XML 数据时,直接读取 XML 比让模型推测其中的数据更可靠。因此,下方的处理流程遵循以下优先顺序:若有结构化数据,优先使用;其次读取 PDF 原生文本;最后才采用视觉识别。

i
本指南未涵盖的内容
通过获批平台接收发票、电子数据报送(e-reporting)以及发票开具义务,属于您所选择的平台和会计软件提供商负责的范围。此流程处理的是仍以普通 PDF 或扫描件形式收到的发票:境外供应商、小型服务商提供的发票、收银小票、数字化后的纸质单据。

#选择模型:视觉对内存的需求

在 Ollama 模型库中,有两个系列适合读取发票。Qwen 3.5 的 90 亿参数版本大小为 6.6 GB,支持文本和图像,并宣称提供 256,000 个 token 的上下文窗口;其 27B 版本大小为 17 GB。根据 Ollama 的模型说明,Gemma 4 的 e4b 变体占用 6.6 至 9.5 GB,并宣称提供 128,000 个 token 的上下文,支持文本和图像。因此,一张 12 GB 显存的显卡足以运行 9B 或 e4b 变体,还能为页面图像留出余量。

不同类型的 PDF 应选用什么工具
文件类型工具为什么
PDF Factur-X 或嵌入式 XML直接读取 XML 文件精确数据,无读取错误风险
文本可选中的原生 PDF先提取文本,再使用文本 LLM速度快,无需处理图像
扫描版 PDF 或清晰的照片视觉模型(Qwen 3.5 9B,Gemma 4)读取排版和表格
低质量扫描OCR 使用 Tesseract,随后进行人工复核视觉识别会受噪声影响而出错;由人工作出判断。

本站提供的内存参考:一个拥有 90 亿参数的 Q4 模型约占 5 至 6 GB,还需加上上下文缓存和页面图像的内存占用。多页发票比其他发票消耗更多资源:在一次请求中提交十页,可能超过 Ollama 的默认上下文窗口;在您调大该窗口之前,它仍远小于模型宣称的 256,000 个 token。

#阅读前先对 PDF 进行分类:原生、扫描件、结构化

检测文件类型可以避免不必要地将图像发送给视觉模型。使用 PyMuPDF 库提取文本时,若一个 PDF 的提取文本超过几百个字符,它就是原生 PDF;若几乎提取不到文本,它就是扫描件。Factur-X 文件包含一个 XML 附件,可以在进行任何其他处理之前将其列出。

根据 PDF 类型进行路由
import fitz  # PyMuPDF

def type_pdf(chemin):
    doc = fitz.open(chemin)
    pieces = doc.embfile_names()  # pièces jointes intégrées
    if any(n.lower().endswith('.xml') for n in pieces):
        return 'structure'
    texte = ''.join(page.get_text() for page in doc)
    return 'natif' if len(texte.strip()) > 300 else 'scan'

对于低质量扫描件,Tesseract 仍是一个免费且离线的备用方案:此时需要安装法语语言文件,并以每英寸 300 点的分辨率扫描。Tesseract 指南详细说明了相关设置。质量不佳的 OCR 产生的文本绝不能未经检查就进入下一步:此时应转入人工复核队列。

安装 Tesseract 及其法语支持
# macOS
brew install tesseract tesseract-lang

# Ubuntu / Debian
sudo apt install tesseract-ocr tesseract-ocr-fra

#使用视觉模型,按强制规定的数据结构提取信息

Ollama 可以约束模型的回答,使其符合指定的 JSON 模式:根据文档,需要在 format 字段中提供模式,并建议在提示词中也重复该模式,以引导回答遵循它。这比用自由文本要求输出“一个 JSON”可靠得多,因为键和类型都受到强制约束。对于图像,REST API 要求在消息的 images 字段中提供经过 base64 编码的图像。

发票的结构化提取(Ollama,视觉)
import base64, json, requests
from io import BytesIO
from pdf2image import convert_from_path

SCHEMA = {
  'type': 'object',
  'properties': {
    'fournisseur': {'type': 'object', 'properties': {
      'nom': {'type': 'string'},
      'siret': {'type': ['string', 'null']},
      'tva_intra': {'type': ['string', 'null']}}},
    'facture': {'type': 'object', 'properties': {
      'numero': {'type': 'string'},
      'date': {'type': 'string'},
      'echeance': {'type': ['string', 'null']}}},
    'montants': {'type': 'object', 'properties': {
      'ht': {'type': 'number'}, 'tva': {'type': 'number'},
      'ttc': {'type': 'number'}, 'devise': {'type': 'string'}}},
    'lignes': {'type': 'array', 'items': {'type': 'object', 'properties': {
      'description': {'type': 'string'}, 'quantite': {'type': 'number'},
      'pu_ht': {'type': 'number'}, 'total_ht': {'type': 'number'}}}}
  },
  'required': ['fournisseur', 'facture', 'montants']
}

PROMPT = ("Tu extrais les données d'une facture française. "
  "Réponds uniquement par un JSON conforme à ce schéma : " + json.dumps(SCHEMA) +
  ". Si une donnée est absente ou illisible, mets null. Les montants sont des nombres (1234.56), "
  "les dates au format AAAA-MM-JJ. N'invente rien.")

def pages_b64(pdf, dpi=200):
    sortie = []
    for img in convert_from_path(pdf, dpi=dpi):
        buf = BytesIO(); img.save(buf, format='PNG')
        sortie.append(base64.b64encode(buf.getvalue()).decode())
    return sortie

def extraire(pdf):
    r = requests.post('http://localhost:11434/api/chat', json={
      'model': 'qwen3.5:9b', 'stream': False, 'format': SCHEMA,
      'messages': [{'role': 'user', 'content': PROMPT, 'images': pages_b64(pdf)}],
      'options': {'temperature': 0, 'num_ctx': 16384}})
    return json.loads(r.json()['message']['content'])

有三项设置值得解释。将温度设为零可使输出可复现。num_ctx 参数扩大了上下文窗口,因为多页图像会很快耗尽 Ollama 默认较小的上下文窗口;上下文窗口指南详细说明了这一机制。最后,“不要编造”的指令加上允许使用 null 值,可降低最严重的风险:模型用看似合理的值填补无法辨认的字段。严格的模式定义约束的是回答的形式,而不是内容的正确性。

#扩展数据结构,以适应你的实际场景

基础数据结构适用于大多数常见供应商的发票。需要添加哪些扩展,取决于您的业务。请逐一添加,并在您的样本上衡量每项扩展的影响:数据结构过于复杂,会降低关键字段的识别效果。

预付款与尾款
添加一个 acompte_paye 字段。没有这个字段,含税总额就无法对应剩余应付金额。
采购订单编号
一个 bon_commande_ref 字段可与您的订单进行匹配。
运费与折扣
使用单独的明细行或 port_ht 字段,否则各明细行的金额之和将与不含税总额不一致。
增值税明细
列出税率、计税基数和税额。只要发票涉及多个税率,这份明细就必不可少。
会计科目归属
不要让模型猜测会计科目:请生成一个明确标注为建议的方案,再由您的业务规则或人工确认。

#导入前验证:能发现错误的检查

这一步决定了整套方案的质量。每项检查都是确定性的,因此比模型更可靠。第一项是算术检查:不含税金额加上增值税必须等于含税金额,允许因四舍五入产生几欧分的误差。第二项检查发票明细:各明细项金额之和必须等于不含税总额。第三项检查日期:既不能早于合理的日期下限,也不能是未来日期。第四项检查 SIRET 编号。

SIRET 编号的校验值得特别注意。维基百科指出,该编号由 14 位数字组成,最后一位是用 Luhn 公式计算出的校验位。但有一个例外:La Poste 的营业机构,其 SIREN 编号为 356000000,采用另一条规则,即 14 位数字之和必须是 5 的倍数。因此,直接使用 Luhn 校验会错误地拒绝 La Poste 的发票。以下代码处理了这两种情况。

对提取出的发票数据进行一致性检查
from datetime import date

def luhn_ok(s):
    total = 0
    for i, c in enumerate(reversed(s)):
        d = int(c)
        if i % 2 == 1:
            d *= 2
            if d > 9:
                d -= 9
        total += d
    return total % 10 == 0

def siret_valide(s):
    if not (s.isdigit() and len(s) == 14):
        return False
    if s.startswith('356000000'):  # La Poste : somme multiple de 5
        return sum(int(c) for c in s) % 5 == 0
    return luhn_ok(s)

def valider(f):
    erreurs = []
    m = f['montants']
    if abs(m['ht'] + m['tva'] - m['ttc']) > 0.02:
        erreurs.append('HT + TVA différent du TTC')
    lignes = f.get('lignes') or []
    if lignes and abs(sum(l['total_ht'] for l in lignes) - m['ht']) > 0.05:
        erreurs.append('somme des lignes différente du HT')
    siret = (f['fournisseur'].get('siret') or '').replace(' ', '')
    if siret and not siret_valide(siret):
        erreurs.append('SIRET invalide : ' + siret)
    try:
        d = date.fromisoformat(f['facture']['date'])
        if d.year < 2000 or d > date.today():
            erreurs.append('date suspecte : ' + str(d))
    except ValueError:
        erreurs.append('date illisible')
    return erreurs
!
绝不导入不一致内容
总金额不匹配的账单应进入「待核对」队列。通过所有检查的提取结果并不能保证准确,但失败的提取结果则肯定需要人工复核:人工审核的重点就在此处。

#用您自己的发票评估准确率

任何已公布的准确率数据都不能替代在您自己的语料上进行测量,因为处理批发商发票的事务所使用的文档与协会不同。请选取约五十份具有代表性的发票作为样本,手动录入关键字段,然后逐字段比较。

  1. 01
    构建样本
    使用各种真实的发票:扫描件、原生PDF、多页、多个供应商。如果分享结果,请进行匿名化处理。
  2. 02
    录入真实参考数据
    手动记录需要自动化处理的字段:编号、日期、不含税金额、增值税(TVA)、含税金额、SIRET 编号。
  3. 03
    逐字段比较
    按字段计算准确率,而非整体准确率:模型可能完美识别日期,却在增值税上出错。
  4. 04
    设置自动化阈值
    确定哪些字段可以无需复核就直接导入。金额字段如果通过算术校验,就很适合这样处理;会计科目归属则绝不能未经复核就导入。
  5. 05
    每次变更都重新运行
    更换模型、分辨率或提示词后,在投入生产前,用样本重新运行测试。

#批量处理文件夹中的发票

批处理依次完成分类、提取和验证,然后根据结果归档每张发票。始终将两个文件放在一起保存:原始 PDF 和提取出的 JSON。

带复核队列的批量处理
from pathlib import Path
import json

def traiter(dossier_in, dossier_ok, dossier_revue):
    for pdf in Path(dossier_in).glob('*.pdf'):
        try:
            f = extraire(str(pdf))
            erreurs = valider(f)
        except Exception as e:
            f, erreurs = {}, ['échec extraction : ' + str(e)]
        dest = Path(dossier_ok if not erreurs else dossier_revue)
        (dest / (pdf.stem + '.json')).write_text(
            json.dumps({'donnees': f, 'erreurs': erreurs}, ensure_ascii=False, indent=2))
        pdf.rename(dest / pdf.name)
        print(('OK ' if not erreurs else 'A VERIFIER ') + pdf.name)

#将数据导入会计软件

每个编辑器都有自己的导入格式,且其规范会不断更新:请参考您所使用工具的官方文档,而非通用模型。会计软件通常提供结构化文件导入或编程接口。最稳妥的方式是生成符合规范的导入文件,将其加载到测试文件夹中,然后将生成的记录与您手动输入的记录进行比对。

保留审计记录:原始PDF、提取的JSON、模型版本及处理日期。在审计时,必须能够从会计记录追溯到相应的凭证文件。发票包含个人及商业数据:本地存储可避免将数据交由第三方处理,但文件的存储仍需符合您的数据保存与安全政策。

FAQ
本地 LLM 能读取扫描的发票吗?+
是的,像 Qwen 3.5 9B 或 Gemma 4 这样的视觉模型可以读取页面图像并提取字段。其可靠性取决于扫描质量和排版。因此,必须通过算术校验来验证结果,任何未通过校验的发票都应交由人工处理。
除了视觉模型,还需要额外的 OCR 模块吗?+
不一定需要。视觉模型直接读取图像,从而避免单独进行 OCR 所造成的信息丢失。对于质量严重下降的扫描件,Tesseract 仍可作为后备手段;对于可直接提取文本的原生 PDF,它也仍有用处。请在您的样本上测试这两种方法,而不是事先作出假设。
如何确保输出为有效的JSON?+
Ollama 允许在 API 的 format 字段中传入 JSON 模式:响应就会被约束为该结构。这能保证格式,但不能保证数值正确。因此,请始终在代码中加入一致性校验:不含税金额加上增值税、SIRET 编号、日期以及各行项目的合计。
强制使用电子发票会对此类工具产生什么影响?+
自 2026 年 9 月 1 日起,所有企业必须能够接收电子发票,而中小企业在 2027 年 9 月必须开始开具电子发票。结构化发票(如 Factur-X)无需 AI 即可读取。该流水线主要服务于仍发送简单 PDF 或纸质发票的供应商。
提取发票信息需要什么样的显卡?+
一个 90 亿参数的模型经 Q4 量化后约占 6 GB,页面图像和上下文还需额外空间。12 GB 显存的显卡适用;对于单页发票,缩短上下文后,8 GB 显存的显卡也足够。没有 GPU 也能处理,但速度会变慢:请安排在夜间处理。
能否信任模型建议的会计科目归类?+
不能,未经核实就不能信任。模型可以根据供应商名称建议一个会计科目,但这只是建议,需要通过业务规则或由会计人员确认。科目归属错误不会自动显现:它能通过所有算术检查。请将科目归属视为唯一始终需要人工审核的环节。
这份指南对您有帮助吗?

有反馈、发现了错误,或想补充说明?请告诉我们,让这份指南对每个人都更有帮助。