ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

生鲜电商级联生成推荐框架:融合微观画像与宏观主题

生鲜电商级联生成推荐框架:融合微观画像与宏观主题

打破部门孤岛!某头部生鲜电商级联生成推荐框架,重构前台微观画像与宏观主题融合

先说个事。我所在的团队,花了将近一个季度,给某头部生鲜电商平台(下文用“I平台”代称)做了一个新的推荐框架。项目代号叫“一本万利”,正式一点的说法是“级联生成推荐框架”。这个项目解决了一个我忍了很久的问题:推荐系统里的“部门孤岛”。

相信很多做电商推荐的同学都有同感。我们的推荐分了很多叶子类目和叶子部门,乳制品、肉禽、冻品、零食、日百……每个部门各自为战。用户上午在乳制品部门看了一圈加购了牛奶,下午打开首页,推荐流里大概率还是那几瓶牛奶相关的商品,他不会收到酸奶、坚果、麦片哪怕他上个月刚买过。你说用户不想买吗?不是,是系统根本没有把“部门”和“部门”之间的关联打通。单个用户是一个孤岛,单个部门也是一个孤岛,推荐结果自然就缺了“全局感”。

这个框架的核心理念,是用“级联生成”的方式,把前台的微观画像(用户此刻想要什么、这个商品和什么搭配)和宏观主题(本周全站主打什么场景、什么季节商品在上升期)拼到同一个决策链路里。效果立竿见影:首页推荐位的客单价平均提升了不少,购物篮从“1+1”变成了“1+3”,连带率涨得很明显。

这篇文章把我从设计到落地的完整过程、踩过的坑、以及最终线上稳定运行的框架结构,从头到尾梳理一遍。里面涉及的技术点有序列建模、双塔召回、主题挖掘、两阶段级联排序,也有大量工程侧的教训。想直接抄作业的,建议重点看第4节和最后的避坑清单。

1. 项目全貌:为什么推荐系统不能继续“各显神通”

1.1 部门孤岛的代价:推荐系统最大的隐性损失

我先给不熟悉业务背景的同学补个场景。生鲜电商和标品电商最大的区别,是购物篮宽度的战略价值远大于单品转化率。用户今天买一盒草莓,如果他顺手又加购了酸奶、燕麦脆和沙拉酱,这一单的毛利可能翻两倍。而现实是,用户看了草莓以后,系统只知道“用户对草莓感兴趣”,于是继续推草莓、蓝莓、樱桃……不会把“草莓”放进“周末轻食早餐”这个场景里去理解。

传统分层推荐结构(也就是常见的召回+粗排+精排)是有天然缺陷的,类目/部门这一层往往是被硬编码进特征的,比如离线算用户在每个部门的活跃度,然后在线按比例分配候选池。这种方式有三个致命问题:

  • 候选生成完全跟着历史行为走,用户没有买过的东西、没逛过的部门,几乎永远进不了候选池;
  • 部门之间的商品关系被切断了,做不了“酸奶+麦片”这类跨部门强关联推荐;
  • 全站运营节奏(比如火锅季、烧烤季、开学季)很难传导给个性化模型,运营只能靠手动改图、改入口,无法实时影响排序。

我们当时看了一个调研数据——大约40%的用户购物篮里只被推荐到了一个部门的商品。这意味着,推荐系统本质上没有在“帮用户组一个篮子”,只是在“把用户可能点一下的商品摆到他面前”。问题是:用户点什么,和用户这单想吃什么,是两码事。

1.2 级联生成式框架的设计取舍:为什么不是纯召回,也不是纯重排

我对业界这两年“生成式推荐”的讨论一直有关注,但说实话,纯粹用一个大模型从零生成整个推荐列表,在线上不太现实。生成结果不够稳定,延迟压不下来,而且没法精细地控制“要给用户解释为什么推荐这个”。

