做 RAG 项目最尴尬的阶段,不是大模型回答得不好,而是你的知识库“一问三不知”。很多同学跟着网上的 demo 跑通了一条链路:加载 PDF、切分文本、调用 Embedding 接口、写入向量数据库、去大模型那里做生成。看起来每一步都正常,但真正放到业务场景里,问题就全暴露了:PDF 解析出来全是乱码,长文本被拦腰截断,向量检索召回的结果驴唇不对马嘴,本地测试好好的,部署到服务器就各种起不来。
如果你也处于这个阶段,那么这篇文章就是为你准备的。本文围绕“企业级 RAG 落地”这个主题,重点拆解 PDF 解析、Milvus 向量数据库、混合检索这三条最核心的工程链路。我会先讲清楚每个环节背后的设计逻辑,再给出可落地的操作步骤和代码示例,最后把常见坑和排错思路整理成表格。读完这篇文章,你至少能回答三个问题:PDF 如何解析才能真正可用、Milvus 从本地开发到服务器部署要克服哪些差异、为什么混合检索比单用向量检索更可靠。
判断先行
先给一个明确判断:RAG 落地的难点从来不在大模型侧,而在数据进出的质量。大模型只是“加工车间”,向量数据库是“仓库”,PDF 解析和切块是“原料预处理”。原料不干净,仓库管理混乱,加工能力再强也白搭。所以本文不会把篇幅浪费在大模型 API 的调用技巧上,而是集中火力讲清楚从原始 PDF 到混合检索这“最后一公里的工程质量”。
另外说明一点:本文是一篇结构化的工程总结,对应的是一个 20 集实战课程的核心知识链路。如果你是刚接触 RAG 的初学者,建议先把基础概念过一遍再来看工程细节;如果你已经在做 RAG 项目,可以直接跳到混合检索和部署差异部分。
1. 企业级 RAG 落地的真正难点:为什么 Demo 很惊艳,上线就翻车
很多团队做 RAG 的路径是:先拿几个 PDF 文档,调用 LangChain 内置的 PDF 加载器,切分后用 OpenAI Embedding 存入 FAISS,最后接一个问答 Prompt。Demo 演示效果确实不错,因为测试文档少、问题简单、文本干净。可一旦进入生产阶段,文档量从几页变成几万页,格式从纯文本变成扫描件、表格、图文混排,问题就来了。
我把这些痛点归纳成四类:
第一,PDF 解析的“脏乱差”问题。市面上很多 PDF 加载器只是把文本“抠”出来,完全不关心版面结构。表格被拆得七零八落,页眉页脚混进正文,英文单词被硬生生切断。这种文本直接拿去切块、向量化,召回质量不可能好。
第二,切块的“既要又要”问题。切块太小,语义不完整;切块太大,向量检索时噪声太多。网上教程常用的split_text(text, chunk_size=500)看似省事,实际上根本不考虑文档标题结构、段落边界和语义完整性。真正工程化的切块要回答三个问题:从哪里切、切多大、切完之后保留什么上下文。
第三,向量数据库选型和部署的“环境地狱”。FAISS 适合本地原型验证,但多用户并发、数据持久化、混合检索、权限隔离这些能力它都没有。Chroma 轻量易用,但单机内存模式很难撑起企业级数据量。Milvus 功能全,但本地环境、Docker 部署、服务器集群之间的配置差异,足以让新手折腾一周。
第四,检索效果的“单一向量陷阱”。纯向量检索擅长语义相似,但面对产品型号、合同编号、人名地名这种精确匹配场景,效果往往很差。向量检索召回“意思相近”的段落,却不代表能命中“字面完全一致”的关键信息。这就是为什么企业级 RAG 一定要上混合检索。
这篇文章要解决的,就是上面四类问题的工程化方案。下文会沿着“PDF 解析 → 切块与向量化 → Milvus 部署 → 混合检索 → 效果验证”的顺序展开,每一节都会给出判断、示例和排错思路。
2. 基础概念:RAG 链路、向量数据库和混合检索
2.1 RAG 是什么
RAG(Retrieval-Augmented Generation,检索增强生成)的核心思想是:大模型不直接回答问题,而是先从外部知识库中检索相关内容,然后把“问题 + 检索到的资料”一起交给大模型生成答案。
没有 RAG 时,让大模型回答私域问题,它只能靠训练数据里碰运气的记忆,或者干脆胡说八道。有了 RAG,等于给大模型配了一个“考试时可以翻的参考资料库”,既能降低幻觉,也能让回答实时更新,不用动不动就重新训练模型。
RAG 链路通常分为五个环节:数据解析(PDF/DOC/HTML)→ 文本切块 → 向量化(Embedding)→ 存储与检索(向量数据库)→ 生成(LLM)。本文聚焦的是前四个环节的工程化,最后一步的 Prompt 拼接只做简单说明。
2.2 向量数据库为什么是核心组件
向量数据库是一个以“向量”为核心数据模型的数据库。文本经过 Embedding 模型后,会变成一个几百维甚至上千维的浮点数组,这个数组承载了文本的语义信息。向量数据库解决的核心问题是:给定一个查询向量,如何在海量向量中找到最相似的 Top K 条记录。
这个问题的技术本质是最近邻搜索(ANN)。暴力计算一条条比相似度当然可行,但数据量百万、千万级时性能完全不可接受。Milvus、Qdrant 等产品通过 HNSW、IVF 等索引算法,把检索耗时从秒级压到毫秒级。
2.3 混合检索:向量 + 关键词的互补逻辑
混合检索(Hybrid Search)指的是同时使用向量检索和关键词检索(BM25 或稀疏向量),再把两种结果融合排序。
为什么需要混合检索?看一个具体场景:用户提问“FT-2301 型传感器的工作温度范围是多少”。如果这段话被 Embedding 成向量,系统检索时会找到“语义相近”的传感器介绍段落,但未必精确命中包含“FT-2301”这个型号参数的表格行。而关键词检索可以精确命中“FT-2301”,但它无法理解“温度范围”和“工作温度”这两个表述之间的语义等价关系。
所以企业级 RAG 的标准做法是:稠密向量检索负责语义召回,稀疏向量或 BM25 负责精确匹配,两者通过 RRF(Reciprocal Rank Fusion,倒数排名融合)等算法合并排序结果。这正是标题里“混合检索”的含义。
2.4 各组件之间的关系
一个完整的 RAG 工程,不只有一个向量数据库。它的组成可以类比成一个仓库管理系统:
| 组件 | 类比 | 职责 |
|---|---|---|
| PDF 解析器 | 收货员 | 把原始文档拆成结构化的、干净的内容 |
| 切块器 | 分拣员 | 把文本按语义划分成适合检索的片段 |
| Embedding 模型 | 标签机 | 给每个片段生成语义向量 |
| Milvus | 仓库 | 存储向量和原始文本,提供检索能力 |
| 混合检索 | 智能查询员 | 向量检索 + 关键词检索合并排序 |
| LLM | 组装员 | 基于检索结果生成最终答案 |
3. PDF 解析的工程化处理:从“能读出来”到“高质量输入”
3.1 先识别一个误区
很多 RAG 教程教你的第一步是「加载 PDF」,但 PDF 从来不是一个“加载”就能解决的问题。
PDF 本质上是一种“印刷排版格式”,它的核心目标是把文字、图片、表格固定到页面上,而不是提供语义结构。你要识别哪些文字属于同一句话、同一段落、同一表格,都要靠解析器“逆推”。而且 PDF 还有两种完全不同的类型:
- 文本型 PDF:文字可以直接复制提取,常见于 Word/WPS 导出的文档。
- 扫描型 PDF:本质是图片,文字以像素形式存在,必须走 OCR(光学字符识别)。
这两种文档的处理方案完全不一样。如果你的项目里混着两种类型,第一步要做的是“文档体检”,而不是直接调一个加载器。
3.2 推荐的解析流程
在实际项目中,PDF 解析建议走这样一个流程:
- 格式判断:用 PyMuPDF(fitz)检测 PDF 是否包含文本层。如果文本提取结果为空或极少,判定为扫描件,走 OCR 流程。
- 文本提取:文本型 PDF 用 PyMuPDF 按“块(block)”提取,而不是按字符流硬提。块级提取能保留段落边界。
- 版面结构分析:对复杂文档,用版面分析模型(如 LayoutParser 或 PaddleOCR 的 PP-Structure)识别标题、正文、表格区域,再按结构顺序输出。
- 表格还原:表格不是文本,是结构化数据。推荐把表格行转成“描述性句子”或“结构化记录”,再交给切块器。例如“型号 FT-2301,工作温度 -20℃ 到 85℃”这样一句描述,比直接贴一个 Markdown 表格更容易被向量检索命中。
- 清洗:去掉页眉页脚、页码、超链接标记,统一换行和空格,纠正因提取产生的半角全角混乱。
3.3 文本型 PDF 的代码示例
下面是一个用 PyMuPDF 做基础提取的示例,适用于文本型 PDF。先安装依赖:
pip install pymupdf# 文件路径:pdf_parser.py import fitz # PyMuPDF def extract_text_blocks(pdf_path: str) -> list: """ 按块提取文本,保留段落边界,去除页眉页脚噪声。 返回格式:[(page_no, block_no, text)] """ doc = fitz.open(pdf_path) blocks = [] for page_no in range(len(doc)): page = doc[page_no] page_blocks = page.get_text("blocks") for block in page_blocks: # block 结构: (x0, y0, x1, y1, text, block_no, block_type) x0, y0, x1, y1, text = block[0], block[1], block[2], block[3], block[4] if not text.strip(): continue # 简单过滤页眉页脚:通常位于页面上下 10% 区域,且文本很短 page_height = page.rect.height if y0 < page_height * 0.1 and len(text.strip()) < 20: continue if y1 > page_height * 0.9 and len(text.strip()) < 20: continue blocks.append((page_no + 1, block[5], text.strip())) doc.close() return blocks if __name__ == "__main__": result = extract_text_blocks("sample.pdf") for page_no, block_no, text in result[:10]: print(f"第{page_no}页 块{block_no}: {text[:50]}")这段代码的关键点在于:按块提取而不是按行提取;过滤页眉页脚;保留页码和块号,方便后续回溯定位原文。
3.4 扫描型 PDF 的处理思路
如果是扫描件,文本型解析就拿不到内容了。工程上通常用 OCR 方案,比如 PaddleOCR 或 Tesseract。OCR 的痛点在于耗时和高并发资源占用,建议在文档入库阶段做离线批处理,而不是让用户上传后实时等待。
注意:扫描件 OCR 后的排版顺序是“识别框坐标排序”的结果,不是原始阅读顺序。你需要先按坐标聚合成行,再按从上到下、从左到右排序。这一步处理不当,OCR 结果会彻底打乱文档逻辑。
4. 切块策略与向量化:影响召回质量的隐形变量
4.1 为什么切块不能一刀切
切块是 RAG 里“看起来简单、做起来最糟心”的环节。切块策略决定了向量检索的“最小语义单元”。
- 切块太小(如 100 字):语义不完整,一个问题可能被拆到两个块里,检索时两边都不完全匹配。
- 切块太大(如 2000 字):向量包含噪声太多,相似度计算被无关内容稀释,召回精度下降。
- 无脑固定切块:会切断列表、表格和段落,造成大量“废块”。
更推荐的做法是结构感知切块(Structure-aware Chunking):先识别文档的标题层级和段落边界,再在预设的块大小范围内,按段落完整切分。
4.2 切块策略对比
| 策略 | 实现难度 | 适用场景 | 缺点 |
|---|---|---|---|
| 固定大小切块 | 低 | 口语化问答、社交媒体文本 | 语义碎片化严重 |
| 按段落切块 | 中 | 技术文档、规章制度 | 段落过长时仍需二次分块 |
| 标题感知切块 | 中高 | 产品手册、白皮书 | 依赖版面解析质量 |
| 语义切块 | 高 | 对话记录、逻辑跳跃的文本 | 需要额外调模型,成本高 |
| 父子分块 | 中高 | 需要“上下文完整 + 检索精准”的场景 | 逻辑复杂,存储量增大 |
对于企业知识库,我最推荐的组合是“标题感知 + 父子分块”:父块携带完整上下文,子块拆成适合检索的精片段,检索时命中的是子块,但回传大模型的是父块。这样既保证召回精准,又保证生成时有足够上下文。
4.3 向量化:Embedding 模型怎么选
切块完成后,每个块要转成向量。选择 Embedding 模型时,注意三点:
- 领域匹配度:中文技术文档场景,建议先测开源中文 Embedding 模型,或直接使用 OpenAI text-embedding-3-small、智源 BGE 系列等。以实际效果为准,不要只看榜单。
- 向量维度与一致性:写入 Milvus 前必须确认切块向量维度是一致的。如果中途换 Embedding 模型,维度不同会直接导致 Collection 创建失败。
- Embedding 模型服务化:不要每次入库都重新加载模型,建议用独立的 Embedding 服务,供入库和查询共用。
5. 向量数据库选型:Milvus、Chroma、Qdrant 怎么选
5.1 一个被反复问的问题
很多同学一上来就问“Milvus 和 FAISS 哪个好”,实际上这是拿“搜索引擎”和“内存计算库”对比,维度就错了。FAISS 是 Facebook 开源的向量检索库,不是一个“数据库”。FAISS 需要你自己管理数据生命周期、持久化和并发访问。Chroma 是一个轻量级向量数据库,安装简单、适合单机小规模场景。Qdrant 是一个 Rust 写的向量数据库,性能好、API 优雅。Milvus 则是一个分布式向量数据库,也是 LF AI & Data 基金会项目,目标就是企业级场景。
5.2 选型对比表
| 维度 | FAISS | Chroma | Qdrant | Milvus |
|---|---|---|---|---|
| 定位 | 向量检索库 | 轻量向量数据库 | 向量数据库 | 分布式向量数据库 |
| 部署难度 | 低 | 极低 | 中 | 中高 |
| 数据持久化 | 无内置 | 有 | 有 | 有 |
| 分布式能力 | 无 | 无 | 独立集群模式 | 强,支持多副本 |
| 混合检索 | 需自行实现 | 有限支持 | 支持 | 支持(稠密 + 稀疏) |
| 企业级功能 | 无 | 较少 | 有 | 全面(权限、监控、备份) |
| 适合场景 | 原型验证、算法实验 | 个人项目、小数据量 | 中等规模、需要快速上线 | 企业级数据量、生产环境 |
结论很清晰:如果只是本地跑 demo,Chroma 或 FAISS 都可以;如果目标是把 RAG 做到企业内部多用户可用的程度,直接选 Milvus。原因不是它的功能列表更长,而是因为你不需要在项目推进到第二阶段时再换数据库。
6. Milvus 从本地到服务器:部署模式与工程差异
6.1 本地开发环境怎么装 Milvus
Milvus 官方推荐用 Docker Compose 方式部署,但需要特别说明:Milvus 依赖 etcd(元数据存储)和 MinIO(对象存储),三个服务组成一套体系。这也是很多新手安装时困惑的地方——明明启动了一个 Milvus 容器,为什么过一会儿就报错。
下面是一份 Milvus 单机版 Docker Compose 配置示例(请基于官方当前版本调整镜像 tag):
# 文件路径:docker-compose.yml version: "3.5" services: etcd: container_name: milvus-etcd image: quay.io/coreos/etcd:v3.5.14 environment: - ETCD_AUTO_COMPACTION_MODE=revision - ETCD_AUTO_COMPACTION_RETENTION=1000 - ETCD_QUOTA_BACKEND_BYTES=4294967296 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd minio: container_name: milvus-minio image: minio/minio:RELEASE.2023-03-20T20-16-18Z environment: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data command: minio server /minio_data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s timeout: 20s retries: 3 standalone: container_name: milvus-standalone image: milvusdb/milvus:v2.4.x command: ["milvus", "run", "standalone"] environment: ETCD_ENDPOINTS: etcd:2379 MINIO_ADDRESS: minio:9000 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus ports: - "19530:19530" - "9091:9091" depends_on: - etcd - minio启动命令:
docker compose up -d docker ps # 查看三个容器是否都处于 healthy 状态注意:Windows 本地装 Milvus,建议用 Docker Desktop + WSL2 后端。Milvus 原生并不直接提供 Windows 运行版本,直接开着 Docker Desktop 跑 Compose 是最稳妥的路径。
6.2 从本地到服务器的关键差异
本地跑通 Milvus 之后,很多人会想“照搬到服务器上行不行”。理论上可以,但工程上要处理几个差异:
一是资源规划。本地 Docker 容器挂了就重启,服务器上不能这么随意。Milvus 的数据存在 MinIO,元数据存在 etcd,这两个组件的磁盘卷必须持久化到宿主机目录或独立存储卷。容器删了没关系,数据不能丢。
二是网络与防火墙。Milvus 默认 gRPC 端口是 19530,Web 监控端口是 9091。部署到服务器后,运维层面应只对应用服务器开放 19530 的访问权限,9091 端口不要暴露到公网。更好一点的做法是把 Milvus 放在内网,应用服务器通过内网 CIDR 访问。
三是鉴权与安全。如果 Milvus 部署在不受控网络环境中,一定要启用用户名密码认证。Milvus 支持 RBAC(基于角色的访问控制)。企业环境里建议为每个业务线创建独立用户和 Collection,不要把 root 用户交给所有应用共用。
四是资源限制。服务器上跑 Milvus standalone,必须给 Docker 容器设置内存和 CPU 限制。如果不限制,Milvus 做索引构建时可能把宿主机内存吃满,影响同机其他应用。
下面是一个带资源的 Compose 配置片段:
standalone: image: milvusdb/milvus:v2.4.x command: ["milvus", "run", "standalone"] deploy: resources: limits: cpus: "8" memory: 16G reservations: cpus: "4" memory: 8G6.3 连接 Milvus 的 Python 示例
本地服务起来后,用pymilvus连接并创建 Collection:
pip install pymilvus# 文件路径:milvus_helper.py from pymilvus import ( connections, CollectionSchema, FieldSchema, DataType, Collection, utility, ) # 1. 连接 Milvus connections.connect(alias="default", host="localhost", port="19530") # 2. 定义字段 fields = [ FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True), FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=768), FieldSchema(name="chunk_text", dtype=DataType.VARCHAR, max_length=2000), FieldSchema(name="source_pdf", dtype=DataType.VARCHAR, max_length=500), FieldSchema(name="page_no", dtype=DataType.INT64), ] schema = CollectionSchema(fields, description="RAG knowledge base collection") # 3. 创建 Collection collection_name = "enterprise_rag" if utility.has_collection(collection_name): collection = Collection(collection_name) else: collection = Collection(collection_name, schema, consistency_level="Bounded") print(f"Collection created: {collection.name}")这段代码里值得注意的细节:FLOAT_VECTOR 的维度 768 必须和你的 Embedding 模型一致;chunk_text 字段保存原始文本,方便生成阶段使用;source_pdf 和 page_no 用于来源回溯。
7. 混合检索再进一步:从 Milvus 到混合检索工程实现
7.1 为什么企业级 RAG 离不开混合检索
单一向量检索的局限,在真实业务数据里表现得很明显。企业知识库中充满产品型号、工单编号、合同条款、人名地名、财务科目。这些词不适合用语义相似度去匹配。用户问“HT-200 报价是多少”,系统如果只做向量检索,可能召回一堆讲“设备价格”“产品介绍”的段落,却不一定包含“HT-200”这个精确串。
反过来,纯关键词检索能精确命中“HT-200”,但用户说“这个设备的采购成本大概多少”,它就很难关联到“报价”相关段落。所以企业级做法是两条腿走路:向量召回 + 关键词召回,再用 RRF 融合。
7.2 RRF 算法说明
RRF 的核心思想很简单:每个召回结果都有排名,融合时按名次倒数的加权和排序。
公式形如:score(d) = 1 / (k + rank_vector(d)) + 1 / (k + rank_bm25(d))
其中 k 是常量,通常取 60。这个公式不关心具体相似度分数是 0.7 还是 0.85,只看名次,好处是两种检索的分数尺度不一致也不影响融合结果。
7.3 Python 实现示例
下面是一个把向量检索和 BM25 检索融合的简化示例。假设你已经有一个 BM25 索引(例如用 Elasticsearch 或 rank_bm25 库构建):
# 文件路径:hybrid_search.py from pymilvus import Collection, connections K = 60 # RRF 常量 def rrf_fusion(vector_results: list, keyword_results: list, top_k: int = 10): """ vector_results: [(id, score), ...] keyword_results: [(id, score), ...] 返回按 RRF 融合后排序的 id 列表。 """ score_map = {} for rank, (doc_id, _) in enumerate(vector_results): score_map[doc_id] = score_map.get(doc_id, 0) + 1.0 / (K + rank + 1) for rank, (doc_id, _) in enumerate(keyword_results): score_map[doc_id] = score_map.get(doc_id, 0) + 1.0 / (K + rank + 1) ranked = sorted(score_map.items(), key=lambda item: item[1], reverse=True) return [doc_id for doc_id, _ in ranked[:top_k]] # 向量检索示例 connections.connect(alias="default", host="localhost", port="19530") collection = Collection("enterprise_rag") collection.load() query_text = "HT-200 传感器的报价是多少" query_embedding = embed_text(query_text) # 传入你的 Embedding 函数 vector_results = collection.search( data=[query_embedding], anns_field="vector", param={"metric_type": "IP", "params": {"nprobe": 10}}, limit=20, output_fields=["chunk_text", "source_pdf", "page_no"], ) # 伪代码:keyword_results 由 BM25 索引查询得到 # keyword_results = bm25_index.search(query_text, top_k=20) # 融合 final_ids = rrf_fusion(vector_results, keyword_results, top_k=10)在实际项目中,还有一个更“Milvus 原生”的做法:Milvus 2.4 之后支持稀疏向量(Sparse Float Vector),可以把 BM25 类似的关键词权重转成稀疏向量,再通过同一套 Milvus API 完成稠密向量与稀疏向量的混合检索。这样就不需要额外维护一套 Elasticsearch 索引,数据同步成本更低。
8. 效果验证:没有评测指标的 RAG 全是感觉
8.1 为什么必须做评测
很多 RAG 项目开发到一半就陷入绝望,不是因为模型不行,而是不知道“现在的效果到底行不行”。今天调了一下 Prompt 觉得好了点,明天加了个 PDF 又觉得差了。没有评测数据集和指标,一切优化都是盲人摸象。
8.2 企业级 RAG 评测的三层指标
第一层:召回评测。针对“检索”环节。构建一个问答测试集,每条包含 query、预期召回文档 ID、预期答案。跑一边检索,看 Top 5 或 Top 10 中是否包含预期文档。常用指标是命中率(Recall@K)。
第二层:生成评测。针对“检索 + 生成”的整体效果。用最终回答与标准答案做比较。指标可以简单到“人工打分 1~5 分”,也可以借助大模型做自动评估,比如评估忠实度(是否基于检索内容回答)和答案相关度。
第三层:端到端评测。用一套固定的测试问题集,每周或每次改动数据后回归一遍,对比前后效果差异。这是企业级 RAG 的“CI/CD 测试”。
8.3 一个简化评测脚本
# 文件路径:evaluate_recall.py def evaluate_recall(search_func, test_set, top_k=5): hit_count = 0 for item in test_set: query = item["query"] expected_doc_ids = set(item["expected_doc_ids"]) retrieved_ids = search_func(query, top_k=top_k) if expected_doc_ids.intersection(retrieved_ids): hit_count += 1 recall = hit_count / len(test_set) print(f"Recall@{top_k} = {recall:.2%}") return recall把这个脚本沉淀成项目里的固定工具,后续每次调整切块策略、换 Embedding 模型、切换数据库,都跑一遍,用数字说话,而不是拍脑袋。
9. 常见问题与排查思路
我在实际项目里遇到过的问题,很多都有共性。下表列出高频问题及解决思路:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| Milvus 容器启动后反复退出 | etcd 未就绪、MinIO 地址配置错误 | docker logs milvus-standalone查看错误 | 检查 deponds_on 和健康检查,确认 etcd 与 MinIO 可访问 |
| 写入数据时提示维度不一致 | Embedding 模型换了维度 | 打印向量维度和 Collection schema 的 dim | 删除旧 Collection 重建,或改用动态字段 |
| 向量检索结果为空 | Collection 未load() | 检查是否执行过collection.load() | 检索前先保证 Collection 加载到内存 |
| 中文检索效果差 | 分词不友好 | 检查切块文本是否乱码,Embedding 模型是否适合中文 | 换中文 Embedding 模型,检查 PDF 解析编码 |
| PDF 文本全是乱码 | 文件本身为扫描件 | 尝试手动复制 PDF 文字看是否可选中 | 对扫描件走 OCR 流程 |
| 本地部署正常,服务器起不来 | 服务器没有 Docker 或内存不足 | 查看docker compose logs,检查free -h | 先保证服务器内存不低于 8G,再启动服务 |
| 用户查询有时能搜索到精确串有时不行 | 只用了向量检索 | 打印召回结果,确认是否出现精确串 | 增加 BM25 / 稀疏向量混合检索 |
10. 最佳实践与工程建议
到这里,整个链路已经跑通了。最后总结几条生产中最重要的建议:
第一,把“解析干净”当作基础设施。花时间做好 PDF 类型识别、版面结构分析、表格转文本描述,收益会传导到后面所有环节。与其在 Prompt 上反复调试图弥补数据质量,不如在入口处解决数据质量问题。
第二,设计好 Collection 的元数据。向量数据库里不该只存 vector 和 text。来源文件、页码、章节路径、部门、上传时间、版本号都要作为元数据存进去。这样既能做权限过滤(比如只检索某个部门的知识),也能在回答时给出精确引用出处。
第三,切块参数不要指望“一次调对”。先用评测集跑一版基线,然后调 chunk_size、overlap、是否启用父块,观察 Recall@K 的变化。每次只改一个变量。
第四,混合检索是标配,不是加分项。哪怕初期数据量小,也要把稀疏向量或 BM25 检索通道预留出来。后面数据量上来,你会感谢当时没有只做单一向量检索。
第五,安全边界与最小权限。服务部署到服务器后,容器不要用 root 运行,Milvus 不要裸奔到公网,知识库数据要按用户/部门做隔离。RAG 做的是知识开放,权限管控必须严格,否则等于把公司资料直接暴露给访问者。
第六,关注检索上游的“上下文完整性”。热门方向如父子分块、上下文压缩、Rerank 重排,都是围绕“检索到的内容是否够用、够准”来优化。建议学完基础链路后,按这个顺序逐步深入。
如果你准备继续进阶,下一步的方向可以是:Agentic RAG(让大模型自主决定何时检索、检索什么)、Rerank 模型(对召回结果二次排序)、RAG 自动评测平台(持续回归测试集),以及把整套链路容器化上 K8s。
先把本地到服务器的 Milvus 部署搞定,把 PDF 解析和混合检索这两条最难啃的链路走通,你再回头看 RAG,就会发现它不再是一个“脆弱的大模型拼装玩具”,而是一个可以承载企业知识服务的系统工程。