← 返回列表

阅读 —下载 —

企业级 RAG:离线构建流水线

本文聚焦 RAG 的离线构建链路:文档解析、Chunking、Metadata Enrichment、Embedding 与索引构建。

一、整体结构

企业级 RAG 整体架构:离线构建与在线检索

二、离线构建(Offline Pipeline)

Document

格式有PDF、Markdown、DOCX、企业 Wiki(飞书、语雀(阿里)) 。

Parsing

决策层两种方案

在 Parsing 这一层,我们当时考虑过两种方案。

  1. 把文档直接交给 LLM,让模型判断文档格式,再选择对应的解析策略。
  2. 第二种是程序准确判断+路由。也就是自己实现一个 Deterministic Document Router,即确定性文档路由,根据文件扩展名以及文档本身的一些确定性特征进行路由。

最终我们选择第二种,主要考虑到两点:

  1. 文件类型可以由程序准确判断。
  2. 额外调用 LLM,成本和延迟更高,高并发场景下也不稳定,容易产生瓶颈。

文件解析策略

文档解析流程

                     Raw Document                              ↓                   Document Extraction                              ↓          ┌───────────────────┴───────────────────┐          │                                       │   根据文件类型选择 Parser                    提取 Visual Elements          │                                       │   ┌──────┼──────┬──────┐                  ┌─────┴─────┐   ↓      ↓      ↓      ↓                  ↓           ↓ Markdown DOCX  Excel   PDF               OCR        Vision   ↓      ↓      ↓      ↓                  ↓           ↓ MD     DOCX   Excel  PyMuPDF          Text      Description Parser  Parser  Parser    │               └─────┬─────┘                         │                       │                         └───────────┬───────────┘                                     ↓                            Text / Structure                             + Visual Result                                     ↓                            Sequence + Position                                     ↓                                   回填                                     ↓                            Structured Document                                     ↓                                  Cleaning                                     ↓                                  Chunking

采用统一的 Document Extraction Pipeline。

首先对输入文档进行解析,将其中的文本、标题、表格等结构化内容提取出来,同时识别并提取图片等视觉 Element,并保留它们在原始文档中的 Page、Sequence、Position 等信息。

对于提取出来的视觉 Element,我们再根据内容特征选择对应的处理方式。

如果是扫描件中的文字,使用 OCR 将图片转换成文本;

如果是架构图、流程图、图表等需要理解视觉语义的内容,则调用 Vision Model 进行分析,生成对应的图片描述。

处理完成之后,再根据原始的 Page 和 Element Sequence 将这些结果回填到原始的 Structured Document 中。

并发解析和顺序保证

整个过程中,文本解析、图片提取、OCR 和 Vision 等任务都可以按照 Page / Element 粒度并发执行。由于不同任务的执行时间可能不同,我们会保留 document_id、page_sequence、element_sequence 等信息,最终按照原始 Sequence 重新排序并回填,保证解析后的 Structured Document 与原始文档的内容顺序一致。

结构化文档示例

{ "doc_id": "employee-handbook-v3", "source": { "filename": "handbook.pdf", "parser": "pdf", "pages": 12 }, "tree": [ { "id": "h1", "type": "heading", "level": 1, "text": "员工管理制度", "page": 1, "bbox": [72, 80, 520, 110], "order": 1 }, { "id": "h2", "type": "heading", "level": 2, "text": "第二章 考勤", "parent": "h1", "page": 3, "bbox": [72, 140, 400, 165], "order": 8 }, { "id": "p12", "type": "paragraph", "text": "病假最长不得超过 30 天。", "parent": "h2", "page": 3, "bbox": [72, 180, 520, 210], "order": 9 }, { "id": "t3", "type": "table", "parent": "h2", "page": 4, "bbox": [72, 240, 520, 420], "order": 15, "cells": [ ["假别", "最长天数", "材料"], ["病假", "30", "医院证明"], ["年假", "10", "无需"] ] }, { "id": "img2", "type": "figure", "parent": "h2", "page": 4, "bbox": [80, 450, 300, 620], "order": 16, "ocr_text": "请假流程:提交 → HR 审核 → 归档", "description": "请假流程图,从员工提交申请到 HR 审核结束。", "caption": "图 2-1 请假流程" } ] }

Cleaning

