想象你是一座拥有百万册藏书的大图书馆的管理员。过去,读者想找书,必须说出书名里的某个词,目录系统就只会去卡片箱里找“字面相同”的词汇。有一天,一位读者红着眼眶问你:“我想找一本关于失去朋友后如何慢慢恢复的书。”老系统搜遍书名和关键词,返回“查无此书”。你忽然意识到:读者描述的是意思,而老系统只认识字眼。于是你引入了一套新系统——它给每本书都安上了一个“语义坐标”,就像城市有经纬度一样。读者描述需求后,系统把这段描述也变成坐标,再在地图上测量距离,把离得最近的几本书推荐出来。这套新方法,就是 Dense Retrieval(稠密检索):用 embedding 向量表达语义,用向量距离实现匹配检索。接下来,我们会像搭建这座语义图书馆一样,从检索根基、核心原理、模型选型、索引构建、相似度计算、效果评估到实战优化,完整走一遍。

岗位关联

一句话价值:掌握稠密检索,你就能让 RAG 系统从“查得到字”升级为“找得懂意”,这是决定检索增强应用召回质量与落地效果的核心技能。

目录
  1. 可视化演示
  2. 检索根基:从稀疏到稠密
  3. 核心原理:双塔编码器
  4. 模型选型:选对Embedding
  5. 索引构建:给向量建导航
  6. 相似度计算:度量语义距离
  7. 效果评估:召回质量体检
  8. 实战优化:混合检索与精排
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示Dense Retrieva的核心机制,通过采集/分析/决策/反馈四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Dense Retrieva:稠密检索 稠密检索与稀疏检索的核心区别 Embedding模型选择与向 核心机制可视化 Dense Retrieva整体架构与关键组件

检索根基:从稀疏到稠密

图书馆的老系统就像一本按关键词排列的目录册。每本书只有在它的书名、作者、标签里出现了某个词,才会被那个词“钩住”。读者说“想找一本讲如何走出低谷的书”,目录册里根本没有“走出低谷”这四个字,于是它只能两手一摊。这种依赖词形重合的检索方式,就是稀疏检索。它的向量像一张巨大的点名单,绝大多数位置是 0,只有出现过的词才点亮。问题在于:字面不同,不代表语义不同。

稠密检索换了一种思路。它不再给每个词单独留一个位置,而是用神经网络把整段文字压缩成一个足够短的稠密向量。这个向量的每个维度几乎都不为 0,每本书都在“语义地图”上拥有了一个独一无二的坐标。读者描述查询时,系统把查询也投射到同一张地图上,接下来只需要计算两个坐标之间的距离。

从“查字面”到“找含义”,这是检索思路的一次根本升级。老系统像逐字核对,新系统像拿着地图定位;前者看到的是词,后者看到的是语义。

ddense=fθ(整段文字)∈RD\mathbf{d}_{dense}=f_{\theta}(\text{整段文字})\in\mathbb{R}^{D}
这句话像说:图书管理员拿了台“语义定位仪”,把整段文字翻译成地图上的一个坐标。
  1. 稠密向量\mathbf{d}_{dense} 表示文档或查询最终得到的稠密向量,也就是语义坐标。
    ddense\mathbf{d}_{dense}
  2. 编码器f_{\theta} 是一个带参数 θ 的神经网络模型,负责把文字“翻译”成向量坐标。
    fθf_{\theta}
  3. 输入内容整段文字 表示输入不只是单个关键词,而是完整句子、段落甚至整篇文档。
    整段文字\text{整段文字}
  4. 向量空间\in\mathbb{R}^{D} 表示输出落在 D 维实数空间,D 常见取值是 256、384、768、1024 等。
    ∈RD\in\mathbb{R}^{D}

核心原理:双塔编码器

明白了要给文字配坐标,接下来要解决:坐标从哪来?图书馆里不能靠人手工量距离,需要一台稳定的“定位仪”。在稠密检索里,这台定位仪就是 embedding 模型。为了让查询和文档可以公平比较,我们通常训练两个编码器:一个叫查询塔,负责把读者的问题变成向量;另一个叫文档塔,负责把候选内容变成向量。两个塔共享同一个语义地图,这样查询才能和文档直接比远近。

