传统推荐首页通常由召回、排序、分组、页面编排等多个阶段共同完成。Netflix 开发的 GenPage 探索了另一条路径:把用户历史和本次请求上下文组织成提示,让一个生成式 AI 模型直接生成完整的个性化首页。来源摘要指出,这种方式改善了用户参与度,同时降低了服务延迟。
从“逐层筛选”转向“整页生成”
传统流水线往往依次回答几个局部问题:候选内容有哪些、每部内容得分多少、哪些内容应放进同一行、各行如何排序。每个阶段都可能使用不同模型、特征和缓存策略,最终页面是多个局部决策叠加的结果。
GenPage 把问题重新表述为:给定用户历史与当前请求上下文,应该展示怎样的一整页内容?模型的输出不再只是内容分数,而是包含页面结构、内容集合及其顺序的整体结果。
这种改变有两个直接影响:
- 模型可以联合考虑行与行、内容与内容之间的关系,减少重复题材挤满首页的情况。
- 在线服务不必串联同样多的模型和编排步骤,因此有机会缩短端到端延迟。
不过,“一个模型”不等于“一个完全无约束的文本生成接口”。生产系统仍然需要候选内容约束、版权与地域过滤、结构校验、超时控制和降级页面。来源摘要没有披露 GenPage 的具体模型接口、输出格式或部署参数,下面的工程示例因此是可以这样实践的参考设计,而不是 Netflix 内部实现的复刻。
Prompt 的关键是把页面写成受约束的数据结构
首页生成任务不能只告诉模型“推荐一些用户喜欢的内容”。服务端至少要提供三类输入:
- 用户历史,例如观看、跳过、搜索和重复播放行为。
- 请求上下文,例如设备、地区、时间和会话入口。
- 可展示候选集,其中应包含稳定内容 ID、类型、可用地区等元数据。
输出最好使用 JSON,并明确限制行数、每行内容数、内容 ID 来源和去重规则。示意 Prompt 可以写成:
你是流媒体首页编排器。
用户历史:
- 完看:title_101, title_205
- 快速退出:title_330
- 最近搜索:crime drama
请求上下文:
- region: SG
- device: tv
- local_time: 21:30
可用候选 ID:
title_101, title_205, title_411, title_512, title_618, title_720
请生成 3 个推荐行,每行 2 个不同的内容 ID。
只能使用候选列表中的 ID,同一内容不能跨行重复。
仅返回符合约定 Schema 的 JSON,不要输出解释文字。
候选集约束尤其重要。让模型直接生成任意内容名称,会引入不存在的条目、不可播放内容和错误版本。更稳妥的做法是让模型负责页面编排,而把内容资格判断留给确定性的业务服务。
可以这样实践:构建一个带校验和降级的最小服务
下面是一个可运行的 FastAPI 示例。为避免假设不存在的 GenPage API,示例用确定性函数模拟生成模型;接入真实模型时,只需替换 call_model,保留 Schema 校验和降级逻辑。
先安装依赖:
python -m pip install fastapi uvicorn pydantic
创建 app.py:
from typing import Literal
from fastapi import FastAPI
from pydantic import BaseModel, Field, ValidationError
app = FastAPI()
class Candidate(BaseModel):
id: str
title: str
genre: str
class PageRequest(BaseModel):
user_id: str
recent_genres: list[str] = Field(default_factory=list)
device: Literal["tv", "mobile", "web"]
region: str
candidates: list[Candidate]
class Row(BaseModel):
title: str
item_ids: list[str] = Field(min_length=1, max_length=6)
class GeneratedPage(BaseModel):
rows: list[Row] = Field(min_length=1, max_length=8)
def call_model(request: PageRequest) -> dict:
"""用确定性逻辑模拟模型;生产环境可替换为受约束的 LLM 调用。"""
preferred = [
item for item in request.candidates
if item.genre in request.recent_genres
]
remaining = [item for item in request.candidates if item not in preferred]
ordered = preferred + remaining
return {
"rows": [
{
"title": "为你精选",
"item_ids": [item.id for item in ordered[:4]],
}
]
}
def validate_catalog(page: GeneratedPage, candidates: list[Candidate]) -> None:
allowed = {item.id for item in candidates}
ids = [item_id for row in page.rows for item_id in row.item_ids]
if any(item_id not in allowed for item_id in ids):
raise ValueError("模型返回了候选集之外的内容")
if len(ids) != len(set(ids)):
raise ValueError("页面包含重复内容")
def fallback_page(candidates: list[Candidate]) -> GeneratedPage:
return GeneratedPage(
rows=[Row(title="热门内容", item_ids=[c.id for c in candidates[:4]])]
)
@app.post("/homepage", response_model=GeneratedPage)
def generate_homepage(request: PageRequest) -> GeneratedPage:
try:
page = GeneratedPage.model_validate(call_model(request))
validate_catalog(page, request.candidates)
return page
except (ValidationError, ValueError, TimeoutError):
return fallback_page(request.candidates)
启动服务:
uvicorn app:app --reload --port 8000
发送请求:
curl -s http://127.0.0.1:8000/homepage \
-H 'Content-Type: application/json' \
-d '{
"user_id": "u-42",
"recent_genres": ["crime"],
"device": "tv",
"region": "SG",
"candidates": [
{"id": "t-101", "title": "Night Case", "genre": "crime"},
{"id": "t-205", "title": "City Files", "genre": "crime"},
{"id": "t-330", "title": "Deep Orbit", "genre": "sci-fi"},
{"id": "t-411", "title": "Kitchen Run", "genre": "reality"}
]
}'
这个示例刻意把生成和验证分开。模型负责选择与编排,代码负责确认输出能否执行。接入真实模型后,还应增加请求超时、并发限制、结构化输出模式、日志脱敏和版本标识。
延迟下降不能只看模型调用次数
单模型生成整页可能减少流水线跳数,但实际延迟仍取决于 Prompt 长度、候选集规模、输出 Token 数、模型推理时间和校验重试次数。若把完整用户历史与数万候选内容全部塞入上下文,生成方案可能反而更慢、更贵。
更实际的部署方式是保留轻量候选检索,再由生成模型完成高层页面编排。可以重点观测以下指标:
p50、p95和p99端到端首页延迟。- 首次生成成功率、Schema 校验失败率和超时率。
- 降级页面触发比例。
- 每次请求的输入、输出 Token 与推理成本。
- 内容重复率、多样性以及点击、播放、观看时长等参与指标。
参与度实验也需要防止短期指标掩盖长期问题。例如,过度强化用户最近看过的类型可能提高即时点击,却降低发现新内容的机会。整页模型应同时接受相关性、多样性、新鲜度和业务约束。
上线前的工程检查
引入整页生成时,可以按以下顺序控制风险:
- 先在影子流量中生成页面,不向用户展示,只比较延迟、合法性和重复率。
- 使用服务端候选 ID,禁止模型凭空创建内容。
- 对输出执行 Schema、版权、地区、年龄分级和去重校验。
- 为超时、空输出和非法输出准备缓存页或传统推荐页。
- 对模型、Prompt、候选集和策略分别记录版本,保证实验可复现。
- 逐步扩大 A/B 流量,同时观察参与度、延迟、成本和内容生态指标。
GenPage 最值得关注的并不只是把多个模型换成一个模型,而是把推荐系统的优化单位从单个内容分数提升到完整页面。它简化了部分在线链路,也把更多责任集中到生成模型及其约束层。真正可用的实现必须同时做好生成、验证、降级和实验治理。