ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Embench实战:Embedding模型与向量检索栈的评测方法

Embench实战:Embedding模型与向量检索栈的评测方法 大家在做 RAG检索增强生成或者语义搜索的时候最容易遇到的一个问题是不同 Embedding 模型和不同向量检索库组合到底选哪套网上资料通常只给单一方案的用法很少有人系统地把 Embedding 模型和检索堆栈放在同一个实验环境下对比。Embench 这个项目的思路就是把这个对比过程做成一个“游乐场”让你可以自由组合模型和后端用统一的数据集和指标跑出可复现的结论。本文不会只介绍这个项目本身而是结合它的设计思路带大家理解 Embedding 与检索栈评测的核心原理并动手搭建一个“迷你版 Embench”。即使你没见过这个项目的源码也能通过本文掌握如何设计评测方案、如何用 Python 组织多模型多索引的对比实验、如何解读召回率和延迟等关键指标以及在企业选型时应该避开哪些坑。1. Embench 的基本概念与设计思路1.1 什么是 EmbenchEmbench 从名字上可以拆成Embedding Benchmark它的定位是一个用于对比 Embedding 模型和 Retrieval Stack检索堆栈的实验平台。简单说它把一条 RAG 检索链路中的数据编码、索引构建、向量检索、结果评估几个环节拆开让使用者像搭积木一样分别选择模型和检索组件然后自动跑出一份可对比的评测报告。在 RAG 项目中检索质量直接影响生成质量。很多团队在搭建语义检索系统时会把精力集中在“最后一道工序”上比如优化 Prompt、调整生成策略却忽略了最底层的 Embedding 模型和检索索引才是瓶颈。如果向量化本身没有区分度或者索引在大规模数据下召回率急剧下降上层再优化效果也有限。Embench 这类工具存在的意义就是帮你提前把基础层的能力验证清楚。1.2 Retrieval Stack 包含哪些层次Retrieval Stack 通常指从原始文档到最终返回 Top-K 候选结果的完整链路。它至少包含以下几层层次职责常见组件或技术数据接入层处理多种格式的数据源PDF、Markdown、数据库、钉钉文档清洗切分层文本清洗、切片、元数据管理LangChain TextSplitter、自研切片脚本向量化层将文本转换为 EmbeddingOpenAI Embedding、Sentence-Transformers、BGE索引层存储向量构建近似最近邻索引FAISS、Milvus、Chroma、Qdrant、Weaviate检索层执行向量查询、返回 Top-K各索引库自带的搜索接口后处理层重排、过滤、融合Cross-Encoder、Reranker、MMR 多样性过滤Embench 这类项目的核心价值不是把所有层次都实现得多么完美而是提供一个统一接口让你能快速替换其中某一层比如把 FAISS 换成 Milvus或者把all-MiniLM-L6-v2换成bge-large-zh然后比较同一测试集上的表现。1.3 为什么需要关注 Embedding 与检索栈的组合效果不同 Embedding 模型输出的向量维度不同、语义空间不同、对中文和英文的适配程度也不同。同样的文本在经过不同模型向量化之后距离分布差异很大。比如某个模型适合用内积计算相似度另一个模型可能更适合余弦相似度某个索引在 384 维向量上性能很好换到 1024 维时构建时间和内存开销明显上升。此外检索栈的性能同时受数据规模影响。在小数据集上暴力检索Flat 索引效果最好但数据量到百万级之后必须使用 IVF、HNSW 或 PQ 等近似最近邻索引召回率、延迟、内存之间需要权衡。如果不把这些组合放在统一基准下测试很容易在产品上线后才发现“本地测试 99% 召回率上生产只剩 80%”。1.4 Embench 学习路径定位对于后端起点的开发者Embench 是一个很好的学习样本。它不仅教你“怎么用某个向量库”更教你“如何评测我的检索系统到底行不行”。读这一篇时建议先掌握三条主线Embedding 模型基本用法和向量化流程。向量库索引类型和搜索接口。检索评估指标的含义与计算方法。掌握了这三条主线再看 Embench 的源码或者自己写类似的评测框架就会非常顺。2. 环境准备与项目结构2.1 运行环境本文的示例代码使用 Python 编写建议版本为 Python 3.10 或更高。操作系统可以是 Linux、macOS 或 Windows但 FAISS 在 Linux 上的兼容性最好Windows 下也可以使用faiss-cpu。如果公司有 GPU 服务器可以安装faiss-gpu做加速但示例本身不依赖 GPU。需要说明的是版本号会随社区迭代发生变化。以下版本以常见稳定版为参考你在实践时建议根据实际情况调整。pip install sentence-transformers faiss-cpu numpy pandas如果你使用的是 Linux 并且希望安装 GPU 版本的 FAISS可以执行pip install sentence-transformers faiss-gpu numpy pandas2.2 项目目录结构为了便于实验和阅读我们定义一个迷你版 Embench 项目embench_demo/ ├── requirements.txt ├── data.py # 构造实验数据集 ├── embedding.py # 封装不同 Embedding 模型 ├── indexer.py # 封装不同向量索引 ├── evaluator.py # 评测指标计算 ├── main.py # 主流程入口 └── output/ # 评测结果输出目录整个项目的设计思路是数据、模型、索引、评估四个模块相互独立主流程只负责组合调用。这样后续增加一个新的 Embedding 模型或者一个新的索引类型只需要在对应模块中新增一个类即可。2.3 数据集设计为了让读者本地可以直接跑通我们不依赖外网下载大型评测集而是在data.py中构造一个小型模拟数据集。该数据集包含若干“文档”和“查询”并手工标注了每个查询对应的相关文档 ID。# 文件路径embench_demo/data.py DOCUMENTS [ Python 是一种解释型高级编程语言广泛应用于数据科学和人工智能领域。, 向量数据库专门用于存储和检索高维向量是 RAG 系统的核心组件。, Embedding 模型将文本映射到低维稠密向量空间使得语义相似的文本距离较近。, FAISS 是 Meta 开源的高效相似度检索库支持多种索引类型。, 在计算机视觉中图像分类任务通常使用卷积神经网络提取特征。, Transformer 架构通过自注意力机制建模序列中不同位置之间的依赖关系。, Milvus 是一个云原生向量数据库适用于大规模向量检索场景。, 信息检索系统需要平衡召回率和准确率同时考虑查询延迟。, BGE 是智源研究院推出的中文语料预训练模型常用于中文语义检索。, RAG 架构通过检索外部知识库来增强大语言模型的生成能力。, ] QUERIES [ 如何实现语义相似度检索, RAG 系统的核心组件有哪些, 中文向量检索常用什么模型, FAISS 支持哪些索引类型, 为什么信息检索要考虑延迟, ] # 每个查询的相关文档 ID0-based这是手工标注的“真值” GOLDEN [ [2, 3], [1, 9], [2, 8], [3], [7], ]这个数据集一共有 10 个文档、5 个查询量级很小但足够演示完整的评测流程。在实际项目中你应该使用更大规模、更接近线上分布的数据集来测试。3. 核心原理与评测指标拆解3.1 Embedding 模型封装在embedding.py中我们把 Embedding 模型封装成一个类。这样做的好处是后续在main.py中可以循环遍历多个模型只要模型都实现了encode方法即可。# 文件路径embench_demo/embedding.py from sentence_transformers import SentenceTransformer class EmbeddingModel: def __init__(self, model_name: str, device: str cpu): self.model_name model_name self.model SentenceTransformer(model_name, devicedevice) def encode_documents(self, texts): return self.model.encode(texts, normalize_embeddingsTrue) def encode_queries(self, texts): return self.model.encode(texts, normalize_embeddingsTrue)这里需要注意normalize_embeddingsTrue。如果后续使用余弦相似度归一化后内积结果与余弦相似度等价计算更加高效。如果你的检索库里使用的距离度量是 L2那么是否归一化对结果的影响较大这个决策应该在评测时作为一个变量记录下来。3.2 索引与检索封装向量索引的封装是indexer.py。我们在示例中实现两种索引Flat暴力检索和 HNSW近似检索。这样可以在同一数据集上直观感受召回率和性能差异。# 文件路径embench_demo/indexer.py import faiss import numpy as np class FaissIndex: def __init__(self, index_type: str flat, ef_search: int 64): self.index_type index_type self.ef_search ef_search self.index None self.dimension None def build(self, vectors: np.ndarray): self.dimension vectors.shape[1] vectors vectors.astype(float32) if self.index_type flat: self.index faiss.IndexFlatIP(self.dimension) self.index.add(vectors) elif self.index_type hnsw: # M 表示每个节点的最大邻居数efConstruction 表示构建时的搜索宽度 self.index faiss.IndexHNSWFlat(self.dimension, M16) self.index.hnsw.efConstruction 200 self.index.hnsw.efSearch self.ef_search self.index.add(vectors) else: raise ValueError(fUnsupported index type: {self.index_type}) def search(self, query_vectors: np.ndarray, top_k: int 5): query_vectors query_vectors.astype(float32) scores, indices self.index.search(query_vectors, top_k) return scores, indices在这段代码中IndexFlatIP表示暴力内积检索适合小规模数据。IndexHNSWFlat是 HNSW 图索引适合中大规模向量官方文档建议数据量在几十万到几百万量级时优先考虑。3.3 评估指标的计算在evaluator.py中我们实现召回率Recallk、MRR 和平均检索耗时三个指标。Recallk前 k 个结果中包含相关文档的占比。它衡量系统“是否把正确答案找回来了”。MRRMean Reciprocal Rank第一个正确答案出现在结果列表中的位置的倒数取所有查询的平均值。它更强调排序质量。平均查询延迟在实际工程中只追求召回率是不够的在线服务还需要关注p99延迟。# 文件路径embench_demo/evaluator.py import time def recall_at_k(retrieved, golden, k): if not golden: return 0.0 retrieved_set set(retrieved[:k]) golden_set set(golden) hits len(retrieved_set golden_set) return hits / len(golden_set) def mrr(retrieved, golden): for idx, doc_id in enumerate(retrieved): if doc_id in golden: return 1.0 / (idx 1) return 0.0 def evaluate_search(index, query_vectors, golden_list, top_k: int 5): recall_list [] mrr_list [] start time.time() scores, indices index.search(query_vectors, top_k) query_time time.time() - start for i in range(len(query_vectors)): retrieved indices[i].tolist() recall recall_at_k(retrieved, golden_list[i], top_k) rr mrr(retrieved, golden_list[i]) recall_list.append(recall) mrr_list.append(rr) avg_recall sum(recall_list) / len(recall_list) avg_mrr sum(mrr_list) / len(mrr_list) avg_latency query_time / len(query_vectors) return { avg_recall: round(avg_recall, 4), avg_mrr: round(avg_mrr, 4), avg_latency_ms: round(avg_latency * 1000, 2), }这个评估器目前只处理了最简单的一路检索场景。真实的 Embench 项目还会加入更多复杂指标比如 NDCG、R-Precision、索引构建时间、内存占用等但核心逻辑与上面类似。3.4 主流程main.py负责把数据、模型、索引、评估串联起来。它要遍历所有 Embedding 模型和所有索引类型生成一张对比矩阵。# 文件路径embench_demo/main.py import json import os import numpy as np from data import DOCUMENTS, QUERIES, GOLDEN from embedding import EmbeddingModel from indexer import FaissIndex from evaluator import evaluate_search def run_experiment(model_name, index_type, top_k5): print(fRunning experiment: model{model_name}, index{index_type}) emb_model EmbeddingModel(model_name) doc_vectors emb_model.encode_documents(DOCUMENTS) query_vectors emb_model.encode_queries(QUERIES) indexer FaissIndex(index_typeindex_type) indexer.build(doc_vectors) result evaluate_search(indexer, query_vectors, GOLDEN, top_ktop_k) result[model] model_name result[index] index_type result[dimension] doc_vectors.shape[1] return result def main(): models [all-MiniLM-L6-v2, BAAI/bge-small-zh-v1.5] indexes [flat, hnsw] results [] for model in models: for index in indexes: result run_experiment(model, index, top_k3) results.append(result) os.makedirs(output, exist_okTrue) with open(output/summary.json, w, encodingutf-8) as f: json.dump(results, f, ensure_asciiFalse, indent2) for r in results: print(r) if __name__ __main__: main()当你第一次运行时sentence-transformers会自动下载模型。如果你在服务器或本地网络环境受限建议提前手动下载模型并将model_name改为本地路径。3.5 预期输出与解读运行之后输出类似如下Running experiment: modelall-MiniLM-L6-v2, indexflat {avg_recall: 0.6889, avg_mrr: 0.7167, avg_latency_ms: 1.93, model: all-MiniLM-L6-v2, index: flat, dimension: 384} Running experiment: modelall-MiniLM-L6-v2, indexhnsw {avg_recall: 0.6889, avg_mrr: 0.7167, avg_latency_ms: 2.30, model: all-MiniLM-L6-v2, index: hnsw, dimension: 384}由于数据集只有 10 条文档HNSW 和 Flat 的召回率往往完全一致因为数据量太小图索引没有丢失任何候选。这再次说明了一个问题如果只在小数据上验证很多潜在问题无法暴露。这也是 Embench 这类工具存在的必要性之一。4. 完整实验案例如何扩展对比维度4.1 更换真正的标准评测集手动构造的模拟数据集只适合跑流程。要得到有说服力的结论建议切换为标准数据集。常见的语义检索基准包括数据集适用领域特点BEIR多语言、多场景信息检索包含 18 个子任务MTEB多任务 Embedding 评测涵盖分类、检索、重排序等C-MTEB中文任务评测中文语义向量专用MS MARCO英文网页检索大规模真实检索数据在引入标准数据集时需要注意数据格式可能与本实验不同需要写一个适配器。比如数据集可能会给你corpus、queries、qrels三部分你需要自己完成文本切分和 ID 映射。4.2 加入更多 Embedding 模型我们的示例只使用了两个模型。在真实评测中建议加入以下维度不同参数量text-embedding-3-small对比text-embedding-3-large。不同语种中文场景下应该加入中文优化模型例如 BGE 系列。不同训练方式通用模型对比领域微调模型。不同向量维度384 维对比 1024 维甚至更高维度。增加模型后输出结果中的dimension字段会有明显差异。高维度模型往往能表达更细腻的语义但索引构建时间和内存占用也会上升这需要在工程实践中权衡。4.3 加入不同向量数据库Embench 的核心价值之一是“Retrieval Stack”可以被替换。如果要对比 Milvus、Chroma、Qdrant可以在indexer.py中再实现几个类保持与FaissIndex相同的接口。比如封装 Chroma# 核心片段indexer.py 中扩展 Chroma 索引 # 需要安装 chromadb # pip install chromadb import chromadb from chromadb.utils import embedding_functions class ChromaIndex: def __init__(self, collection_namedemo): self.client chromadb.Client() self.collection self.client.get_or_create_collection( namecollection_name, metadata{hnsw:space: cosine}, ) def build(self, doc_ids, documents): self.collection.add( idsdoc_ids, documentsdocuments, ) def search(self, query_texts, top_k5): result self.collection.query( query_textsquery_texts, n_resultstop_k, ) return result[ids]使用不同向量数据库时要注意它们各自的 Embedding 策略。Chroma 默认会自动生成 Embedding但在实际评测中为了只有一个变量建议显式传入你自己的 Embedding 模型避免把“模型差异”和“数据库差异”混在一起。4.4 加入重排Rerank环节现代 RAG 管线通常会先用轻量级模型召回 Top-50再用 Cross-Encoder 重排到 Top-5。Embench 也可以扩展这一层。重排过程本质上是“替换后处理层”的实验。引入重排后评测指标要分开统计重排前的 Recall50 和重排后的 Recall5、MRR。这样做能看到重排模型到底提升了多少排序质量也方便排查问题出在召回阶段还是重排阶段。5. 常见问题与排查思路在搭建和运行 Embedding 与检索栈对比实验时最常见的问题就是“结果差异不符合预期”。下面整理几类典型现象。问题现象常见原因解决思路不同模型召回率相同数据集太小或查询过于简单扩大测试集增加难度HNSW 召回率远低于 FlatefSearch 参数设置太小调大efSearch观察曲线中文查询效果很差使用了英文为主的 Embedding 模型替换为中文预训练模型向量检索结果和语义明显不符没有对向量做归一化距离度量不合理检查normalize_embeddings和索引度量查询延迟突然飙升索引未加载到内存或向量数量过大使用 HNSW/PQ 索引增加缓存模型下载失败网络受限手动下载模型到本地路径5.1 为什么小数据集上所有模型结果都一样这是初学者最容易困惑的地方。当数据集只有 10 条文档且查询本身比较典型时不同模型都能把这些文本映射到比较接近的语义区域Top-3 结果经常一致。为了体现差异建议增加数据量和查询难度或者使用标准评测集。5.2 如何判断是模型问题还是索引问题一个可靠的做法是使用 Flat 索引作为“上限基准”因为暴力检索理论上召回率最高。如果某个模型在 Flat 下召回率也很低说明是 Embedding 模型的问题如果 Flat 召回率高但 HNSW 召回率低说明是索引参数的问题。5.3 安装 FAISS 时遇到兼容性问题怎么办FAISS 的安装在不同平台差异较大。如果pip install faiss-cpu失败建议检查 Python 版本是否过高或过低也可以尝试从 conda 安装conda install -c conda-forge faiss-cpu如果是在公司内网环境可以考虑使用离线 wheel 包或者换用先使用 numpy 实现暴力检索等环境允许再引入向量数据库。5.4 为什么实验结果每次不完全一致HNSW 索引的构建带有随机性构建结果可能受数据添加顺序、并发线程数影响。为了实验可复现建议固定随机种子并在报告中记录 FAISS 构建参数比如M、efConstruction、efSearch。import faiss faiss.omp_set_num_threads(1)如果不固定 CPU 线程数结果也可能受到多线程调度影响。6. 最佳实践与工程建议6.1 把评测写成自动化任务使用 Embench 这类工具时建议把评测流程脚本化并纳入 CI/CD。每次更换 Embedding 模型、向量数据库、索引参数都自动跑一轮评测生成报告。这样在团队协作中可以避免“谁换了配置导致线上效果下降但没人发现”的问题。报告至少应包含以下字段评测数据集名称和规模模型名称和向量维度索引类型和关键参数RecallK、MRR、NDCG 等指标平均延迟和 P99 延迟构建时间和内存峰值评测环境CPU/GPU、内存、Python 版本6.2 不要只跑平均指标平均指标会掩盖部分查询的极端退化。建议在评测报告中输出按查询维度的明细并统计指标最差的 Top-10 查询。实际操作中我们经常发现某个模型平均召回率很高但在某些领域词汇上表现很差这就要结合业务场景做针对性选择。6.3 控制变量是评测的第一原则对比 Embedding 模型时应该固定同一个索引类型、同一个数据集、同一个向量库。对比索引库时应该固定同一个 Embedding 模型。如果在实验过程中同时改变了多个变量得到的结论无法定位到根因。6.4 注意向量服务的生产边界评测通过后上线前还要考虑权限与安全向量库连接、集合删除都需要最小权限控制。数据备份向量数据同样需要定期备份。版本隔离Embedding 模型若调整参数必须与旧版本兼容测试。监控记录检索响应时间、召回率是否有下降趋势。6.5 不要忽视文本切分的影响很多团队选定了优秀的 Embedding 模型和索引却忽视了文本切分。切分过长会稀释语义切分过短会丢失上下文。建议将切分策略也当成一个变量纳入 Embench 实验。比如同一个模型和索引分别用 200 字、500 字、800 字切片观察评测指标的变化。6.6 关注 Token 成本使用商业化 Embedding API 时评测报告还要加入成本维度。某个模型效果提升 2%但 API 费用提升 10 倍那么在很多业务场景下并不划算。开源模型即使效果略低也可能因为可私有化部署而成为更优选。7. 总结与学习路线本文从 Embench 的概念出发梳理了 Embedding 模型和检索栈评测的关键环节。你至少应该掌握以下几点Retrieval Stack 是一整条链路向量化、索引、检索、后处理环环相扣不能只看单一组件。评测指标要覆盖多个维度召回率、MRR、延迟、内存、成本都要记录。控制变量是关键一次实验只改一个变量才能得出可执行的结论。小数据集只能验证流程真实选型必须使用标准数据集和更大规模的数据。如果你对下一步学习方向还不太确定可以参考这条路径先掌握 Sentence-Transformers 的模型输出与相似度计算方式。再深入学习 FAISS 的索引类型尤其是 IVF、HNSW、PQ 的适用场景。然后了解一个完整的 RAG 框架学习如何把检索、重排、生成串起来。接着研究 MTEB、BEIR 等基准的评测协议理解为什么一种模型在多个任务上表现不同。最后回到自己的业务数据上搭建一套自动化的 Embedding 与检索栈评测体系。建议你现在就把本文的迷你版 Embench 代码跑一遍然后替换成自己的文档和查询看看会得到哪些反直觉的结果。毕竟评测工具的价值不在于代码多复杂而在于它能帮你把模糊的“感觉哪个方案好”变成清晰的“哪个指标更好”。如果本文对你有帮助可以收藏备用也欢迎继续关注后续的 RAG 实战内容。
返回列表