Cleaning 的核心原则是去除噪声,但不破坏文档的语义和结构,为后面的 Chunking 做准备。

比如重复的 Header/Footer、页码、多余空白、OCR 产生的明显格式问题以及重复内容。同时会根据 Element 类型进行处理,例如 Table、Code、Heading 等不能简单当成普通文本清洗,需要保留它们原有的语义结构。

Chunking

解决什么问题

原始文档可能有几十页甚至几百页,不能直接整个文档做 Embedding、整个文档拿去检索。

所以需要:把一个大的 Structured Document 切成多个语义相对完整、适合检索的 Chunk。

一个好的chunk:语义完整 + 上下文足够 + 不包含大量无关内容。

切分策略

  • 优先进行结构化切分:根据结构化文档的标题、章节等结构进行语义切分成一个个chunk;
  • 二次大小切分:如果某个chunk超过了设定的 chunk Token Size,再进行二次大小切分,设置少量 Overlap 避免边界信息丢失。
  • 高级切分策略:
  • 父子切分:
    • 核心矛盾:chunk过大,上下文完整,但是召回率低;chunk过小,上下文缺失,但是召回率高。
    • 所以我们采用**Parent-Child,**把较大的 Section 作为 Parent,再切成较小的 Child,Child 建立向量索引用于精准召回,这样同时通过 parent_id 找回对应 Parent,为 LLM 提供更完整的上下文。
  • 语意切分: Semantic Chunking,**主要解决 Chunk 边界的问题。**用embadding模型对计算相邻句子的语意相似度,当出现明显下降时,认为主题发生变化,在这个位置建立chunk边界。

最终解析策略

最终解析策略:结构化切分与语义切分

“在设计 Chunking 方案的时候,我们参考了 亚马逊Amazon Bedrock 的 Semantic Chunking 方案。它的核心思路是对所有句子进行 Embedding,通过相邻句子之间的语义相似度变化来识别 Semantic Boundary,再结合 Max Token Size 形成最终 Chunk。我们没有直接照搬这种对整个文档进行 Semantic Chunking 的方式,而是在这个基础上做了一层优化”

结构化切分 + 语意切分 + chunk size约束

  • 首先根据文档本身的结构进行第一层切分,形成 Structure Chunk。如果这个 Chunk 没有超过我们设定的 Max Chunk Size,就直接保留,不需要额外做语义切分。
  • 如果某个 Structure Chunk 比较大,超过了 Max Chunk Size,我们才在这个 Structure Chunk 内进行 Semantic Chunking。具体会把内容拆成 Sentence,对 Sentence 做 Embedding,然后计算相邻 Sentence 的语义相似度,找到语义变化明显的位置作为候选边界。如果切分后依然超过max chunk size,继续语意切分。
  • 一般来说通过语意切分不需要overlap,但是我们保留了这个参数坐兜底,可以通过离线评测效果决定是否启用,补充语意,默认没有。

Metadata Enrichment

用作检索时的过滤和权限控制。

文档级

来源文档本身:document_id、name、文件类型和版本

结构级

来源Parser后:page、section、heading、chunk_index

业务级

来源业务系统:tenant、user_id,部门和权限范围

注意:metadata本身不会被向量化,而是chunk content

Embedding

把文本转换成向量,让机器可以用数学空间中的“距离/相似度”来衡量 Query 和 Chunk 的语义相关性,从而支持后续语义检索。

模型选择

Embedding 模型不是单纯按照公开榜单选择,而是先根据语言、领域和成本筛选候选模型,再用我们自己的 Query-Document 数据集进行离线检索评测,综合 Recall、延迟和成本选择最终模型。

注意:Query 和 Document 必须使用一致的 Embedding 模型,保证 Query Vector 和 Document Vector 位于同一个向量空间。

向量维度也不是越高越好,维度越高 → 表达能力可能更强,但存储、计算、索引成本也会上升。

embedding模型更换,历史数据如何处理?

新模型上线

↓

重新 Embedding 所有历史 Chunk

↓

建立新的 Index

↓

灰度验证

↓

切换到新 Index

Indexing

把 Chunk 的向量建立高效的检索索引,让后面的 Retrieval 不需要把整个Milvus库一个个向量比较。

两种经典的搜索策略:Exact Search和 ANN。注意它们都属于语义检索。

