Skip to content

RAG 原理

RAG 是 AI 应用开发里最常见、也最容易被误解的一条主线。很多人第一次接触时,要么觉得它只是"接一个向量库",要么觉得它就是"把资料塞给模型"。都太粗了。

RAG 做的事情是:先从外部资料里找出和当前问题最相关的内容,放进上下文,让模型基于这些资料回答,把依据一起交代出来。不是单个模块,是一条完整链路。

本章目标

  • 能描述一个完整 RAG 系统的建库与查询链路
  • 理解为什么引用来源和"不知道"机制是必须项,而不是加分项
  • 能分辨 RAG 失败到底是检索问题、上下文问题还是生成问题

什么是 RAG

RAG(Retrieval-Augmented Generation,检索增强生成)可以理解为三步:

  1. 先从外部资料中检索与当前问题相关的内容
  2. 再把这些内容放进 prompt,作为模型回答的依据
  3. 最后由模型基于这些资料生成答案

让模型回答前先拿到当前问题相关的资料,而不是只靠训练时留下的记忆。这就是 RAG 和"直接问模型"的差异。

RAG 改变了什么

RAG 看起来只是在回答前多查了一步资料,但它改变的是模型"作答时可见的信息边界"。

不做 RAG 时,模型靠参数里的统计记忆和当前 prompt。做了 RAG 之后,模型在上下文里会多出一组外部证据。它没有突然学会新知识,只是这一轮推理里多拿到了一批可引用的材料。

RAG 和微调不是一回事:

  • 微调是把某些模式烘焙进参数
  • RAG 是在推理时临时注入证据

一个偏长期改模型,一个偏实时改上下文。分清这个差异,后面很多选型就不容易纠结。

为什么需要 RAG

模型本身有几个天然限制:

  • 训练数据有截止日期,不能稳定回答最新信息
  • 不知道你的企业内部文档、私有知识库
  • 容易在没有来源约束时凭空发挥
  • 不适合直接当事实数据库使用

RAG 主要解决的是让回答更有依据:

  • 基于私有资料回答(内部文档、产品手册、规章制度)
  • 基于最新资料回答(近期新闻、实时更新的知识库)
  • 降低事实性幻觉(答案有资料出处可以核验)
  • 给答案提供来源依据(用户可以追溯原始文档)

一个最小场景:三份文档,一次知识库问答

光讲定义容易飘。用一个很小的场景把整条链路串起来看。

假设你上传了三份文档:产品使用说明、退款政策、常见问题 FAQ。用户提问:"申请退款需要多少天?"

建库阶段(一次性完成,之后不用重复):

  1. 导入三份文档,做文本清洗(去页眉页脚、去空白行)
  2. 按策略把每份文档切成若干 chunk(比如每段 300 字,相邻重叠 50 字)
  3. 为每个 chunk 调用 Embedding 模型,生成向量
  4. 把所有 chunk 和对应向量存入向量库

问答阶段(每次提问都走这个流程):

  1. 用户提问:"申请退款需要多少天?"
  2. 对问题也生成 embedding
  3. 在向量库里找最相近的 3 条 chunk(top-3)
  4. 召回:退款政策里有一段"7 个工作日内原路退回",FAQ 里有一段相关说明
  5. 把这两段拼进 prompt,要求模型"只基于以下资料回答"
  6. 模型生成回答,并标注来源:"根据退款政策第 2 节……"
  7. 返回答案和引用片段

如果没有命中:

用户问:"你们有没有 VIP 专属客服热线?",而向量库里三份文档都没有相关内容,召回的 chunk 相似度也很低。此时正确做法是回复"资料中未找到相关内容,请联系人工客服",而不是让模型强行推测。

这一步很关键。RAG 的目标不是"尽量答出来",而是"有依据就答,没依据就说没有"。

一个完整 RAG 系统的链路

离线建库(Indexing Pipeline)

  1. 文档导入
  2. 文本清洗
  3. 按策略切块(chunk)
  4. 生成 embedding
  5. 存入检索系统(向量库,或向量 + 关键词混合)

在线问答(Query Pipeline)

  1. 用户提问
  2. 问题转成检索请求(生成问题的 embedding)
  3. 召回相关 chunk
  4. 可选:rerank 二次排序
  5. 选出最相关内容,拼接到 prompt
  6. 调模型生成回答
  7. 返回答案和引用