这种结构像一个“双塔”建筑:左边塔解读读者的问题,右边塔解读图书馆的藏书,两座塔的输出都汇入同一个坐标广场。训练时,模型会学习让语义相关的查询和文档向量靠得更近,让不相关的对彼此推开。推理时,我们只做一件事:把查询向量和文档向量送入相似度函数,得到匹配分数。

有了双塔模型,系统就可以预先给全馆图书算好坐标并存入数据库。读者来查询时,只需现场计算查询向量,再与库里的向量比较。坐标一旦统一尺度,“远”与“近”就是语义相关与否。

score(q,d)=sim(EQ(q),ED(d))\text{score}(q,d)=\mathrm{sim}\big(\mathbf{E}_Q(q),\mathbf{E}_D(d)\big)
查询塔和文档塔各就各位,把一个查询和一篇文章送往同一个向量空间,再用距离函数打一个匹配分。
  1. 匹配分数\text{score}(q,d) 是查询 q 与文档 d 的语义匹配分数,越高代表越相关。
    score(q,d)\text{score}(q,d)
  2. 查询编码器\mathbf{E}_Q(q) 将查询 q 编码成查询向量。
    EQ(q)\mathbf{E}_Q(q)
  3. 文档编码器\mathbf{E}_D(d) 将文档 d 编码成文档向量。
    ED(d)\mathbf{E}_D(d)
  4. 相似度函数\mathrm{sim}(\cdot,\cdot) 是相似度计算函数,负责比较两个向量的方向与距离。
    sim(⋅,⋅)\mathrm{sim}(\cdot,\cdot)

模型选型:选对Embedding

定位仪也有不同型号。有的适合全球导航,有的对某个城市的小巷特别熟悉。Embedding 模型同样如此:通用模型在海量多语言数据上训练,覆盖面广,适合中英文混合或者通用知识库;中文场景模型则针对中文语境做了优化,对成语、口语、中文长句的理解通常更好;如果你的知识库有强烈的领域色彩,比如法律、医疗、金融,还需要在领域数据上做微调。

选择模型时,向量维度是一个容易被忽视的决策点。维度越高,通常能表达更丰富的语义,但存储、内存和计算成本也越高。想象你给一百万本书各写一份“性格报告”:报告越厚,信息越细,但书架也会被撑爆。

模型选型不是越贵越好,而是要匹配业务场景。就像导航仪不必支持所有星球的坐标一样,先看语料语言,再看专业领域,最后用检索效果验证,才不会被“更大模型”带偏。

存储量=N×D×4 Bytes\text{存储量}=N\times D\times4\,\text{Bytes}
N 本书 × 每本 D 维向量 × 每个浮点数 4 字节,就是向量库最基础的内存账单。
  1. 文档数量N 是语料中文档或段落的总数。
    NN
  2. 向量维度D 是每条 embedding 向量的维度,决定坐标的精细程度。
    DD
  3. 单值字节数4 是 float32 类型每个数值占用的字节数。
    4 Bytes4\,\text{Bytes}
  4. 总量关系三者相乘得到向量部分的存储量,维度翻倍存储也翻倍。
    N×D×4N\times D\times4

索引构建:给向量建导航

如果你只有几十本书,逐本量距离没问题。但图书馆有百万本书,读者站在门口,系统不可能把每本书都拉出来量一遍。这时就需要给语义地图搭建导航系统。最朴素的方法是暴力搜索:逐一计算查询向量与所有文档向量的距离,精确但太慢。工程上更常用的是 ANN 近似最近邻索引,它在“召回速度”和“召回精度”之间做取舍。

HNSW 是目前非常流行的图式索引。它把文档向量组织成多层图:上层连接稀疏的“高速入口”,下层连接细密的“街区小路”。查询时从顶层快速下探,像开车从高架进入支路,只需要访问一小部分节点就能找到近邻。IVF 则更像给地图划分行政区,先把向量聚类成若干桶,查询时只搜最近的几个桶。

索引决定了检索系统能否撑住线上流量。选好 HNSW 的候选数、IVF 的桶数,就像给导航仪设置“优先高速还是优先距离”,需要在线下评测中反复校准。

