直接回答

可靠的文档工作流是一台版本化状态机:

text
接收 -> 隔离 -> 校验 -> 标准化 -> 提取
     -> 业务检查 -> 必要时人工复核
     -> 提交 -> 按策略留存或删除

每个输出都必须能够追溯到不可变原件,以及生成它的精确处理配置。OCR、多模态模型或 LLM 可以提出文本和字段;只有确定性代码与获得授权的 Reviewer 才能决定这些字段是否可以改变业务系统。

“拆分 PDF、调用 AI、保存 JSON”不是端到端设计。它遗漏了不可信文件处理、来源链、重试、不确定性、复核、授权和纠错。

定义文档契约

从一种文档和一个业务结果开始。以发票为例:

yaml
document_type: supplier-invoice
accepted_inputs: [pdf]
source:
  immutable: true
  max_bytes: policy-defined
required_fields:
  - supplier_id
  - invoice_number
  - currency
  - total_minor_units
business_oracles:
  - supplier_exists
  - purchase_order_matches
  - line_sum_matches_total
  - invoice_not_previously_committed
human_review:
  - missing_required_field
  - conflicting_evidence
  - high-impact_policy
commit:
  idempotent: true
  authorized_principal: required

这份契约区分了语法提取与业务验收。模型可能正确读出“1,000.00”,却把它关联到错误供应商或币种。此时提取成功,业务仍然失败。

建模 Artifact 与状态

原件保持不可变,每个派生制品拥有独立身份:

Artifact 用途 必需来源信息
原始字节 证据与重处理来源 来源渠道、Owner、Digest
清理或标准化文件 更安全的解析器输入 Parent Digest、Sanitizer 版本
页面图像 OCR 或视觉模型输入 Parent、页码、渲染配置
OCR 文本与布局 可搜索文本与坐标 Engine、Model、Language、Page
提取字段 候选业务数据 Schema、Evidence Span、Confidence
已复核记录 人工决定 Reviewer、原因、修改字段
已提交记录 下游副作用 Idempotency Key、Target Version

W3C PROV 模型提供了 Entity、Activity、Agent,以及 Usage、Generation、Derivation 等可复用概念。工作流不必采用 RDF 存储,但应该回答同一组问题:使用了什么、产生了什么、由哪个过程生成、谁负责、何时发生。

状态转换应显式定义:

flowchart LR A["已接收"] --> B["已隔离"] B --> C{"校验"} C -->|拒绝| X["已拒绝"] C -->|通过| D["已标准化"] D --> E["已提取"] E --> F{"业务检查"} F -->|证据不足| H["人工复核"] F -->|通过| G["已批准"] H -->|批准或修正| G H -->|拒绝| X G --> I["幂等提交"] I --> J["按策略留存或删除"]

不能因“最后一个函数返回”就推断流程完成。每次状态变化及其证据都要持久化,使中断 Worker 可以恢复,又不重复执行副作用。

解析前先保护接收边界

文档是不可信输入。OWASP File Upload Cheat Sheet强调分层控制,因为没有任何单一校验足够可靠。

接收阶段需要:

  • 验证上传者身份与权限;
  • 只允许业务必需的扩展名;
  • 规范化原文件名,再生成服务端存储名;
  • 比较扩展名、声明 MIME、检测类型和文件签名;
  • 限制压缩前后大小、页数、尺寸和处理时间;
  • 在 Web Root 之外隔离存储;
  • 按场景执行 Malware Scan 或 CDR;
  • 在没有生产凭证的隔离 Worker 中解析;
  • 按策略拒绝加密、损坏、Active Content 或不支持的文件。

Content-Type: application/pdf 是用户可控元数据。%PDF- 前缀也不能证明所有 Embedded Object 安全。Malware Scan 同样不是语义校验器。

处理压缩包时,应单独限制 Entry 数量、嵌套深度、解压总字节与 Path Traversal。5 MB 上传文件可能解压为远大于原大小的内容。

分离标准化、OCR 与语义理解

这些阶段有不同输出和失败模式:

  1. 标准化:在不改变权威原件的前提下生成解析器可用版本。
  2. OCR:把像素映射为文字与布局坐标。
  3. 文档解析:提取原生文本、表格、表单、附件和结构。
  4. 分类:选择适用的文档契约。
  5. 语义提取:提出带类型的业务字段与证据位置。
  6. 验证:将候选与源证据和外部系统对照。

Digital PDF 可能已有文本层,但阅读顺序或隐藏层仍可能错误;扫描 PDF 需要 OCR;混合文档可能同时需要原生解析和页面渲染。不要假设所有 PDF 都是一类输入。

