大模型知道很多事,但不知道你公司昨天刚发的制度、你自己写的项目文档,也不知道最新一期的行业报告。微调可以把知识“灌”进模型,但成本高、更新慢,还容易遗忘。RAG(Retrieval-Augmented Generation,检索增强生成)换了一个思路:不改模型,而是在回答问题前,先把相关资料找出来,连同问题一起交给模型。
打个比方,闭卷考试考的是模型记住了多少;RAG 是开卷考试,考的是它能不能在资料里找对地方、读懂内容、组织答案。
1. 一张图看懂 RAG
RAG 的流程可以分成两条链路:离线索引和在线问答。看图比看文字快:
上半部分是离线索引,把文档变成可检索的向量;下半部分是在线问答,从一句提问走回一份带引用的答案。两条链路共用同一个 Embedding 模型和向量维度,这是整张图里唯一一处“必须对齐”的约束。
对应到经典的三阶段划分:
| 阶段 | 输入 | 输出 | 核心目标 |
|---|---|---|---|
| 索引 Indexing | 原始文档 | 可检索的向量与元数据 | 把知识整理成“找得到”的形态 |
| 检索 Retrieval | 用户问题 | 若干相关文本块 | 在有限时间内找全、找准 |
| 生成 Generation | 问题 + 上下文 | 答案与引用 | 基于证据回答,而不是自由发挥 |
离线链路通常只在文档变更时运行,在线链路则要在几百毫秒到几秒内完成。两条链路使用同一个 Embedding 模型,否则向量空间对不上,检索结果会非常离谱。
2. 索引流程
索引阶段的目标只有一个:让知识变得“找得到”。下面五步,每一小步的粗糙程度都会在后文被放大。
2.1 文档解析
第一步是把各种格式的文档变成纯文本,同时尽量保留结构信息。
- PDF 是最麻烦的:双栏排版、页眉页脚、表格、扫描件都会干扰抽取。文本型 PDF 可以用
pypdf、pdfplumber,复杂版式建议用unstructured、docling这类版面感知工具;扫描件需要先走 OCR。 - Word、Markdown、HTML 相对简单,但要保留标题层级、列表、代码块和表格。
- 表格最好转成 Markdown 或“字段: 值”的描述,直接拍平成一行文本会丢失列与列的对应关系。
- 图片、流程图可以考虑多模态模型生成描述,再作为文本入库。
解析质量决定了整个系统的上限。垃圾进,垃圾出,后面再好的模型也救不回来。
2.2 清洗与元数据
解析出来的文本通常带有大量噪音:导航栏、版权声明、重复页眉、断行、乱码。需要做去重、去噪、段落合并和编码修正。
比清洗更容易被忽略的是元数据。每个文本块至少应该带上:
- 来源文件、页码、章节标题
- 文档版本、更新时间
- 租户、部门、密级等权限字段
- 文档类型(制度、手册、FAQ、代码文档)
元数据决定了检索时能不能做过滤,也决定了答案能不能给出可追溯的引用。没有元数据的 RAG,回答错了很难定位原因。
2.3 分块(Chunking)
文本太长不能整篇塞进模型,需要切成小块。分块是 RAG 中最值得反复调优的环节之一。
| 策略 | 做法 | 优点 | 适用场景 |
|---|---|---|---|
| 固定长度 | 按字符或 token 切,块间重叠 | 简单、块大小均匀 | 结构混乱的纯文本 |
| 递归切分 | 按段落、句子、标点逐级切 | 尽量保留自然边界 | 通用默认方案 |
| 结构感知 | 按标题、章节、表格切 | 语义完整、便于引用 | Markdown、HTML、规范文档 |
| 语义分块 | 相邻句子向量相似度骤降时断开 | 主题更聚焦 | 长篇文章、会议记录 |
| 父子分块 | 索引小块,返回其所属大块 | 检索准、上下文全 | 问答精度要求高 |
几个实践经验:
- 块太小,语义不完整,模型看不懂;块太大,噪音多,向量被稀释。中文场景可以从 300~600 字、重叠 10%~20% 开始试。
- 不要把一个标题和它的正文切散;标题往往是最好的语义信号,可以在每个块前面拼上章节路径。
- 表格、代码、操作步骤尽量保持完整,不要从中间截断。
- 分块参数没有标准答案,最终要靠评估集来定。
2.4 向量化(Embedding)
Embedding 模型把文本映射成高维向量,语义相近的文本在向量空间里距离更近。
常见选择:
- 中文/多语言:
bge-m3、bge-large-zh-v1.5、text-embedding-v3、jina-embeddings-v3 - 英文为主:
text-embedding-3-small/large、e5系列 - 本地部署:
bge系列配合sentence-transformers,成本可控,数据不出内网
注意几点:
- 文档和问题必须用同一个模型,维度也要一致。
- 相似度通常用余弦相似度;向量归一化后,内积等价于余弦相似度。
- 有些模型区分“文档侧”和“查询侧”输入,比如 BGE 中文模型建议给查询加指令前缀,效果会更好。
- 换模型意味着全量重建索引,选型时要考虑长期维护成本。
2.5 建索引与存储
向量写入向量数据库,常见选项有 Milvus、Qdrant、Weaviate、pgvector、FAISS、Elasticsearch/OpenSearch 的向量能力。
索引结构上:
- Flat:暴力精确检索,适合几万条以内的小数据量。
- HNSW:图索引,召回高、延迟低,内存占用较大,是大多数场景的默认选择。
- IVF / PQ:通过聚类和量化压缩内存,适合亿级数据,但会损失一些精度。
工程上还要考虑:增量更新、删除与版本管理、多租户隔离、快照备份,以及混合索引——同时保存稠密向量和 BM25 倒排,后面检索时可以两路召回。
3. 检索流程
检索的难点在于:用户问得很随意,而文档写得很正式。中间要做的事,就是把两者对齐。
3.1 查询改写
用户的原始问题往往不适合直接检索:有指代(“它怎么配置”)、有口语化表达、有多层意图,或者依赖上一轮对话。
常见的改写手段:
- 对话补全:把“它的默认端口是多少”改写成“Redis 的默认端口是多少”。
- 多查询扩展:让模型生成 3~5 个同义或不同角度的查询,分别检索后合并结果。
- 查询分解:把“A 和 B 有什么区别”拆成两个子问题,分别检索。
- HyDE:先让模型生成一段假设性答案,再用这段答案去检索。答案和文档的表述风格更接近,有时能提升召回。
- 意图与过滤条件抽取:识别时间、部门、文档类型等条件,转成元数据过滤。
改写不是越多越好。每多一步就多一次模型调用和延迟,简单问题直接检索反而更稳。
3.2 召回
召回的目标是“宁可多拿一些,别漏掉”。常见做法:
- 稠密检索:问题向量与块向量做近似最近邻搜索,取 Top-K(常见 20~100)。
- 稀疏检索:BM25 对专有名词、错误码、型号、数字特别有效,正好弥补向量检索的短板。
- 混合检索:两路结果用 RRF(倒数排名融合)或加权分数合并,通常比单路更稳。
- 元数据过滤:先按租户、时间、文档类型过滤,再在子集内做向量搜索,既提精度又避免越权。
- 多索引路由:不同知识域建不同集合,先判断问题属于哪个域,再去对应集合检索。
3.3 重排(Rerank)
向量召回追求的是速度和召回率,排序未必准。重排模型(Cross-Encoder)把“问题 + 文档块”一起输入,直接打分,精度明显更高,但计算量大,所以只对少量候选做。
典型链路是:召回 50 条 → 重排取前 5~10 条。常用模型有 bge-reranker 系列、Cohere Rerank 等。
除了相关性,还可以考虑:
- MMR(最大边际相关性):在相关和多样之间取平衡,避免返回一堆内容重复的块。
- 分数阈值:低于阈值的结果直接丢弃,宁可不答,也不要拿不相关内容硬答。
- 时间衰减:制度、价格、版本类文档,旧版本应该降权。
3.4 上下文组装
拿到候选块后,不能简单拼接了事。需要做去重、按来源合并、控制 token 预算,并给每个块加上编号或来源标记,方便模型引用。
还有一个容易被忽略的现象:模型对上下文开头和结尾的内容更敏感,中间部分容易被忽略(Lost in the Middle)。因此:
- 最相关的块放在前面;
- 同一文档的块尽量按原文顺序排列,避免逻辑断裂;
- 总长度控制在模型窗口的合理比例内,别把 10 个块硬塞进去。
4. 生成流程
生成阶段的 Prompt 通常包含四部分:角色与任务、参考资料、回答约束、用户问题。
你是一个严谨的知识库助手。请仅根据下面的参考资料回答问题。 |
几个关键约束:
- 只依据上下文回答,这是抑制幻觉的第一道防线。
- 要求引用,既方便用户核对,也方便排查是检索错了还是生成错了。
- 允许拒答,比一本正经地胡说八道更有价值。
- 结构化输出:需要接入下游系统时,用 JSON Schema 或函数调用约束格式。
生成之后还可以做后处理:检查引用编号是否存在、答案是否包含上下文中没有的实体、敏感信息是否需要脱敏。对可靠性要求高的场景,可以让另一个模型做一次“忠实性校验”。
5. 进阶形态
基础 RAG 是“一次检索、一次生成”。遇到复杂问题,可以在此基础上扩展:
- Agentic RAG:让模型自己决定要不要检索、检索什么、是否继续追问,支持多轮工具调用。
- Self-RAG / CRAG:模型对检索结果做相关性判断,不够就改写查询重试,或者退回网络搜索。
- GraphRAG:从文档中抽取实体和关系构建知识图谱,适合“全局总结”“跨文档关联”这类向量检索不擅长的问题。
- 多模态 RAG:文本、图片、表格统一向量化,回答时可以引用图表。
- 记忆机制:把历史对话和用户偏好沉淀下来,避免每轮都从零检索。
这些方案不是越复杂越好。先用基础 RAG 跑通评估,再针对具体失败案例加组件,通常比一上来就搭 Agent 更有效。
6. 如何评估
RAG 的评估要拆开看,否则出了问题不知道该修检索还是修生成。下图把“指标—归因—回归”串成了一个闭环:
| 环节 | 指标 | 说明 |
|---|---|---|
| 检索 | Recall@K | 正确答案所在的块有没有被召回 |
| 检索 | MRR / nDCG | 正确结果排得够不够靠前 |
| 生成 | Faithfulness | 答案是否忠实于上下文 |
| 生成 | Answer Relevancy | 答案是否切题 |
| 生成 | Context Precision / Recall | 上下文是否精准且覆盖充分 |
| 端到端 | 人工通过率、用户采纳率 | 最终业务效果 |
实践建议:
- 从真实业务里整理 50~200 条问题,标注标准答案和对应文档,作为回归集。
- 每次调整分块、模型、Top-K、Prompt,都跑一遍同一套评估,避免“感觉变好了”。
- 建立坏例分析习惯:是没召回、召回但不相关、还是召回了但模型没用上?三类问题的解法完全不同。
- 线上记录问题、召回结果、最终答案和用户反馈,持续补充评估集。
7. 常见坑
- 只调模型,不调数据:多数 RAG 效果差,根源在解析和分块,而不是模型不够强。
- 块切得太碎:检索命中率高,但模型拿到的上下文残缺,答案似是而非。
- 只用向量检索:遇到型号、错误码、人名等精确匹配时召回很差,应该加 BM25 做混合检索。
- Top-K 越大越好:无关内容会稀释注意力,增加延迟和成本,重排后保留 5~10 条通常足够。
- 没有权限过滤:检索阶段就把无权访问的文档过滤掉,不要等生成后再脱敏。
- 索引不更新:文档变了索引没变,答案就会“过期”。需要建立增量同步和版本机制。
- 没有评估集:每次改完靠感觉判断,问题和优化都无法复现。
- 忽视 Prompt 注入:文档里可能藏有恶意指令,需要在 Prompt 中明确“资料只是资料”,并对输出做检查。
8. 一个最小可用的流程
把上面的步骤压缩成伪代码,大概是这样:
# ---------- 离线索引 ---------- |
更完整的可运行示例(LangChain + FAISS + 通义千问),可以看之前的 RAG 练习 一文。
9. 总结
RAG 的流程并不复杂:把文档整理好、切好、向量化存起来;用户提问时找回来、排好序、拼进 Prompt,再让模型基于证据回答。
真正拉开差距的是细节:解析是否干净、分块是否合理、检索是否混合、重排是否到位、Prompt 是否克制,以及有没有一套能持续回归的评估集。模型只是流程中的一环,RAG 的效果上限,很大程度上由检索质量决定。
落地时不妨按这个顺序推进:先跑通最小链路,再建评估集,然后针对坏例逐个优化。每加一个组件,都要能回答一个问题:它解决了哪类失败案例,代价是多少延迟和成本。