tsearch≈O(log⁡N)(HNSW)t_{\text{search}}\approx O(\log N)\quad(\text{HNSW})
HNSW 让检索时间随文档数增长不是翻倍地涨,而是极平滑的对数式增长。
  1. 检索耗时t_{\text{search}} 表示一次查询平均消耗的时间。
    tsearcht_{\text{search}}
  2. 对数复杂度O(\log N) 表示当文档数 N 变得很大时,耗时只按对数比例增长。
    O(log⁡N)O(\log N)
  3. 近似意义\approx 表示这是近似复杂度,实际还受图结构、参数和数据分布影响。
    ≈\approx
  4. 索引方法\text{HNSW} 是层次化可导航小世界图的缩写,一种主流 ANN 索引。
    HNSW\text{HNSW}

相似度计算:度量语义距离

现在坐标有了、导航也有了,最后一步是回答:两个点在语义上到底有多近?这很像描述两座城市之间的关系:可以说直线距离,也可以说沿某条路开车的距离。向量空间里最常用的距离叫余弦相似度。它不看向量的长短,只看方向是否一致。如果把查询向量和文档向量都画成从原点出发的箭头,余弦相似度就是看这两支箭的夹角。

还有一种常用方法是点积。点积既看方向,也看模长。如果模型训练时已经对向量做了归一化,点积和余弦相似度在数学上等价。因此,工程上有一条朴素经验:训练时用哪种度量,上线检索就用哪种度量。

相似度计算看似简单,却直接影响排序。就像两座城市直线距离再近,也可能隔着山;余弦相似度高,也不代表业务上一定相关。因此,度量的选择要跟随模型的训练方式,而不是随意更换。

cos⁡(q,d)=q⋅d∥q∥ ∥d∥\cos(\mathbf{q},\mathbf{d})=\frac{\mathbf{q}\cdot\mathbf{d}}{\|\mathbf{q}\|\,\|\mathbf{d}\|}
两支箭的夹角越小,余弦值越接近 1,语义也就越相似。
  1. 点积\mathbf{q}\cdot\mathbf{d} 是两个向量逐维度相乘再求和。
    q⋅d\mathbf{q}\cdot\mathbf{d}
  2. 向量长度\|\mathbf{q}\| 是查询向量的模长,代表这个向量的“总长度”。
    ∥q∥\|\mathbf{q}\|
  3. 归一化分母分母把两个向量长度约去,使得结果只保留方向夹角信息。
    ∥q∥ ∥d∥\|\mathbf{q}\|\,\|\mathbf{d}\|
  4. 余弦值\cos(\mathbf{q},\mathbf{d}) 的取值范围是 -1 到 1,越大越相似。
    cos⁡(q,d)\cos(\mathbf{q},\mathbf{d})

效果评估:召回质量体检

有了新系统,怎么知道它比老系统好?图书馆需要定期做“读者满意调查”。评测稠密检索,通常需要构建一个评测集:给定一组查询,人工标注出哪些文档是真正相关的。然后让检索系统返回前 K 条结果,看看相关文档被捞回来多少。

最常用的指标是 Recall@K,它衡量“所有相关文档里,有多少出现在前 K 个结果中”。此外还有 MRR,它看第一个相关结果排在第几位;NDCG 则更进一步,给排在靠前位置的相关结果更高奖励。这些指标就像检查导航仪的三种方式:是否找到了目的地、是否第一次就指向正确、以及整体路线是否合理。

评测集是检索优化的方向盘。没有它,你可能只是感觉“结果看起来不错”;有了它,才能量化每一次调参究竟提升了多少。它也是后续混合检索、精排优化的决策依据。

Recall@K=∣相关文档∩前K个结果∣∣相关文档∣\text{Recall@K}=\frac{|\text{相关文档}\cap\text{前K个结果}|}{|\text{相关文档}|}
把评测集里的相关文档数当分母,前 K 条结果中实际命中的相关文档数当分子,就得到召回率。
  1. 相关文档\text{相关文档} 是评测集中人工标注为相关的文档集合。
    相关文档\text{相关文档}
  2. 前K个结果\text{前K个结果} 是检索系统返回的 Top-K 集合。
    前K个结果\text{前K个结果}
  3. 交集\cap 表示两者重合的文档。
    ∩\cap
  4. 召回率\text{Recall@K} 表示在 Top-K 内命中的相关文档占全部相关文档的比例。
    Recall@K\text{Recall@K}

