索引阶段是 RAG 的离线建库流程:先把原始数据源加载出来,再切成 Chunk,生成 Embedding,写入向量库,最后做入库验证和版本管理。只有索引阶段做得好,后面的检索生成阶段才可能答得准。
一句话理解
数据源 -> 文档加载 -> 文档切分 -> 嵌入编码 -> Milvus 入库 -> 入库验证 -> 版本更新索引阶段不直接回答用户问题,它的目标是提前把知识整理成“可检索”的形式。
- 输入:PDF、Word、Markdown、Excel、CSV、网页、API、数据库等原始数据源。
- 处理中间态:Document、Chunk、Embedding。
- 输出:可检索的向量数据、原文片段、元数据、索引版本和验证结果。
为什么索引阶段重要
RAG 的核心是“先检索,再生成”。检索质量很大程度取决于索引阶段。
如果索引阶段没做好,会出现这些问题:
- 文档加载不完整:答案缺少依据。
- 文档切分太粗:检索结果噪声大。
- 文档切分太碎:上下文语义不完整。
- Embedding 模型不合适:相似度匹配不准。
- 元数据缺失:无法按来源、页码、业务类型过滤。
- 向量库索引不合理:检索慢、成本高、召回差。
1. 数据源
数据源是知识库的原始输入。
常见来源:
| 类型 | 示例 | 处理重点 |
|---|---|---|
| 文本文件 | .txt、.md | 保留标题、段落、列表结构 |
| 办公文档 | .docx、.xlsx、.csv | 处理表格、标题层级、单元格语义 |
| 电子 PDF、扫描 PDF、混合 PDF | 处理版面、OCR、表格、图片、页码 | |
| 网页 | HTML、文档站点 | 去除导航、广告、页脚等噪声 |
| API | 业务接口、知识接口 | 统一字段结构和更新时间 |
| 数据库 | MySQL、PostgreSQL、ES | 明确哪些字段适合入库 |
索引阶段第一步要先判断数据源是什么类型,因为不同类型的数据源适合不同加载方式。
2. 文档加载
文档加载负责把不同格式的数据统一读取成程序可处理的文档对象。
在 LangChain 这类框架中,加载后的文档通常抽象为Document:
Document - 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_size=500, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", ",", ""], ) chunks = text_splitter.split_documents(docs)chunk_size 和 chunk_overlap
chunk_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-zh | 512 | 轻量,中文可用 |
bge-base-zh | 768 | 中文场景常用 |
bge-large-zh | 1024 | 精度更高,成本更高 |
bge-m3 | 1024 | 多语言,支持稠密和稀疏向量 |
text-embedding-3-small | 1536 | OpenAI 云端模型,性价比高 |
text-embedding-3-large | 3072 | OpenAI 云端模型,精度更高 |
选择模型时要考虑:
- 中文效果。
- 向量维度。
- 速度和成本。
- 是否需要本地部署。
- 是否需要稠密 + 稀疏混合检索。
文档向量和查询向量
索引阶段生成的是文档向量:
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 是什么? 文档 Chunk:Milvus 的 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_dense=True, return_sparse=True, ) 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 | 当前使用;生产环境、大规模向量检索 |
| pgvector | PostgreSQL 项目里增加向量能力 |
| Redis | 已使用 Redis,且需要内存级响应 |
| Elasticsearch | 统一管理全文检索、结构化数据和向量检索 |
6. Milvus 入库设计
本笔记按当前实际选择:向量数据库使用 Milvus。可以把 Milvus 理解成“向量世界的数据库”:它专门用于高维向量的存储、索引和相似度检索。
Collection
Collection类似关系型数据库里的表。
创建 Collection 时要先定义 Schema。
一个典型 RAG Collection 可以包含:
| 字段 | 类型 | 作用 |
|---|---|---|
id | INT64 | 主键 |
tenant_id | VARCHAR | 租户或业务隔离 |
doc_id | VARCHAR | 文档 ID |
chunk_id | VARCHAR | Chunk ID |
source | VARCHAR | 来源文件或 URL |
page | INT64 | 页码 |
version | VARCHAR | 索引版本 |
text | VARCHAR | Chunk 原文 |
metadata | JSON | 扩展元数据 |
vector | FLOAT_VECTOR | 稠密向量,用于语义检索 |
sparse_vector | SPARSE_FLOAT_VECTOR | 稀疏向量,用于关键词检索 |
created_at | INT64 | 创建时间,便于排查和更新 |
示例:
from pymilvus import MilvusClient, DataType schema = ( MilvusClient.create_schema(auto_id=True) .add_field(field_name="id", datatype=DataType.INT64, is_primary=True) .add_field(field_name="tenant_id", datatype=DataType.VARCHAR, max_length=64) .add_field(field_name="doc_id", datatype=DataType.VARCHAR, max_length=128) .add_field(field_name="chunk_id", datatype=DataType.VARCHAR, max_length=128) .add_field(field_name="source", datatype=DataType.VARCHAR, max_length=512) .add_field(field_name="page", datatype=DataType.INT64) .add_field(field_name="version", datatype=DataType.VARCHAR, max_length=64) .add_field(field_name="text", datatype=DataType.VARCHAR, max_length=1500) .add_field(field_name="metadata", datatype=DataType.JSON) .add_field(field_name="vector", datatype=DataType.FLOAT_VECTOR, dim=1024) .add_field(field_name="sparse_vector", datatype=DataType.SPARSE_FLOAT_VECTOR) .add_field(field_name="created_at", datatype=DataType.INT64) )向量字段和索引
向量写入后,如果没有索引,每次检索都要暴力比较,速度会很慢。
这里要区分字段类型和索引类型:
| 向量类型 | Milvus 字段类型 | 常见索引 | 说明 |
|---|---|---|---|
| 稠密向量 | FLOAT_VECTOR | HNSW | RAG 常用,召回和速度比较平衡 |
| 稠密向量 | FLOAT_VECTOR | FLAT | 暴力精确检索,适合小数据或评估基线 |
| 稠密向量 | FLOAT_VECTOR | IVF_FLAT | 数据量较大时常用,可以通过参数平衡召回和速度 |
| 稀疏向量 | SPARSE_FLOAT_VECTOR | SPARSE_INVERTED_INDEX | 类似倒排索引,适合关键词权重匹配 |
| 稀疏向量 | SPARSE_FLOAT_VECTOR | SPARSE_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_name="vector", index_type="HNSW", metric_type="COSINE", ) index_params.add_index( field_name="sparse_vector", index_type="SPARSE_INVERTED_INDEX", metric_type="IP", )相似度 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_name="rag_collection", data=rows)如果使用 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 Tasks
- LangChain Retrieval
- LangChain Embedding Models
- LlamaIndex Embeddings
- OpenAI Embeddings
- BGE-M3 Documentation
- Milvus BGE-M3 Embedding Function
- Milvus Index Explained
- Milvus Sparse Vector
- Milvus Documentation