RAG 全链路实战:Embedding 与向量检索原理及本地部署

📅 发布时间:2026/8/31 14:07:10
RAG 全链路实战:Embedding 与向量检索原理及本地部署 RAG 在这两年的热度不需要我再强调。真正的问题是很多开发者学 RAG 的时间花在了“调接口”上却忽略了它背后两条真正的技术线Embedding 嵌入模型怎么把文本变成向量向量检索怎么把问题变成候选片段。这两块没想明白项目一旦换成新领域文档效果马上打回原形。本文打算用一条完整链路把 RAG 讲透从文档加载、文本切块、Embedding 向量化、向量入库到查询向量化、近邻检索、Prompt 组装、大模型生成。这不是一篇纯概念科普而是一条可以直接跟代码的落地路径。读完你能回答三个问题RAG 到底在解决什么问题Embedding 模型是怎么工作的向量检索和传统搜索差在哪里文章会使用 Ollama BGE-M3 FAISS 给出一个最小可运行的本地 RAG 示例并补充生产环境的工程建议帮你把入门到落地之间的坑提前踩平。1. RAG 的完整业务流程从文档加载到生成回答RAG 全称是 Retrieval-Augmented Generation翻译过来就是“检索增强生成”。它的核心思路并不复杂大模型不知道的知识我们先把相关资料检索出来再把资料作为上下文塞给模型让模型基于资料生成回答。换句话说RAG 不是把文档整个“喂”给模型而是把文档变成“可以被检索的上下文”需要哪段就取哪段。很多人第一次接触 RAG 时容易把注意力全放在大模型 Prompt 上这其实是个误区。RAG 和普通 Prompt 工程最大的区别在于前面多了一套“外部知识索引与检索”系统。如果没有检索模型只能依靠自己的参数记忆回答有了检索模型才能引用你指定的文档内容降低事实性错误。说得更直白一点RAG 的价值不在生成而在“知道去哪找答案”。一个标准的 RAG 项目业务链路通常由两个阶段组成离线索引阶段和在线查询阶段。离线索引阶段要做的是文档加载、清洗、切块、Embedding 向量化、写入向量数据库在线查询阶段要做的是把用户问题向量化、在向量库中检索相似片段、拼接 Prompt、调用大模型生成回答。下图用文字描述就是文档加载读取 PDF、Word、Markdown、TXT、数据库记录等各类业务数据。文本切块把长文档拆成适合检索和模型输入的片段。Embedding 嵌入通过嵌入模型把每个文本片段转换为向量。向量存储把向量写入向量数据库或本地索引文件。查询处理将用户问题用同一个 Embedding 模型转换为查询向量。向量检索在向量库中查找与查询向量最相似的 Top-K 个文档片段。可选重排用 Rerank 模型对召回结果做精细化排序。生成回答把问题、召回片段、系统指令组装成 Prompt交给大模型生成最终答案。如果只看表面很容易误以为 RAG 就是把文档存起来再查一下。真正决定效果的是切块粒度、Embedding 模型的语义表征能力以及检索策略能否命中正确片段。任何一环出错后面的生成环节都无能为力。一个高效的 RAG 系统一定是从索引阶段就严格设计而不是等模型回答错了再来调 Prompt。这一段的小结论RAG 的完整业务流程本质上是“文档向量化 语义检索 生成”三层架构。入门时不要孤立学习某个组件一定要带着全链路视角去理解每一环的输入输出。1.1 为什么需要 RAG它解决了大模型什么问题大模型的核心能力是“概率化地补全下一个 Token”它看到的问题越多越容易生成“听起来合理”的内容。但是垂直领域知识、企业内部文档、最新资料这些信息大模型在训练时不可能全部覆盖。没有外部资料时模型只能靠记忆拼凑结果就是一本正经地胡说八道也就是大家常说的“幻觉”。解决这个问题主要有三条路线长上下文、微调和 RAG。长上下文方案最容易理解把全部资料塞进上下文窗口让模型自己找答案但成本高、有窗口上限而且信息太长时模型注意力会分散。微调方案把知识固化进模型权重适合改变模型表达风格和特定能力但每次知识更新都要重新训练成本很高。RAG 方案则把知识放在模型外部用到哪段取哪段知识更新只改索引不需要重新训练模型。三者各有适用场景但对企业知识库类项目来说RAG 是投入产出比最高的默认选择。方案知识更新成本幻觉风险实现复杂度典型场景长上下文低直接拼进 Prompt高内容太多容易遗漏低单文档问答微调高需要重新训练中高模型能力改造、风格注入RAG低更新向量库即可低中企业知识库、私域数据问答这里需要说明的是RAG 并非万能。它解决的是“知识缺失”和“事实来源可追溯”的问题但不会提升模型本身的推理能力。如果业务要求复杂逻辑推理仍然需要选择合适的大模型甚至配合 Agent 的工作流来拆解任务。2. Embedding 嵌入模型运行机制与适用场景Embedding 是 RAG 里最容易被低估的一层。很多初学者以为 Embedding 就是把文本编码一下实际上Embedding 模型的核心任务是“把语言语义映射成高维空间中的向量”让语义相近的句子在空间中的距离也更近。这个“向量空间”才是 RAG 检索能力的根基。2.1 Embedding 模型到底在做什么从技术实现看现在主流的 Embedding 模型通常基于 Transformer 架构训练。输入一段文本后模型会先做 Token 化把文本拆成模型词表里的 Token然后经过多层 Transformer 编码得到每个 Token 的隐层表示最后通过池化策略比如 CLS 向量或平均池化把整段文本压缩成一个固定维度的向量。这个向量就是文本的 Embedding 表示比如 768 维、1024 维等具体维度以模型配置为准。需要强调一点Embedding 不是“把字编码成数字”而是“把语义编码成坐标”。比如“苹果手机”和“iPhone”在字面上完全不同但优秀的中文 Embedding 模型会把它们的向量放在很近的位置再比如“苹果公司”和“水果”在字面上都包含“苹果”向量距离反而会拉开。这正是传统关键词搜索做不到的语义匹配能力。在 RAG 项目里Embedding 模型有一个硬性要求文档片段和查询问题必须使用同一个模型。如果索引时用了模型 A查询时却换了模型 B两个向量不在同一个语义空间检索效果会非常差。很多线上事故的根源就是团队升级 Embedding 模型之后没有重建索引导致检索结果全部错乱。2.2 常见 Embedding 模型与选型思路目前开源社区和企业服务里能用的 Embedding 模型非常多。比如 BGE 系列中的 BGE-M3这是一个支持多语言、支持长文本的嵌入模型在中文场景下表现稳定并且可以通过多种方式加载既能本地部署也能在部分云平台上直接调用。像阿里云等平台也提供商业化 Embedding 服务适合不想自己维护模型部署的团队。选择 Embedding 模型时有几个关键指标向量维度、支持的最大输入长度、语言覆盖范围、检索效果和推理延迟。向量维度越高理论上表达能力越强但存储和检索成本也越高最大输入长度决定了一个文本片段能编码多长长度不足就依赖合理切块推理延迟则直接影响索引构建速度和在线查询响应时间。从实际项目看最稳妥的做法不是只比较榜单分数而是在自己的业务文档上做小规模验证。构造一批业务问题用不同 Embedding 模型生成向量再用向量检索看召回结果的准确率。榜单分数高不代表你的领域一定好用业务文档里的大量专有名词往往才是决定效果的关键。2.3 什么场景适合 Embedding什么场景不适合Embedding 最适合的场景是语义召回比如“用户问了一句口语化的问题要去知识库里找对应文档片段”。这种场景下用户不会拿原文关键词去搜而是用自然语言表达意图语义匹配的价值非常明显。但 Embedding 也有明显短板。第一它对精确匹配不敏感比如查订单号、身份证号、设备编码向量检索经常不如精确索引稳定第二它不擅长处理数字范围比如“找价格低于 100 的商品”单纯靠语义向量很难表达清楚“小于”这种约束第三它无法理解文档中的逻辑关系比如“排除 A 类设备后的统计结果”。这些问题不能靠提高向量检索阈值解决而是需要混合检索和元数据过滤配合处理。所以真正生产级的 RAG 系统很少只依赖 Embedding 一条路。更常见的做法是“向量 关键词 元数据过滤”组合使用先在多个路上召回候选再把结果融合排序用 Rerank 模型做最终精排。这部分我们放到向量检索章节详细展开。3. 向量检索工作原理相似度、ANN 与向量数据库文档被 Embedding 模型转换成向量后我们面对的问题就变成了给定一个查询向量怎么从千万级向量集合里快速找到最相似的向量这就是向量检索要解决的问题。3.1 向量相似度怎么计算最直观的相似度计算方式是余弦相似度。它计算两个向量之间的夹角余弦值值越接近 1表示方向越一致语义越相近。公式可以理解为两个向量点积除以各自模长的乘积。除了余弦相似度还有内积和欧氏距离两种常用度量方式。内积适用于向量的模长本身携带信息的情况比如某些推荐场景欧氏距离表示向量空间里的实际距离值越小越相似。在 FAISS、Milvus 等向量检索工具里通常会做一个细节处理把向量归一化成单位向量后再计算内积。这样一来内积结果就等价于余弦相似度既简化了计算也让索引的查询速度更快。如果你自己写向量检索代码一定要记住“归一化”这一步否则即使模型选对了结果也可能不理想。3.2 精确检索与近似最近邻 ANN最朴素的向量检索方式是暴力扫描也叫精确最近邻搜索把向量库里每个向量都跟查询向量计算一遍相似度。这种方式在小规模数据上没有问题几千几万条向量都能秒回。但数据量到了百万级、千万级每次查询都扫描全量延迟和 CPU 开销都会失控。所以生产环境普遍使用 ANN也就是近似最近邻搜索。它牺牲少量精度换取巨大的性能提升常见的算法包括 HNSW、IVF、PQ 等。HNSW 是目前应用最广的一种它的核心思想是构建多层图结构先在高楼层快速跳转再在低楼层精确定位查询速度快且召回率高但内存占用相对较高。IVF 则是先对向量做聚类查询时只搜索距离最近的几个簇索引构建快内存占用低但在数据分布不均匀时召回率会下降。算法查询速度召回率内存占用适合场景暴力扫描慢100%低小样本、验证环境HNSW快高高召回要求高的生产场景IVF快中高中超大规模、内存有限的场景3.3 向量数据库在 RAG 中的角色向量数据库的核心职责就是管理向量数据和向量索引。除了基础的相似度检索它还负责数据的增删改查、元数据过滤、持久化、权限控制这些工程能力。常见方案有 Milvus、Qdrant、Weaviate、Chroma 等另外 PostgreSQL 的 pgvector 扩展、Elasticsearch 的 kNN 检索能力也经常被用在已有数据库体系的团队里。在 RAG 项目里向量数据库的选择要结合团队技术栈和数据量。如果只是个人学习或原型验证FAISS 这种本地索引库就够用如果要做企业级系统数据量在百万级以上还需要并发查询、多租户隔离、权限管理那就需要独立的向量数据库。需要注意不要把向量数据库当成“存数据的仓库”它在 RAG 里的角色是“召回引擎”真正决定答案质量的还是上游的切块策略和下游的重排。3.4 混合检索向量检索 BM25 多路召回纯向量检索虽然语义匹配能力强但容易漏掉精确关键词的命中。比如用户查询“退款流程”文档里反复出现“退货退款流程”向量检索可能也能命中但如果是“订单号 abc123”这种强标识信息向量检索的表现就不如传统 BM25 关键词检索。混合检索的思路是同时跑两条路一条路用 Embedding 做语义召回另一条路用 BM25 做稀疏关键词召回然后把多路结果融合最终交给大模型使用。多路召回结果融合时常用一种简单有效的方法叫 RRF也就是倒数排名融合。它不依赖分数归一化直接根据每条结果在不同路中的排序位置计算融合分数对分数尺度差异不敏感。更进一步的方案是使用 Rerank 模型对融合后的候选集做精排。Rerank 模型通常比 Embedding 模型更重但精度更高它会同时输入查询和候选文档片段输出相关性分数把最相关的内容排到最前面。4. RAG 项目环境准备与依赖安装理论部分讲完之后我们从这一节开始动手。为了让示例足够简单、可复现我会选择本地部署大模型的方案用 Ollama 部署一个大语言模型用 BGE-M3 作为 Embedding 模型用 FAISS 作为向量检索索引整个系统不需要申请任何一个云端大模型 API。版本信息请以实际安装时的最新稳定版为准这里重点演示通用思路。4.1 基础环境要求建议使用 Python 3.9 及以上版本操作系统不限Windows、macOS、Linux 都可以。如果你的显卡显存足够大推荐用 GPU 跑 Embedding 模型和大模型如果只是学习验证CPU 慢一点也能跑通。我在示例中尽量控制模型规模实际使用中如果计算资源紧张可以把大语言模型换成参数更小的版本。先创建一个独立 Python 环境避免依赖冲突mkdir rag-demo cd rag-demo python -m venv .venv # Windows .venv\Scripts\activate # macOS / Linux source .venv/bin/activate然后安装依赖pip install --upgrade pip pip install sentence-transformers faiss-cpu requestssentence-transformers负责加载 BGE-M3 模型并生成向量faiss-cpu提供向量索引和检索能力requests用于调用 Ollama 的本地 HTTP 接口。如果你的机器支持 GPU可以把faiss-cpu换成faiss-gpu但安装前要确认 CUDA 版本。4.2 部署本地大模型以 Ollama 为例Ollama 是目前最方便的本地大模型管理工具之一它把下载模型、启动服务、调用接口这三个环节做了封装。安装 Ollama 后使用ollama pull拉取模型即可。中文问答场景下Qwen 系列是口碑较好的选择ollama pull qwen2.5:7b ollama serveollama serve启动后本地会默认监听 11434 端口。验证服务是否正常可以用下面这个请求测试curl http://localhost:11434/api/generate \ -H Content-Type: application/json \ -d {model: qwen2.5:7b, prompt: 你好请简单介绍一下自己, stream: false}如果返回 JSON 中包含response字段说明 Ollama 已经可用。如果你的机器显存较小可以把模型换成qwen2.5:3b效果会弱一些但跑通流程没有问题。4.3 准备 Embedding 模型BGE-M3 是一个多语言文本嵌入模型在中文检索任务里表现比较稳定。你可以直接用 Hugging Face 的模型名加载model SentenceTransformer(BAAI/bge-m3)首次运行会自动下载模型权重。如果下载速度很慢推荐先通过 ModelScope 或国内镜像下载好模型文件再指定本地路径加载。模型下载后的实际加载路径以你保存的位置为准。5. 核心流程拆解文档切块、索引构建与检索召回环境准备好之后我们开始拆解 RAG 项目的核心流程。整个过程可以分成两个阶段索引构建阶段和查询检索阶段。每个阶段都有非常容易踩坑的地方我会逐个说明。5.1 文本切块为什么切块策略如此重要大模型和 Embedding 模型都有输入长度限制整篇文档直接向量化通常不可行所以需要把文档切分成较小的片段。切块策略直接决定了检索的粒度切得太粗每个片段包含无关信息向量会被稀释检索精度下降切得太细语义不完整模型缺乏上下文生成的答案也会很碎。常见的做法是按照文档结构切块比如 Markdown 标题、段落、代码块、表格。如果一段太长再按字符或 Token 数做二次切分。切分时可以设置一个 overlap也就是相邻块之间保留一部分重叠内容避免句子被从中间截断导致语义缺失。具体数值要根据文档形态测试一般以几百 Token 为基准再根据效果调整。5.2 索引构建从文本到向量再到 FAISS索引构建其实只有四步清洗文档去掉无关字符和噪声切分文本得到若干 chunk用 Embedding 模型把每个 chunk 变成向量把向量和原始文本一起写入 FAISS 索引。需要注意的是Embedding 模型输出的向量必须做归一化才能让 FAISS 的内积检索等价于余弦相似度检索。清洗和切分这一步很多人会觉得没必要花时间。实际上RAG 项目上线后效果差很大一部分问题就出在文档质量上。比如 PDF 解析出来全是换行Markdown 表格被拆得稀碎HTML 标签混在正文里。这些噪声进到向量里检索结果自然不相关。5.3 查询检索从用户问题到候选上下文在线查询阶段用户输入问题后系统先用同一个 Embedding 模型把问题转成查询向量然后在 FAISS 索引里执行search返回相似度最高的 Top-K 个片段。这里的 K 值需要根据上下文窗口和业务情况调整。太小可能漏掉正确答案太大可能把噪声带进 Prompt增加模型理解负担。查询阶段最容易犯的错误是忘记用同一个 Embedding 模型。另外查询向量也要做归一化并且要确认 FAISS 索引的向量维度与查询向量维度一致。如果索引维度是 1024查询向量却是 768程序会直接报错。5.4 构建 Prompt如何把检索结果交给大模型检索完成之后需要把用户问题、召回片段和系统指令组装成 Prompt。最基础的结构是先给系统设定角色和任务例如“你是企业知识库助手请严格根据上下文回答”然后把检索到的文本片段按相关性顺序拼接进去最后是用户问题。组装 Prompt 时要记得把“没有找到相关内容”这个场景也考虑进去。如果检索结果和问题完全不相关最稳妥的做法是让模型直接回答“未找到相关资料”而不是强行从无关片段里编答案。这一条能显著降低幻觉问题。6. 完整示例Ollama BGE-M3 FAISS 实现本地 RAG这一节我们用一个完整可运行的 Python 脚本把前面讨论的流程串起来。脚本会读取本地文本文件构建 FAISS 索引并调用 Ollama 部署的大模型回答用户问题。6.1 文档读取与文本切块首先实现文本读取和递归切块逻辑。为了保持示例简洁我们按段落切分超过长度限制再按字符切分同时保留部分重叠。# rag_demo.py import re import requests import numpy as np from sentence_transformers import SentenceTransformer import faiss def load_text(file_path: str) - str: 读取本地文本文件 with open(file_path, r, encodingutf-8) as f: return f.read() def split_text(text: str, chunk_size: int 500, overlap: int 80) - list[str]: 按段落和长度切分文本保留重叠区域 paragraphs re.split(r\n\s*\n, text) chunks [] current for para in paragraphs: para para.strip() if not para: continue if len(current) len(para) chunk_size: current current \n para if current else para else: if current: chunks.append(current) # 如果单个段落超过 chunk_size按字符切分 if len(para) chunk_size: for i in range(0, len(para), chunk_size - overlap): chunks.append(para[i:i chunk_size]) current else: current para if current: chunks.append(current) return chunks切块逻辑中的chunk_size和overlap可以按文档具体情况调整。示例里按字符长度切分实际项目中更推荐按 Token 数切分因为大模型的上下文限制通常以 Token 计。6.2 构建向量索引接着实现构建索引和保存索引的功能。我们使用faiss.IndexFlatIP作为基础索引先归一化再写入让内积等价于余弦相似度。def build_index(chunks: list[str], model: SentenceTransformer): 用 Embedding 模型生成向量写入 FAISS 索引 vectors model.encode(chunks, normalize_embeddingsTrue) dim vectors.shape[1] index faiss.IndexFlatIP(dim) index.add(vectors.astype(np.float32)) return index def save_index(index: faiss.Index, chunks: list[str], index_path: str, chunks_path: str): 保存 FAISS 索引和原始文本片段 faiss.write_index(index, index_path) with open(chunks_path, w, encodingutf-8) as f: for chunk in chunks: f.write(chunk.replace(\n, ) \n)这里把原始文本片段保存成普通文本文件是为了查询时能按行取回对应内容。更规范的做法是把 chunk 的元数据一并保存比如来源文档、页码、章节标题方便生成回答时给出引用信息。6.3 查询检索与大模型生成最后实现查询和生成逻辑。查询时对问题做同样的归一化然后调用 FAISS 的search方法取 Top-K 个片段。得到候选片段后拼进 Prompt再请求 Ollama 的生成接口。def retrieve(query: str, model: SentenceTransformer, index: faiss.Index, chunks: list[str], top_k: int 3): 检索与查询向量最相似的文本片段 query_vec model.encode([query], normalize_embeddingsTrue).astype(np.float32) scores, indices index.search(query_vec, top_k) results [] for score, idx in zip(scores[0], indices[0]): if idx 0: continue results.append({score: float(score), chunk: chunks[idx]}) return results def call_ollama(prompt: str, model: str qwen2.5:7b) - str: 调用本地 Ollama 服务生成回答 resp requests.post( http://localhost:11434/api/generate, json{model: model, prompt: prompt, stream: False}, timeout120, ) resp.raise_for_status() return resp.json().get(response, ) def build_prompt(query: str, results: list[dict]) - str: 把检索结果组装成 Prompt context \n\n.join([item[chunk] for item in results]) prompt f你是企业知识库问答助手。请严格依据以下资料回答用户问题。 资料 {context} 用户问题{query} 如果资料中没有相关信息请直接说明“未找到相关资料”不要编造。 return prompt6.4 跑通整个流程把以上模块组合起来在主函数中完成从读文档到问答的完整流程if __name__ __main__: embed_model SentenceTransformer(BAAI/bge-m3) doc load_text(data/sample.txt) chunks split_text(doc) index build_index(chunks, embed_model) print(f文档已切分为 {len(chunks)} 个片段) print(f向量维度: {index.d}) while True: query input(\n请输入问题输入 exit 退出: ).strip() if query.lower() exit: break results retrieve(query, embed_model, index, chunks, top_k3) if not results: print(未检索到相关资料) continue prompt build_prompt(query, results) answer call_ollama(prompt) print(f\n回答{answer})建议先准备一个测试文档比如企业客服 FAQ 或产品说明放到data/sample.txt里。运行脚本后程序会读取文档、构建索引、进入问答循环。这里直接加载 BGE-M3 模型首次运行需要下载权重耐心等待即可。7. 运行验证与效果判断脚本运行成功之后重点不是“能跑通”而是“效果是否符合预期”。RAG 项目的验证比普通接口复杂因为它没有固定的“通过/不通过”标准只能通过多个维度的检查来判断。第一步确认索引信息。程序会打印文档切分数量和向量维度。如果切分数量异常少说明文档可能没有被正确读取或者切块太大如果向量维度发生变化说明加载的 Embedding 模型和之前不一致。第二步构造有明确答案的测试问题。比如文档里写了“退货条件是签收后 7 天内”那就问“退货条件是什么”。如果检索结果包含正确片段生成回答也会比较准确。如果检索结果不包含正确片段问题大概率出在切块策略或 Embedding 模型选择上而不是大模型 Prompt。第三步测试问题变形和同义改写。把“退货条件”改成“什么情况下可以退”看能否命中语义相近的片段。这一步能直观感受 Embedding 模型带来的语义匹配能力也能发现哪些场景下需要加入 BM25 混合检索。第四步故意问一个文档里不存在的问题观察模型是否老实回答“未找到相关资料”。如果模型强行编造说明 Prompt 还需要强化约束或者需要引入“低置信度拒答”策略。如果回答质量不理想不要急着换大模型。先从检索结果入手打印模型召回的片段看看这些片段是不是真的相关。90% 以上的 RAG 效果问题都出在检索这一层而检索问题又多半出在切块和 Embedding 的匹配上。8. 常见问题与排查思路RAG 项目涉及组件多每个环节都可能成为瓶颈。下面整理几个高频问题供你按图索骥排查。问题现象可能原因排查方式解决方案检索结果与问题无关Embedding 模型不适合业务文档打印 recall 的 top_k 片段观察更换 Embedding 模型或加入 BM25 混合检索回答常常胡编乱造检索结果相关但 Prompt 约束不足检查 Prompt 是否说明“无答案时拒答”强化 Prompt 约束必要时接入拒答策略索引构建很慢模型在 CPU 上运行或文档过大查看 CPU 使用率和文档大小使用 GPU 推理或并行化批量编码查询报维度不一致索引和查询用了不同 Embedding 模型检查模型名称和向量维度统一使用同一个模型并重建索引Ollama 连接失败Ollama 服务未启动或端口被占用检查 11434 端口是否监听重新执行ollama serve文本切块后语义断裂没有按照段落或结构切分观察 chunk 首尾内容增加 overlap或按 Markdown 标题、段落结构切分结果召回了大量重复片段文档中存在大量相似段落检查 chunk 之间的重复度增加去重逻辑或使用 MMR 增加多样性实际排查时建议先确定问题出现在“检索前”还是“检索后”。可以做一个最简单的判断把检索到的原始片段直接打印出来看。如果片段本身就不相关问题在索引侧怎么调 Prompt 都没用如果片段相关但回答不对问题在生成侧可以调整 Prompt 或换更大的模型。9. RAG 工程化最佳实践与进阶方向从最小示例跑通到真正上线一个生产级 RAG 系统中间还有很多工程工作要做。下面这些建议不是“加分项”而是你在做企业项目时大概率会遇到的硬需求。9.1 混合检索与重排要尽早引入只靠纯向量检索在真实业务数据上往往不够。企业内部文档经常包含产品型号、报错码、合同编号这类强标识信息这些内容用 BM25 关键词匹配比向量检索更稳定。所以生产级 RAG 通常采用“向量 BM25 多路召回”的混合检索架构先用两条路分别召回候选再用 RRF 或 Rerank 模型做融合排序。混合检索的引入不是推翻现有代码而是在检索层增加一条召回通道。向量检索擅长语义泛化BM25 擅长精确匹配两者互补之后整体召回率和准确率都会明显提升。如果预算允许再叠加一个 Rerank 模型效果会更加稳定。9.2 向量数据库选型要结合数据规模本文示例用的是 FAISS 本地索引适合学习和验证。如果数据量达到百万级或者需要支持多用户并发查询建议换用 Milvus、Qdrant 这类独立向量数据库。它们提供索引持久化、数据分片、副本、过滤查询等能力能支撑真正的在线业务。选型时不要只看检索性能还要看团队运维能力。已经有 PostgreSQL 的团队可以优先考虑pgvector已经有 Elasticsearch 的团队可以评估它的 kNN 检索能力。引入一个新的向量数据库意味着新的运维成本和故障点不能为了技术新鲜感盲目上。9.3 元数据过滤与知识更新RAG 上线后知识更新是常态。文档改了、下架了、版本升级了对应的向量和片段也要同步更新。如果所有数据都混在同一个索引里旧版本文档会持续干扰检索结果。比较好的做法是在写入向量时保存元数据比如来源、时间、部门、文档 ID查询时先按元数据过滤再执行向量检索。知识更新还涉及增量索引和重建索引的取舍。离线更新量大的场景可以使用定时增量更新如果数据总量不大直接重建索引可能更简单还能避免数据不一致的问题。9.4 从 RAG 走向 Agentic RAG 与知识图谱RAG 的进阶方向是让检索过程也具备智能。传统 RAG 是一次检索、一次生成遇到复杂问题时会显得死板。Agentic RAG 把大模型作为调度中枢先分析问题决定是检索、追问、还是不检索直接回答如果第一次检索结果不够还能改写查询、多次检索、走完多个工具。这种思路适合复杂问答场景但实现复杂度也更高。另一个人气上升的方向是把知识图谱和 RAG 结合。向量检索擅长处理非结构化文本知识图谱擅长表达实体之间的关系。两者结合后系统既能做语义召回又能沿着关系链推理。这对企业场景的“某人关联了哪些项目”“某个设备影响了哪些服务”这一类问题很有价值但知识图谱构建成本较高并非所有项目都需要。9.5 安全与权限边界如果 RAG 系统面向企业内部一定要考虑权限控制。底层文档可能包含不同部门的机密信息不能让所有用户检索到全部内容。向量数据库和检索服务的接口层需要做用户身份认证、权限校验和结果过滤。这些能力不能等到上线后再补否则一次越权查询就可能带来严重的数据泄露风险。涉及生产环境和数据变更时建议先在测试环境验证链路备份原始数据并保留回滚方案。RAG 系统检索结果的准确性直接决定用户体验上线前要准备一套评估集定期回归测试避免某次知识更新引入质量回退。真正扎实的 RAG 项目往往不是模型多先进而是工程细节扣得足够细。