ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

中文语义相关度排序升级指南:从字面匹配到向量检索与精排实践

中文语义相关度排序升级指南:从字面匹配到向量检索与精排实践

做了这么多年搜索、推荐和知识库检索,我越来越确信一件事:中文场景下的相关度排序,如果还停留在字面匹配阶段,天花板已经肉眼可见。所谓“相关度排序”,本质上就是在一堆候选内容里,把跟用户意图最匹配的结果排到最前面。早期我们用关键词重合、TF-IDF、BM25这些“简单匹配”手段,确实解决了从无到有的问题;但到了今天,用户搜索“怎么补办社保卡”,你库里的文档标题写的是“社会保障卡申领与挂失操作指引”——字面重叠少得可怜,可它偏偏就是用户要的结果。这种困境做搜索的同学应该都深有体会。

所以这篇内容我想认真聊聊:中文场景下,如何从“字面匹配”走向“语义相关度排序”。我会把背后的原理、模型选型、数据准备、训练调优、线上部署到效果评估的完整链路拆开讲一遍,既有技术选型层面的思考,也有可以直接落地的实操步骤。适合正在做搜索排序、智能问答、知识库检索、推荐系统召回的工程师,也适合刚入门NLP、想搞明白“语义匹配到底怎么落地”的朋友。

1. 告别简单匹配:中文场景相关度排序究竟难在哪

1.1 字面重合度匹配的“天花板效应”

简单匹配的核心逻辑是“字面长得像,就算相关”。这个思路放到英文场景里能解决不少问题,英文单词之间有空格天然分词,词形变化还可以用词干还原处理一下。但在中文环境里,字面匹配的弱点会被放得很大。比如:

  • “苹果手机”和“iPhone”,一个是中文商品名,一个是英文品牌名,字面完全不重叠,但搜索意图高度一致。
  • “怎么做红烧肉”和“红烧排骨的家常做法”,重叠了“红烧”和“做法”,可用户要的其实是两道不同的菜,相关度没那么高。
  • “武汉市长江大桥”这种经典分词歧义句,不同切分方式得到的匹配结果完全相反。

为什么字面匹配会走到天花板?底层原因是它没有把“语义”建起来。BM25这类模型统计的是词频、逆文档频率和文档长度,它假设“关键词出现越多,文档越相关”。这在精确词匹配场景里有效,但面对同义词、近义词、口语化说法、语序变化,它天然无能为力。更关键的是,用户query通常只有几个词,文档标题可能是一个长句,两者之间的语义桥梁只能靠模型去推断,字面统计信息根本撑不起来。

1.2 中文语境的特殊挑战

中文相关度排序难,不是难在某个单点上,而是难在一连串叠加的问题上。

第一个问题是分词歧义。中文没有天然词边界,同一个字符串在不同语境下切分结果不同。比如“研究生命起源”,可以是“研究/生命/起源”,也可以是“研究生/命/起源”。分词错一步,后面的匹配和向量化全跟着错。

第二个问题是表达方式高度多样。同一件事,有人写成“社保断缴了怎么办”,有人写成“社保中断有什么影响”,还有人直接问“五险一金停了会怎么样”。字面差异巨大,但指的基本是同一件事。要做相关度排序,就必须有一个模型能把这三句话映射到相近的语义空间里。

第三个问题是语序和省略。中文语序相对灵活,“明天去上海出差”和“去上海出差,明天”意思相近;而口语里大量省略主语和连接词,比如“车钥匙呢”“钥匙车呢”这两种说法,机器如果不理解语境,根本分不清哪个是正常表达、哪个是在描述一件事。

第四个问题更隐蔽:相关度本身是一个“场景化”概念。同一个query在不同的产品里,“相关”的定义完全不同。电商场景里,“苹果”更可能指手机而不是水果;医疗知识库里,“感冒”要匹配的是科普文章而不是处方药购买链接。所以中文相关度排序从来不是“一个通用相似度函数走天下”,需要结合业务场景去构建匹配逻辑。

我特意把这些难点摆在前面,是想说清楚一个观念:语义相关度排序不是简单替换一个模型就能解决的,它是从数据、算法到工程的系统性改造。这也是我这篇内容想把每个环节都展开讲的原因。

2. 核心技术选型:从语义理解到排序模型

2.1 语义向量化:让机器理解“意思”的基本功

要摆脱字面匹配,第一步是让文本变成机器能比较“意思”的向量。这就是语义向量化,也叫Embedding。大致思路是:用深度模型把一句话映射成一个高维向量,在这个向量空间里,语义相近的句子距离近,语义无关的句子距离远。

