文档数据通用处理思维框架

从数据整理项目实践中提炼的文档数据处理方法论。 不绑定具体技术栈,适用于任何"多格式文档 结构化信息提取"场景。


一、核心理念:读取与写入严格分离

原则:处理管道中的任何环节都不得修改原始文件。所有输出写入独立的目标位置。

这不是简单的"不要覆盖"——它塑造了整个系统的架构:

  • 读取层可以是无状态的纯函数,输入路径、输出内容,易于测试和组合

  • 写入层集中管理,天然支持断点续跑(读目标已有行数即可知进度)

  • 回滚零成本:删掉输出重跑即可,原始数据永远安全

  • 并发安全:多个读取进程不互相干扰,写入串行化即可

违反此原则的典型场景:在原 xlsx 上直接修改单元格。一旦中途崩溃,已修改的部分无法区分于未修改的,既不能续跑也不能回滚。


二、多格式读取的分层策略

2.1 三层提取优先级

面对任何文件格式,按以下优先级尝试提取:

第1层:结构化解析(按格式规范精准提取)
  ↓ 失败
第2层:通用文本提取(剥离二进制,保留可读文本)
  ↓ 失败
第3层:元信息兜底(仅使用文件名、路径、扩展名)

实际映射

格式

第1层

第2层

第3层

.doc

Piece Table 精准提取

OLE 通用流扫描

"[doc格式] + 路径信息"

.ppt

RecordHeader 递归解析

OLE 通用流扫描

"[ppt格式] + 路径信息"

.pdf

PyMuPDF 文本+嵌入图片提取

全页渲染为图片

"[pdf格式] + 路径信息"

.pak

ZIP 内结构化文件读取

二进制可读文本提取

"[pak格式] + 路径信息"

关键:第3层永远不失败。即便文件损坏到完全无法读取,路径和文件名本身也是信息。一个返回空字符串的函数不如一个返回 [格式: 无法提取] 的函数有用——后者至少告诉下游"我尽力了"

2.2 文本与图片的双重提取

文档信息不只存在于文本中。结构化提取应同时返回两个维度:

  • 文本:结构化可搜索、低 token 消耗、适合精确匹配

  • 图片:承载版式、签名、印章、手写内容等文本无法表达的信息

提取结果统一为 (text, images) 二元组,由下游决定如何使用。这避免了读取层提前做"只用文本""只用图片"的决策。

2.3 内容截断的必要性

任何格式都可能产生超大内容(一个 100MB 的 .doc 理论上包含数百万字符)。截断不是偷懒,而是保护:

  • 保护 API 调用不超 token 限制

  • 保护内存不被单个文件撑爆

  • 保护处理时间不被异常文件拖垮

截断阈值的选择:足够覆盖 95% 的正常文件,同时阻止 5% 的异常文件拖垮系统。截断时必须标注原始大小,让下游知道信息是否完整。


三、LLM 结构化提取的防御性设计

3.1 输出格式不可控 → 多层容错解析

LLM 的 JSON 输出有三种典型"不规矩"

  1. 直接输出合法 JSON(理想情况)

  2. 在 JSON 外包裹 Markdown 围栏 ```json ... ```

  3. 在 JSON 前后添加解释性文字

对应的三层解析策略:

第1层:直接 json.loads() → 成功即返回
第2层:正则提取 Markdown 围栏内容 → json.loads() → 成功即返回
第3层:定位首个 { 和末个 } → json.loads() → 成功即返回
第4层:全部失败 → 返回全空默认值(而非抛异常)

核心思维:解析函数的职责是"尽力从混乱输入中提取结构化数据",而非"验证输入是否合法"。验证是调用方的事,提取函数不应因格式问题导致整个流程中断。

3.2 单次提取不可靠 → 多次调用 + 投票

LLM 对同一输入的多次调用结果存在随机性(temperature > 0 时)。对于取值空间有限的结构化字段(类别、编号、金额),投票取众数是有效的去噪手段。

适用条件判断

字段特征

策略

取值空间有限、答案唯一

多次投票

取值空间开放、需要创意

单次提取

需要与外部列表核对

单次提取 + 验证 + 重试

投票次数的选择:奇数次(3/5/7),3 次是最低可行方案,5 次在准确率和成本间取得平衡。

3.3 开放提取不可靠 → 约束选择 + 注入候选

