RAG 不是魔法:一条可靠检索链路的工程拆解
换更大的模型往往救不了“答非所问”。真正决定 RAG 上限的,是你能否把答案锚定到自己的资料——本文把检索链路拆开,讲清每一步的取舍与踩坑。
先回答一个问题:RAG、微调还是长上下文?
很多人把这三种当成同一件事的竞争者,其实它们解决的是不同维度的问题。RAG 解决的是“知识不在权重里”:你的内部文档、今天刚更新的价格表、某个客户自己的历史工单,模型训练时根本没见过,也不可能塞进权重。微调解决的是“行为/格式/风格不对”:你希望它稳定输出某种 JSON 结构、用特定语气、遵循公司术语,这类“能力形状”的改变靠喂数据比靠检索更稳。长上下文(200K+ token)解决的是“窗口内就能放下全部材料”:当整本文档能一次性塞进上下文,检索的必要性确实下降。
但工程上三者不是互斥的。一个常见的组合是:长上下文承载“当前会话的全量材料”,RAG 负责“从海量历史库里精确定位”,微调负责“固定输出契约”。选型的第一性原理是——数据是否在训练权重中、是否频繁变化、是否需要被引用溯源。会频繁变动、需要给出处、体量远超上下文窗口的知识,RAG 是不二之选。
| 维度 | RAG | 微调 | 长上下文 |
|---|---|---|---|
| 适用知识 | 外部/易变/需溯源 | 固定风格与格式 | 窗口内全量材料 |
| 更新成本 | 极低(改索引) | 高(重训) | 低(改输入) |
| 可溯源 | 强(带引用) | 弱 | 中 |
| 成本/延迟 | 检索+生成 | 仅生成 | 高算力 |
切分:被低估的第一道关口
切分(chunking)直接决定了“召回的片段是否包含完整答案”。常见三种策略:固定切分按字符数(如 512 token)硬砍,实现简单但容易把一句话拦腰截断;递归切分按段落→句子→词的层级递归切,优先在标点/换行处断开,是 LangChain 等框架的默认选择,对大多数 Markdown/HTML 文档很稳;语义切分用_embedding_在相似度突降处断句,能保留完整语义单元,但计算更贵、边界更不可预测。
chunk size 是核心权衡:太小会丢失上下文(一个 definition 被拆成两半,检索只命中一半);太大则单块噪声多、召回精度下降、且占用上下文窗口。经验起点是 256–512 token,重叠(overlap)设为 10–20% 以避免边界信息丢失。对代码、表格这类结构敏感内容,应优先按语法结构(函数、表格行)切,而不是按字符。
检索:稠密 + 稀疏的混合才是常态
纯向量检索(稠密)擅长“语义相似”:用户问“怎么退款”,能命中“取消订单后钱多久到账”。但它对精确字符串(产品型号、错误码、人名)很弱。稀疏检索(BM25)正好相反:精确关键词命中极强,但理解不了同义表达。所以生产环境几乎都用混合检索(hybrid):两者各召回一批,再融合。
融合常用 RRF(Reciprocal Rank Fusion):score = Σ 1/(k + rank),不依赖各自分数量纲、实现简单。稠密向量模型优先选领域适配好的,如 BGE、E5、GTE 系列,中文场景建议用专门的中文版并做归一化。检索后还要重排(rerank):用 Cross-Encoder(如 BGE-reranker)对 top-k 候选做精细打分,它把 query 和 doc 拼在一起算相关性,精度远高于双塔召回,但慢,所以只作用在召回的几十个候选上。
# 混合检索 + 重排的精简示意(Python / 伪代码)
from rank_bm25 import BM25Okapi
import numpy as np
def hybrid_search(query_vec, query_tokens, dense_idx, bm25, chunks, k=20):
dense = dense_idx.search(query_vec, top_k=50) # 稠密召回 top50
sparse = bm25.get_top_n(query_tokens, chunks, n=50) # 稀疏召回 top50
# RRF 融合
rrf = {}
for rank, doc in enumerate(dense + sparse, 1):
rrf[doc] = rrf.get(doc, 0) + 1 / (60 + rank)
fused = sorted(rrf, key=rrf.get, reverse=True)
return reranker.rank(query_tokens, fused[:k]) # 重排后取 top-k
# 上线建议:向量库用 pgvector / Milvus,reranker 本地或独立服务部署
查询改写与 HyDE:让“问法”对齐“写法”
用户的问题是口语化的,文档是书面化的,二者存在表达鸿沟。两种解法:查询改写(Query Rewriting)用一个轻量模型把用户问题改写成更贴近文档索引风格的检索词,或拆成多个子问题(multi-query)分别检索再合并;HyDE(Hypothetical Document Embeddings)让模型先“假设”一个答案,再用这个虚构答案的 embedding 去检索——因为生成的答案措辞更接近真实文档,反而能拉回更相关的块。HyDE 对事实型问答有效,但对需要精确数据的场景可能引入幻觉偏差,需配合重排兜底。
图 RAG 与多路召回:当关系本身就是答案
普通 RAG 召回的是“片段”,但如果问题本质是“A 和 B 有什么关系”“某实体的上下游影响链”,片段检索就力不从心。GraphRAG 先对文档做实体/关系抽取,构建知识图谱,检索时沿图遍历,把子图结构一并喂给模型。它特别适合合规、科研、企业组织关系这类“强关联”场景,代价是索引构建成本高、需要图数据库。更轻量的做法是多路召回:同一查询并行走“摘要库 / FAQ 库 / 全文库 / 图谱”多条路,最后由 reranker 或路由模型统一排序。
一张图看清整条检索链路
怎么知道 RAG 真的变好了:评估与生产坑
评估要分两段看。检索侧看召回率(Recall@k):正确答案是否出现在召回的 k 个块里——这决定“模型有没有机会答对”。生成侧看答案忠实度(Faithfulness)与上下文利用率:模型是否真的基于检索内容作答、有没有编造上下文之外的信息(幻觉率)。实用做法是用 LLM-as-judge 对“答案是否可由给定片段推导”打分,但需定期人工抽检校准,避免评审模型本身有偏差。
生产坑集中在三处:其一是上下文污染,召回块太多太杂,模型反而被带偏,要靠 reranker 收紧;其二是元数据丢失导致无法溯源,前面已强调;其三是冷启动无评估集,建议上线第一天就人工收集 50–100 条真实问答作为黄金集(golden set),每次改动只变一个变量(切分/Prompt/模型/重排阈值)跑回归。
RAG 的核心不在模型多大,而在你能否把答案锚定到自己的资料。把切分、混合召回、重排、引用和量化评估每一环做扎实,模型才真正成为你知识的延伸,而不是又一个会自信胡说的对话框。