RAG 检索增强生成
模型的知识来自训练数据,它不知道你的内部文档、最新的产品手册、上周刚改的退款政策。RAG(Retrieval-Augmented Generation,检索增强生成)的做法是:回答之前,先从你的资料库里找出相关内容,放进上下文,让模型照着资料回答。
做一个能跑的 RAG 演示很简单,做一个可靠的 RAG 却会遇到很多问题:
- 退款政策已经从 7 天改成了 15 天,问答机器人还在自信地回答“7 天”;
- 用户问“错误码 E1023 是什么意思”,系统检索出一堆不相关的段落;
- 普通员工问了一个问题,回答里引用了只有管理层才能看的文档;
- 资料里根本没有答案,模型还是编了一个看起来很专业的回答。
读完这篇你会
- 说清 RAG 的建索引流程和问答流程
- 分别评估“检索”和“生成”两个环节的质量
- 处理索引过期、关键词匹配、分块、权限和无答案等问题
1. 原理:开卷考试
可以把 RAG 想成一场开卷考试:模型是考生,你的资料库是一本很厚的参考书。考生很聪明,但考试时间有限,只能看你翻给他的那几页。
- 翻对了页,他能答得又准又快,还能指出答案在第几页;
- 翻错了页,再聪明的考生也只能根据错误的资料作答。
所以 RAG 的质量,首先取决于“翻页”(检索)的质量,其次才是“答题”(生成)的质量。
RAG 分成两条流程:
text
建索引(资料变化时运行):
文档 → 清洗 → 切成片段 → 每个片段转成向量 → 连同元数据存入索引
问答(每次提问时运行):
用户问题 → 检索相关片段(向量 + 关键词)→ 重排序取前几条
→ 和问题一起组装成提示词 → 模型回答并标注引用 → 校验引用2. 为什么不是把资料“训练进模型”
| 方式 | 知识放在哪里 | 更新成本 | 能否给出处 |
|---|---|---|---|
| 只靠模型 | 模型参数里 | 无法更新 | 不能 |
| 微调 | 模型参数里 | 需要重新训练 | 不能 |
| RAG | 外部资料库 | 增删改文档即可 | 能 |
知识经常变化、需要给出处、需要精确引用原文的场景,RAG 几乎总是更合适。微调更适合改变模型的风格、格式和领域表达习惯,而不是灌输具体的事实。两者也可以叠加使用。
3. 分块:把长文档切成片段
长文档不能整篇放进上下文,要切成片段(chunk)。粒度是一个权衡:
| 粒度 | 优点 | 缺点 |
|---|---|---|
| 太小(一句话) | 匹配精准 | 失去上下文,片段单独看不懂 |
| 太大(一整章) | 上下文完整 | 无关内容多,相关性被稀释,占用 Token |
| 适中(一段或一个小节) | 平衡 | 需要处理好边界 |
实践建议:
- 优先按文档结构切:按标题、段落、列表项切,而不是机械地按固定字数切;
- 相邻片段适当重叠,避免一句完整的话被切成两半;
- 每个片段带上所在章节的标题作为前缀,比如“退款政策 > 申请条件:……”,单独看也知道它在讲什么;
- 保存元数据:文档 ID、标题、章节、版本、更新时间、访问权限。
4. 检索:向量和关键词结合
向量检索:嵌入模型把文本转换成一串数字(向量),意思相近的文本,向量也相近。它能理解“重置密码”和“找回密码”是一回事。
关键词检索(如 BM25):按词语匹配,对专有名词、编号、代码标识符更准。
生产环境里通常两者结合(混合检索),各取一批结果再合并。
重排序:向量检索速度快但比较粗糙,常常召回一批“大致相关”的片段。用一个更精细的重排序模型对这批结果重新打分,只取最相关的几条放进上下文:
text
混合检索召回 50 条(快、粗)→ 重排序模型打分(慢、准)→ 取前 5 条5. 组装提示词:要求依据资料回答
text
## 规则
- 只根据下面“资料”部分的内容回答,不要使用资料之外的知识
- 每个关键结论后面标注来源编号,如 [S2]
- 资料中没有答案时,直接回答“资料中没有找到相关信息”,不要猜测
## 资料
[S1] 退款政策 > 申请条件(2026-09-01 版):自签收之日起 15 天内可申请……
[S2] 退款政策 > 到账时间:审核通过后 3~5 个工作日原路退回……
## 问题
(用户的问题)模型返回后,程序再校验引用:答案里出现的每个来源编号,都必须是这次真正提供给它的片段。引用了不存在的编号,说明模型在编造。
6. 坑一:文档更新了,回答还是旧的
现象:退款期限已经从 7 天改成了 15 天,问答机器人还在回答“7 天”,而且引用看起来也没问题。用户照着旧信息操作,产生了投诉。
原因:索引是源文档的一份独立快照,不会自动跟着变。文档改了,但索引里存的还是旧片段,检索命中的自然是旧内容。
修复:把“文档变更 → 重新建索引”做成自动流程:
text
文档新增 / 修改 / 删除
→ 触发增量索引(只处理变化的文档)
→ 写入时带上文档版本和更新时间
→ 删除该文档的旧片段回答时在引用里展示文档的更新时间;定期抽查索引和源文档是否一致。RAG 的知识新鲜度取决于索引的同步机制,而不是模型。
7. 坑二:检索到的都是不相关的内容
现象:用户问的问题在资料里明明有答案,但检索出的片段都不相关,模型只能回答“没有找到”或者答非所问。
修复:先单独评估检索的质量:
- 准备几十个真实的问题,人工标注每个问题“应该被检索到的片段”;
- 统计正确片段出现在前 5 条结果里的比例(召回率);
- 每次调整分块方式、嵌入模型、检索参数时,重新跑一遍,用数据判断是变好还是变坏。
常见的改进方向:调整分块粒度、给片段加上标题前缀、改用混合检索、增加重排序,或者先让模型把口语化的问题改写成更适合检索的查询。
8. 坑三:编号、型号、专有名词搜不到
现象:用户问“错误码 E1023 是什么意思”,资料里有一篇专门讲 E1023 的文档,却没有被检索出来。
原因:纯向量检索擅长“意思相近”,但对编号、型号、代码这类没有语义的字符串不敏感。“E1023”和“E1032”在向量上可能非常接近。
修复:加入关键词检索,形成混合检索;对于明显是编号、型号的查询,可以直接用精确匹配优先。
9. 坑四:分块切断了表格和步骤
现象:问“退款需要几步”,回答只给出了前三步。原来的文档有六个步骤,被切到了两个片段里,只检索到了第一个。
修复:
- 按结构分块,把一个完整的列表、表格、代码块作为一个整体;
- 片段太大时,再在内部按行切分,并给每一部分加上表头或标题;
- 检索到某个片段时,可以把它前后相邻的片段也一起带上。
10. 坑五:检索到了用户无权查看的文档
现象:普通员工问了一个关于薪酬的问题,回答引用了只有人事部门才能看到的文档内容。
原因:检索时没有按权限过滤,所有文档都在同一个索引里,谁问都能检索到。
修复:在检索阶段就按权限过滤,而不是生成后再过滤:
- 每个片段的元数据里记录访问权限(所属租户、部门、角色、文档 ID);
- 检索时带上当前用户的权限条件,只在他能访问的范围内检索;
- 文档的权限变更后,同步更新索引中的元数据。
一旦无权查看的内容进入了上下文,就不能指望模型“自觉不说”。
11. 坑六:资料里没有答案,模型还是编了一个
现象:用户问了一个资料里完全没有涉及的问题,模型给出了一个看起来很专业的回答,但完全是编的。
修复:
- 在提示词里明确要求“资料中没有答案时直接说明”(见第 5 节);
- 检索结果的相关度都很低时,程序直接返回“没有找到相关资料”,不再调用模型;
- 校验引用:没有引用任何资料的结论,标记为“未经资料支持”或直接不展示;
- 在测试集里加入一些“资料中没有答案”的问题,确认系统会如实回答。
排查思路:先分清是“翻错页”还是“答错题”
一个问题回答错了,先看这次检索到了哪些片段:
- 正确的片段没有被检索到 → 检索问题:检查分块、嵌入模型、关键词检索、权限过滤、索引是否过期;
- 正确的片段检索到了,但回答还是错的 → 生成问题:检查提示词、片段的排列顺序和数量、模型能力;
- 片段本身的内容就是错的或过时的 → 数据问题:检查源文档和索引同步。
为此,日志里要记录每次问答的:问题、检索到的片段 ID 和得分、最终的提示词、模型回答和引用。
检查清单
- [ ] 按文档结构分块,片段带标题前缀和完整元数据
- [ ] 使用混合检索,并有重排序
- [ ] 文档变更会自动触发增量索引,旧片段会被删除
- [ ] 检索阶段按用户权限过滤
- [ ] 提示词要求依据资料回答、标注来源、没有答案时如实说明
- [ ] 程序校验引用,低相关度时不强行回答
- [ ] 有标注好的测试问题集,分别评估检索和生成的质量
延伸阅读
- LLM 基础:为什么模型会编造引用
- LLM 评测与 Guardrails:怎样建立测试集
- AI Agent 开发实战:在 Agent 中使用检索工具