从用户行为序列到规模定律:广告排序系统的多阶段架构

2026-08-06 59 预计阅读时间: 1 分钟
来源: engineering.fb.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

当推荐系统每天处理数十亿次用户互动时,用户刚刚看过什么、点击过什么、停留了多久,以及这些行为发生的先后顺序,往往比一组静态标签更能说明当前意图。Meta 关于广告排序的最新实践,将讨论重点从“如何加入序列特征”推进到了“如何让序列建模在大规模系统中持续扩展”:模型结构、数据处理和计算预算需要一起设计。

静态特征无法完整表达用户意图

传统广告排序系统通常依赖稀疏特征,例如用户兴趣标签、广告类别、设备信息、地域和历史统计值。这些特征仍然有价值,但它们会丢失行为发生的顺序和时间关系。

例如,下面两种行为历史可能包含完全不同的意图:

用户 A:浏览跑鞋 -> 搜索马拉松训练 -> 点击运动手表
用户 B:点击运动手表 -> 浏览跑鞋 -> 观看旅行视频

如果只把行为聚合成“对跑步、运动手表和旅行感兴趣”,排序模型很难区分用户当前更可能购买什么。序列特征则可以保留以下信息:

  • 行为的先后顺序:搜索通常比普通浏览更接近明确意图。
  • 行为间隔:连续几分钟内发生的行为,可能属于同一个任务。
  • 行为类型:曝光、点击、停留、收藏和转化的信号强度不同。
  • 跨产品上下文:用户在内容、广告和其他产品中的互动可以共同描绘兴趣变化。

这并不意味着静态特征会被完全替代。更实际的方向是让序列表示与已有的稀疏特征、上下文特征和广告特征共同参与排序。

多阶段架构解决规模问题

用户行为序列天然会不断增长,但广告系统不可能对每个候选广告都运行一个昂贵的大模型。一个可行的工程分工是把排序拆成多个阶段:

  1. 候选生成:从海量广告中快速找出与用户和上下文相关的一小批候选。
  2. 轻量预排序:使用较低延迟的模型过滤候选,控制后续计算量。
  3. 序列增强排序:对保留下来的候选使用更丰富的用户行为序列和广告上下文。
  4. 最终决策:综合预测结果、业务约束和展示策略,确定最终排序。

多阶段并不只是“把模型串起来”。每个阶段都有不同的目标函数和资源约束。候选生成更重视召回覆盖率,预排序更重视吞吐量,最终排序则可以承担更高的单请求计算成本。

这种分工也改变了序列模型的使用方式:完整历史序列不一定需要进入每一个阶段。早期阶段可以使用压缩后的用户表示或短窗口特征,后期阶段再使用更细粒度的行为顺序和时间信息。

从单点优化转向规模定律

当序列模型逐步扩大时,开发者需要关注的不只是“模型是否更大”,还要观察增加数据、参数和计算量是否带来稳定收益。这里可以把规模定律理解为一种实验方法:在可控条件下改变模型规模、训练数据规模或计算预算,记录离线指标、在线指标、延迟和成本的变化。

一个简单的实验表可以包含:

实验变量 示例取值 需要观察的指标
序列长度 32、128、512 排序质量、延迟、显存
表示维度 128、256、512 增益、模型大小、服务成本
训练数据量 25%、50%、100% 数据效率和过拟合
计算预算 不同训练步数或 GPU 小时 指标提升是否递减

重要的是,离线指标的提升不能单独决定上线方案。广告排序还受到请求延迟、峰值吞吐、模型更新频率、特征新鲜度和基础设施成本的限制。一个更大的序列模型如果只带来很小的增益,却显著增加线上延迟,就不一定适合放在最终排序阶段。

一个可改造的多阶段序列排序原型

下面的 Python 示例是一个最小化的本地原型,用于演示“短序列预排序 + 更长序列精排”的思路。它不代表 Meta 的内部实现,模型和分数只是可运行的工程示例。运行前只需要安装 Python 3.10 或更高版本,不依赖第三方库。

from dataclasses import dataclass
from math import exp
from typing import Iterable