让 LLM 自由输出项目名称,准确率低且无法验证。更好的方式:

  1. 首次提取:让 LLM 自由输出,作为候选

  2. 验证:候选是否在已知项目列表中

  3. 重试时注入候选:将最可能匹配的 N 个项目名注入 prompt,让 LLM 从列表中选择

这把"开放式生成"转变为"受限选择",大幅提升准确率和可控性。

关键:注入的候选列表要精而不是多。top-128 比 "全部项目名" 效果更好——选项越少,LLM 选择越准确。


四、匹配策略的渐进精确原则

4.1 四级匹配框架

任何"用户输入 标准列表"的匹配场景,都应遵循从精确到模糊的渐进策略:

第1级:精确匹配 — O(1) 或 O(n) 的确定性比较
  ↓ 失败
第2级:规范化匹配 — 去除干扰因素后精确比较(空格、大小写、标点)
  ↓ 失败
第3级:子串匹配 — 互为子串即可,容忍增减词
  ↓ 失败
第4级:语义匹配 — 向量相似度或编辑距离,容忍同义改写

为什么不能直接用语义匹配?

  1. 语义匹配有误匹配风险——"项目A"和"项目B"可能语义相近但完全不同

  2. 语义匹配有延迟——向量检索虽快,仍比字符串比较慢几个数量级

  3. 语义匹配有依赖——需要 Embedding 服务可用

  4. 实测数据:前三级纯字符串匹配覆盖 ~75% 的情况,只有 ~25% 需要语义匹配

经验法则:简单确定性方法优先使用,语义/概率方法作为兜底。确定性方法不会"猜错",概率方法一定会"有时猜错"

4.2 中文文本的规范化细节

