RAG 是怎么一步步工作的
两个阶段:先把文档索引一次,之后每个问题都现场检索并作答。每个环节都有各自的失效模式,动手之前值得先摸清楚。
1 · 把问题转成 embedding
「我该怎么重置密码?」变成一个向量——用的就是索引文档时的同一个 embedding 模型。
检索增强生成(RAG)有两个阶段,运行时机完全不同。关于它的困惑,大多来自把这两件事混为一谈。
索引只做一次,提前做,覆盖整个语料库。 检索与生成则每个问题都跑一遍,耗时以毫秒计。
阶段一 · 索引
加载与抽取
把文本从它所在的地方取出来——PDF、HTML、wiki、数据库、代码。
这一环作为故障来源被严重低估。PDF 表格抽取成乱序文本,产生的垃圾会一路流经后面每个环节。检索质量往往要追溯到抽取质量,这一步值得比通常得到的更多关注。
分块
把文档切成可检索的文本块。整篇文档太大,塞不进去;又太散,匹配不上一个具体问题。
分块(chunking)边界直接决定了什么能被检索到。把一个操作步骤拆到两个文本块里,两块就都不含完整的步骤。参见如何为 RAG 做文档分块。
生成 embedding
用 embedding 模型把每个文本块转成向量。语义相近的文本块会落在彼此附近。
有一条规则没有例外:索引和查询必须用同一个 embedding 模型。 不同模型产出的空间互不兼容,而且这种故障是静默的——你会拿到结果,只是那些结果毫无意义。
入库
把向量连同原文和元数据写进向量数据库。向量数据库就是为在几百万个向量里快速找最近邻而生的。
元数据要存得大方些:来源文档、章节、日期、权限、URL。过滤和引用都要用到它,事后再补就得重建索引。
阶段二 · 检索与生成
把查询也转成向量
同一个模型,同一个空间。问题也变成一个向量。
注意由此产生的错配:问题和答案的措辞方式不一样。「我怎么重置密码?」和「密码重置流程:进入设置……」语义相关,形式上却不同。这种不对称是检索漏召回的真实来源,也是查询改写技术要解决的问题。
检索
找出向量与查询向量最接近的文本块,通常用余弦相似度。返回前 k 个——常见取值是 3 到 20。
在生产环境里真正要紧的两项改进:
元数据过滤。 在检索之前或检索过程中按日期、来源或权限做限制。访问控制也是在这一环落实的。
混合检索。 把向量检索和关键词检索结合起来。向量管语义,关键词管精确匹配——错误码、产品 ID、函数名、专有名词。两者谁也覆盖不了谁,而混合检索稳定优于单用任何一种。这是性价比最高的改进之一。
重排序
可选的一步:把候选文本块送进一个更贵的模型,让它直接给每个文本块与查询的相关性打分,而不是比较预先算好的向量。
便宜地召回 50 个,再重排序选出最好的 5 个。这通常是同等投入下质量提升最大的一步。参见为什么你需要重排序模型。
拼装 prompt
把召回的文本块放进上下文,并给出明确的指令:
只依据下面的上下文回答问题。
如果上下文里没有答案,就直说没找到。
每条论断都注明来源。
上下文:
[1] {chunk_1}
[2] {chunk_2}
问题:{question}有三个细节分量很重。**明确允许模型回答「没找到」**能大幅减少编造。给文本块编号让引用在机制上成为可能。顺序也有影响,因为位于上下文中间的内容被可靠使用的程度更低——把最相关的文本块放在开头和结尾。
生成
模型依据提供的文本作答。这是阅读理解,不是凭记忆回忆,整个架构之所以成立,原因就在这里。
哪里会出问题
故障的分布并不均匀,知道往哪儿看能省下大量时间:
| 环节 | 典型故障 |
|---|---|
| 抽取 | 表格和版式被搅成噪声 |
| 分块 | 答案被切分边界截断 |
| Embedding | 查询与文档用了不匹配的模型 |
| 检索 | 前 k 太小;精确词条没有关键词通道 |
| 重排序 | 直接被跳过 |
| prompt | 没给它拒绝的许可,于是开始编 |
| 生成 | 无视上下文,改用参数里的知识 |
先诊断检索,再看生成。 拿一个答错的问题,去看实际召回了什么。如果正确的文本块根本不在召回集合里,改 prompt 也无济于事——而 RAG 的失败大多属于这一类。
要点回顾
- 索引(加载 → 分块 → 生成 embedding → 入库)只跑一次;检索 → 重排序 → 生成每个查询都要跑。
- 两端必须使用同一个 embedding 模型,否则结果会静默地变得毫无意义。
- 混合检索和重排序是给基础管线加成最高的两项。
- 明确允许拒答、给文本块编号、把强相关的放到两端,都能可测量地改善输出。
- 大多数失败都是检索失败——永远先检查召回了什么。
下一篇: 如何为 RAG 做文档分块