实战优化:混合检索与精排

语义地图很强大,但它也有盲区。比如读者精确报出“ISBN 编号 978-7-201-12345”,语义检索可能会把它当作一串普通文本,反而不如老式目录系统精确。更聪明的做法是让两套系统合作:稀疏检索负责精确字面匹配,稠密检索负责语义泛化,再把两路分数融合。

同时,检索流程通常分为粗排和精排两段。向量索引先快速召回几百条候选,再用更重但更准的模型精排。就像导航先给出几条大路线,再由熟悉当地的人挑选最佳小路。为了让知识库保持新鲜,索引还需要支持增量更新:新书入库时,不能把所有旧坐标全部重算一遍。

实战中的稠密检索很少单兵作战。真正能落地的方案,往往是一套组合拳:语义召回、字面兜底、精排修正、增量更新。这样,图书馆既能理解读者模糊的感受,也能精确找到那本指定编号的书。

sfinal=λ⋅sdense+(1−λ)⋅ssparses_{\text{final}}=\lambda\cdot s_{\text{dense}}+(1-\lambda)\cdot s_{\text{sparse}}
两路检索各打一个分,用一个权重 λ 把它们揉成一个最终分,兼顾语义和字面。
  1. 稠密分数s_{\text{dense}} 来自稠密检索的相似度分数。
    sdenses_{\text{dense}}
  2. 稀疏分数s_{\text{sparse}} 来自稀疏检索(如 BM25)的匹配分数。
    ssparses_{\text{sparse}}
  3. 混合权重\lambda 是介于 0 到 1 的超参数,控制两路结果的占比。
    λ\lambda
  4. 融合结果s_{\text{final}} 是用于最终排序的融合分数。
    sfinals_{\text{final}}

AI 应用场景

RAG 企业知识库问答

阶段:检索召回

解决的问题:传统关键词检索无法理解员工的同义表达,例如员工问“发票丢了怎么办”,知识库文档标题是“票据遗失处理流程”,字面不重合导致检索失败。稠密检索通过语义向量召回相关文档,让 RAG 系统拿到真正有用的上下文。

具体案例:某企业上线员工助手,接入内部规章制度数据库。此前用 BM25 搜索“出差住宿超标了怎么说明”,返回结果七零八落;改造为稠密检索后,系统准确召回“差旅费报销附则”和“超标住宿审批说明”,再交给大模型生成回答。

电商客服语义匹配

阶段:相似问题召回

解决的问题:客服机器人需要从历史工单中找到相似问题,但用户表达口语化严重,如“东西没送到”和“包裹物流停滞”意思相近,关键词却完全不同。稠密检索能把这种语义相似的关系捕捉到。

具体案例:电商平台将百万条历史工单向量化,每当用户进线咨询,先用稠密检索召回相似工单,再结合大模型生成回复建议,显著提升一次性解决率。

法律案例语义检索

阶段:案件相关法条召回

解决的问题:法律场景中,当事人描述案情时不会使用精确的法言法语,如“朋友借我钱不还”与“民间借贷纠纷”需要语义桥接。领域微调后的 embedding 模型可以让案情描述与法条、判例在向量空间中靠近。

具体案例:法律科技公司构建判例检索系统,用法学语料对 embedding 模型做领域微调,再结合混合检索,使案情描述能够召回相关判例和法条,辅助律师快速完成类案检索。

Python 演示

