RAG
大模型的参数化知识有两个天然缺陷:知识有截止日期,且覆盖不到企业私有数据。RAG(Retrieval-Augmented Generation,检索增强生成)用「先检索、后生成」的方式把外部知识动态拼进上下文,是目前大模型应用落地最主流的方案。
什么是 RAG
RAG 的核心思想是把「记忆」和「推理」解耦:模型参数只负责语言理解与推理,事实性知识放进外部知识库,回答问题时先检索出与问题最相关的若干片段,再让模型只依据这些片段作答。
一次完整的 RAG 请求拆成两个阶段:
- 离线索引阶段:文档解析 → 切分(chunking)→ 向量化(embedding)→ 写入向量库。
- 在线服务阶段:查询改写 → 检索召回 → 重排 → 拼接上下文 → 生成回答(带引用)。
RAG 的「增强」发生在推理时(推理侧增强),微调的「增强」发生在参数里(参数侧增强)。两者不是替代关系,而是互补关系。
为什么需要 RAG
大模型的三个硬约束
- 知识截止:训练数据有截止日期,模型不知道之后发生的事。
- 私有数据不可见:企业内部文档、代码、工单不可能进入公开训练语料。
- 幻觉:模型被要求回答训练集之外的问题时,倾向编造看似合理的内容。
RAG 用「检索到的原文」作为事实依据,把「凭记忆作答」变成「开卷考试」,同时让知识更新退化成一次文档入库,而不是一次训练。
一种朴素的选型对比
| 方案 | 更新知识 | 幻觉 | 适用场景 |
|---|---|---|---|
| 预训练 | 要重训 | 中 | 通用基础能力 |
| 微调 | 要重训 | 中 | 固定格式与领域语感 |
| RAG | 改文档 | 低 | 多变的事实性知识 |
| 长上下文 | 不动 | 中 | 总量小的文档 |
成本上,预训练 > 微调 ≫ RAG;RAG 的推理成本随检索片段数增长,但远低于一次训练。幻觉一栏的关键差异在于:RAG 的答案有原文可溯源,出了问题能定位到具体片段。
RAG 和微调怎么选
判断标准是问题出在「知识」还是「行为」:
- 缺事实性知识、且知识会变 → 用 RAG。
- 缺输出行为(固定 JSON 格式、特定口吻、领域推理风格)→ 用微调。
- 两者都缺 → 先上 RAG(成本低、迭代快),再用微调固化行为。
「上下文窗口已经 100 万 token 了,还需要 RAG 吗」是常见误判。长上下文解决的是「能装多少」,RAG 解决的是「该装什么」:全量塞入既贵又慢,还会因为「lost in the middle」导致中间位置的信息被模型忽略。
完整链路
1. 文档解析与切分(Chunking)
检索的最小单位是 chunk,切分质量直接决定召回上限——切碎了语义,后面再好的模型也救不回来。
常见策略:
- 固定长度滑动窗口:按 token 数切(如 256~512),相邻块重叠 10%~20%,避免句子被拦腰截断。
- 按结构切分:优先按标题层级、段落、列表项切;Markdown / HTML 天然带结构,是最省事的做法。
- 递归切分:按「段落 → 句子 → 字符」依次降级尝试,尽量在语义边界处断开。
- 父子块(Small-to-Big):用小块建索引保证检索精度,命中后把它的父块(整节内容)交给模型,兼顾「检索准」和「上下文全」。
- 句子窗口检索:命中某个句子后,把前后各 N 句一起补进上下文。
块太小会丢上下文(「它」指代不明),块太大会稀释语义——embedding 是对整块取平均,一块里塞多个主题时向量会漂移。分块大小没有普适最优解,必须结合自己的文档形态调。
# 递归字符切分:优先在段落边界断开,超长才降级到句子、字符
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=512, # 目标块大小(按字符计,中文约等于 token 数)
chunk_overlap=64, # 12% 重叠,避免跨块的句子被切断
separators=["\n## ", "\n### ", "\n\n", "\n", "。", "!", "?", ""],
)
chunks = splitter.split_text(markdown_text)表格、图片、代码块要单独处理:表格建议转成 Markdown 或自然语言描述保留表头(否则「第三行的值是多少」这类问题必然失败),图片走多模态模型生成文字描述后再入库。
2. 向量化与索引(Embedding + 向量库)
把 chunk 交给 embedding 模型编码成定长向量,写入向量数据库。
embedding 模型的选型维度:
- 语言与领域:中文场景常用 bge 系列、m3e 等;专业领域(医学、法律)需要领域微调过的模型。
- 维度:512 / 768 / 1024 / 1536 常见,维度越高精度越好、存储和检索成本越高。
- 对称性:检索模型区分「查询→文档」的非对称任务(如 bge 的
query:/passage:前缀)。用错前缀会让召回明显变差。
向量库与 ANN 索引:数据量大(千万级以上)时不可能暴力算距离,必须用近似最近邻索引。
- HNSW(图索引):召回率和延迟都优秀,是当前主流。参数量级:
M(每个节点的连边数,常用 16~48)、efConstruction(建索引时的搜索深度,影响构建时间与索引质量)、efSearch(查询时搜索深度,越大召回越高、延迟越高)——efSearch是线上唯一可以随流量动态权衡召回与延迟的旋钮。 - IVF(倒排聚类):先聚类分桶,查询时只搜
nprobe个桶。省内存,但边界处的向量容易漏召回。 - PQ / 量化:压缩向量降低内存,代价是精度损失,通常与 IVF/HNSW 组合使用。
换了 embedding 模型(哪怕是同系列的版本升级)就必须**全量重建索引**:新旧向量不在同一个语义空间,混用会让相似度彻底失真。因此索引元数据里务必记录 embedding 模型名与版本,并预留灰度重建的流程。
3. 检索召回(Recall)
最基础的做法是把问题也编码成向量,取相似度最高的 top-k(召回阶段 k 通常放大到 20~50,最终给模型的只有 3~8 条)。
- 相似度度量:余弦相似度、内积、欧氏距离。向量归一化之后,余弦相似度等价于内积,工程上常用内积以获得更好的计算性能。
- 纯向量检索的短板很明确:专有名词、缩写、型号、数字、精确短语。这类查询语义相似度区分度差,必须引入关键词检索。
混合检索(Hybrid Search):向量召回(语义)+ BM25/全文检索(字面)并行,再做融合。
融合方式常用分数归一化加权,或更省事的 RRF(Reciprocal Rank Fusion)——只依赖排名、不依赖分数尺度,天然免调参:
# RRF 融合:score(d) = Σ 1 / (k + rank_i(d)),k 常取 60
def rrf_fuse(rankings, k=60):
scores = {}
for ranking in rankings: # 每路召回一个有序列表
for rank, doc_id in enumerate(ranking, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)查询改写:用户的问题往往不适合直接检索。
- 多查询(Multi-Query):用 LLM 生成 2~3 个同义问法,各召回一路后融合,缓解「措辞不同召回为零」。
- HyDE:先让模型生成一段「假设性的答案」,用这段答案去检索——答案和文档在同一侧(都是陈述句),比问题去匹配文档更准。
- 查询分解:比较型、多跳问题拆成子问题分别检索再汇总。
4. 重排(Rerank)
召回追求「不漏」,重排追求「排序准」。
| 阶段 | 模型结构 | 精度 | 成本 |
|---|---|---|---|
| 召回 | 双塔(bi-encoder) | 中 | 低(向量可预计算) |
| 重排 | 交叉编码器(cross-encoder) | 高 | 高(query 与文档必须一起过模型) |
| 重排 | LLM 打分 | 最高 | 最高 |
因为交叉编码器不能预计算,只能对召回的 top-N(一般 20~100)逐对打分,所以它永远放在召回之后。引入 rerank 通常是性价比最高的单点优化:黄金文档往往已在 top-20 里,只是排名靠后,重排把它提上来即可。
5. 生成与引用(Generation + Citation)
把重排后的片段拼进 prompt,交给大模型作答:
[系统提示]
你是知识库助手。只能依据下方资料回答,资料中没有的内容必须回答「资料中未提及」,不得编造。
回答时用 [序号] 标注依据来源。
[资料]
[1] 来源:员工手册.pdf #12
...
[2] 来源:报销制度.md #3
...
[问题]
试用期可以请年假吗?要点:
- 强制「不知道就说不知道」,这是抑制幻觉最有效的一条 prompt 约束。
- 要求引用编号并回填来源,让答案可溯源,也让用户能自行核验。
- 注意位置效应:模型对上下文中间位置的信息注意力最弱,把最相关的片段放在开头或结尾。
- 上下文不是越长越好:噪声片段会干扰判断,也会挤占生成本身的预算。
效果评估
RAG 的评估必须把检索和生成拆开看,否则无法定位问题。
- 检索侧:Recall@k(黄金文档是否在 top-k 内)、MRR(第一个正确结果的平均倒数排名)、NDCG(考虑排序位置的整体质量)。Recall@k 是天花板——检索不到,生成再强也无解。
- 生成侧:忠实度 faithfulness(答案是否完全来自给定资料)、答案相关性 answer relevancy、上下文精确率/召回率 context precision/recall。RAGAS 这类框架把上述指标用 LLM-as-judge 自动化。
- 端到端:构建「问题 + 标准答案 + 标准相关文档」的评测集,做消融——先看检索命中率,再看命中后能否答对。
「检索命中但回答错误」说明问题在生成侧(prompt、上下文组织、模型能力);「检索未命中」说明问题在检索侧(分块、embedding、融合策略)。这个二分法能省掉大量瞎调参。
常见踩坑
- 只做向量检索:遇到术语、编号、精确匹配就失手,必须混合检索。
- 切分不尊重结构:按固定字符数硬切会把表格、代码、列表切碎,语义边界被破坏。
- embedding 模型与查询不对称:该加
query:前缀的没加,召回率明显偏低。 - 文档更新不做删除:新版本入库而旧 chunk 还在,模型会答出过期内容。必须维护
doc_id → chunk_id映射,更新时先删旧再插新。 - 权限过滤放在生成后:多租户场景必须在检索阶段就用标量过滤(metadata filter)隔离数据,否则会把别人的资料喂进上下文,造成越权泄露。
- 多轮对话不消解指代:第二轮的「那这个呢」直接拿去检索必然失效,要结合对话历史做查询改写。
- 忽略延迟与成本:rerank 和 LLM 调用是延迟大头,可对「query → 最终结果」做缓存,embedding 走批量。
演进方向
- Naive RAG:检索 → 拼接 → 生成,一条直线。
- Advanced RAG:在检索前后加优化——查询改写、混合检索、重排、上下文压缩。
- Modular RAG / Agentic RAG:把检索做成可编排的模块,由 Agent 自主决定「是否需要检索、检索什么、要不要再检索一轮」。
- GraphRAG:用知识图谱表达实体关系,解决「跨文档多跳推理」和「全局性问题(这个数据集的主要主题是什么)」这类向量检索的盲区。
面试问答
RAG 和微调应该怎么选?
看缺的是知识还是行为。知识型、时效性强、需要溯源 → RAG;输出格式、领域语感、推理风格 → 微调。工程上常见组合是:先用 RAG 快速上线,再用微调固化输出行为。
为什么向量检索召不回我要的文档?
按可能性从高到低排查:① 缺混合检索,查询里全是专有名词/编号;② 分块把语义切碎了,或者块太大导致向量漂移;③ embedding 模型不适合中文或本领域,或查询/文档前缀用错;④ 查询本身有指代或歧义,需要改写;⑤ 检索到了但被 top-k 截断或相似度阈值卡掉。
为什么要加 rerank?不能直接把召回 top-k 调大吗?
召回的 top-k 放大只是「多给了候选」,排序仍然是双塔模型算的粗排,位置靠后不代表不相关。交叉编码器让 query 和文档一起进模型做交互编码,排序质量显著更高。代价是慢,所以只能对 top-N 重排;而把 top-k 无脑调大反而会引入噪声、拉高延迟和成本。
用户上传的文档更新了,索引怎么处理?
按 doc_id 维度做增量更新:入库时记录每个 doc 切出的 chunk id,更新时先按 doc_id 删除旧向量再写入新向量,保证同一个文档不会新旧版本共存。同时把内容哈希写进 metadata,避免内容未变时重复 embedding。
怎么评估一个 RAG 系统好不好?
先建评测集(问题、标准答案、标准相关文档),再拆成两段评估:检索侧看 Recall@k / MRR / NDCG,生成侧看忠实度、答案相关性、上下文精确率与召回率。用消融实验区分「检索没找到」和「找到但没答对」,分别优化检索链路和生成 prompt。
RAG 为什么还是会产生幻觉?
因为生成阶段本质仍是概率生成,模型可能忽视给定资料而凭参数记忆作答;上下文过长、噪声过多、prompt 约束不严都会加剧。工程上的应对是:prompt 强制「无依据则拒答」、要求带引用编号、控制片段数量与质量,必要时在生成后加一道事实一致性校验。