
RAG 成熟度模型(一):索引工程与文档理解
很多团队做 RAG 的第一版都长这样:
PDF -> 文本提取 -> 固定长度切块 -> embedding -> vector DB
这条链路的问题在于:它把“文档”错误地当成了“纯文本”。生产系统里的知识并不只存在于句子里,还存在于标题层级、表格行列、页码、脚注、版本、权限、图注、代码块、阅读顺序和上下文位置里。只要这些结构在入库时丢掉,后面的 rerank、prompt、LLM 再强也只能在错误信息上做推理。
这一篇讨论 RAG 成熟度的第一层:索引工程。核心问题不是“用哪个向量库”,而是:
- 原始文档如何变成可验证的结构化元素;
- chunk 如何保留跨段上下文;
- embedding 模型如何匹配语言、领域和检索方式;
- 索引 schema 如何同时支持 dense、sparse、metadata、ACL、版本和引用追溯。
一、成熟度分层:从文本切块到证据索引
可以把 RAG 索引层分成 5 个成熟阶段。
| 阶段 | 索引方式 | 典型能力 | 主要风险 |
|---|---|---|---|
| L0 | 原文全文塞 prompt | 无真正索引 | 长文不可控,成本高 |
| L1 | 固定长度 chunk + dense vector | 能做语义召回 | 表格、标题、版本全部弱化 |
| L2 | 结构化解析 + metadata | 支持过滤、引用、权限 | 仍然容易 chunk 孤岛化 |
| L3 | 父子块 + contextual chunk | 检索粒度和生成粒度分离 | 索引成本上升 |
| L4 | dense + sparse + multi-vector | 语义、关键词、细粒度 token 匹配并行 | 存储和融合复杂 |
| L5 | 图索引 / 树摘要 / 版本化证据库 | 支持全局总结、多跳、时序追溯 | 构建和维护成本高 |
成熟系统不会把所有文档一视同仁。FAQ、合同、代码、财报、研究论文、客服聊天记录需要不同解析策略。真正的工程边界是:入库时能不能还原“这个证据来自哪里、在什么结构中、适用于哪个版本、谁有权看、置信度是多少”。
二、文档解析:RAG 最常见的隐藏瓶颈
文档解析不是简单 OCR。复杂 PDF 里至少有 5 类结构会影响检索质量:
- 阅读顺序:双栏论文、页眉页脚、脚注会打乱文本顺序;
- 表格结构:如果表格被拉平成几行文本,数字关系会丢失;
- 标题层级:chunk 缺少父标题时,模型不知道段落主题;
- 版面元素:图注、公式、列表、代码块需要不同保留方式;
- 页码和坐标:引用必须能回到原文位置,否则用户无法验证。
开源解析工具可以按“通用 ETL”和“深度文档理解”两类看。
| 工具 | 适合场景 | 技术特点 | 风险 |
|---|---|---|---|
| Docling | 企业文档、PDF、Office、图片、音频转文本 | 强调 advanced PDF understanding、reading order、table structure、公式、统一 DoclingDocument 表示、本地运行 | Python 生态,复杂文档解析速度和模型依赖需要压测 |
| Unstructured | 多格式文档 ETL、分区、清洗、chunking、embedding 前处理 | partition 思路清晰,生态成熟,适合把非结构化文档转为 element 流 | 高质量 PDF 场景常需要策略调参 |
| MinerU | 中文/科研/复杂 PDF,转 Markdown/JSON | 目标是把复杂 PDF/Office 转成 LLM-ready Markdown/JSON,对表格、公式、版面友好 | 工程集成时要关注模型体积、GPU/CPU 性能 |
| Marker | 快速 PDF -> Markdown/JSON | 速度快、输出简洁,适合批量知识库原型 | 对极复杂版面需要人工抽样评估 |
| RAGFlow | 想要开箱即用的文档理解 + RAG 引擎 | 把深度文档理解和 RAG workflow 打包成系统 | 自定义底层 pipeline 的灵活度低于自建 |
我的建议:
- 纯文本/Markdown/网页:自己写结构解析器,保留标题路径和 DOM/Markdown 层级;
- Office/PDF 混合知识库:Docling 或 Unstructured 做第一层解析,再做自定义清洗;
- 中文 PDF、论文、表格密集文档:MinerU / Marker 做候选,按样本集比较表格、公式和阅读顺序;
- 不想自建 ingestion pipeline:RAGFlow 可以作为验证基线,但生产系统仍应保留独立评测集。
三、元素流:不要直接从文本进入 chunk
成熟 ingestion pipeline 不应该输出纯字符串,而应该先输出 element stream:
{
"doc_id": "policy-2026-001",
"version": "2026.1",
"element_id": "p12-table-2-row-4",
"type": "table_row",
"title_path": ["财务制度", "差旅", "酒店标准"],
"page": 12,
"bbox": [108, 220, 510, 286],
"text": "上海 | 一线城市 | 酒店上限 | 800 元/晚",
"neighbors": ["p12-heading-3", "p12-table-2-row-3", "p12-table-2-row-5"],
"parser_confidence": 0.91
}
这样做的收益很直接:
- 表格行可以单独检索,同时能回到整张表;
- 段落可以带标题路径,避免“这段话在说什么”丢失;
- 页面坐标可以支持 PDF 高亮;
- 解析置信度可以进入 rerank 或拒答策略;
- 文档版本和 ACL 可以在召回前过滤,而不是召回后删。
如果 ingestion 阶段只保留 text,生产后期想补引用、权限、版本追溯会非常痛苦。
四、chunk 策略:检索粒度和生成粒度要分离
固定长度 chunk 的问题是,它把两件事混在一起:
- 检索需要“小粒度”,便于精确匹配;
- 生成需要“大上下文”,便于模型理解完整含义。
成熟方案通常用父子块:
parent block: 一个章节 / 一个表格 / 一个函数文件 / 一段完整制度条款
child block: 句群 / 表格行 / 函数定义 / 小段落
检索时用 child block,进入 prompt 时返回 parent 或相邻窗口:
hits = dense_search(query, level="child", top_k=50)
parents = group_by_parent(hits)
context = expand_parent_window(parents, max_tokens=6000)
这样做避免两个失败:
- chunk 太大:embedding 被多个主题污染,召回不准;
- chunk 太小:召回到一句话,但模型缺少上下文,生成乱补。
五、Contextual chunk:给 chunk 补“位置语义”
Anthropic 的 Contextual Retrieval 方案强调:在建索引前,为每个 chunk 补一段说明,告诉检索器“这个 chunk 在整篇文档中处于什么位置”。这不是摘要替代原文,而是给 embedding 和 BM25 增加 disambiguation。
例如原 chunk:
上限为 800 元/晚,超出部分需要审批。
增强后:
本文档是 2026 年中国区差旅报销制度。本段位于“酒店标准 / 一线城市”章节,说明上海、北京、深圳等一线城市酒店报销上限。
上限为 800 元/晚,超出部分需要审批。
适合做 contextual chunk 的场景:
- 法务、财务、制度文档;
- 长章节中大量代词、省略主语;
- 表格行、列表项、FAQ 答案脱离标题后语义不完整;
- 多版本文档,需要说明适用时间。
不适合的场景:
- 错误码表、API 参数表这种结构本身已经明确的文档;
- 高频更新小文本,因为每次更新都要重新生成上下文;
- 高合规场景中无法接受 LLM 生成的上下文污染索引,除非上下文也能审计。
六、Embedding 模型比较:不要只看 leaderboard
Embedding 选择要看 6 个维度:
- 语言:中文、英文、多语言、跨语言;
- 上下文长度:512、8K、32K;
- 输出维度和存储成本;
- 是否支持 instruction;
- 是否支持 sparse / multi-vector;
- 推理成本:CPU、GPU、批量吞吐、量化支持。
| 模型 | 开放方式 | 强项 | 适合场景 | 注意点 |
|---|---|---|---|---|
| BGE-M3 | MIT | dense、sparse、multi-vector 三合一;100+ 语言;8192 token | 中英混合、希望一个模型同时支撑 hybrid 检索 | 1024 维,multi-vector 存储成本高 |
| Qwen3-Embedding | Apache-2.0 版本可用 | 0.6B/4B/8B 多尺寸;32K;100+ 语言;支持自定义维度和 instruction | 中文/多语言企业知识库,长文本检索,追求最新开源性能 | 4B/8B 推理成本明显高,线上要做 TEI/vLLM 压测 |
| multilingual-e5-large-instruct | 开放权重 | instruction retrieval 成熟,多语言稳定 | 问答、跨语言检索、需要 instruction 查询模板 | 文档侧和查询侧格式要严格一致 |
| Nomic Embed Text v1.5 | 开放权重 | 英文通用语义检索,维度可控 | 英文知识库、轻量部署 | 中文和跨语言不是第一选择 |
| Jina Embeddings v3 | 开放权重/商业生态混合 | 多语种、多任务、长文本能力 | 多语言搜索和分类任务混合 | 使用前确认 license 与部署边界 |
关键结论:生产 RAG 不应该只依赖单一 dense embedding。即使 BGE-M3 或 Qwen3-Embedding 很强,关键词、数字、代码符号、专有名词仍然需要 sparse 或 BM25 补位。
七、索引 schema:把证据当一等公民
一个成熟的 chunk 表不是只有 text 和 embedding:
CREATE TABLE rag_chunks (
chunk_id text PRIMARY KEY,
doc_id text NOT NULL,
parent_id text,
version text NOT NULL,
title_path text[],
element_type text,
page int,
bbox jsonb,
text text NOT NULL,
contextual_text text,
metadata jsonb,
acl text[],
valid_from timestamptz,
valid_until timestamptz,
parser_confidence real,
created_at timestamptz DEFAULT now()
);
再配多路索引:
-- dense vector
embedding vector(1024)
-- sparse/BM25 可走外部 search engine,也可用 tsvector
search_vector tsvector
-- 权限/版本/时间过滤
CREATE INDEX ON rag_chunks USING gin (acl);
CREATE INDEX ON rag_chunks (doc_id, version);
CREATE INDEX ON rag_chunks (valid_from, valid_until);
生产里最容易被忽视的是 ACL 前置过滤。如果先检索 top-k,再删除用户无权访问的结果,top-k 位置已经被污染,最终进入 prompt 的证据会不足。正确做法是把租户、权限、版本、时间条件尽量推到检索引擎内部。
八、索引平台选择:先看数据规模和事务需求
| 存储/索引 | 强项 | 适合 | 不适合 |
|---|---|---|---|
| pgvector | 和 Postgres 共存,事务、JOIN、ACL、版本管理方便 | 中小规模企业知识库,已有 Postgres 团队 | 百亿级向量、复杂分片、多租户超大规模 |
| Qdrant | 向量检索工程成熟,payload filter 强,hybrid query 体验好 | 中大型向量检索、过滤多、服务独立部署 | 强事务和复杂关系查询 |
| Milvus | 高规模向量数据库,支持 dense+sparse hybrid | 大规模向量、云原生、专门搜索团队 | 小团队轻量项目可能过重 |
| Weaviate | hybrid search、schema、模块化能力强 | 希望搜索和对象 schema 一体化 | 已有稳定数据库栈时迁移成本高 |
选择规则:
- 文档少于 100 万 chunk,团队已有 Postgres:先用 pgvector;
- 需要独立向量服务、payload filter、hybrid query:Qdrant;
- 需要大规模向量、多集合、高吞吐:Milvus;
- 想要对象 schema + hybrid search 一体:Weaviate。
九、索引质量的离线评测
索引层至少要有下面几类测试集:
| 测试类型 | 目的 |
|---|---|
| 标准问答 -> gold evidence | 看正确 chunk 是否能被召回 |
| 数字/表格问题 | 看表格解析和 row-level index 是否可靠 |
| 版本问题 | 看旧版本是否被排除 |
| 权限问题 | 看 ACL 是否在召回前生效 |
| 跨标题问题 | 看 parent-child 和 contextual chunk 是否有效 |
| 无答案问题 | 看系统是否错误召回弱相关内容 |
指标:
recall@20
mrr@20
ndcg@20
gold_parent_recall
acl_violation_rate
version_error_rate
parser_error_bucket
不要等到生成答案再评测。索引层如果 recall@20 不合格,后面所有生成效果都是偶然。
十、一套推荐的索引底座
如果要做一个成熟企业 RAG,我会这样起步:
文档解析:
Markdown/HTML -> 自研结构解析
PDF/Office -> Docling + MinerU 抽样对比
元素模型:
element stream -> parent/child chunk -> source span
上下文增强:
只对制度、合同、长文档做 contextual chunk
Embedding:
中文/多语言:Qwen3-Embedding-0.6B 或 BGE-M3
需要 dense+sparse+multi-vector 统一:BGE-M3
索引:
pgvector 起步
规模扩大后迁移 Qdrant/Milvus
必备 metadata:
doc_id, version, parent_id, title_path, page, bbox, acl, valid_from, valid_until, parser_confidence
这套方案的重点不是“最先进”,而是可验证、可迁移、可迭代。索引层要先把证据建模清楚,检索层才有优化空间。