检索栈评估指南:不要只盯着embedding模型
上周帮一个团队排查知识库问答效果不佳的问题。他们做的第一件事是把 embedding 模型换了个遍中文向量模型试了英文通用模型也试了参数更大的模型也搭进去了一周下来检索准确率基本没变。后来把链路拆开才发现问题根本不是出在 embedding 模型本身而是分块策略太粗糙检索参数没调索引结构和数据规模也不匹配。这个场景其实很普遍。现在提到检索增强生成RAG或者语义检索很多人第一反应就是选一个好 embedding 模型。但 embedding 模型只是检索栈中的一个环节真正决定检索质量的是整条流水线的组合效果。之前在 Hacker News 上看到一个项目叫 Embench标题写得很直接playground for comparing embeddings and retrieval stacks也就是一个用来对比 embedding 和检索栈的试验场。它想解决的核心问题恰好就是这个领域里最容易被低估的一件事——单个模型好不好只是一个维度更重要的是embedding 索引 检索参数 分块策略 重排环节这些变量组合在一起之后效果如何、怎么比较、怎么复现。这篇文章不打算做功能清单式的介绍。我想借这个项目切入的视角把下面几个问题讲透为什么孤立对比 embedding 容易得出错误结论检索栈里到底有哪些变量在同时起作用一个可用的对比 playground 应该具备什么条件以及回到自己的项目里应该怎么设计一套靠谱的检索栈对比流程。1. 只在 embedding 上比高低为什么容易得出错误结论1.1 排行榜和真实检索场景之间隔着一条完整流水线社区里其实不缺 embedding 模型评测。常见的评测基准会覆盖语义相似度、文本分类、检索、聚类、句子对匹配等任务最后给一个综合分数。这类排行榜确实有用它能帮你在几百个模型里快速圈出一个候选范围。但问题在于排行榜的高分建立在固定的评测流程上评测集和你的业务数据往往不是一回事更关键的是排行榜通常只评模型本身不评检索系统。真正做语义检索时你的流程一般是这样先把文档切片再对每个切片做 embedding导入向量索引查询时把 query 转成向量在索引里召回一批候选最后可能还要重排。这里面任何一环变动都会影响最终结果。比如同一个 embedding 模型用固定大小分块和语义分块检索效果可能差不少用暴力检索和用 HNSW 近似索引召回率也会有差别检索时 Top-K 设置不同后面重排环节的输入质量也会完全不同。所以你会看到一种很常见的情况明明这个 embedding 模型在公开基准上分数更高放到自己的知识库里反而不如另一个模型。这不是模型骗了你而是你拿模型的单点能力去预测整条流水线的结果中间隔了太多的变量。1.2 检索质量是多个环节叠加出来的不是单点决定的如果给检索质量做一个粗略分解可以写成这样召回质量 embedding 模型的语义理解能力 × 分块策略的合理性 × 索引结构对数据的适配度排序质量 召回候选的质量 × 重排模型的判别能力 × 排序策略的准确性系统可用性 延迟 × 吞吐量 × 成本 × 稳定性任何一个因子接近零整体结果都会直接崩掉。这也是为什么只换 embedding 模型往往收效甚微——它只是一个乘数其他乘数不动天花板就在那里。这里可以先记住一句话不要问哪个 embedding 模型最好要问哪一套检索栈组合在我的数据和场景里表现最稳定。这也是 Embench 这类对比检索栈的项目让我觉得有价值的原因。它把比较的单位从模型提升到了栈——不是单独看一个 embedding 的静态分数而是看它放进一套检索系统里之后整体表现如何。2. 一套检索栈里真正影响结果的变量有哪些2.1 embedding 模型维度、领域适配和更新成本embedding 模型本身的变量并不只有能力高低还包括输出向量的维度、对领域文本的适配程度、是否支持多语言、推理速度、单条成本。维度直接影响存储和检索消耗。两个 embedding 模型一个输出 768 维一个输出 1024 维在百万级向量库里的内存消耗和检索延迟会差出不少。如果数据量不大维度差异可能无所谓但到了千万级向量维度会直接决定你要不要做量化、要不要升级硬件。领域适配是另一个容易被忽略的点。通用排行榜的高分模型在专业术语密集的领域未必好用。常见做法是从公开基准里选出几个候选模型再在自带的数据集上做小规模验证用结果反推哪个模型更贴合自己的场景。2.2 分块策略最容易被低估的变量很多团队在评估检索效果时把 80% 的注意力放在模型选型上却在分块上非常随意——固定 500 字切一刀overlap 随意设一个数。实际上分块策略对检索质量的影响很多时候比换 embedding 模型还要大。分块太大会带来两个问题一是 embedding 表征会被稀释一个块里有多个主题时向量很难精准表达其中某个局部信息二是后续重排或生成环节的输入噪声更大。分块太小则会导致上下文不完整语义被切碎检索到之后也无法回答完整问题。更合理的做法是先分析文档结构再决定分块粒度。标题、段落、列表项、代码块这些结构本身就是天然的边界。Markdown 文档适合按标题层级先切分再按段落补充表格适合整块保留长段落可能需要结合句子边界做二次切分。分块之后还要考虑 overlap目的是保留跨块的上下文衔接。2.3 索引、检索参数和重排决定召回率和体感索引结构的选择会影响召回的准确率和延迟。暴力检索召回率最高但数据量大时延迟不可接受HNSW 这类近似最近邻索引能在延迟和召回之间取平衡但需要调参数。常见的调参面包括 ef_search、M、候选集大小不同数据分布下最优参数不同不能照搬默认值。检索参数里最容易出问题的是 Top-K 和相似度阈值。Top-K 太小真正相关的文档可能根本进不了候选集Top-K 太大重排环节的负担会增加而且噪声也会变多。相似度阈值则要结合 embedding 模型的输出分布来确定不同模型的相似度分布形态可能差很多用同一个硬编码阈值经常会出问题。重排环节相当于在召回结果上再做一次精细打分。常见做法是用一个更强的排序模型对候选集重新排序也可以基于业务规则做加权。加入重排之后前面的 Top-K 设置、召回质量都会对最终效果产生放大或缩小的影响。这里还没有算上查询改写。用户的原始 query 往往很口语化、有省略、有指代直接 embedding 可能在语义空间里偏离目标。先做查询改写或者扩展再去做向量检索有时能显著提升召回效果。3. 一个可用的对比 playground到底要解决什么问题3.1 评估数据的质量比评估工具本身更关键任何一个对比工具底层都离不开评估数据。没有数据模型和参数都无从比较。所谓 playground本质上是一个把变量控制住、把评估过程标准化的地方。评估数据至少要包含三部分查询集、文档集和标注结果。文档集应该尽量贴近目标场景查询集要覆盖不同的提问方式、不同长度、不同复杂度标注结果则要标清楚每个 query 对应哪些文档是真正相关的。构建评估数据的成本不低但它是一项长期资产以后每次换模型、改参数都可以复用。如果一开始没有标注数据可以先从日志里找真实用户 query再反向找文档。先用简单规则生成初版标注再人工抽样校验。宁可先做一百条高质量的评估数据也不要为了追求数量随便拼凑一千条——评估数据的噪声会让所有对比结果失去意义。3.2 指标要分组看质量指标、效率指标、成本指标对比检索栈时不能只看一个指标。一般来说要分成三组质量指标RecallK、PrecisionK、MRR、nDCG。这些指标衡量的是检索结果里有多少是相关的、排得够不够靠前。效率指标P95 延迟、QPS、索引构建时间。这些决定方案能不能在线上扛住真实流量。成本指标embedding 调用成本、存储成本、推理资源占用。成本在大规模场景里可能是决定性因素。三组指标要放在一起看。某个检索栈召回率高了 2 个百分点但 P95 延迟翻了两倍或者存储成本涨了 40%那就不一定值得切过去。另外指标计算口径要统一。Recall10 在不同项目里可能定义不同有的是正确文档是否出现在前十有的是正确文档占前十的比例有的还考虑多篇正样本的覆盖率。对比之前先明确口径否则数字之间没有可比性。3.3 可复现性固定版本、固定参数、固定数据集对比工具最容易翻车的地方是结果不可复现。今天跑出来一个结果下周再跑一遍数字变了那就没法支撑决策。要保证可复现至少要做三件事固定所有依赖版本包括 embedding 模型版本、索引库版本和算法参数固定评估数据集和评估脚本任何数据改动都要记录固定评测环境包括硬件配置、batch size、并发数等。注意对比检索栈时不要一上来就全量跑。先用小数据集跑通流程确认每个环节都能记录日志、输出指标再逐步扩大规模。否则跑完一个对比实验发现某个参数没固定、某个数据文件被覆盖了整个结果就废了。一个合格的 playground应该让使用者能清楚地知道我上次跑的那组结果是在什么条件、什么参数、什么数据下得到的。没有可复现性的对比结果本质上只是一次性的体感记录。4. 自己动手做检索栈对比评估可以参考的工作流4.1 先定义问题和边界不要急着收集模型很多人的第一步就错了——先找一堆 embedding 模型然后开始跑分。实际上应该先回答几个问题检索要解决什么任务文档是什么类型查询方是谁对延迟和成本的容忍度是多少现有系统的瓶颈在哪个环节这几个问题的答案决定了评估集怎么设计也决定了哪些参数值得花时间调。比如系统瓶颈在召回率那就应该重点对比分块策略、索引参数和不同 embedding 模型如果瓶颈在延迟那可能更值得做量化压缩、索引剪枝和后置过滤。4.2 小规模试点一组基线 每次只改一个变量我比较推荐的做法是分两轮。第一轮先定基线选一个默认的 embedding 模型用最简单的固定分块用暴力索引或者默认 HNSW 参数跑出一组基线指标。第二轮再逐项替换变量先换分块策略再换 embedding 模型再换索引参数再加重排。每轮只改一个变量其他保持不变。这样做的好处是当结果变化时你知道这个变化是由哪个变量带来的。如果同时改了两个变量效果变好了你分不清到底是谁的功劳效果变差了你也找不到该回退到哪一步。下面是一个常见的评估流程结构可以用伪代码表示# 伪代码检索栈对比的最小流程示例 datasets load_eval_query_document_pairs() configs [ {embedding: model_a, chunk_size: 300, index: flat, k: 10}, {embedding: model_a, chunk_size: 500, index: flat, k: 10}, {embedding: model_b, chunk_size: 300, index: hnsw, k: 10}, # 每次只改动一个变量方便定位因果 ] for config in configs: docs chunk_documents(corpus, config[chunk_size]) vectors encode(docs, config[embedding]) index build_index(vectors, config[index]) for query in datasets.queries: results index.search(encode(query), top_kconfig[k]) record_metrics(results, datasets.ground_truth) summarize_metrics() # 汇总 RecallK、MRR、延迟等指标这段代码只是流程示意具体实现要结合你的数据格式和所选索引库。它的核心逻辑是把环境、参数、数据固化下来让每一组对比都能追溯到完整上下文。4.3 从对比结果到生产决策不要只看最好的一组跑完对比之后不要直接选指标最高的一组就上线。还要考虑三件事稳定性最高分那一组是不是只靠运气多跑几次看方差大不大。方差过大说明对数据或者参数敏感生产环境里很容易翻车。边界情况在长文档、短查询、噪声数据这些边界场景里表现如何平均分高不代表各个场景都好。运维成本最优组合是不是需要很大的索引、很贵的 embedding 调用、很复杂的参数维护如果引入的成本远大于收益那最均衡的那组方案可能是更好的选择。更稳妥的做法是把对比结果当成一个候选清单从里面挑出两三组方案做线上小流量验证用真实用户行为数据回头再确认。这也是为什么评估数据集要持续维护——它不是一次性工具而是后续所有优化决策的基础。5. 这类对比 playground 的价值边界以及最值得长期投入的部分5.1 它最适合的三种使用者第一类是刚入门语义检索和 RAG 的人。通过 playground 能直观看到不同变量对结果的影响比自己盲调参数有方向得多。第二类是在做技术选型的团队。面对一堆 embedding 模型、索引库和参数组合团队需要一个可复现的方法来支撑选型决策而不是凭 PPT 和排行榜做判断。第三类是已经在线上运行检索系统、需要持续优化的团队。每次模型升级、数据变化、参数调整都需要重新评估。一套标准化的评估流程长期来看会节省大量时间。5.2 它解决不了的问题也要早点知道对比工具可以告诉你哪一套栈在评估集上表现更好但它不会告诉你业务到底应该用什么评估集。评估集的构建必须由了解业务的人来完成。另外工具对比的是静态的数据切面它对数据分布漂移的感知是滞后的今天的最佳组合半年后数据变了可能就不是最佳了。还有一点容易被忽略自动评估指标和真实用户满意度之间始终存在差距。RecallK 上去了不代表用户一定觉得答案好用延迟达标了不代表用户不会被奇怪的重排结果劝退。指标是重要的参考但不是最终答案。5.3 长期来看评估体系和数据资产比工具本身更重要工具会迭代界面会变化甚至项目的维护状态也会变化。但是一套围绕你业务数据构建的评估体系包括评估数据集、评估脚本、基线和完整的排查流程会成为团队长期的技术资产。以后每次面对要不要换模型要不要调索引这个问题能不能通过换检索栈解决这类问题你都能快速给出基于数据的答案而不是靠感觉。回到 Embench 这个项目本身我没办法在没看到完整代码和文档的情况下评价它的具体功能细节。但它的切入方向是对的把 embedding 和检索栈放在同一个比较框架里让变量可控、让结果可复现、让对比有数据支撑。这类工具的意义不在于替你做出选择而在于让你把选择建立在可以被验证、被追溯、被讨论的事实之上。如果你现在正好在折腾 RAG 或者语义检索我的建议很简单先别急着换 embedding 模型先把你自己的评估集建起来定好基线然后每次只改一个变量用数据说话。这一套流程跑下来你对检索系统的理解会比单纯看排行榜深得多。