RAG 索引阶段
索引阶段是 RAG 的离线建库流程先把原始数据源加载出来再切成 Chunk生成 Embedding写入向量库最后做入库验证和版本管理。只有索引阶段做得好后面的检索生成阶段才可能答得准。一句话理解数据源 - 文档加载 - 文档切分 - 嵌入编码 - Milvus 入库 - 入库验证 - 版本更新索引阶段不直接回答用户问题它的目标是提前把知识整理成“可检索”的形式。输入PDF、Word、Markdown、Excel、CSV、网页、API、数据库等原始数据源。处理中间态Document、Chunk、Embedding。输出可检索的向量数据、原文片段、元数据、索引版本和验证结果。为什么索引阶段重要RAG 的核心是“先检索再生成”。检索质量很大程度取决于索引阶段。如果索引阶段没做好会出现这些问题文档加载不完整答案缺少依据。文档切分太粗检索结果噪声大。文档切分太碎上下文语义不完整。Embedding 模型不合适相似度匹配不准。元数据缺失无法按来源、页码、业务类型过滤。向量库索引不合理检索慢、成本高、召回差。1. 数据源数据源是知识库的原始输入。常见来源类型示例处理重点文本文件.txt、.md保留标题、段落、列表结构办公文档.docx、.xlsx、.csv处理表格、标题层级、单元格语义PDF电子 PDF、扫描 PDF、混合 PDF处理版面、OCR、表格、图片、页码网页HTML、文档站点去除导航、广告、页脚等噪声API业务接口、知识接口统一字段结构和更新时间数据库MySQL、PostgreSQL、ES明确哪些字段适合入库索引阶段第一步要先判断数据源是什么类型因为不同类型的数据源适合不同加载方式。2. 文档加载文档加载负责把不同格式的数据统一读取成程序可处理的文档对象。在 LangChain 这类框架中加载后的文档通常抽象为DocumentDocument - page_content正文文本 - metadata元数据比如 source、page、file_name - id可选文档唯一标识文档加载的目标把不同格式统一成同一种结构。尽量保留来源、页码、标题等元数据。为后续切分、嵌入、检索做准备。Markdown 加载Markdown 是半结构化文本适合做知识库文档。原因#、##可以表达标题层级。-、1.可以表达列表。代码块、引用块、表格等结构比较清晰。加载 Markdown 时要尽量保留标题层级因为标题可以帮助后续 Chunk 判断语义边界。Word 加载.docx本质上是 XML 压缩包机器可以读取段落和文本块。难点在于Word 用户不一定使用标准“标题 1 / 标题 2”样式。有些文档只是把字体加粗、放大看起来像标题但对解析程序来说可能还是普通段落。处理建议如果 Word 标题样式规范可以直接用文档加载器。如果标题层级混乱加载后要检查解析结果。对结构要求高的文档可以先转成 Markdown 或用专业解析工具处理。PDF 加载PDF 是最复杂的文档类型之一。常见问题有些 PDF 是扫描图片需要 OCR。有些 PDF 是双栏排版直接抽文本可能顺序错乱。表格、公式、图片、页眉页脚容易混在正文里。页码、水印、目录可能变成无效噪声。PDF 解析时要关注两个结果文本是否完整。文本顺序是否符合人类阅读顺序。MinerU 这类文档解析工具可以把 PDF 转成 Markdown、JSON 等机器可读格式。对于复杂 PDF可以先解析成结构化 Markdown再进入 RAG 流程通常比直接抽纯文本更稳定。3. 文档切分加载后的文档通常很长不能整篇直接拿去做检索所以要切分成多个 Chunk。Document - Chunk([text])为什么要切分问题不切分的后果Token 限制长文档可能超过模型上下文窗口检索不精确返回整篇文档会带入大量噪声向量化成本高整篇文档生成 Embedding 成本更高语义被稀释长文本向量容易丢失局部重点Chunk 是后续存储和检索的基本单位。切分要平衡什么切分不是越细越好也不是越大越好需要平衡三个因素维度目标问题长度控制每个 Chunk 大小适中太大噪声多太小语义不完整语义完整尽量保留完整段落或句子切断句子会影响理解边界合理优先按标题、段落、句号切强行截断会丢上下文常见切分策略策略原理优点缺点固定长度切分按字符数或 Token 数切简单稳定容易切断语义语义切分根据语义变化切分语义完整性好成本高块大小不稳定递归字符切分按分隔符优先级逐级切工程上常用平衡效果和成本需要调分隔符和大小递归字符切分器实际项目里常用递归字符切分因为它在“语义完整”和“长度可控”之间比较平衡。核心思想优先按大边界切不行再降级到小边界常见分隔符优先级separators [\n\n, \n, 。, , , , , ]理解方式分隔符语义边界\n\n段落\n行。、、句子短句词字符级兜底示例from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , , ], ) chunks text_splitter.split_documents(docs)chunk_size 和 chunk_overlapchunk_size控制每个 Chunk 的最大长度。chunk_overlap控制相邻 Chunk 的重叠长度。为什么要 overlap没有 overlap边界信息可能丢失 有 overlap相邻块共享一小段上下文减少断裂常见经验普通知识文档chunk_size可从 500-800 字符开始试。代码、配置、表格类文档要按结构切不要只按长度切。chunk_overlap不宜太大否则重复内容多存储和检索成本会上升。4. 嵌入编码切分后每个 Chunk 还是普通文本。计算机无法直接理解“语义相似”所以要用 Embedding 模型把文本转成向量。Chunk([text]) - Embedding([float])Embedding 是一组浮点数。语义相近的文本在向量空间里距离通常更近。为什么需要 Embedding检索阶段要做的是用户问题向量 vs 文档 Chunk 向量然后通过余弦相似度、内积或 L2 距离找到最相似的内容。如果没有 Embedding只能做关键词匹配无法很好处理同义词、近义词和语义表达差异。常见模型选择模型维度特点bge-small-zh512轻量中文可用bge-base-zh768中文场景常用bge-large-zh1024精度更高成本更高bge-m31024多语言支持稠密和稀疏向量text-embedding-3-small1536OpenAI 云端模型性价比高text-embedding-3-large3072OpenAI 云端模型精度更高选择模型时要考虑中文效果。向量维度。速度和成本。是否需要本地部署。是否需要稠密 稀疏混合检索。文档向量和查询向量索引阶段生成的是文档向量doc_vectors embeddings.embed_documents([ 第一段文档, 第二段文档, ])检索阶段生成的是查询向量query_vector embeddings.embed_query(用户问题)注意文档和查询最好使用同一个 Embedding 模型至少要保证维度一致、语义空间兼容。Embedding 抽象接口Embedding 抽象接口就是把“文本转向量”这件事封装成统一入口。一句话理解Embedding 抽象接口 不管底层用什么模型对外都提供统一的向量生成方法它的作用是让 RAG 系统不用直接依赖某一个具体 Embedding 模型。比如现在用bge-m3以后换成 OpenAI Embedding 或其他本地模型时索引流程和查询流程不用大改只需要替换接口背后的模型实现。文档 Chunk - Embedding 接口 - 文档向量 用户问题 - Embedding 接口 - 查询向量常见接口一般会分成两个方法方法用来干什么使用阶段embed_documents()把多个文档 Chunk 转成文档向量索引阶段embed_query()把用户问题转成查询向量查询阶段示例doc_vectors embeddings.embed_documents([第一段文档, 第二段文档]) query_vector embeddings.embed_query(RAG 的索引阶段是什么)这个抽象接口主要解决三个问题统一调用方式不管底层是 BGE、OpenAI 还是本地模型外层代码都调用同一套方法。方便替换模型模型升级时只改接口实现不改整个 RAG 流程。保证维度一致Embedding 输出维度必须和 Milvus Collection 里的向量字段维度一致。需要特别注意文档向量和查询向量最好使用同一个 Embedding 模型。比如文档入库用的是bge-m3生成的1024维向量查询阶段也应该用bge-m3生成1024维查询向量。否则两边不在同一个向量空间里Milvus 检索出来的相似度就不可靠。稠密向量和稀疏向量BGE-M3 比较适合用来理解稠密向量和稀疏向量因为它可以在同一个模型里同时生成稠密向量 dense_vector负责语义相似 稀疏向量 sparse_vector负责关键词匹配稠密向量稠密向量就是一组连续浮点数。以bge-m3为例它的稠密向量是1024维也就是每段文本会被转换成1024个数字。简化示例文本RAG 的索引阶段会把文档切分成 Chunk再生成 Embedding 存入 Milvus。 bge-m3 稠密向量 [0.018, -0.034, 0.126, ..., 0.052]这个向量真正存储时不是几个数字而是1024个浮点数。它的作用是表达整段文本的语义。稠密向量更擅长处理“意思相近但关键词不完全一样”的问题。例子用户问题RAG 怎么把资料放进向量库 文档 Chunk索引阶段会把文档切分成 Chunk再生成 Embedding 存入 Milvus。这两句话没有完全相同的表达但意思接近。bge-m3生成查询向量后可以通过稠密向量检索找到这个 Chunk。稀疏向量稀疏向量也是向量但它不是每一维都有值而是只给重要 token 分配权重。大部分位置都是0所以叫“稀疏”。简化示例文本Milvus 的 HNSW 索引参数包括 M、efConstruction 和 ef。 bge-m3 稀疏向量可以理解成 { Milvus: 0.92, HNSW: 0.88, efConstruction: 0.95, ef: 0.71, 索引: 0.63 }真实存储时通常不是直接存中文词而是存 token id 和权重可以简单理解为{ 1842: 0.92, 9135: 0.88, 12077: 0.95 }稀疏向量更擅长处理关键词、术语、编号、参数名、错误码这类精确匹配。例子用户问题efConstruction 是什么 文档 ChunkMilvus 的 HNSW 索引参数包括 M、efConstruction 和 ef。这里efConstruction是关键。如果只靠语义系统可能召回很多“索引优化”相关内容稀疏向量会更容易命中包含efConstruction这个精确术语的 Chunk。BGE-M3 输出示例使用bge-m3时可以同时拿到稠密向量和稀疏向量from FlagEmbedding import BGEM3FlagModel model BGEM3FlagModel(BAAI/bge-m3) output model.encode( [Milvus 的 HNSW 索引参数包括 M、efConstruction 和 ef。], return_denseTrue, return_sparseTrue, ) dense_vector output[dense_vecs][0] sparse_vector output[lexical_weights][0]可以这样理解输出结果输出含义用途dense_vecs稠密向量1024维浮点数语义检索lexical_weights稀疏向量重要 token 的权重关键词检索在 Milvus 里怎么用如果用 BGE-M3 做混合检索Milvus Collection 可以设计两个向量字段dense_vector - FLOAT_VECTOR存 bge-m3 的稠密向量 sparse_vector - SPARSE_FLOAT_VECTOR存 bge-m3 的稀疏向量查询时也生成两种向量用户问题 - bge-m3 - 查询稠密向量 查询稀疏向量然后分别检索稠密检索找语义相似的 Chunk 稀疏检索找关键词匹配的 Chunk 融合排序把两路结果合并得到最终 Top-K对比总结类型bge-m3 输出擅长例子稠密向量dense_vecs语义相似“怎么入库”和“索引阶段”语义接近稀疏向量lexical_weights关键词匹配HNSW、efConstruction、错误码、参数名BGE-M3 的价值在于一个模型就能同时提供语义检索能力和关键词检索能力后续可以在 Milvus 里做混合检索。5. 向量存储向量生成后需要存到支持相似度检索的向量库里。Embedding([float]) - VectorStore向量库通常不只存向量还要存原文 Chunk。元数据。主键 ID。稠密向量字段。稀疏向量字段。业务过滤字段。为什么不用普通数据库直接查普通数据库擅长精确匹配和范围查询比如where id 1 where created_at 2026-01-01向量检索要找的是“语义最相似”不是字段完全相等。如果在普通数据库里对所有向量逐个计算相似度会接近全表扫描数据量大后性能很差。向量库的核心价值支持高维向量相似度检索。支持 ANN、HNSW 等索引加速。支持 Top-K 检索。支持向量字段和元数据字段一起查询。常见向量库向量库适用场景FAISS本地实验、高性能相似度搜索库Chroma快速原型、轻量知识库Milvus当前使用生产环境、大规模向量检索pgvectorPostgreSQL 项目里增加向量能力Redis已使用 Redis且需要内存级响应Elasticsearch统一管理全文检索、结构化数据和向量检索6. Milvus 入库设计本笔记按当前实际选择向量数据库使用 Milvus。可以把 Milvus 理解成“向量世界的数据库”它专门用于高维向量的存储、索引和相似度检索。CollectionCollection类似关系型数据库里的表。创建 Collection 时要先定义 Schema。一个典型 RAG Collection 可以包含字段类型作用idINT64主键tenant_idVARCHAR租户或业务隔离doc_idVARCHAR文档 IDchunk_idVARCHARChunk IDsourceVARCHAR来源文件或 URLpageINT64页码versionVARCHAR索引版本textVARCHARChunk 原文metadataJSON扩展元数据vectorFLOAT_VECTOR稠密向量用于语义检索sparse_vectorSPARSE_FLOAT_VECTOR稀疏向量用于关键词检索created_atINT64创建时间便于排查和更新示例from pymilvus import MilvusClient, DataType schema ( MilvusClient.create_schema(auto_idTrue) .add_field(field_nameid, datatypeDataType.INT64, is_primaryTrue) .add_field(field_nametenant_id, datatypeDataType.VARCHAR, max_length64) .add_field(field_namedoc_id, datatypeDataType.VARCHAR, max_length128) .add_field(field_namechunk_id, datatypeDataType.VARCHAR, max_length128) .add_field(field_namesource, datatypeDataType.VARCHAR, max_length512) .add_field(field_namepage, datatypeDataType.INT64) .add_field(field_nameversion, datatypeDataType.VARCHAR, max_length64) .add_field(field_nametext, datatypeDataType.VARCHAR, max_length1500) .add_field(field_namemetadata, datatypeDataType.JSON) .add_field(field_namevector, datatypeDataType.FLOAT_VECTOR, dim1024) .add_field(field_namesparse_vector, datatypeDataType.SPARSE_FLOAT_VECTOR) .add_field(field_namecreated_at, datatypeDataType.INT64) )向量字段和索引向量写入后如果没有索引每次检索都要暴力比较速度会很慢。这里要区分字段类型和索引类型向量类型Milvus 字段类型常见索引说明稠密向量FLOAT_VECTORHNSWRAG 常用召回和速度比较平衡稠密向量FLOAT_VECTORFLAT暴力精确检索适合小数据或评估基线稠密向量FLOAT_VECTORIVF_FLAT数据量较大时常用可以通过参数平衡召回和速度稀疏向量SPARSE_FLOAT_VECTORSPARSE_INVERTED_INDEX类似倒排索引适合关键词权重匹配稀疏向量SPARSE_FLOAT_VECTORSPARSE_WAND稀疏检索加速适合更关注查询速度的场景注意SPARSE_FLOAT_VECTOR 是字段类型不是索引类型。也就是说sparse_vector 字段类型SPARSE_FLOAT_VECTOR sparse_vector 常见索引SPARSE_INVERTED_INDEX 或 SPARSE_WAND示例index_params MilvusClient.prepare_index_params() index_params.add_index( field_namevector, index_typeHNSW, metric_typeCOSINE, ) index_params.add_index( field_namesparse_vector, index_typeSPARSE_INVERTED_INDEX, metric_typeIP, )相似度 metric 怎么选向量度量用来判断“两个向量有多相似”。Milvus 里常见的度量可以这样理解度量英文 / Milvus 常见写法看什么常见场景欧式距离L2两个向量的直线距离越小越相似图像、空间距离、通用向量余弦相似度COSINE两个向量方向是否接近越接近越相似文本语义匹配RAG 常用内积IP两个向量点乘结果越大越相似归一化后的文本向量、稀疏向量汉明距离HAMMING二进制向量有多少位不同二进制向量Jaccard 距离JACCARD两个集合的重叠程度集合、标签、二进制特征工程上可以这样选择如果文本 Embedding 已做归一化IP和COSINE的排序通常接近IP计算更快如果不确定是否归一化优先使用COSINE更稳。L2更适合需要空间距离语义的向量场景。7. 完整入库流程索引阶段最终要把前面几步串起来加载文档 - 切分文档 - 生成向量 - 组装字段 - 插入 Milvus伪代码import time # 1. 加载文档 docs loader.load() # 2. 切分文档 chunks text_splitter.split_documents(docs) # 3. 生成向量 texts [chunk.page_content for chunk in chunks] vectors embedding_model.encode(texts) # 4. 组装入库数据 rows [] for chunk, vector in zip(chunks, vectors): rows.append({ tenant_id: default, doc_id: chunk.metadata[doc_id], chunk_id: chunk.metadata[chunk_id], source: chunk.metadata[source], page: chunk.metadata.get(page, 0), version: rag_index_v1, vector: vector, text: chunk.page_content, metadata: chunk.metadata, created_at: int(time.time()), }) # 5. 插入向量库 client.insert(collection_namerag_collection, datarows)如果使用 BGE-M3 同时生成稠密和稀疏向量则入库数据可以包含两个向量字段rows.append({ tenant_id: default, doc_id: chunk.metadata[doc_id], chunk_id: chunk.metadata[chunk_id], source: chunk.metadata[source], page: chunk.metadata.get(page, 0), version: rag_index_v1, vector: dense_vector, sparse_vector: sparse_vector, text: chunk.page_content, metadata: chunk.metadata, created_at: int(time.time()), })8. 入库后验证向量写入 Milvus 后不代表索引阶段就结束了还要确认“数据真的可检索”。可以按这几项验证验证项看什么插入数量insert_count是否等于本次准备写入的 Chunk 数向量维度文档向量维度是否和 Collection Schema 的dim一致字段完整性doc_id、chunk_id、source、page、version是否写入索引状态稠密向量和稀疏向量是否创建了对应索引抽样检索随机选一个 Chunk用相关问题检索看能否召回元数据过滤用doc_id、source、tenant_id过滤看结果是否正确最小验证流程插入数据 - 确认写入数量 - 确认字段和维度 - 加载 Collection - 抽样做一次 Top-K 检索 - 检查返回 Chunk、score、source、page如果抽样检索都召回不到刚入库的内容通常要回头检查Embedding 模型是否和查询阶段一致。向量维度是否一致。metric_type 是否和索引一致。metadata filter 是否过滤过严。Chunk 是否切得过碎或过大。9. 索引版本与更新知识库不是一次建完就不变后面会遇到文档新增、文档修改、文档删除、模型升级所以要管理索引版本。为什么要有版本这些变化都会影响检索结果换 Embedding 模型。调整chunk_size和chunk_overlap。修改文档解析方式。修改 Milvus Schema。新增或删除文档。建议给每批索引数据记录版本version: rag_index_v1 embedding_model: bge-m3 chunk_size: 800 chunk_overlap: 100 created_at: 当前时间戳常见更新方式方式做法适合场景全量重建重新解析、切分、向量化、写入新版本换模型、换维度、改切分策略增量更新只处理新增或修改的文档日常文档更新删除重建删除某个doc_id的旧 Chunk再写入新 Chunk单篇文档发生变化版本切换新版本验证通过后再切换线上检索版本生产环境发布回滚新版本效果差时切回旧版本线上检索质量下降注意如果 Embedding 维度变了不要直接写入旧向量字段。比如旧模型是768维新模型是1024维应该新建 Collection 或新建向量字段然后重新向量化。最小更新闭环计算文档 hash - 判断文档是否变化 - 未变化跳过 - 新增/变化重新切分和向量化 - 删除旧 doc_id 对应 Chunk - 写入新 Chunk - 抽样检索验证 - 更新索引版本这样可以避免重复入库也方便后续排查“为什么这次检索结果变了”。10. 索引阶段检查清单建知识库时可以按这个清单检查文档加载原文是否完整读取。PDF 页码、文件名、来源是否进入 metadata。表格和标题是否被正确解析。是否过滤了页眉、页脚、目录、水印等噪声。文档切分Chunk 是否太大或太小。是否保留标题和段落语义。是否配置了合理 overlap。抽样查看前几个 Chunk确认阅读顺序正常。嵌入编码文档向量和查询向量是否使用同一模型。向量维度是否和向量库 Schema 一致。中文文档是否选择中文效果较好的模型。是否需要稠密 稀疏混合检索。向量存储是否存了原文 Chunk。是否存了 source、page、file_name 等元数据。是否创建了合适索引。metric_type 是否和检索阶段保持一致。是否能按元数据过滤。入库验证插入数量是否和 Chunk 数一致。是否能抽样检索到刚入库的内容。source、page、doc_id是否能正确返回。metadata filter 是否能正常过滤。版本更新是否记录索引版本。是否记录 Embedding 模型和维度。是否支持增量更新和删除旧文档。是否支持新旧版本切换和回滚。11. 三高设计索引阶段虽然是离线流程但真实项目仍然要考虑高并发、高性能、高可用。高并发批量文档入库不要阻塞在线问答使用队列异步处理。Embedding 调用要控制并发避免打爆模型服务或 API 限流。大文件解析、OCR、向量化可以拆成任务分片。在线查询和离线索引最好分开部署避免互相抢资源。高性能文档加载阶段先清洗噪声减少无效 Chunk。合理设置chunk_size和chunk_overlap减少重复向量。批量调用 Embedding减少网络和模型启动开销。向量库使用合适索引例如 HNSW、倒排索引。对重复文档做 hash 去重避免重复入库。高可用入库流程要可重试失败任务要能恢复。每个 Chunk 要保留 source、page、doc_id方便回溯。向量库要定期备份 Collection 或底层存储。模型、向量库、解析服务都要有超时和错误处理。索引版本要可追踪避免新旧数据混乱。新版本上线前要先抽样检索验证失败时能回滚旧版本。12. 记忆口诀先读把各种数据源加载成 Document 再切把长文档切成语义完整的 Chunk 再转把 Chunk 编码成 Embedding 向量 再存把向量、原文、元数据写入 VectorStore 再验抽样检索确认能查到正确 Chunk 再管用版本控制支持更新和回滚索引阶段做得越干净检索阶段就越容易召回正确内容检索召回越准生成阶段越不容易胡编。参考文档Retrieval-Augmented Generation for Knowledge-Intensive NLP TasksLangChain RetrievalLangChain Embedding ModelsLlamaIndex EmbeddingsOpenAI EmbeddingsBGE-M3 DocumentationMilvus BGE-M3 Embedding FunctionMilvus Index ExplainedMilvus Sparse VectorMilvus Documentation
