Appearance
RAG 原理
RAG 是 AI 应用开发里最常见、也最容易被误解的一条主线。很多人第一次接触时,要么觉得它只是"接一个向量库",要么觉得它就是"把资料塞给模型"。都太粗了。
RAG 做的事情是:先从外部资料里找出和当前问题最相关的内容,放进上下文,让模型基于这些资料回答,把依据一起交代出来。不是单个模块,是一条完整链路。
本章目标
- 能描述一个完整 RAG 系统的建库与查询链路
- 理解为什么引用来源和"不知道"机制是必须项,而不是加分项
- 能分辨 RAG 失败到底是检索问题、上下文问题还是生成问题
什么是 RAG
RAG(Retrieval-Augmented Generation,检索增强生成)可以理解为三步:
- 先从外部资料中检索与当前问题相关的内容
- 再把这些内容放进 prompt,作为模型回答的依据
- 最后由模型基于这些资料生成答案
让模型回答前先拿到当前问题相关的资料,而不是只靠训练时留下的记忆。这就是 RAG 和"直接问模型"的差异。
RAG 改变了什么
RAG 看起来只是在回答前多查了一步资料,但它改变的是模型"作答时可见的信息边界"。
不做 RAG 时,模型靠参数里的统计记忆和当前 prompt。做了 RAG 之后,模型在上下文里会多出一组外部证据。它没有突然学会新知识,只是这一轮推理里多拿到了一批可引用的材料。
RAG 和微调不是一回事:
- 微调是把某些模式烘焙进参数
- RAG 是在推理时临时注入证据
一个偏长期改模型,一个偏实时改上下文。分清这个差异,后面很多选型就不容易纠结。
为什么需要 RAG
模型本身有几个天然限制:
- 训练数据有截止日期,不能稳定回答最新信息
- 不知道你的企业内部文档、私有知识库
- 容易在没有来源约束时凭空发挥
- 不适合直接当事实数据库使用
RAG 主要解决的是让回答更有依据:
- 基于私有资料回答(内部文档、产品手册、规章制度)
- 基于最新资料回答(近期新闻、实时更新的知识库)
- 降低事实性幻觉(答案有资料出处可以核验)
- 给答案提供来源依据(用户可以追溯原始文档)
一个最小场景:三份文档,一次知识库问答
光讲定义容易飘。用一个很小的场景把整条链路串起来看。
假设你上传了三份文档:产品使用说明、退款政策、常见问题 FAQ。用户提问:"申请退款需要多少天?"
建库阶段(一次性完成,之后不用重复):
- 导入三份文档,做文本清洗(去页眉页脚、去空白行)
- 按策略把每份文档切成若干 chunk(比如每段 300 字,相邻重叠 50 字)
- 为每个 chunk 调用 Embedding 模型,生成向量
- 把所有 chunk 和对应向量存入向量库
问答阶段(每次提问都走这个流程):
- 用户提问:"申请退款需要多少天?"
- 对问题也生成 embedding
- 在向量库里找最相近的 3 条 chunk(top-3)
- 召回:退款政策里有一段"7 个工作日内原路退回",FAQ 里有一段相关说明
- 把这两段拼进 prompt,要求模型"只基于以下资料回答"
- 模型生成回答,并标注来源:"根据退款政策第 2 节……"
- 返回答案和引用片段
如果没有命中:
用户问:"你们有没有 VIP 专属客服热线?",而向量库里三份文档都没有相关内容,召回的 chunk 相似度也很低。此时正确做法是回复"资料中未找到相关内容,请联系人工客服",而不是让模型强行推测。
这一步很关键。RAG 的目标不是"尽量答出来",而是"有依据就答,没依据就说没有"。
一个完整 RAG 系统的链路
离线建库(Indexing Pipeline)
- 文档导入
- 文本清洗
- 按策略切块(chunk)
- 生成 embedding
- 存入检索系统(向量库,或向量 + 关键词混合)
在线问答(Query Pipeline)
- 用户提问
- 问题转成检索请求(生成问题的 embedding)
- 召回相关 chunk
- 可选:rerank 二次排序
- 选出最相关内容,拼接到 prompt
- 调模型生成回答
- 返回答案和引用
这条链路里有一点常被忽略:离线建库和在线问答的优化目标不一样。
建库关心覆盖率和可检索性——资料切得合理、存得稳定、后面能找得到。问答关心的是当前问题的证据质量——在有限上下文里,尽快拿到最值得给模型看的那几段。
很多 RAG 系统后面难调,就是因为这两个目标从一开始就没分清。建库时只想着"先存进去再说",问答时才发现切块粒度、元数据、过滤字段全都不够用。
RAG 不等于"喂全文"
初学者最常见的误解之一。既然有资料,直接整份塞给模型不就好了?不行,几个现实障碍绕不过去:
- 上下文窗口不够(长文档直接超出模型限制)
- token 成本过高(每次问答都要把整份文档送一遍)
- 无关内容太多,模型注意力被稀释
- 命中质量无法评测,出问题时无从排查
RAG 的思路反过来:不求把所有资料都给模型,只给"当前问题最需要的那几段"。降低成本,系统也更可控,出了问题知道该查哪一层。
换个说法,RAG 只给模型当前这一题需要的证据窗口。窗口小,回答更容易被约束在证据上;窗口过大,模型会掉回"看了很多,抓不住重点"的状态。
引用来源为什么重要
能落地的 RAG 系统,应该告诉用户:回答依据哪份文档、来自哪一页或哪一段、支撑回答的原始片段是什么。
引用的价值很实际:
- 让用户能核验,而不是盲信
- 出问题时能追溯,是检索错了还是模型理解错了
- 尤其在企业内部知识库场景里,没有引用几乎没法维护
"不知道"机制为什么重要
RAG 不是万能命中系统。资料可能根本没命中,命中了但片段太碎凑不出答案,或者资料本身就不完整。
碰到这种情况,正确做法是明确说资料不足,让用户知道该补充什么或换个问法。别让模型用"我猜"的方式填补空白。
拒答机制不是"最后兜底",它是系统可信度的一部分。一个总想硬答的知识库,短期看起来聪明,长期只会让人越来越不敢信。
为什么 RAG 能降低幻觉,但不能消灭幻觉
很多介绍把 RAG 讲得过于理想,好像接了检索幻觉就没了。没那么简单。
RAG 确实降低幻觉——模型不再只靠参数记忆补全,多了外部证据。但它消灭不了幻觉,原因有三层:
- 检索可能本身就错了,模型拿到的是不对的证据
- 检索拿到的证据虽然相关,但并不足以支撑完整回答
- 模型即使拿到了证据,仍可能过度总结、错误归纳,或者把证据之外的常识混进来
RAG 的目标不是"绝不幻觉",而是把幻觉从不可追溯变成可定位、可约束、可改进。
RAG 常见失败点
检索失败
没召回正确资料,后面的生成再强也没用。这通常是 chunk 策略不对、embedding 模型选型不匹配,或者文档没清洗干净。
召回了,但不够关键
结果"沾边",却回答不准。这时要看是 top-k 太小,还是需要 rerank 进一步筛选。
上下文拼装不合理
塞了太多无关内容,导致模型注意力被稀释,给出的答案泛泛而谈而不是基于资料直接回答。
模型仍然超出资料发挥
说明 prompt 约束不足,或者引用与回答没有绑定好。模型在没有足够依据时应该说不知道,而不是靠推理填补。
排查时如果老想先改 prompt,先停一下。RAG 的故障里,大部分不是生成层的问题。资料没找对、片段没拼好、召回太噪——靠 prompt 救不回来。
检索和生成在优化两个不同目标
检索层追求"别漏掉关键证据",是 recall 导向。宁可先多拿几段,再慢慢筛。生成层追求"用尽量少、尽量准的材料给出可信回答",是 precision 导向。材料一多,反而会犹豫、分散、总结过头。
这里有一个经典张力:
- top-k 开太小,证据可能不够
- top-k 开太大,噪音会压住关键信息
单调某个参数解决不了这个问题。检索目标和生成目标天然不同,RAG 设计的难点就在于怎么把这两个目标接起来。
如何提升 RAG 效果
提升效果时,按这个顺序来,别一上来就调模型或堆花活:
- 先修检索:清洗文档、优化 chunk 策略、调整 top-k
- 再看召回质量:加 rerank,观察每次问答命中了什么
- 最后收紧生成约束:严格要求回答基于资料,没资料就说不知道
- 分层评测:检索层(召回了正确内容吗?)和生成层(基于资料回答对了吗?)分开评,不要混在一起
RAG 和 Fine-tuning 的区别
这两者经常被混淆,但解决的问题不同:
| RAG | Fine-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":
| 维度 | RAG | CLAUDE.md |
|---|---|---|
| 检索方式 | 向量语义检索,按相关性选 chunk | 全量注入,不检索不筛选 |
| 覆盖范围 | 可以管理数万篇文档 | 适合几 KB 到几十 KB 的精华规则 |
| 内容性质 | 动态外部知识,问什么查什么 | 静态背景规则,次次都注入 |
| 确定性 | 检索结果可能有偏差 | 写什么就注入什么,不会漏 |
Claude Code 选 CLAUDE.md 而不是向量检索,是有意识的工程取舍(来自 Claude Code v2.1.88 utils/claudemd.ts):项目规范、编码约定、禁止事项这些内容不多,但每次对话都必须稳定带上,不能因为检索偏差而漏掉。全量注入比检索更合适。
反过来,文档库有几百篇产品手册、每次问答只需要其中几段的场景,RAG 检索就比全量注入合理得多。几百篇文档塞不进系统提示词。
规模小、每次都必须知道的内容,适合全量注入;规模大、按需取用的内容,适合 RAG。没有通用最优方案,看内容规模和确定性要求选。
常见面试考点
读完这一章后,面试里最常被追问的不是“RAG 是什么”这一句定义,而是你能不能把链路拆开、定位问题:
- 完整流程:能不能从文档导入、清洗、分块、Embedding、索引、召回、上下文拼装一直讲到最终生成和引用。
- 分块策略:为什么需要 chunk,chunk 太大或太小分别会带来什么问题,overlap 起什么作用。
- 混合检索:向量检索和关键词检索各自解决什么问题,为什么技术文档、产品名、API 参数这类场景常需要混合检索。
- Rerank:为什么 top-k 召回后还要二次排序,Rerank 适合解决“召回到了但排序不准”的问题,而不是替代检索。
- 调优与评估:RAG 效果不好时,应该先看召回内容和 Recall@k,再判断是检索层、上下文拼装层还是生成层的问题。
- 拒答与引用:为什么“资料不足就说不知道”和返回来源不是展示细节,而是 RAG 系统可信度的一部分。
下一章:Agent 基础原理——RAG 解决的是先找资料再回答,Agent 要处理的是分步做事、再决定下一步干什么。