1. 背景与意义
随着互联网流量红利见顶,广告主对投放效率的要求越来越高。传统广告投放系统往往依赖人工圈选人群、固定频控策略和粗粒度的渠道定向,存在三个突出问题:一是投放决策滞后,无法实时响应用户兴趣变化;二是规则配置复杂,中小团队维护成本高;三是数据链路冗长,从行为采集到策略生效往往需要数小时甚至数天。
基于用户行为推荐的轻量化广告投放系统,核心思路是把用户实时行为(点击、浏览、搜索、加购等)作为推荐信号,通过轻量级算法在毫秒级完成广告候选排序与投放决策。相比传统重型推荐平台,它不追求超大模型和全量离线训练,而是强调快速落地、低资源占用和可解释性,特别适合中小型业务团队、垂直行业站点和预算有限的增长团队。
其意义主要体现在三个方面:
- 提升投放效率:基于实时行为信号动态调整广告排序,减少无效曝光,提高点击率和转化率。
- 降低落地门槛:采用轻量技术栈和简化数据链路,团队无需搭建大规模分布式计算平台即可上线。
- 增强可解释性:规则与模型结合,投放决策可回溯、可干预,便于运营人员理解和调优。
2. 系统整体架构
系统整体采用模块化分层设计,核心链路为「行为采集 → 特征加工 → 候选召回 → 排序推荐 → 投放反馈」。整体架构如下图所示:
flowchart TD A[用户端行为埋点] --> B[行为采集服务] B --> C[实时特征计算] C --> D[候选广告召回] D --> E[轻量排序模型] E --> F[广告投放引擎] F --> G[曝光与点击反馈] G --> C各模块职责如下:
- 行为采集服务:接收前端埋点数据,进行清洗、去重和标准化,输出统一行为事件。
- 实时特征计算:基于用户近期行为序列,计算兴趣标签、行为频次、活跃度等特征。
- 候选广告召回:通过标签匹配、协同过滤或向量检索,从广告库中召回候选集。
- 轻量排序模型:使用逻辑回归、GBDT 或浅层神经网络对候选广告打分排序。
- 广告投放引擎:结合频控、预算和投放策略,决定最终展示广告并记录反馈。
3. 技术栈选型
轻量化系统的技术选型遵循「够用、易维护、可扩展」原则,避免引入过重组件。推荐技术栈如下:
| 层次 | 技术选型 | 说明 |
|---|---|---|
| 前端埋点 | JavaScript SDK / 小程序 SDK | 采集点击、曝光、浏览时长等行为数据 |
| 接入层 | Nginx + Spring Boot / Go | 提供高并发接入与简单路由能力 |
| 消息队列 | Kafka / Redis Stream | 削峰填谷,异步解耦行为写入与消费 |
| 实时计算 | Flink / Redis + 定时聚合 | 轻量场景可用 Redis 计数 + 窗口聚合替代 Flink |
| 特征存储 | Redis / RedisJSON | 存储用户实时行为序列与特征缓存 |
| 广告索引 | Elasticsearch / MySQL + 内存索引 | 支持标签过滤与候选召回 |
| 排序模型 | Python + scikit-learn / XGBoost | 离线训练,在线加载模型文件进行预测 |
| 投放服务 | Java / Go 微服务 | 负责频控、预算控制与广告下发 |
| 数据报表 | ClickHouse / MySQL + 定时任务 | 汇总曝光、点击、消耗等投放指标 |
对于日活十万级以内的业务,可以进一步简化:去掉 Flink,使用 Redis 配合定时任务完成分钟级特征聚合;排序模型使用逻辑回归即可获得不错效果。
4. 核心代码实现
下面给出系统中最核心的三个代码片段:行为特征计算、候选召回和轻量排序打分。
4.1 用户行为特征计算
该模块从 Redis 中读取用户最近行为序列,计算兴趣标签权重和活跃度特征。以下为 Java 实现示例:
import redis.clients.jedis.Jedis; import java.util.HashMap; import java.util.List; import java.util.Map; public class UserFeatureService { private static final String BEHAVIOR_KEY_PREFIX = "user:behavior:"; private static final int MAX_SEQUENCE_LENGTH = 50; private final Jedis jedis; public UserFeatureService(Jedis jedis) { this.jedis = jedis; } public Map<String, Object> buildFeatures(String userId) { String key = BEHAVIOR_KEY_PREFIX + userId; List<String> behaviors = jedis.lrange(key, 0, MAX_SEQUENCE_LENGTH - 1); Map<String, Integer> tagWeight = new HashMap<>(); int clickCount = 0; int viewCount = 0; for (String behavior : behaviors) { String[] parts = behavior.split(":"); String type = parts[0]; String tag = parts[1]; int weight = getBehaviorWeight(type); tagWeight.merge(tag, weight, Integer::sum); if ("click".equals(type)) { clickCount++; } else if ("view".equals(type)) { viewCount++; } } Map<String, Object> features = new HashMap<>(); features.put("tagWeight", tagWeight); features.put("clickCount", clickCount); features.put("viewCount", viewCount); features.put("activeScore", clickCount * 2 + viewCount); return features; } private int getBehaviorWeight(String type) { switch (type) { case "click": return 3; case "search": return 4; case "cart": return 5; case "view": default: return 1; } } }4.2 候选广告召回
召回阶段根据用户兴趣标签从广告索引中筛选候选广告。以下为基于标签匹配的召回实现:
import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public class AdRecallService { private final AdIndex adIndex; public AdRecallService(AdIndex adIndex) { this.adIndex = adIndex; } public List<Ad> recall(Map<String, Integer> tagWeight, int topN) { // 按权重排序用户兴趣标签 List<Map.Entry<String, Integer>> sortedTags = new ArrayList<>(tagWeight.entrySet()); sortedTags.sort((a, b) -> b.getValue() - a.getValue()); Set<String> seenAdIds = new HashSet<>(); List<Ad> candidates = new ArrayList<>(); for (Map.Entry<String, Integer> entry : sortedTags) { String tag = entry.getKey(); List<Ad> adsByTag = adIndex.getAdsByTag(tag); for (Ad ad : adsByTag) { if (seenAdIds.add(ad.getAdId())) { candidates.add(ad); if (candidates.size() >= topN) { return candidates; } } } } return candidates; } }4.3 轻量排序模型打分
排序阶段使用逻辑回归模型对候选广告打分。模型离线训练后导出权重文件,在线服务加载权重进行预测。以下为 Python 训练脚本示例:
import pandas as pd from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import auc, roc_curve import joblib 加载训练数据:特征列 + 是否点击标签 df = pd.read_csv("ad_train_data.csv") feature_cols = ["tag_match_score", "user_active_score", "ad_ctr", "ad_cvr", "time_decay"] X = df[feature_cols] y = df["is_click"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) model = LogisticRegression(C=1.0, max_iter=200) model.fit(X_train, y_train) 评估 AUC y_prob = model.predict_proba(X_test)[:, 1] fpr, tpr, _ = roc_curve(y_test, y_prob) print("AUC:", auc(fpr, tpr)) 保存模型权重,供在线服务加载 joblib.dump(model, "lr_ad_model.pkl") print("feature_weights:", dict(zip(feature_cols, model.coef_[0])))在线服务加载模型后,对召回候选逐条计算点击概率,按概率降序排列,再结合频控和预算策略决定最终投放列表。
5. 投放策略与频控
排序模型给出候选广告的优先级后,投放引擎还需要执行频控和预算控制,避免过度打扰用户或超预算消耗。核心策略包括:
- 用户级频控:同一用户对同一广告的曝光次数上限,例如 24 小时内最多 3 次。
- 广告级频控:控制单条广告在全站的总曝光速率,防止流量集中。
- 预算控制:按天预算和小时预算双重限制,超出后自动暂停投放。
- 平滑投放:将预算均匀分配到全天时段,避免凌晨或高峰时段集中消耗。
频控判断使用 Redis 计数器实现,具备原子性和低延迟特性,适合高并发投放场景。
6. 总结与优化方向
基于用户行为推荐的轻量化广告投放系统,通过实时行为特征、标签召回和轻量排序模型的组合,能够在较低成本下实现个性化广告投放。系统核心优势在于技术栈轻、落地快、可解释性强,适合中小团队快速验证和迭代。
后续优化可以从以下几个方向展开:
- 特征扩展:引入用户长期兴趣画像、广告素材特征和上下文特征,提升排序效果。
- 模型升级:在数据量增长后,从逻辑回归升级为 GBDT 或深度排序模型。
- 冷启动优化:对新用户采用热门广告兜底和探索策略,积累行为后再个性化推荐。
- A/B 实验平台:建立分流实验能力,量化评估算法和策略迭代效果。