news 2026/7/13 16:33:03

语义搜索架构:召回、粗排、精排三阶段缺一不可

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
语义搜索架构:召回、粗排、精排三阶段缺一不可

语义搜索架构:召回、粗排、精排三阶段缺一不可

一、为什么只靠向量相似度搜索不够

向量检索(Vector Search)是语义搜索的基石。将查询和文档都编码为向量,通过余弦相似度或欧氏距离找最相近的 Top K 文档。这套方案的简洁性使得它在 RAG 系统中被大量采用。

但在实际使用中,向量检索有三个明显缺陷。第一,embedding 模型对语义的理解有限,长文本的全局向量可能丢失关键细节。第二,相似度最高的文档不一定是"最相关"的文档——向量空间中的近邻可能只是词汇层面相似。第三,跨领域的零样本泛化能力不足,在训练集未见过的领域,检索质量断崖式下降。

搜索系统的目标不是"找到最像的",而是"找到最相关的"。这两个目标在向量空间中不是完全重合的。弥补这一差距的方法,就是多阶段检索架构:用不同粒度、不同成本的方法,逐级筛选候选集。

见证奇迹的时刻是:当召回阶段用向量检索抓"海选",粗排阶段用轻量模型筛"候选",精排阶段用重量级模型定"最终排名"——三阶段配合后,检索准确率比单一向量搜索提升了 30%~50%。

二、三阶段检索架构的设计原理

graph TD A[用户查询] --> B[查询理解模块] B --> B1[查询改写与扩展] B --> B2[意图分类与实体抽取] B1 --> C[第一阶段: 召回] B2 --> C C --> C1[向量检索<br/>Top K=1000] C --> C2[关键词检索<br/>BM25, Top K=100] C --> C3[知识图谱检索<br/>实体链接] C1 --> D[第二阶段: 粗排] C2 --> D C3 --> D D --> D1[轻量 Cross-encoder<br/>Top K=50] D --> D2[规则特征: 时效性/热度/权威性] D1 --> E[第三阶段: 精排] D2 --> E E --> E1[重量级 Cross-encoder<br/>逐条精排] E --> E2[多维度特征融合<br/>相关性+时效+权威+多样性] E1 --> F[最终结果: Top K=10] E2 --> F style C fill:#e1f5fe style D fill:#fff3e0 style E fill:#e8f5e9 style F fill:#f3e5f5

第一阶段:多路召回。不是单一的向量检索,而是向量、关键词和知识图谱三条通路同时召回候选文档。三条通路互为补充:向量检索捕捉语义相似性,关键词检索保证字面匹配的精确性,知识图谱检索利用结构化知识做实体关联。多路召回保证了高召回率(Recall > 95%),但精确率很低(候选集中 90% 以上是不相关的)。

第二阶段:粗排(Re-ranking Lite)。对召回的 1000+ 候选文档,用轻量级双塔模型(Query-Document BERT)快速打分,筛到 Top 50。粗排模型的速度要求很高——每条文档的处理时间必须在 1~5 毫秒级别。精度不如精排,但速度够快,可以过滤掉明显不相关的文档。

第三阶段:精排(Fine Ranking)。对 Top 50 候选文档,用重量级的 Cross-encoder(如交互式 BERT)逐条精排。精排模型将查询和文档拼接后送入模型做全注意力交互,精度最高,但速度最慢(每条 20~50 毫秒)。通过前两阶段的筛选,精排阶段只需要处理 50 条文档,总耗时在可接受范围内。

三、三阶段检索引擎的实现

以下代码展示了简化版的三阶段检索管道。

import numpy as np from typing import List, Dict, Tuple, Any from dataclasses import dataclass @dataclass class SearchResult: """搜索结果""" doc_id: str content: str score: float source: str # 召回来源: vector/keyword/kg class MultiStageSearcher: """三阶段语义搜索引擎 设计原因:将检索拆分为三个阶段,用逐渐增加的模型精度 换取逐渐缩小的候选集规模。三个阶段的精度-速度关系: 召回(高召回+低速度+大集)-> 粗排(中精度+高速+中集) -> 精排(高精度+低速+小集)。 """ def __init__(self, embedding_model, bm25_index, light_reranker, heavy_reranker): self.embedding_model = embedding_model self.bm25_index = bm25_index self.light_reranker = light_reranker self.heavy_reranker = heavy_reranker def search(self, query: str, top_k: int = 10) -> List[SearchResult]: """执行三阶段搜索""" # 阶段1: 多路召回 candidates = self._multi_recall(query) # 阶段2: 粗排 reranked = self._light_rerank(query, candidates) # 阶段3: 精排 final = self._heavy_rerank(query, reranked[:50], top_k) return final def _multi_recall(self, query: str) -> List[SearchResult]: """多路召回 设计原因:单路召回容易漏掉语义词相关但字面不匹配的文档。 多路召回通过不同机制的互补,大幅提升召回率。 不同召回源的结果使用统一的 SearchResult 格式归一化。 """ results = [] # 通路1: 向量检索 # 设计原因:对于语义模糊或抽象概念的查询, # 向量检索能捕捉到文本中不直接出现但语义相关的文档。 query_vector = self.embedding_model.encode(query) vector_results = self._vector_search(query_vector, top_k=1000) for doc in vector_results: results.append(SearchResult( doc_id=doc['id'], content=doc['text'], score=doc['score'], source='vector' )) # 通路2: 关键词检索 (BM25) # 设计原因:对于包含专有名词或精确术语的查询, # 关键词匹配的精确率远高于向量检索。 keyword_results = self.bm25_index.search(query, top_k=100) for doc in keyword_results: results.append(SearchResult( doc_id=doc['id'], content=doc['text'], score=float(doc['score']), source='keyword' )) # 按 doc_id 去重(保留最高分) seen = {} for r in results: if r.doc_id not in seen or r.score > seen[r.doc_id].score: seen[r.doc_id] = r return list(seen.values()) def _light_rerank(self, query: str, candidates: List[SearchResult]) -> List[SearchResult]: """轻量粗排 设计原因:使用双塔模型(Query 和 Document 分别编码后点积), 速度快(1~5ms/条),精度中等(NDCG@10 ≈ 0.75)。 目标是保留 Top 50 候选进入精排。 """ q_vec = self.light_reranker.encode_query(query) for candidate in candidates: d_vec = self.light_reranker.encode_document(candidate.content) # 将原始检索分数与粗排分数加权融合 # 设计原因:不单纯依赖粗排分数, # 防止粗排模型的偏差覆盖掉召回阶段的正确排序。 candidate.score = ( 0.3 * candidate.score + 0.7 * np.dot(q_vec, d_vec) ) candidates.sort(key=lambda x: x.score, reverse=True) return candidates[:50] def _heavy_rerank(self, query: str, candidates: List[SearchResult], top_k: int) -> List[SearchResult]: """精确重排 设计原因:Cross-encoder 将 Query 和 Document 拼接后做全注意力, 精度最高(NDCG@10 ≈ 0.85),但速度最慢(20~50ms/条)。 这就是为什么必须在精排之前通过粗排大幅缩小候选集。 """ for candidate in candidates: score = self.heavy_reranker.score(query, candidate.content) candidate.score = score candidates.sort(key=lambda x: x.score, reverse=True) return candidates[:top_k]

