ToWow 算法解读:从候选检索到合作安排 · 阅读系列导读 →
BM25 是一种文本检索评分方法:给定查询和一组文档,依据查询词在各文档中出现的情况计算分数,再将文档排序。在 ToWow 的发现前端研究中,查询是一项合作需求,文档是参与者已经披露的能力或服务卡片。系统需要先从这些卡片中找出可能有帮助的候选,供后续了解和协商。
我们把 BM25 保留为检索基线,用来比较向量表示、多路融合等方法是否真的改善了候选排序。它只对已有文字及披露标签打分,无法发现尚未表达的能力;它的优势是规则明确,每一次加分都能查回某个词或特征。本文先解释评分怎样改变排名,再说明合作发现中的特征适配与已有诊断结果。
词频计数的问题:常见词和长卡片容易获得过高分数
假设有人写下需求:“找维修服务,修一把旧木椅。”候选卡片里,一位师傅简短写着“木椅维修、榫卯加固”;另一位写了很长的介绍,多次提到“服务周到、提供服务、售后服务”;还有一家主要运送家具。
最直接的办法,是数需求里的词在卡片里出现了多少次。它能找到明显的文字重合,却也给长卡片留下了很大的优势:只要文字够多、某个常见词重复够多,就容易获得高分。我们真正关心的线索,反而可能埋在一句很短的“榫卯加固”里。
BM25 对这种情况作了三个互相配合的处理。罕见的词通常比到处都有的词更能区分候选;同一个词重复出现,追加的价值会逐渐变小;一张本来就很长的卡片,需要为更多的词命中作长度调整。
这些处理没有替系统理解木工手艺。它们先把“文字提到了什么”这件事计算得更合理。