@dataclass(frozen=True)
class Ad:
    ad_id: str
    topic: str
    quality: float


@dataclass(frozen=True)
class Event:
    topic: str
    kind: str
    age_hours: float


EVENT_WEIGHT = {
    "impression": 0.05,
    "view": 0.20,
    "click": 0.60,
    "save": 0.90,
    "conversion": 1.20,
}


def sequence_score(ad: Ad, events: Iterable[Event], limit: int) -> float:
    """Use recent events as a tiny sequence representation."""
    score = 0.0
    recent_events = list(events)[:limit]
    for event in recent_events:
        if event.topic != ad.topic:
            continue
        recency = exp(-event.age_hours / 24.0)
        score += EVENT_WEIGHT.get(event.kind, 0.0) * recency
    return score


def rank_ads(events: list[Event], ads: list[Ad], shortlist_size: int = 3) -> list[tuple[str, float]]:
    # Stage 1: cheap pre-ranking with a short behavior window.
    pre_ranked = sorted(
        ads,
        key=lambda ad: sequence_score(ad, events, limit=8) + 0.1 * ad.quality,
        reverse=True,
    )[:shortlist_size]

    # Stage 2: richer ranking with a longer behavior window.
    final_ranked = sorted(
        pre_ranked,
        key=lambda ad: sequence_score(ad, events, limit=32) + 0.2 * ad.quality,
        reverse=True,
    )
    return [(ad.ad_id, round(score, 4)) for ad, score in [
        (ad, sequence_score(ad, events, 32) + 0.2 * ad.quality)
        for ad in final_ranked
    ]]


if __name__ == "__main__":
    user_events = [
        Event("running", "click", 2),
        Event("running", "view", 5),
        Event("travel", "click", 12),
        Event("running", "save", 30),
    ]
    candidates = [
        Ad("ad-running-shoes", "running", 0.8),
        Ad("ad-travel-bag", "travel", 0.9),
        Ad("ad-camera", "photography", 1.0),
        Ad("ad-running-watch", "running", 0.7),
    ]
    print(rank_ads(user_events, candidates))

在真实系统中,可以把这个原型替换为以下组件:

  • 用事件 ID、时间间隔和行为类型 embedding 表示每个行为。
  • 用 Transformer、注意力模块或其他序列编码器生成用户表示。
  • 在不同排序阶段使用不同的序列长度和模型宽度。
  • 对线上延迟设置硬性预算,并记录每个阶段的候选数量和耗时。
  • 通过离线回放和在线实验验证序列表示是否带来真实增益。

落地时要守住几个边界

数据新鲜度比盲目加长序列更重要。 很久以前的行为可能对当前意图帮助有限,还会增加存储、传输和计算成本。可以通过时间衰减、窗口截断或分层摘要保留更有价值的信息。

序列越长,训练和服务成本越高。 长序列会带来更高的显存、计算和通信开销。多阶段架构的价值就在于:只把昂贵计算留给最有希望的候选。

离线收益必须与线上约束一起评估。 需要同时记录预测质量、延迟、吞吐、资源消耗和模型更新成本。否则,实验可能得到一个离线更强、线上无法稳定运行的模型。

隐私和数据治理不能后置。 用户行为序列包含高密度的兴趣与意图信息。实际部署时应明确数据保留周期、访问权限、用途限制和删除机制,并对跨产品信号进行严格治理。

给工程团队的采用清单

  • 先确认序列信号是否解决了静态特征无法表达的顺序或新鲜度问题。
  • 为候选生成、预排序和精排分别设定延迟与吞吐预算。
  • 用小规模实验测量序列长度、数据量和模型容量的边际收益。
  • 同时关注离线指标、在线指标、成本和系统稳定性。
  • 将用户序列视为可治理的数据资产,而不是无限增长的缓存。

从用户行为序列走向规模定律,核心变化不是简单地把模型做大,而是把数据、模型、阶段划分和系统资源放进同一个设计框架。只有当每一阶段都清楚自己要优化什么,序列学习才能从研究原型变成可持续运行的广告排序能力。


相关推荐