所以我们最终选择的是级联生成,不是端到端生成。两级结构:

  • 第一级(微观画像级):负责生成“用户当前最想要的单个商品和商品组合”,输入是用户实时行为序列、购物篮、商品语义特征,输出是候选商品集合以及每个商品和用户的匹配分数。
  • 第二级(宏观主题级):负责生成“当前时段、当前用户最应该进入什么场景/主题”,输入是宏观运营主题、季节节奏、全站热度和用户长期偏好,输出是一个主题分布,然后用主题信息去约束和调整第一级的候选排序。

如果打个比方,第一级像一个资深导购,一眼看出你今天是想买鱼还是买牛排;第二级像一个区域经理,知道今天全店要推“火锅食材节”,就让导购优先把黄喉、毛肚、火锅底料拿出来。导购懂人,经理懂场。两者凑在一起,才会有一个既符合个体意图、又符合全站节奏的推荐结果。

这个设计还有一个隐性好处,就是可解释性和可干预性。运营想强推某个主题,只需要调第二级的主题权重,不用改任何模型。算法同学想优化用户偏好,只需要迭代第一级的序列模型。两边边界清晰,出了问题也好回溯。

1.3 适用范围与团队配置

做这个项目,团队不需要特别大。我们当时核心团队一共四个人:一个算法工程师负责整体框架和微观模型,一个算法工程师负责主题挖掘和统计模型,一个后端工程师做特征服务和在线推理,还有一个推荐平台工程师负责AB实验和数据回流。从开始设计到第一版上线,大概用了六周;又花了三周做稳定性优化和人工案例复盘。

如果你所在的业务也是“用户会一次买多品类商品”的电商形态,比如社区团购、综合超市到家、生鲜前置仓,这个框架都有直接参考价值。如果是纯标品、纯单品转化的业务,比如数码3C,收益可能会打折,因为用户购物篮本身就不宽,跨部门融合的意义没那么大。

2. 微观画像构建:从“单点偏好”到“此刻需要什么”

2.1 双塔召回与实时特征:用户画像不能是“昨天”的

微观画像的第一层,是我们做了一套基于双塔结构的实时召回模型。这层负责从全站商品池里,快速选出每个用户当下最可能有反应的几百个商品。

用户塔的特征,当时分了五组:

  1. 短期行为序列:最近24小时内浏览、加购、收藏的商品ID序列,按时间排序;
  2. 中期兴趣分布:近7天在各部门的曝光点击率、人均浏览时长、品类占比,以特征数组形式输入;
  3. 购物篮实况:当前购物车内所有商品的部门、品类、价格段、热量、保质期等属性,这个特征在生鲜电商里非常重要,因为用户是先加购后结账的;
  4. 历史偏好兜底:近90天的长期静态偏好embedding,用来防止短期行为过强、导致推荐非常窄;
  5. 环境信号:当前时间、星期、是否工作日、实时天气温度、是否是饭点,这些信号对生鲜场景影响极大,尤其是雨天和临近晚饭时段。

商品塔这边,除了常规的标题、类目、价格、品牌特征,我们重点加了两个生鲜特有特征:保存条件(常温/冷藏/冷冻)和履约距离(这个商品从哪个仓发货、预计多久到)。为什么加保存条件?因为跨部门推荐的基础逻辑是“能放进同一个冰箱”,你在推冷藏酸奶的时候,绝对不应该搭配冷冻披萨。

线上实现时,用户塔是实时更新的。用户每产生一次浏览或加购行为,行为序列变化,通过消息队列实时更新用户embedding缓存。实测中,这种“实时画像”比T+1离线画像在加购率上提升了将近20%。代价是对特征服务压力更大,后面再讲。

2.2 序列模型与图嵌入:两种微观建模的互补

双塔结构适合做大规模召回,但有个问题:它对“用户行为之间的顺序关系”敏感性不够。比如“买了牛奶以后再买咖啡豆”和“买了咖啡豆以后再买牛奶”,虽然商品相同,但用户心智状态完全不同。所以我们在双塔的user tower里嵌了一个轻量GRU序列编码器,输入最近20个行为商品的embedding,输出一个行为序列向量,和静态特征拼接。

