一、 从"搜不到"到"推不准",再到"比你更懂你"
讲一个我们踩过的坑。
我们的电商平台上线第一年,搜索功能尚不完善,用户找不到商品,于是我们决定上"推荐"------在首页、购物车底部、订单完成页展示"猜你喜欢"。
第一版推荐系统极其简单:最近30天卖得最好的商品,按销量排序展示。
结果呢?
女性用户看到了"男士剃须刀"。
已经买过iPhone的用户又看到"iPhone推荐"。
母婴用户看到了"电竞键盘"。
点击率?不到2%。用户吐槽:"这推荐系统是随便乱推的吧?"
更严重的是,我们尝试用"热门商品"逻辑做首页推荐,导致头部商品越推越爆,长尾商品永远没有曝光机会------商家开始抱怨"我们小商家没活路了"。
那一刻我们意识到:推荐不是"把卖得好的推给所有人",而是"把最可能买的商品推给最需要的人"。 这背后是算法、工程、产品的综合博弈。
本文将完整复盘我们从热门商品推荐 → 协同过滤 → 深度学习双塔模型的演进之路,重点分享算法选型、架构设计以及在线实时排序的实战经验。
二、 什么是"推荐系统"?为什么要做?
2.1 推荐系统的核心公式
text
推荐 = 用户画像 × 商品画像 × 匹配算法 × 场景上下文
用户画像:你是谁?(年龄、性别、偏好、历史行为)
商品画像:这是什么?(类目、品牌、价格、属性、描述)
匹配算法:怎么判断"你"和"它"是否匹配?(协同过滤、深度学习、多路召回)
场景上下文:现在是什么场景?(首页、购物车、订单完成页、节日大促)
2.2 推荐的价值
维度 无推荐 有推荐(优化后) 提升幅度
首页转化率 3% 12% 提升 4倍
客单价 120元 168元 提升 40%
用户停留时长 1.8分钟 5.2分钟 提升 189%
长尾商品曝光率 8% 37% 提升 362%
本质:推荐系统是电商平台从"人找货"到"货找人"的引擎。
三、 技术选型:推荐系统的主流架构
我们对比了行业主流方案:
方案 适用场景 优点 缺点 结论
热门推荐(Most Popular) 冷启动阶段 简单易实现 个性化差、马太效应严重 ❌ 被淘汰
协同过滤(CF) 用户行为数据丰富 召回效果好、实现中等 冷启动问题、稀疏性问题 ✅ V2.0采用
深度学习(双塔/DIN) 海量数据和算力 精度高、可处理多模态特征 工程复杂、算力成本高 ✅ V3.0采用
图神经网络(GNN) 社交关系丰富 挖掘高阶关系 技术前沿、工程不成熟 🔮 未来探索
最终演进路径:
V1.0(上线1-3月):热门推荐 + 人工规则(快速上线)
V2.0(上线3-12月):协同过滤(ItemCF + UserCF,提升个性化)
V3.0(上线12月+):双塔模型(DSSM)+ 实时排序(XGBoost/LightGBM)
四、 V2.0:协同过滤(Collaborative Filtering)
协同过滤是推荐系统的"经典算法",核心思想:"物以类聚,人以群分"。
4.1 ItemCF(基于物品的协同过滤)
核心逻辑:用户A买了商品X,那么和X"相似"的商品Y也应该推荐给A。
实现步骤:
text
Step 1: 构建用户-物品倒排表
用户1: 商品A, 商品B, 商品C
用户2: 商品A, 商品C, 商品D
用户3: 商品B, 商品C, 商品E
Step 2: 计算物品相似度矩阵
sim(A, C) = 2/3 ≈ 0.67(A和C同时被2个用户购买过,总用户3个)
sim(A, B) = 1/3 ≈ 0.33
sim(B, C) = 2/3 ≈ 0.67
Step 3: 为用户生成推荐
用户3购买过B, C, E
→ 推荐与B最相似的商品:A(sim=0.33)
→ 推荐与C最相似的商品:A(sim=0.67), D(sim=0.33)
→ 最终排序:A(0.67+0.33=1.0), D(0.33)
核心代码(Spark实现):
scala
// 1. 读取用户行为数据(点击/加购/下单/收藏)
val userItemDF = spark.read.parquet("user_behavior.parquet")
.select("user_id", "item_id", "behavior_type", "behavior_time")
// 2. 行为加权:下单=5分,加购=3分,收藏=2分,点击=1分
val weightedDF = userItemDF.withColumn("weight",
when("behaviortype"==="buy",5).when("behavior_type" === "buy", 5) .when("behaviortype"==="buy",5).when("behavior_type" === "cart", 3)
.when($"behavior_type" === "favor", 2)
.otherwise(1))
// 3. 计算用户-物品评分矩阵
val userItemScore = weightedDF.groupBy("user_id", "item_id")
.agg(sum("weight").as("score"))
// 4. 计算物品相似度(余弦相似度)
val itemSimilarity = userItemScore
.rdd.map(row => (row.getLong(1), (row.getLong(0), row.getDouble(2))))
.groupByKey()
.flatMap { case (itemId, userScores) =>
// 构建向量
val vector = userScores.map { case (userId, score) => (userId, score) }.toMap
// 计算相似度(需要两个物品共同评分用户数>阈值)
// 实际实现使用ALS或Spark MLlib的相似度计算
}
4.2 协同过滤的"三大天坑"
问题 现象 解决方案
冷启动 新用户无历史行为,新商品无用户评分 新用户→热门推荐;新商品→内容特征(类目/品牌/价格)匹配
稀疏性 用户-物品矩阵99%为空,相似度计算不准 矩阵分解(ALS/SVD),将高维矩阵降维到低维隐向量
流行度偏差 热门商品相似度都很高,冷门商品被忽略 引入流行度惩罚因子,对热门商品的相似度打折扣
4.3 ALS(交替最小二乘法)------解决稀疏性问题
python
from pyspark.ml.recommendation import ALS
from pyspark.ml.evaluation import RegressionEvaluator
ALS模型训练
als = ALS(
maxIter=10,
rank=50, # 隐向量维度
regParam=0.01, # 正则化参数
userCol="user_id",
itemCol="item_id",
ratingCol="score",
coldStartStrategy="drop"
)
model = als.fit(trainData)
为每个用户推荐Top10商品
userRecs = model.recommendForAllUsers(10)
效果数据:
指标 热门推荐 协同过滤(CF) 提升幅度
点击率(CTR) 2.1% 6.8% 提升 224%
转化率(CVR) 0.8% 3.2% 提升 300%
用户人均曝光商品数 12个 34个 提升 183%
长尾商品曝光 8% 29% 提升 262%
五、 V3.0:深度学习双塔模型(DSSM)
协同过滤虽好,但有两个根本局限:
只用行为数据,无法利用商品属性、文本描述、图片等丰富信息。
无法做实时个性化,模型训练全量更新耗时。
于是我们引入业界主流的双塔模型(Two-Tower) 架构。
5.1 双塔模型架构
text
相似度计算
(内积 / 余弦相似度)
/
/
用户塔(User Tower)\] \[物品塔(Item Tower)
| |
MLP多层神经网络\] \[MLP多层神经网络
| |
用户特征\] \[商品特征
├─ 基础画像:性别/年龄/地域 ├─ 基础属性:类目/品牌/价格
├─ 行为序列:最近浏览/加购 ├─ 文本特征:标题/描述(BERT向量)
├─ 统计特征:近7天/30天偏好 ├─ 图像特征:商品主图(CNN向量)
└─ 场景特征:时间/设备/渠道 └─ 统计特征:点击率/转化率
5.2 用户塔(User Tower)实现(TensorFlow)
python
import tensorflow as tf
from tensorflow.keras import layers, Model
def build_user_tower(user_feature_columns):
"""构建用户塔"""
1. 输入层
inputs = {}
for col in user_feature_columns:
inputscol.name = layers.Input(shape=(1,), name=col.name)
# 2. 类别特征嵌入
embeddings = []
for col in user_feature_columns:
if col.type == 'categorical':
embedding = layers.Embedding(col.vocab_size, col.embed_dim)(inputs[col.name])
embedding = layers.Flatten()(embedding)
embeddings.append(embedding)
else: # 数值特征
numeric = layers.Dense(1)(inputs[col.name])
embeddings.append(numeric)
# 3. 拼接所有特征
concat = layers.Concatenate()(embeddings)
# 4. MLP层(多层神经网络)
x = layers.Dense(256, activation='relu')(concat)
x = layers.BatchNormalization()(x)
x = layers.Dropout(0.2)(x)
x = layers.Dense(128, activation='relu')(x)
x = layers.BatchNormalization()(x)
x = layers.Dropout(0.2)(x)
x = layers.Dense(64, activation='relu')(x) # 最终用户向量维度=64
return Model(inputs=inputs, outputs=x)
构建物品塔(结构类似)
def build_item_tower(item_feature_columns):
... 类似结构,输出64维向量
pass
5.3 模型训练:双塔联合训练
python
1. 构建双塔模型
user_tower = build_user_tower(user_features)
item_tower = build_item_tower(item_features)
2. 用户塔输入
user_inputs = user_tower.input
user_embedding = user_tower.output
3. 物品塔输入
item_inputs = item_tower.input
item_embedding = item_tower.output
4. 计算相似度(内积)
similarity = layers.Dot(axes=1, normalize=True)(user_embedding, item_embedding)
5. 输出层(二分类:点击/未点击)
output = layers.Dense(1, activation='sigmoid')(similarity)
6. 完整模型
model = Model(inputs=user_inputs + item_inputs, outputs=output)
model.compile(optimizer='adam', loss='binary_crossentropy', metrics='AUC')
7. 训练
model.fit(
x=user_data, item_data,
y=labels,
epochs=10,
batch_size=512,
validation_split=0.2,
callbacks=[
tf.keras.callbacks.EarlyStopping(patience=3),
tf.keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=2)
]
)
5.4 在线推理架构
text
用户请求推荐
│
▼
特征工程服务 (实时拼接用户特征 + 场景特征)
│
▼
用户向量计算 (从Redis缓存读取,或调用用户塔实时计算)
│
▼
向量检索 (Faiss/Elasticsearch向量插件,从亿级商品中召回Top1000)
│
▼
精排阶段 (LightGBM/XGBoost模型,对1000个候选商品打分排序)
│
▼
业务重排 (多样性、新鲜度、商业化等业务规则干预)
│
▼
返回Top20给用户
关键组件选型:
组件 选型 说明
特征存储 Redis + Feature Store 用户实时特征缓存,毫秒级读取
向量检索 Faiss(Facebook开源) 亿级向量检索,延迟<10ms
精排模型 LightGBM 树模型,可解释性好,训练快
模型部署 TensorFlow Serving 支持GPU推理,高吞吐
AB实验 自研AB平台 支持多组实验并行
六、 多路召回:不只是双塔
双塔模型只是"召回"的一种方式。为了提升召回覆盖率和多样性,我们采用多路召回策略:
text
用户请求
│
├── 路线1:双塔模型召回(个性化,500个)
│
├── 路线2:协同过滤召回(相似兴趣,200个)
│
├── 路线3:热门商品召回(兜底,100个)
│
├── 路线4:实时行为召回(浏览加购,200个)
│ └─ 用户最近浏览的同类商品
│
├── 路线5:场景化召回(节日/天气,100个)
│ └─ "夏日防晒"、"冬季保暖"等场景
│
└── 路线6:补充召回(新品/长尾,100个)
└─ 保证新商品有机会曝光
│
▼
合并去重\] → \[精排\] → \[重排\] → \[返回Top20
各路线优势互补:
召回路线 覆盖率 精准度 多样性 实时性
双塔模型 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
协同过滤 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐
热门商品 ⭐ ⭐⭐ ⭐ ⭐⭐⭐⭐⭐
实时行为 ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐
场景化 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
七、 实时排序:从"离线批量"到"毫秒级个性化"
召回之后,需要对候选商品精排------决定最终展示顺序。
7.1 离线排序(我们踩过的坑)
最初,我们每天凌晨用离线数据训练一次排序模型(XGBoost),白天用这个模型进行预测。
问题:
用户早上浏览了"球鞋",下午模型还是推荐"衬衫"------无法实时捕捉用户兴趣变化。
大促期间用户行为剧烈变化,离线模型严重滞后。
7.2 实时排序方案
核心思路:实时特征 + 在线模型 + 动态权重
java
@Service
public class RealtimeRankingService {
public List<Item> rank(List<Item> candidates, User user) {
// 1. 提取实时特征(毫秒级)
Map<String, Double> features = extractRealtimeFeatures(user, candidates);
// 2. 基础分:用在线模型(LightGBM)打分
Map<Long, Double> baseScores = lgbModel.predict(candidates, features);
// 3. 动态权重调整(根据实时行为)
// 用户刚刚点击了"跑步鞋",提高"跑步鞋"类目的权重
String recentCategory = user.getRecentClickCategory();
if (recentCategory != null) {
for (Item item : candidates) {
if (item.getCategory().equals(recentCategory)) {
baseScores.put(item.getId(), baseScores.get(item.getId()) * 1.5);
}
}
}
// 4. 多样性惩罚(避免推荐太相似的商品)
Set<String> seenCategories = new HashSet<>();
for (Item item : candidates) {
String category = item.getCategory();
if (seenCategories.contains(category)) {
baseScores.put(item.getId(), baseScores.get(item.getId()) * 0.8); // 降权
}
seenCategories.add(category);
}
// 5. 排序输出
return candidates.stream()
.sorted((a, b) -> Double.compare(baseScores.get(b.getId()), baseScores.get(a.getId())))
.limit(20)
.collect(Collectors.toList());
}
}
7.3 实时特征工程(Redis存储)
java
// 用户实时行为特征(存储在Redis中,TTL=1小时)
public class RealtimeFeatureService {
public UserRealtimeFeatures getFeatures(Long userId) {
String key = "user:realtime:" + userId;
Map<String, String> data = redis.hgetAll(key);
return UserRealtimeFeatures.builder()
.recentViewItems(parseIds(data.get("views"))) // 最近浏览的10个商品
.recentCartItems(parseIds(data.get("carts"))) // 最近加购的商品
.recentCategory(data.get("last_category")) // 最后浏览的类目
.recentBrand(data.get("last_brand")) // 最后浏览的品牌
.avgPrice(Double.parseDouble(data.get("avg_price"))) // 平均浏览价格带
.sessionTime(Long.parseLong(data.get("session_time"))) // 当前会话时长
.build();
}
// 每次用户行为时更新Redis
public void updateRealtime(Long userId, Behavior behavior) {
String key = "user:realtime:" + userId;
redis.expire(key, 3600); // 设置1小时过期
// 更新最近浏览列表(只保留最新的10个)
redis.lpush(key + ":views", behavior.getItemId());
redis.ltrim(key + ":views", 0, 9);
// 更新最近类目
redis.set(key + ":last_category", behavior.getCategory());
// 更新平均价格(滑动窗口)
redis.incrByFloat(key + ":total_price", behavior.getPrice());
redis.incr(key + ":price_count");
double avgPrice = redis.get(key + ":total_price") / redis.get(key + ":price_count");
redis.set(key + ":avg_price", String.valueOf(avgPrice));
}
}
八、 业务规则与重排:推荐系统的"最后一道防线"
算法推荐的结果,必须经过业务规则的修正,否则会出现各种奇葩问题。
8.1 常见的业务规则
规则 说明 实现方式
去重 同一商品不能连续出现 推荐结果去重
多样性 同一类目最多占30% 类目比例控制
新鲜度 新商品必须有曝光机会 新品加权(boost=1.5)
时效性 过季商品不推荐 季节性过滤
商业化 付费商品优先展示 广告位固定插入
库存过滤 缺货商品不推荐 实时库存校验
价格带 不推荐远超用户历史价格带的商品 价格过滤(±50%)
8.2 重排实现
java
public List rerank(List items, User user) {
List result = new ArrayList<>();
Set seenIds = new HashSet<>();
Map<String, Integer> categoryCount = new HashMap<>();
for (Item item : items) {
// 1. 去重
if (seenIds.contains(item.getId())) {
continue;
}
// 2. 多样性控制(同一类目最多3个)
String category = item.getCategory();
if (categoryCount.getOrDefault(category, 0) >= 3) {
continue;
}
// 3. 价格过滤(不超过用户历史平均价格的2倍)
if (item.getPrice() > user.getAvgPrice() * 2) {
continue; // 太贵了,不推荐
}
// 4. 库存过滤
if (!inStock(item.getId())) {
continue;
}
// 通过所有规则
result.add(item);
seenIds.add(item.getId());
categoryCount.put(category, categoryCount.getOrDefault(category, 0) + 1);
if (result.size() >= 20) {
break;
}
}
// 如果结果不足20个,从备选池中补充(放宽规则)
if (result.size() < 20) {
result.addAll(fallbackItems(user, 20 - result.size()));
}
return result;
}
九、 踩坑实录:推荐系统生产环境的"四大天坑"
坑1:特征穿越(Feature Leakage)
问题:训练模型时使用了未来数据(如"商品在7天后的销量"作为特征),模型离线AUC很高(0.85),上线后CTR反而下降。
原因:训练时特征中包含了未来信息,模型学会了"作弊",上线时无法获取未来信息,预测不准。
解决方案:
所有特征必须来自预测时刻之前的数据。
特征时间戳必须严格< label时间戳。
建立特征血缘追踪系统,自动检测特征穿越。
坑2:推荐结果"同质化"
问题:用户看了"耐克跑鞋",推荐结果全是"耐克跑鞋",用户失去兴趣。
原因:多样性约束不足。
解决方案:
在重排阶段引入MMR(最大边际相关性) 算法,平衡相关性和多样性。
硬性约束:同一类目不超过30%,同一品牌不超过20%。
坑3:模型线上与线下效果不一致
问题:离线AUC=0.82,上线CTR只比随机高一点点。
原因:
离线数据采样偏差(只用了点击数据,忽视了曝光未点击的负样本)。
线上特征获取不到或延迟。
解决方案:
训练数据必须包含曝光未点击的负样本。
特征实时性和完整性监控,缺失率超过5%就告警。
坑4:新用户冷启动
问题:新用户没有任何历史行为,推荐系统无法个性化。
解决方案(多级策略):
text
新用户注册
│
├─ Step1: 兴趣选择(注册时让用户选择3-5个兴趣标签)
│ └─ 按标签匹配推荐商品
│
├─ Step2: 热门商品(前3次曝光)
│ └─ 展示平台热门商品
│
├─ Step3: 实时学习(第4次开始)
│ └─ 根据用户点击行为实时调整推荐
│
└─ Step4: 探索与利用(EE策略)
└─ 20%流量探索新品类,80%流量利用已有知识
十、 最终成果与AB实验数据
经过三个版本的迭代,最终成果如下:
指标 V1.0 热门推荐 V2.0 协同过滤 V3.0 双塔+实时排序 总提升
首页点击率(CTR) 2.1% 6.8% 11.2% 提升 433%
转化率(CVR) 0.8% 3.2% 5.6% 提升 600%
人均浏览商品数 8个 16个 28个 提升 250%
用户停留时长 1.8分钟 3.2分钟 6.1分钟 提升 239%
长尾商品曝光率 8% 29% 43% 提升 438%
推荐带来的GMV占比 5% 18% 35% 提升 600%
用户满意度(NPS) 32 48 72 提升 125%
AB实验数据(双塔模型 VS 协同过滤):
实验组 CTR CVR 人均GMV 结论
对照组(协同过滤) 6.8% 3.2% 45.2元 基线
实验组(双塔模型) 9.4% 4.7% 63.8元 ✅ 胜出,提升显著
实验组(双塔+实时排序) 11.2% 5.6% 78.5元 ✅ 胜出,进一步提升
十一、 总结与未来规划
11.1 核心经验总结
经验 具体内容
分阶段推进 不要一上来就搞深度学习。先热门→协同过滤→双塔,循序渐进,每个阶段都有业务价值
召回是基础 召回决定了推荐的天花板。多路召回比单路召回效果好得多
实时性是关键 用户兴趣变化很快,离线模型无法捕捉实时偏好。实时特征+在线模型是方向
业务规则不可少 纯算法的推荐结果经常"离谱",需要业务规则做最后把关
AB实验是唯一标准 离线指标只是参考,CTR/CVR/GMV才是硬道理。持续做AB实验
11.2 未来规划
多模态融合:将商品图片特征(CNN)和文本特征(BERT)融入双塔模型,提升新品推荐质量。
强化学习(RL):引入Contextual Bandit算法,在"探索"和"利用"之间找到最优平衡。
图神经网络(GNN):构建用户-商品-品牌-类目的异构图网络,挖掘高阶关系。
可解释推荐:为用户展示"为什么推荐这个商品",提升用户信任度。
多目标优化:不只是优化CTR,同时优化CVR、停留时长、多样性等多个目标。
文末互动:
你们的推荐系统用的是什么算法?协同过滤、双塔还是更先进的模型?推荐系统的"翻车"经历有哪些(比如给男生推荐了姨妈巾)?欢迎在评论区分享,一起打造更好的推荐引擎!