
简介这是面向推荐系统学习与实战的Python电商广告推荐系统源码包以阿里巴巴淘宝展示广告点击率预估数据集Ali_Display_Ad_Click为依托该数据集包含2600万条广告展示/点击日志与80万条广告特征项目完整覆盖离线召回、在线推荐、CTR预估等核心环节适合具备一定Python基础的算法工程师、数据分析人员或对广告推荐感兴趣的学习者参考。包内共13个文件包括12个Python脚本和1个README说明脚本涵盖ALS协同过滤召回、点击率模型、在线推荐、类目与品牌评分、数据存储及特征处理等模块目录结构清晰便于按功能阅读和二次开发。压缩包仅21KB轻量易部署可直接运行或修改目前已有920人学习下载读者可从中掌握从数据预处理、特征工程到离线召回与在线排序的推荐系统完整实现思路。1. Python电商广告推荐系统源码.zip先分清它是召回、排序还是全链路“Python电商广告推荐系统源码.zip”这类压缩包在技术社区里很常见尤其在免费 Python 源码大全式的资源帖里几乎每周都能看到新版本挂出来。但下载之后它往往是一堆 .py、.csv 和模型文件我见过不少同事拿到手就解压、装依赖、跑 train.py结果不是报错就是模型排序结果肉眼可见地差。原因通常不在代码而在没分清这套源码到底覆盖了推荐链路里的哪一段只做召回、做了精排还是仅仅是一份数据预处理工具集。先摸清数据流再决定投入多少精力复现。这篇笔记按我实际搭广告推荐系统的工作路径来写先讲物料、日志、样本三张主线再给一套最小可运行的训练命令然后是参数调优与离线评估口径最后是上线前必须排查的坑。适合刚入门、想在本机跑通一套推荐代码的 Python 使用者也适合团队里想快速搭一个广告 CTR 基线、又怕被历史特征坑拖垮的工程师。2. 拆源码前先读懂数据流电商广告推荐的物料、日志与样本主线2.1 物料表与用户侧画像先打通三张表的字段拿到源码包后第一件事不是看模型结构而是找数据。常见的一版 Python 电商广告推荐源码数据入口一般是三张表物料表item/ad、用户表user_id 与画像特征、行为日志曝光、点击、转化。广告推荐和普通商品推荐最大的差别是物料表里除了 item_id、类目、价格还会带广告维度的字段广告主 id、投放计划 id、出价、计费方式。这不只是多几个字段的问题它直接影响召回过滤规则和排序目标——电商推荐可以只猜“用户想不想买”广告推荐还要在猜意愿的同时考虑这条广告能不能带来 GMV、会不会超预算。先把下面这几类字段对齐后面所有特征才能拼得上。字段命名在不同源码包里可能差别很大但语义基本是这几类表关键字段广告场景额外字段物料表item_id, cate_id, title, price, image_featuread_id, campaign_id, 出价 bid_price, 计费方式用户表user_id, age, gender, 消费力分档用户历史 CTR、大促行为标记行为日志request_id, user_id, item_id, 场景, 时间戳曝光位置 position, 点击标签, 转化标签建表时我习惯把 request_id 设成会话级主键之一。一次请求里一个用户会被推很多物料不带这个标识后面做样本去重会非常痛苦。一张最简的曝光点击日志表可以这样建CREATE TABLE ad_expose_log ( request_id VARCHAR(64) COMMENT 一次请求唯一 id, user_id BIGINT COMMENT 用户 id, item_id BIGINT COMMENT 物料 id, ad_id BIGINT COMMENT 广告 id, position INT COMMENT 曝光位置, 0 开始, expose_time TIMESTAMP COMMENT 曝光时间, click_flag TINYINT COMMENT 1 点击 0 未点击, pay_flag TINYINT COMMENT 转化标记, 默认 0 ) PARTITIONED BY (dt STRING);参数说明click_flag 和 pay_flag 分开是为了既能训 CTR 模型又能训 CVR 模型position 一定要留广告场景位置偏差很大排序模型里它既是特征又是评估时要控制的分组维度。如果源码包里只有 item 之间的相似度矩阵没有这张日志表那它大概率是个简化 demo别指望直接上线。数据做齐有两个路径一是找公开的电商点击日志数据集二是自己写 Python 爬虫从允许采集的页面拿脱敏后的行为数据。后者要花大量时间在去重和字段清洗上我建议第一版先用公开数据集把链路跑通后面再替换成业务数据。这里有个血泪经验物料表、用户表、日志表三者的 user_id / item_id 编码必须一致很多包内自带的数据集是分别从不同地方采集的ID 体系对不上 join 出来全空。2.2 曝光日志与点击日志 join 成样本为什么不直接拿未曝光物料当负样本广告推荐训练样本的标准做法是“曝光日志 left join 点击日志”曝光过且没有点击的是负样本点击过的是正样本。很多新手图省事把全量物料表里没被点击过的商品直接当负样本这会引入严重的采样偏差——本来没被曝光过、用户根本没见过的东西被模型学成了“用户不喜欢”。这个偏差在离线阶段会被整体 AUC 掩盖一上线就露馅。先看最小实现import pandas as pd expose pd.read_csv(expose_log.csv, parse_dates[expose_time]) click pd.read_csv(click_log.csv, parse_dates[click_time]) # 一次请求用户物料粒度去重避免重复曝光重复计数 expose expose.drop_duplicates([request_id, user_id, item_id]) # 点击日志只保留那条曝光请求里的点击记录 click[click_flag] 1 samples expose.merge( click[[request_id, user_id, item_id, click_flag]], on[request_id, user_id, item_id], howleft ) samples[click_flag] samples[click_flag].fillna(0).astype(int)逻辑说明以曝光表为基表把点击标记通过 merge 回填到对应曝光样本上。merge 之后没匹配上的行就是“曝光但未点击”用 fillna(0) 统一置为负样本。这里两个关键点一是 merge 的 key 必须带上 request_id否则同一个用户在同一次请求里的多个曝光位会错误合并二是曝光表最好先按 request_id user_id item_id 去重防止一次请求里同一物料被多次曝光导致权重失真。负样本也不是越多越好。点击率通常在 1%5%全量负样本进训练会让模型偏向预测低分。业界常见的做法是负采样把正负比控制在 1:2 到 1:5。更进阶的做法是难负样本挖掘从召回结果里挑那些“模型给分高、但用户实际没点”的物料作为额外负样本。这种样本能让模型学会区分相似物料而不是只学“曝光过的就点”。实现上就是在样本集里追加一个 is_hard_negative 标记训练时给它更高采样权重。2.3 多路召回与候选合并item2item 协同过滤的最小实现排序模型只对候选集排序候选集由召回层给。大部分 Python 推荐源码包里最容易跑通的是 item2item 协同过滤两个商品被同一批用户点击过就认为它们相似。广告场景下物品就是广告或商品下面这段代码用共现矩阵做召回from collections import defaultdict # click_df: user_id, item_id 两列的点击记录 user_items click_df.groupby(user_id)[item_id].apply(list) cooccur defaultdict(lambda: defaultdict(int)) for items in user_items: for i in range(len(items)): for j in range(i 1, len(items)): a, b items[i], items[j] cooccur[a][b] 1 cooccur[b][a] 1 def recall_by_similar(user_history, topk20): scores defaultdict(float) for item in user_history: for sim_item, cnt in cooccur[item].items(): if sim_item not in user_history: scores[sim_item] cnt # 共现次数即相似度 return sorted(scores.items(), keylambda x: -x[1])[:topk]逻辑说明先按用户聚合点击物品序列再对每个用户序列里任意两两物品累加共现次数。召回的相似度就是共现次数这是最简单的一版再往后可以换成余弦相似度、皮尔逊系数或者用向量召回。回到“多路召回”这个词广告系统通常会同时跑热门召回、item2item、同店召回、向量召回然后把多路结果按加权分数合并去重。合并时有两件事必须做过滤用户已经看过的 item过滤已下架、预算耗尽或不在投放周期的广告物料否则排序模型会把算力浪费在根本不可能展示的候选上。2.4 广告候选集的过滤规则预算、频控与合规广告推荐和自然推荐最大的区别在于排序之外还有一层广告投放约束。候选集进入排序模型前要先过规则过滤器广告主预算是否耗尽、是否在定向人群内、当天投放频控是否超限、物料是否涉及敏感品类。这层过滤通常在召回后、精排前用专为筛选设计的离线配置表做交集。下面是一个伪代码级的过滤逻辑def filter_candidates(cands, user_profile, ad_config): valid [] for item_id in cands: ad ad_config.get(item_id) if not ad or ad[status] ! running: continue if ad[budget_left] 0: continue if user_profile[uid] in ad[negative_audience]: continue if ad[freq_cap] and user_profile[freq][item_id] ad[freq_cap]: continue valid.append(item_id) return valid参数说明budget_left 用离线缓存每分钟同步一次freq_cap 是频控上限防止同一个广告对同一个用户展示太多次negative_audience 是广告主排除的人群。这层逻辑如果源码包里没写你接真实业务时必须自己补上否则模型评分再高广告系统也投放不出去。3. 把源码跑通的落地路径解压、环境与最小训练命令3.1 zip 解压与 Python 环境先解决两个最基础的拦路虎拿到源码包第一步不是急着写代码而是干净利落地把它解压出来。常见做法是在 Linux 上创建独立项目目录mkdir -p ~/ad_reco cd ~/ad_reco unzip Python电商广告推荐系统源码.zip -d ./src ls ./srcunzip的-d参数指定解压目标目录避免文件直接散落在当前目录。这里有两个高频坑。一是包名是中文一些老版本 unzip 在 Windows 上解压会出现文件名乱码建议统一在 Linux 上解压用ls -b查看转义后的真实文件名。二是 zip 伪加密解压时提示需要密码但用 7-Zip 打开却能正常读取。这是压缩包把加密标志位置成了 1文件内容其实没有加密。Windows 上用 7-Zip 菜单里的“解压”常常能绕过去服务器上可以试试7z x 包名.zip -p 传空密码或者用 7-Zip 重新压缩一遍。至于 Python 环境linux 系统安装 python 的方法在各类 python 安装教程里讲得很多但我要提醒一句不要动系统默认解释器。直接用 conda 或 pyenv 固定版本conda create -n ad_reco python3.10 -y conda activate ad_reco pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple依赖安装用国内镜像源主要是为了下载速度。很多源码包 requirements.txt 里会锁 pandas、numpy、scikit-learn 的版本区间如果直接安装报依赖冲突优先用python -m pip install --upgrade pip升级 pip 再装。这个阶段最常见的失败不是算法问题而是 numpy 版本和 Python 版本不匹配报错会指向某个 .so 文件无法导入。别去改代码先重建虚拟环境。3.2 原始点击日志到特征向量处理脚本拆解不管源码里的模型是 LR、GBDT 还是 DeepFM输入基本都是“用户特征 物料特征 上下文特征”拼成的向量。下面这段脚本把上一章的样本表转成模型可用的特征import pandas as pd from sklearn.preprocessing import LabelEncoder samples pd.read_csv(samples.csv) # 离散特征分桶编码user_id 类高基数特征直接 hash 分桶 def hash_bucket(val, bucket10000): return int(abs(hash(str(val))) % bucket) samples[user_bucket] samples[user_id].map(lambda x: hash_bucket(x, 100000)) samples[item_bucket] samples[item_id].map(lambda x: hash_bucket(x, 200000)) # 类目与位置这类中低基数特征用 LabelEncoder 保持连续编号 for col in [cate_id, position]: enc LabelEncoder() samples[col _enc] enc.fit_transform(samples[col].astype(str)) # 保存 encoder训练和预测需用同一个 # joblib.dump(enc, fenc_{col}.pkl) # 连续特征做 min-max 归一化到 [0,1] for col in [price, user_ctr]: max_val samples[col].max() samples[col _norm] samples[col] / max_val features samples[ [user_bucket, item_bucket, cate_id_enc, position_enc, price_norm, user_ctr_norm] ].values labels samples[click_flag].values逻辑说明hash_bucket 是把 user_id、item_id 这类高基数 ID 映射成固定桶号的做法内存可控且不依赖训练集全部 ID。注意这里有一个隐藏坑Python 内置hash()对字符串有随机盐同一个 user_id 在不同进程里哈希结果可能不一样会导致训练和预测特征错位。实际项目里应该用 hashlib 的 md5我在第 4 章会给稳定版本。LabelEncoder 适合类目这类取值少而稳定的字段训练和预测必须使用同一个 encoder所以要 joblib 保存。连续特征归一化我用了 min-max因为广告 CTR 模型里价格、点击率的量纲差异很大直接用原始值会让梯度更新不稳定。归一化参数同样要保存线上预测不能用重新统计的 max 值。3.3 训练一版可用的 CTR 模型LR 起步、树模型兜底源码包里如果是深度学习模型别急着训 DeepFM先用一个简单的逻辑回归把数据流跑通确认特征管道没问题再换复杂模型。逻辑回归在广告点击率预估里至今仍是工业界的强基线from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score # 按时间切分不随机切分前 7 天训练第 8 天评估 train_df samples[samples[day] 7] test_df samples[samples[day] 8] clf LogisticRegression(C1.0, solverliblinear, max_iter100) clf.fit(train_df[feat_cols], train_df[click_flag]) test_pred clf.predict_proba(test_df[feat_cols])[:, 1] auc roc_auc_score(test_df[click_flag], test_pred) print(ftest auc{auc:.4f})逻辑说明按时间切分是 CTR 模型与普通分类任务的最大差别随机切分会把未来信息泄漏进训练集得到的 AUC 没有任何上线参考价值。liblinear 求解器在小数据集上比 lbfgs 更快更稳C 是正则强度的倒数广告数据通常特征极稀疏C 设在 1.0 附近即可后面再用验证集调。如果 LR 的 AUC 只有 0.5 出头多半是特征管道有问题而不是模型不行此时换 LightGBM 也只是用树的非线性把 bug 掩盖住。跑通基线之后把模型文件保存下来import joblib joblib.dump(clf, ./model/lr_baseline.pkl)这个模型文件要和特征 encoder 放在同一目录我后面会讲为什么不这样做会翻车。3.4 特征有效性验证单特征 AUC 与分桶统计训练完第一版模型后不要急着调参先做一次特征有效性验证。做法很简单每个特征单独训练一个 LR看单特征 AUC。AUC 接近 0.5 说明这个特征没有区分度AUC 低于 0.45 说明特征方向可能反了需要检查是不是标签写错。另一种更鲁棒的方式是分桶统计把连续特征按分位数切成 10 个桶看每个桶里的真实点击率是否单调。如果点击率随特征值上升反而下降说明特征编码方式或符号有问题。import numpy as np def bucket_ctr_check(series, label, bins10): cut pd.qcut(series, qbins, duplicatesdrop) stats pd.DataFrame({feature: series, label: label}) grouped stats.groupby(cut, observedTrue)[label].agg([mean, count]) return grouped这个函数会输出每个桶的 CTR 和样本量。样本量为 0 的桶说明分位点有大量重复值特征需要修正CTR 单调性是判断特征鲁棒性的关键。这一步看着基础但能省下后面大量调参时间因为很多“离线指标玄学”问题都出在特征本身是脏的。4. 影响广告推荐效果的参数负采样、embedding 维度与评估口径4.1 负采样率广告场景为什么不能把全量负样本直接堆进去标准的曝光日志里真实 CTR 通常在 1%5% 量级负样本远多于正样本。如果把全量负样本直接拿进训练模型会倾向把所有候选都预测成低点击率而且内存占用巨大。我一般把正负样本比控制在 1:2 到 1:5具体做法是从负样本里随机采样而不是丢弃正样本。不同采样率的实测表现如下负采样率正负比训练速度AUC 表现线上风险全量负样本约 1:20慢通常偏低无明显1/2 采样约 1:10较快回升需校准1/4 采样约 1:5快较好需校准1/10 采样约 1:2最快表面最高预测分虚高注意最后一行采样率过低时离线 AUC 常常虚高因为负样本被抽得太少正样本占比被人为抬高模型输出的概率已经不是真实点击率。线上使用前必须对预测分数做校准常见做法是保序回归或 Platt scaling。这也是“离线指标涨、线上 CTR 不动”最常见的根源之一模型学到的只是采样空间里的相对顺序不是真实概率。负采样实现时还有一个容易被忽略的规则采样要按用户和时间分层而不是全表随机。否则高活跃用户的负样本被大量抽样低活跃用户的负样本被稀释模型对不同用户的区分度不一致。落地的采样策略写出来很简单就是先按 user_id 分组每组内按固定比例采样负样本。4.2 embedding 维度与特征哈希离散特征的处理尺度广告推荐模型里 ID 特征最常规的编码方式是 embedding。embedding 维度没有标准答案工程上常用简化公式估算def calc_embed_dim(unique_count): return max(8, min(100, int(6 * pow(unique_count, 0.25))))比如 item_id 有 50 万种取值算出来大约 6 * 26.5 159 的量级但为了省显存一般直接截到 32 或 64。维度太小容易欠拟合维度太大在广告海量物料下内存爆炸而且出现次数少的 token 根本学不饱。经验值百万级 item 表embedding 维度 32 到 64 足够十亿级行为序列场景才需要上 128。上一章提到的 hash_bucket 在这里要换成稳定版本import hashlib def hash_feature(value, bucket100000): return int(hashlib.md5(str(value).encode()).hexdigest()[:8], 16) % bucket这里用 md5 而不是内置 hash原因是 Python 内置 hash 对字符串有随机盐训练进程和预测进程的哈希结果可能不一致会造成线上特征错位。桶数设置上我通常把 user 桶数设为用户量的 4 倍左右item 桶数设为物料量的 8 倍因为物料侧新增频率高桶数给多一点能降低碰撞。桶数太大会让交叉特征失去泛化能力太小会让不同 ID 落到同一桶特征失去区分度。4.3 离线评估AUC、GAUC 与业务指标的对齐单看全局 AUC 是广告推荐评估中最容易自欺欺人的做法。不同用户的点击率基线不同把所有人的预测混在一起算 AUC会被高 CTR 用户主导。GAUCGroup AUC按用户分组计算 AUC再按曝光数加权平均才是排序场景下的主流离线指标from sklearn.metrics import roc_auc_score def gauc(y_true, y_pred, users, exposures): total_w 0.0 total_auc 0.0 for u in set(users): mask users u if mask.sum() 2 or len(set(y_true[mask])) 2: continue w exposures[mask].sum() total_auc roc_auc_score(y_true[mask], y_pred[mask]) * w total_w w return total_auc / total_w参数说明组内样本数少于 2 或 y_true 只有一个类别时roc_auc_score 会直接报错所以要跳过这些用户曝光权重用次数是因为同一用户的多条曝光样本间有相关性简单平均会高估指标。另外一定要看覆盖率和冷启动命中率如果 AUC 很高但系统永远推头部热销品这模型没有实用价值。广告场景还有个容易被忽略的口径按 position 分别统计 AUC。位置靠前的样本点击率高模型区分度天然更高不要拿全量 position 混在一起谈效果。4.4 增量更新与模型热加载新广告怎么进候选广告物料是动态的商家今天上架明天就希望进入推荐候选。离线全量重训通常一天一次但增量物料需要被召回层快速感知。最常见的轻量方案是召回侧把新物料先塞进热门召回或类目召回兜底排序侧用统一特征字典把新 item_id 哈希到已有桶里模型无需重训就能给分。等每日全量重训时这些新 ID 才真正进入 embedding 表和统计特征。增量层代码往往是一段离线调度脚本# 每小时做一次增量样本合并再触发一次小规模微调 python merge_incr_log.py --dt $(date -d -1 hour %Y%m%d%H) python train_ctr.py --mode finetune --base_model ./model/lr_baseline.pkl这里 train_ctr.py 的 finetune 模式会在原有模型参数基础上继续拟合新数据而不是从零训练。广告侧的教训是不要对大规模模型频繁做增量否则旧记忆会快速被新分布冲掉导致 CTR 波动。更稳的是“小时级增量样本 天级全量重训”模型文件通过版本号切换回滚也容易。我习惯把模型文件名带上日期比如 lr_20241107.pkl线上配置指向软链 latest发布后保留上一个版本的软链作为后悔药。5. 广告推荐系统排查手册5 个让模型失效的高频坑5.1 离线 AUC 高、线上 CTR 不涨先怀疑特征穿越现象训练集 AUC 0.78看起来模型很强可上线 A/B 实验点击率反而下降。原因样本里用了点击或转化之后才能拿到的信息最典型的是把“用户当天是否购买”或者“该商品未来 7 天销量”拼进特征。训练时这些字段天然存在线上预测发生时未来还没发生只能用历史版本值特征分布一错位模型就废了。解决逐字段检查逻辑时间戳所有特征只允许使用预测时刻之前的数据用asof join或把特征表按日期分区严格保证feature_dt sample_dt。这条应该写进每次特征迭代的 code review 清单。还有一个自查技巧把预测分为按样本时间倒序排列看 AUC 是否随时间下降如果深夜样本 AUC 显著低于白天多半是日常统计特征里混入了当天全局数据。5.2 正负样本失衡模型训练震荡现象loss 反复上下跳动AUC 在 0.5 附近波动或者预测分全部集中在 0.05 以下。原因曝光日志里真实点击率只有 2% 左右全量负样本进训练模型学到的几乎全是负类先验再加上 batch 太小每个 batch 里可能一个正样本都没有梯度方向乱跳。解决按 4.1 的负采样表把正负比拉到 1:3 左右训练时对 batch 做正样本加权同时检查曝光日志是否有反爬或机器人流量污染把同一 IP 高频曝光但从不点击的样本过滤掉。判断流量质量有个土办法看曝光量与点击量的比值是否在合理区间内如果某些 user_id 曝光上千次点击为 0直接剔除。5.3 线上特征与训练特征对不齐字典文件没同步现象模型文件重启加载后分数整体偏移新旧模型在同一批特征上打分差异巨大。原因训练侧的 LabelEncoder、hash 桶数、归一化 min/max 参数存在训练机本地线上服务加载的是另一份旧字典或者新增了一种渠道来源字段训练侧自动纳入了特征列表线上侧还在用旧特征拼装。解决把编码器和归一化参数随模型一起打包用统一的 feature_pipeline.pkl 加载上线前做“训练特征 vs 线上特征”字段交集校验少一个字段就 fail-fast。Python 侧一个简单校验是assert set(train_cols) set(serving_cols), ffeatures mismatch: {set(train_cols) ^ set(serving_cols)}用 assert 卡住发布流程能避免大多数“黑匣子一样说不清”的线上问题。这个坑在源码包二次开发时特别常见因为包内可能没有线上服务代码你自己写服务时必须保证从样本构造到特征编码全链路复用同一套代码而不是在线上一行一行重写。5.4 zip 伪加密与文件损坏导致源码缺文件现象解压时报“需要密码”用 7-Zip 打开却正常或者解压后运行python train.py报 ModuleNotFoundError提示 data 目录下某个 csv 不存在。原因这类源码 zip 常见伪加密标志或压缩包在下载过程中部分损坏zip 的 central directory 索引错乱某些文件实际没被展开。解决Linux 上先执行完整性测试unzip -t Python电商广告推荐系统源码.zip-t会逐文件测试 CRC 校验输出 ok 才算完整。伪加密用7z x 包名.zip -p 尝试空密码绕过或者用 7-Zip 重新打包。解压后对照包内 md5 校验清单逐文件比对如果包内没有校验清单至少确保 train.py、model.py、data/ 目录都真实存在再开始配环境。这个坑很基础但我见过有人折腾了半天环境最后发现是压缩包本身缺了 60% 的文件。5.5 时间窗口切错评估结果虚高现象用随机切分训练测试集AUC 0.82 看着漂亮上线后效果差到不可用。原因推荐是时间序列问题随机切分会让同一天的样本同时出现在训练和测试集模型相当于在“开卷考试”里见过答案。解决严格按自然日切分比如前 6 天训练、第 7 天测试评估时还要过滤掉训练集中已出现过的 item单独统计冷启动召回命中率。冷启动命中率低说明模型依赖记忆而非泛化这时候就算整体 AUC 再高也扛不住新广告上架。我一般把冷启动评估单独出一版报告训练集里没有出现过的 item在测试集里被模型排进 top20 的比例是多少这个指标低于 5% 就要重点修召回和特征。6. 把模型变成接口最后的增量更新、缓存与 A/B 验证模型训练完剩下最关键的是让它以接口形式对外服务。我不建议新项目一上来就上重型推理框架先用一个 Python 进程把模型和特征管道装好压测通过再迁移。常见做法是 FastAPI 加载模型请求进来先查 Redis 缓存命中直接返回未命中再走特征拼装和预测import joblib from fastapi import FastAPI app FastAPI() model joblib.load(./model/lr_baseline.pkl) encoders joblib.load(./model/feature_pipeline.pkl) app.post(/predict) def predict(req: dict): vec encoders.transform(req[features]) # 复用训练侧同一套编码器 prob model.predict_proba(vec)[0, 1] return {ad_score: float(prob)}这个接口的核心是 encoders 必须和训练时是同一个对象。我习惯把它和模型打成同一个 tar 包发布避免线上“模型是新的、编码器是旧的”这类事故。缓存建议只缓存热门物料和头部用户的预测结果TTL 设 5 分钟以内因为广告出价和预算变化很快缓存时间太长会导致超预算。上线后先切 5% 流量做 A/B观察 CTR、GMV 和广告消耗三个指标至少跑满 3 天再决定全量。召回侧的新物料要记得投进热门兜底否则新广告永远没有曝光机会模型也就学不到它们。我会在每周排查里保留一个习惯把离线预测分和线上日志里的实际点击分布画在一张图上分布漂移超过 20% 就该考虑重训或回滚。这一整套链路跑顺之后再回头折腾 DeepFM 和向量召回才不亏。希望帮到你。本文还有配套的精品资源点击获取