RAG 检索增强生成

先查资料,再让模型回答。大模型不知道你的私有文档,也不知道训练截止后的新信息。RAG 在回答前增加一次检索:先找到相关材料,再让模型依据材料作答。

01
用户提出问题“公司的退款规则是什么?”
03
模型依据资料回答答案可以核对,也可以附上出处
RAG 的核心变化:模型回答之前,多了一次检索。

RAG 是什么

RAG(Retrieval-Augmented Generation,检索增强生成)由“检索”和“生成”两个动作组成。

用户提出问题后,系统先从资料库中找出最相关的片段,再把这些片段连同问题一起交给模型组织答案。

闭卷

纯 LLM

只依赖模型训练时记住的内容

  • 知识停留在训练完成时
  • 不知道组织内部的私有资料
  • 没有答案时仍可能自行编造
  • 回答通常不能追溯来源
开卷

RAG

回答前先查找相关材料

  • 资料更新后可以立即参与回答
  • 能够使用内部文档和业务数据
  • 没有依据时可以明确说明不知道
  • 回答可以附上引用片段

什么时候使用 RAG

当模型需要使用训练数据之外的知识时,才需要考虑 RAG。

下面三类需求最常见。如果资料很少、长期不变,直接放进提示词通常更简单。

知识经常变化

产品规则、业务政策和操作手册持续更新。修改资料库后,新内容就能参与回答,不需要重新训练模型。

资料没有参与训练

合同、客服工单和内部文档属于组织自己的数据。模型原本不知道,但可以在回答时即时查阅。

答案需要依据

回答需要能够回到原始材料,方便人工核对。检索结果可以和答案一起返回,说明信息来自哪里。

三种方案怎么选

它们解决的问题不同,也可以组合使用。先根据资料规模和变化频率选择最简单的方案。

方案 更适合 不适合
RAG 大量、经常变化、需要追溯来源的知识 直接改变模型的语气和输出习惯
微调 固定格式、表达风格和特定技能 频繁变化的事实知识
长上下文 资料较少、固定,并且一次能够完整放入 大型资料库和需要精确召回的场景

两个阶段

RAG 由两条独立流程组成:资料新增或更新时进行离线建库;用户提问时进行在线检索与生成。

离线建库

资料变化时重新运行

  1. 原始文档PDF、网页、工单
  2. 切块保持语义完整
  3. 嵌入文字转换为向量
  4. 写入向量库保存原文与元数据

在线问答

每次提问都会运行

  1. 用户问题使用相同模型嵌入
  2. 检索找出相关片段
  3. 重排按需进一步精排
  4. 生成答案资料与问题交给模型

两条流程必须使用同一个嵌入模型。否则文档与问题不在同一个向量空间中,无法正确比较相似度。

五个核心环节

完整的 RAG 通常包含切块、嵌入、检索、重排和生成。先从文档进入系统后的第一步开始。

切块

把长文档切成可检索的小段

整篇文档的颗粒度太粗,也可能无法完整放进提示词。切成几百字一段后,每一块尽量表达一个完整意思;相邻块保留少量重叠,避免语义在边界处断开。

块太大
容易混入多个主题,检索噪声增加。
块太小
上下文不完整,单个片段难以独立理解。
chunk.py
def chunk(text, size=500, overlap=50):
    chunks, i = [], 0
    while i < len(text):
        chunks.append(text[i:i + size])
        i += size - overlap
    return chunks

嵌入

把文字映射到可比较的向量空间

嵌入模型把文字转换成高维向量。含义相近的文字会落在更接近的位置,语义检索也就可以转化为向量之间的距离计算。

关键:文档和问题必须使用同一个嵌入模型。

embed.py
from sentence_transformers import SentenceTransformer

model = SentenceTransformer(
    "all-MiniLM-L6-v2"
)
vecs = model.encode(
    chunks,
    normalize_embeddings=True
)

检索

按相似度取回最相关的片段

问题先使用同一个模型生成向量,再与资料库中的向量比较。按照相似度排序后,取回前 K 个片段交给后续环节。

注意:取回太少容易遗漏,取回太多则会引入噪声。

search.py
def search(query, k=3):
    q = model.encode(
        [query],
        normalize_embeddings=True
    )[0]
    scores = vecs @ q
    top = scores.argsort()[-k:][::-1]
    return [chunks[i] for i in top]

重排

对候选片段进行更精细的排序

向量检索速度快,但排序相对粗略。可以先召回一批候选片段,再让重排模型同时阅读问题和片段,选出其中最相关的几条。

常见做法:先召回 20~50 条,再精排出 3~5 条。

rerank.py
candidates = search(query, k=30)
pairs = [(query, text) for text in candidates]
scores = reranker.predict(pairs)
ranked = sorted(
    zip(scores, candidates),
    reverse=True
)
context = [text for _, text in ranked[:3]]

生成

把资料和问题一起交给模型

最后把检索到的片段和用户问题组合成提示词。应当明确要求模型只依据材料回答,并在材料没有答案时直接说明不知道。

需要可信度时:答案应当同时返回所依据的原始片段或来源。

prompt.py
context = "\n\n".join(retrieve(question))
prompt = f"""
只根据资料回答;资料中没有就说不知道。

资料:{context}
问题:{question}
"""

检索质量

检索质量不是一个单独的配置项,而是检索结果的整体表现。下面三个因素会共同影响它,评测集则负责验证调整是否有效。

影响它的三个因素

块大小

决定检索的基本单位,以及每个片段能否保持完整语义。

取回数量

决定遗漏与噪声之间的平衡,过少会漏,过多会混入无关内容。

检索方式

决定如何寻找候选片段,包括向量检索、关键词检索或两者结合。

检索结果的质量

  • 需要的片段能够被找回
  • 正确片段排在前面
  • 无关片段足够少

评测集

准备一组“问题及其应命中的片段”,检查正确片段是否被找回、排名是否靠前,再据此调整上面的三个因素。

误区与上手

RAG 能让模型使用外部资料,但不会自动解决所有知识与生成问题。先明确它的能力边界,再考虑如何落地。

五个常见误区

RAG 就是把整篇文档交给模型?

不是。关键是先找出最相关的少量片段,再把这些片段放进上下文。资料较多时,不可能也不应该全部塞入。

使用 RAG 后就不会产生幻觉?

仍然可能。检索结果错误、资料本身过时,或者模型没有遵守材料约束,都会导致错误答案。

检索回来的片段越多越好?

不是。片段过多会引入噪声、增加费用并分散模型注意力。取回数量与答案质量并不等价。

RAG 与微调只能选择一种?

两者解决的问题不同,可以配合使用。微调主要改变表达方式和行为习惯,RAG 负责提供外部事实。

问题和文档可以使用不同的嵌入模型?

不可以。它们必须位于同一个向量空间中,向量距离才有比较意义。

如何上手

  1. 1

    先跑通最小链路

    只实现切块、嵌入、检索和生成。先确认完整流程能够工作,不急着加入重排、混合检索等额外组件。

  2. 2

    准备一小批真实问题

    为每个问题标记应该命中的资料片段,用它检查系统是否找到了正确内容,而不是只看最终回答是否流畅。

  3. 3

    先调检索,再调生成

    依次调整块大小、取回数量和检索方式。正确片段能够稳定找回后,再优化提示词和模型回答。