这个方案在初期是够用的。但后来我们发现一个现象:很多连带购买关系不是显性的同一Session行为,而是跨天、跨部门发生的。比如用户周一买了即食鸡胸肉,周五又买了生菜,周六买了全麦卷饼,这个“健康轻食”的关系链长达三天。如果只看Session内行为,根本捕捉不到。

于是我们补了一个商品共现图模型:把“同一个购物篮里同时出现”的商品看成一条边,部门和部门之间用商品作为桥梁,构建一个包含数十万节点、数百万边的异质图,然后做graph embedding。最终把商品的图向量拼到item塔的输入里。

图嵌入的效果非常直观:跨部门推荐新商品的命中率明显提高。比如“三文鱼刺身”可以关联到“芥末”和“寿司酱油”,这是传统语义模型很难学到的,但在共现图里,因为有大量真实购物篮支撑,关系非常坚固。

2.3 微观画像踩过的坑

这个模块做了三周,踩了三个坑,值得说说。

第一个是特征穿越。我们把“用户当前购物车的商品特征”放进了训练样本,但训练样本的Label是“用户是否点击/购买了该商品”。坑在于,如果商品已经在购物车里,用户当然不会“点击”它,导致这部分样本的负反馈是假的。解决方法是:构造样本时,把商品已在购物车的情况单独建模为“加购成功”目标,不和点击目标混在一起。

第二个是embedding维度不是越大越好。我们一开始拍脑袋用了128维,结果离线AUC没提升,线上延迟却涨了。后来改用64维,并加了L2约束让向量分布更聚拢,效果不降反升。经验是:生鲜电商的商品总量比标品少很多,几十万级商品规模,64维足够表达;维度太大反而会让向量空间稀疏,相似商品距离变远。

第三个是曝光偏差。线上推荐系统的样本天然只包含“被曝光过”的商品,没被曝光的商品即便用户喜欢也没有样本。我们采用了业界常规的解决方式——随机曝光策略,大约5%的流量给随机或低分商品,这些样本统一带一个“is_random=1”的标记,训练时用来矫正分布。这个让新品的召回命中率从不到1%提升到接近5%。

3. 宏观主题体系:把“部门”升级为“场景”,再用算法生成主题

3.1 主题挖掘:不是运营拍脑袋,是数据里长出来的

宏观主题这个概念的雏形,可能很多平台都有,就是“运营活动页”。比如“火锅节”“烧烤季”“开学早餐计划”。但传统运营主题有两个问题:一是更新慢,通常按周甚至按月换一次;二是覆盖面窄,只服务活动入口,进不了首页信息流。

我们想做的是算法生成的动态主题,主题本身就是模型产出,而不是人工配置。具体做法分三步:

  1. 从购物篮聚类。把近30天所有购物篮按商品共现强度聚类,一个类簇就是一个候选主题。比如“火锅类”聚簇里,出现的高频商品是肥牛卷、毛肚、火锅底料、虾滑、鸳鸯锅……这些商品的组合本身就定义了主题内容。
  2. 用商品属性给主题做语义标注。每个聚簇的商品聚合出高频属性标签,比如“辣的”“冷冻”“即食”“儿童”“进口”,把这些标签组合成主题的描述性名称,便于运营理解。
  3. 用时间序列预测主题趋势。每个主题计算近7天热度、环比增长率、分时段活跃度,然后做一个简单的时序预测,预测未来24小时这个主题的热度变化。同时接入外部特征,比如当地天气预报、节假日、二十四节气,用这些预测下一个小时该主题是否处于上升期。

这套主题体系跑起来以后,我们发现了一些很有意思也很有价值的主题,比如“露营冷餐组合”“加班深夜泡面局”“宝宝辅食周”,这些主题在传统运营眼里不一定会被想到,但对用户来说非常真实。而且它们天然是跨部门的,一个“露营主题”包含熟食、蛋糕、饮料、零食、一次性餐具,直接把部门孤岛打碎了。

3.2 主题与部门KPIs:宏观主题怎么吃下“部门战略”