python59 行
import numpy as np  class SimpleDenseRetriever:    def __init__(self, dim=32, vocab_size=200, seed=0):        np.random.seed(seed)        self.dim = dim        self.vocab_size = vocab_size        self.embeddings = np.random.randn(vocab_size, dim).astype(np.float32)        self.doc_vecs = {}        self.doc_tokens = {}     def tokenize(self, text):        return list(text)     def _normalize(self, vec):        norm = np.linalg.norm(vec)        if norm == 0:            return vec        return vec / norm     def embed(self, tokens):        vec = np.zeros(self.dim, dtype=np.float32)        for token in tokens:            idx = abs(hash(token)) % self.vocab_size            vec += self.embeddings[idx]        return self._normalize(vec)     def add_document(self, doc_id, tokens):        self.doc_tokens[doc_id] = tokens        self.doc_vecs[doc_id] = self.embed(tokens)     def search(self, query_tokens, top_k=3):        qvec = self.embed(query_tokens)        scores = []        for doc_id, dvec in self.doc_vecs.items():            score = float(np.dot(qvec, dvec))            scores.append((doc_id, score))        scores.sort(key=lambda x: x[1], reverse=True)        return scores[:top_k]  if __name__ == "__main__":    corpus = {        "doc1": "公司在月底统一报销差旅费用",        "doc2": "发票丢失后需要提交遗失说明",        "doc3": "出差住宿标准按城市等级调整",        "doc4": "报销流程在OA系统发起申请"    }     retriever = SimpleDenseRetriever()    for doc_id, text in corpus.items():        retriever.add_document(doc_id, retriever.tokenize(text))     query = "出差住宿发票怎么报销"    results = retriever.search(retriever.tokenize(query))    print("Top results:")    for doc_id, score in results:        print(f"{doc_id}: {retriever.doc_tokens[doc_id]} score={score:.3f}")
目的:用随机初始化的词向量模拟一个最简稠密检索器,演示“文本 → 向量 → 相似度排序”的完整流程。实际项目中请把 self.embeddings 替换为预训练 embedding 模型产出的向量。
  1. self.embeddings = np.random.randn(vocab_size, dim).astype(np.float32)模拟一张“词向量表”,每个词都有一个初始坐标。
  2. vec = np.zeros(self.dim, dtype=np.float32)每条文档或查询先准备一个全零的稠密向量。
  3. vec += self.embeddings[idx]把每个词对应的向量坐标累加进整段文本的向量。
  4. return self._normalize(vec)归一化后,后续点积结果就等于余弦相似度。
  5. self.doc_vecs[doc_id] = self.embed(tokens)入库时提前算好文档向量,检索时不用重复计算。
  6. qvec = self.embed(query_tokens)查询也要用同一台“定位仪”映射到同一语义空间。
  7. score = float(np.dot(qvec, dvec))用点积计算查询与文档的相似度分数。
  8. scores.sort(key=lambda x: x[1], reverse=True)按分数从高到低排序,返回 Top-K。

常见误区

误区既然稠密检索能理解语义,那它肯定全面碾压 BM25,上线后直接把老方案替换掉就行。

正解稠密检索虽擅长语义泛化,但对精确匹配、产品型号、人名、ID 等场景反而容易失灵。比如查询“iPhone 15 Pro 256G 黑色”,语义模型可能只记住“手机 黑色”,把具体型号忘掉。更稳妥的做法是混合检索,让稀疏检索兜底精确字面。

为何常见:因为 Demo 里语义检索的效果看起来非常惊艳,容易让人忽略它面对精确查询时的短板。

误区维度越大,信息越丰富,检索效果一定越好,所以直接选 1024 维甚至更高。

正解维度提高会带来存储和计算成本的大幅上涨,而且当训练数据不足时,高维向量反而容易过拟合,产生“维数灾难”。选维度要结合语料规模、硬件资源和评测指标综合判断。

为何常见:人们天然认为“更多信息=更好”,却忽略了成本和边际收益递减。

误区点积和余弦相似度都是算相似度,线上用哪个都一样,选实现方便的那个就好。

正解如果模型训练时对向量做了归一化,两者等价;如果没有归一化,点积会受向量模长影响,导致高频文档更容易被排到前面。上线检索的度量必须和模型训练时的度量保持一致。

为何常见:初学时只看到公式长得像,没注意到模长归一化这个隐藏前提。

误区向量索引是一次性工作,把文档向量全部写入 HNSW 就可以了,后续新增文档时再说。

正解线上知识库每天都会新增和删除内容。如果不做增量更新,索引会逐渐过期,检索到已删除的内容或漏掉最新文档。生产系统需要设计定时增量构建策略。

为何常见:离线实验只关注单次效果,容易忽略线上数据的持续变化。

面试题

