RAG 基础:给模型一场开卷考试
建立「RAG 是开卷考试」的心智模型:从「模型不知道你的私有文档」出发,走通检索、增强、生成三步,掌握加载、切块、嵌入、存储的入库管线与切块的艺术,理解查询管线、引用来源、RAG 与长上下文及微调的决策表,以及朴素 RAG 的三大失败模式,含三十行最小 RAG 动手实验。
前置知识
- 读过 结构化输出:知道怎么让模型的输出可解析、可校验——本文的查询管线要把检索结果可靠地拼进提示词,靠的是同样的纪律。
检索一步会用到向量检索,嵌入与向量检索 讲过完整原理。没读过也可以,本文会用一节把需要的部分(查询过嵌入模型、比方向、取 TopK)带上。
学习目标
读完本文你将能够:
- 向别人解释为什么「重训模型」和「全部塞进上下文」都不是让模型了解私有知识的好办法;
- 画出 RAG 的开卷考试心智模型,说清检索、增强、生成三步各自在做什么;
- 说出入库管线(加载、切块、嵌入、存储)四个环节的职责,并解释固定长度切、按结构切、带重叠切三种切块策略的取舍;
- 分析 chunk 大小的权衡:太小丢上下文,太大稀释相关性;
- 写出查询管线的完整流程:查询嵌入、TopK 检索、带「只依据资料回答」约束的提示拼装、生成;
- 识别朴素 RAG 的三大失败模式(检索不到、检索到了但没用、拼接破坏),并说出各自的初步对策。
预计 30 到 45 分钟,含 1 个动手实验与 4 道练习。
1. 问题引入:模型不知道你公司的事
一个再日常不过的需求:给公司内部文档建一个问答助手。员工问「年假怎么申请」,你把问题发给模型,得到一段流畅、自信、完全编造的回答——模型的训练数据里从来没有你们公司的考勤制度。
怎么让模型「知道」这些?三条路摆在面前,各有各的死穴:
路一:重新训练。 把私有文档混进训练数据再训一轮。先不说成本是天文数字,更致命的是知识更新:制度下周一改版,你要再训一轮吗?训练解决的是「能力」,不是「事实」,而问答助手缺的是事实。
路二:全部塞进上下文窗口。 把文档全量贴在提示词里。文档少了可行,但库一大就撞墙:几百万字远超上下文窗口;即便塞得下,按 token 计费的价格让每次问答都像发了一本小说,而且文档越长,模型对中部内容的注意力越差,「读了但没看见」的问题越明显。
路三:检索增强生成(RAG,Retrieval-Augmented Generation)。 不把整个库给模型,而是每次提问时,先从库里找出最相关的几段,把这几段连同问题一起交给模型。模型永远只读「此刻最相关的几千字」。
路三就是 RAG,它有一个极好的类比。
2. 心智模型:开卷考试
闭卷考试靠记忆(模型参数里的知识),记忆既装不下你的私有文档,也无法随时更新。开卷考试靠翻书:题目不会做,先翻到相关章节,看着书作答。
RAG 就是给模型安排一场开卷考试,监考流程固定三步:
flowchart LR
Q["用户问题"] --> R["检索:从文档库找出最相关的 K 个片段"]
R --> A["增强:把片段拼进提示词"]
A --> G["生成:模型看着资料回答"]
G --> AN["带引用的答案"]- 检索(Retrieve):用上一章的向量检索,从库里找出与问题最相关的 K 个文档片段;
- 增强(Augment):把片段、问题、行为约束拼装成一条提示词;
- 生成(Generate):模型在这条「资料齐全」的提示词上作答。
这个拆分揭示了 RAG 的两个工程本质:知识在库不在模型(更新知识等于更新库,模型一个参数都不用动),答案可溯源于片段(模型说了什么不重要,它引用了哪段资料才重要)。这两点分别是路一、路二做不到的。
RAG 系统因此拆成两条独立的管线:入库管线(离线,把文档变成可检索的向量)与查询管线(在线,每次提问走一遍)。下面两节各走一遍。
3. 入库管线:加载、切块、嵌入、存储
flowchart LR
L["加载:PDF / 网页 / 数据库"] --> C["切块:整篇拆成小片段"]
C --> E["嵌入:每块变成一个向量"]
E --> S["存储:向量 + 原文 + 元数据"]加载:把各种格式的原始文档(PDF、Word、网页、Markdown、数据库记录)读成纯文本,保留标题等结构信息与来源元数据。格式越复杂,这一步坑越多——PDF 的多栏排版、表格、页眉页脚是常见的脏数据来源。
切块(chunking):嵌入模型对输入长度有限制,更根本的是检索粒度问题——用户问的是「年假几天」,你不想检索回来整本员工手册。要把长文档切成合适大小的片段。切块是入库管线里最影响最终效果的一步,三种基本切法:
| 切法 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 固定长度切 | 每 N 个 token 一刀 | 实现十行代码,粒度均匀 | 一刀拦腰斩断语义:表格被劈开、句子的主语留在上一块 |
| 按结构切 | 按标题、段落、章节切 | 块边界与语义边界天然对齐 | 依赖文档质量;遇到没有结构的纯文本就退化 |
| 带重叠切 | 相邻块共享一段重复内容(overlap) | 跨界的句子在两块里都完整出现,防边界丢失 | 存储翻倍;同一内容可能被召回两次 |
三者通常组合使用:先按结构切,没有结构的部分退化成固定长度切,再统一加上 10% 到 20% 的重叠。实践中的顺序是:能用标题段落切就按结构切,退而求其次才动刀硬切。
chunk 大小的权衡是切块的灵魂,往两头走都会出问题:
- 太小(比如一两句话):一块只承载一个孤立片段,缺主语、缺上下文,「它适用于哪些岗位」这句话单独成块后谁也看不懂——检索时它跟任何问题都像又都不像,嵌入质量也差;
- 太大(比如几千字一块):一块里混了几十个话题,嵌入向量是整块内容的「平均方向」,任何具体问题与它的相似度都被稀释——同时 TopK 检索回来的几块巨石会迅速吃掉提示词预算。
经验起点在几百 token 上下,但正确答案取决于你的文档:条例式文档按条款切得细,叙事式文档切得粗。用真实问题测召回效果来调,不要抄别人的数字。
嵌入与存储:每块文本过嵌入模型变成向量(原理见 嵌入与向量检索),连同原文、来源、标题、章节路径一起存进向量库。存原文与元数据不是可选项——检索回来的东西要能拼提示词、要能标注引用,全靠它们。
4. 查询管线:从问题到答案
用户问题进来,四步:
- 查询嵌入:问题过同一个嵌入模型,变成向量(「同一个模型」是硬约束,理由在嵌入篇第 2 节);
- TopK 检索:在向量库里找方向最接近的 K 个块,K 常取 3 到 10——太少容易漏,太多稀释注意力还费 token;
- 拼装提示:把检索到的块、用户问题、行为约束组装成一条提示词;
- 生成:模型看着资料作答。
第 3 步的拼装模板值得原样拿走,因为约束句不是礼貌用语,是效果的一部分:
请仅依据下面的参考资料回答用户问题。
如果资料不足以回答,直接说「根据现有资料无法回答」,不要猜测。
回答时标注所依据资料的编号。
[资料 1] {块 1 的内容}
[资料 2] {块 2 的内容}
用户问题:{问题}「只依据资料回答」这条约束对抗的是模型的默认习惯——用流畅的编造补齐资料的空白。开卷考试里学生最常犯的错误也是这个:书上的没找到,凭印象写了一堆。约束句、拒答出口(「说无法回答」)、引用要求三者配合,把编造压到最低。完整的防编造评估与更多约束设计,在 进阶 RAG 展开。
5. 引用来源:让每个答案可核查
提示模板里的「标注资料编号」值得单独一节,因为引用是 RAG 区别于「更聪明的聊天机器人」的分水岭。
引用的做法很直接:每个块入库时带上来源元数据(文件名、章节、页码),拼提示词时给每块编号,要求答案里标注「[资料 2]」这样的记号,界面层再把记号渲染成可点击的原文位置。
它买回三样东西:
- 用户信任:答案能点回原文核查,用户才敢用;「模型说的」与「资料说的」被区分开了;
- 调试能力:答案错了,先看引用——引用到了错的块,是检索问题;引用的块没错而答案错了,是生成问题。没有引用,RAG 出错时你连该修哪条管线都不知道;
- 压制编造:要求逐条标注来源,等于逼模型「每句话出示证据」,凭空编造的成本被抬高。
要让引用可核查,约束输出格式是前提——这正是 结构化输出 的用武之地:答案 + 引用编号列表走 schema,下游才能稳定渲染。
6. 决策表:RAG、长上下文还是微调
三种「让模型表现更好」的手段不是竞争关系,是分工关系。判断依据四个:知识多久变一次、要不要溯源、成本预算、要改的是知识还是行为。
| RAG | 长上下文 | 微调 | |
|---|---|---|---|
| 本质 | 开卷考试:按需检索 | 把书整本带上考场 | 重塑考生本身 |
| 适合的知识 | 频繁更新的私有文档、事实型知识 | 单次任务所需的少量固定材料 | 写作风格、输出格式、领域行话 |
| 知识更新 | 更新库即可,分钟级 | 每次都重新贴,量大则贵 | 要重新训练,天级起步 |
| 可溯源性 | 有(引用到块) | 有(就在上下文里) | 无,知识融进参数 |
| 单次成本 | 低(只带相关片段) | 随文档量线性上涨 | 推理时不额外增加 |
| 典型场景 | 企业文档问答、客服知识库 | 单篇长文的深度分析 | 让输出稳定符合业务格式 |
两条常见误用值得点名:用微调灌输事实(该 RAG)——事实会过期,参数里的知识过期就是事故;用 RAG 改风格(该微调或提示词)——检索解决不了「答得不像我们公司的话」。三者常常组合:微调管行为,RAG 管事实,长上下文兜小材料。
7. 朴素 RAG 的三大失败模式
按上面搭出来的系统叫朴素 RAG(naive RAG),演示效果通常惊艳,真实文档一灌就开始翻车。翻车方式高度集中为三种,逐个认识它们的症状与初步对策——系统化的解法是下一篇的主题。
失败一:检索不到。 症状:明明库里有一年假制度,问「年假怎么申请」,TopK 里没有它。最常见的原因是词不匹配:用户问「年假」,文档写「带薪休假」;用户问「报销」,文档写「费用申报」。这正是嵌入篇第 8 节的老朋友——语义检索的盲区,也可能是切块太碎、这块的嵌入方向被稀释。初步对策:确认切块粒度;补同义词;上混合检索(见 090 篇)。
失败二:检索到了但没用。 症状:TopK 里确实有相关块,但排在后面的高相关块被排在前面的「看起来像」的块挤掉了——比如问「年假几天」,检索回来五块都在讲「考勤打卡」,制度块排第六没进 TopK。这是排序质量问题:向量相似度把「相关」排上来了,但区分度不够。初步对策:调大 K 再让模型自己甄别;加排序层(rerank,见 090 篇)。
失败三:拼接破坏。 症状:检索都成功了,答案却自相矛盾或张冠李戴。典型情形:库里存着新旧两个版本的制度(2025 版与 2026 版),两块都被召回、都拼进了提示词,模型看到矛盾资料后随机偏向其一,或干脆各取一半缝在一起。这是资料间冲突问题,提示词救不了。初步对策:元数据带时间与版本,检索时过滤过期版本;答案要求标注引用版本,让矛盾暴露出来。
三者共同的排查入口是先看检索结果再怪模型:把 TopK 的块原文打印出来,人工判断「该召回的召回了没有、排前了没有、拼起来矛盾没有」。这个习惯会陪你整个 RAG 工程生涯。
8. 常见困惑
问:有了 RAG,是不是就不需要提示词工程了? 不是。RAG 只解决「资料从哪来」,提示词决定「资料怎么用」。同样五块资料,没有「只依据资料回答」的约束,模型照样自由发挥。检索与提示是乘法关系,任何一项为零,结果为零。
问:K 取多大合适?越大越好吗? 不是。K 大了召回更全,但三样代价同步上涨:token 成本、模型注意力稀释(资料越多,每块被认真读的概率越低)、矛盾资料相撞的概率。从 3 到 5 起步,按失败模式一、二的实测结果调,而不是默认「多多益善」。
问:我的文档总共就十几页,还需要 RAG 这一套吗? 大概率不需要。十几页直接全量塞进上下文(长上下文路线)更简单、更可靠、零检索错误。RAG 的复杂度是买给「文档量大到塞不下、更新频繁到贴不过来」的场景的——按第 6 节的决策表判断,别为了用技术而用技术。
9. 动手实验:三十行跑通一个最小 RAG
不装向量数据库,不装 RAG 框架——三条制度文档、暴力余弦相似度,把检索、增强、生成三步亲手走通。这三十行是所有 RAG 系统的共同骨架:
import math
from openai import OpenAI
client = OpenAI() # 从环境变量读取密钥
docs = [
{"id": 1, "text": "报销制度:每月 5 日前提交上月发票,单笔超 500 元需部门主管审批。"},
{"id": 2, "text": "年假制度:入职满一年享有 5 天带薪年假,需提前 3 个工作日系统申请。"},
{"id": 3, "text": "考勤制度:工作日 9:00-18:00,午休 1 小时,每月允许 2 次弹性打卡。"},
]
def embed(texts: list[str]) -> list[list[float]]:
resp = client.embeddings.create(
model="your-embedding-model", # 嵌入模型名,以所用平台文档为准
input=texts,
)
return [d.embedding for d in resp.data]
def cosine(a: list[float], b: list[float]) -> float:
dot = sum(x * y for x, y in zip(a, b))
return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(x * x for x in b)))
# 入库管线:真实系统里这一步离线做好存进向量库,这里现场算
doc_vecs = embed([d["text"] for d in docs])
# 查询管线
question = "年假有几天,怎么申请?"
q_vec = embed([question])[0]
ranked = sorted(docs, key=lambda d: cosine(q_vec, doc_vecs[d["id"] - 1]), reverse=True)
top_k = ranked[:2]
context = "\n".join(f"[资料 {d['id']}] {d['text']}" for d in top_k)
prompt = f"""请仅依据下面的参考资料回答用户问题。
如果资料不足以回答,直接说「根据现有资料无法回答」,不要猜测。
回答时标注所依据资料的编号。
{context}
用户问题:{question}"""
resp = client.chat.completions.create(
model="...", # 模型名以所用平台文档为准
messages=[{"role": "user", "content": prompt}],
)
print(resp.choices[0].message.content)运行后做四个观察:
- 答案里应该出现「[资料 2]」的引用标记,且数字与制度文档一致——开卷考试的正样本;
- 把问题换成「加班有加班费吗」,观察模型是否按约束句拒答(库里没有加班制度,TopK 里最像的块也不该被拿来编);
- 把约束句整段删掉重跑第 2 步,对比模型是否开始编造——这是失败模式与提示词约束关系的直接证据;
- 在 docs 里追加一条「2025 年旧版:年假 3 天」,重问「年假几天」,观察失败模式三(拼接破坏)真实上演。
10. 小练习
预测题:把实验里的 top_k 从 2 改成 3(全部文档都拼进去),对第 2 个观察(拒答行为)有什么影响?先预测再验证。
修改题:给 docs 里的每条记录加上 "source": "员工手册 2026 版" 元数据,修改拼装模板要求答案标注来源名称。
修 Bug 题:同事把切块大小从 500 token 调到 5000 token,理由是「块大信息多,召回更全」。用第 3 节的权衡指出这个改法的两个具体风险。
分析题:用户反馈「问社保基数,系统答的是公积金的比例」。判断这属于三大失败模式中的哪一种(或哪几种的组合),并说出你的排查步骤。
11. 与之前和之后的知识的关系
- 往前:嵌入与向量检索 提供了检索这一步的全部原理;结构化输出 提供了让引用可渲染、输出可校验的手段;
- 往后:进阶 RAG 系统性地解决本文的三大失败模式——混合检索、重排序、查询改写、评估体系;文档问答助手实战 把整条管线组装成一个完整应用。
12. 官方文档
- OpenAI embeddings 指南:https://platform.openai.com/docs/guide
- Anthropic 检索增强指南:https://docs.anthropic.com
- pgvector(入门实验后期可迁移的向量存储):https://github.com/pgvector/pgvector
13. 自我检查
- 能说出为什么重训与全量塞上下文都不是私有知识的好答案,并画出检索、增强、生成三步图;
- 能说出三种切块策略各自的优缺点,以及 chunk 太小、太大各坏在哪;
- 能默写出带「只依据资料回答」约束与拒答出口的拼装模板,并解释约束句为什么不是可选项;
- 能说出引用来源买回的三样东西,以及没有引用时 RAG 出错为什么无从排查;
- 能对给定症状判断它属于三大失败模式中的哪一种,并说出对应的初步对策。
本章总结
模型不知道你的私有文档,重训不可行、长上下文塞不下也贵,RAG 用开卷考试解决:检索(找相关片段)、增强(拼进提示)、生成(看着资料回答)。知识在库不在模型,更新知识只需更新库;答案可溯源,引用让每个结论可核查、出错可定位。入库管线是加载、切块、嵌入、存储,切块的艺术在固定长度、按结构、带重叠三种切法的组合与 chunk 大小的权衡;查询管线是查询嵌入、TopK 检索、带「只依据资料回答」约束的拼装、生成。朴素 RAG 有三大失败模式——检索不到(词不匹配)、检索到了但没用(排序差)、拼接破坏(资料矛盾),系统化的对策在下一篇。
下一步
进入 进阶 RAG:混合检索、重排序、查询侧优化、评估体系与权限感知检索——朴素 RAG 与生产级 RAG 之间的全部台阶。