一、背景:别一上来就搞向量数据库
很多团队做 RAG 的第一反应是:切分文档、调 embedding 模型、搭向量数据库、加 rerank,一套重型流水线直接上。结果索引慢、调试难、问个”发票 #12345 怎么查”反而答不上来。
真相是:检索选型应该从需求出发。用户查询是关键词型(pandas merge dataframe)、精确匹配型(工单号、报错码),还是语义问答型(我该怎么申请退款),对应方案完全不同。本文按从简到精给出 6 种检索方案,每一级都说明适用场景、做法和升级信号,帮你避免过度工程。
决策五要素(先自检再选型):数据新鲜度(实时更新还是月更)、语料特征(专有名词多不多、长尾分布明不明显)、查询模式(关键词 vs 语义 vs 混合)、规模与性能(日查询小于 1000 还是大于 1 万)、团队能力(有无 ML 经验)。
二、原理:检索的本质是召回加排序
RAG 等于 Retriever(找资料)加 Generator(写答案)。生成部分都是 LLM,差别全在检索。
- 稀疏检索(BM25/全文检索):基于词频统计,精确匹配关键词,可解释、零成本、毫秒级,适合专有名词和编号查询,缺点是不懂同义词。
- 稠密检索(向量检索):把文本变成向量,按语义相似度召回,能处理汽车和轿车这类表达差异,但需要切分、建索引,调试偏黑盒。
- 混合检索(Hybrid):两路召回再融合排序,兼顾精确与语义,是生产环境最常用的形态。
- 查询改写与扩展(Rewrite/HyDE):把用户口语问题改写成更适合检索的形式,或先让 LLM 生成假设答案再去检索,解决问法与文档写法不一致的问题。
- 重排与多轮(Rerank/Multi-turn):粗排召回一批,精排模型再精选;复杂问题拆成多轮检索,逐步收敛。
升级原则:每一级的复杂度都要有指标证明其必要,先用简单方案测出召回率基线,再决定加不加料。
三、环境准备
本文示例用 Python,方案一只需要一个轻量依赖即可跑通,后续方案按需叠加。
python -m venv rag-demo && source rag-demo/bin/activate
pip install rank-bm25 jieba scikit-learn numpy sentence-transformers
测试语料准备:找 20 到 50 篇你们自己的真实文档(帮助中心、API 说明、工单记录),再人工写 30 条真实用户问题并标注正确答案,这是后面所有方案共用的评测集。没有评测集就升级,等于闭眼开车。
四、分步实战:6 种方案从简到精
方案一:BM25 全文检索起步(MVP)
适用:刚起步、关键词查询为主、有精确编号、私有术语多、零 ML 成本要求。
from rank_bm25 import BM25Okapi
import jieba
docs = [
"如何重置密码:进入设置页点击忘记密码,通过邮箱验证码重置",
"发票查询:在账单中心输入发票编号即可下载PDF",
"pandas合并表格:使用pd.merge按user_id关联",
"退款政策:购买7天内未激活可申请全额退款",
]
tokenized = [list(jieba.cut(d)) for d in docs]
bm25 = BM25Okapi(tokenized)
def search(query, topk=2):
scores = bm25.get_scores(list(jieba.cut(query)))
ranked = sorted(zip(docs, scores), key=lambda x: -x[1])
return ranked[:topk]
print(search("忘记密码怎么办"))
print(search("发票如何下载"))
要点:文档即原文,不用切分;能精确解释为什么命中;中文记得先分词。先用它跑评测集,很多团队发现 Top-3 命中率直接到 70% 以上,根本不需要向量。升级信号:用户常问口语化问题,关键词完全对不上文档时。
方案二:查询重写 Query Rewriting
适用:用户问法随意、口语化严重,但答案文档本身写得规范。
做法:检索前让 LLM 把问题改写成 2 到 3 个更书面的查询,再分别检索合并。注意保留原始查询一路召回,防止改写跑偏。改写提示词示例:你是检索助手,把用户问题改写成 3 个适合关键词检索的查询,保留专有名词和编号,每行一个,不要解释。
def rewritten_search(user_q):
queries = [user_q, user_q.replace("怎么办", "操作步骤")]
hits = {}
for q in queries:
for doc, s in search(q):
hits[doc] = hits.get(doc, 0) + s
return sorted(hits.items(), key=lambda x: -x[1])[:3]
成本极低,效果立竿见影。
方案三:混合检索 Hybrid(BM25 加向量)
适用:查询混合,既有编号也有语义问,是生产环境的主力形态。做法是两路分别召回、分数归一化后加权融合。向量模型选轻量中文模型(如 BGE-small)即可;权重 alpha 从 0.5 起调,专有名词多就调高 BM25 权重;融合前务必归一化,否则一路分数碾压另一路。
def hybrid_search(query, alpha=0.5, topk=3):
b = dict(search(query, topk=10))
v = {} # 此处接向量检索结果,演示占位
def norm(d):
m = max(d.values()) or 1
return {k: x / m for k, x in d.items()}
bn, vn = norm(b), norm(v)
fused = {d: alpha * bn.get(d, 0) + (1 - alpha) * vn.get(d, 0)
for d in set(bn) | set(vn)}
return sorted(fused.items(), key=lambda x: -x[1])[:topk]
方案四:HyDE 假设文档扩展
适用:问法与文档写法完全不在一个频道的语义鸿沟场景。原理:先让 LLM 凭常识生成一份假设答案,再用这份假设答案去检索真实文档——用答案找答案,比用问题找答案更容易命中。注意:假设答案只用于检索,不直接展示给用户,避免幻觉污染。
方案五:Rerank 精排
适用:召回率够了但排序不行,正确答案总排在第 5 到 10 名。做法:粗排召回 20 到 50 篇,调用 rerank 模型精选 Top-3 送入生成。这可能是性价比最高的单点优化,但会增加延迟,只对进入生成的那几篇做精排,不要全量精排。
方案六:完整管道加多轮检索 Agentic RAG
适用:复杂问题,一次检索答不全,需要查多步、边查边想。流程:检索一批资料,让 LLM 判断资料够不够,不够则给出下一个检索查询,最多 3 轮,最后基于全部资料生成答案并标注引用。配套还要加上:每个论断标出来源 chunk、无答案时拒答、日志记录每次检索的查询与命中,方便复盘。
def multi_turn_rag(question, max_rounds=3):
context, queries = [], [question]
for _ in range(max_rounds):
hits = hybrid_search(queries[-1])
context.extend([d for d, _ in hits])
break # 实际接入LLM判断是否继续
return context
五、常见坑
- 跳过 BM25 直接上向量:专有名词、编号、产品型号全军覆没。永远先跑 BM25 基线。
- 切分拍脑袋:窗口多大、重叠多少,不做评测就定参。建议按标题段落做语义切分,并对比两组参数的召回率。
- 只测生成不测检索:答案不好先查召回的 chunk 对不对,八成问题出在检索而非 LLM。
- HyDE 幻觉直出:假设答案只能用于检索,不能直接展示。
- 无评测集就扩复杂度:每加一级都要有 Top-K 命中率提升证据,否则回滚。
- 团队能力错位:没有 ML 经验硬上微调 embedding 和自训 rerank,维护成本爆炸,先用 API 和开源小模型。
六、总结与团队分级路线图
个人小团队:方案一 BM25 加方案二查询重写,基本解决 70% 问题。成长型团队:加上方案三混合检索加方案五 Rerank,进入生产可用态。有 ML 能力的团队:再引入 HyDE、多轮检索与自建评测看板,追求最后 10%。
RAG 的核心竞争力不是堆了多少模型,而是评测集加逐级升级的纪律。从 BM25 起步,每一步都用数据证明值得,才是真正的简单之道。
七、深入:为什么 BM25 在专有名词上经常打败向量
很多新手直觉认为向量检索全面碾压关键词检索,实测恰恰相反。原因有三。
第一,专有名词在 embedding 空间里没有好表示。发票编号、产品型号、报错码这类字符串,embedding 模型训练时很少见,切分 tokenizer 可能把它拆成无意义碎片,向量相似度基本随机。而 BM25 是精确匹配,发票 12345 就命中发票 12345,稳如磐石。所以凡是工单号、订单号、版本号查询,永远保留 BM25 一路。
第二,IDF 直觉:越稀有的词越重要。BM25 内置词频逆文档频率,冷门术语自动加权。向量检索对高频词和低频词一视同仁,容易被热门文档带偏。私有术语(公司内部黑话、缩写)多的语料,BM25 优势更大。
第三,可解释可调试。BM25 能告诉你命中是因为哪个词,调参方向明确(停用词、权重)。向量检索出问题只能换模型、调切分,黑盒试错成本高。
完整可跑的混合检索(含真实向量一路):
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
emb = model.encode(docs, normalize_embeddings=True)
def vector_search(query, topk=10):
q = model.encode([query], normalize_embeddings=True)[0]
scores = emb @ q
ranked = sorted(zip(docs, scores), key=lambda x: -x[1])
return ranked[:topk]
def hybrid_search(query, alpha=0.5, topk=3):
b = dict(search(query, topk=10))
v = dict(vector_search(query, topk=10))
def norm(d):
m = max(d.values()) or 1e-9
return {k: float(x) / m for k, x in d.items()}
bn, vn = norm(b), norm(v)
fused = {d: alpha * bn.get(d, 0) + (1 - alpha) * vn.get(d, 0)
for d in set(bn) | set(vn)}
return sorted(fused.items(), key=lambda x: -x[1])[:topk]
print(hybrid_search("密码忘了怎么找回", alpha=0.4))
调参经验:先固定评测集,alpha 从 0.2 到 0.8 网格搜索,选 Top-3 命中率最高的;语料专有名词多,alpha 往高走(0.6 到 0.7);口语问答多,alpha 往低走(0.3 到 0.4)。
八、评测集搭建:没有它不要谈升级
评测集做法:收集 50 到 100 条真实用户问题(从客服记录、搜索日志里捞),人工标注每题的标准答案文档。每上一级方案,跑一遍评测集,记录 Top-1 和 Top-3 命中率。只有提升超过 5 个百分点,才值得增加复杂度。评测脚本本身很简单:遍历问题调检索函数,看标准答案在不在前 K 名。建议把评测脚本接入 CI,语料更新后自动重跑,防止索引 regression。
精排的量化启用标准:当混合检索 Top-10 命中率比 Top-3 高 15 个百分点以上,说明答案能召回但排不上来,这时加 rerank 收益最大。否则加了也白加。
九、实战:给你的文档跑一遍分级选型
光讲道理不够,这里给一套今晚就能跑的选型流程。第一步,准备三样东西:你的文档(先拿 30 篇试)、30 条真实问题、标准答案出处。第二步,用方案一的 BM25 代码跑一遍,记录 Top-1 和 Top-3 命中率,这就是你的基线。第三步,把答错的题分类:关键词对不上的是语义鸿沟(上查询重写或混合检索),答案排在后面的是排序问题(上精排),需要查两三篇才能拼出答案的是多跳问题(上多轮检索)。分类错题再选武器,一打一个准。
举个真实分布:某 SaaS 帮助中心 200 篇文档,BM25 基线 Top-3 命中率 68%。错题里 40% 是用户口语化(密码忘了怎么办对重置密码操作指南),加查询重写后涨到 79%。剩下错题一半是排序问题,加精排到 86%。最后的多跳问题上多轮检索,到 90%。每一级都有 5 个点以上的提升,投入产出清清楚楚,老板问起来也有数据。
切分策略单独说两句,因为这是向量方案最大的暗坑。固定长度切分(如 512 token 一刀)会把表格、代码块从中间劈开,检索到半截 chunk,生成质量直接崩。推荐按 Markdown 标题切分,一节一块,超长的节再按段落细分。chunk 别太小(200 token 以下噪声大),也别太大(1500 以上稀释主题)。切完人工抽 10 块看看:每块主题单一、首尾完整,才算合格。评测时固定两组切分参数对比,选命中率高的,不要拍脑袋。
多轮检索的停止条件值得精打细算。最多 3 轮是经验值,超过 3 轮收益衰减、延迟和成本翻倍。每轮让模型输出一个判断:资料够了直接回答,不够给出下一个查询。查询要具体(把已找到的信息带上,如已知退款期限是 7 天,再查退款到账时间),不要重复问。日志记下每轮的查询和命中,上线后复盘全靠它。
十、团队分级落地清单与什么时候不用 RAG
个人与小团队:一台机器跑 BM25(Postgres 全文检索或 Elasticsearch 单节点),查询重写调一个 LLM API,评测用表格记即可。成长型团队:向量索引进 Milvus 或 pgvector,rerank 用开源小模型,评测脚本定时跑。有 ML 能力的团队:自训领域 embedding、HyDE 与多轮检索、完整看板。每一档的运维成本差一个数量级,不要跳级。
最后泼一盆冷水:很多场景根本不需要检索。答案在模型训练语料里(如通用编程知识),直接问就行,加检索反而引入噪声。判断标准是关门测试:把问题直接抛给裸 LLM,答对了就不要建索引。另一种是高频固定问答(如 100 条 FAQ),缓存问答对比走完整 RAG 便宜十倍。RAG 的甜蜜区是:语料私有、更新频繁、答案需要出处,三条中两条才值得上。微调和 RAG 也不是对立:语料稳定风格统一(如公司行文规范)微调更合适,语料常变需要引用 RAG 更合适,很多团队是 RAG 打底、高频问题沉淀缓存、风格问题微调,三层叠加。 点击阅读原文
参考资料:Lighthouse Newsletter《RAG Is Simpler Than You Think》一文的 6 架构思路。 点击阅读原文