Q1. 稠密检索与稀疏检索的核心区别是什么?

单选题 · easy

  • A.稠密检索只搜中文内容
  • B.稠密检索依赖向量语义匹配← 正确答案
  • C.稀疏检索使用神经网络
  • D.稠密检索不需要建索引

答案:B。稀疏检索依赖关键词字面重合,稠密检索用 embedding 向量表达语义后计算距离,因此核心区别是是否依赖向量语义匹配。A、C、D 都不符合事实。

Q2. 稠密向量通常由什么模型产生?

单选题 · easy

  • A.词频统计模型
  • B.倒排索引模型
  • C.神经网络编码器← 正确答案
  • D.决策树模型

答案:C。稠密向量由 embedding 模型或神经网络编码器将文本映射到低维稠密空间而得到。词频统计和倒排索引都只产生离散的稀疏表示,决策树不适合直接生成向量。

Q3. 双塔编码器中的“双塔”指什么?

单选题 · easy

  • A.查询塔和文档塔← 正确答案
  • B.标题塔和正文塔
  • C.训练塔和推理塔
  • D.中文塔和英文塔

答案:A。双塔模型通常包含查询编码器和文档编码器,分别处理查询与文档,再把两边向量映射到同一语义空间比较相似度。

Q4. HNSW 索引的主要作用是什么?

单选题 · mid

  • A.提升向量维度
  • B.训练 embedding 模型
  • C.做近似最近邻搜索← 正确答案
  • D.替换相似度计算

答案:C。HNSW 是一种图式 ANN 索引,用多层图结构加速近邻查找。它不提升维度、不训练模型、不替换相似度函数,只是把“全库暴力搜索”变成近似搜索。

Q5. 余弦相似度的定义是什么?

单选题 · mid

  • A.向量内积除以模长乘积← 正确答案
  • B.向量各维度平方和
  • C.两个向量逐项相减
  • D.向量模长的算术平均

答案:A。余弦相似度关注方向夹角,数学形式是向量点积除以两个模长的乘积。B 是模长平方,C 是差向量,D 是长度平均。

Q6. Recall@K 衡量的是什么?

单选题 · mid

  • A.前 K 个结果中相关文档占比← 正确答案
  • B.全部文档的平均得分
  • C.精排模型训练准确率
  • D.单次检索响应耗时

答案:A。Recall@K 的分母是评测集中所有相关文档数,分子是前 K 个结果中命中的相关文档数,衡量召回覆盖能力。

Q7. 模型训练时用点积计算相似度,检索时最好怎么做?

单选题 · mid

  • A.改用欧氏距离
  • B.改用余弦相似度
  • C.保持点积保持一致← 正确答案
  • D.完全放弃相似度

答案:C。检索度量要与训练度量一致。如果训练时用了点积,线上最好继续用点积,避免因度量不一致导致排序行为偏离训练目标。

Q8. 纯稠密检索在专有名词查询上可能退化的原因是?

单选题 · hard

  • A.向量维度必然不够
  • B.语义模型对精确字面不敏感← 正确答案
  • C.索引结构不稳定
  • D.查询语句长度太长

答案:B。embedding 模型优先学习语义关联,对“型号、编号、人名”等精确 token 不够敏感。因此纯稠密检索可能混淆相似字面,需要混合检索补足精确匹配能力。

Q9. 以下哪些属于常见的向量索引方案?

多选题 · mid

  • A.暴力搜索← 正确答案
  • B.HNSW 图索引← 正确答案
  • C.IVF 倒排索引← 正确答案
  • D.B+ 树索引

答案:ABC。暴力搜索是精确基线,HNSW 和 IVF 是主流 ANN 近似索引。B+ 树主要用于关系型数据库的数值/字符串索引,不是向量索引方案。

Q10. 下列哪些指标可以反映稠密检索的排序质量?

多选题 · hard

  • A.Recall@K← 正确答案
  • B.MRR← 正确答案
  • C.NDCG@K← 正确答案
  • D.索引构建时间

答案:ABC。Recall@K 看召回覆盖,MRR 关注第一个相关结果的位置,NDCG@K 评估排序质量。索引构建时间属于性能指标,不反映检索结果的相关性排序。