BM25 评分:稀有度、词频饱和与长度归一化
先把上面的自然语言压缩成一个可以算清的小例子。设候选池只有三张等长卡片,我们已经提取出一个检索词,它只出现在其中一张卡片里。这里假设完成了词提取,暂时不讨论中文怎么切分。
稀有度:含词卡片越少,区分候选的权重越大
本地实现给这个词的稀有度权重是:
ln(1 + (候选总数 − 含词卡片数 + 0.5) / (含词卡片数 + 0.5))
代入三张卡、一张含词,得到 ln(1 + 2.5 / 1.5),约为 0.981。如果三张卡全都含有这个词,权重约为 0.134。算法没有宣布后者毫无意义,只是认为它不太能帮助我们把三张卡区分开。
词频饱和:重复十次,不等于十倍相关
接下来处理重复。当前实现的默认参数 k1 是 1.2。在卡片长度等于平均长度时,词频这一项可以简化为:
重复贡献系数 = 词频 × 2.2 / (词频 + 1.2)
出现一次,系数为 1;出现两次,系数为 1.375;出现十次,也只有约 1.964。它仍会因重复而加分,但不会因为把同一个词写十遍就加十分。
| 同一罕见词的出现次数 | 重复贡献系数 | 该词的分数贡献 |
|---|---|---|
| 1 | 1.000 | 0.981 |
| 2 | 1.375 | 1.349 |
| 10 | 1.964 | 1.927 |
这张表解释了 BM25 的一个重要性格:第一次命中很有意义,后面的重复逐渐失去说服力。k1 控制这种饱和的形状,它不是“智能程度”旋钮。
现在让这些数真正决定一次排序。设查询里有一个罕见词和一个三张卡都包含的常见词;卡片甲各出现一次,卡片乙没有罕见词,却重复了十次常见词,卡片丙只出现一次常见词。三张卡用其他无关词补成等长。直接数命中次数,乙以十次排在甲的两次之前;按上述 BM25 规则,用未取整的权重计算,甲的两项贡献相加约为 1.114,乙的常见词权重乘以重复系数约为 0.262,丙约得 0.134。排序变成甲、乙、丙。真正改变选择的,是稀有度与重复饱和共同压过了机械计数。
长度归一化:用参数 b 调整长卡片的命中优势
长度调整再加进来。完整的重复贡献系数是 词频 × (k1+1) / [词频 + k1 × (1−b+b×卡片长度/平均长度)]。长度等于平均值时,方括号内的长度调整部分回到 1,便得到前面的简式。若卡片长到平均值的两倍,词频仍为 1,代入默认 b=0.75,系数就是 2.2 / [1+1.2×(0.25+0.75×2)] = 2.2/3.1,约为 0.710。该词的贡献从约 0.981 降到约 0.696。算法因此不会把长文档自带的更多命中机会,全当成更强的相关性。
这不意味着长介绍应该被惩罚。b 控制的是需要补偿多少长度差异;如果卡片很长恰好因为它包含更多真实能力,过强的长度调整也可能损失好候选。默认参数是一种实现选择,要靠任务数据判断是否合适。
合作发现中的适配:区分需求与供给,处理中文特征
BM25 来自信息检索的概率相关性研究,Stephen Robertson 与 Hugo Zaragoza 的综述交代了它的理论背景和扩展。我们使用的是已有方法,而不是把它重新命名为 ToWow 的原创算法。The Probabilistic Relevance Framework: BM25 and Beyond
把它用在合作发现里,仍然需要处理文档搜索里未必突出的细节。一个人写“我需要维修”,另一个人也写“我需要维修”,文字可以很相近,但两个人不一定互相提供帮助。对需求方来说,候选的提供方向才是关键。
我们的基线因此不只读取正文,还把已经披露的语言、方向等信息变成检索特征。例如,需求中识别到“寻找服务”的方向,会对应期待候选是“提供服务”。这些特征仍然参与同一套打分,不是偷偷从未披露材料中推测能力。
中文输入还有另一层处理。实现会使用中文二字、三字片段等词特征,避免把整句中文当成一个不可拆的词。因此,前面的“木椅维修”故事说明的是使用情境,计算表说明的是提取出某个词后的规则。两者不能混在一起,假装自然语言已经被完美理解。
真正的输出是一张排序后的候选表,以及推动每张卡得分的词。若某张卡排名出乎意料,我们可以检查它究竟因“维修”命中,还是因语言、方向标签命中。这种可追查性,使它很适合用来发现输入表示里的问题。
诊断结果:BM25 为新方法提供了什么比较基准
在一次本地诊断评估中,BM25 的平均 Recall@100 约为 0.538。这个指标的意思是:对每个有可评估正例的案例,看前一百个候选找回了多少相关项,再对案例求平均。该项有 37 个案例进入计算;整份报告包含的任务数不能替代这个样本数。
这个读数来自部分研究材料的诊断集,相关标注并不完整。它没有证明 BM25 已经能解决现实合作,也没有证明 0.538 是一个足够好的业务水平。它给我们的是一条可以比较的起跑线:任何更复杂的方法,都应在同样的资料、候选和指标下说明自己改善了什么。
我们后来确实比较了多路融合与新的表示。结果没有支持一个简单的故事——“方法更新,所以效果更好”。这也是保留基线的价值:它让新方法必须面对具体的旧能力,而不只是面对一个被想象得很弱的对手。
当然,关键词有清楚的盲区。师傅只写“加固榫卯”,用户只说“椅子晃”,两边未必共享足够的词;一个人的能力尚未表达出来,文本排序也无从发现。解决这些问题,需要补充表达、交互或其他检索线索。先把关键词用好,不会让这些差异消失;它会让我们更清楚地看见,下一步到底应该补哪一种能力。
材料说明。 实现与默认参数见 arms.py(实现定位保留于编辑档案) 的 score_sparse。本例各检索词的查询频率取 1;实际实现还对查询中的重复频率加权。历史数字来自 M1 evaluation-v3/report.json,为诊断性宏平均,非线上表现。完整来源坐标集中在本套文章的来源说明中。
公众号排版与配图
本篇另备有明亮、适合手机连续阅读的公众号排版稿,配图按照本文机制单独绘制。查看公众号版,或下载 Markdown、HTML、PNG 与 SVG 配图包。