# 「解析、切分、检索」：从 PDF 到向量检索的代码拆解

> 作者：晨旭｜发布：2026-01-10｜系列：动手落地AI
> 来源：https://chenxu.xin/writing/pdf-to-vector-retrieval

接着金融研报 RAG 项目往下走一层：一份保险 PDF 从入库到被检索出来，要经历 PARSED → CHUNKED → EMBEDDED → INDEXED 四个状态，每个环节的代码在优化什么。

---

在之前的[金融研报 RAG 应用：「离线解析」和「在线问答」双链路详解](/writing/financial-rag-dual-pipeline)文章中，是从 RAG 系统的上帝视角，梳理了数据从入库到检索的流动逻辑。但当真的有一堆文档需要作为知识库给模型的时候，好像还是无从下手。

这篇文章是接着之前的金融研报项目，深入代码层面，梳理了从保险 PDF 文档到向量检索的代码逻辑。一共会分为四个部分，先从数据的角度梳理整个流程，再分别展开解析、切分、检索这三个模块。

（后面三章涉及到的代码会比较多，如果不太看得懂代码的话，可以看一看代码注释理解一下逻辑。）

## 从「静态文档」到「向量索引」

一个 PDF 文档「入库」的过程可以拆解为以下四个状态流转：

> PARSED（已解析）→ CHUNKED（已切分）→ EMBEDDED（已向量化）→ INDEXED（已索引）

### 1、解析阶段：不仅仅是提取文字

如果只是简单地提取文本（`text = pdf.read()`），就会丢失掉最重要的结构信息。标题、正文、页眉、页脚、表格如果全部混成一团字符串，后面的切分就会一团糟。

因此在工业级 RAG 中，解析阶段的输出不是一段文本，而是一个**结构化的文档对象树**：

- **层级结构**：谁是父章节，谁是子章节
- **样式信息**：字号、加粗、缩进（这些是判断标题级别的重要依据）
- **版面信息**：表格的行列关系、图片的上下文位置

在这个阶段，文档仅仅只是被「看懂」了。

### 2、切分阶段：元数据的诞生之地

拿着上一步得到的结构化对象就可以开始切分了。这个阶段的核心任务有两个：

**确定边界**：利用解析阶段得到的章节、段落信息，尽量让切分点落在自然的语义边界上（比如句号、段落结束符），而不是生硬地切断一句话。

**生成元数据（Metadata）**：不是切完就扔进数据库，而是在切分的同时，给每个 Chunk 办一张身份证：

- `chunk_id`：它的唯一标识
- `section_path`：它属于《保险条款》>第二章>责任免除
- `page_num`：它在第 15 页
- `prev/next_id`：它的前文是 Chunk A，后文是 Chunk B

此时得到的是一批「携带丰富身份信息的文本块」。

### 3、向量化阶段：文本转数学

在这个阶段，系统会遍历上一步生成的所有 Chunk，提取其中的 `content` 字段，调用 Embedding 模型（如 `bge-large-zh`），将其转化为一个高维向量。

这里只对文本做向量化，元数据通常不需要向量化，而是直接存储用于过滤。

### 4、存储与索引：双库联动

存完了是不是就能检索了呢？还不可以。为了兼顾「搜得准」和「看得懂」，通常会把数据存两份（逻辑上是这样，具体按情况而定）：

- **向量索引**：存放 `chunk_id` + `vector`，用来算余弦相似度，快速找到 Top-K 的 `chunk_id`
- **文档存储**：存放 `chunk_id` + `text` + `metadata`，用来存原始内容。当向量索引找到 ID 后，去这里把人能看懂的字和元数据也捞出来

再回顾一下这条链路：

1. PDF → 解析成结构化对象
2. 结构化对象 → 切分成带元数据的 Chunk
3. Chunk → 向量化成 Vector
4. Vector → 建索引，发布上线
5. 在线检索 → Query 变向量，去索引里找 Chunk，最后由 LLM 生成答案



