ARTICLE DETAIL

资讯详情

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

embedding向量召回实战:从原理到多路融合的工程指南

embedding向量召回实战:从原理到多路融合的工程指南 做搜索和推荐的同学这两年聊到召回几乎都会撞上同一个词embedding。如果说传统搜索靠的是字面匹配那向量检索就是先把文本、图片、用户行为统统压成一串数字再在高维空间里找“长得像”的邻居。我给自己项目做过语义检索、给推荐系统做过 i2i 召回、给 RAG 应用挂过知识库最后发现大多数问题都出在“embedding 向量检索召回”这条链路的理解和配置上。这篇文章不贴 PPT只讲我实际拆过的原理和踩过的坑适合正在做 RAG、搜索召回、推荐召回或者刚接触向量数据库的工程师。1. 为什么 embedding 能用来做召回先把“向量语义”这件事说透1.1 从词到向量一个空间坐标的故事很多人第一次接触 embedding听到的解释是“把文字变成向量”但为什么向量就能表达语义很少有人讲清楚。我习惯用坐标来解释你想象一个城市每一个词都是城市里的一个地点“苹果”和“香蕉”在水果市场旁边“苹果”和“华为”虽然不同类但可能在“电子产品街区”也离得很近。embedding 做的事情就是给每个词安排一个多维空间里的坐标训练的目标是让语义上接近的词坐标距离更近。早期做词向量经典的是 Word2Vec 的 CBOW 和 Skip-gram核心思想其实特别朴素一个词的含义由它周围的词决定。训练时候拿“上下文窗口”里的词去预测中心词或者反过来用中心词预测上下文模型被迫把词编码成向量。后来的句子级模型、双塔模型、对比学习本质都是在放大这个思路把“语义相似的对象”拉近把“不相似的对象”推远。所以 embedding 不是一串随机数字而是经过大规模语料训练得到的语义压缩坐标。同一个词在不同模型里坐标不同不同模型之间的向量不能直接混用这也就是为什么后面会强调“模型版本锁死”这件事。1.2 句子、段落级 embedding检索的是语义不是字面词向量解决了词层面的相似但检索场景里我们处理的是句子、段落、甚至整个文档。句子级 embedding 的做法一般是在词向量基础上做池化mean pooling、max pooling或者直接用预训练模型的 [CLS] 向量再用对比学习优化。举个例子用户搜“怎么开通微信支付”如果走关键词需要分词“开通”“微信”“支付”再去倒排索引匹配。如果走向量检索这条 query 会被编码成一个向量知识库里“绑定银行卡并完成实名认证的步骤”如果语义相近即使没有一个词完全重合也能被召回。这就是向量检索和传统检索最核心的区别匹配的不再是字面而是语义空间里的距离。但这里有一个很容易忽视的点句子 embedding 的质量很大程度上取决于训练数据和训练目标。通用模型在新闻、百科上表现好放到法律条款、医疗问答、代码注释这种垂直领域可能就“飘”了。所以做向量召回前不要默认一个模型跑所有场景。1.3 选 embedding 模型别只看排行榜网上经常看到“embedding 模型排行”说实话我早期也照着榜单选过模型后来发现榜单分数高不代表你的场景效果好。排行榜用的评测集通常是公开的语义相似度数据集或者 MTEB 这类基准但你的业务语料有自己的表达习惯实体名、缩写、乱序描述公开榜单覆盖不到。我一般是这样选型的先把自己业务里最典型的 100 条 query 和对应的正确文档捞出来形成一个小评测集然后用候选模型做召回算 Recall10看哪些 case 召回对了、哪些漏了。选模型时关注几个硬指标向量维度、支持语言、最大输入长度、是否开源、部署难度。下面是我常用的几类模型对比以常见部署版本为例。模型向量维度最大长度语言开源我的使用感受text-embedding-3-large30728191 token 左右多语言否效果强但向量维度高存储和内存成本高BAAI/bge-m310248192中英等多语言是中文场景综合表现稳能处理长文本m3e-base768512 token 左右中文为主是轻量适合快速验证长文本注意截断e5-mistral-7b-instruct4096较长多语言是效果强但太重生产要斟酌延迟这里有几个选型经验一是如果 query 很短、文档很长要确认模型对长文档的编码质量二是看模型是否支持指令前缀像 bge 和 e5 系列查 query 和存文档时往往要加不同前缀不加会掉点三是模型一旦选好就锁版本别随意升级因为不同版本输出向量空间不一致索引得全部重建。这个坑我踩过后面在第五章详细说。2. 向量检索召回是怎么查的从精确最近邻到 ANN2.1 一套完整的向量召回系统长什么样很多人以为向量召回就是把文本塞进模型然后建索引实际上完整链路分两段离线构建和在线检索。离线侧要做的事是把文档切分成合适的块用 embedding 模型批量编码成向量存进向量索引同时保留原始内容字段方便召回后映射回文档。在线侧则是对用户 query 做同样的文本预处理和编码然后去向量索引里做近似最近邻搜索拿到 topK 命中的 chunk再进入后续融合、重排。这个结构可以类比成图书馆离线侧是给每本书做编号、贴标签、摆上书架在线侧是读者报出要找的主题图书管理员根据编号快速圈出可能相关的几个书架而不是把整个图书馆每本书都翻一遍。“编号”就是向量“书架”就是索引结构“快速圈出”就是 ANN 搜索。2.2 相似度度量怎么选余弦、点积还是欧氏距离向量召回本质是找最近邻但“最近”的定义有讲究。最常用的是余弦相似度公式是 cos(a,b) a·b / (|a|·|b|)范围在 [-1, 1]只关心方向不关心长度。点积则直接算 a·b如果你对向量做了 L2 归一化点积就等于余弦相似度这也是很多模型默认建议的做法。欧氏距离则关心向量在空间里的绝对位置数值范围受维度影响大在高维空间里区分度往往不如余弦直观。我实际使用的原则是query 和文档都用同一个模型编码并且统一 normalize_embeddingsTrue然后索引里用内积IP方式建 FAISS搜索时返回的分数就是余弦值。这样分数天然有阈值参考比如 0.6 以上算基本相关0.75 以上算强相关。如果你不归一化所有向量的模长参差不齐点积分数就会偏向“长向量”阈值完全没法统一。2.3 精确 KNN 和 ANN为什么必须做近似如果数据量只有几千、几万条直接暴力算 query 向量和所有文档向量的余弦相似度再排序取 topK完全没问题。但一旦到百万、千万甚至亿级别每次查询都全量计算一次延迟和 CPU 开销都扛不住。精确 KNN 的时间复杂度是 O(N)N 是向量总数这个复杂度在响应要求百毫秒级的在线系统里不可接受。所以工程上普遍用 ANNApproximate Nearest Neighbor近似最近邻来换性能。常见方案有三类基于倒排的 IVF聚类划分先找候选簇再精排、基于量化的 PQ把高维向量压缩成短码牺牲精度换内存、基于图的 HNSW构建分层近邻图搜索时按图跳跃。三者各有适用场景实际选型看数据量和延迟要求。方案核心思想优点缺点典型场景IVF先聚类搜索落在少数簇实现简单、内存可控召回率受聚类中心影响参数要调千万级以下索引可以重建PQ向量压缩成倒置的短码内存占用大幅降低量化有信息损失精度下降亿级以上超大库HNSW多层小世界图召回率高、延迟稳定索引内存大、构建慢百万到千万级在线检索2.4 HNSW 的构图逻辑和关键参数HNSW 是目前落地最普遍的 ANN 算法之一思想很简单把向量组织成多层图上层图稀疏适合快速跳跃到大概区域下层图稠密适合找精确邻居。查询时从最上层入口开始逐层往下走类似跳表。三个参数必须理解M控制每个节点的最大连接数efConstruction是建图时动态候选集大小efSearch是查询时动态候选集大小。M越大图连接越密召回率越高但内存和构建时间也涨。efSearch越大查询越准但延迟越高。efConstruction主要影响建图质量对查询延迟几乎没有副作用只是离线构建慢一点。我自己的经验值千万级向量库M16efConstruction200efSearch100配 1024 维向量单机内存大概 30 到 40 GB线上单次查询延迟在 20 毫秒上下Recall10 能做到 95% 左右。如果延迟充裕可以把efSearch提到 200 换更高召回如果内存吃紧就降M。顺带提醒一句efSearch不是越大越好超过一定阈值后召回率提升非常缓慢延迟却线性上升一定要压在服务能接受的 P99 以内。3. 别让召回只有一条腿多路召回、swing 与 langchain4j 融合实战3.1 向量召回单兵作战的短板向量召回能力很强但它不是银弹。我在生产环境用下来单靠向量召回有几个明显问题第一是漏召回。向量表达的是整体语义处理精确实体、型号、编号时反而容易失效。比如用户搜“iPhone 15 Pro 256G 白色”向量能理解“iPhone 手机”的大方向但对“256G”这种精确属性未必能严格区分导致召回一堆 128G 或别的颜色的商品。第二是时效性。新上架的商品、刚发布的热点新闻如果还没来得及被行为数据训练向量表达就不稳定。第三是热门偏置。如果 embedding 模型是在用户行为序列上训练的高频内容会被压缩到相近区域查询结果容易集中在头部 item长尾内容即使相关也被淹没。第四是“确认式查询”吃亏。用户明确输入了商品编号或合同编号这种时候精确匹配远比语义匹配可靠。所以多路召回不是可选项而是工程常态。3.2 各路召回通道的定位与互补一个成熟召回系统通常同时跑好几路通道每一路负责一种“相关”。我常用的搭配是向量召回负责语义泛化BM25/倒排负责字面精确swing 负责行为协同规则和标签负责运营兜底。召回通道匹配维度适合场景单路特点向量召回语义向量长句、同义改写、跨语言泛化能力强但精确属性弱BM25词项匹配精确型号、编号、专有名词精确但死板无法处理同义词swing用户行为图商品 i2i、新闻关联能挖出语义之外的“买了还买”关系规则/标签人工定义运营强插、类目过滤可控但覆盖有限这里要注意每一路的分数分布完全不可比向量返回的是 0 到 1 之间的余弦值BM25 返回的是 0 到十几的加权分数swing 又是另一套。直接比较没有任何意义必须做融合。3.3 swing 召回原理与工程实现swing 这个词最近在推荐和搜索圈很热它不是新算法但在做 i2i 召回时特别管用。核心思想是利用用户行为序列里的“共现结构”计算两个 item 的相似度。两个商品如果经常在同一批用户的行为序列里出现而且这两个用户的其他交集越小说明这条共现关系越有“信息量”商品越相似。用大白话说如果用户 A 同时买了苹果和橙子用户 B 也同时买了苹果和橙子那就觉得苹果和橙子有点关系但如果整个平台所有用户都同时买过苹果和充电器这种共现就不太能说明问题。swing 通过降低热门 item 的干扰比传统 ItemCF 更抗噪。工程实现上swing 通常离线跑从点击、下单、收藏等行为日志里构建“用户-物品”序列然后计算每对 item 的 swing score取每个 item 的 topN 作为索引。线上直接用 itemId 查表所以延迟很低。它非常适合给没有丰富文本特征的场景做补充比如直播间、非标品、短视频。这里有个心得swing 和向量召回在融合时最好分开看。向量召回的强项是内容语义相似swing 的强项是“行为共现但内容不相似”的关联比如“买打印机的人往往也买 A4 纸”文本上两者完全不相关但行为上强关联。多路融合之后这种互补效果非常明显。3.4 langchain4j 场景下的多路召回组装最近很多人用 langchain4j 做 Java 生态的 RAG 应用它封装了 embedding 模型接入、向量存储、文档切分和检索器等组件。单个 EmbeddingStore 只能提供一路向量召回但在真实业务里知识库问答往往需要“向量召回 关键词召回 规则召回”的组合。我基于 langchain4j 做过多路召回的整合思路是不让框架替你做全部召回而是把框架当成一个底座自己定义多个 Retriever并行执行后合并结果。比如向量 Retriever 去 EmbeddingStore 里查BM25 Retriever 去倒排索引里查规则 Retriever 去标签表里查。每个 Retriever 返回带来源 id 和原始内容的结果最后统一做 RRF 融合。大致结构可以这样理解// langchain4j 场景下的多路召回示意接口名称以你使用的版本为准 ContentRetriever vectorRetriever new VectorStoreRetriever(vectorStore, embeddingModel); ContentRetriever bm25Retriever new Bm25Retriever(invertedIndex); ContentRetriever ruleRetriever new RuleRetriever(tagIndex); // 并行执行各路召回 ListListContent results retrievers.parallelStream() .map(r - r.retrieve(request)) .collect(Collectors.toList()); // 进入融合再交给后续生成或重排 ListContent merged RrfMerger.merge(results);这里要注意langchain4j 内置的 VectorStoreRetriever 通常只做向量召回你不要指望它帮你做多路融合。多路召回的分工和融合策略需要自己在业务代码里实现框架只是减少了你对接 embedding 和向量存储的工作量。3.5 RRF 分数融合与阈值策略多路召回各自返回 topN 后最常见也是最稳的融合方法是 RRFReciprocal Rank Fusion倒数排名融合。它的核心思路是不看绝对分数只看每条结果在每一路里的排名。公式是score(item) Σ 1 / (k rank_i)其中 k 是平滑常数一般取 60。把每路返回的 item 排名套进去累加得到综合分再按综合分排序取 topK。举个例子假设有三路召回向量路返回 [A, B, C]BM25 路返回 [C, A, D]swing 路返回 [E, A]。取 k60A 在三路里的排名分别是 1、2、1所以 A 的融合分是 1/61 1/62 1/61 ≈ 0.049。B 只在第一路出现排名第 2得分只有 1/62 ≈ 0.016。最后 A 赢因为它在多路里都稳定出现。这个方法的优点是鲁棒不需要做分数归一化也不会被单路高分带偏。阈值策略上每路先各自截断 topN比如向量取 50 条、BM25 取 30 条、swing 取 20 条融合后再截断 topK。这里的 topK 取决于下游精排能力如果后面有 reranker召回可以放大到 200 甚至 500让精排去挑如果后面没有重排topK 就要控制在 20 以内。4. 从零跑通 embedding 向量召回索引构建、查询与评估4.1 数据切分与向量化先说切分。向量召回的效果很大程度取决于文档切分我见过太多人把整篇三千字文档直接塞进 embedding 模型然后抱怨召回不准。模型有最大输入长度超长文本会被截断关键信息可能在尾巴上被丢掉。正确做法是先把文档按段落、标题层级或固定窗口切分成小的 chunk每个 chunk 控制在 200 到 500 个 token 之间相邻 chunk 之间可以留少量重叠避免句子被拦腰截断。切分完成后用统一的模型做批量向量化。这里以 Python 生态为例我用 sentence-transformers 加载一个开源模型from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-m3) docs [ 向量检索是搜索引擎的第一环负责从海量候选里快速圈出相关内容。, HNSW 是一种基于图的近似最近邻算法适合在线低延迟检索。, # ... 更多 chunk ] doc_embeddings model.encode(docs, normalize_embeddingsTrue) print(doc_embeddings.shape)这里 normalize_embeddingsTrue 是关键加了之后向量的模长为 1后续内积就等于余弦相似度分数更好解释。至于批量大小要根据显存或内存来定我的习惯是先拿 100 条试试内存占用再逐步加到 512 或 1024。4.2 索引构建与检索向量化完成后把向量写入索引。数据量不大、开发验证阶段直接用 FAISS 就行数据量大了再考虑 Qdrant、Milvus、Elasticsearch 这类服务或者公司自建的向量引擎。import faiss import numpy as np dim doc_embeddings.shape[1] index faiss.IndexFlatIP(dim) # 归一化后用内积等价于余弦 index.add(doc_embeddings)查询时query 也要走完全一样的预处理和编码然后用同一个索引搜query HNSW 算法为什么适合在线召回 query_embedding model.encode([query], normalize_embeddingsTrue) scores, indices index.search(query_embedding, k10) for score, idx in zip(scores[0], indices[0]): print(score, docs[idx])这里有个很容易犯的错query 和文档如果来自同一个模型但模板前缀不一致会导致分布偏移。像 bge 和 e5 系列文档和查询通常要用不同的指令比如“为这个句子生成表示以用于检索相关文档”和“查询”。训练时怎么用的推理时就必须怎么用否则真的会掉点。4.3 效果评估构建评测集和计算指标向量召回上线前一定要有评测集不然你根本不知道模型和参数改动是变好还是变坏。我的习惯是从线上日志和人工标注里抽 300 到 500 条 query每条标注 1 到 3 个正确文档 id组成一个静态评测集。然后跑一遍召回流程计算 RecallK 和 MRR。RecallK 的含义是前 K 个结果里包含正确文档的比例占总 query 的比例。MRR 是第一个正确结果排名的倒数再取平均能反映“是否把正确答案放在前面”。def evaluate_retrieval(queries, gold_docs, index, docs, k10): recall_sum, mrr_sum 0.0, 0.0 for q, gold in zip(queries, gold_docs): q_emb model.encode([q], normalize_embeddingsTrue) _, indices index.search(q_emb, k) retrieved [docs[i] for i in indices[0]] hit_pos None for pos, doc in enumerate(retrieved): if doc in gold: hit_pos pos 1 break if hit_pos: recall_sum 1 mrr_sum 1 / hit_pos n len(queries) return recall_sum / n, mrr_sum / n这个脚本看起来简单但它是所有调优的基础。我每次换 embedding 模型、调 HNSW 参数、改切分策略都先跑一遍这个脚本用数字说话而不是靠感觉。这里又回到第一章说的不要只看榜单用自己的数据实测。4.4 和 langchain4j 集成时的实操要点在 Java 项目里用 langchain4j 时流程跟 Python 类似先加载 embedding 模型再把文档转成 TextSegment 存入 EmbeddingStore查询时用 EmbeddingModel 编码 query然后从 store 里搜索。以我常用的整合方式为例代码思路大概是这样// 示意代码接口名字以你依赖的 langchain4j 版本为准 EmbeddingModel embeddingModel OpenAiEmbeddingModel.builder() .apiKey(your-key) .modelName(text-embedding-3-large) .build(); EmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); for (String text : chunks) { TextSegment segment TextSegment.from(text); store.add(embeddingModel.embed(segment).content(), segment); } Embedding queryEmbedding embeddingModel.embed(query).content(); ListEmbeddingMatchTextSegment matches store.findRelevant(queryEmbedding, 10);这里有几个实操要点一是如果用的是本地模型需要自己封装一个 EmbeddingModel 实现接 Hugging Face 模型或者 vllm 服务二是 store 初始化后要及时持久化InMemoryEmbeddingStore 重启就没了生产环境切到支持持久化的向量存储三是多路召回时不要在 langchain4j 默认流程里硬塞最好自建检索器组合像第三章那样做并行召回和 RRF 融合。5. 上线后最容易踩的 5 个坑问题与排查实录5.1 召回结果“看着像其实不对”这是向量召回最典型的失败模式。我遇到过的 case用户搜“2024 年公司年会策划方案”系统召回了一堆“年会抽奖规则”“年会主持稿”看着都沾边但用户其实要的是整体方案。排查时先做三件事第一看切分是不是文档被切成了碎片导致 chunk 语义不完整第二看模型是不是通用模型理解不了业务黑话换垂直领域微调或用更强的指令模型试试第三看阈值如果相似度低于 0.6 的结果也被放进来噪音自然多。我的经验是先不要急着调模型先把切分和阈值这两个因素排除掉往往解决一大半问题。5.2 向量检索延迟压不下来线上延迟高常见原因有这几个向量维度太高、HNSW 的 efSearch 太大、topK 设置过大、索引内存命中率低。1536 维甚至 3072 维的向量内积计算本身就比 768 维慢如果业务对延迟敏感可以换 1024 维模型或者用 PQ 量化压缩向量。我调优的顺序一般是先看 P99 延迟曲线确认是 CPU 计算瓶颈还是内存访问瓶颈然后把 efSearch 从 100 慢慢降到 40观察 Recall10 的掉点幅度如果掉点不严重就保持小 efSearch。如果 topK 需要 500但业务最终只用 20那不如让向量召回只出 100 条换 BM25 和 swing 来补覆盖。5.3 模型升级后索引全部失效这个坑我踩得特别深。某次我把 embedding 模型从 V1 升级到 V2没有重新建索引线上查询质量暴跌。原因是两个模型输出的向量空间不一致V1 的向量和 V2 的 query 向量在空间里根本不对齐近似检索完全是错的。正确做法是模型版本升级必须伴随全量索引重建并且建议灰度切流。先建好一套 V2 的新索引验证评测集效果确认无问题后再用“双跑对比”的方式把流量切过去。索引重建期间旧索引继续服务新索引离线构建最后一次性切换。如果你用的是自研切分或者文本预处理规则也要一并版本化。5.4 冷启动与热门偏置新商品、新文档刚进系统时没有行为数据swing 召回完全失效如果文本特征也不丰富向量召回的向量表达也可能很差。我的处理方案是保留一路规则召回和一路 BM25 召回作为兜底确保冷启动内容也能被精确词或者人工标签打到。热门偏置问题更隐蔽。如果向量模型是在点击行为上训练的高频 item 会被“过度压缩”到中心区域导致任何 query 都容易召回热门内容。缓解的办法是训练和构建索引时做降权采样或者融合时给长尾内容加一点权重或者干脆用纯内容语义模型而不是行为模型来做向量召回行为层面的相似交给 swing 负责。5.5 多路融合的“贡献度黑洞”多路召回上线后很容易出现一种情况总效果没有提升甚至变差但你不知道是哪一路拖后腿。我一开始也遇到过后来养成了一个习惯每一路召回的结果都要带 channel 标识线上日志里记录每个结果的频道来源然后定期分析融合后 top20 里各路占比和单独跑某一路的指标对比。如果某一路长期在融合结果里占比极低说明它可能是“空转”白白增加延迟如果某一路单独指标不错但融合后反而不出来说明 RRF 的 k 值或者各路 topN 比例需要调整。我常用的做法是先固定一个基线版本然后每次只改一路上线观察用 abs 实验衡量整体 GMV 或者点击率而不是只看离线 Recall。多路召回不是越多越好每一路都要能证明自己的增量价值。6. 最后一点个人体会做 embedding 向量检索召回这几年我最大的体会是向量召回不是一锤子买卖它是整条搜索或推荐链路里的一个“强路由”负责把候选集从千万缩到几百但真正决定用户体验的还有后面的融合、排序和业务规则。我自己现在的标配是好用的 embedding 模型选型 多路召回 RRF 融合 一个轻量 reranker而不是把宝全押在向量相似度上。如果你刚入门可以先别急着接大规模向量数据库用 FAISS 加几千条数据把切分、向量化、检索、评测这条闭环跑通感受一下哪里容易出问题。等你对相似度分数、ANN 参数、召回评估都有了手感再上多路融合和更重的架构。踩过几次坑之后你会发现向量检索的原理并不复杂复杂的是把每一个细节都做对。
返回列表