想象你走进一座藏书百万的图书馆,一位姓陈的管理员为你服务。你说:“我想找一本让人读完心里暖洋洋、关于普通人坚持梦想的故事书。”老陈走到一张巨大的卡片柜前,抽出你的需求卡,几秒后就抱来三本书,翻开其中一页:“你看,这段写的就是主角在雨夜里练琴。” 老陈的秘诀是什么?他没有逐页翻书,而是给每本书调制了一张“气味卡”——用几百个数字浓缩整本书的气质。他也把你的需求调成气味卡,然后在百万张卡片里快速嗅出气味最相近的几张。这个方法不依赖“书名里有没有'梦想'两个字”,而是真正理解“坚持”“温暖”这些词背后的语义。这就是稠密检索(Dense Retrieval)的直觉:把查询和文档都编码成向量,再用近邻搜索(ANN)快速找到最匹配的候选。 这篇文章,我们就跟着老陈一起,看看这套“闻香识书”系统背后的关键部件:怎么调制气味卡(向量编码)、用哪种闻香流程(双塔 vs 交互式)、怎么训练调香师(难负样本 + 对比学习)、以及如何在图书馆里规模化落地(ANN 索引 + 混合检索)。
稠密检索是 RAG、语义搜索、推荐召回的核心技术底座,掌握它意味着你真正理解了现代 AI 应用“如何从海量信息中精准找到答案”的底层逻辑,这是搜索算法工程师、大模型应用工程师绕不开的硬技能。
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
双塔编码器架构 (Dual-Encoder) Query Tower Document Tower 相似度计算 q = E_q(query) d = E_d(doc) 稠密检索Embedding ANN可视化:展示双塔/交互编码器架构、对比学习训练、向量近邻检索...
语义向量编码
老陈刚接手图书馆时,靠的是逐本翻书来推荐。后来他发现,与其翻完百万本书,不如先给每本书制作一张“气味卡”——用几百个数字,比如“温暖值 0.8、励志值 0.7、悬疑值 0.1”,把整本书的气质浓缩进去。这张卡,就是我们说的向量。
把文本变成向量,这步叫语义向量编码(Embedding)。传统关键词检索像“看书的封面有没有印某个词”——封面没印“梦想”,这本书就被漏掉了。而向量编码像“闻整本书的味道”,哪怕书里写的是“雨夜练琴”,也能闻出“坚持”的味道。文本转换成的向量是一个稠密向量(Dense Vector),每个维度都是实数,所有维度共同表达语义。
现实中,这个“调香师”通常是预训练语言模型(如 BERT、SimCSE)。它会读一遍语料学会语言的规律,然后对任意文本输出一个定长向量。向量维度 d 一般在 128 到 768 之间,维度太小装不下复杂语义,太大则存储和计算开销陡增。
有了高质量的气味卡,老陈接下来要考虑的不是“如何逐本翻书”,而是“如何让查询卡和书卡快速比对”——这正是双塔架构登场的地方。
- 输入文本先把原始文本拆成 token 序列,作为编码器的原料。
- Encoder 编码器预训练语言模型对 token 逐层融合上下文,把整句语义压缩成一个向量。
- 稠密向量空间输出向量的维度为 d,每个维度都参与语义表达,因此是“稠密”的。
双塔架构解析
老陈实现“闻香识书”时,雇了两位调香师:一位负责把书调成书卡,另一位负责把读者的需求调成查询卡。两人共享同一本调香秘方(参数),确保“温暖”在书卡和查询卡里闻起来是同一种味道。两座塔各干各的活,互不打扰——这就是双塔架构(Two-Tower / Dual-Encoder)。
双塔架构包含两个独立的编码器:查询塔(Query Tower)和文档塔(Document Tower)。查询塔只吃查询文本,文档塔只吃文档文本,两者在编码阶段完全隔离,不产生任何 token 级交互。正因为这种隔离,文档塔可以提前把全图书馆的书都做成气味卡存起来,线上来一个读者,只需要把读者的话过一遍查询塔,然后去卡片柜里做近邻搜索。
两条塔的产出是两段向量:查询向量 \(\mathbf{v}_q\) 和文档向量 \(\mathbf{v}_d\)。两者有多“配”,通过余弦相似度衡量——也就是两条“气味方向”的夹角。夹角越小,说明气味越接近。
两位调香师各管一摊的好处是:书的气味卡可以提前做好、反复使用。读者来临时只花一份查询的时间就能从百万张卡里嗅出最像的几十张——这是双塔成为工业界召回标配的根本原因。
- 查询向量查询塔对查询文本编码得到的向量。
- 文档向量文档塔对文档编码得到的向量。
- 余弦相似度两个向量的夹角余弦,消除长度影响,只比方向。
交互式编码器
有时候,只比较两张独立的气味卡并不够。比如读者说“想看结局出人意料的书”,老陈把书卡拿过来单闻,总觉得差口气。他干脆把读者的话和书的内容倒进同一口锅里炖,让每个词都跟每个词照个面——这就是交互式编码器(Cross-Encoder)的直觉:让查询和文档在编码时充分对话。
交互式编码器把查询和文档拼接成一个序列,一起送进编码器。在注意力机制的作用下,查询中的每个 token 都能看到文档中的每个 token,深层语义交互因此成为可能。它能捕捉到“你说的结局出人意料,和这本书第三章节的反转完全呼应”这种微妙关系,精度通常显著高于双塔。
但代价也很昂贵:每一个查询和文档配对都要完整跑一次编码。若有 100 万个文档,一个查询就要跑 100 万次编码,无法离线预计算,耗时不可接受。因此它通常只用在候选集被大幅缩小之后的精排阶段。
老陈的分工很清晰:先用双塔快速海选,再用交互式编码器精读。如果对每个候选都精读,图书馆一天也接待不了几位读者,这就是“快粗筛”与“慢精读”的取舍。
- 拼接输入把查询和文档拼接成一个长序列。
- 共同编码编码器内每个 token 都与全部 token 做注意力交互。
- 输出分数取特殊标记或池化表示,通过线性层映射成相关性分数。
难负样本挖掘
训练两位调香师时,老陈起初只给他们看“苹果味书”和“锤子味书”的差别,他们很快学会:只要闻到甜美果香就配对。然而真到了图书馆,读者要的是“像苹果但不是苹果”的梨香型书,于是调香师犯难了。要让模型真正理解细微语义,得给它出难题——这就是难负样本挖掘(Hard Negative Mining)。
在稠密检索训练中,正样本是与查询匹配的文档,负样本是与查询不匹配的文档。若负样本全是随机抽取的“锤子味书”,模型学不到精细判别力。因此要主动构造难负样本:一种方式是拿 BM25 关键词检索出的高分但语义不相关的文档当负样本——它们表面相关、实际不匹配;另一种方式是 ANN 近邻搜索中与正样本很接近的文档,让训练信号更有挑战性。此外,批次内其他样本天然可以作为负样本(In-batch Negative),计算成本极低。
难负样本讲究“循序渐进的难”。一上来全是地狱难度,调香师会懵;难度太低,又学不到东西。训练时通常将随机负样本和难负样本混合,并逐渐提高难负样本的比例。
老陈深有体会:给调香师喂的考题水平,直接决定了他们出师后的功力。随机负样本只能培养“初学者”,难负样本才能培养出能分辨“梨香”与“苹果香”的大师。
- 正样本相似度查询和匹配文档的相似度越高越好。
- 负样本相似度查询和不匹配文档的相似度越低越好。
- 间隔惩罚正负分数差不足 \(m\) 时,损失为正,驱动模型继续学习。
对比学习增强
调香师训练的核心心法,老陈总结为十二个字:对的书,气味越近越好;错的书,气味越远越好。这套“拉近正例、推开反例”的训练思路,在学术上叫对比学习(Contrastive Learning)。
对比学习在稠密检索中通常用 InfoNCE 损失来落地。给定一个查询,批次里有 1 个正样本(与之匹配的文档)和 \(B-1\) 个负样本(批次内其余文档),模型被要求正确地把正样本从 \(B\) 个候选中“挑”出来。这像是在一组气味卡中,让调香师必须找出那张正确的卡,找不到就扣分。
InfoNCE 损失里藏着一个关键旋钮——温度系数 \(\tau\)。\(\tau\) 越小,Softmax 分布越尖锐,模型会越“较真”地惩罚那些把难负样本排得太高的错误;\(\tau\) 越大,分布越平滑,训练更温和。通常取 0.05 左右。
这套心法让老陈的调香师有了清晰的目标函数:每一次训练都在强化“像的更像、不像的更不像”,气味卡的质量因此稳步提升。
- 正样本分子正样本配对的相似度经温度缩放后取指数,分数越高越好。
- 全部样本分母批次内所有样本(含正含负)的相似度指数求和。
- 温度系数控制分布的尖锐程度,\(\tau\) 越小模型越严格。
效率优势分析
老陈的图书馆藏书越来越多,即便有了气味卡,一张一张嗅也要一小时。于是他在卡片柜里修了一座“气味索引塔”:塔分很多层,顶层很粗糙,底层很精细。闻过一次上层,就能直接排除掉一半区域的书,越往下层越精确。这就是 ANN(Approximate Nearest Neighbor,近似最近邻)索引的直觉。
稠密检索的优势有两面。其一是语义泛化能力:同样的意思,不管用户怎么换说法,向量表示都接近;其二是计算效率:配合 ANN 索引,可以在百万甚至亿级向量中快速定位候选,而无需全库线性扫描。最常见的 ANN 索引是 HNSW(Hierarchical Navigable Small World),它把向量组织成多层图,搜索时从顶层快速下探到底层。
HNSW 的搜索代价近似对数级——这意味着文档量从 10 万涨到 100 万,查询时间不是涨 10 倍,而是只涨一点点。不过 ANN 是“近似”近邻,索引参数(如 efSearch、M)决定了召回率与速度的平衡点:参数调大,结果更准更慢;参数调小,更快但可能漏掉真近邻。
有了气味索引塔,老陈再也不用担心图书馆扩建。百万本书的检索,从一杯茶的功夫,缩短到眨一下眼——这就是稠密检索结合 ANN 的工业化威力。
- 顶层粗搜从最上层的少量节点开始,快速确定大致方向。
- 逐层下探每层做局部邻域搜索,逐步逼近目标区域。
- 总搜索代价层数约为 \(\log N\),每层常数步,合计对数复杂度。
部署实践要点
老陈的“闻香识书”系统终于上线了。完整流程并非读者一说需求就直接给答案,而是分三步走:先用双塔加 ANN 从百万本里快速召回几百本候选,再用轻量粗排模型过滤到几十本,最后让交互式编码器精读这几十本,排出最终推荐。这就是工业界标准的“召回 → 粗排 → 精排”级联架构。
部署中还有一个关键决策:混合检索。稠密检索擅长语义改写,但对专有名词、型号、精确短语容易失手——比如读者要找“ISBN 978-7-1234-5678-9“,靠语义根本闻不出来。老陈的做法是保留传统关键词索引(BM25)作为”双保险”,把两路分数融合起来。
向量存储选型上,FAISS、Milvus、Qdrant 是常见选项;同时要监控召回率、检索延迟、向量分布漂移等指标。老陈的故事到这里不再是个人经验,而是一套可复制、可扩展的工程系统——既懂语义,又稳如泰山。
- BM25 分数基于词频和稀有度计算的稀疏检索分数。
- 稠密分数基于向量相似度的语义检索分数。
- 融合权重\(\alpha\) 控制两种信号的强弱,通常通过线上实验调节。
AI 应用场景
RAG 知识库问答
阶段:检索增强生成(RAG)
解决的问题:大语言模型的知识有截止日期且易产生幻觉,直接回答私有知识库问题时错误率高。传统关键词检索又无法理解“公司年假和调休怎么结算”这类语义复杂的提问。
具体案例:某企业客服机器人把数千份制度文档切成片段,用双塔模型离线编码存入向量库;用户提问时,系统用查询塔编码问题,通过 ANN 索引召回 Top 20 片段,再送入交互式编码器精排,最后交给大模型生成答案。整个过程就像老陈先闻气味卡找书,再精读候选书页。
电商搜索语义召回
阶段:召回 / 匹配
解决的问题:用户搜“透气不闷脚的运动鞋”,传统关键词召回只能匹配到标题中含“透气”或“运动鞋”的商品,漏掉了标题写“网面跑步鞋”但实际完全符合需求的商品。
具体案例:某电商平台在召回阶段用双塔模型,商品侧离线编码建索引,用户查询实时编码后走 ANN 检索,再合并关键词召回结果做混合排序。上线后“语义相关但词面不同”的商品曝光量显著提升,搜索转化率上涨。
相似文章推荐
阶段:相关性匹配
解决的问题:新闻资讯平台需要给读者推荐“讲述同一事件不同角度”的文章,字面重复度低但主题相关。传统基于 TF-IDF 的相似度方法无法识别换一种说法讲述的同一事件。
具体案例:某新闻 App 为每篇文章生成向量,存入向量数据库;读者打开一篇文章后,系统用它作为查询向量检索出主题最相近的几篇,并展示“相关报道”栏目,人均阅读时长明显提升。
Python 演示
import numpy as np class DenseRetriever: """稠密检索器:文档向量入库 + 查询向量近邻检索""" def __init__(self, dim=128): self.dim = dim self.doc_vecs = [] self.doc_ids = [] def add_document(self, doc_id, vector): """把文档向量加入候选库""" vec = np.asarray(vector, dtype=np.float32) vec /= np.linalg.norm(vec) self.doc_vecs.append(vec) self.doc_ids.append(doc_id) def index(self): """将所有文档向量堆叠成矩阵,模拟 ANN 索引前的准备""" self.vectors = np.vstack(self.doc_vecs) def retrieve(self, query_vec, top_k=3): """查询向量与库内向量做余弦近邻检索""" q = np.asarray(query_vec, dtype=np.float32) q /= np.linalg.norm(q) scores = self.vectors @ q order = np.argsort(scores)[::-1][:top_k] return [(self.doc_ids[i], float(scores[i])) for i in order] def info_nce_loss(q_vecs, doc_vecs, tau=0.05): """InfoNCE 对比学习损失:让批次内正样本对的得分最高""" q = q_vecs / np.linalg.norm(q_vecs, axis=1, keepdims=True) d = doc_vecs / np.linalg.norm(doc_vecs, axis=1, keepdims=True) logits = (q @ d.T) / tau batch_size = q.shape[0] labels = np.arange(batch_size) exp_logits = np.exp(logits) pos = exp_logits[np.arange(batch_size), labels] denom = exp_logits.sum(axis=1) loss = -np.mean(np.log(pos / denom)) return loss if __name__ == "__main__": rng = np.random.default_rng(42) dim = 64 retriever = DenseRetriever(dim) # 模拟 1000 篇文档的预计算向量 for i in range(1000): vec = rng.standard_normal(dim) retriever.add_document(f"doc-{i:04d}", vec) retriever.index() # 模拟一个查询向量并检索 Top 5 q = rng.standard_normal(dim) results = retriever.retrieve(q, top_k=5) print("Top 5 检索结果:") for doc_id, score in results: print(f" {doc_id}: {score:.4f}") # 模拟一个小批次训练数据,计算 InfoNCE 损失 batch = 32 q_batch = rng.standard_normal((batch, dim)) d_batch = rng.standard_normal((batch, dim)) loss = info_nce_loss(q_batch, d_batch) print(f"InfoNCE 损失: {loss:.4f}")
retriever = DenseRetriever(dim)初始化一个 64 维的稠密检索器,对应“气味卡”的维度retriever.add_document(f"doc-{i:04d}", vec)把每篇文档的向量加入候选库,相当于给书贴气味卡vec /= np.linalg.norm(vec)对向量做 L2 归一化,让余弦相似度简化为向量内积scores = self.vectors @ q查询向量与全部文档向量做矩阵乘法,一次性算出所有相似度分数np.argsort(scores)[::-1][:top_k]按分数降序取前 top_k 个索引,即 ANN 检索的简化版logits = (q @ d.T) / tau计算批次内所有查询与文档的相似度矩阵,并用温度系数缩放loss = -np.mean(np.log(pos / denom))对每个查询求正样本的归一化概率,取负对数后求平均,得到 InfoNCE 损失
常见误区
误区交互式编码器精度更高,那就把整个检索流程都换成交互式,效果一定更好。
正解交互式编码器无法离线预计算文档向量,每个查询都要和全部文档配对做联合编码。在百万级语料上,这种方式完全跑不起来。工业界标准做法是双塔负责召回缩小候选集,交互式只在几十到几百个候选上精排。这就像老陈不会对百万本书逐本精读,而是先闻气味卡海选,再细读几本候选。
为何常见:只比较了离线精度指标,忽略了在线推理的计算复杂度和延迟约束,把“精度更高”错误理解为“处处可用”。
误区训练时负样本越接近正样本,模型学到的判别力越强,所以应该全程使用最难挖的负样本。
正解负样本太难会引入“伪负例”风险——有些负样本可能实际与查询相关,只是标注没覆盖。如果模型硬要把它们推开,反而会学到错误映射,损害泛化能力。正确策略是循序渐进:先随机负样本打底,再逐步提高难负样本比例,同时过滤掉高置信的疑似正样本。
为何常见:"Hard Negative"这个概念听起来越难越高级,初学者容易忽略训练信号的噪声和难度曲线的必要性。
误区向量维度越大,能装载的语义信息越多,检索效果肯定更好,所以直接把维度设成 4096。
正解高维度向量带来两个问题:一是存储和计算开销大幅增加,二是训练数据有限时容易过拟合,泛化反而变差。实践中 128 到 768 维是主流区间,具体维度需要通过评测集上的召回率与延迟权衡决定。
为何常见:受“信息越多越好”直觉影响,忽略了维度与数据量、计算成本之间的工程约束。
误区部署了 HNSW 或 FAISS 索引后,检索结果就是数学上最精确的最近邻。
正解ANN 的 A 是 Approximate(近似),它通过牺牲一定召回率换取速度。HNSW 的 efSearch、M 等参数直接决定近似程度:参数调大越接近精确但越慢。如果业务要求绝对精确,需要做精确 top-k 验证或适当调参。
为何常见:许多教程直接说“ANN 索引加速检索”,忽略了“近似”是这类算法的本质特征与代价。
误区稠密检索语义能力强,关键词匹配已经过时,直接删掉 BM25 即可。
正解稠密检索对专有名词、ID 号、精确型号等“必须逐字匹配”的查询经常失手——这些词的向量语义本身就很稀疏。工业界主流方案是稠密 + 稀疏混合检索,按权重融合分数,兼顾语义泛化和精确匹配。老陈的图书馆里,气味卡和关键词索引一直并存。
为何常见:看多了稠密检索的成功案例,容易把“语义能力强”误当成“完全覆盖词面匹配”的能力。
面试题
Q1. 稠密检索中的 Embedding 向量本质是什么?
- A.文本压缩成的语义数值编码← 正确答案
- B.增加磁盘读写的元信息
- C.记录词汇出现频次的稀疏向量
- D.对原始文本无损压缩的存储格式
答案:A。Embedding 是把文本压缩成稠密语义向量的数值编码,每个维度都参与表达语义。B 把它当成元信息,C 是稀疏向量(与稠密向量相反),D 强调无损压缩,但向量编码本身是有损且面向语义匹配的。
Q2. 双塔架构的最大优势是什么?
- A.文档向量可以离线预计算← 正确答案
- B.查询文档必须在线联合编码
- C.不需要任何训练语料数据
- D.精度一定高于交互式模型
答案:A。双塔架构中查询塔和文档塔独立编码,文档向量能离线算好并建索引,线上只需算查询向量。B 描述的正是交互式编码器;C、D 都不符合事实。
Q3. 交互式编码器通常被用在哪个阶段?
- A.对海量候选做重排精排← 正确答案
- B.对海量候选做快速召回
- C.对语料做离线索引构建
- D.对查询做分词预处理
答案:A。交互式编码器需要查询与文档配对做联合编码,无法离线预计算,因此适合候选集已缩小的精排阶段。召回阶段追求速度,用双塔加 ANN;C、D 与交互式编码器无关。
Q4. 以下哪些属于难负样本的常见来源?
- A.批次内其他样本作为负例← 正确答案
- B.BM25 检索的高分段结果← 正确答案
- C.ANN 近邻检索的相近文档← 正确答案
- D.仅随机抽取少量不相关文档
答案:ABC。批次内负样本利用同批次其他文档作为负例,计算零成本;BM25 高分结果表面相关但可能不匹配,是典型难负样本;ANN 近邻文档与正样本向量接近,具有挑战性。D 是随机负样本,属于“简单负样本”,不是难负样本。
Q5. 温度参数 τ 调大后,Softmax 分布会?
- A.变得更加平滑均匀← 正确答案
- B.变得更加尖锐集中
- C.始终保持均匀不变
- D.梯度方向完全反转
答案:A。τ 在 InfoNCE 损失中充当缩放因子。τ 越大,指数分数被除以更大的数,Softmax 输出越平滑,模型对难负样本的惩罚越温和。B 是 τ 调小的效果,C 的“不变”、D 的“梯度反转”均不成立。
Q6. 为什么双塔更适合大规模召回?
- A.文档向量预计算后建 ANN 索引← 正确答案
- B.让文档查询在编码时深度融合
- C.需要为每次查询重新编码文档
- D.对文档长度没有任何限制要求
答案:A。双塔的文档塔离线把所有文档编码成向量并建立 ANN 索引,线上查询只需编码查询向量然后检索。B 是交互式编码器的特点,C 与双塔相反,D 并非双塔特有的优势。
Q7. 混合检索(稠密+稀疏)的价值有哪些?
- A.兼顾语义匹配与精确匹配← 正确答案
- B.缓解专有名词的匹配短板← 正确答案
- C.完全消除索引存储的开销
- D.彻底替代向量索引的部署
答案:AB。混合检索的核心动机是让稠密检索的语义泛化能力与稀疏检索的精确词汇匹配能力互补,专有名词、ID 类查询正是稠密检索的短板。C 说“完全消除存储开销”、D 说“彻底替代向量索引”都是错误理解——混合检索两者都用,而不是互相替代。
Q8. ANN 近邻索引要解决的核心问题是?
- A.避免全库线性扫描候选文档← 正确答案
- B.增加向量存储的内存开销
- C.让文本编码模型的参数更少
- D.完全代替模型训练的过程
答案:A。ANN 索引(如 HNSW)通过分层图结构把查询时间从全库扫描的 O(N) 降到 O(log N),解决海量候选下的检索速度问题。B 是副作用而非目标,C、D 与索引无关。
Q9. 难负样本太“难”会带来什么风险?
- A.语义相近的负样本被误认正例← 正确答案
- B.模型训练速度必然加快
- C.让召回精度变得只升不降
- D.向量维度需要同步缩小
答案:A。难负样本如果与正样本语义过于接近,甚至实际相关只是标注没覆盖(伪负例),模型强行推开它们会把相关文档学成不相关,损害泛化能力。B、C、D 均不是难负样本的直接后果。
Q10. 关于稠密检索训练,以下哪些说法正确?
- A.对比学习拉近正样本对的距离← 正确答案
- B.难负样本挖掘提升模型判别力← 正确答案
- C.批次内负样本无需额外 ANN 查询← 正确答案
- D.双塔共享参数一定降低检索精度
答案:ABC。对比学习的目标就是让正样本对向量更接近、负样本对更远离;难负样本能迫使模型学会细微差别;批次内负样本免费取自同批次数据,无需额外检索。D 说法绝对化,是否共享参数取决于任务,不一定降低精度。