
RAG 落地实践:切片、检索与效果评估
RAG 是看起来最容易被低估的技术:调通一个 Demo 只需要一个下午,但要让它稳定地回答真实业务问题,往往要反复折腾好几周。这篇记录几个实践中真正影响效果的环节。
先明确失败模式
在动手优化之前,先分清回答错误的来源,这决定了该往哪儿使劲:
- 检索没召回:正确的文档根本没进候选集,模型再强也答不出来。
- 检索召回了但排序靠后:被截断丢弃,模型看不到。
- 上下文噪声太大:塞进去一堆无关片段,模型被带偏。
- 模型没按上下文回答:明明给了资料却自己编。
前两类是检索问题,第三类要在切片和重排上解决,第四类属于提示词与模型能力问题。很多团队一上来就换更大的模型,其实瓶颈在第一步。
切片不是越细越好
切片的目标是让每个片段单独拿出来都是一个完整的意思。常见的几个做法:
- 固定长度切分:实现最简单,但容易把一句话劈成两半,检索到的片段经常缺主语。
- 按结构切分:依据标题、段落、代码块切,效果通常最好,前提是解析时保留了原始结构。
- 小块检索、大块喂给模型:用小块做向量匹配保证精度,命中后再把它所在的整段上下文交给模型,兼顾召回与完整性。
- 重叠切分:相邻片段保留一点重叠,缓解边界处的语义断裂。
此外,给每个片段补一段元数据(来源、章节标题、时间)几乎总是划算的,它既能在生成阶段帮助模型定位,也能在后期过滤里用上。
检索:向量不够,关键词也别丢
纯向量检索擅长处理语义相近但用词不同的情况,缺点是对精确名词、编号、专有缩写不敏感。而企业文档里恰恰大量存在这类内容。
比较实用的组合是:
召回层:BM25 关键词检索 + 向量检索
融合层:RRF(倒数排名融合)
排序层:轻量重排模型(cross-encoder)取 Top-5
RRF 不需要调参,把两路结果按排名做加权倒数求和即可:
def rrf(rank_lists, k=60):
scores = {}
for ranked in rank_lists:
for rank, doc_id in enumerate(ranked, start=1):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)
重排(rerank)是投入产出比最高的一步:它把候选片段和问题放在一起编码,精度远高于向量相似度,代价是要多算一次前向,所以只对前 30~50 条候选做。
必须有一套评估
没有评估,所有优化都是凭感觉。最小可用的方案是准备 50~100 条真实问题,每条标注标准答案和应该命中的文档,然后盯住两个分别的指标:
- 检索指标:Recall@k、MRR,衡量正确的资料有没有被找出来。
- 生成指标:忠实度(回答是否只依据上下文)和有用性。忠实度可以由人工打分,也可以用模型互评放大样本量。
两个指标要分开看。召回率上去了但忠实度下降,说明塞进去的噪声变多了;召回率没变而忠实度提升,那多半是提示词改进的功劳。
提示词里的几个细节
- 明确要求「如果上下文中没有答案,就直说不知道」,能显著减少胡编。
- 让模型在生成答案时带上引用编号,方便人工核查,也便于后续做忠实度自动评测。
- 把指令放在上下文之后,长上下文场景下模型对结尾部分的注意力更可靠。
小结
RAG 的效果上限由检索决定,下限由提示词决定。先把评估跑起来,再按失败模式对症优化,比逐条试技巧要高效得多。