主题体系上线前,运营团队很担心一件事:如果首页全被主题占据,那各个部门自己的主推品、清仓品、新品怎么办?每个部门都有KPI的。

我们在设计时专门加了一个软约束机制,叫**“部门配比调节器”**。主题生成候选商品时,不只看商品与主题的契合度,还会参考该部门当前战略目标(新品曝光率、动销率、清仓进度),把业务约束编码成一个分数,和主题契合度做加权相加。例如冷启动新品,在新品期内有一个专门的“新品加乘”boost,使这个商品在相关主题里更容易露头,一旦动销上来就恢复正常概率。

这个设计的好处是:宏观主题不是一个独立的“运营位”,它就像一个可编程的分发框架。各个部门的战略可以借主题的势能流动,而不是反过来把主题拆成部门来运营。这样既保住了部门KPI,又实现了跨部门融合。实际运行中,部门和推荐算法团队的对齐效率高了很多,以前改推荐策略要靠运营提需求单,现在直接在框架配置台改权重就能完成。

3.3 主题热度的实时化:避免“昨天很热,今天没人要”

主题如果固定不变,很快就会疲劳。我们要求主题的热度信号按小时更新。主链路的实时化方式是:每小时的整点,离线任务先计算所有候选主题的近24小时曝光/点击/加购/支付数据,然后更新主题的趋势分。趋势分公式很简单,就是:

趋势分 = 近3小时热度/近24小时热度 × 归一化系数

如果某个主题近3小时热度显著高于全天均值,就认为该主题正处于上升期,在宏观层面加大它的注入比例;反之,如果热度持续走低,就降低甚至下线。这套机制上线后,主题推荐位的CTR比固定运营位高出将近一半,因为用户看到的永远是“正在发生”的场。

4. 级联融合与服务化实现:微观×宏观在线决策的完整链路

4.1 两阶段级联的推理流程:先生成候选,再注入主题

这个框架的在线推理流程,可以分成四步走:

第一步,微观候选计算。根据用户当前Profile,用双塔+GRU打分,从全站候选池里取回600个候选商品。这600个商品完全由“用户个性化”驱动,不受主题影响,保证用户的基本偏好不丢。

第二步,主题匹配。把当前时刻所有活跃主题按热度排序,选出用户最可能感兴趣的5个主题,再从每个主题的商品池里取回100个主题候选,对这些主题候选做去重,得到最多500个主题商品。

第三步,候选合并与级联打分。微观候选和主题候选合并成一个候选池,交给精排模型打分。精排模型的输入包括四组特征:

  • 微观匹配度(用户塔和商品塔的内积);
  • 主题契合度(商品在主题高相关商品里的PageRank分数,或商品和主题向量的余弦相似度);
  • 上下文全局状态(当前时段各主题热度、当前品类转化率、天气、价格弹性);
  • 多样性约束(与已展示商品的类目/部门/品牌差异度)。

精排模型是一个带注意力的DNN,输出每个商品在这个“用户×时刻×主题”上下文下的预估CTR和预估加购概率。注意这里模型不看单一概率,而是把CTR和加购率分开建模,最后在业务层用可配的权重合成最终分。

第四步,主题穿插规则。最终排序结果出来以后,我们会做一次“主题穿插”:确保最终返回给用户的20个商品里,至少有30%~40%的商品来自主题商品池。这个比例是动态可调的,根据用户是否是新手、当日是否有大促等条件切换。

用公式概括融合分数,就是下面这个:

最终分 = α × 微观匹配度 + β × 主题契合度 + γ × 上下文全局状态 − δ × 多样性惩罚

α、β、γ、δ不是fixed参数,而是根据用户和场景动态计算的。老用户α调高,保持个性化;新用户β调高,用主题帮助他们发现冷门部门和商品;大促期间γ权重上调,把全站热度的影响放进来。

4.2 在线部署与性能预算:每一层都不能拖后腿

做在线部署的时候,我们定了几个硬指标:P95延迟不超过85毫秒,可用性不低于99.95%。