做个类比的话,字面匹配就像比较两个人的简历里写过哪些一样的公司名;语义向量化更像是看这两个人聊天时的思维方式和表达习惯,判断他们是不是一路人。

中文场景里做向量化,模型底座很关键。早期用Word2Vec把词变成向量再池化得到句向量,效果其实一般,因为它没有建模上下文。后来大家普遍转向预训练语言模型,比如中文BERT系列。这里我个人的建议是:不要盲目追求大模型,要关注预训练语料和分词是否覆盖了你的业务领域。比如做医疗问答,通用预训练模型在医学术语上的表示能力往往不够好,优先考虑在医疗语料上继续预训练过的模型。

向量化之后怎么比较相关度?最常见的是余弦相似度。假设两个向量分别是A和B,余弦相似度计算的是它们之间夹角的余弦值,取值在[-1, 1],越接近1越相似。这个指标对向量模长不敏感,也就是说即使两个句子的长度差异很大,只要方向一致,相似度依然很高。这一点正好缓解了query短、文档长的匹配问题。

2.2 双塔结构为什么适合召回阶段

有了向量,还要回答一个工程问题:线上来了一个query,怎么从几百万甚至上亿条文档里快速找出一批最相关的候选?如果每条都实时算一遍相似度,延迟根本扛不住。所以业内普遍采用“召回-排序”级联结构,而召回阶段最常用的就是双塔模型。

双塔模型的结构不复杂:左边一个塔编码query,右边一个塔编码文档,两个塔各自独立生成向量,然后计算向量的相似度分数。为什么叫双塔?因为两边的参数可以完全不同,各自编码各自的输入,互不干扰。关键好处在于:文档向量可以提前算好、建好索引,线上来一个query,只需要算一遍query向量,然后用近邻检索去找最相似的文档向量,延迟可控。

双塔模型在训练时通常用对比学习的方式:把相关的一对(query, document)拉近,把不相关的一对推远。这里有一个实操经验:正样本好办,真正的难点在负样本怎么选,这个我在下一章专门讲。

当然双塔模型也有明显短板。两个塔是独立编码的,query和document在底层没有交互,很多细粒度关系学不到。比如“高血压患者能不能吃西瓜”这句话,它和“高血压患者的饮食禁忌”相关,双塔模型可能抓得住;但和“常见水果对血压的影响”之间的精细关联,双塔往往不如交互式模型。所以双塔一般只用来做召回,保证“别把好结果漏掉”,精排阶段还得靠更细的模型。

2.3 精排阶段:交叉编码器补足交互不足

精排阶段的主流选择是交叉编码器,也叫Cross-Encoder。它和双塔最大的区别是:query和document在输入端就拼在一起送进模型,Transformer的每一层注意力都能让两边充分交互。打个比方,双塔是两个陌生人各自独立表达、再由旁人打分;交叉编码器是让两个人直接面对面聊天,从对话细节里判断他们是不是真的聊得来。

实际落地时,交叉编码器通常不是重新从头训一个大模型,而是基于中文预训练语言模型做微调。输出层一般接一个二分类或者回归头,直接预测“相关”或“不相关”的概率。因为输入拼接了query和document,模型能看到完整的上下文,所以精排的效果通常明显好于双塔,但它每个候选都要单独过一遍模型,延迟高、计算贵。所以常规做法是:召回阶段给几百条候选,精排阶段用交叉编码器逐条打分,再取Top N结果输出。

这里要提醒一句:双塔和交叉编码器不是二选一,而是配合关系。召回负责“宽进”,哪怕混进来一些不太准的候选都没关系;精排负责“严出”,把最准的结果顶到前面。这种级联结构既能控制延迟,又能保证效果。我见过一些团队想跳过召回、直接全部用交叉编码器硬排,结果代价就是线上延迟动不动冲到几百毫秒,还得不停加机器,最后不得不回头把召回补上。

3. 实操:搭建一个中文语义相关度排序系统

3.1 样本构造:正样本好办,难负样本才是关键

任何有监督模型都绕不开数据。语义相关度排序一般用的是(query, document, label)形式的三元组样本,label为1表示相关,0表示不相关。

正样本的来源通常比较直接:线上点击数据里,用户点了某个搜索结果,那条就可以视为正样本。日志数据是个金矿,但要做好清洗,比如用户误点、标题党导致的点击,都会污染样本质量。

比正样本更讲究的是负样本。如果负样本太简单,比如随机挑一些完全不相关的文档给模型学,模型很快就能区分——但你要的不是这个,你要让模型学会区分“看起来相关但实际不相关”的复杂情况。这就要引入难负样本。