AI 辅助提取时,应保留:

  • 每个字段的页码与 Bounding Box;
  • 原文 Evidence Span;
  • OCR 与模型版本;
  • Prompt 与 Schema 版本;
  • 源语言与标准化方式;
  • 备选值或不确定性;
  • Validator 与 Reviewer 决策。

坐标和证据管理可继续阅读AI 图像理解:OCR、解析与 VQA。

校验字段,而不只是 JSON

Structured Output 只证明语法,不证明语义。

发票可以执行以下确定性检查:

text
currency 属于支持的代码集合
total_minor_units >= 0
sum(line_amounts) + tax 按规定舍入后等于 total
supplier_id 在当前 Tenant 中存在
purchase_order 处于可用状态且属于该 Supplier
invoice_number 在该 Supplier 下尚未提交
date 与 payment terms 位于允许范围

Validation 应返回类型化原因。“Confidence 低于 0.8”还不够,系统应区分证据缺失、解析失败、跨系统冲突、Duplicate、Policy Denial 和模型不确定。

Confidence 只是路由信号

模型 Confidence 只有在代表性数据上完成 Calibration 后才有实际含义。应按 Confidence Bucket、Document Class、Source、Language 与 Field 测量真实错误率。

高影响字段即使 Confidence 很高,也可能仍需复核;低影响字段即使阈值较低,只要容易纠错,也可能可以自动接收。路由应基于预期错误成本,而不是统一全局数字。

把人工复核作为正式状态

Review Queue 需要展示:

  • 原始页面和候选字段;
  • 高亮证据与坐标;
  • 旧值和冲突原因;
  • 允许的修正与 Reviewer Identity;
  • 必要时的职责分离;
  • 禁止由模型生成的文字自行批准;
  • 持久化 Decision 与 Audit Event。

测量 Reviewer Agreement 与 Correction Rate。若 Reviewer 也经常分歧,问题可能来自模糊政策或源文档,而不是模型能力。

人工修正不能自动成为训练数据。复用前仍需 Consent、Provenance、质量审查、去重和防泄漏切分。

让重试与提交具备幂等性

阶段幂等身份可以采用:

text
source_digest + stage + processor_revision + configuration_digest

相同输入和配置的同一阶段可以返回既有 Artifact。OCR 模型或 Schema 发生变化时,应创建新 Artifact,而不是覆盖历史。

下游写入可以使用:

text
approved_record_digest + target_system + operation

请求发送后超时,应将结果记为 Unknown,并先与目标系统对账。盲目重试可能重复创建发票、Case、通知或支付。

可运行的 Go 状态机

下面的无依赖示例创建 Content-addressed Artifact,并拒绝非法状态转换。它只演示编排;真实 Parser 仍应放在隔离 Worker 中。

go
package main

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
)

type State string

const (
	Received    State = "received"
	Quarantined State = "quarantined"
	Validated   State = "validated"
	Extracted   State = "extracted"
	Reviewed    State = "reviewed"
	Committed   State = "committed"
	Rejected    State = "rejected"
)

var transitions = map[State]map[State]bool{
	Received:    {Quarantined: true},
	Quarantined: {Validated: true, Rejected: true},
	Validated:   {Extracted: true, Rejected: true},
	Extracted:   {Reviewed: true, Rejected: true},
	Reviewed:    {Committed: true, Rejected: true},
}

type Document struct {
	ID          string
	SourceSHA   string
	State       State
	Revision    int
	CommittedID string
}

func digest(data []byte) string {
	sum := sha256.Sum256(data)
	return hex.EncodeToString(sum[:])
}

func advance(document Document, next State) (Document, error) {
	if !transitions[document.State][next] {
		return document, fmt.Errorf(
			"invalid transition %s -> %s",
			document.State,
			next,
		)
	}
	document.State = next
	document.Revision++
	return document, nil
}

func commitKey(document Document, target string) (string, error) {
	if document.State != Reviewed || target == "" {
		return "", fmt.Errorf("reviewed document and target are required")
	}
	return digest([]byte(document.SourceSHA + "|" + target)), nil
}

func main() {
	source := []byte("%PDF-1.7 fixture")
	document := Document{
		ID:        "doc-42",
		SourceSHA: digest(source),
		State:     Received,
		Revision:  1,
	}

	var err error
	for _, next := range []State{Quarantined, Validated, Extracted, Reviewed} {
		document, err = advance(document, next)
		if err != nil {
			panic(err)
		}
	}

	document.CommittedID, err = commitKey(document, "erp.invoice.create")
	if err != nil {
		panic(err)
	}
	document, err = advance(document, Committed)
	if err != nil {
		panic(err)
	}

	fmt.Printf(
		"id=%s state=%s revision=%d source=%s commit=%s\n",
		document.ID,
		document.State,
		document.Revision,
		document.SourceSHA[:12],
		document.CommittedID[:12],
	)
}

