面试里问参数有两种问法。一种是BM25 的公式写一下这种好过。另一种是你们的 k1 取多少为什么这种能过的人少一半。原因不在公式背不熟在于大部分人把 k1 和 b 当成了两个经验数字别人写 1.2 和 0.75我也写。真被问为什么不是 1.5就只剩一句调参调出来的。这篇把这两个参数和 top-p、temperature 放在一起讲因为它们考的是同一件事**你能不能说出这个参数在补偿什么偏差。**说不出来就只是在背数字。一、BM25 公式与三个东西公式先摆出来面试要能写score(D, Q) Σ IDF(qi) × [ f(qi,D) × (k1 1) ] / [ f(qi,D) k1 × (1 - b b × |D| / avgdl) ]符号含义f(qi,D)查询词qi在文档D里出现的次数也就是词频。|D|文档长度avgdl是全部文档的平均长度。k1词频饱和参数。b文档长度归一化参数。IDF(qi)这个词的区分度出现在越少的文档里权重越高。三个东西各管一件事拆开讲。1.1 IDF谁的命中更有分量IDF 决定的是命中这个词算不算本事。满语料都是的词命中了也没信息量只出现在少数文档里的词命中一次就很值钱。工程上常用的是平滑版本IDF(qi) ln( 1 (N - n(qi) 0.5) / (n(qi) 0.5) )N是文档总数n(qi)是包含该词的文档数。加 0.5 平滑是为了避免除零也避免某个词在全部文档出现时 IDF 变成负数。这一条经常被追问为什么不用原始 IDF答案是原始写法在n接近N时会出负值词频越高反而扣分实际排序会乱。1.2 k1不让词频无限加分k1控制的是词频增益的上限。同一个词出现 10 次不应该比出现 3 次重要三倍多否则关键词堆砌就能刷分。公式里f出现在分子和分母上f趋于无穷时整项收敛到k1 1。所以k1的实际含义是这一项的增益最多能到k1 1倍。k1越大词频影响越久才饱和k1越小出现三次就到顶了。常见取值 1.2 到 2.0。取值方向这样判断文档短、查询短、术语重复有意义比如型号、产品名反复出现k1可以大一点1.6 到 2.0。文档长、有堆砌风险、垃圾内容多k1小一点1.0 到 1.2早点饱和压住刷分。这里要收一句别把它当成万能旋钮。上面两条只管分数长什么样不管排序变不变。**k1在排序用途下用默认 1.2 就够只有做阈值截断、或要和向量分数融合时才需要动它。**因为纯排序只看相对顺序1.2 和 1.5 排出来的 Top-10 常常一模一样一旦你要拿分数去卡硬阈值比如低于 0.4 就不召回或者要把两路分数加权分数的绝对分布才开始有意义这时候才值得为k1做一轮网格搜索。1.3 b这是长度补偿不是精度旋钮b是最容易被误解的一个。它不是调精度的它补偿的是长度偏差长文档天然词多任何词都更容易出现如果不惩罚长文档在所有查询上都占优。b 0完全不惩罚b 1完全按长度归一。常见取值 0.75。这里有一个反常识的判断我觉得比记 0.75 有用得多**改 b 之前先看你的文档长度分布。**如果语料里文档长度差两三倍b的影响很小怎么调排序都不变如果长度差两个数量级比如一部分是 FAQ 短句一部分是几百页的合同b就是最敏感的那个参数0.5 和 0.9 能给出完全不同的 Top-3。下面这段代码就是为了让你看见这件事。1.4 参数速查表参数管什么常见取值调大之后调小之后k1词频饱和速度1.2 到 2.0词频影响更久长文档得分更依赖重复早饱和堆砌刷分被压住b长度归一强度0.5 到 0.9常写 0.75短文档占优长文档被压长文档占优容易命中长而全的文档IDF词的区分度由语料决定无法直接调靠停用词表和自定义词典间接影响同上二、跑一遍把 k1 和 b 的影响看出来语料故意设计成两种长度doc0 很长、还款出现七次doc1 只有四个词但精准命中提前 还款。查询是提前 还款。importmathfromcollectionsimportCounter DOCS[还款 还款 还款 每月 还款 金额 固定 提前 还款 需要 预约 还款 方式 等额本息 还款 期限,提前 还款 收取 违约金,贷款 额度 测算 需要 查询 征信 记录 还款 能力,]QUERY提前 还款deftokenize(text):returntext.split()classBM25:def__init__(self,docs,k11.2,b0.75):self.tfs[Counter(tokenize(d))fordindocs]self.lens[len(tokenize(d))fordindocs]self.nlen(docs)self.avgdlsum(self.lens)/self.n self.k1,self.bk1,b dfCounter()fortfinself.tfs:forwintf:df[w]1self.idf{w:math.log(1(self.n-c0.5)/(c0.5))forw,cindf.items()}defscore(self,query,i):s0.0forwintokenize(query):fself.tfs[i].get(w,0)iff0:continuedenomfself.k1*(1-self.bself.b*self.lens[i]/self.avgdl)sself.idf.get(w,0.0)*f*(self.k11)/denomreturnsprint(文档长度 %s, avgdl %.2f%([len(tokenize(d))fordinDOCS],sum(len(tokenize(d))fordinDOCS)/len(DOCS)))print(%-14s %9s %9s %9s %s%(参数,doc0,doc1,doc2,top1))fork1,bin[(1.2,0.75),(2.0,0.75),(0.5,0.75),(1.2,0.0),(1.2,1.0)]:mBM25(DOCS,k1k1,bb)ss[m.score(QUERY,i)foriinrange(len(DOCS))]top1max(range(len(DOCS)),keylambdai:ss[i])print(%-14s %9.3f %9.3f %9.3f doc%d%(k1%.1f b%.2f%(k1,b),ss[0],ss[1],ss[2],top1))实际输出Python 3.13 跑出来的不是示意文档长度 [16, 4, 9], avgdl 9.67 参数 doc0 doc1 doc2 top1 k11.2 b0.75 0.605 0.794 0.137 doc1 k12.0 b0.75 0.635 0.854 0.138 doc1 k10.5 b0.75 0.585 0.707 0.136 doc1 k11.2 b0.00 0.721 0.604 0.134 doc0 k11.2 b1.00 0.575 0.887 0.139 doc1这张表能读出三件事第一b能把排序翻过来。b0.75时 doc1 第一b0时 doc0 第一。doc0 里还款出现七次一旦取消长度惩罚词频优势直接压过精准匹配。这就是改 b 之前先看长度分布的实际含义。**第二在这组语料里k1只改分数不改排序。**三组k1下 doc1 都是第一只是分差变了。但这不能反过来说成k1不影响排序。翻不翻取决于你有词频优势的那个词 IDF 有多高这组语料里还款三篇都有IDF 只有 0.134提前是 0.470doc0 那七次词频全堆在低权重的词上k1抬到 5 也追不回来0.721 对 0.953抬到 10 仍是 0.794 对 1.005。把两个查询词的 IDF 拉平就翻得动换成长短两篇文档doc0 里还款十一次、提前一次doc1 里两个词各一次两个词的 IDF 都变成 0.182k10.3时短文档领先0.396 对 0.399k11.2时长文档反超0.500 对 0.458。所以面试里别说k1不影响排序说在我这组语料上不影响因为词频优势落在低 IDF 的词上。还有一处更隐蔽当召回是分数过阈值的前 K 条时k1会改变谁能过线。候选集卡在阈值边界上、窗口里刚好差一两条的时候k1变大完全可能把第三个候选捞进来过线的人换了排序自然跟着变。要复现上面那组把开头代码里的DOCS换成长短两篇长的那篇十一个还款加一个提前短的那篇提前 还款 违约金 收取QUERY不动打印那行的doc2去掉想复现k15和k110那两行把循环里的参数补上(5.0, 0.75)和(10.0, 0.75)。b和k1的分工也不一样b修排序错误k1主要调分数分布。**第三f1的文档也会被k1影响。**doc1 里还款只出现一次f1时分子分母都带k1所以k1从 0.5 走到 2.0doc1 的分从 0.707 涨到 0.854。这里要分清这条看的是同一个 f 下k1怎么改分数要看饱和得把 f 拉大看高词频那一侧的涨幅怎么收敛。还有一点必须提醒**BM25 的分数绝对值没有意义只有相对顺序有意义。**表里 doc1 是 0.794 还是 0.854除了它跟 doc0 的相对关系之外什么都说明不了。它的分数没有上界也不构成概率不能直接拿来当相关度百分比展示给用户。我见过有产品把 BM25 分数乘 100 当匹配度 79%显示用户一看就问为什么不是 100%解释不清。要做展示就做归一化或者干脆只展示排序。三、中文场景有两个坑比调参重要调 k1 和 b 之前先把这两个坑填了不然调参全是白调。**坑一不分词“等额本息会被切碎。**BM25 是词面匹配词表怎么切直接决定能不能匹配上。中文要先分词而且金融、医疗、法律这类垂类要加自定义词典。不加的话等额本息可能被切成等额和本息用户搜全称反而匹配不上。这一条面试经常被追问你怎么保证术语不被切坏”答案是自定义词典加停用词表并且要把词表版本纳入发版管理。**坑二BM25 解决不了同义改写。**用户说提前结清文档写提前还款词面匹配一律为零分。这也是为什么生产上基本都是混合检索BM25 抓精确术语、数字、型号向量抓语义近似两路结果融合。融合用 RRF 而不是分数加权score(d) Σ 1 / (k rank_i(d)), k 常取 60为什么不用分数加权因为 BM25 的分数量纲和余弦相似度完全不一样前者没有上界后者在 0 到 1 之间直接加权等于让 BM25 单方面决定排序。排名是可比的分数不可比所以按排名倒数求和。k取 60 是一个经验值作用是压住头部排名的差距让第二名和第三名的贡献不至于差太多。再往上还有一层重排。召回阶段用 BM25 加向量拿回 50 到 100 条候选重排阶段用 Cross-Encoder 把这 50 条逐条打分最后取前 5 条给模型。Cross-Encoder 的做法是把 query 和文档拼成一个输入过模型输出一个相关性分它能看到两边的交互所以比向量更准但代价是每一条候选都要单独过一次模型50 条就是 50 次前向做不了初筛只能用在重排。这个三段式召回、融合、重排是面试里最容易被追问结构的地方。被问你的检索链路怎么设计的答BM25 加向量加 RRF 加 Cross-Encoder 重排再补一句每一段的候选数量就比只说技术名词扎实得多。四、top-p 和 temperature一句话分清转到采样侧。这两个参数经常被混着问其实只有一个难点分清它们改的不是同一个东西。temperature 缩放 logits改的是分布的平坦程度。温度高分布变平冷门候选也有机会温度低分布变尖几乎总是选概率最高的那个。注意它改的是同一个候选集内部的相对高低。top-p核采样改的是候选集的大小。把候选按概率从高到低排累加到累计概率达到p就截断只在核里采样。它改的是从多大的范围里选。一句话记法temperature 决定选得多疯top-p 决定从多大范围里选。和 top-k 的区别在动态与固定top-k 固定取前 k 个top-p 的候选数是动态的模型越确定累计概率越快到达 p核就越小。参数改什么与谁正交常见取值temperature分布平坦度缩放 logits与 top-p 正交抽取 / 工具调用 0 到 0.3创意写作 0.7 到 1.0top-p候选集大小累计概率截断与 temperature 正交与 top-k 是动态 vs 固定0.9 到 0.95top-k固定取前 k 个一般与 top-p 二选一20 到 50跑一段代码看候选集怎么变VOCAB[查,询,额,度,吧,啊,嗯,么]PROBS[0.42,0.31,0.14,0.07,0.03,0.02,0.008,0.002]deftop_p_keep(probs,p):ordersorted(range(len(probs)),keylambdai:-probs[i])keep,cum[],0.0foriinorder:keep.append(i)cumprobs[i]ifcump:breakreturnkeep,cumdeftop_k_keep(probs,k):ordersorted(range(len(probs)),keylambdai:-probs[i])[:k]returnorder,sum(probs[i]foriinorder)assertabs(sum(PROBS)-1.0)1e-9,概率必须归一print(%-10s %-8s %s%(策略,候选数,候选词 / 累计概率))forpin(0.5,0.9,0.99):keep,cumtop_p_keep(PROBS,p)print(%-10s %-8d %s / %.3f%(top-p%.2f%p,len(keep),[VOCAB[i]foriinkeep],cum))forkin(2,4):keep,cumtop_k_keep(PROBS,k)print(%-10s %-8d %s / %.3f%(top-k%d%k,len(keep),[VOCAB[i]foriinkeep],cum))实际输出策略 候选数 候选词 / 累计概率 top-p0.50 2 [查, 询] / 0.730 top-p0.90 4 [查, 询, 额, 度] / 0.940 top-p0.99 6 [查, 询, 额, 度, 吧, 啊] / 0.990 top-k2 2 [查, 询] / 0.730 top-k4 4 [查, 询, 额, 度] / 0.940这里有个细节值得留意p0.5的时候实际保留的累计概率是 0.73不是 0.5。因为截断条件是累加到不小于 p 为止是向上取整到下一个候选所以实际保留的核会比 p 大一点。这一点在写代码时容易写错如果你的实现是累加到不超过 p 为止核会偏小输出会更保守看起来像模型变笨了。再看top-p0.9和top-k4这两行候选集完全一样。这是一个很实用的观察**在概率分布比较尖的时候top-p 和 top-k 的效果高度重合。**它们真正拉开差距是在分布平坦的时候也就是模型自己都不确定的时候这时候 top-p 会自动放宽候选top-k 依然只给 4 个。典型取值的场景对应工具调用、输出 JSON、槽位抽取temperature0 到 0.3top-p0.9甚至可以走贪心。这里要的是稳定性不是多样性。问答与摘要temperature0.3 到 0.5top-p0.9。留一点措辞空间但事实不能变。文案、头脑风暴temperature0.7 到 1.0top-p0.95。此时放大候选集才有意义。最后补一个反常识的点面试里讲出来是加分**temperature0不等于完全确定。**在批处理、多卡推理、浮点累加顺序不一致的情况下同一输入仍可能出现细微差异。所以要做可复现的评测光设温度 0 不够还得固定批大小、固定推理后端、固定随机种子并且接受多次跑取多数这种工程妥协。这句话能说出我做过评测比背参数值有用。还有一个实际会踩的坑两个参数调到极端会互相打架。有人为了输出稳定把temperature设成 0同时又设一个很小的top-p比如 0.1。结果候选集只剩一两个词模型只要第一步选错后面整段就崩而且崩得很自信。正确的做法是只动一个要稳定就压temperaturetop-p留在 0.9 附近要多样性就抬temperaturetop-p也可以一起抬到 0.95。两个同时往极端调你调的是两个变量出了问题定位不了是哪个的责任。五、面试怎么答三句话版本背到能脱口而出就行别背长篇。BM25 三句它是 TF-IDF 的改进用 IDF 算词的区分度用k1压住词频堆砌用b做长度归一防止长文档天然占优。它是词面匹配抓不住同义改写所以生产上跟向量检索混合用 RRF 按排名倒数融合。中文要先分词垂类要加自定义词典。top-p 三句核采样把候选按概率排序累加到累计达到 p 就截断只从核里采样。它改的是候选集大小temperature改的是分布平坦度两个正交可以一起调。和 top-k 的差别是动态和固定模型越确定核越小。追问兜底如果他继续问 k1 为什么不是别的值就答我按 1.2 起步是因为语料里长文档占比高先把饱和点压早一点换成短文本为主的库我会往 1.6 试。具体值是我的取舍不是标准答案。参数类问题最忌讳硬报一个数然后说不出为什么那比承认这是我的经验值更掉分。参数这类问题本质上考的不是记忆是你有没有在真实语料上跑过、看过排序变化、判断过原因。调过的人讲取舍没调过的人讲数字面试官听两句就分得出来。参数这类东西光看文章记不住自己拿一段真实语料跑一遍把k1和b各扫五档、看看 Top-10 有没有翻比背十条结论都管用。你在自己项目里踩过哪个参数的坑评论区说说我挑几个典型的做复现。