📑 本页目录(点开跳转)
15 · 工程落地
⏱ 25 分钟 | ⭐ 核心(想做这行的必看)
🎯 一句话
推荐系统 80% 的工作量不在模型,在工程。 模型是那个 5% 的亮点,剩下 95% 是让它每天稳定地、低延迟地、不出事故地服务几亿人。
🏗️ 全景架构图
🧊 组件一:特征平台(Feature Store)⭐
它解决的问题(第 7 节的坑 2):离线算特征用 Spark,在线算特征用 Java,两边逻辑对不上 → 效果莫名其妙地差。
开源选择:Feast、Tecton、Hopsworks。中小规模也可以自己用「Spark + Redis + 一份 YAML 定义」实现。
在线特征的存储要求: | 要求 | 数值 | |---|---| | 读延迟 | P99 < 5ms | | QPS | 单请求要读几百个物品的特征 → 批量读接口是必须的 | | 数据量 | 用户特征 亿级 key,物品特征 亿级 key | | 常用方案 | Redis Cluster / Aerospike / 自研 KV |
🔧 性能关键点:一次推荐要读 1000 个物品的特征。 循环读 1000 次 = 死。必须用 MGET / Pipeline 批量读。
⚡ 组件二:在线服务(延迟是生命线)
延迟预算表(真实系统的样子)
| 环节 | 预算 | 超了怎么办 |
|---|---|---|
| 网关 + 路由 | 5ms | — |
| 用户特征读取 | 10ms | 缓存、批量读 |
| 召回(并行) | 30ms | 减少路数、降低 efSearch |
| 物品特征读取 | 20ms | 批量读、本地缓存热门物品 |
| 粗排 | 15ms | 减少候选、简化模型 |
| 精排 | 80ms | 降级:直接用粗排结果 |
| 重排 | 10ms | 简化规则 |
| 总计 | ~170ms | 预算 200ms,留 30ms buffer |
🛡️ 降级策略(必须设计,不是可选项)
def recommend(user_id, n=10, deadline_ms=200):
"""带超时降级的推荐服务骨架"""
t0 = now_ms()
try:
candidates = retrieve(user_id, timeout=remaining(t0, 40, deadline_ms))
except TimeoutError:
return hot_cache.get(user_id) or global_hot_list[:n] # 🛟 兜底1
try:
candidates = pre_rank(candidates, timeout=remaining(t0, 80, deadline_ms))
ranked = rank(candidates, timeout=remaining(t0, 170, deadline_ms))
except TimeoutError:
ranked = candidates # 🛟 兜底2: 跳过精排
return rerank(ranked)[:n]
降级层级(从轻到重): 1. 减少召回路数 / 降低 ANN 精度 2. 跳过精排,用粗排结果 3. 用上一次请求的缓存结果 4. 返回全局热门榜(永远不能返回空)⭐
🚨 真实事故教训:推荐服务挂了返回空列表,用户看到白屏 → 比推荐得不准严重一百倍。 可用性 > 准确性。 这条要刻在脑子里。
🧠 组件三:模型服务
推理优化清单
| 手段 | 收益 | 说明 |
|---|---|---|
| 批量推理 (Batching) | 5–20× | 500 个候选一次前向,别循环 500 次 ⭐ |
| 模型量化 (INT8) | 2–4× | 精度损失通常 < 0.1% AUC |
| Embedding 预取 | 2× | 物品 Embedding 提前放内存/显存 |
| 算子融合 / ONNX / TensorRT | 1.5–3× | 编译期优化 |
| 模型蒸馏 | 5–10× | 大模型教小模型(也用于粗排) |
| 缓存 | 极大 | 热门用户/物品的结果缓存几秒 |
Embedding 表太大怎么办
问题:item_id Embedding = 1 亿 × 64 维 × 4 字节 = 25.6 GB
单机装不下,GPU 显存更装不下
解法:
① 参数服务器 (Parameter Server):Embedding 分片存多台机器
② 哈希分桶:hash(id) % 1000万,接受少量冲突
③ 混合精度:Embedding 用 FP16 甚至 INT8
④ 频次过滤:低频 ID 统一映射到 <UNK>(第 7 节)
⑤ Semantic ID:从根本上缩小词表(第 14 节)⭐
🔄 组件四:训练与更新
三种更新节奏
【天级全量】
每天凌晨用过去 N 天数据完整训练
✅ 效果最稳 ❌ 追不上今天的热点
【小时级增量】 ⭐ 工业界主流
在昨天的全量模型上,用最近 1 小时数据继续训练
✅ 平衡了效果和时效
【分钟级/流式在线学习】
Kafka 消息一到就更新模型
✅ 极致时效(突发热点几分钟内响应)
❌ 容易被脏数据带偏,需要严格监控和回滚机制
样本拼接:一个巨大的工程坑 ⚠️
问题:曝光发生在 t 时刻,点击可能发生在 t+10 分钟
怎么知道这次曝光有没有被点?
⭐ 标准解法:延迟窗口拼接
1. 曝光日志进入"等待队列"
2. 等 30 分钟(业务定),期间关联进来的点击都算正样本
3. 30 分钟后仍没点击 → 标记为负样本,落盘
⚠️ 但转化类目标(购买)可能延迟几天 → 「延迟反馈问题」
解法:延迟反馈建模(Delayed Feedback Model)、
或先当负样本、后续用「正样本重要性加权」修正
样本拼接还要记录: - 请求时刻的特征快照 ⭐(不能事后重新算,否则就是特征穿越!) - 曝光位置(用于位置偏差纠正) - 实验分组(用于分组分析) - 各召回路来源(用于归因)
🔑 「特征快照」是整个系统里最容易做错、后果最严重的一件事。 正确做法:在线推理时把用的特征原样打到日志里,训练时直接用日志里的值。
📊 组件五:监控(没有它你就是瞎子)
三层监控
【系统层】—— 出问题最快发现
· QPS、延迟 P50/P95/P99
· 错误率、超时率
· 各下游服务的可用性
【业务层】—— 用户感受到的
· CTR、时长、留存(分小时看)
· 各召回路的曝光占比 ⭐ 某路突然归零 = 那路挂了
· 兜底策略触发率 ⭐ 这个涨了说明链路有问题
【模型层】—— 最容易被忽略,也最阴险
· 预估分数的分布(均值、方差)⭐⭐
· 特征覆盖率(某个特征突然大量缺失 = 上游数据源挂了)
· 特征分布漂移(PSI 指标)
· 在线/离线打分一致性
🚨 最重要的一个监控:预估分数分布
正常:pCTR 均值稳定在 0.05 左右,波动 ±10%
异常情况和含义:
突然跌到 0.01 → 特征大面积缺失(上游挂了)
突然涨到 0.5 → 模型加载错了 / 特征串了
方差趋近于 0 → 模型退化,所有物品打一样的分
阶跃变化 → 有人上线了新模型(是不是没通知你?)
这个指标能在业务指标(CTR)掉下来之前几小时就报警,是最有价值的早期信号。
🧯 事故手册(真实世界会发生的事)
| 事故 | 表现 | 处理 |
|---|---|---|
| 上游特征源挂了 | 特征覆盖率暴跌,pCTR 分布异常 | 用默认值/上次值兜底,报警 |
| 模型文件损坏/加载失败 | 服务启动失败或打分全 0 | 自动回滚到上一个版本 ⭐ |
| 热点用户/物品打爆缓存 | 少数 key QPS 极高 | 本地缓存 + 限流 |
| ANN 索引重建失败 | 召回结果为空或过时 | 保留上一版索引,双索引热切换 |
| 样本拼接延迟 | 训练数据缺失最近几小时 | 训练任务加数据完整性检查,不达标不训练 |
| 实验配置错误 | 100% 流量跑了实验策略 | 实验平台加流量上限保护 |
| 反馈循环失控 | 某类内容占比一路飙升 | 曝光分布监控 + 自动熔断 |
🔑 黄金规则:每一个上线都必须能在 5 分钟内回滚。 模型、配置、代码、实验开关——全都要有一键回滚。
🚀 上线流程(标准 SOP)
① 离线实验
指标达标(GAUC / Recall@K)+ 分组指标检查
② 一致性校验 ⭐ 必做
取 1000 条线上真实请求 → 离线模型打分 → 对比线上打分
差异 > 1e-5 → 停,先查问题
③ 影子流量(Shadow)
新模型跑真实流量但结果不展示,观察延迟和分数分布
持续 1-2 天
④ 小流量灰度 1%
观察核心指标 + 护栏指标,至少 1 天
⑤ A/B 实验 10-50%
跑满 7-14 天(含完整周末)
统计显著 + 护栏不跌
⑥ 全量 + 保留 1-5% 反转组
持续观察长期效应
每一步都要能停下来回滚。 跳过 ② 和 ③ 是新人最常犯的错误。
🛠️ 技术选型速查(2026)
| 环节 | 常见选择 |
|---|---|
| 消息队列 | Kafka / Pulsar |
| 流处理 | Flink(主流)/ Spark Streaming |
| 批处理 | Spark / Ray |
| 在线 KV | Redis Cluster / Aerospike / 自研 |
| 向量检索 | Faiss / HNSWlib / Milvus / Qdrant / pgvector |
| 模型训练 | PyTorch(主流)/ TensorFlow / 内部框架 |
| 模型服务 | Triton / TorchServe / TF Serving / ONNX Runtime |
| 特征平台 | Feast / Tecton / 自研 |
| 实验平台 | 自研为主 / GrowthBook |
| 调度 | Airflow / Argo |
| 监控 | Prometheus + Grafana |
💡 小团队的最小可用方案: PostgreSQL(+pgvector) + Redis + Python 服务 + 一个 cron 训练脚本。 别一上来就搭 Kafka + Flink + Feast,那是给亿级流量准备的。
🔗 这一章连到哪里
| 去哪 | 为什么 |
|---|---|
| AI 基础设施 20 | 「批量推理 5–20×」的完整版:动态 batching、并发模型、服务化架构怎么把 GPU 喂饱 |
| 上线之后 03 | 本节 SOP 里的影子流量 / 1% 灰度 / 5 分钟回滚,在这里有可直接照做的操作细节 |
| 上线之后 05 | 三层监控的通用版:为什么「预估分数分布」比 CTR 早几小时报警,阈值怎么定 |
✅ 检查点
- 特征平台解决什么问题?
- 端到端超时了有哪几级降级?最后的兜底是什么?
- 为什么说「特征快照」是最容易做错的一件事?正确做法是什么?
- 最有价值的模型层监控指标是什么?为什么?
- 上线流程里的「一致性校验」怎么做?
- Embedding 表 25GB 装不下,有哪些解法?
👀 答案
1. 解决离线训练特征和在线服务特征不一致(Training-Serving Skew)的问题——一份定义产出离线和在线两条路径。 2. ① 减召回路/降 ANN 精度 ② 跳过精排用粗排结果 ③ 用缓存结果 ④ 返回全局热门。永远不能返回空。 3. 因为如果训练时重新计算特征,算出来的是「事后」的值,而线上用的是「当时」的值 → 特征穿越。正确做法:在线推理时把实际使用的特征值原样打进日志,训练时直接读日志。 4. 预估分数的分布(均值/方差)。它能在业务指标掉下来之前几小时就发现异常(特征缺失、模型加载错误、模型退化)。 5. 取 1000 条线上真实请求,用离线模型跑一遍,对比线上打分,差异 > 1e-5 就说明有问题。 6. 参数服务器分片、哈希分桶、混合精度(FP16/INT8)、低频 ID 过滤、Semantic ID 缩小词表。🛑 可以停在这里
⚡ 走神救援
工程 80%:特征平台(解决离线在线不一致)、在线服务(200ms 预算 + 多级降级,绝不返回空)、模型服务(批量推理是最大优化)、训练更新(天级全量+小时级增量,样本拼接要存特征快照防穿越)、监控(预估分数分布是最有价值的早期信号)。上线 SOP:离线→一致性校验→影子→1%灰度→AB→全量+反转组。每步能 5 分钟回滚。⭐ 如果只记一句,记这句:可用性 > 准确性。降级要一层层设计好——减召回路 → 跳精排用粗排结果 → 用上次缓存 → 返回全局热门榜,永远不能返回空列表:白屏比推得不准严重一百倍。⚠️ 特征快照是全系统最容易做错、后果最严重的一件事。正确做法只有一个:在线推理时把实际用到的特征原样打进日志,训练时直接读日志里的值——事后重算一定会穿越。⭐ 样本拼接靠延迟窗口(曝光先进等待队列,等 30 分钟,期间关联到的点击算正样本,超时才落成负样本);但购买这类转化可能延迟几天,那叫延迟反馈问题,得用延迟反馈建模或「先当负样本、后续正样本重要性加权」修正。日志里还要一并记下曝光位置(纠位置偏差)、实验分组、召回路来源(做归因)。⚠️ 小团队别一上来就搭 Kafka + Flink + Feast——那是给亿级流量准备的;PostgreSQL(+pgvector) + Redis + 一个 cron 训练脚本就够跑很久了。
下一节 👉 16-实战项目.md