乐于分享
好东西不私藏

企业级知识框架(二)PDF,Docx,云文档等原始数据文件,如何解析入库?

企业级知识框架(二)PDF,Docx,云文档等原始数据文件,如何解析入库?

知识库文档入库Pipline:解析、切块、存块、完成

在知识库系统中,一个文档从上传到可被检索,要经过一条固定的入库流水线。无论上传的是 PDF、Word、Excel 还是图片,系统都会将其送入同一条流程,依次经过解析、切块、存块、完成四个阶段,把原始数据文件转换成结构化、可检索的文本片段。

本文按顺序拆解这四个阶段:每一步的输入与输出、内部实现,以及关键设计取舍。

入库流水线总览

本节先给出流水线的整体视图:它包含哪几个阶段、要处理哪些输入,以及为什么选用 Markdown 作为统一的中间格式。

四个处理阶段

四个阶段各自独立,前一阶段的输出是后一阶段的输入。

阶段
职责
输入 → 输出
解析
将任意格式统一转换为 Markdown
原始文件 → Markdown(含图片引用)
切块
将长文档切分为语义完整的片段
Markdown → 一组 chunk
存块
原文写入关系库,向量写入向量库
chunk → 关系库 + 向量库
完成
更新任务状态与进度
处理中 → 已完成

原始数据的格式分类

按信息的组织方式,保存到知识库的原始数据可分为四类。格式类别决定了解析阶段的处理方式。

类别
代表格式
本质
解析要点
纯文本 / 标记文本
txt、markdown、html、json
普通字符文本
接近可直接使用
富版式文档
pdf、word、ppt
内容与版式混合存储
需版面分析还原阅读顺序
表格数据
excel、csv
二维数据,语义体现在行列关系
须保留行列与表头的对应
多模态
图片、音频、视频
内容非文字
先转写为文字再进入后续阶段

富版式文档内部差异明显:Word 底层是 XML,保留了一定结构;PPT 由逐页的图文框组成;PDF 记录的是每个字符在页面上的坐标,而非逻辑结构,因此多栏排版、页眉页脚、跨页表格、扫描图像都需要通过版面分析还原阅读顺序。表格数据若按普通文本流抽取,会丢失表头与数据的对应关系。多模态内容必须先转写为文字才能进入后续阶段:图片经光学字符识别(OCR,Optical Character Recognition)或视觉模型转为文本,音频经自动语音识别(ASR,Automatic Speech Recognition)转为文本。

中间格式:Markdown

解析阶段将所有格式统一转换为 Markdown,并以 Markdown 作为贯穿全流程的中间格式。Markdown 是一种轻量级、以语义为中心的文档格式:它保留了标题、列表、表格、引用、代码块等层级结构,同时剥离了 Word、PDF 中大量与内容无关的排版细节。

选择 Markdown 作为中间格式,基于以下几点:

  • 纯文本,可直接消费。 大模型以 token(模型处理文本的基本单位)的形式处理文本,Markdown 接近最简文本形态,模型与后续代码无需再解析复杂文件格式。
  • 保留结构,且结构即切分依据。 标题、列表、表格等标记用极少的符号表达文档层级,这些标记正是切块阶段的天然切分边界。
  • Token 利用率高。 表达同样的结构,Markdown 几乎没有冗余符号,占用 token 更少,成本更低,也更不易超出上下文窗口。
  • 训练语料充分。 训练数据中包含大量 Markdown,模型对其解析成本低。
  • 易于二次转换。 产出后可方便地转为网页、PDF、Word 等格式。

解析

解析的目标是将各种格式的原始文件统一转换为 Markdown(必要时附带图片)。系统将解析实现为可配置的两条路线:调用第三方 API,或使用本地代码解析。

方案 A:第三方解析 API(MinerU)

MinerU 是上海人工智能实验室 OpenDataLab 团队开源的文档解析工具,可将 PDF、图片、DOCX、PPTX、XLSX 等转换为 Markdown / JSON(项目地址,技术报告见 arXiv:2409.18839)。将文件传给该服务,即可直接得到一份解析好的 Markdown 与图片。

以 PDF 为例,MinerU 的处理链路分三步:

  1. 版面分析。 用检测模型扫描每一页,将页面切分为若干区域并分类(标题 / 正文 / 表格 / 公式 / 图片 / 页眉页脚),同时判断阅读顺序。
  2. 分区识别。 每类区域交由专门的模型处理。
