· liyu · algorithm · 13 min read
当双塔模型遇见 RAG:召回与检索,殊途同归
搜广推系统中的双塔模型和 RAG 中的向量检索,一个诞生于推荐系统,一个兴起于大模型时代。当我把两者的架构图放在一起时,突然发现它们竟如此相似——本质上都是在向量空间中寻找最近的"灵魂伴侣"。
前段时间,我一边在搜广推的召回层调研双塔模型,一边又在大模型项目中搭建 RAG 流水线。某天晚上画架构图时,我把两张图摆在一起,愣了几秒——
这不是同一个东西吗?
一个叫”双塔召回”,一个叫”向量检索”,术语不同、出身不同,但骨子里干的是同一件事:把 Query 和候选集分别编码成向量,然后在向量空间中找最近邻。
这篇文章,我想把这个”顿悟时刻”完整地讲出来。
第一幕:搜广推中的双塔模型
召回层的使命
在搜广推系统中,面对百万甚至千万级别的候选池(商品、视频、广告),不可能对每一个候选都跑一遍精排模型。召回层的任务是:用尽可能低的计算成本,从海量候选中筛选出数百到数千个粗粒度相关的候选,交给下游精排。
传统的召回方式包括基于规则的热门召回、协同过滤召回、倒排索引召回等。但这些方法要么缺乏个性化,要么无法捕捉深层语义。双塔模型应运而生。
双塔架构
双塔模型(Two-Tower Model / Dual Encoder)的核心思想极其简洁:
┌──────────────┐ ┌──────────────┐
│ User Tower │ │ Item Tower │
│ │ │ │
│ 用户特征输入 │ │ 物品特征输入 │
│ (ID, 行为, │ │ (ID, 标题, │
│ 画像...) │ │ 类目...) │
│ ↓ │ │ ↓ │
│ Embedding │ │ Embedding │
│ layers │ │ layers │
│ ↓ │ │ ↓ │
│ User Vector │ │ Item Vector │
│ (128-d) │ │ (128-d) │
└──────┬───────┘ └──────┬───────┘
│ │
└────────┐ ┌────────────┘
↓ ↓
cos(u, v) 或 dot(u, v)
↓
相关性得分两座”塔”各自独立地将输入编码为一个稠密向量,然后通过内积或余弦相似度计算匹配分数。
训练过程
训练双塔模型本质上是在学习一个向量空间,使得:
- 用户点击/购买/观看过的物品,两者向量距离近
- 用户没有交互过的物品,两者向量距离远
常用的损失函数是对比学习风格的:
# 简化的双塔训练逻辑
def compute_loss(user_vec, pos_item_vec, neg_item_vecs):
"""
user_vec: 用户塔输出 [batch, dim]
pos_item_vec: 正样本物品向量 [batch, dim]
neg_item_vecs: 负样本物品向量 [batch, num_neg, dim]
"""
# 正样本相似度
pos_score = torch.sum(user_vec * pos_item_vec, dim=-1) # [batch]
# 负样本相似度
neg_scores = torch.bmm(
neg_item_vecs, user_vec.unsqueeze(-1)
).squeeze(-1) # [batch, num_neg]
# Softmax cross-entropy
logits = torch.cat([pos_score.unsqueeze(-1), neg_scores], dim=-1)
labels = torch.zeros(logits.size(0), dtype=torch.long)
loss = F.cross_entropy(logits, labels)
return loss服务阶段:离线 + 在线的优雅分工
双塔模型最大的工程优势在于两座塔完全解耦:
- 离线:用 Item Tower 把所有候选物品的向量预计算好,存入向量数据库(如 Faiss、Milvus)
- 在线:用户请求到来时,只需过一遍 User Tower 得到用户向量,然后去向量数据库做 ANN(近似最近邻)检索
在线请求:
User 特征 → User Tower → user_vec
↓
向量数据库 ANN 检索
(Faiss / Milvus / Qdrant)
↓
返回 Top-K 候选物品这个流程保证了毫秒级响应,因为最耗时的 Item Tower 推理在离线阶段已经完成。
第二幕:RAG 中的向量检索
RAG 的动机
大语言模型(LLM)虽然强大,但有两个硬伤:
- 知识截止:训练数据有时间边界,无法感知最新信息
- 幻觉问题:对于训练数据中覆盖不足的领域,可能生成看似合理但错误的内容
RAG(Retrieval-Augmented Generation,检索增强生成)的解决思路是:先检索,再生成。 在 LLM 生成回答之前,先从外部知识库中检索出相关文档片段,作为上下文注入 Prompt。
向量检索的流程
RAG 的检索环节,核心就是向量检索:
┌──────────────┐ ┌──────────────┐
│ Query Tower │ │ Doc Tower │
│ (Encoder) │ │ (Encoder) │
│ │ │ │
│ 用户问题输入 │ │ 文档块输入 │
│ ↓ │ │ ↓ │
│ Embedding │ │ Embedding │
│ Model │ │ Model │
│ ↓ │ │ ↓ │
│ Query Vec │ │ Doc Vec │
│ (768-d) │ │ (768-d) │
└──────┬───────┘ └──────┬───────┘
│ │
└────────┐ ┌────────────┘
↓ ↓
cos(q, d) 或 dot(q, d)
↓
相关性得分离线索引阶段
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS
# 1. 文档分块
chunks = text_splitter.split_documents(documents)
# 2. 用 Embedding 模型将每个文档块编码为向量
embedding_model = OpenAIEmbeddings()
# 3. 存入向量数据库
vector_store = FAISS.from_documents(chunks, embedding_model)在线查询阶段
# 1. 用户提问
query = "BM25 和 TF-IDF 有什么区别?"
# 2. 用同一个 Embedding 模型编码 Query
# 3. 在向量数据库中做 ANN 检索
relevant_docs = vector_store.similarity_search(query, k=5)
# 4. 将检索到的文档拼入 Prompt,交给 LLM 生成回答
prompt = f"基于以下参考资料回答问题:\n{relevant_docs}\n\n问题:{query}"
answer = llm(prompt)Embedding 模型:共享的编码器
在 RAG 中,Query 和 Document 通常使用同一个 Embedding 模型(如 text-embedding-3-small、bge-large-zh、GTE 等)。这个模型的训练目标和双塔模型如出一辙:
- 语义相关的 Query-Document 对 → 向量距离近
- 不相关的 Query-Document 对 → 向量距离远
只不过 RAG 场景中 Query Tower 和 Doc Tower 共享权重(Siamese 架构),而推荐系统中双塔通常不共享(因为用户和物品的特征空间完全不同)。
第三幕:把两张图放在一起
当我真正把两个架构图并排放在一起时,相似之处清晰得令人震撼:
| 维度 | 双塔模型(搜广推召回) | RAG 向量检索 |
|---|---|---|
| Query 侧 | User Tower:编码用户特征 → 用户向量 | Query Encoder:编码用户问题 → 查询向量 |
| Candidate 侧 | Item Tower:编码物品特征 → 物品向量 | Doc Encoder:编码文档块 → 文档向量 |
| 匹配方式 | cos / dot product | cos / dot product |
| 候选集存储 | 向量数据库(Faiss / Milvus) | 向量数据库(Faiss / Milvus / Qdrant / Pinecone) |
| 检索方式 | ANN 近似最近邻 | ANN 近似最近邻 |
| 离线阶段 | 预计算所有 Item 向量入库 | 预计算所有文档块向量入库 |
| 在线阶段 | 实时计算 User 向量 → ANN 检索 | 实时计算 Query 向量 → ANN 检索 |
| 下游消费 | 精排模型 → 重排 → 展示 | LLM Prompt → 生成回答 |
从数据流的角度看,它们是完全同构的:
[在线输入] → [Encoder] → [Query Vector]
↓
[向量数据库 ANN] ← [离线预计算的 Candidate Vectors]
↓
[Top-K 结果]
↓
[下游模块消费]唯一的区别在于:
- 编码器是否共享:RAG 通常共享,双塔通常不共享
- 输入特征的性质:推荐系统是结构化特征(ID + 行为 + 画像),RAG 是纯文本
- 下游消费方式:推荐给精排模型打分,RAG 给 LLM 当上下文
但这些都是”皮肤”层面的差异,骨架是一样的。
更深一层的相似
如果继续深入,还能发现更多共鸣:
1. 负样本策略一样重要
双塔模型中,负样本的选择直接决定模型质量:随机负采样、In-batch Negatives、Hard Negative Mining,各有利弊。
RAG 的 Embedding 模型训练也面临同样问题。bge 系列模型就大量使用了 Hard Negative 来提升检索质量。如果只用随机负样本,模型只学会了区分”完全不相关”,面对”相关但不是最佳答案”的文档就会犯错。
2. 向量维度的权衡一样纠结
双塔模型中,向量维度通常选 64 ~ 256。维度太低,表达能力不足;维度太高,存储和检索成本爆炸。
RAG 的 Embedding 模型同样面临这个选择。OpenAI 的 text-embedding-3-small 输出 1536 维,bge-small 输出 512 维。在大规模场景下,降维(如 Matryoshka Representation Learning)是必修课。
3. ANN 检索引擎完全通用
双塔模型离线建好的 Faiss 索引,和 RAG 建好的 Faiss 索引,底层用的是同一套 ANN 算法:
- IVF(Inverted File Index):先聚类,再在类内搜索
- HNSW(Hierarchical Navigable Small World):基于图的近似搜索
- PQ(Product Quantization):向量压缩,用空间换速度
我在推荐系统中调过的 Faiss 参数(nprobe、nlist、M、efSearch),拿到 RAG 项目中几乎可以直接复用。
4. 都在走向混合检索
推荐系统早已不是纯向量召回,而是多路召回融合:向量召回 + 协同过滤 + 热门召回 + 规则召回。
RAG 也在走同样的路:向量检索 + BM25 关键词检索 的混合检索(Hybrid Search)已经成为最佳实践。Elastic、Weaviate 等向量数据库都原生支持混合检索。
殊途同归。
悟到
回头看,这种相似其实不是巧合,而是必然。
双塔模型解决的问题是:如何从海量候选中,快速找到与当前用户最匹配的物品。
RAG 向量检索解决的问题是:如何从海量文档中,快速找到与当前问题最相关的片段。
把”用户”换成”问题”,把”物品”换成”文档”,把”匹配”换成”相关”——底层的数学问题完全一致:在高维向量空间中做最近邻搜索。
这让我意识到一件事:技术领域之间的壁垒,远比我们想象的要薄。搜广推工程师觉得 RAG 是”AI 新概念”,NLP 工程师觉得推荐系统是”另一个世界”。但当你剥开术语的外壳,看到的是同一套数学、同一套工程范式。
我在医疗 AI 和军工 Agent 的跨领域经历也反复印证了这一点——不同领域的技术往往共享相同的底层范式,跨界学习的成本远低于从零开始。
所以,并不能用几个标签代表我。但”在向量空间中寻找最近邻”这件事,倒是贯穿了我最近做的几乎所有项目。
也许这就是技术最迷人的地方——万法归一。