具体的服务架构是:

  • 特征服务层:用户实时行为缓存(Redis),商品静态特征(Tablestore),实时天气/时间信号(一个轻量HTTP接口包了一层缓存);
  • 召回层:两个独立召回服务并行跑——双塔召回用向量检索服务(HNSW索引,单机几百路QP),主题召回走主题商品表(因为主题数量有限,事实上是离线把主题商品表直接缓存到内存,按期更新,在线查询时做过滤和打分);
  • 精排层:一个带GPU的TensorFlow SavedModel服务,模型输入拼接所有特征成一个大Tensor,batch内部做动态padding,支持单请求多商品打分;
  • 规则层:主题穿插、多样性控制、竞价约束、黑名单,都放在精排后面的一层轻量规则引擎里。

这里我特意强调一下向量检索的坑。双塔召回用的向量库,我们一开始存在Elasticsearch里,结果延迟到了200ms压不下来。后来换成专门的高性能向量检索服务,把索引放在内存里,延迟才降到10ms级别。经验是:不要把向量检索和业务文档检索混在一个引擎里,数据量大起来以后,方方面面都是坑。

另一个部署经验是关于嵌入式层模型和精排模型的版本兼容。因为推荐框架是级联的,第一级“微观画像”的item embedding和第二级“精排模型”的输入特征里都用到了商品embedding,如果两个模型同时升级但embedding版本不一致,精排模型会学到“旧embedding+新权重”的组合,行为会很奇怪。我们上线一个自动化任务,检测两个模型是否存在embedding版本不一致,发现问题时自动回滚到兼容版本,才把这个坑彻底堵住。

4.3 AB实验设计:不能只看CTR,要看“购物篮宽度”

这个框架上线的AB实验,我们折腾了挺久,很大的原因在于推荐系统团队和增长团队对指标的看法不一致。

推荐团队习惯看CTR、GMV;增长团队坚持还要看“购物篮宽度”——用户本单购买了多少个不同部门的商品。后来我们把两组指标都列下来,作为这个实验的“北极星”体系:

  • 用户侧:CTR、点击后加购率、购物篮商品数、购物篮部门数、跨部门连带率;
  • 业务侧:客单价、GMV、新部门渗透率、部门KPI达成率;
  • 健康侧:曝光多样性、主题疲劳度、冷启动新品曝光占比。

结果是很有意思的:单纯看CTR,新框架可能略降0.5%;但购物篮宽度提升9%以上,客单价提升4%以上,GMV净增长非常可观。这说明微观匹配和宏观主题的融合,牺牲了一点即时点击率,换来了更强的跨部门连带转化,全站层面是更健康的增长。

所以,做这类框架,一定要把分析视角拉高,从单次点击的胜负,转移到用户整个购物旅程的完整度上来。

5. 常见问题与排查技巧实录

5.1 级联误差放大:微观候选池把主题商品全剪掉了怎么办

“级联”最大的风险就是误差传导。我们第一次上线时发现:主题商品注入比例很低,用户看到的还是老几样。排查后发现,问题出在第一级微观候选池,虽然我们设了600个候选,但主题商品在池子里的数量很少。原因是精排模型学习到的分布是以往“纯个性化推荐”的信号。主题商品对老用户来说,在微观打分上天然不如“用户最常点击的商品”。

解决方式有两条:

  1. 候选池扩容:600扩到1200,并保证主题商品池在合并前至少有300个名额,无论如何不被过滤;
  2. 人工规则兜底:在精排阶段加一个“主题保护”通道,模型预估分乘以一个主题加权系数。加权系数不设成0/1硬开关,而是一个平滑函数,避免分数突变导致排序抖动。

这个调整上线后,主题注入比例从5%提升到了35%,且整体GMV没受影响。教训是:级联系统里每一级的“通过率”都要监控,不能只看最后一级的输出。

5.2 疲劳与惊喜感的平衡:老用户对主题越来越“无感”

主题推荐跑了两周以后,我们发现一个典型问题:老用户对同样的主题商品开始无感,主题商品点击率明显下滑,但新用户反而越来越喜欢。这就是主题疲劳。