区域类型
处理方式
产出
文字区
OCR 或读取文字层
纯文本
公式区
公式识别模型
LaTeX(数学公式排版语言,形如 $...$
表格区
表格识别模型
HTML / Markdown 表格
图片区
裁剪并存为图片文件
正文插入 ![](images/xxx.jpg)
  1. 拼接。 按阅读顺序将各区域拼接成完整的 Markdown 文档。

该方案成本较高,解析质量高,能较完整地还原 PDF 结构。

方案 B:本地代码解析

本地方案不依赖外部服务,用一批开源库处理。成本低,解析质量与源格式强相关。

PDF

主流程使用微软开源的文档转 Markdown 工具 MarkItDown(项目地址)抽取文字,其底层调用 PDF 文本抽取库 pdfminer.six(项目地址)。抽取结果只有文字、不含格式。pdfminer.six 源码注释明确写道:Most style information is ignored, so the results are essentially plain-text(样式基本被丢弃,结果本质上是纯文本)。

当抽取不到文字时(典型为扫描件 PDF),触发兜底:用 PDF 渲染库 pypdfium2(Google PDFium 引擎的 Python 绑定)将 PDF 渲染成页面图,经 Python 图像处理库 Pillow 转为 JPEG,再调用具备 OCR 能力的多模态大模型识别文字。

该方案成本低,但仅适合纯文本 PDF:它只能平铺输出文字流,表格会塌陷为乱序文本,公式会乱码,图片会丢失。

DOC / DOCX

先判断格式。老版 .doc 需先转为 .docx:命令行调用开源办公套件 LibreOffice 完成转换,等效于在后台用办公软件"另存为 docx";转换失败时,用 antiword(专门读取老 .doc 的命令行工具)直接抽取纯文本。

转为 docx 后走 docx → md,同样调用 MarkItDown。MarkItDown 对不同格式采用不同的内部实现:PDF 走 pdfminer.six(丢结构),docx 走一条保留结构的链路:

  1. 将 docx 转为 HTML。 由 docx 转 HTML 库 mammoth 完成——它读取 docx 的 XML,将 Word 样式语义映射为 HTML 标签:"标题 1" → <h1>、加粗 → <strong>、项目符号 → <ul><li>、表格 → <table>。这是一次保留语义的转换。
  2. 将 HTML 转为 Markdown。 底层用 HTML 解析库 BeautifulSoup 解析 HTML、移除 <script>/<style>,再用 HTML 转 Markdown 库 markdownify 将标签转为 Markdown:<h1> → #<table> → 管道表格、<strong> → **粗**

因此 docx → markdown 能保留标题层级与表格,质量高于 PDF。差异根源不在代码,而在于 docx 本身携带结构信息,PDF 不携带。

主路失败时走 docx 文档处理库 python-docx 兜底:遍历文档对象树,抽取纯文字与图片,不保留格式与表格结构。它在主路失败时保底,至少能保留文字和图片。

PPT

调用 MarkItDown,底层是 PPT 文档处理库 python-pptx,逐页逐形状翻译后拼成 Markdown:

  1. 打开 PPT,获取所有幻灯片。
  2. 逐页遍历,将当前页的所有形状(shape)按位置从上到下、从左到右排序,还原阅读顺序。
  3. 按形状类型转换:标题文本框 → # 标题,普通文本框 → 原文字。
  4. 将所有页拼接成一段 Markdown 返回。

Excel

Excel 的本质是二维数据,直接按文本流抽取会丢失行列关系。本地解析器基于数据分析库 pandas,采用逐行转记录的策略:

  1. 逐个工作表读取,丢弃全空行。
  2. 将每一行转为一句 列名: 值, 列名: 值,例如 姓名: 张三, 年龄: 25, 城市: 北京
  3. 每一行直接对应一个切块单元(chunk,定义见下文"切块"一节)——Excel 在解析阶段就完成了切块。

这种形式还原的是带字段名的记录,而非表格外观,对按行检索更友好。

解析质量取决于源格式的结构信息

将所有格式横向排列,解析质量由源格式保留的结构信息决定,与代码实现的努力程度无关。

源格式的结构信息
代表格式
可还原的结果
完整结构(标签 / 样式)
docx、pptx、html、xlsx
保留标题、表格、列表
仅有文字,无版式
PDF 文字层
只能平铺为纯文本
无文字层
扫描 PDF、图片
只能转为图,交 OCR / 视觉模型

下游拿到的 Markdown 质量,在文件被创建的那一刻就已大致确定。解析层能做到不丢失源格式已有的结构,但无法还原源格式中不存在的结构。

切块

定义

切块(chunking)是将一篇完整的长文档切分为若干较短、语义相对完整的片段(chunk)。每个 chunk 独立编号、独立存储,各自进行向量化并被独立检索。

切块要解决的问题

切块服务于检索增强生成(RAG,Retrieval-Augmented Generation)场景:先从知识库检索出与问题相关的内容,再将这些内容提供给大模型作答。切块解决四个问题:

  • 长度限制。 文本向量化(embedding)模型和大模型的输入窗口有限,整篇长文无法直接输入,必须切小。
  • 检索精度。 用户问题通常只对应文档中的某一小段。若整篇作为一个单位,检索只能整篇命中或不命中,无法定位具体段落;切小后才能精准召回相关片段。
  • 向量聚焦。 用一个定长向量表示整篇长文,区分度低;内容越长越杂,向量越模糊。单段只讲一件事,向量才聚焦。
  • 成本控制。 只将命中的片段放入 prompt,而非整篇文档,可节省 token,并使模型注意力集中在相关内容上。

切块规则

规则的整体流程:先保护不可切分的内容,再沿自然边界从粗到细切分,达到约 512 字符封口,相邻块保留约 80 字符重叠,每块附加所属标题。

  1. 长度上限。 每个 chunk 最多约 512 个字符(据代码注释换算,约合 100–130 个英文 token 或约 300 个中文 token,随分词器而变)。
  2. 重叠。 相邻两块共享约 80 个字符,即上一块结尾的 80 字符会出现在下一块开头,避免一句话或一个答案被切口截断导致两块都召不全。
  3. 保护规则。 先圈定公式 $$…$$、表格、图片 ![]()、链接、代码块,这些内容整块保留,不从中间切分(单块超过 7500 字符的硬上限时才强制切分)。
  4. 自然边界。 普通文字按分隔符切分,优先级为空行 \n\n → 换行 \n → 句号 ,从粗到细。
  5. 标题规则。 每块附加所属章节标题作为上下文,标题本身不计入正文长度。
  6. 自适应与兜底。 先对文档做特征判断,自动选择切法(按标题切 / 启发式 / 递归);切完校验,不合格则降级换用下一种,最终兜底用递归切分。
  7. 父子规则(可选)。 先切大的父块提供上下文,再将父块切为小的子块用于检索,子块记录所属父块。

规则的设计依据

  • 按分隔符切分而非按字数硬切。 硬切到第 512 个字符,可能将一句话截成两半,两个 chunk 都不通顺、向量都变差。沿段落、句子等自然边界切分,每块才是一个完整语义单位。
  • 逐级递归、先大后小。 优先保留更大的语义单位:能按段落切就不按句子切,段落上下文更完整;只有当一段仍超预算时,才在段内按更细的边界切分。
  • 保护公式 / 表格 / 代码。 这些内容从中间切开即失效:半张表、半个公式没有意义,向量也会失真,必须整块保留。
  • 附加所属标题。 单独被检索出的片段已脱离原章节,附加标题相当于补上出自哪一节的上下文,提升检索与回答的准确度。

512 与 80 的取值依据

本项目将单块上限设为约 512 个字符、相邻块重叠约 80 个字符(约为块长的 15%)。字符与 token 是不同单位,512 字符换算成 token 随分词器而变,中文约数百、英文约 100–130。这组数值属于工程配置,其背后的取舍有以下依据:

  • 块大小直接影响检索效果,需要在"精确"与"上下文"之间权衡。 论文《Rethinking Chunk Size for Long-Document Retrieval》(arXiv:2505.21700)在多个数据集上的实验表明:较小的块(64–128 token)更适合事实型、实体型问题的精确检索,较大的块(512–1024 token)更适合需要跨段上下文的问题;不同 embedding 模型对块大小的敏感度也不同。本项目 512 字符的量级偏中小,倾向于精确检索,与知识库问答"定位具体段落"的目标一致。
  • 沿自然边界、从粗到细切分是主流做法。 例如 LangChain 的递归字符切分器 RecursiveCharacterTextSplitter,默认分隔符顺序为 ["\n\n", "\n", " ", ""](官方文档),同样先按段落、再按行、最后按更细粒度切分,以尽量保留完整语义单位。本项目的"空行 → 换行 → 句号"与之思路一致。
  • 重叠用于避免语义被切口截断,但取值无统一标准。 各框架默认值不一:LangChain 的 chunk_overlap 默认 200,DataStax RAGStack 建议约 128 token;社区常用区间约为块长的 10%–20%,本项目取约 15%,落在该区间内。重叠的实际收益与语料、切分方式相关,若要为本项目确定最优值,【建议补充针对自身语料的对比实验数据作为依据】。

字符与 token 是两个不同的单位,512 字符不等于 512 token。上述研究与框架默认值多以 token 计量,此处作为量级参考,不宜与字符数直接等同。

存块

存储方式

存块将切好的每个 chunk 持久化,通常保存两份:原文写入关系型数据库的 chunk 表(内容加元数据),向量写入向量库。本项目的向量库使用 pgvector(PostgreSQL 的向量检索扩展,项目地址)。

遍历每个 chunk,执行两步操作。

写关系库:构造一条记录,除 content 外附带一组元数据,逐条或批量 insert 进 chunk 表。

写向量库:将 content 送入 embedding 模型得到向量,连同 chunkID / knowledgeId / knowledgeBaseId 一起批量写入(BatchIndex)向量库,建立向量与 chunkID 的对应,并将该 chunk 标记为已索引。

图片的 OCR / caption 子块以及父子结构同样入库,子块记录 ParentChunkID

主要元数据字段:

字段
作用
seq / chunkIndex
顺序号,用于还原上下文、定位相邻块
knowledgeId
所属文档
knowledgeBaseId
所属知识库
tenantId
多租户隔离
chunkType
类型(text / image_ocr / caption 等),检索时按类型过滤
start / end
原文位置,用于高亮与溯源
status / isEnabled
状态,控制是否参与检索、标记索引进度
时间戳
创建 / 更新时间

双库存储的设计依据

  • 关系库与向量库分工。 向量库擅长按语义近似快速召回,不适合存储和管理原文;关系库擅长按 ID 精确存取、支持事务与增删改,无法完成语义检索。两者组合:向量库定位是哪几段,关系库取回这几段的原文。
  • 向量库只存向量与 ID。 向量库为相似度计算优化,负责召回;召回后凭 ID 回关系库取原文即可。原文只保留一处,避免冗余和多点维护。
  • 元数据的用途。 顺序号还原上下文;knowledgeBaseId 支持按库检索与级联删除;tenantId 实现多租户隔离;chunkType 支持按类型过滤;start/end 用于溯源与高亮;status 控制片段是否参与检索。
  • 子块记录 ParentChunkID。 支持层级检索:子块精准召回,命中后经 ParentChunkID 找到父块补充上下文。
  • 入库容错。 embedding 依赖外部模型,可能变慢或失败。设计上让向量化失败不影响已完成的原文入库,避免主流程被一次模型抖动拖垮。

完成

状态与进度

最后一步更新状态:文档记录的状态从"解析中"改为"已完成解析",进度设为 100。

整个解析入库是异步后台任务。用户上传后,前端无法感知后台进度,因此需要一个明确的状态机与进度值。

状态
进度
前端表现
pending
0
排队等待
processing
50 → 90
解析中
completed
100
可被检索
failed
显示错误信息

独立完成阶段的作用

completed 是对外的就绪信号,表示文档解析入库全部结束、可以被检索。在此之前状态为 processing,前端显示解析中;中途出错则走 markAsFailed 并写入错误信息,而非 completed。

小结

入库流水线的四个阶段构成从文档到可检索知识的完整转换:解析统一格式,切块控制粒度,存块完成双份归档并支持召回与溯源,完成阶段对外发出就绪信号。两个设计约束贯穿始终:解析质量的上限由源格式的结构信息决定;切块与存块的参数取舍,都围绕上下文窗口、检索精度与成本三者的平衡。

参考资料

  • MinerU(OpenDataLab 开源文档解析工具):GitHub、技术报告 arXiv:2409.18839
  • MarkItDown(微软开源文档转 Markdown 工具):GitHub
  • pdfminer.six(PDF 文本抽取库):GitHub
  • mammoth(docx 转 HTML 库):GitHub
  • 分块大小研究:《Rethinking Chunk Size for Long-Document Retrieval》arXiv:2505.21700
  • LangChain 递归字符切分器 RecursiveCharacterTextSplitter:官方文档
  • pgvector(PostgreSQL 向量检索扩展):GitHub