
RAG 的三个隐藏成本召回、排序和你看不见的那部分做 RAG 半年我最常被问的一句话是strong大多数人的第一反应是换个更好的 embedding 模型或者调 chunk size。但真实原因往往不在模型上——strong。成本一召回 —— 你以为找到了其实只是看起来像向量检索的本质是strong而最近不等于相关。一个真实案例用户问「怎么让订阅自动续费」文档里写的是「关闭自动扣款需在账户设置中取消订阅」。语义上高度相关但纯向量检索经常strong——因为两段话没有共享关键词。这不是模型不够好是strong它擅长捕捉语义相似但对精确术语、缩写、数字、专有名词是弱项。怎么补关键词检索不是被淘汰的旧技术strong# 混合检索向量负责意思像BM25 负责字面准 def hybrid_search(query, top_k20): dense vector_store.similarity_search(query, ktop_k) sparse bm25_index.search(query, ktop_k) # 用 RRF倒数排名融合合并两个排名而不是直接混分数 return reciprocal_rank_fusion([dense, sparse])关键在strong。BM25 分数是无界的向量余弦相似度是 [-1,1]两者不在一个量纲上硬加权等于让某一边的量级盖过另一边。RRF 只看strong绕开了这个坑score(d) Σ 1 / (k rank_i(d)) # k 通常取 60strong成本二排序 —— 召回对了答案还是错的召回 20 条然后呢直接把 top-1 塞进 prompt或者取 top-3这两种都常见都是错的。strong。设想检索到 3 段第 1 段完全相关第 2 段是广告第 3 段是页脚导航 —— 大模型会把三段混在一起回答而且strong。一个廉价的改进在塞进 context 之前先做一次strong# 不是简单截断而是逐段问「这段能回答问题吗」 def rerank(query, docs, model): scored [] for d in docs: keep model.judge(f问题{query}\n片段{d.text}\n f该片段能否直接支撑答案只答 yes 或 no) if keep yes: scored.append(d) return scored这一步不优雅要额外调用但它把「20 段全塞进去」变成「只塞 3~5 段真正相关的」。strong——这是上下文窗口越大反而效果越差的原因之一。关于 rerank 模型规模够大的团队会接专门的 rerankercross-encoder。但要先说清楚它解决的是strong混合检索解决「召不到」rerank 解决「召到了但排序不对」这两个是strong关系不是替代关系只上 rerank 不改召回等于在一条漏水的管道上装了压力表。成本三你看不见的那部分 —— 端到端评估这是最容易被忽略、也最致命的一项。strong你改了 chunk size感觉变好了过两天又改了 top_k感觉更好了 —— 但你怎么知道strong最小可行的评估集不需要 500 条。strong1. 从线上真实日志里抽 30 个用户的实际提问2. 人工标注「标准答案应该来自哪几段文档」3. 每次改动后跑一遍算两个指标召回率标注的那几段有多少进了 top-k答案正确率最终回答是否覆盖标注要点关键在第 2 步的strong——它是最耗时但最有价值的部分。跳过它你的所有优化都只是在凭感觉移动指针。两个指标要分开看strong→ 回去改检索混合检索、chunk 策略strong→ 回去改生成prompt、rerank、上下文组织把两者混成一个准确率看你永远不知道该改哪一环。回到最初的问题「为什么接了向量检索效果还是差」——按成本从高到低排查| | 检查项 | 30 分钟能做完吗 | 症状| 是不是只用了向量检索 | ✅ | 精确术语/数字类问题召不到| 有没有评估集 | ⚠️ 最难但最关键 | 每次改动都靠感觉判断好坏| top-k 里噪声多不多 | ✅ | 答案混杂、跑题| rerank 在召回之前还是之后 | ✅ | 单独上 rerank 没效果strong因为没有评估集前三项你都无法验证是否真的改好了。strongRAG 效果差八成不是检索算法不够先进而是strong。先建评估集再谈优化。