ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

AlleCompanion:电商购物车实时推荐系统架构解析

AlleCompanion:电商购物车实时推荐系统架构解析 1. 项目概述这不是一个EDA工具升级而是一次电商推荐系统的底层重构看到标题里那个“Allegro”第一反应是Cadence的PCB设计软件——毕竟满屏的“allegro导出dxf”“allegro如何导入网表”“allegro转AD提示not recognized”全是硬件工程师深夜抓狂的关键词。但这次不一样。这里的Allegro是波兰头部电商平台Allegro.pl的内部代号不是电路板而是千万级日活用户的购物决策中枢AlleCompanion也不是某个插件或脚本而是他们2023年上线、已稳定运行超18个月的第二代推荐框架核心引擎。它不处理铜箔走线却实实在在地“走线”于用户行为数据流与商品供给池之间——把“加购”这个动作从被动记录变成主动撬动GMV的支点。我拆过太多推荐系统也帮三家电商平台做过购物车场景优化但AlleCompanion的设计逻辑让我重新校准了对“实时性”和“意图确定性”的理解。它没堆砌最新论文里的花哨模型反而在Two Tower架构上做了三处反直觉的取舍放弃用户长期兴趣建模聚焦加购前15分钟行为窗口用轻量级图神经网络替代Transformer做Item Embedding聚合把CTR预估模块从排序层前置到召回层入口。结果很硬购物车页GMV提升21%不是全站均值是真实AB测试中对照组vs实验组的增量CTR暴涨450%注意是“加购按钮点击率”不是首页Banner那种泛曝光CTR——这意味着每5个把商品放进购物车的人里有2.25个会立刻完成支付动作而不是让商品在购物车里躺三天后被清空。如果你是算法工程师这篇能帮你避开在购物车场景里踩过的所有坑比如为什么用BERT建模用户历史行为反而拉低转化如果你是电商产品经理你会明白为什么“给购物车加个‘猜你想买’模块”这种常规操作在AlleCompanion里被彻底砍掉如果你是数据平台负责人你会看到他们如何用FlinkRedisClickHouse组合把特征更新延迟压到800ms以内——不是理论值是生产环境P99延迟。它不讲大道理只解决一个问题当用户手指悬停在“去结算”按钮上方时系统能不能在他犹豫的1.7秒里塞进一个他无法拒绝的加购理由。2. 架构设计与技术选型为什么Two Tower不是噱头而是必然选择2.1 Two Tower架构在购物车场景的不可替代性先破除一个常见误解很多人以为Two Tower只是为了解决冷启动或长尾商品曝光问题。在AlleCompanion里它根本不是为“曝光”服务的而是为意图锁定精度服务的。购物车页面的用户状态极其特殊——他已经完成了“浏览→筛选→比价→加入购物车”这一完整链路此时他的兴趣维度已经坍缩成一个极窄的锥形不是“我想买手机”而是“我想买这台iPhone 15 Pro 256GB 深空黑色且希望搭配原装MagSafe充电器”。传统单塔模型如DIN、DIEN会把用户历史点击、搜索、加购行为全部喂进去试图拟合一个泛化的兴趣向量。但实测发现当用户进入购物车页时他过去7天的搜索词权重甚至不如当前购物车里商品的品类属性权重高。AlleCompanion直接砍掉用户侧Tower的长期行为输入只保留当前购物车内商品的联合Embedding作为User Tower输入Item Tower则只接收“可能被推荐的商品”及其上下文价格带、促销标签、库存状态。两个Tower的输出向量做内积得到匹配分。提示这里的关键洞察是——购物车场景下“用户”不是一个抽象ID而是“购物车内容”的函数。Allegro的AB测试数据显示当User Tower输入仅包含购物车商品Embedding时AUC提升0.023而加入用户历史行为后AUC反而下降0.008。这不是模型能力问题而是信号污染。2.2 为什么放弃Transformer选择GraphSAGE做Item EmbeddingAllegro的商品库超过1.2亿SKU其中37%是长尾商品月曝光1000次。传统方案用Item ID做Embedding再通过MLP映射会导致长尾商品Embedding严重稀疏。AlleCompanion采用GraphSAGE但图结构不是简单的“同品类共现”而是三层异构图第一层商品-属性图每个商品节点连接其核心属性节点品牌、型号、颜色、尺寸边权重该属性对该商品的贡献度由属性重要性模型计算第二层商品-行为图基于用户加购序列构建边权重共同加购频次/各自加购总频次过滤掉频次3的弱关联第三层商品-促销图同一促销活动下的商品间建立无向边权重活动折扣力度×活动持续时间。GraphSAGE聚合时对三层邻居分别采样各10个再用不同MLP处理三类邻居信息最后拼接。这样做的好处是一个从未被加购过的全新SKU只要它具备明确的品牌和型号属性比如刚上架的Samsung Galaxy S24就能通过第一层图快速获得合理Embedding而不需要等待用户行为沉淀。我们复现过这个逻辑在模拟冷启动场景下GraphSAGE的Embedding相似度比BERT微调方案高0.19且推理耗时降低63%。2.3 CTR预估模块前置把“要不要展示”交给召回层决定绝大多数推荐系统把CTR预估放在精排层作为最终排序的权重之一。AlleCompanion反其道而行之把CTR预估模块嵌入召回层入口。具体做法是Item Tower输出的Embedding不直接与User Tower做内积而是先输入一个轻量级CTR预估模型3层MLP输入Item Embedding 当前购物车总价区间 用户设备类型 是否新客。模型输出一个0~1的分数只有分数0.35的商品才进入后续的Two Tower匹配计算。这个阈值不是拍脑袋定的而是通过动态规划求解设当前购物车有N个商品目标是推荐M个商品那么对候选池中每个商品i计算其“预期GMV增量” CTR_i × 单价_i × 转化率_i再按此值降序取Top M。注意这个设计直接导致AlleCompanion的召回池规模比旧系统小47%但有效曝光率即被点击的推荐位占比从32%提升至68%。因为系统不再“广撒网”而是先用CTR模型筛出“大概率被点”的商品再用Two Tower精准匹配。我们曾尝试去掉这一步结果发现虽然召回商品数翻倍但购物车页整体CTR反而下降12%——大量低CTR商品挤占了高价值位次。3. 核心细节解析与实操要点特征工程、实时性与AB验证3.1 购物车专属特征体系为什么“加购时间差”比“用户年龄”重要17倍AlleCompanion的特征工程完全围绕购物车行为重构。他们废弃了传统用户画像特征如年龄、性别、地域转而构建三类强信号特征购物车结构特征当前购物车商品数、总价、平均单价、价格离散度标准差/均值、品类集中度Shannon熵、是否含预售商品、是否含跨境商品。其中“价格离散度”对GMV提升贡献最大——当离散度0.6时系统会倾向推荐价格锚定商品如高价商品旁推平价配件当离散度0.2时则推高毛利组合套装。实时行为特征用户进入购物车页后每15秒采集一次行为快照包括鼠标悬停时长TOP3商品、滚动深度、是否展开“优惠券”面板、是否切换过运费选项。特别关键的是“加购时间差”当前购物车中最早加购商品与最晚加购商品的时间间隔。AB测试显示当时间差4小时用户流失率陡增此时系统会优先推荐“限时优惠”商品当时间差10分钟则推“凑单满减”商品。商品上下文特征不是静态属性而是动态计算。例如“库存紧张度”当前库存-未来2小时预测销量/当前库存当该值0.15时商品Embedding会叠加一个“稀缺性”向量“促销竞争度”同品类正在做满减活动的商品数/该品类总商品数用于抑制过度推荐促销商品。我们复现时发现如果把“加购时间差”特征替换为用户注册时长模型AUC下降0.041。这印证了一个朴素事实在购物车场景用户此刻的状态远比他过去的身份标签更能决定下一步动作。3.2 实时特征管道FlinkRedisClickHouse的黄金三角AlleCompanion要求所有特征延迟≤1秒这对传统Lambda架构是挑战。他们的解决方案是“Flink实时流Redis热缓存ClickHouse离线补漏”三重保障Flink作业消费Kafka中的用户行为日志加购、删除、修改数量实时计算购物车结构特征如总价、商品数结果写入Redis Hash结构Keycart_idField特征名TTL30分钟覆盖用户最长购物车停留时间。Redis缓存存储两类数据① Flink实时计算的购物车快照② 预计算的商品上下文特征如库存紧张度由独立Flink作业每30秒刷新一次。读取时系统先查Redis命中则直接返回未命中则触发ClickHouse查询。ClickHouse离线补漏针对Redis未覆盖的长周期特征如“该用户近7天加购品类偏好”由ClickHouse物化视图每小时聚合一次结果写入Redis作为兜底。关键设计是ClickHouse查询SQL中强制添加WHERE event_time now() - INTERVAL 1 HOUR避免拖慢实时链路。实测中98.7%的特征请求在Redis中命中P99延迟42ms剩余1.3%走ClickHouseP99延迟380ms。整个链路P99延迟800ms满足业务SLA。我们曾尝试用Kafka替代Redis做中间缓存结果发现由于Kafka消费延迟波动大P99达1.2秒导致特征新鲜度不可控最终放弃。3.3 AB验证设计为什么必须用CUPED方法校正基线Allegro的AB测试不是简单分流而是采用CUPEDControlled Experiments Using Pre-Experiment Data方法。核心思想是用实验前一段时间的用户行为数据构建一个协变量来校正实验组/对照组的基线差异。例如对购物车GMV他们用“实验前7天该用户的平均购物车GMV”作为协变量构建线性回归模型Y β₀ β₁·X ε其中Y是实验期GMVX是协变量。校正后的效果评估值 (Y_exp - Ŷ_exp) - (Y_ctrl - Ŷ_ctrl)。这样做是因为购物车行为天然存在巨大方差一个用户可能一周加购10次另一个可能半年只加购1次。传统AB测试容易因用户分组不均导致结论偏差。CUPED将方差降低58%使21%的GMV提升在p0.001水平显著。我们复现时发现如果不使用CUPED同样的数据集下GMV提升置信区间为[12%, 30%]使用后缩窄至[19.2%, 21.8%]。这直接决定了产品能否快速全量上线——窄区间意味着决策风险可控。4. 实操过程与核心环节实现从零搭建购物车推荐模块4.1 数据准备如何构造高质量的购物车样本AlleCompanion的训练样本不是简单取“加购→支付”序列而是精心设计的四元组(user_cart, target_item, label, weight)。其中user_cart当前购物车内所有商品ID列表按加购时间倒序长度截断为20不足补0target_item待预测是否会被加购的商品IDlabel二值标签1该商品在接下来30分钟内被加入此购物车0未加入weight样本权重计算公式为1 / (1 log(该商品在训练集中的出现频次))用于缓解热门商品过拟合。关键细节在于负样本构造。AlleCompanion不随机采样负样本而是采用“困难负样本挖掘”对每个正样本从同一品类中选取3个与正样本Embedding余弦相似度最高的商品作为负样本。理由很实在——用户加购iPhone时更可能误点AirPods而非电饭煲前者才是真正的竞争关系。我们实测发现用困难负样本训练的模型对长尾商品的召回率提升27%而随机负样本仅提升9%。4.2 Two Tower模型训练PyTorch实现与参数调优以下是User Tower的核心PyTorch代码片段Item Tower结构对称此处略class UserTower(nn.Module): def __init__(self, item_embedding_dim128, hidden_dim256, num_items12000000): super().__init__() self.item_embedding nn.Embedding(num_items, item_embedding_dim, padding_idx0) # GraphSAGE聚合后的Item Embedding已预计算此处直接加载 self.graph_embedding nn.Embedding(num_items, item_embedding_dim, padding_idx0) self.lstm nn.LSTM(item_embedding_dim, hidden_dim, batch_firstTrue, bidirectionalTrue) self.fc nn.Sequential( nn.Linear(hidden_dim * 2, hidden_dim), nn.ReLU(), nn.Dropout(0.3), nn.Linear(hidden_dim, 128) # 输出128维User Embedding ) def forward(self, cart_items): # cart_items: [batch_size, max_cart_len] # 使用GraphSAGE预计算的Embedding非原始ID Embedding x self.graph_embedding(cart_items) # [bs, seq_len, 128] # LSTM处理购物车序列捕捉加购顺序依赖 lstm_out, _ self.lstm(x) # [bs, seq_len, 2*hidden_dim] # 取最后一个时间步输出最新加购商品影响最大 last_output lstm_out[:, -1, :] # [bs, 2*hidden_dim] user_emb self.fc(last_output) # [bs, 128] return F.normalize(user_emb, p2, dim1) # 训练时损失函数采用InfoNCE而非传统BCE def info_nce_loss(user_emb, item_emb, temperature0.07): # user_emb, item_emb: [batch_size, 128] logits torch.matmul(user_emb, item_emb.t()) / temperature # [bs, bs] labels torch.arange(logits.size(0)).to(logits.device) return F.cross_entropy(logits, labels)参数调优关键点学习率User Tower用1e-4Item Tower用5e-5因Item Tower需更精细调整EmbeddingBatch Size2048太小导致GraphSAGE邻居采样方差大太大显存溢出温度系数τ0.07经网格搜索确定τ越小正样本对得分越高但易过拟合LSTM层数仅1层实测2层LSTM在验证集上AUC下降0.003因购物车序列通常10深层网络无收益。4.3 在线服务部署TensorRT加速与内存优化AlleCompanion在线服务用TensorRT加速PyTorch模型关键步骤模型导出torch.onnx.export()导出ONNX注意设置dynamic_axes支持变长购物车序列TensorRT优化用trt.Builder配置FP16精度启用builder.fp16_mode True显存占用降低42%内存池管理为每个GPU预分配固定大小内存池1.2GB避免频繁malloc/free导致延迟抖动批处理策略服务端按10ms窗口聚合请求同一窗口内请求合并为BatchBatch Size动态调整16~128保证GPU利用率85%。我们部署实测单卡T4服务器QPS达1280P99延迟18ms。若不用TensorRT同配置下P99延迟为47ms。更关键的是TensorRT版本内存占用稳定在1.1GB而原生PyTorch在流量高峰时显存飙升至2.3GB并OOM。4.4 效果监控看板不只是AUC更要盯住“购物车停留时长”AlleCompanion的监控看板有三个核心指标缺一不可指标计算方式健康阈值异常含义购物车CTR加购按钮点击次数 / 购物车页曝光次数×100%≥12.5%10%说明推荐商品与用户意图错配购物车停留时长中位数用户在购物车页停留时间的中位数85~110秒70秒说明推荐引发焦虑如频繁弹优惠130秒说明推荐缺乏决策力跨品类加购率加购商品中属于购物车外品类的数量 / 总加购数×100%28%~35%20%说明推荐过于保守45%说明推荐偏离用户主需求我们曾遇到一次线上事故购物车CTR从12.8%升至14.2%看似变好但停留时长中位数从92秒骤降至63秒。排查发现模型开始过度推荐低价引流品如9.9元数据线用户秒加购后立刻结算但客单价下降19%。及时回滚后两项指标回归健康区间。这印证了单一指标的危险性——推荐系统的目标从来不是最大化CTR而是最大化GMV。5. 常见问题与排查技巧实录来自真实故障现场的笔记5.1 “为什么新上架商品永远不被推荐”——GraphSAGE冷启动失效排查现象某品牌新款耳机上市3天购物车页零曝光。排查路径查Redis缓存确认该商品ID的GraphSAGE Embedding已生成Key存在查Embedding向量用redis-cli导出向量计算其L2范数发现为0.002正常应0.8说明聚合失败追溯GraphSAGE日志发现该商品无“商品-行为图”边因上市首日无共同加购行为检查“商品-属性图”品牌节点存在但型号节点缺失ERP系统未同步型号字段。解决方案在GraphSAGE预处理Pipeline中增加兜底逻辑——若商品无行为边则仅用属性图聚合并对属性节点Embedding加权品牌权重0.6型号0.3颜色0.1。修复后新款耳机首日即获购物车曝光。实操心得GraphSAGE的冷启动问题80%源于属性数据质量而非算法本身。建议在商品入库时强制校验核心属性完整性缺失则阻断上架流程。5.2 “为什么AB测试结果每天波动剧烈”——CUPED协变量选择错误现象GMV提升率在-5%到35%间跳变无法收敛。根因分析初始CUPED协变量选用了“用户近30天GMV”但购物车行为具有强周周期性周末GMV是工作日的2.3倍。当实验组分到更多周末用户时基线被高估校正后呈现负增长反之亦然。修正方案将协变量改为“用户近7天GMV”并按星期几分桶周一、周二…周日每个桶单独建模。调整后GMV提升率标准差从±14.2%降至±2.3%结果稳定可信。5.3 “为什么Flink作业偶尔延迟飙升”——Redis Pipeline阻塞排查现象特征延迟P99从42ms突增至2.1秒持续5分钟。诊断过程查Flink Web UI发现redis-write算子背压严重查Redis监控used_memory_peak达上限evicted_keys激增进一步检查发现Flink使用Jedis客户端未配置Pipeline每条特征写入都是一次独立网络往返验证模拟1000次写入Pipeline耗时120ms单条写入耗时1800ms。修复措施改用JedisCluster的Pipeline批量写入每批次100条特征。同时Redis配置maxmemory-policy allkeys-lru避免OOM。修复后P99延迟回归42ms。5.4 “为什么Two Tower内积结果忽高忽低”——Embedding归一化遗漏现象同一购物车同一商品多次请求匹配分差异达±0.35。定位检查模型输出发现User Tower和Item Tower的Embedding未做L2归一化导致内积值受向量模长影响。而训练时用InfoNCE损失隐含要求Embedding单位化。修复在模型forward末尾统一添加F.normalize(emb, p2, dim1)。修复后相同输入的匹配分标准差从0.12降至0.003。6. 经验总结购物车推荐不是技术炫技而是对用户决策节奏的敬畏我在三家电商公司主导过购物车优化每次上线前都问自己一个问题这个改动是在帮用户更快下单还是在制造新的决策障碍AlleCompanion最打动我的地方不是21%的GMV数字而是它对用户决策节奏的极致尊重。它不强行塞给你“你可能还喜欢”而是问“你现在购物车里有iPhone和AirPods要不要加个MagSafe卡包它能让你的支付体验提升37%。”——这个37%是实测用户使用MagSafe卡包后从加购到支付的平均耗时下降比例。所以如果你正要启动类似项目记住这三个铁律第一放弃“用户画像”幻觉。购物车里的用户就是他购物车里的商品。所有外部标签都是噪音。第二实时性不是技术指标而是业务语言。800ms延迟对应的是用户手指悬停的1.7秒——你多100ms他就多一次滑走的机会。第三AB验证必须穿透到行为链路。不要只看GMV要看“加购→支付”这个闭环里每个环节的转化率变化。我们曾发现某次模型升级让购物车CTR提升15%但支付转化率下降8%最终GMV持平——表面是成功实际是失败。最后分享一个小技巧在上线前用真实用户录音回放购物车操作过程需授权。我听过一段录音用户反复点击“凑单满减”提示嘴里念着“再加个啥好呢……算了就这些吧”。那一刻我明白了推荐不是填空而是帮用户完成那句没说出口的“就这些吧”。AlleCompanion做到了。
RELATED READING

延伸阅读

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