M3.6 · 商品路径接入 cross-encoder 精排 —— 开发文档(面试向)#
讲为什么这么做、踩了哪个坑,不讲代码。是 M3.5(召回 hybrid 升级)之后的一个小而完整的补腿。
一句话概括#
项目技术栈一直写着「BGE-Reranker 精排」,但这条精排腿之前只接在品类知识库(category_insight),
商品检索(item_search)是裸召回直出——hybrid 粗排完就交给 item_picker,中间缺了 cross-encoder。
这一轮把已经造好的 RerankerClient 接进商品路径,让商品检索也走完整的「召回→精排」。
1. 为什么要补:召回和精排是两种不同的「看」#
hybrid 召回(dense+稀疏)本质是双塔各算各的——query 编码成一个向量、商品编码成另一个,最后比相似度, 两者之间没有交叉特征。所以粗排池里常夹着「相关但跑题」的货:查「旅行收纳包」混进一个只命中 「旅行」二字的洗漱包。
cross-encoder 精排不一样:它把 query 和候选拼进同一个模型一起判相关性,能看到词与词的交叉,更准—— 但也更慢,所以只能对粗排出来的几十条做,不能对全库做。召回管「广」、精排管「准」,各司其职。 这是 检索系统的标准两段式,商品路径之前缺了后半段。
2. 关键设计:funnel 怎么搭#
做成一个漏斗:粗召回 top_k × N(默认 3 倍)条进精排池 → cross-encoder 重排 → 截回 top_k。
粗召回多捞一些,给精排留「把埋在 top_k 边缘的好货捞上来」的空间——这正是 funnel 比「召回完直接截」强的地方。
两个容易做错、特意想清楚的点:
-
truncated信号看「返回数」不看「池子」。 截断信号是给下游判断「要不要换更窄的 query」用的, 语义是「还有没有比我返回的 top_k 更多的货」。所以它必须比capped_k(实际返回数),不能比pool_k(精排池大小)——否则池子一放大,本该报「还有更多」的场景会被误判成「就这些」。 -
复用同一个
RerankerClient,不另造。 和category_insight用的是同一个get_reranker()单例 (远程/rerank+ 本地回退)。同质复用,不给商品路径搞一套专用精排。
3. 踩的坑:别让「测试 stub」去动真实排序#
接完一跑功能验收,立刻发现不对:配置里没有 RERANKER_ENDPOINT,精排退化到本地 Jaccard 回退,
而这个回退把商品排序搞乱了。
刨根问底,这个本地回退本就是个 stub——它在 reranker 模块里写得很清楚:存在的意义是「没有真精排 服务时,让调用链、短路逻辑在离线/CI 下照样测得通」,不是真精排。它按 query×候选的字符/词 token 重叠 (Jaccard)打分。问题来了:
- 跨语言时它完全空转:中文 query 切成单字
{旅,行,收,纳,包},候选标题却是英文{sweet, style, bubble, bag…}——两边零重叠,所有候选都得 0 分。 - 同语言时它是粗糙覆盖:Jaccard 只看字面词重叠,会拿这个糙信号去盖掉已经不错的 hybrid 排序, 反而可能把语义相关但用词不同的好货压下去。
所以正确做法是:只有配了真 cross-encoder(reranker.remote 为真)才重排;没有真模型就直接输出 hybrid 序,
不让一个测试 stub 去扰动生产级排序。改完再验,离线下商品检索干净地直出 hybrid 序(实测「旅行收纳包」top3
全是 travel/storage 包),配上真 /rerank 端点精排才接管。
面试怎么讲:这个坑的价值不在「写对一行 if」,在于想清楚一个组件存在的目的边界。本地回退是为「链路
可测」而生的 stub,不是为「生产排序」而生的。把它无差别地用在真实排序上,就是用错了工具——跨语言空转、
同语言帮倒忙。识别出「这是 stub、不该让它动真序」,比这一行 if reranker.remote 本身重要。
4. 交付与边界#
交付:item_search 走完整「hybrid 召回 → cross-encoder 精排 → top_k」;复用现成 RerankerClient;
新增重排单测(假 reranker 验证输出顺序确由精排分决定);自动门(ruff/format/mypy/pytest 123)全绿;
真实索引端到端验证(离线直出 hybrid、配端点则精排接管)。
诚实的边界:
- 真精排要配
RERANKER_ENDPOINT(现成 BGE-Reranker-v2-m3 的 HTTP 服务)。离线/CI/未配端点时 不重排、直出 hybrid 序——这是有意为之,不是降级 bug。 - 精排池倍数
RERANK_POOL_MULT暂用默认 3,最优值要有召回金标(ESCI)才能调——和融合权重、 BGE-M3 sparse 升级一样,都等 ground truth 落地再精调,没金标盲调没意义。 - 个性化只在 dense 召回通路(
fuse(query,user)),精排目前只用 query×候选,不掺个性化——保持 「精排判相关性、个性化交给召回与 item_picker」的分工。