Exact Search

**精确搜索,**Recall:最高,速度:较慢

FLAT/Brute Force

最简单的精确搜索。全表扫描/暴力搜索。

例子:

用户问:“Java 线程池核心线程数怎么配置?”

先embadding:

q = [0.13, 0.81, 0.25, ...] ← 768维

Query Vector

 │

 ├── 和 Vector 1 算相似度 → 0.31

 ├── 和 Vector 2 算相似度 → 0.82

 ├── 和 Vector 3 算相似度 → 0.21

 ├── ...

 └── 和 Vector 1,000,000 算相似度 → 0.76

                          ↓

                     排序 / TopK

                          ↓

假设已经做过正则化,每次点积都会计算768次乘法和767次加法

score = q1v1 + q2v2 + ... +qn*vn

粗略计算量:

1,000,000 * 768

这还只是一次扫描,所以我们要建立索引,如hnsw。

建立索引之后,不再逐个计算 100 万个向量,而是先根据索引进入图搜索,根据配置参数ef缩小搜索范围,平衡搜索的速度和质量。

但这并不意味着flat/brute force就无用。

FLAT/Brute Force 会对所有向量进行精确距离计算,可以作为 Recall 的 Ground Truth;生产环境通常使用 HNSW、IVF 等 ANN 索引,通过减少实际参与距离计算的候选数量降低 P95/P99 延迟、提升 QPS,但会引入一定 Recall 损失。因此实际调优不是简单追求最快,而是在 Recall@K、P95/P99、QPS 和内存之间做 Trade-off。

算相似度的几种方式:余弦和点积最常见

  1. 余弦相似度 最常见 两个向量的方向是否相似。语义方向的企业 RAG、知识库问答、文档检索里非常常见。

$$\cos(q,d)=\frac{q\cdot d}{|q|,|d|}$$

  1. 点积 如果 Query 和 Document Vector 都做了 L2 Normalize,即

$$||q||=||d||=1$$

那么:

$$q\cdot d = cos(q,d)$$

Cosine ≈ Normalized Inner Product

这时候用 Inner Product 可以直接做点积,计算更方便。

  1. 欧氏距离 适合模型本身的向量空间把欧氏距离作为主要度量方式的场景。如聚类,异常检测,空间距离分析

$$d(q,d)=\sqrt{\sum(q_i-d_i)^2}$$

ANN

Approximate Nearest Neighbor 近似最近邻搜索 快速找到大概率最接近的 10 个,Recall:可能略低,速度:非常快。

HNSW

Hierarchical Navigable Small World。基于多层图结构进行近似最近邻搜索。

类似于跳表的思想,从高层快速定位,下降到下一层,继续缩小范围,底层精确搜索候选Topk。

三个参数:M,efConstruction,ef

M

每个节点大概维护多少条图连接。

M ↑——图连接更多——搜索质量可能更高——内存增加——建索引成本增加

efConstruction

建 Index 的时候搜索多少候选。影响建索引阶段。

efConstruction ↑——建索引时搜索范围更大——构建时间 ↑——内存/计算成本 ↑——Index 质量通常更好——Recall 通常更高

ef

查询的时候搜索多少候选。影响查询的时候。

ef ↑——搜索更多候选——Recall ↑——Latency ↑

IVF

Inverted File Index

先把向量空间划分成很多 Cluster,找 Query 最接近哪些 Cluster,只搜索这些 Cluster,Top-K。通过减少搜索区域加速检索。

参数有nlist,nprobe。

nlist

整个向量空间划分成多少个 Cluster。

nprobe

Query 查询的时候,要搜索多少个 Cluster。

PQ

Product Quantization

解决:向量太多,存储和计算成本太高。

核心:牺牲一定精度,换取更低的存储和计算成本。

简单理解PQ: 原始 Vector [0.12, 0.83, 0.42, ...] ↓ 切成多个子空间 [0.12, 0.83] [0.42, ...] ... ↓ 每个子空间用较短的 Code 表示 [12] [87] [31] ... 最终: Vector ↓ 压缩 ↓ 更小的表示

经常和IVF 组合:IVF_PQ

四种索引选择/比较

四种向量索引选择对比(一)

四种向量索引选择对比(二)

不同索引评测trade-off

不同索引评测 trade-off