难负样本怎么构造?我给一个标准做法:先用简单的BM25或者双塔模型跑一遍,从返回结果里挑那些“分数靠前但人工判定为不相关”的样本。举个例子,用户搜“苹果手机壳”,BM25把“苹果手机新品发布”排得很靠前,因为它包含了“苹果手机”,但内容跟“手机壳”毫无关系——这就是一个很好的难负样本。模型如果能学会把这种结果压下去,排序效果会有质的提升。

构造样本时还需要注意比例。经验做法是:一个正样本配3到5个负样本,其中至少一半是难负样本。负样本一味堆多,容易让模型变得保守,把一些边缘相关的结果也误杀了。

3.2 微调训练:损失函数、参数与效果观察

数据准备好了,接下来要在预训练模型上做微调。这里以双塔模型的对比学习训练为例,说一下核心环节。

损失函数常用InfoNCE,也叫对比学习损失。公式不展开,核心思想是:对于一个给定的query,它有一个正样本文档,还有一堆负样本文档。目标是让query向量和正样本文档向量的相似度,远大于query向量和任意负样本文档向量的相似度。在实际项目里,温度系数是一个很值得调的超参。温度太小,模型会过度放大微小差异,训练不稳定;温度太大,所有样本的相似度都被拉平,学不出区分度。我在中文场景里常用的初始值是0.05,然后根据loss变化微调,通常在0.03到0.1之间。

训练时的batch size很关键。对比学习很吃batch size,因为batch越大,每个query能看到的负样本越多,训练效果越好。但显存有限,所以很多人会采用梯度累积或者把负样本放在同一个batch里来绕过限制。我见过一些项目为了省显存把batch size压到8,效果一塌糊涂,后来换成batch size 128(用了梯度累积),同一个模型效果上涨明显。

微调完成后,上线前一定要观察训练集和验证集上的loss曲线。如果训练loss下降但验证loss不降了,说明开始过拟合,可以提前停或者加大负样本难度。这里还有一条实操经验:定期把所有样本切成一个小集合做人工抽检,不要只盯着数值指标。模型有时候会“学歪”,例如学会按文本长度来打分,这种问题在loss曲线上看不出来,只有人工抽检才能暴露。

3.3 混合召回与向量索引构建

模型训练好了,就要进入线上索引构建环节。召回阶段我强烈建议做混合召回:一边保留BM25或ES的经典字面召回,一边加入语义向量召回。为什么两个都要?因为不是所有场景都适合纯语义召回。

举个真实例子:用户搜索一个很生僻的产品型号“CS-2000B”,语义模型根本没学过这个实体,向量表示会比较飘;但BM25可以靠精确字符串匹配把它找出来。反过来,用户搜“便宜好用的降噪耳机”,文档里写的是“性价比很高的主动降噪耳机”,BM25几乎匹配不上,语义向量召回就能顶上。两条路各管一段,合并之后候选集的质量会更稳。

向量召回落地时,需要对所有文档离线算好向量,然后用近邻检索工具建索引。现在可用的方案不少,有些开源工具本身就对中文向量支持得很好。选择时主要权衡几个点:内存占用、召回速度、是否支持实时更新。早期我习惯用暴力计算,换索引工具之后,几百万条文档的召回耗时从几十毫秒降到了几毫秒。索引构建完毕后,建议先用一批真实query过一遍,专门检查有没有“某类query完全招不回东西”的情况,这类问题通常意味着文档向量和query向量分布不一致,需要回头检查向量化模型。

3.4 在线排序流程与延迟控制

到了线上这一步,要把召回、粗排、精排串成一条流水线。完整流程大致是:

  1. 用户输入query,先做基础的文本预处理,包括分词、纠错、归一化。
  2. 同时走BM25召回和向量召回,合并去重后得到几百条候选文档。
  3. 粗排阶段,可以用双塔向量相似度和BM25分数做一个轻量加权融合,把候选缩减到几十条。粗排的目的是快速砍掉明显不行的,没必要用重模型。
  4. 精排阶段,用交叉编码器对几十条候选打分,按分数降序取Top N。
  5. 如果业务需要,后面还可以接业务规则和多样性控制层。

延迟控制是这个环节的重中之重。我对精排的预算要求一般是单条打分不超过10毫秒,整体排序链路不超过100毫秒。如果精排模型太大导致延迟超标,可以考虑量化加速、减少候选数量或者用更小的精排模型。这里有个技巧:先用粗排把候选从几百砍到三五十,精排只处理三五十条,整体延迟就能稳住。有些团队把排序模型换成超大模型,精度没涨多少、延迟翻了好几倍,属于不划算的买卖。