预期输出以此开头:

text
id=doc-42 state=committed revision=6 source=

Source 与 Commit 标识是确定性的。相同已批准操作再次执行时,可以先读取已有结果,而不是制造第二次副作用。

评测完整工作流

按 Document Class、Source、Language、Layout、Quality 和 Risk 分切片:

层次 有用指标
Intake 拒绝原因、恶意样本、解压大小失败
OCR 字符/词错误、阅读顺序、坐标覆盖
Classification 每类 Precision、Recall、Abstention
Extraction 字段 Precision/Recall、精确值、证据位置
Validation 冲突检出、False Acceptance、False Rejection
Review Queue Age、Correction Rate、Reviewer Agreement
Commit 重复率、Unknown Outcome、下游修正
Operations p50/p95 时延、吞吐、重试、每个合格文档成本
Governance 删除完成、来源覆盖、越权访问

不要发布单一“文档准确率”平均数。发票总额错误与缺少一个可选备注的代价完全不同。

运行和演进

版本化 Parser、OCR Engine、Model、Prompt、Schema、Validator 与 Routing Policy。让新版本在既有文档上 Shadow,对比字段级回归,并保留旧路径用于回滚。

只要提取文本会进入 LLM,就应把所有外部文档视为潜在 Prompt Injection。AWS 摄取安全指南说明了为什么知识库摄取前和推理时都需要控制。

原件、页面图像、OCR 文本、Embedding、提取记录、Prompt、日志、Review Snapshot 与训练数据需要分别定义 Retention。只有策略要求的副本与索引都被处理后,删除才算完成。

常见问题

什么是智能文档处理工作流?

它是一条把不可信文件变成已验证业务记录的受治理管线,覆盖接收、隔离、解析或 OCR、提取、确定性检查、人工复核和授权提交,同时负责来源、重试、留存、删除与恢复。

Confidence 低时都应该转人工吗?

不应该。Confidence 必须先根据真实错误完成校准,路由还要考虑字段影响、业务规则冲突、源质量、不同解析器分歧和修正成本。部分高影响字段即使 Confidence 很高,也仍应复核。

MIME 与 Malware Check 通过后文件就安全吗?

不安全。每项检查只降低一种风险,Parser 仍可能存在漏洞,文件也可能包含 Active Content。系统仍需分层校验、隔离执行、资源限制、不可变原件,并对任何下游副作用单独授权。

文档工作流应该怎样重试?

Source、Stage、Processor 与 Configuration 完全相同时复用已有 Artifact,只重试已分类的瞬时故障。写操作结果未知时,必须先向目标系统对账,再决定是否重试。

哪个指标最能代表工作流质量?

没有单一指标。应按切片报告字段正确率和业务验收、False Acceptance、Review 负载、下游修正、时延、每个合格文档成本、重复副作用,以及恢复和删除完成度。

常见失败模式

  • 信任扩展名或浏览器 Content-Type;
  • 在特权应用进程中解析文档;
  • 用转换后文件覆盖原件;
  • 所有字段共用一个 Confidence Threshold;
  • JSON 有效便直接写入业务系统;
  • 只用正则表达式脱敏敏感数据;
  • 超时后不对账便重试写操作;
  • 日志没有 Source 和 Processor Revision;
  • 未经来源和 Consent 审核就用人工修正训练;
  • 只看 OCR 平均质量,不看财务或安全错误;
  • 宣称“零人工”却让 Exception Queue 持续堆积;
  • 推荐一批库,却没有 Support 与补丁负责人。

生产检查清单

  • [ ] 定义一种文档、一个 Schema、一个业务结果和失败策略。
  • [ ] 上传后先隔离,并在解析前执行分层校验。
  • [ ] 保持原件不可变,为每个派生 Artifact 建立身份。
  • [ ] 记录 Parser、OCR、Model、Prompt、Schema 与配置版本。
  • [ ] 为提取字段保留页码、坐标与 Evidence Span。
  • [ ] 提交前与权威业务系统校验。
  • [ ] 按校准风险和错误成本路由,而不是只看 Confidence。
  • [ ] 让阶段与下游副作用具备幂等性。
  • [ ] 重试写操作前先对账 Unknown Outcome。
  • [ ] 测试质量、安全、复核负载、时延、成本、恢复与删除。

相关资源

一手资料

总结

只有保留证据并控制副作用,文档自动化才可靠。隔离原件,创建版本化 Artifact,分离 OCR 与语义理解,对照业务事实验证字段,把不确定结果送入复核,并用幂等方式提交。先让系统能够解释每份文档发生了什么,并在任一阶段失败后安全恢复,再优化速度和成本。