中英文混排场景中,规范化匹配尤其重要:

  • 中文全角空格(U+3000)与英文半角空格(U+0020)混用

  • 中英文标点混用( vs , vs /

  • 大小写不一致(项目编号、英文缩写)

规范化匹配必须同时处理全角和半角空格,否则约 15% 的本应匹配的项目会被遗漏。


五、外部依赖的降级兜底设计

5.1 降级不是可选功能,是核心架构

任何依赖外部服务的系统(API、数据库、向量引擎),都必须回答:服务不可用时系统怎么办?

data-collation 的答案是一个三级降级链:

FAISS 向量检索(最快、最准)
  ↓ 不可用(未安装 / API 故障 / 维度不匹配)
SequenceMatcher 字符级匹配(较慢、较准)
  ↓ (无更底层的降级,此层总能工作)

降级设计的关键约束:

  • 降级路径必须总能工作:最底层不能依赖任何外部服务

  • 降级是自动的:调用方不应感知到降级的发生

  • 降级可观测:日志记录降级事件,方便事后分析

5.2 空输入的特殊处理

当 LLM 无法提取出任何候选名称时(输入文件内容与项目无关、文件损坏等),常规的重试注入候选策略失效。此时需要一条替代路径:

  1. 从文件路径和已有标签中提取关键词

  2. 用关键词做向量检索,获取候选项目列表

  3. 将候选列表注入 prompt,让 LLM 做"受限选择"

思维:当主要通道(内容提取)走不通时,从辅助信息(文件名、标签、路径)中挖掘线索。路径本身是人类组织的,文件名通常包含项目相关信息。


六、处理管道的健壮性设计

6.1 断点续跑:生产环境的刚需

长时间运行的处理管道(数百至数千文件 × 多次 API 调用)必须支持断点续跑。实现方式:

  1. 输出写入独立的目标区域(sheet / 文件 / 数据库表)

  2. 启动时读取目标区域已有记录数,作为起始位置

  3. 跳过已处理的输入,只处理增量

关键约束:任何修改输出逻辑的代码都必须保持此行为。一个破坏断点续跑的"优化"比没有优化更糟——它会导致重复处理或数据丢失。

6.2 批量保存:平衡数据安全与 I/O 开销

每处理一条就保存 I/O 开销大;全部处理完再保存 中途崩溃全丢。

折中方案:每 N 条保存一次。N 的选择取决于:

  • 单条处理耗时(API 调用约 1-5 秒,N=100 意味着最多丢失 100 条的进度)

  • 保存操作耗时(写入 xlsx 约 1-3 秒,100 条保存一次几乎无感)

6.3 异常隔离:一个文件的失败不影响其他

处理管道中,单文件级别的异常(读取失败、API 超时、JSON 解析失败)不应导致整体中断。每条记录的处理应是独立的 try-except 块,失败时记录错误信息到专用字段,继续处理下一条。

错误信息应包含:异常类型 + 简要描述,写入输出的"读取失败"字段。这比静默跳过更好——用户可以事后筛选出失败记录,针对性重跑。

6.4 进度可见:用户不应盲等

长时间运行的管道必须提供进度反馈。但日志输出和进度条可能互相干扰(进度条被日志打断会换行错乱)。

解决方案:解耦进度显示与日志输出。业务代码只管写日志(logger.info/debug/error),Handler 层负责将日志输出路由到进度条的 write() 方法,避免打断进度条刷新。业务代码完全不知道进度条的存在。


七、图片处理的成本控制

7.1 压缩 → 传入 → 丢弃 的流水线

图片在处理管道中只是中间载体,不需要保留原始质量:

  1. 压缩:长边限制 1024px + JPEG quality 75,在视觉识别质量和 token 消耗间取得平衡

  2. 传入:base64 编码后作为 VLM 输入

  3. 丢弃:不缓存 base64,用完即弃,避免内存堆积

7.2 图片数量限制

每个文件最多传入 5 张图片(PPT 限 2 张)。原因:

  • 超过 5 张后 VLM 的信息利用效率递减

  • 每张图片消耗的 token 是文本的数十倍

  • 5 张足以覆盖文档首页、签名页、金额页等关键信息

选择策略:如果文件内嵌图片超过 5 张,按出现顺序取前 5 张。不做"智能选图"——这个决策的复杂度不值得收益。


八、配置管理:环境变量优于硬编码

8.1 敏感信息绝不作为默认值

# 错误:API Key 作为默认值硬编码
API_KEY = os.environ.get("OPENAI_API_KEY", "sk-xxx")

# 正确:仅从环境变量读取,缺失时明确报错
API_KEY = os.environ.get("OPENAI_API_KEY")
if not API_KEY:
    raise ValueError("OPENAI_API_KEY 环境变量未设置")

8.2 可调参数暴露为环境变量

所有可能需要调整的参数都应支持环境变量覆盖:

  • API 模型名称(LLM_MODEL / VLM_MODEL)

  • Embedding 模型和维度(EMBEDDING_MODEL / EMBEDDING_DIMENSION)

  • 相似度阈值(SIMILARITY_THRESHOLD)

  • 批次大小和重试次数

这允许不修改代码即可适配不同环境(开发 / 测试 / 生产)和不同模型。


九、思维框架速查表

场景

核心思维

关键策略

多格式读取

三层提取优先级

结构化 → 通用 → 元信息兜底

LLM 输出解析

尽力提取,而非验证格式

三层容错解析

结构化字段提取

确定性 > 概率性

投票取众数

开放式命名匹配

约束选择 > 自由生成

注入候选列表 + 受限选择

外部依赖

降级是核心架构

自动降级 + 底层总能工作

处理管道

断点续跑 + 异常隔离

独立输出区 + 批量保存

图片处理

压缩→传入→丢弃

数量限制 + 质量平衡

配置管理

环境变量 > 硬编码

敏感信息无默认值


十、反面教训

  1. 不要假设 LLM 输出格式——三层容错不是过度设计,是实战必须

  2. 不要跳过字符串匹配直接上语义匹配——75% 的情况不需要,且确定性方法不会误匹配

  3. 不要在读取层做写入——一旦破坏只读原则,断点续跑和数据安全都无从谈起

  4. 不要忽略文件路径本身的信息——文件名、目录结构是人类组织的,包含大量隐含信息

  5. 不要缓存大量 base64 图片——每个图片约 100-500KB 的 base64,千个文件就是百 MB 级内存占用

  6. 不要让日志和进度条互相干扰——解耦不是锦上添花,是长时运行管道的基本要求

  7. 不要让一个文件的失败中断整体——异常隔离 + 错误记录字段,让管道永远跑完

  8. 不要在三版并存时不提取公共代码——复制粘贴的维护成本是指数级的


文档数据通用处理思维框架
https://blog.cikaros.cn/archives/wen-dang-shu-ju-tong-yong-chu-li-si-wei-kuang-jia
作者
Cikaros
发布于
2026年06月16日
许可协议