notebook 里 AUC 0.92 的模型,离产生业务价值还差一条完整的服役链路:序列化、接口化、容器化、监控。**模型部署的本质是把”一次实验”变成”一个服务”**:有版本、有契约、有容量、有告警。本文用 scikit-learn + FastAPI + Docker 走通最小生产链路。

前置准备

以一个已训练好的分类模型为例,安装服务化依赖:

pip install scikit-learn fastapi uvicorn joblib pydantic
  • joblib:sklearn 模型序列化事实标准
  • pydantic:定义请求/响应契约,接口文档自动生成

步骤一:模型产物与元数据一起登记

只存 .pkl 是事故源头——半年后没人知道它用哪些特征、什么版本训练:

import joblib, json, sklearn

artifact = {
"model": model,
"feature_order": feature_cols, # 特征顺序契约
"sklearn_version": sklearn.__version__,
"metrics": {"auc": 0.92, "val_size": 5000},
"trained_at": "2026-10-13",
}
joblib.dump(artifact, "model_v3.joblib")
  • 文件名带版本号(v3),新模型不覆盖旧文件——回滚即换文件
  • feature_order 是关键契约:线上特征必须按训练顺序组装

步骤二:FastAPI 包装推理接口

用 pydantic 把入参校验前置,非法请求不进模型:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import joblib, numpy as np

ART = joblib.load("model_v3.joblib") # 仅加载自有可信产物,见"常见问题"安全条
app = FastAPI(version="3")

class In(BaseModel):
features: dict[str, float]

@app.get("/healthz")
def healthz():
return {"model": "v3", "ok": True}

@app.post("/predict")
def predict(inp: In):
try:
x = np.array([[inp.features[c] for c in ART["feature_order"]]])
except KeyError as e:
raise HTTPException(422, f"缺少特征: {e}")
return {"score": float(ART["model"].predict_proba(x)[0, 1])}

本地起服务验证:uvicorn serve:app --port 8000,打开 /docs 即得交互文档。

步骤三:容器化固化运行环境

“我本地能跑”的解药是镜像:Python 版本、依赖版本与模型文件一起冻结:

FROM python:3.11-slim
WORKDIR /srv
COPY requirements.txt model_v3.joblib serve.py ./
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 8000
CMD ["uvicorn", "serve:app", "--host", "0.0.0.0", "--port", "8000"]
docker build -t churn-model:v3 . && docker run -p 8000:8000 churn-model:v3

镜像 tag 与模型版本对齐(v3),部署平台按 tag 发布与回滚。

步骤四:上线监控与漂移检测

服务活着 ≠ 模型好用,监控要覆盖三层:

  • 服务层:QPS、p99 延迟、错误率(/healthz 供探针)
  • 输入层:特征分布 PSI > 0.2 告警——上游埋点改了的典型信号
  • 输出层:预测分数分布漂移、正例率突变;无标签期用代理指标(如人工抽检一致率)
def psi(expected, actual, bins=10):
edges = np.quantile(expected, np.linspace(0, 1, bins + 1))
e, a = np.histogram(expected, edges)[0] / len(expected), np.histogram(actual, edges)[0] / len(actual)
return float(np.sum((a - e) * np.log((a + 1e-6) / (e + 1e-6))))

常见问题

  • 线上离线特征不一致(training-serving skew):特征组装逻辑抽成共享模块,训练与 serving 同代码路径
  • sklearn 版本升级后加载报错:镜像冻结版本;升级 = 新模型重训 + 新镜像,不做原地升级
  • 冷启动慢:模型加载放启动阶段(lifespan),就绪探针通过后才接流量
  • 批量预测需求:另开 /predict_batch 接口,复用同一模型实例,注意限流防 OOM
  • 反序列化安全红线:joblib/pickle 加载时会执行对象构造代码,等价于运行任意程序——只加载自己训练管线产出、存放于可信仓库的产物;对外交换的模型数据优先用 ONNX 等不带代码执行的格式,并校验文件哈希/签名

小结

模型服务化四步:产物带元数据登记(可追溯)、接口带契约校验(可调用)、镜像固化环境(可回滚)、三层监控(可运营)。链路搭好后,”上线一个新模型”就退化为一次配置变更:换镜像 tag、跑回归、灰度、全量——模型迭代速度取决于这条链路的自动化程度,而不是训练脚本。