意图召回与相关性过滤实战:把出价放在排序之后

做推荐或广告系统的同学大概都见过一种排序实现:把出价直接乘进主分数里。跑起来很快,但相关性会退化成事后补丁。本文把这条链路拆成三段------召回、过滤、排序,给出一个可复现的实现。文中的代码是演示用的精简版本,域名统一用 your-domain.test 占位。

一、为什么相关性要排在出价之前

先说结论:相关性过滤必须先于竞价发生,否则出价会污染整个候选集。

朴素竞价的做法是谁出价高谁靠前。这在候选集里全是相关商品时没问题,一旦混进不匹配的候选,就会出现荒谬结果:一个人在找跑鞋,系统因为手机壳的出价更高,把手机壳推到了前面。对用户来说这是噪声,对平台来说这是长期价值的透支。

正确顺序是两段式:

  1. 召回层------把自然语言需求结构化,拉出一批可能相关的候选
  2. 过滤层------按相关性判据把不匹配的剔除
  3. 排序层------在留下的候选里做竞价与效果预估
    三者顺序不能换。换了,收益是短期的,代价是长期的。
    二、召回层:把需求结构化
    过去召回入口是关键词。用户必须先把需求翻译成搜索词,系统再按词去匹配。现在入口更常见的是自然语言的场景描述,所以先要有一层结构化。

intent_schema.py ------ 需求结构化(演示用)

from dataclasses import dataclass, field, asdict

@dataclass

class Intent:

scene: str # 场景,例如「长途飞行」

goal: str # 要办成什么事

constraints: dict = field(default_factory=dict) # 预算、尺寸、时间窗

tolerance: float = 0.5 # 可放宽程度,用于召回扩缩

raw_text: str = ""

def parse(raw_text, extractor, defaults=None):

"""把一句自然语言解析成 Intent。extractor 可以是模型,也可以是规则。"""

d = dict(defaults or {})

got = extractor(raw_text) or {}

d.update(got)

it = Intent(

scene=d.get("scene", "unknown"),

goal=d.get("goal", ""),

constraints=d.get("constraints", {}),

tolerance=float(d.get("tolerance", 0.5)),

raw_text=raw_text,

)

if not it.goal:

raise ValueError("goal 为空,禁止进入召回:缺目标的召回等于随机抽样")

return it

这里有个坑要提前说:goal 缺失时必须直接失败,不能让请求往下走。一个没有目标的召回请求,等价于把全量候选随机抽样,后面的过滤层会承受全部压力,而日志里只会显示「召回量偏高」,看不出真实原因。

另一个细节是 tolerance。它决定召回是收紧还是放宽:值小的时候只捞严格匹配的候选,值大的时候允许跨品类补足。这个参数通常按场景配置,不跟用户走------同一句需求,在标品场景和非标场景里,合适的扩缩幅度差得很远。

三、过滤层:相关性判据与阈值

过滤层的关键不是算法,而是阈值从哪来。把阈值全局写死,是常踩的错法,因为不同品类的容忍度差异极大。

relevance_gate.py ------ 相关性过滤(演示用)

def relevance_score(intent, item, weight=None):

"""输出 0~1 的相关性分值。weight 按品类注入,不写死在函数里。"""

w = weight or {"scene": 0.4, "goal": 0.4, "attr": 0.2}

s = 0.0

s += w"scene" * (1.0 if item.get("scene") == intent.scene else 0.0)

s += w"goal" * float(item.get("goal_hit", 0.0))

s += w"attr" * float(item.get("attr_match", 0.0))

return round(s, 4)

def gate(candidates, intent, weight=None, floor=None):

"""返回 (通过, 被拦)。floor 缺省时按品类配置注入。"""

if floor is None:

raise ValueError("floor 未注入:全局写死阈值会让窄品类全被拦掉")

passed, blocked = \[\], \[\]

for it in candidates:

sc = relevance_score(intent, it, weight)

(passed if sc >= floor else blocked).append({**it, "relevance": sc})

return passed, blocked

两个细节值钱。一是权重按品类注入,不由函数内部决定。二是门限 floor 必须显式传入,缺省直接报错------这类静默降级排查起来很费时。

