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

做推荐或广告系统的同学大概都见过一种排序实现:把出价直接乘进主分数里。跑起来很快,但相关性会退化成事后补丁。本文把这条链路拆成三段------召回、过滤、排序,给出一个可复现的实现。文中的代码是演示用的精简版本,域名统一用 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 会随业务阶段调整,写死之后模型会在策略变化时静默失效。

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

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

相关推荐
ZzT15 小时前
AI 时代吃香的三种人:有想法的懒人、替别人着想的人、不被 AI 带偏的人
人工智能·ai编程
姚瑞南16 小时前
【PRD】招投标商机雷达爬虫系统
人工智能·经验分享·笔记·爬虫·产品运营·产品经理
4SAPI16 小时前
多国产模型混合调度与企业级治理生产选型指南:4SAPI、硅基流动与 OpenRouter 的架构权衡
大数据·人工智能·架构·php
武子康16 小时前
Cosmos Curator 总览:从一条样本的组成,选对数据处理入口
人工智能·后端·agent
XMAIPC_Robot16 小时前
-40~85℃宽温实测:RK3588 机器人控制器硬件底座,长时间 SLAM 定位稳定性验证
人工智能·fpga开发·机器人·边缘计算·arm+fpga·rk3588+fpga
锋行天下16 小时前
Langgraph 运行时上下文使用示例
人工智能
zandy101116 小时前
中电金信金融 AI 工程化实践:异构模型纳管、智能体治理与 AI 原生软件工厂
人工智能·金融
吴佳浩16 小时前
中小企业跑125B 大模型:显存装不下的时候,Strata 做了什么
人工智能·llm
学IT的周星星17 小时前
LangGraph:用「流程图」的方式编排 Agent
数据库·人工智能·大模型·llm·流程图·agent
NC 王炸17 小时前
不用 API Key,也能让网页调用 AI?Opera 把本地模型接进了浏览器
人工智能·浏览器扩展·ai 工具·本地 ai·opera·webai