🏠 总目录📚 本教程 15 · 工程落地
📑 本页目录(点开跳转)

15 · 工程落地

25 分钟 | ⭐ 核心(想做这行的必看)


🎯 一句话

推荐系统 80% 的工作量不在模型,在工程。 模型是那个 5% 的亮点,剩下 95% 是让它每天稳定地、低延迟地、不出事故地服务几亿人。


🏗️ 全景架构图

① 在线服务链路(毫秒级)App / Web网关推荐服务P99 端到端预算 < 200ms任何一环超时即降级到兜底特征服务Feature StoreRedis / KV召回服务ANN / 倒排索引Faiss / Milvus排序服务模型推理TF Serving / Triton / ONNX埋点 / 曝光日志(异步)② 数据链路(分钟 ~ 天级)客户端埋点KafkaFlink 实时处理实时特征 (Redis)实时特征回灌在线HDFS / S3 数据湖Spark 离线处理离线特征表训练样本③ 训练链路天级全量训练模型验证一致性校验灰度全量上线小时级增量训练 ── 汇入同一条验证流水线实时训练(部分公司):Kafka → 在线学习 → 分钟级模型更新↑ 新模型回到排序服务
三条链路各有各的时延量级:在线以毫秒计,数据以分钟到天计,训练以小时到天计;它们首尾相接构成一个闭环 —— 线上服务产生日志,日志喂出模型,模型再回到线上。

🧊 组件一:特征平台(Feature Store)⭐

它解决的问题(第 7 节的坑 2):离线算特征用 Spark,在线算特征用 Java,两边逻辑对不上 → 效果莫名其妙地差。

一份特征定义同一份 DSL / 配置,只写一次离线路径 · Spark 批处理在线路径 · Flink 流处理训练样本表 (Hive)特征缓存 (Redis)模型训练模型推理口径必须一致⭐ 同一份定义产出两条路径 —— 离线 / 在线口径天然对齐
要看的不是这两条路各有几步,而是它们共用同一个源头:定义只写一次,离线训练和在线推理就不会算出两套不一样的特征。

开源选择: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 预取 物品 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 早几小时报警,阈值怎么定

✅ 检查点

  1. 特征平台解决什么问题?
  2. 端到端超时了有哪几级降级?最后的兜底是什么?
  3. 为什么说「特征快照」是最容易做错的一件事?正确做法是什么?
  4. 最有价值的模型层监控指标是什么?为什么?
  5. 上线流程里的「一致性校验」怎么做?
  6. 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

打卡记录保存在你的浏览器里,首页能看到总进度