四、单阶段与多阶段检索的权衡分析

多阶段检索的收益是明确的——用较低的总体计算成本获得较高的检索精度。但代价同样不容忽视。

维度单阶段(仅向量检索)三阶段
检索延迟50~100ms200~500ms
准确率 (Recall@10)0.850.92+
系统复杂度
维护成本1 个模型3 个模型 + 2 个索引
资源占用1 路3 路(召回) + 2 路(排序)

选择建议

  • 文档总量 < 10 万且召回质量要求不高:单阶段向量检索即可。
  • 文档总量在 10 万~100 万:两阶段(召回 + 粗排)足够。
  • 文档总量 > 100 万且对 Top 10 精度要求高:必须三阶段。

见证奇迹的时刻在于认识到:检索系统的问题不是"该用几阶段",而是"每阶段的定位"——召回是保证不漏(Recall 优先),粗排是快速清理噪音,精排是保证对(Precision 优先)。把一个阶段的职责分配给另一个阶段,是检索系统设计的根本错误。

五、总结

语义搜索的三阶段架构将检索任务分解为召回(高召回率、多通路)、粗排(中精度、高速度)和精排(高精度、低速度)三个层次。每阶段处理的候选集规模依次缩小(1000+ → 50 → 10),而精度依次提升。多路召回要求向量检索、关键词检索和知识图谱检索三通路互补,共同保证召回覆盖率。粗排模型优先使用双塔架构以保证速度,精排模型使用 Cross-encoder 以保证精度。三阶段架构的延迟代价约为 200500ms,相比单阶段增加了 35 倍,但检索准确率提升 30%~50%。文档规模越大,多阶段架构的收益越明显。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/13 16:32:39

AMD量化模型部署常见问题:解决10个最常见的安装和运行错误

AMD量化模型部署常见问题&#xff1a;解决10个最常见的安装和运行错误 【免费下载链接】Llama-3.3-70B-Instruct-w4a16-asym-torchao-v0.17.0 项目地址: https://ai.gitcode.com/hf_mirrors/amd/Llama-3.3-70B-Instruct-w4a16-asym-torchao-v0.17.0 部署AMD量化模型时&…

作者头像 李华
网站建设 2026/7/13 16:32:32

当研究问题指向蛋白状态,空间单细胞蛋白组为什么可以成为直接方案?

空间组学技术选择并不是越多越好&#xff0c;而是取决于科研问题本身。如果研究者的目标是探索未知空间表达区域、寻找新的基因模块&#xff0c;空间转录组具有很好的发现价值&#xff1b;但如果单细胞测序已经明确了细胞类型、候选marker和关键细胞状态&#xff0c;后续问题又…

作者头像 李华
网站建设 2026/7/13 16:32:21

从入门到精通:LabelImg高效标注流程与实战技巧解析

1. LabelImg快速入门指南第一次打开LabelImg时&#xff0c;那个黑底白字的终端窗口可能会让你有点懵&#xff0c;别担心&#xff0c;它只是记录软件运行日志的后台程序&#xff0c;完全不用理会。真正需要关注的是同时弹出的图形化操作界面&#xff0c;这才是我们标注工作的主战…

作者头像 李华
网站建设 2026/7/13 16:31:41

【WorkBuddy专栏09】WorkBuddy多模态能力深度揭秘

文章目录 一、先搞懂:什么是「多模态」?为什么它很重要? 1.1 一句话定义 1.2 为什么单模态不够用? 1.3 WorkBuddy 多模态的独特之处 二、WorkBuddy 多模态能力全景图 2.1 六大能力矩阵 2.2 能力调用的底层机制 三、实战案例:三个典型场景拆解 3.1 场景一:文章封面图自动生…

作者头像 李华