08 - 推荐系统设计
| > 面试频率: 需求类型 | 指标 |
|---|---|
| 用户规模 | 1 亿日活 |
| 物品规模 | 1000 万商品/视频 |
| QPS | 10 万(推荐请求) |
| 响应时间 | P99 < 100ms |
| 准确率 | CTR > 5%(点击率) |
| 实时性 | 行为 → 推荐 < 1 分钟 |
1.3 面试官可能的追问
Q1: 推荐系统和搜索有什么区别?
A1:
- 搜索:用户主动输入关键词,有明确意图
- 推荐:系统主动推送,挖掘潜在需求
- 搜索:召回 → 排序
- 推荐:召回 → 粗排 → 精排 → 重排
Q2: 如何解决冷启动问题?
A2:
- 新用户:热门推荐、基于人口属性(性别、年龄)
- 新物品:基于内容相似度、少量流量试探
- AB 测试:逐步放量
2. 容量估算
2.1 场景假设
假设为一个中型电商/短视频平台设计推荐系统:
- 日活用户(DAU):1 亿
- 每用户日均推荐请求:50 次
- 物品总数:1000 万
- 用户行为:浏览、点击、收藏、购买
2.2 QPS 估算
日推荐请求 = 1 亿 × 50 = 50 亿次
平均 QPS = 50 亿 / 86400 ≈ 58,000 QPS
峰值 QPS = 58,000 × 2 = 116,000 QPS
2.3 存储估算
用户行为数据
单条行为 = 用户ID(8字节) + 物品ID(8字节) + 行为类型(1字节) + 时间戳(8字节) = 25 字节
日行为数 = 1 亿 × 50 = 50 亿条
日存储 = 50 亿 × 25 字节 ≈ 125 GB
月存储 = 125 GB × 30 ≈ 3.75 TB
年存储 ≈ 45 TB
用户画像
用户画像 = 1 亿用户 × 1 KB = 100 GB
物品特征
物品特征 = 1000 万物品 × 2 KB = 20 GB
推荐模型
协同过滤模型:用户-物品矩阵(稀疏)
深度学习模型:Embedding 向量
总存储 ≈ 100 GB
总存储需求:约 50 TB(含历史数据)
2.4 计算资源估算
离线计算
协同过滤:每日全量计算
计算时间:2-4 小时
Spark 集群:100 台(16核 64GB)
在线计算
实时推荐:召回 + 排序
单次推荐耗时 < 100ms
所需服务器:116,000 QPS / 1000 QPS ≈ 116 台
3. API 设计
3.1 核心 API
3.1.1 获取推荐列表
GET /api/v1/recommendations
Request:
{
"user_id": 12345,
"scene": "homepage", // homepage | detail_page | cart
"count": 20, // 推荐数量
"context": {
"device": "ios",
"location": "Beijing",
"time": "2023-11-13T14:30:00Z"
},
"filters": {
"exclude_item_ids": [101, 102], // 已看过的
"category": "electronics"
}
}
Response:
{
"code": 0,
"message": "success",
"data": {
"items": [
{
"item_id": 1001,
"title": "iPhone 15 Pro",
"score": 0.95, // 推荐分数
"reason": "猜你喜欢", // 推荐理由
"tags": ["数码", "热门"],
"recall_strategy": "user_cf" // 召回策略
},
{
"item_id": 1002,
"title": "AirPods Pro",
"score": 0.88,
"reason": "看了还看",
"recall_strategy": "item_cf"
}
],
"trace_id": "rec_20231113_12345" // 用于追踪
}
}
3.1.2 上报用户行为
POST /api/v1/behaviors
Request:
{
"user_id": 12345,
"behaviors": [
{
"item_id": 1001,
"behavior_type": "click", // view | click | collect | purchase
"timestamp": 1699876200,
"duration": 30, // 停留时长(秒)
"context": {
"page": "homepage",
"position": 1,
"trace_id": "rec_20231113_12345"
}
}
]
}
Response:
{
"code": 0,
"message": "success"
}
3.1.3 相似物品推荐
GET /api/v1/recommendations/similar
Request:
{
"item_id": 1001,
"count": 10
}
Response:
{
"code": 0,
"message": "success",
"data": {
"items": [
{
"item_id": 1002,
"similarity": 0.92,
"title": "AirPods Pro"
}
]
}
}
4. 数据模型设计
4.1 用户行为表
CREATE TABLE user_behaviors (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
item_id BIGINT NOT NULL,
behavior_type VARCHAR(20) NOT NULL COMMENT 'view|click|collect|purchase',
duration INT COMMENT '停留时长(秒)',
-- 上下文
scene VARCHAR(50) COMMENT 'homepage|detail_page',
position INT COMMENT '推荐位置',
trace_id VARCHAR(64) COMMENT '追踪ID',
-- 设备信息
device VARCHAR(20),
os VARCHAR(20),
timestamp BIGINT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_time (user_id, timestamp),
INDEX idx_item_time (item_id, timestamp),
INDEX idx_trace (trace_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户行为表';
4.2 用户画像表
CREATE TABLE user_profiles (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL UNIQUE,
-- 基本信息
gender VARCHAR(10),
age_range VARCHAR(20),
city VARCHAR(50),
-- 行为统计
total_views INT DEFAULT 0,
total_clicks INT DEFAULT 0,
total_purchases INT DEFAULT 0,
-- 偏好标签(JSON)
preference_tags JSON COMMENT '[{"tag": "数码", "weight": 0.8}]',
-- 偏好品牌
favorite_brands JSON,
-- 价格偏好
avg_price_range VARCHAR(20),
-- 活跃度
last_active_time DATETIME,
active_days INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户画像表';
4.3 物品特征表
CREATE TABLE item_features (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
item_id BIGINT NOT NULL UNIQUE,
-- 基本信息
title VARCHAR(256) NOT NULL,
category_id INT,
brand_id INT,
price DECIMAL(10, 2),
-- 标签
tags JSON COMMENT '["数码", "手机", "5G"]',
-- 统计信息
total_views INT DEFAULT 0,
total_clicks INT DEFAULT 0,
total_purchases INT DEFAULT 0,
-- CTR(点击率)
ctr DECIMAL(5, 4) DEFAULT 0,
-- 转化率
cvr DECIMAL(5, 4) DEFAULT 0,
-- 质量分
quality_score DECIMAL(5, 2) DEFAULT 0,
-- Embedding 向量(存储路径或二进制)
embedding_vector BLOB,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_item_id (item_id),
INDEX idx_category (category_id),
INDEX idx_ctr (ctr)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品特征表';
4.4 推荐结果表(缓存)
CREATE TABLE recommendation_cache (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
scene VARCHAR(50) NOT NULL,
-- 推荐列表(JSON)
items JSON COMMENT '[{"item_id": 1001, "score": 0.95}]',
-- 生成时间
generated_at DATETIME NOT NULL,
-- 过期时间
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_scene (user_id, scene),
INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推荐结果缓存表';
4.5 相似度矩阵表(物品 CF)
CREATE TABLE item_similarity (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
item_id_a BIGINT NOT NULL,
item_id_b BIGINT NOT NULL,
similarity DECIMAL(5, 4) NOT NULL,
-- 计算方法
method VARCHAR(20) COMMENT 'cosine|jaccard|pearson',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_item_pair (item_id_a, item_id_b),
INDEX idx_item_a (item_id_a, similarity)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品相似度表';
5. 架构设计
5.1 整体架构
┌─────────────────────────────────────────────────────────────┐
│ 用户端 │
│ (App / Web / 小程序) │
└─────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ API Gateway │
│ (限流、鉴权、路由) │
└─────────────┬───────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ 推荐服务层 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 召回层 │→ │ 粗排层 │→ │ 精排层 │→ │ 重排层 │ │
│ │ (Recall) │ │(Pre-rank)│ │ (Rank) │ │(Re-rank) │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ↑ ↑ ↑ ↑ │
└───────┼─────────────┼─────────────┼─────────────┼──────────┘
│ │ │ │
┌─────────────────────────────────────────────────────────────┐
│ 数据层 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 用户画像 │ │ 物品特征 │ │ 实时特征 │ │
│ │ (Redis) │ │ (Redis) │ │ (Redis) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 离线计算层 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 协同过滤 │ │ 物品相似度 │ │ 深度学习 │ │
│ │ (Spark) │ │ (Spark) │ │ (TF) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 实时计算层 │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 行为流处理 │ │ 特征更新 │ │
│ │ (Flink) │ │ (Flink) │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
5.2 V1: 基于协同过滤的推荐系统(MVP)
适用场景:初创产品、用户量 < 100 万
package main
import (
"fmt"
"math"
"sort"
)
// ==================== 数据结构 ====================
// UserBehavior 用户行为
type UserBehavior struct {
UserID int64
ItemID int64
BehaviorType string // "view" | "click" | "purchase"
Score float64 // 隐式评分
Timestamp int64
}
// RecommendationItem 推荐物品
type RecommendationItem struct {
ItemID int64
Score float64
Reason string
}
// ==================== 协同过滤:User-based CF ====================
// UserBasedCF 基于用户的协同过滤
type UserBasedCF struct {
// 用户-物品评分矩阵
userItemMatrix map[int64]map[int64]float64
// 用户相似度矩阵
userSimilarity map[int64]map[int64]float64
}
// NewUserBasedCF 创建 User-based CF
func NewUserBasedCF() *UserBasedCF {
return &UserBasedCF{
userItemMatrix: make(map[int64]map[int64]float64),
userSimilarity: make(map[int64]map[int64]float64),
}
}
// AddBehavior 添加用户行为
func (cf *UserBasedCF) AddBehavior(behavior *UserBehavior) {
if cf.userItemMatrix[behavior.UserID] == nil {
cf.userItemMatrix[behavior.UserID] = make(map[int64]float64)
}
// 隐式评分:浏览=1, 点击=3, 购买=5
score := map[string]float64{
"view": 1.0,
"click": 3.0,
"purchase": 5.0,
}[behavior.BehaviorType]
// 累加评分
cf.userItemMatrix[behavior.UserID][behavior.ItemID] += score
}
// CalculateUserSimilarity 计算用户相似度(余弦相似度)
func (cf *UserBasedCF) CalculateUserSimilarity() {
users := make([]int64, 0)
for userID := range cf.userItemMatrix {
users = append(users, userID)
}
// 计算所有用户对之间的相似度
for i := 0; i < len(users); i++ {
userA := users[i]
if cf.userSimilarity[userA] == nil {
cf.userSimilarity[userA] = make(map[int64]float64)
}
for j := i + 1; j < len(users); j++ {
userB := users[j]
similarity := cf.cosineSimilarity(userA, userB)
if similarity > 0 {
cf.userSimilarity[userA][userB] = similarity
if cf.userSimilarity[userB] == nil {
cf.userSimilarity[userB] = make(map[int64]float64)
}
cf.userSimilarity[userB][userA] = similarity
}
}
}
}
// cosineSimilarity 余弦相似度
func (cf *UserBasedCF) cosineSimilarity(userA, userB int64) float64 {
itemsA := cf.userItemMatrix[userA]
itemsB := cf.userItemMatrix[userB]
// 找出共同物品
commonItems := make(map[int64]bool)
for itemID := range itemsA {
if _, exists := itemsB[itemID]; exists {
commonItems[itemID] = true
}
}
if len(commonItems) == 0 {
return 0
}
// 计算余弦相似度
var dotProduct, normA, normB float64
for itemID := range commonItems {
dotProduct += itemsA[itemID] * itemsB[itemID]
}
for _, score := range itemsA {
normA += score * score
}
for _, score := range itemsB {
normB += score * score
}
if normA == 0 || normB == 0 {
return 0
}
return dotProduct / (math.Sqrt(normA) * math.Sqrt(normB))
}
// Recommend 生成推荐列表
func (cf *UserBasedCF) Recommend(userID int64, count int) []*RecommendationItem {
// 1. 找到相似用户(Top K)
similarUsers := cf.getTopKSimilarUsers(userID, 50)
// 2. 聚合相似用户喜欢的物品
candidateItems := make(map[int64]float64)
userItems := cf.userItemMatrix[userID]
for _, su := range similarUsers {
similarUserID := su.UserID
similarity := su.Similarity
for itemID, score := range cf.userItemMatrix[similarUserID] {
// 过滤用户已交互物品
if _, exists := userItems[itemID]; exists {
continue
}
// 加权累加
candidateItems[itemID] += score * similarity
}
}
// 3. 排序并返回 Top N
items := make([]*RecommendationItem, 0)
for itemID, score := range candidateItems {
items = append(items, &RecommendationItem{
ItemID: itemID,
Score: score,
Reason: "猜你喜欢",
})
}
sort.Slice(items, func(i, j int) bool {
return items[i].Score > items[j].Score
})
if len(items) > count {
items = items[:count]
}
return items
}
// getTopKSimilarUsers 获取最相似的 K 个用户
func (cf *UserBasedCF) getTopKSimilarUsers(userID int64, k int) []*SimilarUser {
similarities := cf.userSimilarity[userID]
users := make([]*SimilarUser, 0)
for otherUserID, similarity := range similarities {
users = append(users, &SimilarUser{
UserID: otherUserID,
Similarity: similarity,
})
}
sort.Slice(users, func(i, j int) bool {
return users[i].Similarity > users[j].Similarity
})
if len(users) > k {
users = users[:k]
}
return users
}
type SimilarUser struct {
UserID int64
Similarity float64
}
// ==================== 协同过滤:Item-based CF ====================
// ItemBasedCF 基于物品的协同过滤
type ItemBasedCF struct {
// 用户-物品评分矩阵
userItemMatrix map[int64]map[int64]float64
// 物品相似度矩阵
itemSimilarity map[int64]map[int64]float64
}
// NewItemBasedCF 创建 Item-based CF
func NewItemBasedCF() *ItemBasedCF {
return &ItemBasedCF{
userItemMatrix: make(map[int64]map[int64]float64),
itemSimilarity: make(map[int64]map[int64]float64),
}
}
// AddBehavior 添加用户行为
func (cf *ItemBasedCF) AddBehavior(behavior *UserBehavior) {
if cf.userItemMatrix[behavior.UserID] == nil {
cf.userItemMatrix[behavior.UserID] = make(map[int64]float64)
}
score := map[string]float64{
"view": 1.0,
"click": 3.0,
"purchase": 5.0,
}[behavior.BehaviorType]
cf.userItemMatrix[behavior.UserID][behavior.ItemID] += score
}
// CalculateItemSimilarity 计算物品相似度
func (cf *ItemBasedCF) CalculateItemSimilarity() {
// 构建倒排索引:物品 -> 用户列表
itemUsers := make(map[int64]map[int64]float64)
for userID, items := range cf.userItemMatrix {
for itemID, score := range items {
if itemUsers[itemID] == nil {
itemUsers[itemID] = make(map[int64]float64)
}
itemUsers[itemID][userID] = score
}
}
// 计算物品相似度
items := make([]int64, 0)
for itemID := range itemUsers {
items = append(items, itemID)
}
for i := 0; i < len(items); i++ {
itemA := items[i]
if cf.itemSimilarity[itemA] == nil {
cf.itemSimilarity[itemA] = make(map[int64]float64)
}
for j := i + 1; j < len(items); j++ {
itemB := items[j]
// 使用 Jaccard 相似度(简化)
usersA := itemUsers[itemA]
usersB := itemUsers[itemB]
commonUsers := 0
for userID := range usersA {
if _, exists := usersB[userID]; exists {
commonUsers++
}
}
if commonUsers == 0 {
continue
}
totalUsers := len(usersA) + len(usersB) - commonUsers
similarity := float64(commonUsers) / float64(totalUsers)
cf.itemSimilarity[itemA][itemB] = similarity
if cf.itemSimilarity[itemB] == nil {
cf.itemSimilarity[itemB] = make(map[int64]float64)
}
cf.itemSimilarity[itemB][itemA] = similarity
}
}
}
// Recommend 生成推荐列表
func (cf *ItemBasedCF) Recommend(userID int64, count int) []*RecommendationItem {
// 1. 获取用户历史交互物品
userItems := cf.userItemMatrix[userID]
// 2. 找到相似物品
candidateItems := make(map[int64]float64)
for itemID, userScore := range userItems {
similarItems := cf.itemSimilarity[itemID]
for similarItemID, similarity := range similarItems {
// 过滤已交互物品
if _, exists := userItems[similarItemID]; exists {
continue
}
// 加权累加
candidateItems[similarItemID] += userScore * similarity
}
}
// 3. 排序并返回 Top N
items := make([]*RecommendationItem, 0)
for itemID, score := range candidateItems {
items = append(items, &RecommendationItem{
ItemID: itemID,
Score: score,
Reason: "看了还看",
})
}
sort.Slice(items, func(i, j int) bool {
return items[i].Score > items[j].Score
})
if len(items) > count {
items = items[:count]
}
return items
}
// ==================== 测试代码 ====================
func main() {
// 创建 User-based CF
userCF := NewUserBasedCF()
// 添加用户行为
behaviors := []*UserBehavior{
{UserID: 1, ItemID: 101, BehaviorType: "purchase"},
{UserID: 1, ItemID: 102, BehaviorType: "click"},
{UserID: 1, ItemID: 103, BehaviorType: "view"},
{UserID: 2, ItemID: 101, BehaviorType: "purchase"},
{UserID: 2, ItemID: 102, BehaviorType: "purchase"},
{UserID: 2, ItemID: 104, BehaviorType: "click"},
{UserID: 3, ItemID: 102, BehaviorType: "click"},
{UserID: 3, ItemID: 103, BehaviorType: "purchase"},
{UserID: 3, ItemID: 105, BehaviorType: "view"},
}
for _, behavior := range behaviors {
userCF.AddBehavior(behavior)
}
// 计算相似度
userCF.CalculateUserSimilarity()
// 生成推荐
recommendations := userCF.Recommend(1, 5)
fmt.Println("=== User-based CF 推荐结果 ===")
for i, item := range recommendations {
fmt.Printf("%d. ItemID: %d, Score: %.2f, Reason: %s\n",
i+1, item.ItemID, item.Score, item.Reason)
}
// 创建 Item-based CF
itemCF := NewItemBasedCF()
for _, behavior := range behaviors {
itemCF.AddBehavior(behavior)
}
itemCF.CalculateItemSimilarity()
recommendations = itemCF.Recommend(1, 5)
fmt.Println("\n=== Item-based CF 推荐结果 ===")
for i, item := range recommendations {
fmt.Printf("%d. ItemID: %d, Score: %.2f, Reason: %s\n",
i+1, item.ItemID, item.Score, item.Reason)
}
}
输出示例:
=== User-based CF 推荐结果 ===
1. ItemID: 104, Score: 13.50, Reason: 猜你喜欢
2. ItemID: 105, Score: 8.20, Reason: 猜你喜欢
=== Item-based CF 推荐结果 ===
1. ItemID: 104, Score: 5.67, Reason: 看了还看
2. ItemID: 105, Score: 3.00, Reason: 看了还看
V1 特点:
- 实现简单,易于理解
- 可解释性强(相似用户/物品)
- 数据稀疏问题
- 冷启动问题
- 可扩展性差(矩阵计算)
5.3 V2: 多路召回 + 排序
优化点:
- 多路召回策略
- 两阶段排序(粗排 + 精排)
- 特征工程
- 模型融合
// ==================== 多路召回 ====================
package recall
import (
"sort"
)
// RecallStrategy 召回策略接口
type RecallStrategy interface {
Recall(userID int64, count int) []*CandidateItem
Name() string
}
// CandidateItem 候选物品
type CandidateItem struct {
ItemID int64
Score float64
RecallStrategy string
Features map[string]float64 // 特征
}
// MultiRecallEngine 多路召回引擎
type MultiRecallEngine struct {
strategies []RecallStrategy
}
// NewMultiRecallEngine 创建多路召回引擎
func NewMultiRecallEngine() *MultiRecallEngine {
return &MultiRecallEngine{
strategies: make([]RecallStrategy, 0),
}
}
// AddStrategy 添加召回策略
func (e *MultiRecallEngine) AddStrategy(strategy RecallStrategy) {
e.strategies = append(e.strategies, strategy)
}
// Recall 多路召回
func (e *MultiRecallEngine) Recall(userID int64, countPerStrategy int) []*CandidateItem {
// 1. 并发召回
resultChan := make(chan []*CandidateItem, len(e.strategies))
for _, strategy := range e.strategies {
go func(s RecallStrategy) {
items := s.Recall(userID, countPerStrategy)
resultChan <- items
}(strategy)
}
// 2. 合并结果
allItems := make([]*CandidateItem, 0)
for i := 0; i < len(e.strategies); i++ {
items := <-resultChan
allItems = append(allItems, items...)
}
// 3. 去重
itemMap := make(map[int64]*CandidateItem)
for _, item := range allItems {
if existing, exists := itemMap[item.ItemID]; exists {
// 取最高分
if item.Score > existing.Score {
itemMap[item.ItemID] = item
}
} else {
itemMap[item.ItemID] = item
}
}
// 4. 转为列表
uniqueItems := make([]*CandidateItem, 0)
for _, item := range itemMap {
uniqueItems = append(uniqueItems, item)
}
return uniqueItems
}
// ==================== 召回策略实现 ====================
// UserCFRecall 基于用户协同过滤的召回
type UserCFRecall struct {
cf *UserBasedCF
}
func (r *UserCFRecall) Recall(userID int64, count int) []*CandidateItem {
recommendations := r.cf.Recommend(userID, count)
items := make([]*CandidateItem, 0)
for _, rec := range recommendations {
items = append(items, &CandidateItem{
ItemID: rec.ItemID,
Score: rec.Score,
RecallStrategy: "user_cf",
Features: make(map[string]float64),
})
}
return items
}
func (r *UserCFRecall) Name() string {
return "user_cf"
}
// ItemCFRecall 基于物品协同过滤的召回
type ItemCFRecall struct {
cf *ItemBasedCF
}
func (r *ItemCFRecall) Recall(userID int64, count int) []*CandidateItem {
recommendations := r.cf.Recommend(userID, count)
items := make([]*CandidateItem, 0)
for _, rec := range recommendations {
items = append(items, &CandidateItem{
ItemID: rec.ItemID,
Score: rec.Score,
RecallStrategy: "item_cf",
Features: make(map[string]float64),
})
}
return items
}
func (r *ItemCFRecall) Name() string {
return "item_cf"
}
// HotRecall 热门召回
type HotRecall struct {
hotItems []int64
}
func (r *HotRecall) Recall(userID int64, count int) []*CandidateItem {
items := make([]*CandidateItem, 0)
for i, itemID := range r.hotItems {
if i >= count {
break
}
items = append(items, &CandidateItem{
ItemID: itemID,
Score: float64(len(r.hotItems) - i), // 热度分
RecallStrategy: "hot",
Features: make(map[string]float64),
})
}
return items
}
func (r *HotRecall) Name() string {
return "hot"
}
// ==================== 排序层 ====================
package ranking
// RankingModel 排序模型接口
type RankingModel interface {
Predict(item *CandidateItem, userFeatures map[string]float64) float64
}
// LRModel 逻辑回归模型(简化)
type LRModel struct {
weights map[string]float64
bias float64
}
// Predict 预测 CTR
func (m *LRModel) Predict(item *CandidateItem, userFeatures map[string]float64) float64 {
// 特征拼接
features := make(map[string]float64)
// 用户特征
for k, v := range userFeatures {
features["user_"+k] = v
}
// 物品特征
for k, v := range item.Features {
features["item_"+k] = v
}
// 召回分数
features["recall_score"] = item.Score
// 线性加权
score := m.bias
for feature, value := range features {
if weight, exists := m.weights[feature]; exists {
score += weight * value
}
}
// Sigmoid
return 1.0 / (1.0 + math.Exp(-score))
}
// RankingService 排序服务
type RankingService struct {
model RankingModel
}
// Rank 排序
func (s *RankingService) Rank(items []*CandidateItem, userFeatures map[string]float64) []*CandidateItem {
// 1. 预测 CTR
for _, item := range items {
item.Score = s.model.Predict(item, userFeatures)
}
// 2. 排序
sort.Slice(items, func(i, j int) bool {
return items[i].Score > items[j].Score
})
return items
}
// ==================== 完整推荐流程 ====================
type RecommendationService struct {
recallEngine *MultiRecallEngine
rankingService *RankingService
}
func (s *RecommendationService) Recommend(userID int64, count int) []*CandidateItem {
// 1. 召回(每路召回 500 个)
candidates := s.recallEngine.Recall(userID, 500)
// 2. 粗排(筛选 Top 100)
candidates = s.preRank(candidates, 100)
// 3. 精排
userFeatures := s.getUserFeatures(userID)
rankedItems := s.rankingService.Rank(candidates, userFeatures)
// 4. 重排(多样性、打散)
finalItems := s.rerank(rankedItems, count)
return finalItems
}
func (s *RecommendationService) preRank(items []*CandidateItem, count int) []*CandidateItem {
// 简单规则排序
sort.Slice(items, func(i, j int) bool {
return items[i].Score > items[j].Score
})
if len(items) > count {
items = items[:count]
}
return items
}
func (s *RecommendationService) rerank(items []*CandidateItem, count int) []*CandidateItem {
// 去重、打散(避免同类物品连续出现)
// 这里简化实现
if len(items) > count {
items = items[:count]
}
return items
}
func (s *RecommendationService) getUserFeatures(userID int64) map[string]float64 {
// 从 Redis/DB 获取用户特征
return map[string]float64{
"age": 25,
"gender": 1, // 0=女, 1=男
"active_days": 180,
"avg_price": 500,
}
}
V2 特点:
- 多路召回提高覆盖率
- 特征工程提升精度
- 两阶段排序平衡效率和准确率
- ⚠️ 特征工程需要人工设计
5.4 V3: 深度学习推荐系统
优化点:
- Embedding 向量化
- 深度学习模型(Wide & Deep)
- 实时特征更新
- A/B 测试框架
// ==================== Embedding 向量化 ====================
package embedding
import (
"math"
)
// EmbeddingModel Embedding 模型
type EmbeddingModel struct {
// 用户 Embedding
userEmbeddings map[int64][]float64
// 物品 Embedding
itemEmbeddings map[int64][]float64
// Embedding 维度
dimension int
}
// NewEmbeddingModel 创建 Embedding 模型
func NewEmbeddingModel(dimension int) *EmbeddingModel {
return &EmbeddingModel{
userEmbeddings: make(map[int64][]float64),
itemEmbeddings: make(map[int64][]float64),
dimension: dimension,
}
}
// GetUserEmbedding 获取用户 Embedding
func (m *EmbeddingModel) GetUserEmbedding(userID int64) []float64 {
if emb, exists := m.userEmbeddings[userID]; exists {
return emb
}
// 新用户:随机初始化或零向量
return make([]float64, m.dimension)
}
// GetItemEmbedding 获取物品 Embedding
func (m *EmbeddingModel) GetItemEmbedding(itemID int64) []float64 {
if emb, exists := m.itemEmbeddings[itemID]; exists {
return emb
}
return make([]float64, m.dimension)
}
// CosineSimilarity 计算余弦相似度
func CosineSimilarity(vec1, vec2 []float64) float64 {
if len(vec1) != len(vec2) {
return 0
}
var dotProduct, norm1, norm2 float64
for i := 0; i < len(vec1); i++ {
dotProduct += vec1[i] * vec2[i]
norm1 += vec1[i] * vec1[i]
norm2 += vec2[i] * vec2[i]
}
if norm1 == 0 || norm2 == 0 {
return 0
}
return dotProduct / (math.Sqrt(norm1) * math.Sqrt(norm2))
}
// RecallByEmbedding 基于 Embedding 召回
func (m *EmbeddingModel) RecallByEmbedding(userID int64, count int) []int64 {
userEmb := m.GetUserEmbedding(userID)
// 计算与所有物品的相似度
type ItemScore struct {
ItemID int64
Score float64
}
scores := make([]ItemScore, 0)
for itemID := range m.itemEmbeddings {
itemEmb := m.GetItemEmbedding(itemID)
similarity := CosineSimilarity(userEmb, itemEmb)
scores = append(scores, ItemScore{ItemID: itemID, Score: similarity})
}
// 排序
sort.Slice(scores, func(i, j int) bool {
return scores[i].Score > scores[j].Score
})
// 返回 Top K
result := make([]int64, 0)
for i := 0; i < count && i < len(scores); i++ {
result = append(result, scores[i].ItemID)
}
return result
}
// ==================== Wide & Deep 模型(概念)====================
// WideDeepModel Wide & Deep 模型
type WideDeepModel struct {
// Wide 部分:线性模型
wideWeights map[string]float64
// Deep 部分:深度神经网络
deepLayers [][]float64 // 简化:权重矩阵
}
// Predict 预测 CTR
func (m *WideDeepModel) Predict(features map[string]float64) float64 {
// Wide 部分
wideScore := 0.0
for feature, value := range features {
if weight, exists := m.wideWeights[feature]; exists {
wideScore += weight * value
}
}
// Deep 部分(简化:多层感知机)
deepScore := 0.0
// ... 这里需要实现前向传播 ...
// 融合
finalScore := 0.5*wideScore + 0.5*deepScore
// Sigmoid
return 1.0 / (1.0 + math.Exp(-finalScore))
}
// ==================== 实时特征更新 ====================
package realtime
import (
"context"
"time"
)
// RealtimeFeatureService 实时特征服务
type RealtimeFeatureService struct {
redis *redis.Client
}
// UpdateUserBehavior 更新用户行为特征
func (s *RealtimeFeatureService) UpdateUserBehavior(userID int64, behavior *UserBehavior) {
ctx := context.Background()
// 1. 更新最近浏览列表(保留最近 100 个)
key := fmt.Sprintf("user:%d:recent_items", userID)
s.redis.LPush(ctx, key, behavior.ItemID)
s.redis.LTrim(ctx, key, 0, 99)
s.redis.Expire(ctx, key, 7*24*time.Hour)
// 2. 更新物品交互次数
itemKey := fmt.Sprintf("user:%d:item:%d:count", userID, behavior.ItemID)
s.redis.Incr(ctx, itemKey)
s.redis.Expire(ctx, itemKey, 30*24*time.Hour)
// 3. 更新用户活跃时段
hour := time.Now().Hour()
hourKey := fmt.Sprintf("user:%d:active_hours", userID)
s.redis.ZIncrBy(ctx, hourKey, 1, fmt.Sprintf("%d", hour))
// 4. 发送到 Kafka 进行异步处理
// kafka.Produce("user-behavior", behavior)
}
// GetUserRecentItems 获取用户最近浏览
func (s *RealtimeFeatureService) GetUserRecentItems(userID int64, count int) []int64 {
ctx := context.Background()
key := fmt.Sprintf("user:%d:recent_items", userID)
items, err := s.redis.LRange(ctx, key, 0, int64(count-1)).Result()
if err != nil {
return []int64{}
}
itemIDs := make([]int64, 0)
for _, item := range items {
itemID, _ := strconv.ParseInt(item, 10, 64)
itemIDs = append(itemIDs, itemID)
}
return itemIDs
}
V3 特点:
- 深度学习自动特征提取
- Embedding 向量降维
- 实时特征更新
- 更高的推荐精度
- ⚠️ 需要大量训练数据
- ⚠️ 模型训练成本高
6. 核心算法优化
6.1 冷启动解决方案
// ColdStartHandler 冷启动处理器
type ColdStartHandler struct {
hotItems []int64 // 热门物品
categoryHot map[string][]int64 // 分类热门
demographicRec map[string][]int64 // 人口统计推荐
}
// HandleNewUser 处理新用户
func (h *ColdStartHandler) HandleNewUser(userProfile *UserProfile) []*RecommendationItem {
items := make([]*RecommendationItem, 0)
// 1. 基于人口属性推荐
demographic := fmt.Sprintf("%s_%s_%s",
userProfile.Gender, userProfile.AgeRange, userProfile.City)
if recs, exists := h.demographicRec[demographic]; exists {
for _, itemID := range recs {
items = append(items, &RecommendationItem{
ItemID: itemID,
Score: 0.8,
Reason: "为你精选",
})
}
}
// 2. 全站热门
for i, itemID := range h.hotItems {
if i >= 10 {
break
}
items = append(items, &RecommendationItem{
ItemID: itemID,
Score: 0.9 - float64(i)*0.05,
Reason: "热门推荐",
})
}
return items
}
// HandleNewItem 处理新物品
func (h *ColdStartHandler) HandleNewItem(itemID int64, features *ItemFeatures) {
// 1. 基于内容相似度推荐
// 找到相似物品,给相似物品的用户推荐新物品
// 2. 小流量试探
// 给一定比例的流量展示新物品,根据 CTR 决定是否继续推广
}
6.2 多样性与打散
// DiversityReranker 多样性重排
type DiversityReranker struct {
categoryService *CategoryService
}
// Rerank 重排(MMR 算法:Maximal Marginal Relevance)
func (r *DiversityReranker) Rerank(items []*RecommendationItem, count int, lambda float64) []*RecommendationItem {
if len(items) <= count {
return items
}
result := make([]*RecommendationItem, 0)
remaining := make([]*RecommendationItem, len(items))
copy(remaining, items)
// 第一个:最高分
result = append(result, remaining[0])
remaining = remaining[1:]
// 迭代选择
for len(result) < count && len(remaining) > 0 {
maxScore := -1.0
maxIndex := 0
for i, item := range remaining {
// MMR = λ × Relevance - (1-λ) × Similarity
relevance := item.Score
// 计算与已选物品的最大相似度
maxSimilarity := 0.0
for _, selected := range result {
similarity := r.calculateSimilarity(item, selected)
if similarity > maxSimilarity {
maxSimilarity = similarity
}
}
mmr := lambda*relevance - (1-lambda)*maxSimilarity
if mmr > maxScore {
maxScore = mmr
maxIndex = i
}
}
result = append(result, remaining[maxIndex])
remaining = append(remaining[:maxIndex], remaining[maxIndex+1:]...)
}
return result
}
// calculateSimilarity 计算物品相似度(基于类目)
func (r *DiversityReranker) calculateSimilarity(item1, item2 *RecommendationItem) float64 {
cat1 := r.categoryService.GetCategory(item1.ItemID)
cat2 := r.categoryService.GetCategory(item2.ItemID)
if cat1 == cat2 {
return 1.0
}
return 0.0
}
6.3 Exploration vs Exploitation
// EpsilonGreedyStrategy ε-贪心策略
type EpsilonGreedyStrategy struct {
epsilon float64 // 探索概率
}
// Select 选择推荐策略
func (s *EpsilonGreedyStrategy) Select(items []*RecommendationItem) []*RecommendationItem {
result := make([]*RecommendationItem, 0)
for _, item := range items {
random := rand.Float64()
if random < s.epsilon {
// 探索:随机选择
result = append(result, s.randomItem())
} else {
// 利用:选择高分物品
result = append(result, item)
}
}
return result
}
// UCBStrategy UCB(Upper Confidence Bound)策略
type UCBStrategy struct {
itemStats map[int64]*ItemStats
}
type ItemStats struct {
Impressions int
Clicks int
CTR float64
}
// CalculateUCB 计算 UCB 分数
func (s *UCBStrategy) CalculateUCB(itemID int64, totalImpressions int) float64 {
stats := s.itemStats[itemID]
if stats == nil || stats.Impressions == 0 {
return math.MaxFloat64 // 未曝光物品优先
}
// UCB = CTR + sqrt(2 * ln(总曝光) / 物品曝光)
ucb := stats.CTR + math.Sqrt(2*math.Log(float64(totalImpressions))/float64(stats.Impressions))
return ucb
}
7. 性能优化
7.1 召回加速
// ANN (Approximate Nearest Neighbor) 近似最近邻
// 使用 FAISS、Annoy 等库加速向量检索
import "github.com/spotify/annoy-go"
type ANNRecallService struct {
index *annoy.AnnoyIndex
}
func (s *ANNRecallService) BuildIndex(embeddings map[int64][]float64, dimension int) {
s.index = annoy.NewAnnoyIndex(dimension, "euclidean")
for itemID, emb := range embeddings {
s.index.AddItem(int(itemID), emb)
}
s.index.Build(10) // 10 trees
}
func (s *ANNRecallService) Recall(userEmbedding []float64, count int) []int64 {
results, _ := s.index.GetNnsByVector(userEmbedding, count, -1)
itemIDs := make([]int64, len(results))
for i, id := range results {
itemIDs[i] = int64(id)
}
return itemIDs
}
7.2 特征缓存
type FeatureCacheService struct {
redis *redis.Client
}
// GetUserFeatures 获取用户特征(带缓存)
func (s *FeatureCacheService) GetUserFeatures(userID int64) map[string]float64 {
ctx := context.Background()
cacheKey := fmt.Sprintf("features:user:%d", userID)
// 1. 查询缓存
cached, err := s.redis.Get(ctx, cacheKey).Result()
if err == nil {
var features map[string]float64
json.Unmarshal([]byte(cached), &features)
return features
}
// 2. 查询数据库
features := s.loadUserFeaturesFromDB(userID)
// 3. 写入缓存
data, _ := json.Marshal(features)
s.redis.Set(ctx, cacheKey, data, 10*time.Minute)
return features
}
7.3 结果预计算
// 离线预计算推荐结果
type OfflineRecommendationJob struct {
db *sql.DB
redis *redis.Client
}
func (job *OfflineRecommendationJob) Run() {
// 1. 获取所有活跃用户
users := job.getActiveUsers()
// 2. 批量计算推荐
for _, userID := range users {
recommendations := job.calculateRecommendations(userID)
// 3. 存储到 Redis
key := fmt.Sprintf("rec:user:%d:offline", userID)
data, _ := json.Marshal(recommendations)
job.redis.Set(context.Background(), key, data, 24*time.Hour)
}
}
8. 监控与评估
8.1 推荐指标
// RecommendationMetrics 推荐指标
type RecommendationMetrics struct {
// 业务指标
CTR float64 // 点击率
CVR float64 // 转化率
GMV float64 // 成交金额
// 用户体验指标
Diversity float64 // 多样性
Coverage float64 // 覆盖率
Novelty float64 // 新颖度
// 系统指标
LatencyP99 time.Duration // 延迟
QPS int
ErrorRate float64
}
// CalculateCTR 计算点击率
func CalculateCTR(impressions, clicks int) float64 {
if impressions == 0 {
return 0
}
return float64(clicks) / float64(impressions)
}
// CalculateCoverage 计算覆盖率
func CalculateCoverage(recommendedItems, totalItems int) float64 {
return float64(recommendedItems) / float64(totalItems)
}
8.2 A/B 测试
package abtest
type ABTestService struct {
experiments map[string]*Experiment
}
type Experiment struct {
Name string
Variants []string // ["control", "treatment_a", "treatment_b"]
TrafficSplit map[string]float64 // {"control": 0.5, "treatment_a": 0.3, "treatment_b": 0.2}
}
// GetVariant 获取用户分组
func (s *ABTestService) GetVariant(experimentName string, userID int64) string {
exp := s.experiments[experimentName]
if exp == nil {
return "control"
}
// 哈希分流
hash := hashUserID(userID, experimentName)
ratio := float64(hash%10000) / 10000.0
cumulative := 0.0
for variant, traffic := range exp.TrafficSplit {
cumulative += traffic
if ratio < cumulative {
return variant
}
}
return "control"
}
func hashUserID(userID int64, experimentName string) int {
h := fnv.New32a()
h.Write([]byte(fmt.Sprintf("%d_%s", userID, experimentName)))
return int(h.Sum32())
}
// 使用示例
func (s *RecommendationService) Recommend(userID int64, count int) []*RecommendationItem {
variant := s.abtestService.GetVariant("rec_algorithm_v2", userID)
if variant == "treatment_a" {
// 使用新算法
return s.recommendV2(userID, count)
} else {
// 使用旧算法
return s.recommendV1(userID, count)
}
}
8.3 Prometheus 监控
var (
recommendationTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "recommendation_total",
Help: "Total recommendation requests",
},
[]string{"scene", "strategy"},
)
recommendationLatency = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "recommendation_latency_seconds",
Help: "Recommendation latency",
Buckets: []float64{0.01, 0.05, 0.1, 0.5, 1},
},
[]string{"scene"},
)
recommendationCTR = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "recommendation_ctr",
Help: "Recommendation CTR",
},
[]string{"scene"},
)
)
func (s *RecommendationService) RecordMetrics(scene string, strategy string, latency time.Duration) {
recommendationTotal.WithLabelValues(scene, strategy).Inc()
recommendationLatency.WithLabelValues(scene).Observe(latency.Seconds())
}
9. 面试问答(10个高频题)
协同过滤的 User-based 和 Item-based 有什么区别?
答:
| 对比项 | User-based CF | Item-based CF |
|---|---|---|
| 原理 | 找相似用户的喜好 | 找相似物品推荐 |
| 计算量 | O(用户数²) | O(物品数²) |
| 适用场景 | 用户少,物品多(新闻) | 物品少,用户多(电商) |
| 实时性 | 差(用户相似度变化快) | 好(物品相似度稳定) |
| 冷启动 | 新用户困难 | 新物品困难 |
| 可解释性 | "和你相似的人喜欢" | "看了还看" |
实际选择:
- 电商(淘宝):Item-based(物品稳定)
- 资讯(今日头条):User-based + 内容推荐
如何解决推荐系统的冷启动问题?
答:
新用户冷启动:
- 热门推荐:全站热门物品
- 人口属性:基于性别、年龄、地域推荐
- 引导注册:让用户选择感兴趣的类目
- 基于内容:如果用户有搜索/浏览记录
新物品冷启动:
- 基于内容:找相似物品的用户推荐
- 小流量试探:给少量用户曝光,根据 CTR 决定
- 编辑精选:人工干预推荐
- 协同过滤 + 内容:混合推荐
代码示例:
func (s *RecommendationService) HandleColdStart(userID int64) []*RecommendationItem {
// 检查用户行为数
behaviorCount := s.getUserBehaviorCount(userID)
if behaviorCount < 10 {
// 新用户:热门 + 人口属性
return s.coldStartHandler.HandleNewUser(userProfile)
} else {
// 老用户:正常推荐
return s.Recommend(userID, 20)
}
}
召回和排序有什么区别?为什么要分两阶段?
答:
召回(Recall):
- 目标:快速筛选候选集
- 规模:从百万 → 数千
- 算法:简单、快速(协同过滤、向量检索)
- 延迟:< 10ms
排序(Ranking):
- 目标:精准排序
- 规模:从数千 → 数十
- 算法:复杂、精准(深度学习)
- 延迟:< 50ms
为什么分两阶段:
- 效率:不可能对所有物品精排(计算量太大)
- 分工:召回保证覆盖,排序保证精准
- 灵活性:多路召回 + 统一排序
流程:
全量物品(1000万)
↓ 召回(多路)
候选集(3000个)
↓ 粗排
精选集(200个)
↓ 精排
最终推荐(20个)
如何评估推荐系统的效果?
答:
离线评估:
准确率指标:
- Precision@K:前 K 个推荐中命中的比例
- Recall@K:前 K 个推荐覆盖用户兴趣的比例
- NDCG(Normalized Discounted Cumulative Gain):考虑排序位置
多样性指标:
- Coverage:覆盖物品的比例
- Diversity:推荐列表的类目分散程度
在线评估:
业务指标:
- CTR(点击率)
- CVR(转化率)
- GMV(成交金额)
- 人均浏览时长
A/B 测试:
- 对照组 vs 实验组
- 统计显著性检验
示例:
// Precision@K
func PrecisionAtK(recommended, actual []int64, k int) float64 {
if k > len(recommended) {
k = len(recommended)
}
actualSet := make(map[int64]bool)
for _, item := range actual {
actualSet[item] = true
}
hit := 0
for i := 0; i < k; i++ {
if actualSet[recommended[i]] {
hit++
}
}
return float64(hit) / float64(k)
}
如何处理推荐系统的实时性?
答:
挑战:
- 用户行为实时变化
- 推荐结果需要快速更新
解决方案:
1. 实时特征更新:
// 用户点击后立即更新 Redis
func (s *RealtimeService) OnUserClick(userID, itemID int64) {
// 更新最近点击
s.redis.LPush(fmt.Sprintf("user:%d:clicks", userID), itemID)
// 更新实时兴趣标签
tags := s.getItemTags(itemID)
for _, tag := range tags {
s.redis.ZIncrBy(fmt.Sprintf("user:%d:interest", userID), 1, tag)
}
}
2. 离线 + 在线结合:
- 离线:每天计算基础推荐(User CF、Item CF)
- 在线:根据实时行为调整排序
3. 流式计算:
用户行为 → Kafka → Flink → 实时特征更新 → Redis
4. 增量更新:
- 不是全量重新计算
- 只更新相关用户/物品
推荐系统如何保证多样性?
答:
问题:
- 推荐结果都是同类物品
- "信息茧房"
解决方案:
1. MMR 重排(Maximal Marginal Relevance):
// 选择物品时,同时考虑相关性和多样性
score = λ × relevance - (1-λ) × max_similarity
2. 类目打散:
// 确保连续推荐不是同类目
func (r *DiversityReranker) SpreadCategories(items []*Item) []*Item {
result := make([]*Item, 0)
usedCategories := make(map[string]int)
for _, item := range items {
// 如果这个类目连续出现 2 次,跳过
if usedCategories[item.Category] >= 2 {
continue
}
result = append(result, item)
usedCategories[item.Category]++
// 重置计数
if len(result) % 5 == 0 {
usedCategories = make(map[string]int)
}
}
return result
}
3. Exploration(探索):
- 一定比例推荐新物品
- ε-贪心、UCB 策略
深度学习推荐和传统推荐有什么区别?
答:
| 对比项 | 传统推荐 | 深度学习推荐 |
|---|---|---|
| 特征工程 | 手工设计 | 自动学习 |
| 非线性 | 弱 | 强(深度网络) |
| 数据需求 | 少 | 大量数据 |
| 训练成本 | 低 | 高(GPU) |
| 可解释性 | 强 | 弱 |
| 实时性 | 好 | 差(模型推理) |
常见深度学习模型:
- Wide & Deep:结合线性和深度
- DeepFM:自动学习特征交叉
- DIN(Deep Interest Network):注意力机制
- DSSM(双塔模型):用户塔 + 物品塔
选择建议:
- 小公司、数据少:传统协同过滤
- 大公司、数据多:深度学习
如何设计推荐系统的 A/B 测试?
答:
步骤:
1. 定义实验:
experiment := &Experiment{
Name: "rec_algorithm_v2",
Variants: []string{"control", "treatment"},
TrafficSplit: map[string]float64{
"control": 0.9, // 90% 旧算法
"treatment": 0.1, // 10% 新算法
},
}
2. 流量分配:
// 基于 UserID 哈希分流(保证同一用户始终在同一组)
func GetVariant(userID int64) string {
hash := hashUserID(userID) % 100
if hash < 10 {
return "treatment"
}
return "control"
}
3. 指标统计:
// 记录每个分组的指标
func RecordMetrics(userID int64, variant string, metrics *Metrics) {
// 存储到数据仓库
dataWarehouse.Insert(&ABTestMetrics{
ExperimentName: "rec_algorithm_v2",
Variant: variant,
UserID: userID,
CTR: metrics.CTR,
CVR: metrics.CVR,
Timestamp: time.Now(),
})
}
4. 显著性检验:
# t-检验
from scipy import stats
control_ctr = [0.05, 0.06, 0.05, ...]
treatment_ctr = [0.07, 0.08, 0.07, ...]
t_stat, p_value = stats.ttest_ind(control_ctr, treatment_ctr)
if p_value < 0.05:
print("显著提升")
推荐系统如何处理马太效应(热门物品越来越热)?
答:
问题:
- 热门物品曝光多 → CTR 高 → 更多曝光
- 长尾物品无法曝光
解决方案:
1. 降权热门物品:
func (s *RecommendationService) AdjustScore(item *Item) float64 {
score := item.RawScore
// 热门物品降权
if item.TotalViews > 100000 {
penalty := math.Log(float64(item.TotalViews) / 100000)
score = score / (1 + penalty*0.1)
}
return score
}
2. 时间衰减:
// 老物品降权
func TimeDecay(item *Item) float64 {
days := time.Since(item.CreatedAt).Hours() / 24
decay := math.Exp(-0.01 * days)
return item.Score * decay
}
3. 探索机制:
- 一定比例推荐长尾物品
- 监控长尾物品的 CTR
4. 分层推荐:
20% 热门 + 50% 中等热度 + 30% 长尾
如何设计大规模推荐系统的架构?
答:
挑战:
- 亿级用户
- 千万级物品
- 10 万 QPS
架构设计:
1. 分层架构:
召回层(并行)→ 粗排层 → 精排层 → 重排层
2. 存储方案:
- MySQL:用户画像、物品特征
- Redis:实时特征、推荐缓存
- HBase:用户行为历史
- Elasticsearch:基于内容召回
3. 计算方案:
- 离线:Spark 计算协同过滤(每日)
- 实时:Flink 处理用户行为流
- 在线:Go 服务召回+排序
4. 缓存策略:
// 三级缓存
1. 本地缓存(Caffeine):热点用户
2. Redis 缓存:推荐结果(10分钟)
3. 预计算:离线推荐(每日更新)
5. 降级方案:
if 精排服务故障 {
return 粗排结果
}
if 召回服务故障 {
return 热门推荐
}
6. 监控告警:
- QPS、延迟、错误率
- CTR、CVR 实时监控
- 模型效果 A/B 对比
10. 总结
核心要点
推荐算法
- 协同过滤:User-based、Item-based
- 内容推荐:基于物品特征
- 深度学习:Embedding、Wide & Deep
- 混合推荐:多路召回 + 融合
系统架构
- 召回 → 粗排 → 精排 → 重排
- 离线计算 + 实时计算
- 多级缓存
核心问题
- 冷启动:热门推荐、人口属性、小流量试探
- 多样性:MMR、类目打散、探索机制
- 实时性:流式计算、增量更新
效果评估
- 离线:Precision、Recall、NDCG
- 在线:CTR、CVR、GMV
- A/B 测试
架构演进
V1: 协同过滤(User CF / Item CF)
↓ (数据量增大)
V2: 多路召回 + 特征工程 + LR/GBDT
↓ (深度学习)
V3: Embedding + 深度学习 + 实时特征
面试建议
- 算法原理:协同过滤、矩阵分解要能讲清楚
- 工程实现:召回、排序的分工和优化
- 实际问题:冷启动、多样性、实时性
- 业务理解:CTR、CVR、用户体验
- 系统设计:大规模架构、缓存、降级
本章完,祝面试顺利!