RAG技术详解:从索引构建到检索增强生成的完整工作流
做知识库问答项目做了三年我越来越认同一句话RAG的难点从来不在跑通Demo而在把每个环节都真正吃透。很多人一上来就调向量库、换Embedding模型效果不理想就再堆算法折腾一圈发现还是不知道问题出在哪。我自己的经验是先花时间把RAG从文档入库到答案输出的完整链路捋清楚后面所有优化才有方向。这篇文章就用11张图把RAG的工作原理和工作流程从头到尾拆一遍覆盖索引阶段、检索阶段、生成阶段也讲多轮对话和Agentic RAG这些演进方向。适合正在做企业知识库、文档问答、客服机器人或者刚入门RAG想系统搞明白原理的朋友。1. 先搞清楚RAG到底解决什么问题1.1 大模型的“闭卷考试困境”大语言模型本质上是一个超大型的“记忆压缩系统”。训练阶段把互联网上的海量文本压缩进几百GB甚至几TB的参数里推理时它只能依靠这些参数里的“记忆”来回答问题。这就带来两个很实际的问题。第一是幻觉。模型对不确定的内容会一本正经地编造尤其当问题涉及它训练数据里覆盖不足的冷门知识时这种倾向会非常明显。第二是知识时效性。训练数据有截止日期今天发生的新事件、企业内部的最新制度、某个产品的内部文档模型天然是不知道的。第三是数据权限问题。企业私有知识不能进训练集你不可能为了一个问答系统重新训练一遍模型。这三个问题叠加起来直接拿通用大模型做知识库问答效果必然翻车。如果能把“开卷考试”的思路引入进来让模型回答问题时先查资料、再作答幻觉和知识过时的问题就能被大幅缓解——这正是RAG想做的事。1.2 RAG 检索 增强 生成RAG全称Retrieval-Augmented Generation检索增强生成。拆开来看三个词分别对应三个环节Retrieval检索从一个预先构建好的知识库中找出与当前问题最相关的内容片段。Augmented增强把检索到的内容片段拼进Prompt上下文作为“参考资料”喂给大模型。Generation生成大模型基于增强后的上下文生成有依据、可溯源的答案。这是一个非常朴素的闭环思路。类比一下就是学生闭卷考试容易瞎编现在允许他带着教材进考场他做题时先翻书找相关内容再结合自己的理解组织答案。RAG中的“书”就是外部知识库“翻书”就是检索“组织答案”就是大模型生成。这套方案的优点很直接。知识可以随时更新换新文档直接重建索引数据权限容易控制私有知识始终在内部存储中成本低不需要训练和微调模型。所以RAG成了目前企业知识库问答的主流技术方案几乎所有基于大模型的知识库应用底层都是RAG或者它的变体。图1RAG整体工作流程┌──────────────────────────────────────────────────────┐ │ 离线阶段索引构建 │ │ │ │ 文档加载 → 文本清洗 → 切块(Chunk) → Embedding → │ │ 向量化 → 写入向量数据库同时保存原始文本 │ └──────────────────────────────────────────────────────┘ │ │ 知识库 ▼ ┌──────────────────────────────────────────────────────┐ │ 在线阶段问答推理 │ │ │ │ 用户提问 → Query理解/改写 → Embedding → 向量检索 │ │ → 候选结果 → Rerank重排 → Prompt构造 → LLM生成 │ │ → 返回答案附引用来源 │ └──────────────────────────────────────────────────────┘这张图是整个RAG系统的总纲。离线阶段负责把知识“存”进去在线阶段负责把知识“取”出来。后面的每一张图都是围绕这条主线上的某个节点展开的。2. 索引阶段知识库是怎么被“读进去”的很多教程会把重点放在检索和生成上但以我的经验索引阶段才是决定RAG天花板的关键。知识库没有处理好后面检索和生成再怎么做都是杯水车薪。2.1 文档加载与清洗别把垃圾喂给系统知识库的原始输入形形色色PDF、Word、Markdown、HTML、扫描件、PPT。第一步是把这些非结构化文档转成纯文本再按需清洗。文档加载工具有很多比如PyMuPDF适合处理文本型PDFOCR工具如PaddleOCR、Tesseract处理扫描件Unstructured库能覆盖大多数常见格式。加载之后务必做清洗这一步很多人偷懒后面会花十倍的力气来还债。清洗的内容包括去掉页眉页脚、页码、水印这些内容经常导致切块时语义割裂纠正PDF抽取后常见的换行粘连问题比如一句话被断成两半删除表格乱码和多余空白统一编码格式避免中文乱码。清洗的目标是让文本干净、连贯为后续切块打基础。注意不要直接拿脏数据去做Embedding。垃圾进垃圾出这是RAG项目里最现实的教训。你在检索阶段发现的很多“找不到内容”问题根源往往就在文档清洗阶段。2.2 切块最容易被忽视但影响最大的一步文档清洗完之后不能整篇塞给系统必须先切成小块。为什么要切块原因有两点。第一检索精度。向量检索是在“相似度”层面匹配问题与文本块。整篇几十页的文档作为一条向量去匹配问题相关片段会被淹没在大量无关内容里相似度被严重稀释。切成小块后每个块的主题更聚焦检索精度自然更高。第二上下文窗口限制。大模型的输入长度有限即使现在很多模型支持几十万Token的窗口也不能把所有文档都塞进去——太贵而且长上下文中无关内容反而会干扰模型判断。切块后只需要把最相关的小块拼进Prompt。图2切块过程示意原始文档一篇完整的Markdown文档 │ │ 按标题/段落/固定长度切分 ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Chunk 1 │ │ Chunk 2 │ │ Chunk 3 │ │ 引言 │ │ 环境准备 │ │ 核心功能 │ │ “本章介绍...” │ │ “安装依赖...” │ │ “实现流程...” │ └──────────────┘ └──────────────┘ └──────────────┘这里有个核心参数需要自己调块大小。块太小语义不完整检索引擎拿到的上下文碎片化块太大语义虽然完整了但检索颗粒度变粗相似度容易被噪声拉低。我在实践中常用的经验值是普通技术文档、操作手册300500字一个块。法律条文、规章制度按条款切分一条一快不受字数限制。代码库按函数或类切分一个函数一快。还要设置重叠即相邻块之间保留少量重复内容比如重叠50100字。这么做的原因在于真正的边界语义可能横跨两个块如果一刀切下去语义就被切断了重叠可以保证关键信息至少在两个块中完整出现提升召回率。图3固定长度切块 重叠文本流按顺序排列的字符 ┌──────────────────────────────────────────────┐ │ Block 1 (0-500字) │ overlap │ │ │ │ ←80字→ │ │ │ Block 2 (420-920字) │ overlap │ ... │ │ ←80字→ │切块策略除了固定长度还有按段落切、按语义切比如利用Embedding自动识别语义边界和递归字符切分先按段落层级切再按句子切逐级降级。文本结构明显的文档优先按结构切结构不明显的用递归切分这是通用的做法。切完块之后每块会保留一些元信息比如来源文件名、章节路径、页码这些元信息在最终生成阶段做引用溯源时会派上大用场。2.3 Embedding把文字变成坐标切好的文本块不能直接被计算机检索必须转成向量。Embedding模型做的事情就是把一段文字映射到一个高维向量空间中的一个点。这个空间里语义相近的文本会被映射到彼此接近的位置语义无关的文本则距离很远。这就像把书里的内容搬进一座巨型图书馆内容相近的书被放在相邻的书架上。图4Embedding映射示意二维简化高维向量空间图示为二维投影 玫瑰 / 月季 / 蔷薇 ● │ │ 短距离语义相近 │ 牡丹 ●───● 芍药 篮球 ● ● 操作系统 │ 距离远语义无关 │ 足球 ●选Embedding模型时要注意两点。一是语言匹配度中文场景要选择对中文理解好的模型比如BGE系列、M3E系列而不是拿通用英文模型硬跑。二是维度与性能的平衡高维向量表达能力强但存储和计算开销大。很多场景下768维或1024维的模型已经够用。这里再强调一个容易被忽略的细节检索阶段用户的提问也必须用同一个Embedding模型做向量化。一旦训练或推理时模型不一致向量的坐标空间就错位了匹配精度会大打折扣。项目上线后要固化Embedding模型的版本不能随意升级替换否则整个索引都要重建。2.4 向量数据库把向量存起来并按需取回向量数据库负责存储向量并支持高效的相似度检索。市面上的选择很多简单分个类方案部署方式适合场景关键词FAISS嵌入式/本地单机、原型开发简单直接Chroma嵌入式快速验证、轻量项目易上手Milvus服务端/分布式海量数据、生产环境功能全、性能强pgvectorPostgreSQL扩展已有PG体系、事务一致性与业务数据打通Qdrant服务端生产环境、高可用过滤能力强选型逻辑并不复杂。数据量在百万级以下、团队没有太多运维精力FAISS或者Chroma就能顶住数据量上了亿级、要求高并发和复杂过滤就直接上Milvus或Qdrant。如果企业已经以PostgreSQL作为核心数据库希望知识库数据和业务数据放一起管理pgvector是自然的选择。项目初期追求快速验证的话完全不必过度设计。除了向量本身向量数据库里通常还保存着原始文本内容和元信息。因为向量只是坐标真正给模型看的还是文本元信息则用于回答时定位来源比如返回“这段话出自《XX产品手册》第3章”。3. 检索阶段怎么从海量内容里捞出相关的部分索引建好了知识库就绪了接下来进入在线问答流程的第一步检索。3.1 相似度计算向量检索的底层逻辑用户的提问经过同一个Embedding模型向量化后变成查询向量。检索要做的事情就是在向量空间中找到离这个查询向量最近的K个文本向量。衡量“距离”的指标常用余弦相似度计算公式是cosine_similarity(A, B) (A · B) / (||A|| × ||B||)A和B分别代表两个向量。分子是点积分母是两者模长的乘积结果范围在-1到1之间越接近1表示方向越一致、语义越相近。这个指标只看向量之间的夹角不受向量长度影响所以特别适合文本相似度场景。计算完相似度后取Top-K个结果返回。K的取值要根据实际情况调太小容易漏掉真正相关的文档太大会把大量无关内容送进Prompt干扰模型判断。纯向量检索的K值我一般设在5到10之间具体看知识库的体量和文档粒度。图5向量检索的Top-K流程用户问题 │ ▼ Embedding 模型 → 查询向量 q │ ▼ 向量数据库计算 q 与库中所有向量 v_i 的余弦相似度 │ ├─ v_1: 0.872 ← 最相似 ├─ v_2: 0.815 ├─ v_3: 0.764 ├─ v_4: 0.701 └─ v_5: 0.623 ← Top-5 截断 相似度低于阈值的直接过滤3.2 混合检索关键词和向量一起上纯向量检索有一个弱点对专有名词、精确代码、特定ID这类“词面匹配”场景不敏感。比如用户搜索“ERR_CONNECTION_TIMED_OUT”这样的字符串在语义上很难被Embedding模型理解但关键词检索一找一个准。反过来关键词检索又搞不定同义词和语义扩展比如用户问“你们的手机续航怎么样”文档里写的是“电池坚持一天没问题”没有直接出现“续航”两个字关键词就召回不了。所以现在主流方案都是混合检索同时跑一路关键词检索BM25算法和一路向量检索再把两路结果合并去重按融合分数重新排序。图6混合检索流程用户问题 │ ├──────────────────┐ ▼ ▼ BM25关键词检索 向量检索 │ │ ▼ ▼ 关键词命中文档集 语义相似文档集 │ │ └────────┬─────────┘ ▼ 结果融合去重 加权 │ ▼ 候选文档列表融合方法最简单的就是加权求和比如score 0.4 × bm25_score_norm 0.6 × vector_score_norm权重根据数据特点调。BM25结果少但精准向量结果多且能扩展语义两者互补后召回率会明显提升。这一步在代码里实现并不复杂用一个列表合并再按融合分排序就行。3.3 重排序把最相关的内容顶到最前面检索阶段拿到的是“候选集”里面可能混着不那么相关的内容。如果直接把Top-5全部塞给大模型噪声会影响答案质量。这时候需要重排序用Cross-Encoder模型对“查询-文档”的配对相关性做精确打分重新排一遍顺序。图7Rerank前后对比Rerank之前混合检索结果 1. 文档C向量分高但实际与问题只有表面关系 2. 文档A关键词命中相关性一般 3. 文档E真正的答案但初始分排第三 4. 文档B无直接关系 5. 文档D废话较多 Rerank之后Cross-Encoder精确打分 1. 文档E真正回答了问题 ← 精确相关性最高 2. 文档A 3. 文档C 4. 文档D被过滤掉 5. 文档B被过滤掉Rerank的价值在于把“语义相似”升级为“真的相关”。向量检索找到的是“像答案的内容”Rerank负责判断“这到底是不是答案”。实际操作中Rerank一般只对Top-20或Top-50的候选做精排最后保留Top-3到Top-5。因为Cross-Encoder计算成本远高于向量检索不能全库跑只用来做“最后一公里”的筛选。常用的Rerank模型有bge-reranker系列、Cohere Rerank等。这一步做了之后RAG的答案质量通常会有肉眼可见的提升强烈建议生产环境都要加不要省。4. 生成阶段把检索结果变成最终答案检索拿到相关文本下一步就是用大模型生成答案。这个环节看起来只是“把内容塞进Prompt再问一次”但细节里藏着很多影响输出质量的学问。4.1 Prompt构造给模型一份结构化的“参考资料”生成阶段的核心是Prompt的设计。一个好的RAG Prompt至少包含三部分系统指令、上下文片段、用户问题。图8生成阶段的Prompt结构┌─────────────────────────────────────────────┐ │ 系统指令System Prompt │ │ 你是一个企业知识库助手。请严格根据提供的 │ │ 参考内容回答问题。如果参考内容中没有答案 │ │ 请明确回答“知识库中未找到相关信息” │ │ 不要编造。回答时标明引用来源。 │ ├─────────────────────────────────────────────┤ │ 参考内容Context │ │ [1] 来源产品手册-第3章-第2节 │ │ 内容设备最大支持并发连接数为200... │ │ [2] 来源FAQ-常见问题-第5条 │ │ 内容设备固件升级需要预留20分钟... │ ├─────────────────────────────────────────────┤ │ 用户问题Question │ │ 设备能支持多少并发连接升级固件要多久 │ └─────────────────────────────────────────────┘系统指令里要做三件事限定回答来源、设定不知道怎么办的策略、要求引用来源。严格参考内容这一点约束幻觉让模型只基于检索结果作答而不是自由发挥。未知回答策略也重要很多RAG系统翻车不是因为答错而是明明没有资料还硬答一句“知识库中未找到相关信息建议咨询人工客服”反而能兜住很多问题。4.2 增强上下文的组织别让模型被噪声带偏检索回来的多个文档块不是简单拼在一起就行。我的经验是先按Rerank后的相关度排序然后在每块前面加上元信息告诉模型这段话出自哪里。这相当于给参考内容打上出处标签模型回答时可以引用“[1]”或“[2]”用户能直接溯源到原始文档可信度完全不同。还有个常被忽略的点上下文冲突处理。检索出的不同块之间可能有矛盾这时候需要在Prompt里加一句“如果参考内容存在冲突请指出并优先采信相关性更高的内容”。模型有冲突识别能力但你得明确授权它这样做否则它会自己糊弄过去。关于“RAG必须用API吗”这个问题也在生成阶段体现——并非必须。很多人担心RAG绑定商业API其实完全不是。生成端的模型可以换成本地部署的开源模型比如Qwen系列、ChatGLM系列通过Ollama或vLLM跑起来离线环境下照样能用RAG。唯一的代价是本地小模型的理解能力可能弱一些对Prompt设计的要求更高。如果你追求推理效果好可以用大厂API如果是数据敏感场景完全纯本地化部署也可以只是需要接受效果折损。4.3 引用溯源RAG系统的信任基石RAG相比纯大模型的最大优势是“答案可验证”。生成阶段一定要把“引用”带出来每个回答都能定位到知识库里的具体文档。实现的思路不复杂检索阶段保留每个块的文件名、页码、章节路径增强阶段将来源编号注入Prompt生成阶段要求模型在输出中标注对应编号。图9带引用的回答输出用户设备保修期是多久 模型 根据产品手册信息设备整机保修期为12个月。 [1] 来源产品手册-售后服务条款-第2章引用溯源不只能增强用户信任也方便开发者在效果不好时快速定位“这个回答来自哪段资料”排查问题会非常高效这是我认为RAG项目里最值得做对的设计之一。5. 完整工作流串联与进阶演进前面把每个环节拆开讲了现在重新串一遍完整的在线问答流程再看多轮对话和Agentic RAG这两个最常见的进阶场景里工作流是怎么演进的。图10完整在线问答时序用户提问 │ ▼ ┌─────────┐ ┌──────────┐ ┌──────────┐ │Query改写│ → │ Embedding │ → │ 检索 │ │如有历史│ │ 查询向量化 │ │ 混合/ │ │ 对话 │ │ │ │ Top-K │ └─────────┘ └──────────┘ └────┬─────┘ ▼ ┌──────────────┐ │ Rerank重排 │ │ 保留Top-3~5 │ └──────┬───────┘ ▼ ┌──────────────┐ │ 组装Prompt │ │ 注入上下文 │ └──────┬───────┘ ▼ ┌──────────────┐ │ LLM生成 │ │ 答案引用 │ └──────┬───────┘ ▼ 返回给用户5.1 多轮对话中的Query改写前面的流程假设用户第一句话就问问题。现实中用户会连续追问“它的价格呢”“那保修多久”这些指代性提问没有上下文根本检索不了。解决办法是引入Query改写模块每次提问到来时把对话历史中相关的信息合并进去改写成一条独立的、完整的检索问题再去检索。图11多轮对话中的Query改写用户第一轮这个笔记本电池能用多久 系统回答正常使用约8小时。 用户第二轮那它的充电器呢 │ ▼ Query改写模块用LLM 对话历史 │ ▼ 改写后的检索Query “这个笔记本的充电器能用多久/有什么规格” │ ▼ 进入检索 → 生成 → 回答实现方式很简单把最近几轮对话和当前问题一起发给LLM让它生成一条适合检索的独立问题然后走正常的检索生成流程。注意改写后的问题和原始问题差别可能很大但检索效果会好得多。生产环境做长记忆时这个模块几乎必加。5.2 Agentic RAG让模型自己决定找什么、找几次传统RAG是固定流水线Question进来就一定检索、一定生成。Agentic RAG改变了这个范式加入Agent智能体编排模型可以自主决定是否需要检索、检索什么内容、是否需要二次检索纠正自己。应用场景很典型。用户问“上周A区服务器的故障报告里提到的问题解决了吗”这类问题涉及多个条件、可能要查好几类不同的知识。传统RAG一次检索很难凑齐答案。Agentic RAG里Agent把任务分解成多个子查询分别检索汇总后再回答。如果生成过程中发现证据不足它还知道自己去知识库再查一次而不是硬编。技术栈上LangChain加LangGraph是做Agentic RAG的常用组合。LangGraph提供了状态图和条件分支能力能定义“如果检索结果相关性不足→触发二次检索”这类循环逻辑。效果上Agentic RAG更适合复杂、多跳、条件交叉的问题但推理延迟更高Token消耗明显增加。简单问答场景用传统RAG就够了没必要上Agent增加成本。6. 常见问题与排查经验写到这里补充一些实战中高频踩过的坑按排查顺序整理成速查表遇到问题直接对照检查。6.1 检索质量差的排查顺序检索不到相关内容我建议从下往上排查先看索引再看查询最后看排序。现象可能原因排查动作相似度普遍低于0.5Embedding模型不匹配确认查询和库用同一模型关键词命中但向量检索不到语义模型对专有名词不敏感加BM25混合检索召回了但相关度不够切块太大语义被稀释缩小块大小、增加重叠高相关文档排不到前面缺少Rerank接Rerank精排文档更新后检索结果没变索引未重建确认增量更新任务跑通6.2 切块参数实战参考块大小和重叠没有万能值但可以参考这个范围起步调试文档类型建议块大小字符建议重叠切分依据技术手册/操作指南3005005080段落/标题政策法规/合同按条款分割030条款编号代码文件按函数/类分割0语法结构常见问题FAQ一问一答为一个块0问题条目调试时可以随机抽20个真实用户问题人工标注“正确答案出自文档哪个位置”然后调参看命中率。命中率从60%提升到80%以上效果就差不多了。这个标注集积累下来以后每次调Embedding或切块策略都能复用。6.3 上下文爆炸与Token成本控制随着检索内容变多、多轮对话历史变长Prompt体积会迅速膨胀Token成本也随之走高。建议做三件事限制检索块数通常Top-3到Top-5就够了不要贪多做对话历史裁剪保留最近4到6轮再早的内容如果重要就通过摘要压缩对超长文档做摘要缓存一句话而不是一段话进入上下文。6.4 评估RAG系统不能只靠感觉RAG效果不能只看一两个例子就下结论。推荐用RAGAS这类框架做自动评估核心指标有三个忠实度答案是否严格基于检索到的上下文没有幻觉相关性答案是否切题没有答非所问上下文精度检索回来的内容里真正用得上的比例有多高。定期跑一批测试集把指标记录下来每次改动之后对比变化这才叫客观调优。7. 实操心得做好RAG的几条个人经验最后分享几点在这类项目里反复验证过的经验也是我踩过不少坑后总结的。第一先跑通再优化。第一版用最简单的流程固定长度切块、单路向量检索、Top-5、不加Rerank先让整个链路能回答出一个像样的答案再一步步加复杂模块。一上来就全功能上马出问题时根本定位不了是哪一环。第二数据的优先级永远高于算法。很多时候问题不出在模型和检索而是知识库本身存在缺失或者文档表述吃力检索和生成做得再好也检索不出一个不存在的事实。先保证知识库覆盖关键问题再优化算法效果提升会更快。第三RAG是七分工程、三分算法。真正影响上线效果的不是你用多炫的模型而是数据清洗、切块质量、Prompt细节、评估闭环这些看似平凡的事。先把基础环节做扎实再去追Agentic RAG这类新概念你会发现很多问题在基础层面就已经解决了。第四系统上线后一定要记录检索日志。用户问了什么、检索到了什么、最终回答了什么都存下来。定期看看那些回答质量低的case你会发现很多改进点——哪些问题知识库没覆盖、哪些检索总召回无关内容、哪些Prompt指令没被模型遵守。这些来自真实场景的问题是任何教程都教不了的。RAG的完整工作流从文档预处理走向索引构建从混合检索走向重排序再从Prompt增强走向结果生成每一个环节都有属于自己的坑也都值得投入精力打磨。希望这11张图能帮你建立全局视野下次再遇到“RAG效果差”这种大问题时你能顺着流程一步步定位而不是毫无头绪地乱调参数。