前阵子一个做企业内部知识库的朋友跟我吐槽:他们用最新的大模型 API 做了一个 RAG 系统,上线一周就翻车了。用户问“去年第四季度的销售返利政策”,系统答非所问;问“X 项目验收需要准备哪些材料”,系统只召回了一份完全不相关的会议纪要。最离谱的是,用户问“怎么报销打车费”,它居然回答“请参考 2021 年员工手册”。这其实不是个例。很多团队做 RAG 时,把“文档切片 + 向量检索 + 大模型生成”这三件套拼起来就以为完事了,结果一到真实业务场景就露馅。问题的根源不是大模型不够聪明,而是检索链路没做好。
这篇文章要解决的就是这个问题。我会完整拆解一个企业级 RAG 知识库系统的全链路:文档解析、切片策略、向量化、混合检索、重排、生成,以及工程化落地时的评估与避坑。核心判断先放在这里:RAG 系统的效果上限,取决于检索质量,而不是生成模型。如果你的知识库“一问就蠢”,90% 的精力应该花在检索链路上,而不是反复换模型。
读完这篇文章,你会清楚为什么“向量检索 + 关键词检索 + 重排”是企业级 RAG 的标配,也能照着代码跑通一个完整的 RAG pipeline,知道每一步的调优方向是什么。
1. 这篇文章真正要解决的问题
先说实话:现在网上关于 RAG 的资料已经很多了,但大多数停留在“跑通 demo”层面。你跟着教程做完一个 LangChain + Chroma 的小项目,能回答几个问题,但把同样的方案放到企业环境里,马上会遇到三类问题。
第一类是召回质量差。企业知识库里的文档类型极其复杂:PDF、Word、Excel、PPT、扫描件、网页、数据库记录。很多文档版面长、公式多、表格嵌套、页眉页脚混入正文。解析不干净,切出来的 chunk 本身就是脏数据,后面无论怎么检索都是白费。
第二类是检索方式单一。很多项目只用了向量检索。向量检索擅长语义匹配,但对精确词、型号、编号、人名这类硬匹配很不友好。比如用户搜“iPhone 15 电池寿命”,向量检索可能召回一堆讲“手机续航优化”的泛泛内容,而真正包含“iPhone 15”技术规格的文档却没有排到前面。
第三类是缺少工程化意识。没有评估集、没有指标、没有缓存、没有权限控制、没有监控。系统跑起来了,但没人知道它到底好不好,出了问题也不知道是解析、切片、检索还是生成环节的锅。
这篇文章不是简单给一个“能跑的 demo”,而是带你走一遍完整的 RAG 架构设计、代码实现和调优方法。你会得到一个可复制的 baseline,并且知道如何针对业务数据把它逐步优化到可上线状态。
适合读这篇文章的读者主要有三类:正在做企业内部知识库的后端开发工程师,负责大模型应用落地的算法工程师,以及准备从零搭建 RAG 系统的技术团队负责人。
2. RAG 的核心原理与常见误区
2.1 什么是 RAG
RAG(Retrieval-Augmented Generation,检索增强生成)是一种把“外部知识检索”和“大模型生成”结合起来的架构。
用一个类比帮你理解:你是一家咨询公司的合伙人,团队里新来了一位名校毕业的分析师。这位分析师专业能力很强,但刚从学校出来,对公司内部的项目历史、客户资料、报销制度一无所知。你不会让他凭记忆瞎编,而是给他配一个资料员。每次遇到问题,资料员先去档案室把相关材料找出来,分析师再根据材料整理答案。
在这个类比里:
- 分析师 = 大模型
- 资料员 = 检索模块
- 档案室 = 向量数据库 + 关键词索引
- 找出来的材料 = 召回的文档片段
- 整理答案 = 大模型基于上下文生成回答
RAG 的技术流程可以拆成五个步骤:
- 文档解析:把 PDF、Word、HTML 等原始文档提取为纯文本。
- 切片(Chunking):把长文档切分成适合检索的片段。
- 向量化(Embedding):把文本片段转换为向量,存入向量数据库。
- 检索(Retrieval):用户提问时,把问题转为向量,检索最相似的片段;同时配合关键词检索做混合召回。
- 重排与生成(Reranking & Generation):用重排模型对召回的候选片段精排,再注入 Prompt,交给大模型生成答案。
2.2 为什么用 RAG 而不是微调
很多人会问:既然要让大模型懂业务,为什么不直接微调?这里有一个很清晰的分工。
| 方案 | 知识更新成本 | 适合场景 | 主要问题 |
|---|---|---|---|
| RAG | 低,更新索引即可 | 知识频繁变化、私域文档多、需要引用来源 | 检索质量决定上限 |
| 微调 | 高,每次要重训 | 调整输出格式、风格、领域术语 | 不适合存储大量新知识,容易过拟合 |
| 纯长上下文 | 很低,直接拼接 | 单次问答涉及的文档少、窗口够用 | 成本高、长文本利用率低 |
大多数企业知识库场景,知识的更新频率远高于模型的迭代频率。你不可能每次更新一份制度文档就重训一次模型。RAG 天然适合这种“知识动态变化”的场景。
2.3 四个高频误区
误区一:RAG 就是“向量检索 + 大模型”。其实关键词检索(BM25)在企业知识库中常常比向量检索更重要,因为企业文档里的型号、编号、人名都是精确匹配,向量检索很难处理这类硬匹配。
误区二:切片越小越准。切片太小,语义信息被切碎,检索到了也看不懂;切片太大,噪音太多,大模型容易被无关内容带偏。
误区三:Embedding 模型随便选。英文 Embedding 模型处理中文效果通常较差。中文场景推荐使用中英双语模型,比如 BGE 系列。
误区四:问题答不对就是 Prompt 写得不好。大多数时候,答案质量差的根源是召回结果不对。上下文里根本没有正确答案,Prompt 写出花来也没用。
3. 整体架构设计与技术选型
3.1 系统分层
一个可上线的 RAG 系统,我建议按以下六层来设计:
数据接入层:从内网盘、Wiki、数据库、SaaS 系统同步文档。
处理层:文档解析、去重、清洗、格式转换、切片、打标签。
存储层:向量数据库(用于语义检索)+ 倒排索引(用于关键词检索)+ 元数据存储(用于权限过滤)。
检索层:多路召回,向量检索与关键词检索并行执行,再用 RRF(Reciprocal Rank Fusion)或加权融合合并结果。
精排层:用 Cross-Encoder 重排模型对候选片段打分排序。
生成层:把重排后的 Top-K 片段拼接进 Prompt,调用大模型生成答案,同时输出引用来源。
3.2 技术选型参考
没有一套技术栈适合所有团队,下面给出的是我在企业项目中比较常用的选型参考:
| 组件 | 可选方案 | 建议 |
|---|---|---|
| 编排框架 | LangChain、LlamaIndex、LangChain4j、Dify、RagFlow | Python 团队用 LangChain;Java 团队用 LangChain4j;想要可视化操作可用 Dify |
| 向量数据库 | Milvus、Elasticsearch、Chroma、pgvector | 企业级优先 Milvus 或 Elasticsearch,支持分布式和海量数据 |
| Embedding 模型 | BGE-M3、bge-large-zh、text-embedding-3、通义 | 中文业务场景优先 BGE 系列,支持私有化部署 |
| 重排模型 | BGE-Reranker-v2-m3、Cohere Rerank | 开源选 BGE-Reranker;追求简单可选 Cohere |
| 大模型 | Qwen、DeepSeek、ChatGLM、GPT 系列 | 私有化部署选 Qwen/DeepSeek;云端 API 选 GPT 或国内大模型 |
| 混合检索 | Elasticsearch BM25 + Milvus 向量检索 | 也可以直接用 Milvus 2.4+ 的 Hybrid Search 能力 |
为什么要用 Milvus?它不是最简单易上手的向量数据库,但它是少数能支撑千万级向量、支持标量过滤、具备生产级可用性的开源方案。企业知识库一旦涉及权限隔离(不同部门只能搜到自己的文档)、数据量增长、高并发检索,简单的单机 Chroma 会很难撑住。
为什么推荐 BGE 系列?BGE-M3 支持中文、英文多语言,同时具备稠密向量、稀疏向量、多向量三种检索能力,对中文混合场景非常友好。BGE-Reranker 是开源重排模型里效果稳定的选择,可以本地部署,不用担心数据出境。
3.3 为什么“混合检索 + 重排”是标配
只看一个场景:员工问“打印机型号 HP LaserJet Pro M405dn 的驱动怎么装”。
- 纯向量检索:可能会召回“办公设备使用指南”“打印机常见问题”这类语义相近但不包含具体型号的文档。
- 纯关键词检索:能精确匹配“HP LaserJet Pro M405dn”,却可能漏掉说“惠普 M405 驱动安装”这种写法不完全一致的文档。
- 混合检索 + 重排:向量负责语义泛化,BM25 负责精确匹配,两者结果融合后,再用重排模型对候选集精打分,把最相关的文档排到最前面。
这一步直接决定了 RAG 系统的“上限”。重排模型(Cross-Encoder)会把 query 和每个候选文档拼接后做深度交互,比单纯的向量相似度(Bi-Encoder)判断更精确,但计算量更大。所以工程上常用“先粗召回,再精排”的两阶段设计。
4. 环境准备与基础搭建
4.1 Python 环境与依赖
本文代码以 Python 3.10 为基础,使用 LangChain 生态演示完整流程。建议用 conda 创建独立环境:
conda create -n rag python=3.10 -y conda activate rag安装核心依赖。版本请以实际安装环境为准,本文重点演示通用思路:
pip install "langchain>=0.2" \ langchain-community \ langchain-huggingface \ langchain-milvus \ "pymilvus>=2.4" \ "sentence-transformers" \ "pypdf" \ "python-docx" \ "rank-bm25" \ "jieba" \ "openai"说明几个关键包的作用:
langchain-huggingface:提供 HuggingFaceEmbeddings 封装,用来加载 Embedding 模型。langchain-milvus:LangChain 对 Milvus 的集成封装。sentence-transformers:底层模型加载和推理工具。rank-bm25:用于在 demo 里实现 BM25 关键词检索。jieba:中文分词,BM25 中文检索前需要分词。
4.2 启动 Milvus 向量数据库
本地开发环境建议用 Docker Compose 启动 Milvus Standalone。Milvus 依赖 etcd 和 MinIO,官方提供了标准部署文件。这里给出一个精简的 docker-compose 配置:
# 文件路径:docker-compose.yml services: etcd: image: quay.io/coreos/etcd:v3.5.5 environment: - ETCD_AUTO_COMPACTION_MODE=revision - ETCD_AUTO_COMPACTION_RETENTION=1000 - ETCD_QUOTA_BACKEND_BYTES=4294967296 volumes: - etcd_data:/etcd minio: image: minio/minio:RELEASE.2023-03-20T20-16-18Z environment: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin volumes: - minio_data:/minio_data command: minio server /minio_data standalone: image: milvusdb/milvus:v2.4.1 command: ["milvus", "run", "standalone"] ports: - "19530:19530" - "9091:9091" environment: ETCD_ENDPOINTS: etcd:2379 MINIO_ADDRESS: minio:9000 volumes: - milvus_data:/var/lib/milvus volumes: etcd_data: minio_data: milvus_data:启动命令:
docker-compose up -d验证 Milvus 是否启动成功:
docker ps | grep milvus如果看到milvusdb/milvus容器状态为 Up,同时端口 19530 在监听,说明 Milvus 已经就绪。191 端口说明:19530 是 gRPC 通信端口,9091 是 HTTP 健康检查端口。
4.3 准备 Embedding 模型和重排模型
本文使用 BGE-M3 作为 Embedding 模型,BGE-Reranker-v2-m3 作为重排模型。模型可以从 Hugging Face 或 ModelScope 下载,下载后保存到本地目录,通过本地路径加载。这样可以在内网环境部署,避免每次启动都要联网拉取模型。
加载 Embedding 模型的示例:
# 文件路径:model_loader.py from langchain_huggingface import HuggingFaceEmbeddings embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-m3", model_kwargs={"device": "cuda"}, # GPU 环境设为 cuda,CPU 环境设为 cpu encode_kwargs={"normalize_embeddings": True}, )这里有一个关键细节:normalize_embeddings=True表示对向量做 L2 归一化。Milvus 默认使用余弦相似度,归一化后内积等价于余弦相似度,检索效果更稳定。
BGE 系列的模型使用官方建议的查询指令(query instruction)会提升效果。BGE-M3 一般不需要额外指令,但 BGE 系列其他模型(比如 bge-large-zh-v1.5)通常建议在查询前加上“为这个句子生成表示以用于检索相关文章:”。具体是否加指令,以模型卡描述为准。
5. 核心代码实现:完整 RAG Pipeline
接下来我们实现一个完整的企业级 RAG pipeline。为便于演示,假设有一个./docs目录,里面放了若干 PDF 格式的企业制度文档。
5.1 文档加载与解析
# 文件路径:document_loader.py from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader # 加载 ./docs 目录下所有 PDF 文件 loader = DirectoryLoader( "./docs", glob="**/*.pdf", loader_cls=PyPDFLoader, ) documents = loader.load() print(f"加载文档数量: {len(documents)}")这里需要注意:PyPDFLoader只能处理文本型 PDF。如果你的企业文档里有大量扫描件,需要先用 OCR 工具(比如 PaddleOCR)转换为文本,再进入后面的链路。否则即使检索到这一页,内容也全是乱码或空字符串。
另外,如果是 Word 文档,可以使用UnstructuredWordDocumentLoader或Docx2txtLoader;如果是网页,使用WebBaseLoader。不同 loader 的解析质量差别很大,这一步值得花时间做验证。
5.2 切片策略
# 文件路径:text_splitter.py from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""], ) chunks = text_splitter.split_documents(documents) print(f"切片数量: {len(chunks)}")chunk_size=500是经验值。如果文档以制度条款为主,500 到 800 都是合理范围;但如果是技术手册,一个操作步骤很短,500 可能太长,混入了无关上下文。
chunk_overlap=50的作用是让相邻片段之间保留部分重叠,避免一个完整语义单元被拦腰切断。
separators的优先级从前往后,会优先按照“段落”“换行”“句号”“分号”等自然边界切分。这里把中文标点加入分隔符,对中文文档效果更好。
如果文档有清晰的标题结构,更好的做法是先把标题和正文关联起来,再切片。比如给每个 chunk 附加一个"title"metadata,在切片的开头带上“所属章节”的前缀,检索时匹配率会显著提升。
5.3 向量化并写入 Milvus
# 文件路径:vector_store.py from langchain_milvus import Milvus vector_store = Milvus.from_documents( documents=chunks, embedding=embeddings, connection_args={"host": "localhost", "port": "19530"}, collection_name="enterprise_rag_demo", drop_old=True, # 如果集合已存在,先删除。生产环境慎用! )说明几个参数:
collection_name:集合名,相当于数据库中的表名。drop_old=True:本地演示时方便重复创建。生产环境千万不要这样设置,否则一次误操作会清空整个索引。connection_args:Milvus 的连接参数。
写入完成后,可以在 Milvus 控制台或通过 pymilvus 查询集合统计信息,确认数据是否写入成功:
from pymilvus import utility, connections connections.connect(host="localhost", port="19530") print(utility.has_collection("enterprise_rag_demo"))5.4 混合检索:向量召回 + 关键词召回 + RRF 融合
这一步是本文的关键。混合检索的工程实现通常有两种做法:
- 简化 demo:用
rank-bm25在内存里做关键词检索,用 Milvus 向量检索,然后用 RRF 融合。 - 生产方案:用 Elasticsearch 做 BM25,Milvus 做向量检索,在应用层融合。
下面先给出可以在本地跑通的 RRF 融合示例:
# 文件路径:hybrid_retriever.py from langchain_milvus import Milvus import jieba from rank_bm25 import BM25Okapi # 重新连接已有的 Milvus 集合 vector_store = Milvus( embedding_function=embeddings, connection_args={"host": "localhost", "port": "19530"}, collection_name="enterprise_rag_demo", ) # 从 Milvus 中取回所有 doc 用于构建内存 BM25 索引(demo 演示) # 生产环境建议用 Elasticsearch 等独立引擎维护 BM25 索引 def build_chunk_corpus(): # 实际项目可以从 Milvus 按分页拉取所有文档文本 all_chunks = [] all_texts = [] # 演示:假设我们从某个持久化文件或数据库中读取了所有切片文本 with open("./chunks.txt", "r", encoding="utf-8") as f: all_texts = [line.strip() for line in f.readlines()] for text in all_texts: all_chunks.append({"page_content": text}) return all_chunks def bm25_retrieve(query: str, corpus, top_k: int = 10): tokenized_corpus = [list(jieba.cut(doc["page_content"])) for doc in corpus] bm25 = BM25Okapi(tokenized_corpus) tokenized_query = list(jieba.cut(query)) scores = bm25.get_scores(tokenized_query) top_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:top_k] return [corpus[i] for i in top_indices] def vector_retrieve(query: str, top_k: int = 10): hits = vector_store.similarity_search_with_score(query, k=top_k) return [{"page_content": doc.page_content, "score": score} for doc, score in hits] def reciprocal_rank_fusion(result_lists, k=60): fused_scores = {} for result_list in result_lists: for rank, doc in enumerate(result_list): doc_id = doc["page_content"] if doc_id not in fused_scores: fused_scores[doc_id] = 0.0 fused_scores[doc_id] += 1.0 / (k + rank) ranked = sorted(fused_scores.items(), key=lambda x: x[1], reverse=True) return [{"page_content": doc_id, "score": score} for doc_id, score in ranked] def hybrid_retrieve(query: str, top_k: int = 10): corpus = build_chunk_corpus() vector_results = vector_retrieve(query, top_k=top_k) keyword_results = bm25_retrieve(query, corpus, top_k=top_k) fused_results = reciprocal_rank_fusion([vector_results, keyword_results], k=60) return fused_results[:top_k]RRF 融合公式简单但非常有效。它不看具体分数,只看每个文档在每路召回中的排名。一个文档在向量检索中排第 3,在关键词检索中排第 5,那么它的融合分就是1/(60+3) + 1/(60+5)。这样既避免了不同检索方式分数大小不可比的问题,又能让两路召回的“共识”文档浮上来。
5.5 重排:用 Cross-Encoder 精排
召回阶段拿到的是“候选集合”,可能仍然包含不相关的内容。现在用 BGE-Reranker 精排:
# 文件路径:reranker.py from sentence_transformers import CrossEncoder reranker = CrossEncoder("BAAI/bge-reranker-v2-m3", device="cuda") def rerank(query: str, candidates, top_k: int = 5): pairs = [(query, doc["page_content"]) for doc in candidates] scores = reranker.predict(pairs) scored = sorted( zip(candidates, scores), key=lambda x: x[1], reverse=True, ) return [doc for doc, score in scored[:top_k]]为什么用 Cross-Encoder 而不是直接按 Embedding 相似度排序?因为 Embedding 模型(Bi-Encoder)把 query 和文档分别编码成向量再算相似度,query 和文档之间没有深度交互;而 Cross-Encoder 把 query 和文档拼接后一起过 transformer,能捕捉更细粒度的匹配信号。代价是速度慢,所以只对粗召回后的 Top 20 到 50 个候选做精排,不会成为瓶颈。
5.6 组装 Prompt 并生成答案
这里以 OpenAI 兼容接口为例,实际项目无论是私有化部署的 Qwen、DeepSeek,还是云厂商的大模型 API,基本都支持 OpenAI 协议。
# 文件路径:generator.py from openai import OpenAI client = OpenAI( base_url="http://localhost:11434/v1", # Ollama 或其他兼容服务地址 api_key="EMPTY", # 本地服务一般不需要鉴权,云端 API 替换为真实 Key ) def generate_answer(query: str, docs): context = "\n\n".join( f"[文档片段 {i+1}] {doc['page_content']}" for i, doc in enumerate(docs) ) prompt = f"""你是一个企业内部知识库助手。 请严格基于以下参考资料回答用户问题。 如果参考资料中没有答案,请直接回答“知识库中没有相关信息”,不要编造。 如果参考资料内容冲突,请明确指出冲突点。 参考资料: {context} 用户问题:{query} 回答:""" response = client.chat.completions.create( model="qwen2.5:14b", # 实际模型名以部署环境为准 messages=[{"role": "user", "content": prompt}], temperature=0.1, max_tokens=512, ) return response.choices[0].message.contentPrompt 里做了三件重要的事:
- 限定回答范围:明确告诉模型“只基于参考资料回答”,降低幻觉概率。
- 给出无答案时的处理方式:避免模型强行编造。
- 要求指出内容冲突:企业知识库里经常出现新旧制度并行的情况,这能暴露数据问题。
5.7 串起完整链路
# 文件路径:rag_pipeline.py from document_loader import documents from text_splitter import chunks from vector_store import vector_store from hybrid_retriever import hybrid_retrieve from reranker import rerank from generator import generate_answer def ask(query: str): print(f"用户问题: {query}\n") # 1. 混合召回 candidates = hybrid_retrieve(query, top_k=20) print(f"混合召回候选数: {len(candidates)}") # 2. 重排 reranked_docs = rerank(query, candidates, top_k=5) print("重排后 Top-5 片段:") for i, doc in enumerate(reranked_docs): print(f" {i+1}. {doc['page_content'][:80]}...") # 3. 生成 answer = generate_answer(query, reranked_docs) print(f"\n最终回答:\n{answer}") if __name__ == "__main__": ask("公司年假政策是什么?")到这里,你已经拥有一个可以本地跑通的完整 RAG 系统。下面来看如何验证效果。
6. 运行结果与效果验证
运行上面的 pipeline:
python rag_pipeline.py预期会看到类似输出:
用户问题: 公司年假政策是什么? 混合召回候选数: 20 重排后 Top-5 片段: 1. 第三条 员工累计工作已满1年不满10年的,年休假5天... 2. 员工考勤管理制度(2024修订版) 年假申请流程... 3. 人力资源手册 第二章 假期管理... 4. 员工福利说明 补充医疗保险报销... 5. 2024年度团建安排通知... 最终回答: 根据《员工考勤管理制度(2024修订版)》,员工累计工作已满1年不满10年的,年休假为5天;已满10年不满20年的,年休假为10天;已满20年的,年休假为15天。具体申请流程需提前3个工作日在 OA 系统提交...如何判断系统是否成功?提供三个检查点:
- 检索结果是否相关:重排后的 Top 5 是否真的和问题相关。这一步在日志里就能判断。
- 回答是否有依据:最终答案中的关键信息能否在 Top 5 片段里找到对应原文。如果答案内容在检索片段里找不到,说明生成模型在“自由发挥”,这个问题比答错更严重。
- 引用定位是否准确:把回答中引用的片段编号和实际片段对应一下,看是否一致。
一个很实用的排查技巧:把generate_answer里实际传给大模型的 Prompt 原样打出来。如果 Prompt 里没有正确答案,无论大模型多强都不可能答对。通过这一步,就能迅速把“检索问题”和“生成问题”区分开。
7. 全链路优化:从检索到重排的调优方向
跑通之后,你大概率会发现效果不理想。这是正常的,RAG 系统的优化本身就是“找短板”的过程。
7.1 先定位问题属于哪一环
回答效果不好时,要先确认问题出现在哪个环节。
| 现象 | 可能的问题环节 | 排查方式 |
|---|---|---|
| 检索结果完全没提到关键信息 | 文档解析、切片、召回 | 检查文档加载日志,人工翻看切片文本 |
| 检索结果里有相关词,但整体不相关 | 切片粒度、Embedding 模型、召回融合策略 | 调整 chunk_size,尝试换 Embedding 模型 |
| 检索结果相关,但回答不对 | 生成链路 | 检查 Prompt 实际内容,看是否信息溢出 |
| 所有问题回答都很慢 | 索引类型、算力、并行设计 | 监控向量检索和 LLM 推理耗时 |
7.2 关键指标怎么理解
很多读者会问“RAG 知识库指标有哪些”。这里推荐从四个维度建立评估体系:
- 召回率(Recall@K):前 K 个结果里有没有包含“标准答案片段”。它衡量系统“找得到”的能力。
- 命中率(Hit Rate):问题是否命中了至少一个正确答案片段,是召回率的一个简化版本。
- MRR(Mean Reciprocal Rank):第一个正确答案排在第几位。它衡量系统“排得准”的能力。
- 忠实度(Faithfulness):生成的答案是否严格基于检索到的内容,而不是模型凭空发挥。
建议先做一个小规模评估集,找业务方标注 50 到 100 条“问题 → 标准答案片段”的对应关系。每次改动切片策略、Embedding 模型、重排模型,都跑一遍评估,用分数判断是变好还是变坏。这个步骤虽然费时间,但它是企业级 RAG 工程化的分水岭。
7.3 常用优化手段
优化切片策略:制度类文档可以用“章节标题 + 正文”的方式,只把正文切片,但保留标题作为 metadata。检索时优先匹配标题,匹配失败再匹配正文。
使用父文档召回(Parent Document Retriever):检索到小切片后,返回它所属的更大段落。这种方式兼顾了“检索准确”和“上下文完整”。
增加元数据过滤:给每个切片打上部门、文档类型、生效日期、密级等标签。检索时先通过权限和业务过滤条件缩小范围,再做向量和关键词匹配。
引入查询改写(Query Rewrite):用户问题往往口语化、指代不清。比如“它的报销流程是什么”,如果不做改写,检索效果会很差。可以在检索前用大模型把问题改写为多个更具体的子问题,或者补全指代词。
设置重排阈值:重排分数低于某个阈值的候选片段直接丢弃,而不是强行塞进 Top-K。这能避免模型因为上下文中有不相关内容而被带偏。
8. 常见问题与排查思路
下面是 RAG 知识库系统实战中最常见的问题排查表,建议直接收藏备用。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 检索结果为空或极少 | 文档解析失败,collection 中没有数据 | 查看文档加载日志,确认切片数量 | 修复文档解析器,验证切片文本非空 |
| PDF 加载后乱码 | PDF 是扫描件,无文本层 | 用 PDF 阅读器打开确认 | 接入 OCR 流水线(如 PaddleOCR) |
| 中文问题效果明显差 | 用的是英文 Embedding 模型 | 抽样检索结果,看相似文档是否英文 | 更换为 BGE-M3 等中英双语模型 |
| 检索到相关词但不相关 | 切片过大或过小 | 人工检查切片文本,调整 chunk_size | 调整 chunk_size 和 overlap,加入标题前缀 |
| 答案里有知识库中不存在的内容 | 检索召回不相关,或 Prompt 约束不足 | 打印实际 Prompt,检查 Top-K 片段 | 优化检索链路,强化 Prompt 约束,增加重排阈值 |
| 某类文档总是检索不到 | 文档结构特殊(表格、多栏、公式) | 分别测试不同页面的解析效果 | 对特殊文档类型定制解析和切片逻辑 |
| 检索耗时越来越长 | 数据量增长,索引参数未调整 | 监控 Milvus 查询耗时 | 使用 HNSW 索引,调大 nprobe,或横向扩容 |
| 同一问题短时间内重复请求 | 没有缓存机制 | 查看接口调用日志 | 增加 query 级缓存和相似语义缓存 |
| 更新文档后老答案依旧返回 | 知识库索引未更新或缓存过期 | 检查 collection 数据版本 | 使用版本号管理集合,及时刷新索引 |
9. 企业级工程化落地建议
9.1 先建评估集,再动手优化
很多团队一上来就调切片大小、换模型,折腾两周后发现效果时好时坏。正确做法是:先让业务方整理 50 到 100 条真实高频问题,每条问题打上“应该命中哪些文档片段”的标注。然后用这个评估集跑 baseline,记录 Recall@5、MRR、忠实度等指标。每次改动都重新跑评估,用数字说话。没有评估集,所有优化都是“凭感觉”,很容易陷入调参陷阱。
9.2 数据权限与安全边界
企业知识库最大的坑是权限泄露。如果所有员工共享一个向量集合,那么销售岗就能检索到财务岗的薪酬数据,这是绝对不行的。
建议方案:
- 集合级隔离:不同业务线使用不同 collection,用代码保证请求只会路由到有权限的集合。
- 元数据过滤:在 collection 中增加
department、security_level等标量字段,检索时通过 Milvus 的过滤表达式强制拼接权限条件。 - 接口鉴权:知识库 API 必须走统一鉴权网关,不建议直接把向量库端口暴露给内部应用。
连接 Milvus 时,过滤表达式可以这样用:
from langchain_milvus import Milvus vector_store = Milvus( embedding_function=embeddings, connection_args={"host": "localhost", "port": "19530"}, collection_name="enterprise_rag_demo", ) # 只检索 department == "HR" 且 security_level <= 2 的文档 results = vector_store.similarity_search( query, k=10, expr='department == "HR" && security_level <= 2', )9.3 缓存与异步设计
企业知识库的流量特征通常是“少量高频问题占据大部分请求”。做两层缓存很有必要:
- 完全相同的 query:直接用 Redis 缓存答案,命中直接返回。
- 语义相似的 query:对 query 做 Embedding,与历史 query 计算相似度,超过阈值就复用缓存答案。
文档解析和向量化是重计算操作,建议放到异步任务队列中执行,避免用户上传文档后接口长时间阻塞。
9.4 版本管理与回滚
知识库索引的更新不应该“原地覆盖”。建议:
- 每次重建索引时,使用新的 collection 名称,例如
enterprise_rag_v2。 - 索引构建完成后,先在灰度环境验证效果,再切换线上流量。
- 一旦出现问题,把流量切回旧 collection,完成回滚。
Embedding 模型升级时要特别注意:一旦换了 Embedding 模型,所有已有向量必须重新生成,新旧向量不能混用。这件事要提前和业务方确认,否则会出现“为什么文档比以前更搜不到了”的投诉。
9.5 成本控制
企业知识库的成本大头通常在 Embedding API 和大模型 API 上。
控制成本的方法:
- 文档入库前做去重和重要性筛选,不要把海量历史邮件全部灌进知识库。
- 长文档采用“前段摘要 + 后段分片”的方式,摘要向量进入索引,完整分片在重排后按需取用。
- 简单问题走规则匹配或关键词检索,不调用大模型;复杂问题才走完整 RAG 链路。
10. 总结与后续学习方向
RAG 系统的本质不是一个“能聊天的工具”,而是一个“信息检索系统 + 内容生成系统”的组合。它的效果上限由数据质量、检索质量、生成质量三者共同决定,而其中最容易出问题的、也最值得花时间优化的,是检索链路。
这篇文章帮你把一个大而全的 RAG 知识库项目拆解成了六个环节:文档解析、切片、向量化、混合检索、重排、生成。只要你能清晰地定位每个环节的输入输出,就能快速判断问题出在哪里,也就能针对性地做优化。
建议你先不要追求大而全。找 10 到 20 篇真实业务文档,用本文的代码搭出最小闭环,把每一步的日志都打出来。当你亲手看过一次“检索结果和正确答案完全无关”的失败案例,并把它调通之后,你对 RAG 的理解会超过 90% 只跑过 demo 的人。
后续可以继续深入研究的方向:GraphRAG(把知识图谱和 RAG 结合,适合多跳问答)、Agentic RAG(让大模型自主决定何时检索、检索什么、是否需要二次检索)、RAGAS 评估框架(自动化评估生成答案的忠实度和相关性),以及如何微调属于自己的 Embedding 和 Rerank 模型。
知识库的项目没有“一次做完”的终点,它是一个持续迭代的数据工程。先把检索链路做好,再谈模型调优。