这条链路里有一点常被忽略:离线建库和在线问答的优化目标不一样。

建库关心覆盖率和可检索性——资料切得合理、存得稳定、后面能找得到。问答关心的是当前问题的证据质量——在有限上下文里,尽快拿到最值得给模型看的那几段。

很多 RAG 系统后面难调,就是因为这两个目标从一开始就没分清。建库时只想着"先存进去再说",问答时才发现切块粒度、元数据、过滤字段全都不够用。

RAG 不等于"喂全文"

初学者最常见的误解之一。既然有资料,直接整份塞给模型不就好了?不行,几个现实障碍绕不过去:

  • 上下文窗口不够(长文档直接超出模型限制)
  • token 成本过高(每次问答都要把整份文档送一遍)
  • 无关内容太多,模型注意力被稀释
  • 命中质量无法评测,出问题时无从排查

RAG 的思路反过来:不求把所有资料都给模型,只给"当前问题最需要的那几段"。降低成本,系统也更可控,出了问题知道该查哪一层。

换个说法,RAG 只给模型当前这一题需要的证据窗口。窗口小,回答更容易被约束在证据上;窗口过大,模型会掉回"看了很多,抓不住重点"的状态。

引用来源为什么重要

能落地的 RAG 系统,应该告诉用户:回答依据哪份文档、来自哪一页或哪一段、支撑回答的原始片段是什么。

引用的价值很实际:

  • 让用户能核验,而不是盲信
  • 出问题时能追溯,是检索错了还是模型理解错了
  • 尤其在企业内部知识库场景里,没有引用几乎没法维护

"不知道"机制为什么重要

RAG 不是万能命中系统。资料可能根本没命中,命中了但片段太碎凑不出答案,或者资料本身就不完整。

碰到这种情况,正确做法是明确说资料不足,让用户知道该补充什么或换个问法。别让模型用"我猜"的方式填补空白。

拒答机制不是"最后兜底",它是系统可信度的一部分。一个总想硬答的知识库,短期看起来聪明,长期只会让人越来越不敢信。

为什么 RAG 能降低幻觉,但不能消灭幻觉

很多介绍把 RAG 讲得过于理想,好像接了检索幻觉就没了。没那么简单。

RAG 确实降低幻觉——模型不再只靠参数记忆补全,多了外部证据。但它消灭不了幻觉,原因有三层:

  1. 检索可能本身就错了,模型拿到的是不对的证据
  2. 检索拿到的证据虽然相关,但并不足以支撑完整回答
  3. 模型即使拿到了证据,仍可能过度总结、错误归纳,或者把证据之外的常识混进来

RAG 的目标不是"绝不幻觉",而是把幻觉从不可追溯变成可定位、可约束、可改进。

RAG 常见失败点

检索失败

没召回正确资料,后面的生成再强也没用。这通常是 chunk 策略不对、embedding 模型选型不匹配,或者文档没清洗干净。

召回了,但不够关键

结果"沾边",却回答不准。这时要看是 top-k 太小,还是需要 rerank 进一步筛选。

上下文拼装不合理

塞了太多无关内容,导致模型注意力被稀释,给出的答案泛泛而谈而不是基于资料直接回答。

模型仍然超出资料发挥

说明 prompt 约束不足,或者引用与回答没有绑定好。模型在没有足够依据时应该说不知道,而不是靠推理填补。

排查时如果老想先改 prompt,先停一下。RAG 的故障里,大部分不是生成层的问题。资料没找对、片段没拼好、召回太噪——靠 prompt 救不回来。

检索和生成在优化两个不同目标

检索层追求"别漏掉关键证据",是 recall 导向。宁可先多拿几段,再慢慢筛。生成层追求"用尽量少、尽量准的材料给出可信回答",是 precision 导向。材料一多,反而会犹豫、分散、总结过头。

这里有一个经典张力:

  • top-k 开太小,证据可能不够
  • top-k 开太大,噪音会压住关键信息

单调某个参数解决不了这个问题。检索目标和生成目标天然不同,RAG 设计的难点就在于怎么把这两个目标接起来。

如何提升 RAG 效果