搞清楚了整体的流程，接下来深入到每一个环节的代码实现，就会发现：所有复杂的代码，无非是在为这张流程图里的某一个箭头做优化。

## 解析模块

解析的质量直接决定了 RAG 的上限。

虽然可以直接使用通用的 PyPDF2，但结果可能会不尽人意；也可以使用 Deepdoc（LayoutLMv3 + PaddleOCR），但面对复杂场景仍需定制化改造。

### 1、攻克表格解析

保险文档中最难处理的是表格。文档中存在三类表格，单一的解决方案无法通吃。这个项目中设计了一套分流策略：

- **有边框表格**：Deepdoc 原生处理（准确率 95%）
- **无边框 / 半结构化表格**：这是重灾区，Deepdoc 识别率仅 62%

针对无边框表格，引入 MinerU 2.5。但因为它推理慢，不能全部都用这个方法，需要在解析前置加一个分类器，只对「对齐」但「无框」的区域调用 MinerU。



### 2、扫描件的去噪与去水印

金融存量文档中有 30% 是扫描件，且常常会带有「仅供内部使用」的红色印章或水印。这些干扰会导致 OCR 将文字识别为乱码。

这个项目的做法是摒弃传统的全局高斯滤波（会模糊文字），采用动态预处理 Pipeline。



这一步的优化让 OCR 在扫描件上的准确率直接提升了 21 个百分点。

## 切分模块

在之前的那篇文章中提到了 `naive_merge`（基于 Token 数的简单装箱）。但这种方法在保险条款面前则会失效。比如：

> 条款原文：「本保险承保……但以下情况除外：(1) xx……(2) yyy……」

固定切分的问题：切分点恰好落在「但以下情况除外」之前。

- Chunk A：「本保险承保……」
- Chunk B：「(1) xx……(2) yyy……」

后果：用户问「xx 保不保？」系统检索到 Chunk B，但缺少了前文的「除外」限定，导致 LLM 回答「保」。

为了解决这个问题，需要全面重构切分策略，从「固定长度切分」进化到「语义感知切分 + 智能 Overlap」。

### 1、基于文档树的递归切分

保留文档的章节感。切分逻辑不再是线性的，而是树状的：优先按章节切 → 章节过长按小节切 → 还长按段落切。

### 2、智能 Overlap

传统的 Overlap（如固定 100 tokens）可能会把句子拦腰截断。因此可以设计基于句子边界的 Overlap：它会回溯前一个 Chunk，找到最近的句号、问号或感叹号，确保 Overlap 的部分是一句完整的话。



此外，如果 Chunk 里只存了文本和向量，却缺少元数据，那么这个 chunk 就失去了灵魂。比如无法实现以下功能：

**答案溯源**

- 用户问：「核辐射在保障范围内吗？」
- 系统回答：「根据第 3 条 保险责任 > 3.2 责任免除 > (2)，核辐射不在保障范围。」

**检索加权**

- 关键条款（责任、免责、费率）的权重 ×1.5
- 识别方法：关键词匹配 + 章节标题判断

**上下文扩展**

- 如果检索到的 chunk 语义不完整，自动拉取前后 chunk
- 例如：检索到「但以下除外：」，自动拉取前一个 chunk 的「承保范围」

所以不能仅仅是存储文本，需要将 Chunk 定义为一个富信息的对象。可以扩展为四个维度：基础信息、结构信息、类型信息和位置信息。



## 检索模块

到了检索阶段，发现单一的向量检索在金融领域有致命弱点：**对低频专业词汇不敏感**。比如：

> 用户问：「犹豫期退保扣费吗？」

向量模型可能会召回「退保流程」相关的文档，因为它觉得语义很近，但却漏掉了包含「犹豫期」这个精确关键词的条款。

因此需要构建混合检索架构，并引入动态权重。

### 1、架构升级：向量 + BM25