解决办法是给每个主题加了一个**“用户级疲劳衰减因子”**:

主题最终分 = 主题热度分 × 用户对该主题的历史偏好分 × exp(−k × 该主题近期曝光数/用户)

k值调参后,如果用户近期已经看到某个主题超过10次,该主题的权重会衰减到最初的40%左右。同时,全局主题池会定期轮换,把一些热度低于阈值但用户交互好的小众主题重新激活。经过这一轮调整,主题商品点击率在上线三周后仍能维持在一个稳定的高位。

5.3 新用户冷启动:宏观主题是救星,但不能“全是主题”

新用户没有历史行为,微观画像接近于零。这时候主题推荐成为最重要的信息来源。但有一次我们试验“新用户全主题化”后,次日留存反而下降了。分析原因发现,新用户如果看到的前几个商品都是跨部门组合,会觉得自己“被疯狂推销”了,反而失去信任感。

所以最终的策略是:新用户的前3个推荐位必须是“主流热门单品”,也就是全站转化率最高的Top商品;从第4个位置开始才进入主题商品区。这样既保留了主题的探索感,又给新用户一个平稳的接受过程。上线后新用户的次日留存提升了近8%。

5.4 数据质量是命脉:购物篮数据的清洗不能嫌麻烦

最后我想重点说说数据问题。生鲜电商的购物篮数据非常脏。用户会在购物车里放几周然后一起结账;会把已经下架的商品留在车里;会一次性买十件同款但最终取消;会深夜下单然后早上起来退一半。

我们一开始没有做严格的购物篮清洗,导致图嵌入模型学到了一些“怪边”——比如“啤酒”和“尿布”这种经典玩笑关系居然出现了,还有大量“囤货型”购物篮把并无关联的商品强行拉到一起。

后来我们加了一套清洗规则:

  • 只使用“结账后2小时内未退单”的订单作为图嵌入的样本;
  • 剔除商品数超过20件的“批发型”购物篮;
  • 加购超过3天未结账的商品不参与当前篮分析;
  • 当购物篮同时包含冷冻和常温商品时,降低该篮子的权重,因为这种篮子大概率是“多目的混合购物”,不反映真实搭配逻辑。

数据干净了,图嵌入的质量明显提升。所以我的强烈建议是:任何推荐算法项目,先花时间把数据清洗规则定好,否则后面所有模型都在垃圾上跳舞,调参都是白费功夫。

结尾:这个框架运行半年后,我个人的几点体会

说实话,这个框架本身的技术难度并不是“颠覆式”的。双塔、GRU、图嵌入、主题挖掘、两阶段级联,每一个模块单独拿出来,业界都有一堆成熟实践。真正的难点在于把这些模块以“级联”的方式咬合在一起,并且让推荐结果在“用户个性”和“全站节奏”之间找到那个动态平衡点。

我个人最大的体会是,推荐系统做到后期,技术栈反而不容易拉开差距,拉开差距的是对“人”和“场”的理解方式。用户不是一张静态画像,他今天来打开App时的状态,和昨天是完全不同的。部门也不该是静态商品类目,它应该被组织成一个个可变化的场景。把这两层理解放进同一个决策链路里,才是“级联生成推荐框架”真正的价值所在。

如果你也要做类似的项目,我最后再送你三个建议:第一,不要试图用一个超大规模模型解决所有问题,两级或三级级联在工程上更好维护;第二,上线前就把“实验指标体系”定完整,特别是购物篮宽度这类长期健康指标,否则很容易被短期CTR误导;第三,保存一份“模型输入特征版本”和“embedding版本”的对应关系,自动监控版本漂移,这个能帮你省掉无数排查事故的夜晚。

这个框架后续还有很多可扩展的方向,比如把主题挖掘做成纯端上的实时聚类、把主题感知加到商品图模型里、甚至用主题序列给用户做“购物菜谱”推荐。每条路都值得再开一篇文章细聊,有机会我们下次接着写。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进