提升效果时,按这个顺序来,别一上来就调模型或堆花活:

  1. 先修检索:清洗文档、优化 chunk 策略、调整 top-k
  2. 再看召回质量:加 rerank,观察每次问答命中了什么
  3. 最后收紧生成约束:严格要求回答基于资料,没资料就说不知道
  4. 分层评测:检索层(召回了正确内容吗?)和生成层(基于资料回答对了吗?)分开评,不要混在一起

RAG 和 Fine-tuning 的区别

这两者经常被混淆,但解决的问题不同:

RAGFine-tuning
改模型参数?
适合什么场景动态外部知识、私有资料问答学风格、专门任务、固定行为偏好
可追溯性高(可看到引用资料)低(知识烘焙进参数里)
成本按检索和生成付费训练成本高

当前阶段优先掌握 RAG。Fine-tuning 是后续独立话题,等分清 Prompt、RAG 和评测各自的边界之后再进入更合适。

如果你想继续理解微调到底适合什么、不适合什么,可以继续阅读 模型微调与定制化

一个最小可运行 RAG 示例

把建库和问答流程用代码串起来。前面的描述不再只是流程图,直接看骨架:

python
import chromadb
from openai import OpenAI

openai_client = OpenAI()
chroma_client = chromadb.Client()
collection = chroma_client.create_collection("faq_kb")

# ---- 建库阶段(离线运行一次)----

documents = [
    {"id": "refund_1", "text": "退款申请需在购买后 7 个工作日内提交,逾期不予受理。申请通过后原路退回,3-5 个工作日到账。"},
    {"id": "refund_2", "text": "数字商品(激活码、会员)一旦激活不支持退款。"},
    {"id": "vip_1", "text": "VIP 用户享有专属客服通道,工作日 9:00-18:00 在线。"},
]

def get_embedding(text: str) -> list[float]:
    response = openai_client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return response.data[0].embedding

for doc in documents:
    embedding = get_embedding(doc["text"])
    collection.add(
        ids=[doc["id"]],
        embeddings=[embedding],
        documents=[doc["text"]]
    )

# ---- 问答阶段(每次提问都走这个流程)----

SYSTEM_PROMPT = """你是一个客服问答助手。

规则:
1. 只根据提供的参考资料回答,不要在资料之外发挥
2. 如果资料不包含答案,明确说"根据现有资料无法回答这个问题"
3. 回答后注明信息来源(哪条参考资料)"""

def rag_query(question: str, top_k: int = 3) -> dict:
    # 检索
    query_embedding = get_embedding(question)
    results = collection.query(query_embeddings=[query_embedding], n_results=top_k)
    retrieved_chunks = results["documents"][0]
    retrieved_ids = results["ids"][0]

    # 如果最高相似度太低,直接拒答
    distances = results.get("distances", [[]])[0]
    if distances and distances[0] > 1.5:  # 距离越大越不相关
        return {"answer": "根据现有资料无法回答这个问题。", "sources": []}

    # 拼接上下文
    context = "\n".join([
        f"[来源 {i+1}] {chunk}"
        for i, chunk in enumerate(retrieved_chunks)
    ])

    # 生成回答
    response = openai_client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": f"参考资料:\n{context}\n\n问题:{question}"}
        ]
    )

    return {
        "answer": response.choices[0].message.content,
        "sources": retrieved_ids
    }

# 测试
result = rag_query("申请退款需要多少天?")
print(result["answer"])
# 退款申请需在购买后 7 个工作日内提交……(来源 1)
print(result["sources"])
# ['refund_1']

示例很小,结构是完整的:建库、检索、上下文拼装、生成、引用来源都有。真实系统就是在这个骨架上继续扩展——加文档清洗、加 rerank、加更复杂的拒答判断、加缓存和评测。

RAG 失败的排查路径

出问题时最容易犯的错误就是去调 prompt。但 RAG 的问题大多出在检索层,prompt 改不动。

排查按这个顺序来:

第一步:检查召回内容

把每次查询召回的 chunk 打印出来,看一眼:

  • 召回的内容和问题有没有语义关联?
  • 正确答案在 top-k 里面吗?
  • 如果正确答案不在里面,是文档里根本没有,还是切 chunk 时把关键句子切断了?

