My Blog
Agent 学习

Agent 学习第 8 周:文本匹配,双塔与交互式的分工

双塔负责召回、交互式负责精排;先用 Jaccard 字面基线校准,再看 BiEncoder 与 CrossEncoder 的真实差距。

发布于 2026/9/11更新于 2026/9/11阅读约 12 分钟
只问这篇文章 →

第八周做文本匹配。这周的内容后来被反复用到——RAG 的召回 + 精排两阶段,理论基础就在这里。

作业是在两个数据集(lcqmc 和 bq_corpus)上对比不同方法,跑了 BiEncoder 的两种 loss、CrossEncoder,还有 LLM 路线。


一、和前两周的区别

week6 文本分类   一段文本    → 一个类别
week7 序列标注   一段文本    → 每个字一个标签
week8 文本匹配   两段文本    → 一个 0/1

「输入是一对」带来了本周的核心问题:两句话在什么时候发生交互。

两种架构给出了完全不同的答案,这就是本周要对比的东西。


二、两大范式

表示型 BiEncoder(双塔)

句子 A → Encoder → 向量 A ┐
                          ├→ 算余弦相似度
句子 B → Encoder → 向量 B ┘

两句话分别编码,最后才用余弦相似度比一下。

交互型 CrossEncoder

[CLS] 句子A [SEP] 句子B [SEP] → Encoder → 分类头 → 相似/不相似

两句话拼在一起送进模型,从第一层就开始交互。

这个差别决定了用途

BiEncoderCrossEncoder
能否预计算能——句向量可离线算好不能,每对都要完整跑一遍
速度快(查询编码一次 + 矩阵乘法)慢(N 对就要 N 次前向)
精度较低(丢失了交互信息)高
用途向量检索 / RAG 召回精排

工业界的标准做法是两者串联:双塔召回(快)+ 交互式精排(准)。

这句话就是 RAG 里 检索 Top-50 → Rerank 到 Top-5 的由来。


三、双塔的两种 loss

我在作业里对比了两种训练目标:

  • cosine loss:直接优化正例对的余弦相似度趋近 1、负例趋近 0
  • triplet loss:max(0, d(a,p) − d(a,n) + margin),让正例比负例近至少一个 margin

有个数据上的细节值得注意:

triplet 的训练样本天然比另外两种少——负例对只被用作负样本来源,不直接产生训练样本。这是方法本身的性质,不是配置问题。


四、数据:先看清楚再下结论

老师的项目默认跑 afqmc,我换了另外两个:

lcqmcbq_corpus
来源百度知道问句银行客服问句
train238,76668,960
训练集正例率58.0%49.9%
验证集正例率50.0%50.2%
句长均值10.9 字13.9 字

两者的验证/测试集都接近 50/50,所以 accuracy 是有意义的指标(如果是 9:1,accuracy 就没法看了)。

lcqmc 训练集正例率 58% 而验证集 50%,存在轻微的分布偏移。

字面重合度基线:所有分数的参照系

这一步我觉得是这周最有价值的操作。

在跑任何模型之前,先算一个最朴素的基线:用 Jaccard 系数(按字算交集/并集)在验证集上搜最优阈值,看「完全不训练能到多少」。

数据集正例平均 Jaccard负例平均基线 accuracy基线 F1
lcqmc0.7140.5560.60770.7078
bq_corpus0.2840.1490.65920.7160

这个基线暴露了两个数据集的本质差异:

lcqmc 的正负例在字面上都极其相似(0.714 vs 0.556,只差 1.28 倍),字面重合几乎区分不开。看它的负例就明白了:

[不同义] 大家觉得她好看吗            /  大家觉得跑男好看吗?
[不同义] 晚上睡觉带着耳机听音乐有什么害处吗?  /  孕妇可以戴耳机听音乐吗?

负例是刻意挑的「字面像但意思不同」——这个数据集从设计上就在逼模型用语义。

而 bq_corpus 是客服问句,用户表述五花八门,正负例字面重合都低(0.284 vs 0.149,差 1.91 倍),反倒靠字面能多分对一点。

有了这个基线,后面所有模型分数才有意义。 一个模型跑到 0.70,在 lcqmc 上是真本事(基线 0.61),在 bq_corpus 上就很一般(基线 0.66)。


五、结果

以 lcqmc 为例(accuracy):

方法结果训练耗时
BiEncoder-cosine0.8700224s
BiEncoder-triplet0.8609158s
字面基线0.60770

CrossEncoder 的精度更高,但代价是无法预计算——对每一对都要完整跑一遍模型。这也再次印证了「召回用双塔、精排用交互」的分工。


六、LLM 路线

我还跑了一条 LLM 路线(src_llm/llm_match_deepseek.py),用 DeepSeek 做零样本判断。

课程给的定位很清楚:

LLM 方式主要用于打标和冷启动,不是用来上线的。

工业落地的典型流程是:

LLM 打标 → BiEncoder 预训练 → 难负例挖掘 → Bi + Cross 上线

LLM 在这条链路里的位置是生产标注数据,而不是在线服务——因为延迟和成本都不允许。


这周的收获

1. 架构差异决定用途,不是决定优劣。

BiEncoder 不是「更差的 CrossEncoder」,它们在解决不同的问题。能不能预计算,是这两者最本质的分野——这一条决定了系统架构长什么样。

2. 先建基线,再看模型。

Jaccard 基线只花了十几分钟,但它让后面所有分数都变得可解释。没有基线的准确率是没有意义的数字。

这个习惯我后来一直在用——Week10 做 RAG 时也是先跑纯 BM25 当基线,才发现向量检索在我那批数据上反而更弱。

3. 看负例比看正例有信息量。

lcqmc 那两条负例样本(「她好看吗 / 跑男好看吗」)一下子就让我明白了这个数据集难在哪。数据集的难度是设计出来的,看负例最容易看出设计者的意图。

下周是大模型应用的补充知识——Tokenizer、KV Cache、vLLM,还要在 WSL 里搭环境。

相关文章