还有一点建议:把被拦下的候选单独落盘。它有两个用途。一是排查误拦,尤其是窄品类;二是给阈值调参提供样本。只留通过集,等于丢掉了模型反馈里信息量很高的那一部分。

四、排序层:从预测点击切到预测成交

过滤之后才是竞价。这里的目标函数也值得说一句:只优化点击,长周期一定跑偏。

rank_objective.py ------ 目标切换与回归校验(演示用)

def bid_score(pred_ctr, pred_cvr, bid, lam=0.3):

"""把点击与成交的目标揉在一起;lam 是可调的长期项权重。"""

return bid * (1 - lam) * pred_ctr + bid * lam * pred_ctr * pred_cvr

def rank(passed, predict, bid_of, lam=0.3):

scored = \[\]

for it in passed:

p_ctr, p_cvr = predict(it)

scored.append({**it, "score": bid_score(p_ctr, p_cvr, bid_of(it), lam)})

return sorted(scored, key=lambda x: x"score", reverse=True)

lam 不是拍脑袋定的,它应该来自离线评估:把同一批请求分别按纯点击目标和混合目标排序,比较两者在成交指标上的差异。只看点击指标验证成交目标,属于口径错配,上线后会发现排序偏好整体反转。

评估时建议固定三组对照:召回通过率、相关性通过率、成交口径下的排序一致性。前两项描述链路的健康度,第三项描述目标是否真的生效。三者缺一项,调参就会变成盲调。

五、踩坑记录

坑一:把出价写进主排序函数。 相关性会顺势退化成事后补丁,日志里也看不出来。修法是把出价留在收尾那一段,前三段一律不看价格。

坑二:相关性阈值用全局固定值。 窄品类的候选会被整体拦掉,表现为「某些类目永远召不回来」。阈值必须按品类注入,并且要能被单独调。

坑三:用点击指标验证成交目标。 离线口径错配,上线后才发现排序偏好完全反了。评估口径要和目标函数对齐。

坑四:把同一套权重平移到所有品类。 场景权重、目标权重、属性权重三者的比例,在标品与非标品之间差别很大。照搬的结果是某个类目被系统性压死。

六、这套链路的适用边界

需要说清楚的是,这套实现适合做链路雏形与归因排查,不适合直接拿去上线。

其一,它没有处理冷启动。新候选没有历史交互,预测值会偏保守,需要单独的开采策略。

其二,它假设结构化字段是齐的。字段缺失时要走降级路径,而不是用默认值填充------默认值会把噪声伪装成信号。

其三,目标函数的权重是静态的。真实系统里 lam 会随业务阶段调整,写死之后模型会在策略变化时静默失效。

把这三条补齐之后,这条链路才算有资格进评审。

如果团队也在做召回与排序的拆分,评论区说一声,可以把字段口径表补充完整。

相关推荐
知几蜗牛44 分钟前
Java 17标准HTTP调用语音转写API的超时与错误处理
人工智能
静水深码1 小时前
AI 能翻好谐音梗吗
人工智能·后端
9i编程1 小时前
8. 教 AI 上班:带出我的数字同事 —— 亲测第一轮:7 个问题逐条改,打包运行再回看
人工智能·openai·ai编程
用户360055579001 小时前
【FHE 同态加密】我们如何实现同态加密推理(九):逐层尺度自适应——同态加密里为什么每层都要单独定标(scale 管理)
人工智能
知几蜗牛1 小时前
Java标准HTTP调用多模态模型的票据抽取前置校验
人工智能
智驭未来掌门人1 小时前
AI 的“手和眼”:一场关于感知与行动的静默革命
人工智能
柯南46681 小时前
【AI工程师精讲】05:FlashAttention:它不是算得更快,是搬得更少
人工智能·ai编程
C++ 老炮儿的技术栈1 小时前
char (*csConnectName)[256]; 和 char csConnectName [256] 有什么区别
java·c语言·开发语言·c++·人工智能·算法·c
Rocky Ding*1 小时前
VideoChat3 深度解析:视频理解的效率,来自时空压缩与主动感知的共同设计
论文阅读·人工智能·深度学习·机器学习·aigc·ai-native·视频理解