先说结论:如果你的RAG答非所问,八成不是大模型的锅,是召回的那几段根本没把对的内容排到前面。最省事的解法,是在向量检索之后、丢给大模型之前,塞一层重排序(rerank),把真正相关的拎到Top3。我上周就靠这一步,把一个内部问答的命中率从一半多干到九成。下面是完整过程和前后数据。
问题:向量召回回来的,看着像、其实不对
背景是这样。我给团队做了个查公司制度的小工具,知识库是47篇规章,切成大概600个chunk,用的BGE做embedding,余弦相似度召回Top5,然后塞给大模型让它总结回答。
跑demo挺好,一上线就翻车。
有个同事问"试用期能不能请年假",召回回来的Top5里,前三段全是"年假天数怎么算""年假过期作废"这种——主题词全中,"年假"两个字密度拉满,可就是没答到"试用期"那个限制条件上。真正写着"试用期员工不享受年假"的那段,排在第6,压根没进Top5。
大模型拿着前五段一本正经地编,告诉人家试用期能请5天。我当场就有点慌。
排查下来,根因是向量相似度只管"语义像不像",不管"能不能回答这个问题"。"年假怎么算"和"试用期能不能请年假",在向量空间里挨得特别近,但对用户来说一个有用一个没用。bi-encoder(双塔)把query和文档分开编码,本来就丢信息,细粒度的相关性它分辨不出来。
加rerank那一步:让模型重新读一遍query和文档
思路很简单:向量检索负责召得全(粗排,捞回Top20),rerank负责排得准(精排,从20里挑出真正相关的Top3)。
关键区别在于,rerank用的是cross-encoder——把query和每篇候选文档拼在一起喂进模型,让它直接打一个相关性分。query和文档之间的token能互相看见,"试用期"和"不享受年假"的对应关系,这下能被捕捉到了。代价是慢,所以只在小范围候选上做。
代码改动其实就夹一层,我用的sentence-transformers的CrossEncoder:
from sentence_transformers import CrossEncoder reranker = CrossEncoder("BAAI/bge-reranker-base") def retrieve(query, top_k=3): # 1. 向量粗排,先捞回20条 candidates = vector_search(query, top_k=20) # 2. query 和每条候选拼一起打分 pairs = [[query, doc.text] for doc in candidates] scores = reranker.predict(pairs) # 3. 按 rerank 分数重排,取前3 ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True) return [doc for doc, _ in ranked[:top_k]]就这么几行。粗排把范围圈到20,精排在20里精挑3条交给大模型。那段"试用期不享受年假",rerank分数直接飙到第1。
召回前后对比:数据摆这
我手攒了60个真实问题做评测集,标好每个问题对应的标准答案chunk,量了三个指标。纯向量 vs 向量+rerank:
指标 | 纯向量Top5 | 向量Top20+rerank Top3 |
Hit@3(对的进前三) | 58% | 91% |
MRR(正确答案平均排名倒数) | 0.61 | 0.88 |
答案被同事吐槽"不对" | 19次/天 | 2次/天 |
单次检索耗时 | ~80ms | ~430ms |
最直观的是那个吐槽数,从一天被戳19次降到2次。MRR从0.61到0.88,意味着对的内容基本稳定排在第一第二位,大模型不用再从一堆噪声里猜。
代价也写在表里了:延迟从80ms涨到430ms,翻了五倍多。reranker毕竟要逐条过模型。
真实取舍:它不是免费的
说几个我踩过、你大概率也会遇到的点。
慢是真的慢。候选从20加到50,延迟能到800ms以上,体感就有点卡了。我的折中是粗排只给20条,够用了——再多rerank也救不回向量没召回来的东西,精排不背粗排的锅。
小模型够用,别上来就堆大的。我先试了bge-reranker-large,效果好一丢丢,但慢一截。base版在我这60题上只差2个点,果断用base。
reranker也不是神。有几个问题是知识库本身就没写清楚,rerank排得再对,大模型也答不出来,这跟检索没关系。别指望一层rerank包治百病。
还有个小插曲:我一开始把rerank分数当成概率卡了个0.5的阈值,结果好多对的被滤掉了——bge-reranker输出的是logits,不是0~1的概率,直接拿来排序就行,别瞎卡阈值。这坑我debug了俩小时。
最后补一句。这套东西我没全自己写。粗排精排串起来、知识库挂上去、最后发布成一个能用的问答接口,我是在一个零代码就能拖配智能体的平台上搭的,可视化配检索流程,rerank这层勾一下就接进去了,省了我不少胶水代码。它干的是杂活,真正调参挑模型还得自己来,但确实让我把精力留在了刀刃上。
底层大模型API我走的讯飞星辰MaaS,现成调,没自己折腾算力部署。
你们的RAG翻车,是召回没召全,还是召全了排不准?评论区聊聊,我顺手帮看看是不是也该夹层rerank。