大部分问题在这一步就能发现。看一眼召回片段,通常就知道答案为什么歪。

第二步:检查 chunk 质量

把几个召回的 chunk 逐条看:

  • 每条 chunk 是不是一个完整的意思单元?
  • 有没有被切断的句子(上文在上一个 chunk,下文在下一个 chunk)?
  • 文档有没有清洗干净(页眉页脚、目录条目有没有被当作正文 chunk)?

第三步:检查相似度分数

如果向量库返回距离分数,把它打出来。距离过高(即相似度过低)的召回结果,模型拿到也没用,不如直接触发拒答。

第四步:才是调整 prompt

只有前三步都没问题,但生成结果仍然不好时,才考虑调整 prompt,比如加强"不要在资料之外发挥"的约束,或者要求更具体的引用格式。

上线前的几个坑

RAG 做到能上线,几件事容易被低估。

检索是独立系统,不是模型的附属品。回答质量出问题时,别急着调 prompt,先看召回结果。

引用不是展示层的锦上添花。它是团队排障的抓手,也是用户信任的来源。内部知识库场景里,没有引用的答案几乎没法维护。

建库阶段别当一次性脏活处理。今天怎么切块、怎么存元数据,直接决定了后面能不能做过滤、重排和引用回溯。很多系统后期调不动,根源在建库。

拒答能力不是减分项。一个系统敢说"不知道",说明它的边界是清晰的,清晰的边界才有迭代空间。

对应项目

本章最直接的落地练习是 RAG 知识库

  • 支持上传文档并完成建库
  • 支持知识库问答并展示引用来源
  • 支持资料不足时明确拒答
  • 支持排查命中片段

从 Claude Code 看极简确定性记忆

理解了 RAG 的检索增强逻辑之后,可以用 Claude Code 的 CLAUDE.md 机制来做一个对比。

CLAUDE.md 是 Claude Code 的持久化记忆文件,每次对话开始时被整体注入系统提示词。可以把它理解为"极简确定性 RAG":

维度RAGCLAUDE.md
检索方式向量语义检索,按相关性选 chunk全量注入,不检索不筛选
覆盖范围可以管理数万篇文档适合几 KB 到几十 KB 的精华规则
内容性质动态外部知识,问什么查什么静态背景规则,次次都注入
确定性检索结果可能有偏差写什么就注入什么,不会漏

Claude Code 选 CLAUDE.md 而不是向量检索,是有意识的工程取舍(来自 Claude Code v2.1.88 utils/claudemd.ts):项目规范、编码约定、禁止事项这些内容不多,但每次对话都必须稳定带上,不能因为检索偏差而漏掉。全量注入比检索更合适。

反过来,文档库有几百篇产品手册、每次问答只需要其中几段的场景,RAG 检索就比全量注入合理得多。几百篇文档塞不进系统提示词。

规模小、每次都必须知道的内容,适合全量注入;规模大、按需取用的内容,适合 RAG。没有通用最优方案,看内容规模和确定性要求选。

常见面试考点

读完这一章后,面试里最常被追问的不是“RAG 是什么”这一句定义,而是你能不能把链路拆开、定位问题:

  1. 完整流程:能不能从文档导入、清洗、分块、Embedding、索引、召回、上下文拼装一直讲到最终生成和引用。
  2. 分块策略:为什么需要 chunk,chunk 太大或太小分别会带来什么问题,overlap 起什么作用。
  3. 混合检索:向量检索和关键词检索各自解决什么问题,为什么技术文档、产品名、API 参数这类场景常需要混合检索。
  4. Rerank:为什么 top-k 召回后还要二次排序,Rerank 适合解决“召回到了但排序不准”的问题,而不是替代检索。
  5. 调优与评估:RAG 效果不好时,应该先看召回内容和 Recall@k,再判断是检索层、上下文拼装层还是生成层的问题。
  6. 拒答与引用:为什么“资料不足就说不知道”和返回来源不是展示细节,而是 RAG 系统可信度的一部分。

下一章:Agent 基础原理——RAG 解决的是先找资料再回答,Agent 要处理的是分步做事、再决定下一步干什么。

面向开发者的 AI 实战路线——Vibe Coding 与 AI 应用开发