另外,线上服务别忘了做兜底策略。万一向量索引出问题或者精排服务抖动,必须能自动降级回BM25排序,保证用户不至于搜不到结果。这种兜底在项目初期可能用不上,但等到流量起来后再补,代价就大了。

4. 评估与调优:如何衡量相关度并把它调到好用

4.1 离线评估指标体系

效果好不好,不能只靠感觉,得有指标。相关度排序里,我常用的离线评估指标有三个:

第一个是Recall@K,召回阶段最看重它。它的含义是:前K条召回的文档里,包含多少条真正相关的文档,除以总的相关文档数。比如有100条真实相关文档,你的召回在Top 50里覆盖了80条,Recall@50就是80%。这个指标衡量的是“有没有漏掉好结果”,漏得越多,后面的排序再精准也没用。

第二个是NDCG@K,精排阶段最常用。它不只看相关文档有没有被召回来,还看相关文档排得够不够靠前。NDCG对位置非常敏感,排第1的相关文档比排第5的相关文档得分高很多。为什么这么设计?因为用户大概率只看前几条结果,你把最相关的放在第10位,等于没排。

第三个是MRR,适合问答和单答案场景。它只关心第一条正确结果出现在第几位。比如系统返回的结果里,第一条正确答案出现在第2位,那MRR就是1/2。这个指标很残酷,但跟用户实际体验高度相关。

实际项目中,我一般要求先人工标注一批评测集,数量在几百到几千条之间都可以,然后定期把线上结果跑进去算指标。人工标注虽然贵,但它是所有自动评估的基准,没有它,其他指标都不太可信。

4.2 常见问题排查速查

在项目里跑了一段时间后,有些问题会反复出现。我整理了一个问题排查表,基本上覆盖了大部分情况。

常见现象可能原因排查与解决建议
相关结果排后面,不相关结果排前面难负样本不够,模型没学会区分细微差别增加难负样本比例,迭代训练数据
搜索某些冷门词完全没结果向量召回和字面召回都没覆盖到检查文档是否入库,确认分词器是否切碎实体词
向量召回效果好但延迟太高索引工具选型不当或候选数量过大换更高效的ANN索引,压召回候选数量
精排打分几乎全是高分或全是低分精排模型温度参数不合适,或未做温度校准调温度系数,做分数归一化或等频分桶
线上效果和离线指标对不上线上query分布与离线评测集分布不一致定期从线上日志采样扩充评测集,做A/B测试

有一个容易被忽略的问题是:文档标题和正文长度差异极大时,向量化表示会偏向长文本,导致短标题的相关文档被低估。处理办法是在训练时对长文档做分段表示,或者把标题和正文当作两个独立的字段分别编码再融合。

4.3 调优实操心得

最后分享几个我自己踩过坑之后沉淀下来的调优心得,这些经验不太容易从教科书里看到。

第一,难负样本的收益常常大于换模型。很多团队一上来就想着换更大的预训练模型,其实在数据量不够的情况下,效果提升反而不如把负样本做难。一次真实经历:某知识库项目,换大模型涨了一个点,难负样本重做后涨了四个点,成本还低得多。

第二,温度系数一定要调。很多人用对比学习时直接抄默认参数,却不理解温度在中文场景下的敏感性。温度太高,模型分不清“高血压饮食建议”和“高血压用药指南”这类细粒度差异;温度太低,训练容易震荡。建议每轮训练都记录不同温度下的验证集指标,而不是只看loss曲线。

第三,频繁更新在线模型不如先查数据质量。我见过有项目月度更新模型,指标却连续几个月不涨,排查后发现是标注数据里混入了大量噪音。后来把样本重新清洗了一轮,效果立刻起来。数据质量是排序效果的天花板,模型只是去逼近这个天花板。

第四,语义匹配不是万能的。有些场景,比如商品型号搜索、法律条文编号查询,字面匹配反而比语义匹配精确得多。所以混合召回里一定要给字面匹配留位置,不要全盘向量化。这是我反复强调的。

我个人在实际操作中的体会是:中文场景相关度排序这套技术,难的不是单个模型原理,而是把数据、训练、检索、排序、评估全链路理顺。很多项目效果不好,真正的问题往往出现在源头上:要么是正负样本质量太差,要么是召回和精排没有配合好,要么是评估方式跟业务目标脱节。模型反而不是最大的瓶颈。

所以如果你正在做搜索、问答或者知识库检索,我建议不要急着追最新的模型架构,先把基础链路的数据和评估体系夯实。每一步都不要偷懒,尤其要舍得在负样本上下功夫。这个内容后续你还可以沿着多模态检索、个性化排序这些方向扩展,但前提是单一文本相关度的地基稳了。这套从字面到语义的升级路径,不管技术怎么变,底层思路都不会过时。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进