- **向量检索**：使用 bge-large-zh-v1.5，处理语义查询（如「孩子摔伤怎么赔」）
- **关键词检索**：使用 BM25，处理精确查询

### 2、核心算法：基于意图识别的动态权重

这是检索系统的核心部分。不能对所有的用户问题都使用固定的 `0.5 * Vector + 0.5 * BM25`，需要先判断用户的意图：是查定义（精确）还是问咨询（语义）呢？



### 3、结果融合：RRF（倒数排名融合）

如果使用加权求和，需要对分数做归一化（Vector 是 0-1，BM25 可能是 0-50），这样很难调平。但如果使用 RRF，它不看分数，只看排名，会更加鲁棒。



### 4、Cross-Encoder 精排

为什么还要进行一次精排呢？经过混合检索召回的 Top-10 文档，虽然相关，但排序可能不准。

因此额外引入 bge-reranker-large 模型进行精排。但精排速度慢，一般只精排 Top-10，而不是 Top-50：

- 向量检索（Bi-Encoder）是「用户 query」和「知识库文档」独立编码
- Cross-Encoder 是将 query 和文档拼接在一起输入 BERT，能捕捉更细微的交互语义

至此，检索链路的使命完成。最后经过 Deepdoc 的解析、语义化的切分、混合检索的召回，以及最后 Cross-Encoder 的「去粗取精」，最终从成千上万的文档碎片中筛选出了含金量最高的 Top-10。

所有以上的代码优化，就是为了获取这几段优质文本。本质上都是在为目标业务场景买单。在这个金融领域，是对「犹豫期」「免责条款」的精准召回；在其他场景下，也许就是对速度的极致追求。

代码只是工具，业务场景才是归宿。

## 核心结论

- 工业级 RAG 的解析阶段输出的不是一段文本，而是一棵结构化文档对象树——包含层级、样式与版面信息。如果只做 text = pdf.read()，标题、正文、页眉、表格混成一团，后面的切分必然一团糟。（判断 · 把握较大）
  永久链接：https://chenxu.xin/writing/pdf-to-vector-retrieval#parsing-outputs-a-tree-not-text
- 切分阶段真正的产出不是文本块，而是元数据。chunk_id、section_path、page_num、prev/next_id 这张身份证，决定了系统后面能不能做答案溯源、检索加权和上下文扩展。只存文本和向量的 chunk 是没有灵魂的。（判断 · 把握较大）
  永久链接：https://chenxu.xin/writing/pdf-to-vector-retrieval#chunking-is-where-metadata-is-born
- 基于 Token 数的固定切分在保险条款面前会直接失效。切分点落在「但以下情况除外」之前，用户问「xx 保不保」就会检索到缺少除外限定的下半段，模型答「保」。必须换成基于文档树的递归切分加句子边界的智能 Overlap。（把握较大）
  永久链接：https://chenxu.xin/writing/pdf-to-vector-retrieval#fixed-length-chunking-breaks-clauses
- 单一向量检索在金融领域有个致命弱点：对低频专业词汇不敏感。问「犹豫期退保扣费吗」，向量模型会召回语义相近的「退保流程」，却漏掉真正包含「犹豫期」精确关键词的条款。这是必须上 BM25 的理由。（把握较大）
  永久链接：https://chenxu.xin/writing/pdf-to-vector-retrieval#vector-search-misses-rare-terms
- 所有这些解析、切分、检索的代码优化，本质都是在为目标业务场景买单。在金融领域是对「犹豫期」「免责条款」的精准召回，换个场景也许就是对速度的极致追求。代码只是工具，业务场景才是归宿。（判断 · 把握较大）
  永久链接：https://chenxu.xin/writing/pdf-to-vector-retrieval#code-serves-the-business-scenario

---

本文出自晨光里的AI（https://chenxu.xin），作者晨旭。
引用时请保留来源链接。文中标注「判断」「假设」的部分是作者的个人看法，不是事实。
