将文档转换成可供检索的向量并存入数据库,是 RAG 系统搭建中最基础也最关键的一环。这个过程通常在离线阶段完成,决定了后续检索的效率和准确度。下面以真实项目中最常见的做法为例,逐步说明从原始文档到可用向量索引的完整流程。
1. 文档预处理:为切片做准备
原始文档格式五花八门(PDF、Word、网页、Markdown),在进入向量化之前需要先做一次“清洗”:
- 提取纯文本:使用对应解析器(如 PyPDF2、python-docx)将文档内容抽取为无格式文本。对于扫描件或图片型 PDF,需要先经过 OCR 识别。
- 去除干扰信息:清理页眉页脚、水印、乱码、多余空格,避免这些内容被当成有效知识切片。
- 保留结构化线索:如果文档有清晰的标题层级,可以在文本中保留标记(如
## 第二章 费用报销),这些信息对后续切片和检索有帮助。
2. 文本切片:把长文档拆成可检索的片段
大模型的上下文窗口有限,且检索精度在较短、语义集中的片段上更高。因此需要将长文档切分成若干小块(chunk)。
实用参数建议:
- 块大小:通常设置在 256~512 个 token(约 400~800 个中文字符)。太小会丢失上下文,太大则检索精度下降。
- 重叠量:相邻块之间保留 10%~20% 的重叠。例如块大小为 500 字,重叠 50 字。这能避免关键信息恰好落在两个块的边界而检索不到。
- 切分策略:优先按自然段落或句子边界切分,避免将一句话拆成两半。许多文本分割器(如 LangChain 的 RecursiveCharacterTextSplitter)会按段落、换行、句号的优先级递归切分。
示例:一份 5000 字的政策文档,按 500 字一块、重叠 50 字切分,大约产生 10~11 个切片。
3. 选择嵌入模型:决定向量质量
嵌入模型(embedding model)负责将文本片段转换成固定维度的向量,是决定检索语义匹配度的核心。选型时考虑三个因素:
- 领域适配性:通用嵌入模型(如 text-embedding-ada-002、bge-large-zh)在大部分场景表现良好。如果知识库有极强专业术语(如医药、法律),可考虑用领域数据微调过的模型。
- 向量维度:常用维度在 768 到 1536 之间。维度越高,语义表达能力越强,但存储和检索的开销也越大。如果没有明确收益,不必盲目追求高维。
- 成本与延迟:本地部署开源模型(如 BGE、M3E)可节省 API 调用费用,但需要自管推理资源;云端 API 简单省心,但大量入库时费用需评估。
实用做法:初期用通用中文嵌入模型(如 bge-large-zh-v1.5,维度 1024)起步,等系统上线后根据检索准确率再决定是否微调。
4. 批量向量化与元数据整理
切片完成后,对每个切片调用嵌入模型接口(本地或远程),得到对应的向量数组。同时为每个切片整理好元数据,随向量一同存库:
- 基本元数据:
source(文档名)、page(页码)、chunk_id(唯一标识)、updated_at(入库时间)。 - 业务元数据:文档类型(政策、手册、FAQ)、生效日期、部门等,方便后续按条件过滤检索。
注意:向量化是相对耗时的步骤,尤其切分出大量片段时。建议使用批量接口(一次传多条文本而非循环单条)以缩短处理时间。
5. 写入向量数据库
常见的向量数据库有 Milvus、Pinecone、Weaviate、Chroma 等,也可以使用带向量扩展的传统数据库(如 pgvector)。入库时一般做以下几件事:
- 建立集合(collection):指定向量维度和相似度度量方式(常用余弦相似度或内积)。
- 批量插入:将向量数组和对应的元数据一起写入。大多数数据库支持 upsert,即遇到相同主键(如
chunk_id)时会自动更新,这为增量更新提供了便利。 - 构建索引:数据库通常会基于向量构建近似最近邻(ANN)索引(如 HNSW、IVF),以支持快速检索。建议在数据写入完成后触发索引创建,避免逐条写入时频繁更新索引拖慢速度。
6. 验证与全量更新策略
入库完成后,需要简单验证检索是否生效:
- 用几个已知的问题查询,看返回的切片是否是预期的内容。
- 观察返回的相关度分数分布,如果普遍偏低(例如余弦相似度 < 0.7),可能需要调整切片策略或换用嵌入模型。
知识更新的典型做法:
- 增量更新:新文档或修改的文档,单独预处理、切片、向量化,然后通过
upsert写入库,利用原有索引。几分钟内即可生效。 - 全量重建:当大量内容变更或切分策略调整时,直接清空集合,重新跑完整入库流程。小型知识库(< 几万切片)一般几十分钟内可完成。
7. 一个真实场景的入库脚本骨架
下面是一个简化的 Python 伪代码,展示从文档到入库的核心步骤:
from langchain.text_splitter import RecursiveCharacterTextSplitter
from sentence_transformers import SentenceTransformer
import pymilvus
# 1. 读取文档
text = read_document("政策文件_v3.pdf")
# 2. 切分
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_text(text)
# 3. 向量化(本地模型)
model = SentenceTransformer("BAAI/bge-large-zh-v1.5")
embeddings = model.encode(chunks, batch_size=32, normalize_embeddings=True)
# 4. 组装元数据并写入 Milvus
entities = []
for i, (chunk, vec) in enumerate(zip(chunks, embeddings)):
entities.append({
"chunk_id": f"policy_v3_{i}",
"text": chunk,
"source": "政策文件_v3.pdf",
"page": i, # 简化示例,实际需从解析时获取页码
"embedding": vec.tolist()
})
collection.insert(entities)
collection.flush()
collection.create_index("embedding", index_type="HNSW", metric_type="COSINE")
通过这套流程,原始文档就被转化成了可随时检索的向量化知识库,为后续的实时问答打下基础。实际项目中,还可以用队列或定时任务将文档更新自动化,实现知识库的持续新鲜。