做企业级 RAG 的同学多少会遇到同一个尴尬:本地 demo 跑得很顺,文档一换、数据一多,检索结果就开始离谱。上一分钟还在演示“能回答问题”,下一分钟就在现场排查为什么合同编号查不到、PDF 解析出来全是乱码、向量库连不上。玩具级 RAG 和能用进生产的 RAG,差的不是一句 prompt,而是一条完整的工程链路。
这次聊的这个 20 集系列,主题很直接:不再停留在玩具级 RAG,而是把企业级向量检索的全流程走完。核心模块就三个——Milvus 向量数据库、PDF 文档解析、混合检索,中间穿插分块策略、向量化、RAG 框架接入和 API 化。这篇文章不打算复述课程内容,而是把整套链路整理成一篇可以照着做、照着排错的技术地图。标题里的关键词都不是摆设:能用 Milvus 存大规模向量还控制延迟,能把 PDF 解析成干净可检索的文本,能同时做语义召回和精确匹配,这三件事到位,基本就走出了玩具模式。
按实际使用顺序展开:先看这套方案值不值得、适用边界在哪,再准备环境、部署 Milvus、解析 PDF、写入向量、做混合检索、接口化,最后是性能观察、排错和从 RAG 走向知识图谱(KG)的进阶方向。
1. 核心能力速览
先给一张速览表,把整套方案的关键信息放在最前面,方便快速判断要不要继续往下看。
| 能力项 | 说明 |
|---|---|
| 方案类型 | 企业级 RAG 落地技术方案(20 集系列教程沉淀的完整链路) |
| 核心模块 | Milvus 向量数据库、PDF 解析、混合检索(稀疏向量 + 稠密向量)、RAG 编排与 API |
| 运行平台 | Linux / Windows / macOS |
| GPU 要求 | 不强依赖 GPU,CPU 即可完成向量化和检索测试;生产环境按数据量和模型选型配置 |
| 启动方式 | Docker Compose 启动 Milvus Standalone,或用 Milvus Lite 本地文件模式快速体验 |
| 接口能力 | Milvus 提供 Python / Java / Go SDK;RAG 服务可基于 FastAPI 封装 HTTP API |
| 批量任务 | 适合批量 PDF 解析、批量向量化入库、批量问答测试 |
| 主要瓶颈 | 检索召回质量、PDF 扫描件解析准确率、大规模数据下的索引和内存开销 |
| 适用场景 | 企业知识库、文档问答、客服辅助、内部资料检索、审计合规问答 |
| 核心价值 | 从“能跑 demo”升级到“能接真实业务数据” |
这套方案不是某一个小工具,而是由多个开源组件拼接成的一条生产链路。如果你已经有 RAG 基础,只是卡在数据解析、检索质量或者工程化上,这套内容正好覆盖这些缺口。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 玩具级 RAG 为什么不够用
先说结论:玩具级 RAG 通常只做对了一件事——把文本切成块、做 embedding、丢进向量库、检索 top-k 再丢给大模型。这个流程在公开演示数据上很漂亮,但换到企业真实数据以后,几乎每个环节都会出问题。
第一,文档解析太粗糙。企业里大量知识沉淀在 PDF、扫描件、表格、PPT 中。直接对 PDF 调用 get_text(),遇到扫描件会得到空白页,遇到分栏文档会得到错乱顺序,遇到表格会丢失结构。最后这些脏文本被送进向量库,检索质量自然崩。
第二,只做单路向量检索。向量检索擅长语义相似,但不擅长精确匹配。合同编号、产品型号、法条序号这类 token,改动一个字符语义变化不大,向量召回很容易漏。这也是为什么混合检索在企业级 RAG 里几乎是必选项,而不是加分项。
第三,没有规模和性能概念。玩具项目用 CSV 文件或小型向量库就能跑,但企业数据量很容易到几十万、上百万条记录。索引类型、分片策略、查询并发、内存占用,这些平时不起眼的参数会直接影响接口延迟和稳定性。Milvus 这类专业向量数据库的价值就在这里:它不只是存向量,还把索引、分区、标量过滤、混合检索这些能力做成了工程化接口。
第四,缺少评估和监控。很多项目做完了只靠人工肉眼判断回答好不好。生产环境需要一套评估集:预先准备几十条问题,记录每条问题的检索召回是否命中、最终回答是否准确、接口延迟是多少。没有评估集,RAG 调优就没有闭环,前面做的所有优化都是盲改。
第五,没有边界意识。企业知识库往往涉及内部资料、个人信息和版权内容。玩具项目不会考虑权限隔离、脱敏和日志审计;生产项目必须在一开始就把数据来源授权、内容合规和越权访问的边界划清楚。
下面用一张表总结差距:
| 维度 | 玩具级 RAG | 企业级 RAG |
|---|---|---|
| 文档解析 | 直接把 PDF 文本塞进去 | 区分扫描件、表格、分栏,做结构化和清洗 |
| 存储 | 本地文件 / 小型向量库 | Milvus,支持分区、过滤、多索引、水平扩展 |
| 检索 | 单路向量召回 | 稠密向量 + BM25 稀疏向量 + RRF 融合 + 重排 |
| 工程化 | Notebook 脚本 | Pipeline + HTTP API + 日志 + 监控 |
| 验收 | 肉眼判断 | 评估集 + 指标对比 + 回归测试 |
3. 适用场景与使用边界
这套技术栈适合以下场景:
- 企业内部知识库问答:产品手册、技术文档、流程制度、项目档案,员工提问后返回带出处的答案。
- 客服和运维辅助:把历史工单、故障记录、FAQ 做成检索库,客服输入问题后快速获得候选答案和相关工单。
- 审计与合规检索:合同、条款、法规文档的精确查找,要求检索结果可溯源,这时混合检索的精确匹配能力很重要。
- 垂直行业 RAG 应用:法律、医疗、金融、制造等领域的大模型问答,先用文档解析和向量化建库,再按领域做精调或重排。
同样要说明不适合什么。需要强事务一致性的场景不要用 RAG,比如订单状态查询、库存数量核对,这类数据正确的做法是直接查数据库或调用业务接口,而不是靠向量检索生成。需要多跳逻辑推理的复杂问题,单纯 RAG 会明显吃力,可以考虑知识图谱(KG)辅助,后文会单独展开。
合规边界必须反复强调:PDF 数据来源要确认有授权,涉及个人信息要提前脱敏,内网部署要考虑访问权限分离,对外发布的结果要有人工复核。图片、扫描件里如果包含人脸、签名等敏感信息,处理前就要做好合规评估。这一点不是套话,生产环境踩坑的代价往往远超开发成本。
4. 环境准备与前置条件
在开始部署之前,先给出一个通用环境清单。不同系统的细节有差异,按检查项逐项确认即可。
| 检查项 | 建议 |
|---|---|
| 操作系统 | Linux / macOS / Windows 均可,WSL2 可以跑 Milvus Docker |
| Python 版本 | 3.8 以上,建议 3.10 或更新版本 |
| Docker | 部署 Milvus Standalone 需要,Docker Desktop 或 Linux Docker 均可 |
| 内存 | 开发测试建议 4G 以上;数据量大时按索引类型和数据规模扩容 |
| 磁盘 | 预留 10G 以上;Milvus 镜像、PDF 样本、向量库文件都会占用空间 |
| GPU | 不强依赖;本地跑 BGE 系列 embedding 模型时 CPU 可运行,但速度偏慢 |
| 网络 | 需要拉取 Docker 镜像和 Python 依赖包;如果公司内网有限制,提前配置镜像源 |
接下来创建一个虚拟环境,再安装常用依赖。版本以你安装时的官方最新稳定版为准,不要照搬旧教程的锁定版本:
bash复制python -m venv rag_env
source rag_env/bin/activate # Windows 下改为 rag_env\Scripts\activate
pip install pymilvus langchain langchain-community \
pymupdf pdfplumber sentence-transformers \
fastapi uvicorn requests
依赖用途简单说明:
pymilvus:Milvus 的 Python SDK,同时内置 Milvus Lite 的本地文件模式。pymupdf/pdfplumber:PDF 文本提取,前者偏通用,后者更适合提取表格和坐标信息。sentence-transformers:本地跑 embedding 模型,比如 BGE-M3。langchain/langchain-community:RAG 编排和向量库接入。fastapi/uvicorn:把检索服务封装成 HTTP API。
如果电脑上没有 Docker,或者只是想在 Mac 上快速验证流程,可以跳过 Docker,直接使用 Milvus Lite。下一节会分别讲两种方式。
5. Milvus 部署:Standalone 与本地体验
企业级 RAG 的向量存储选型,核心考虑是:数据规模、索引能力、混合检索支持、运维复杂度。Milvus 是目前比较均衡的选择。它支持多种索引类型,支持标量字段过滤,也支持在同一个 Collection 里同时存放稠密向量和稀疏向量,这正是做混合检索的基础。
5.1 三种部署方式对比
| 方式 | 适合场景 | 特点 |
|---|---|---|
| Milvus Standalone | 开发、测试、小规模生产 | 单机部署,用 Docker Compose 拉起 etcd + MinIO + Milvus 三个组件 |
| Milvus Lite | 本地学习、原型验证 | 通过 pymilvus 直接创建本地文件数据库,不需要 Docker,Mac 上体验最方便 |
| Milvus 集群 | 大规模生产 | 多节点部署,支持更高并发和更大数据量,运维更复杂 |
Milvus Lite 带来的最大变化是:Mac 用户想搭建 RAG 知识库做验证时,不需要装 Docker、不需要配容器网络,直接在虚拟环境里建一个本地 .db 文件就能跑向量检索。代价是不适合大规模生产数据,超过一定量级还是建议换 Standalone。
5.2 用 Docker Compose 启动 Standalone
Milvus Standalone 的完整部署方式以官方文档为准。下面给一个用于理解组件关系的精简示例,实际使用建议直接参考官方提供的 docker-compose 配置:
yaml复制# 精简示例,完整配置请以 Milvus 官方文档为准
services:
etcd:
image: quay.io/coreos/etcd:v3.5.5
command: etcd -advertise-client-urls=http://etcd:2379 -listen-client-urls=http://0.0.0.0:2379 --data-dir /etcd
minio:
image: minio/minio:latest
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
command: minio server /minios --console-address ":9001"
standalone:
image: milvusdb/milvus:latest
command: ["milvus", "run", "standalone"]
ports:
- "19530:19530"
- "9091:9091"
depends_on:
- etcd
- minio
在包含该配置的目录下执行:
bash复制docker compose up -d
docker ps
启动后,Milvus 的 gRPC 端口是 19530,监控和健康检查端口是 9091。看到三个容器都在运行时,说明服务已经就绪。
5.3 用 Milvus Lite 做本地快速体验
如果不想动 Docker,直接在代码里初始化一个本地向量库:
python复制from pymilvus import MilvusClient
client = MilvusClient("./rag_kb.db")
print(client.list_collections())
执行后会在当前目录生成 rag_kb.db 文件,所有 Collection 和数据都存到这个文件里。这个模式对学习、调试、单元测试都友好。等流程跑通了,再把连接地址换成本机或内网的 Milvus Standalone 服务即可。
5.4 验证 Milvus 连接
无论用哪种方式,验证连接时都可以看端口和 SDK 调用:
bash复制# 如果部署了 Docker Standalone,先确认端口可访问
nc -zv 127.0.0.1 19530
python复制from pymilvus import MilvusClient
client = MilvusClient(uri="http://127.0.0.1:19530")
print(client.list_collections())
能打印出空列表,说明连接正常。
6. PDF 解析:企业数据入口
PDF 解析是绝大多数企业 RAG 项目的第一道坎,也是决定检索质量上限的环节。在向量化和模型之前,先得保证能从文档里拿到干净、有序、带结构的文本。
6.1 PDF 类型判断
PDF 大致分两类:
- 文本型 PDF:文字以字体和文本对象存在,可以用
PyMuPDF、pdfplumber直接提取。 - 扫描型 PDF:页面是一张图片,没有文本层,必须走 OCR 才能得到文字。
判断方法很简单:对一个页面执行文本提取,如果返回内容基本为空,大概率是扫描件。
6.2 文本型 PDF 提取
用 PyMuPDF 提取所有页面文本:
python复制import fitz # PyMuPDF
def extract_pdf_text(pdf_path: str) -> str:
doc = fitz.open(pdf_path)
pages_text = []
for page in doc:
text = page.get_text("text")
if text.strip():
pages_text.append(text)
doc.close()
return "\n".join(pages_text)
if __name__ == "__main__":
content = extract_pdf_text("demo.pdf")
print(content[:2000])
如果页数多,建议按页保留而不是全部拼接成一个字符串,后续分块会更灵活。
6.3 扫描型 PDF 的 OCR 方案
扫描件需要 OCR。常见可选方案包括 PaddleOCR、Tesseract 等。以 PaddleOCR 为例,流程是先把 PDF 页面转成图片,再逐页识别。
python复制import fitz
from paddleocr import PaddleOCR
def ocr_pdf_pages(pdf_path: str, dpi: int = 200):
doc = fitz.open(pdf_path)
# 接口参数在不同版本有差异,以实际安装版本为准
ocr = PaddleOCR(use_angle_cls=True, lang="ch")
results = []
for page in doc:
pix = page.get_pixmap(dpi=dpi)
img_path = f"page_{page.number}.png"
pix.save(img_path)
res = ocr.ocr(img_path, cls=True)
for line in res:
if line:
results.append(line[1][0])
doc.close()
return "\n".join(results)
注意,OCR 引擎的 API 在不同版本变化较大,示例代码主要是表达流程,实际运行要按安装版本调整参数。OCR 速度比文本提取慢得多,批量处理时建议用 GPU 或做并发加速,并且把识别结果落盘,避免失败后全部重跑。
6.4 解析阶段必须处理的细节
- 页眉页脚过滤:页码、公司名、日期会污染语义,检索时容易带出无意义内容。
- 表格结构:
pdfplumber可以提取表格行列,保留为 Markdown 表格格式比纯文本拼接效果好。 - 分栏文档:双栏 PDF 如果直接按页提取,文本顺序会错乱,需要按栏切分后再合并。
- 公式和图片:公式转文本质量普遍不高,实际项目里要么舍弃,要么单独抽出来做 OCR,不要混在正文里。
PDF 解析没有一步到位的完美工具,更合理的做法是做一个解析 Pipeline:先判断类型,再按文档特点选择工具,最后统一输出为带段落分隔的干净文本。
7. 分块、向量化与写入 Milvus
PDF 解析完成之后,文本还只是字符串,需要经过分块和向量化才能进入 Milvus。
7.1 分块策略
常见分块方式有四类,按效果从低到高排列:
- 固定长度切分:按 token 数量切,实现简单,但容易切断语义。
- 递归字符切分:设置分隔符优先级,先按段落再按句子切,LangChain 的
RecursiveCharacterTextSplitter比较常用。 - 结构感知切分:根据 Markdown 标题层级、HTML 标签切分,适合结构化文档。
- 父子分块:小块用于检索,命中后返回其所属的大块作为大模型上下文,兼顾精确性和语义完整性。
具体配置没有一个通用参数,需要按文档类型试。起步建议:chunk size 取 512,overlap 取 50 到 100。核心原则是不要让一块切完的文本同时包含多个无关主题,也不要让一个完整概念被硬切两半。
7.2 Embedding 模型选择
分块完成后,每个块要通过 embedding 模型变成向量。选择时考虑三点:语言支持、向量维度、是否支持稀疏向量。
如果做中文企业文档,推荐测试 BGE 系列模型,例如 BAAI/bge-m3。它能同时输出稠密向量和稀疏向量,一个模型解决混合检索的两路输入,让 Pipeline 更简单。
加载和编码:
python复制from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-m3")
texts = ["第一段文本", "第二段文本"]
# 稠密向量
dense_embeddings = model.encode(texts, normalize_embeddings=True)
# 稀疏向量,返回形式按模型版本而异
sparse_embeddings = model.encode(texts, return_dense=False, return_sparse=True)
如果不想本地跑模型,也可以用云厂商的 embedding API。区别是本地模型不产生网络费用、数据不出内网,但需要一定的 CPU/GPU 资源和模型管理成本。
7.3 设计 Milvus Collection
写入前先设计 Collection。企业检索场景通常需要至少四个字段:主键、文本内容、稠密向量、稀疏向量。如果还要做时间过滤、部门过滤或文档类型过滤,就再加标量字段。
python复制from pymilvus import MilvusClient, DataType
client = MilvusClient("./rag_kb.db")
schema = client.create_schema(auto_id=True)
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True)
schema.add_field(field_name="text", datatype=DataType.VARCHAR, max_length=4096)
schema.add_field(field_name="dense", datatype=DataType.FLOAT_VECTOR, dim=1024)
schema.add_field(field_name="sparse", datatype=DataType.SPARSE_FLOAT_VECTOR)
index_params = client.prepare_index_params()
index_params.add_index(field_name="dense", index_type="HNSW", metric_type="COSINE")
index_params.add_index(field_name="sparse", index_type="SPARSE_INVERTED_INDEX", metric_type="IP")
client.create_collection(
collection_name="enterprise_docs",
schema=schema,
index_params=index_params,
)
dim 必须和 embedding 模型输出维度一致。如果你换了一个模型,忘记调整 dim,写入时就会报维度错误,这是最常见的问题之一。
7.4 批量写入 Milvus
把解析、分块、向量化、写入串成一个批量任务。写入时按批提交,避免单条循环写入带来的性能损耗:
python复制def batch_insert(texts, batch_size=32):
for i in range(0, len(texts), batch_size):
batch_texts = texts[i:i + batch_size]
dense_emb = model.encode(batch_texts, normalize_embeddings=True)
sparse_emb = model.encode(batch_texts, return_dense=False, return_sparse=True)
rows = []
for idx, txt in enumerate(batch_texts):
sparse_data = sparse_emb[idx]
# 根据实际模型输出格式转换成 {token_id: weight}
# 如果得到的是 scipy 稀疏矩阵,先转成字典
if hasattr(sparse_data, "tocsr"):
sparse_dict = {
int(k): float(v)
for k, v in zip(sparse_data.indices, sparse_data.data)
}
else:
sparse_dict = {int(k): float(v) for k, v in sparse_data.items()}
rows.append({
"text": txt,
"dense": dense_emb[idx].tolist(),
"sparse": sparse_dict,
})
client.insert(collection_name="enterprise_docs", data=rows)
print(f"inserted batch {i // batch_size + 1}")
写入完成后,通过 client.get_collection_stats("enterprise_docs") 查看实体数量,确认数据已经落库。
8. 混合检索:BM25 稀疏向量与稠密向量的结合
单一向量检索在企业场景最大的问题是:对专有名词、编号、精确术语的召回不可控,而混合检索正好补上这一环。
8.1 为什么必须做混合检索
稠密向量擅长语义相近但字面不同的表达,比如用户问“合同到期时间”能召回“合同期限条款”。但碰到“CT-20240325-001”这种精确编号,向量化之后语义表达模糊,召回很容易失败。BM25 这类稀疏检索方案正好相反,对精确 token 匹配非常敏感。
所以企业级 RAG 的标准做法是:一路稠密向量检索负责语义扩展,一路稀疏向量检索负责精确命中,两边结果做融合后统一返回到生成环节。Milvus 在同一个 Collection 里支持两种向量字段,做完两路检索再融合,不需要额外维护两套存储。
8.2 Milvus 双路检索与 RRF 融合
先在库里做两路检索:
python复制query = "合同编号怎么查?"
dense_vec = model.encode([query], normalize_embeddings=True)[0].tolist()
sparse_vec = model.encode([query], return_dense=False, return_sparse=True)[0]
if hasattr(sparse_vec, "tocsr"):
sparse_dict = {int(k): float(v) for k, v in zip(sparse_vec.indices, sparse_vec.data)}
else:
sparse_dict = {int(k): float(v) for k, v in sparse_vec.items()}
dense_res = client.search(
collection_name="enterprise_docs",
data=[dense_vec],
anns_field="dense",
limit=10,
output_fields=["text"],
)
sparse_res = client.search(
collection_name="enterprise_docs",
data=[sparse_dict],
anns_field="sparse",
limit=10,
output_fields=["text"],
)
两路结果的 id 列表不一定相同,直接拼接会造成重复。这里用 RRF(Reciprocal Rank Fusion)融合排序:
python复制def rrf_fuse(ranked_lists, k=60):
score_map = {}
for docs in ranked_lists:
for rank, hit in enumerate(docs):
doc_id = hit["id"]
score_map[doc_id] = score_map.get(doc_id, 0) + 1 / (k + rank + 1)
return sorted(score_map.items(), key=lambda x: x[1], reverse=True)
fused = rrf_fuse([dense_res[0], sparse_res[0]])
for doc_id, score in fused[:10]:
print(doc_id, score)
融合后再根据 id 回显文本,拼装成大模型上下文。如果两路分数的尺度差异很大,建议先做归一化。RRF 的好处是不依赖具体分数,只依赖排序位置,实现简单、稳定性好,是混合检索入门首选。
8.3 混合检索的验收方法
混合检索上线后,不能只看一两个例子。准备一个 20 到 50 条问题的测试集,分三类:语义匹配类、精确编号类、模糊意图类。分别记录两路检索单独召回率和融合后召回率,确认融合结果是提升而不是倒退。这一步是评估闭环的开始,后面调模型和调参都有依据。
9. RAG 框架集成与 API 服务化
检索链路通了之后,下一步是把 Milvus 接进 RAG 编排框架,再封装成可被业务系统调用的 HTTP API。
9.1 LangChain 接入 Milvus
在 LangChain 里,可以用 Milvus 向量存储类把 Collection 变成 retriever。要注意,LangChain 的 retriever 默认走稠密向量检索,适合快速验证链路;精细的混合检索建议在 SDK 层完成后再用 FastAPI 包一层接口。
python复制from langchain_community.vectorstores import Milvus
from langchain_community.embeddings import HuggingFaceEmbeddings
embedding = HuggingFaceEmbeddings(model_name="BAAI/bge-m3")
vector_store = Milvus(
embedding_function=embedding,
collection_name="enterprise_docs",
connection_args={"host": "127.0.0.1", "port": "19530"},
)
retriever = vector_store.as_retriever(search_kwargs={"k": 5})
LangChain 版本更新很快,接口参数会变,上面的写法对应较新版本。如果使用的是 LlamaIndex,则用 MilvusVectorStore 做类似的接入。RAG 框架的作用不是必须,但它能帮你减少 prompt 模板、文档加载、检索器管理这些样板代码的重复工作。
9.2 FastAPI 封装检索接口
用 FastAPI 暴露一个查询接口。这里的关键在于:接口只负责返回检索结果和上下文,大模型生成环节可以再接业务系统自己的大模型服务,避免把检索和生成耦合死。
python复制from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class QueryRequest(BaseModel):
question: str
@app.post("/v1/retrieve")
def retrieve(req: QueryRequest):
docs = retriever.invoke(req.question)
context = "\n\n".join([doc.page_content for doc in docs])
return {
"question": req.question,
"context": context,
"source_count": len(docs),
"docs": [doc.page_content[:200] for doc in docs],
}
启动服务:
bash复制uvicorn main:app --host 0.0.0.0 --port 8000
接口调用示例:
bash复制curl -X POST "http://127.0.0.1:8000/v1/retrieve" \
-H "Content-Type: application/json" \
-d '{"question": "合同编号怎么查?"}'
生产环境注意三点:第一,接口必须加鉴权,不能裸奔在内网或者公网上;第二,要记录查询日志,便于后续评估和排错;第三,加超时和并发控制,防止批量压测打满下游服务。
9.3 批量任务设计
批量任务的通用流程是:输入 PDF 目录,解析文本,分块,向量化,写入 Milvus,逐批打日志。建议加入“断点续跑”机制,即每条解析结果或每个文本块有唯一标识,失败后跳过而不是从头再跑。这样一个几千页 PDF 的建库任务也能在普通开发机上稳定执行,避免一次崩溃全部重来。
10. 资源占用与性能观察
对 RAG 这类系统,资源占用通常集中在三个地方:embedding 推理、Milvus 服务、大模型推理。大模型单独考察,这里重点记录前两项的观察方法。
- CPU:embedding 模型在 CPU 上可以跑,
bge-m3这类模型批量编码时 CPU 占用明显升高。可以用top或任务管理器观察。如果批量建库想提速,可以换 GPU 推理或改用云 embedding API。 - 内存:Milvus 的常驻内存和 Collection 的索引类型、数据量强相关。HNSW 属于内存型索引,数据量大需要更大内存;DiskANN 偏向磁盘型索引,内存压力小但查询延迟可能变高。起步阶段用小数据集观察即可,不要一上来就追求大索引。
- 磁盘:Milvus Standalone 会把数据写入 MinIO 存储,向量库文件、日志和 DDL 文件会持续占用磁盘。批量入库时留意磁盘增长。
- 查询延迟:观察 p95 响应时间。影响最大的参数是
limit、索引类型、标量过滤条件和并发数。检索层一般建议 p95 控制在百毫秒级到几百毫秒级,具体看数据规模和硬件配置。
最直接的观察方式是跑一个脚本循环查询,记录每次耗时,配合 docker stats 看容器资源。如果数据量达到几十万条以上,再考虑调 HNSW 参数、加副本或者上集群。
11. 常见问题与排查方法
企业级 RAG 落地过程中,很多问题现象相同,原因却完全不同。下面列出最高频的几个,按“现象-原因-排查-解决”四列整理。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 连不上 Milvus | Docker 容器未启动、端口映射错误、防火墙拦截 | 执行 docker ps 查看容器状态,用 nc -zv 127.0.0.1 19530 检查端口 |
重启容器,修正 compose 端口映射,确认内网访问策略 |
| PDF 提取结果为空 | PDF 是扫描件或加密文档 | 打印 page.get_text() 长度,查看页面是否有图片 |
对扫描件接 OCR,加密文档先解密或转格式 |
| 向量写入报维度错误 | embedding 模型输出维度与 Collection 的 dim 不一致 |
打印 len(embedding) 对比 Collection 配置 |
重新创建 Collection,统一维度参数 |
| 检索返回空结果 | Collection 为空,或向量 metric 类型不匹配 | 用 get_collection_stats 查看实体数,手动执行一条检索测试 |
确认写入成功,检查索引和 metric 配置 |
| 混合检索结果不稳定 | 稠密和稀疏召回结果差异大,没有归一化 | 分别打印两路结果的分数和 top10 重合度 | 换成 RRF 融合,或为不同场景分配权重 |
| Mac 上 Docker 启动慢或失败 | Docker Desktop 资源不足、镜像拉取慢 | 查看 Docker Desktop 资源设置和镜像源 | 改用 Milvus Lite 本地文件模式 |
| 批量入库卡住 | 单批数据量过大,embedding 接口限流,或磁盘空间不足 | 查看日志、检查 batch size、观察磁盘占用 | 减小批量,加重试和断点续跑 |
| 接口响应变慢 | 检索结果太多、大模型上下文过长、索引参数不合理 | 分别测纯检索耗时和生成耗时 | 调小 top-k,限制上下文长度,优化索引参数 |
遇到问题先定位环节:是解析阶段、入库阶段、检索阶段还是生成阶段。每阶段的日志单独打,排查速度会明显提升。
12. 从 RAG 走向 KG:突破企业知识检索瓶颈
纯 RAG 做多了之后,会碰到几个典型的瓶颈:检索召回不足但不知道问题出在哪一块;多跳问题回答不了,因为单靠向量召回无法串联实体关系;知识更新后旧结果仍然出现,因为向量库没有一致性控制。关于 RAG 瓶颈,业界的共识是:单靠非结构化文档向量化,很难覆盖结构化关系查询和推理型问答。
这时候需要区分三类知识库:
- RAG 知识库:面向非结构化文档,用分块和向量检索实现语义问答,适合“这篇文档里怎么说”这类问题。
- 结构化知识库:面向数据库和表格,用 SQL 或规则查询实现精确计数、状态查询,适合“这个订单是什么状态”这类问题。
- KG 知识库:面向实体关系,用三元组表示实体之间的连接,适合“哪些合同的供应商属于同一集团”这类多跳关系问题。
实际项目不建议一开始就上知识图谱。先用 RAG 解决大部分文档问答,再把高频出现的实体关系梳理成 KG,最后用 Ontology 约束实体类型和关系类型,这就是 Ontology RAG 的基本思路:在检索之前先做一轮实体识别和关系约束,让召回更稳定,减少幻觉。顺序是:先把文档解析和混合检索做好,再考虑图谱;图谱是增量优化,不是必选项。
13. 总结
企业级 RAG 落地不是把几个组件拼起来就结束,而是要打通“PDF 解析 -> 分块 -> 向量化 -> Milvus 存储 -> 混合检索 -> RAG 编排 -> API 服务 -> 评估调优”这条完整链路。这套 20 集系列给出的路线很清晰:先用 Milvus 做存储底座,再用 PDF 解析解决真实数据入口,最后用混合检索补足精确匹配能力。
拿到这套方案后,建议从最小闭环开始验证:找 20 个真实 PDF,跑通解析和入库,准备 20 条测试问题,观察混合检索是否真的比单一向量检索好。第一版不要追求复杂索引和高并发,先把评估集和日志体系建起来。最容易踩的坑是 PDF 解析阶段:脏数据进库后,后面所有优化都会事倍功半。后续如果业务问题多跳逻辑变强,再考虑引入 KG 和 Ontology。建议收藏备用,从一个小数据集开始把流程跑通,再逐步替换成真实业务数据。
