- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 1. 引言:生产模型会悄悄退化
- 2. 漂移的种类:变化的是什么
- 3. Evidently AI 架构与核心功能
- 4. Evidently AI 实战用法
- 5. MLflow 模型注册表与监控集成
- 6. 构建自动再训练流水线
- 7. 监控工具对比:Evidently vs NannyML vs WhyLabs vs Alibi Detect
- 8. Grafana/Prometheus 仪表盘搭建
- 9. 运维注意事项
- 10. 故障案例与恢复流程
- 11. 生产监控检查清单
- 12. 参考资料
- 测验

1. 引言:生产模型会悄悄退化
ML 模型的准确率在部署那一刻达到峰值。此后,随着现实世界的变化,预测质量会逐渐下降。问题在于,这种劣化是在没有明确错误的情况下发生的。不会抛出 HTTP 500,日志里不会出现 CRITICAL,服务照常正常响应。只是推荐结果慢慢变得离谱,欺诈检测开始漏掉新模式,需求预测开始与现实脱节。
根据 Google 的研究,生产 ML 系统中发生的故障有 60% 以上并非源于模型代码,而是源于数据相关问题。模型本身并没有坏掉,核心原因在于模型所学习的世界与现实世界之间的差距在不断拉大。
本文将介绍如何把开源监控工具 Evidently AI 与实验/模型管理平台 MLflow 结合起来,在生产环境中持续监控 ML 模型的健康状态、检测漂移,并触发自动再训练流水线。
2. 漂移的种类:变化的是什么
漂移(Drift)指的是模型训练时的数据分布与实际服务(serving)时点的数据分布之间的不一致。漂移根据发生位置与性质,大致可分为三类。
数据漂移(Data Drift,Covariate Shift)
指输入特征的分布发生变化的现象。模型的输入空间 P(X) 会随时间推移。例如,在电商推荐模型中用户年龄段分布发生变化,或者购买品类的占比随季节改变,都属于这种情况。目标变量 Y 与特征 X 之间的关系 P(Y|X) 保持不变,而输入本身的统计特性发生了变化。
概念漂移(Concept Drift)
指特征与目标之间的关系本身发生变化的现象,即 P(Y|X) 发生变化。这比数据漂移更严重,因为对相同的输入而言,正确答案本身变了。典型的例子包括新冠(COVID-19)疫情期间需求预测模型彻底失效、金融欺诈检测中欺诈手法不断演进导致既有模式不再有效等。
预测漂移(Prediction Drift)
指模型输出 P(Y_pred) 的分布发生变化的现象。它可能是输入漂移带来的结果,也可能由模型内部问题独立引发。分类模型中特定类别的预测比例突然失衡、回归模型中预测值的均值或方差发生显著变化,都属于此类。
| 漂移类型 | 变化对象 | 检测难度 | 代表性检测方法 | 再训练紧急度 |
|---|---|---|---|---|
| 数据漂移 | P(X) 输入分布 | 中等 | PSI、KS 检验、Wasserstein | 中等 |
| 概念漂移 | P(Y|X) 关系 | 高 | 性能指标监控、ADWIN | 高 |
| 预测漂移 | P(Y_pred) 输出 | 低 | 输出分布统计、Chi-squared | 视情况而定 |
| 标签漂移 | P(Y) 目标分布 | 中等 | 标签分布比较 | 高 |
3. Evidently AI 架构与核心功能
Evidently AI 是一个用于 ML 模型监控与数据质量校验的开源库。它运行在 Python 原生环境中,内置了 20 多种统计漂移检测方法。
核心组成
- Report:一次性数据分析报告,可以以 HTML、JSON、Python 字典等形式输出,适合探索性分析与调试。
- Test Suite:针对预先定义条件的自动化校验,可集成进 CI/CD 流水线作为数据质量门禁。
- Metric:单项测量指标。系统内置了
DataDriftTable、DatasetSummaryMetric、ColumnCorrelationsMetric等数十种指标。 - Collector/Workspace:Evidently 的服务器模式,将监控结果以时间序列形式存储,并可在仪表盘中查询。
主要漂移检测算法
Evidently 会根据特征类型(数值型/类别型)与数据集规模,自动选择最优的检测算法。
| 算法 | 适用对象 | 原理 | 优点 | 局限 |
|---|---|---|---|---|
| Kolmogorov-Smirnov (KS) | 数值型,小规模 | 累积分布函数的最大差异 | 无需分布假设 | 在大规模数据上过于敏感 |
| Population Stability Index (PSI) | 数值型/类别型 | 两个分布对数比率的加权和 | 行业标准,易于解释 | 对分箱(bin)设置敏感 |
| Wasserstein Distance | 数值型 | 两个分布间的最小搬运成本 | 反映分布形态差异 | 计算成本高 |
| Jensen-Shannon Divergence | 数值型/类别型 | KL 散度的对称版本 | 始终有限、具有对称性 | 对尾部分布变化不敏感 |
| Chi-squared Test | 类别型 | 观测/期望频数差异 | 对类别型数据直观 | 在低频类别上不稳定 |
| Z-test(比例检验) | 类别型,大规模 | 比例差异的标准化 | 对大规模数据高效 | 假设正态近似 |
4. Evidently AI 实战用法
安装与基础设置
# 安装 Evidently AI(包含 MLflow 集成)
# pip install evidently mlflow scikit-learn pandas
import pandas as pd
import numpy as np
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from evidently.report import Report
from evidently.metric_preset import DataDriftPreset, DataQualityPreset
from evidently.metrics import (
DatasetDriftMetric,
DataDriftTable,
ColumnDriftMetric,
)
# 准备参考数据 / 当前数据
data = load_iris(as_frame=True)
df = data.frame
df.columns = ["sepal_length", "sepal_width", "petal_length", "petal_width", "target"]
reference_data = df.sample(frac=0.5, random_state=42)
current_data = df.drop(reference_data.index)
# 生成带有数据漂移的模拟数据
current_drifted = current_data.copy()
current_drifted["sepal_length"] = current_drifted["sepal_length"] + np.random.normal(2.0, 0.5, len(current_drifted))
current_drifted["petal_width"] = current_drifted["petal_width"] * 1.8
# 生成漂移报告
drift_report = Report(metrics=[
DatasetDriftMetric(),
DataDriftTable(),
])
drift_report.run(
reference_data=reference_data,
current_data=current_drifted,
)
# 将结果提取为字典(供程序化使用)
result = drift_report.as_dict()
dataset_drift = result["metrics"][0]["result"]["dataset_drift"]
drift_share = result["metrics"][0]["result"]["share_of_drifted_columns"]
print(f"数据集漂移检测: {dataset_drift}")
print(f"漂移列比例: {drift_share:.2%}")
# 保存为 HTML 报告
drift_report.save_html("drift_report.html")
用 Test Suite 实现自动化数据质量校验
from evidently.test_suite import TestSuite
from evidently.test_preset import DataDriftTestPreset, DataQualityTestPreset
from evidently.tests import (
TestColumnDrift,
TestShareOfDriftedColumns,
TestNumberOfMissingValues,
TestShareOfOutRangeValues,
TestMeanInNSigmas,
)
# 配置数据漂移 + 质量测试套件
monitoring_suite = TestSuite(tests=[
# 漂移测试:全部列中有 30% 以上漂移时判定失败
TestShareOfDriftedColumns(lt=0.3),
# 校验各个关键特征的漂移
TestColumnDrift(column_name="sepal_length"),
TestColumnDrift(column_name="petal_width"),
# 数据质量测试
TestNumberOfMissingValues(eq=0),
# 数值范围校验:sepal_length 需在参考数据 ±3 个标准差之内
TestMeanInNSigmas(column_name="sepal_length", n=3),
])
monitoring_suite.run(
reference_data=reference_data,
current_data=current_drifted,
)
# 以程序化方式检查测试结果
suite_result = monitoring_suite.as_dict()
all_passed = all(
test["status"] == "SUCCESS"
for test in suite_result["tests"]
)
print(f"全部测试是否通过: {all_passed}")
for test in suite_result["tests"]:
status_icon = "PASS" if test["status"] == "SUCCESS" else "FAIL"
print(f" [{status_icon}] {test['name']}: {test['status']}")
# 在 CI/CD 流水线中作为 exit code 使用
if not all_passed:
print("ALERT: 检测到数据漂移或质量异常。需要触发再训练流水线。")
# sys.exit(1) # 在 CI 中判定构建失败
5. MLflow 模型注册表与监控集成
MLflow 提供实验追踪、模型打包与模型注册表功能。通过将 Evidently 的漂移检测结果记录到 MLflow,可以在同一平台上追踪各模型版本的性能历史与漂移状态。
把漂移指标记录到 MLflow
import mlflow
from evidently.report import Report
from evidently.metrics import (
DatasetDriftMetric,
DataDriftTable,
ColumnDriftMetric,
)
import json
from datetime import datetime
# 配置 MLflow 追踪服务器
mlflow.set_tracking_uri("http://mlflow.internal:5000")
mlflow.set_experiment("model-monitoring/fraud-detection-v2")
def log_drift_to_mlflow(
reference_data,
current_data,
model_name: str,
model_version: str,
batch_id: str,
):
"""将漂移分析结果记录到 MLflow 的函数"""
# 生成 Evidently 漂移报告
drift_report = Report(metrics=[
DatasetDriftMetric(),
DataDriftTable(),
])
drift_report.run(
reference_data=reference_data,
current_data=current_data,
)
result = drift_report.as_dict()
drift_result = result["metrics"][0]["result"]
# 以 MLflow Run 的形式记录
with mlflow.start_run(run_name=f"drift-check-{batch_id}") as run:
# 基础漂移指标
mlflow.log_metric("dataset_drift_detected", int(drift_result["dataset_drift"]))
mlflow.log_metric("drifted_columns_share", drift_result["share_of_drifted_columns"])
mlflow.log_metric("number_of_drifted_columns", drift_result["number_of_drifted_columns"])
mlflow.log_metric("total_columns", drift_result["number_of_columns"])
# 记录各列的漂移分数
column_drift = result["metrics"][1]["result"]["drift_by_columns"]
for col_name, col_info in column_drift.items():
safe_col_name = col_name.replace(" ", "_").replace("/", "_")
mlflow.log_metric(
f"drift_score_{safe_col_name}",
col_info.get("drift_score", 0.0),
)
mlflow.log_metric(
f"drift_detected_{safe_col_name}",
int(col_info.get("column_drift", False)),
)
# 以标签形式记录元数据
mlflow.set_tags({
"monitoring.type": "drift_detection",
"monitoring.model_name": model_name,
"monitoring.model_version": model_version,
"monitoring.batch_id": batch_id,
"monitoring.timestamp": datetime.utcnow().isoformat(),
"monitoring.reference_size": str(len(reference_data)),
"monitoring.current_size": str(len(current_data)),
})
# 将 HTML 报告保存为工件
report_path = f"/tmp/drift_report_{batch_id}.html"
drift_report.save_html(report_path)
mlflow.log_artifact(report_path, artifact_path="drift_reports")
# 将 JSON 结果也保存为工件
json_path = f"/tmp/drift_result_{batch_id}.json"
with open(json_path, "w") as f:
json.dump(result, f, indent=2, default=str)
mlflow.log_artifact(json_path, artifact_path="drift_reports")
print(f"漂移结果已记录到 MLflow。Run ID: {run.info.run_id}")
return drift_result["dataset_drift"], drift_result["share_of_drifted_columns"]
# 使用示例
is_drifted, drift_share = log_drift_to_mlflow(
reference_data=reference_data,
current_data=current_drifted,
model_name="fraud-detector",
model_version="3",
batch_id="2026-03-06-batch-001",
)
基于模型注册表别名(Alias)的管理
从 MLflow 2.x 开始,官方推荐使用基于别名(Alias)的模型管理,而不是传统的 Stage(Staging/Production/Archived)方式。可以据此制定一套策略,根据漂移检测结果自动切换模型别名。
from mlflow import MlflowClient
client = MlflowClient(tracking_uri="http://mlflow.internal:5000")
MODEL_NAME = "fraud-detector"
def handle_drift_detection(
is_drifted: bool,
drift_share: float,
model_name: str = MODEL_NAME,
drift_threshold_warn: float = 0.2,
drift_threshold_critical: float = 0.5,
):
"""根据漂移检测结果执行模型注册表操作"""
# 确认当前生产模型版本
try:
prod_version = client.get_model_version_by_alias(model_name, "production")
current_version = prod_version.version
print(f"当前生产模型版本: {current_version}")
except Exception as e:
print(f"查询生产模型别名失败: {e}")
return
if not is_drifted:
print("未检测到漂移。维持当前模型。")
client.set_model_version_tag(
model_name, current_version,
key="last_drift_check",
value="passed",
)
return
if drift_share >= drift_threshold_critical:
# 临界漂移:立即切换到 fallback 模型 + 触发再训练
print(f"CRITICAL: 漂移比例 {drift_share:.1%} - 切换 fallback 模型并触发再训练")
client.set_model_version_tag(
model_name, current_version,
key="drift_status", value="critical",
)
# 如果存在 fallback 模型,则切换
try:
fallback = client.get_model_version_by_alias(model_name, "fallback")
client.set_registered_model_alias(model_name, "production", fallback.version)
print(f"已切换到 fallback 模型版本 {fallback.version}")
except Exception:
print("WARNING: 没有可用的 fallback 模型。需在维持当前模型的同时进行紧急再训练。")
# 触发再训练(调用外部系统)
trigger_retraining(model_name, reason="critical_drift")
elif drift_share >= drift_threshold_warn:
# 警告级别漂移:记录标签 + 告警
print(f"WARNING: 漂移比例 {drift_share:.1%} - 加强监控并安排再训练")
client.set_model_version_tag(
model_name, current_version,
key="drift_status", value="warning",
)
# 加入调度再训练队列
schedule_retraining(model_name, priority="normal")
def trigger_retraining(model_name: str, reason: str):
"""触发紧急再训练(调用 Airflow DAG、Kubeflow Pipeline 等)"""
print(f"再训练已触发: model={model_name}, reason={reason}")
# requests.post("http://airflow.internal/api/v1/dags/retrain/dagRuns", ...)
def schedule_retraining(model_name: str, priority: str):
"""注册到调度再训练队列"""
print(f"再训练调度已注册: model={model_name}, priority={priority}")
# 执行
handle_drift_detection(
is_drifted=True,
drift_share=0.55,
model_name=MODEL_NAME,
)
6. 构建自动再训练流水线
从漂移检测到再训练的自动化流水线,由以下几个阶段构成。
整体流水线流程
- 调度器:在批量推理之后,或按固定周期(日/周)触发漂移检查
- 漂移分析器:用 Evidently 对参考数据与当前数据进行分析
- 判断引擎:基于漂移阈值决定是否需要再训练
- 再训练编排器:在 Airflow/Kubeflow 上执行训练 Job
- Champion/Challenger 评估:将新模型与现有模型进行对比评估
- 部署门禁:性能达标时自动部署,未达标时回滚
与 Airflow DAG 的集成模式
# Airflow DAG 示例:漂移检查 + 条件式再训练
# dag_drift_monitor.py
from airflow import DAG
from airflow.operators.python import PythonOperator, BranchPythonOperator
from airflow.operators.empty import EmptyOperator
from airflow.utils.dates import days_ago
from datetime import timedelta
import pandas as pd
default_args = {
"owner": "ml-platform",
"retries": 2,
"retry_delay": timedelta(minutes=5),
"execution_timeout": timedelta(minutes=30),
}
dag = DAG(
dag_id="ml_drift_monitor_fraud_detection",
default_args=default_args,
description="每日漂移监控与条件式再训练",
schedule_interval="0 6 * * *", # 每天早上 6 点
start_date=days_ago(1),
catchup=False,
tags=["ml-monitoring", "drift-detection"],
)
def fetch_data(**context):
"""加载参考数据与最近 24 小时的服务数据"""
from sqlalchemy import create_engine
engine = create_engine("postgresql://reader:password@db.internal/features")
reference = pd.read_sql(
"SELECT * FROM fraud_features_reference", engine
)
current = pd.read_sql(
"""SELECT * FROM fraud_features_serving
WHERE created_at >= NOW() - INTERVAL '24 hours'""",
engine,
)
# 通过 XCom 传递路径(大体量数据存放在 S3)
ref_path = "/tmp/reference_data.parquet"
cur_path = "/tmp/current_data.parquet"
reference.to_parquet(ref_path)
current.to_parquet(cur_path)
context["ti"].xcom_push(key="reference_path", value=ref_path)
context["ti"].xcom_push(key="current_path", value=cur_path)
context["ti"].xcom_push(key="current_size", value=len(current))
def run_drift_check(**context):
"""执行 Evidently 漂移分析并记录到 MLflow"""
from evidently.report import Report
from evidently.metrics import DatasetDriftMetric, DataDriftTable
import mlflow
ti = context["ti"]
ref_path = ti.xcom_pull(key="reference_path")
cur_path = ti.xcom_pull(key="current_path")
reference = pd.read_parquet(ref_path)
current = pd.read_parquet(cur_path)
# 校验最小样本数
if len(current) < 100:
print(f"当前数据样本数不足: {len(current)}。跳过漂移检查。")
ti.xcom_push(key="drift_action", value="skip")
return "skip_retraining"
report = Report(metrics=[DatasetDriftMetric(), DataDriftTable()])
report.run(reference_data=reference, current_data=current)
result = report.as_dict()
drift_detected = result["metrics"][0]["result"]["dataset_drift"]
drift_share = result["metrics"][0]["result"]["share_of_drifted_columns"]
# 记录到 MLflow
mlflow.set_tracking_uri("http://mlflow.internal:5000")
mlflow.set_experiment("monitoring/fraud-detection")
with mlflow.start_run(run_name=f"drift-{context['ds']}"):
mlflow.log_metric("drift_detected", int(drift_detected))
mlflow.log_metric("drift_share", drift_share)
ti.xcom_push(key="drift_detected", value=drift_detected)
ti.xcom_push(key="drift_share", value=drift_share)
def decide_action(**context):
"""根据漂移水平决定是否再训练"""
ti = context["ti"]
drift_detected = ti.xcom_pull(key="drift_detected")
drift_share = ti.xcom_pull(key="drift_share")
if drift_share is None or drift_share < 0.2:
return "skip_retraining"
elif drift_share >= 0.5:
return "trigger_emergency_retrain"
else:
return "trigger_scheduled_retrain"
fetch_task = PythonOperator(
task_id="fetch_data", python_callable=fetch_data, dag=dag,
)
drift_task = PythonOperator(
task_id="run_drift_check", python_callable=run_drift_check, dag=dag,
)
branch_task = BranchPythonOperator(
task_id="decide_action", python_callable=decide_action, dag=dag,
)
skip_task = EmptyOperator(task_id="skip_retraining", dag=dag)
scheduled_retrain = EmptyOperator(task_id="trigger_scheduled_retrain", dag=dag)
emergency_retrain = EmptyOperator(task_id="trigger_emergency_retrain", dag=dag)
fetch_task >> drift_task >> branch_task >> [skip_task, scheduled_retrain, emergency_retrain]
再训练触发阈值指南
| 漂移水平 | drift_share 范围 | 建议措施 | 响应时间 |
|---|---|---|---|
| 正常 | 0% ~ 15% | 维持监控 | - |
| 注意 | 15% ~ 30% | 发送告警,开始原因分析 | 48 小时内 |
| 警告 | 30% ~ 50% | 加入调度再训练队列 | 24 小时内 |
| 危险 | 50% 以上 | 立即再训练 + 切换 fallback 模型 | 立即 |
注意:阈值应根据领域与模型特性进行调整。像金融欺诈检测这类漏检成本很高的领域,适合采用更低的阈值(10~20%);而像推荐系统这类容忍范围较宽的领域,适合采用更高的阈值(30~50%)。
7. 监控工具对比:Evidently vs NannyML vs WhyLabs vs Alibi Detect
生产环境 ML 监控工具有多种选择。下面对比各工具的优势与劣势。
| 标准 | Evidently AI | NannyML | WhyLabs | Alibi Detect |
|---|---|---|---|---|
| 许可证 | Apache 2.0 (OSS) | BSD-3 (OSS) | SaaS + 免费层 | BSD-3 (OSS) |
| 核心优势 | 通用数据/模型监控 | 无标签性能估计 (CBPE) | 实时流式数据画像 | 高级漂移检测算法 |
| 漂移检测方法数 | 20+ | 10+ | 15+ | 15+ |
| 无标签性能估计 | 有限 | 核心功能(CBPE、DLE) | 不支持 | 不支持 |
| 实时监控 | Collector 模式 | 不支持(批处理) | 原生支持 | 不支持(批处理) |
| 可视化 | 内置 HTML/仪表盘 | 内置 HTML | Web 仪表盘(SaaS) | 基础可视化 |
| CI/CD 集成 | Test Suite(原生) | 有限 | 基于 API | 需手动配置 |
| Prometheus 集成 | 官方支持 | 需自定义 | 内置 | 需自定义 |
| MLflow 集成 | 简单(Python 原生) | 手动配置 | API 集成 | 手动配置 |
| 学习曲线 | 低 | 中等 | 低(SaaS) | 高 |
| 生产使用场景 | 通用 | 标签延迟环境 | 大规模实时 | 研究/高级检测 |
选型指南:
- 无法立即获得标签的环境(例如金融欺诈检测中,标签确认要经过数月):NannyML 基于 CBPE(Confidence-Based Performance Estimation) 的性能估计是唯一可行的选择。
- 优先开源 + 快速落地:Evidently AI 提供最广的功能范围,且上手难度最低。
- 大规模实时流式场景:WhyLabs 的数据画像针对每秒数万条的处理量做了优化。
- 需要高级统计检测的研究环境:Alibi Detect 的深度核 MMD、Learned Kernel 漂移检测方法比较合适。
8. Grafana/Prometheus 仪表盘搭建
下面来看如何把 Evidently 的监控结果暴露为 Prometheus 指标,并在 Grafana 仪表盘上以时间序列形式可视化。
Prometheus 指标导出
# prometheus_drift_exporter.py
from prometheus_client import start_http_server, Gauge, Counter, Histogram
from evidently.report import Report
from evidently.metrics import DatasetDriftMetric, DataDriftTable
import pandas as pd
import time
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 定义 Prometheus 指标
DRIFT_DETECTED = Gauge(
"ml_model_drift_detected",
"数据集是否检测到漂移(0/1)",
["model_name", "model_version"],
)
DRIFT_SHARE = Gauge(
"ml_model_drift_column_share",
"检测到漂移的列比例",
["model_name", "model_version"],
)
COLUMN_DRIFT_SCORE = Gauge(
"ml_model_column_drift_score",
"各列的漂移分数",
["model_name", "model_version", "column_name"],
)
DRIFT_CHECK_TOTAL = Counter(
"ml_model_drift_checks_total",
"漂移检查执行次数",
["model_name"],
)
DRIFT_CHECK_DURATION = Histogram(
"ml_model_drift_check_duration_seconds",
"漂移检查耗时",
["model_name"],
buckets=[0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0],
)
MODEL_NAME = "fraud-detector"
MODEL_VERSION = "3"
def run_periodic_drift_check(
reference_path: str,
current_query_fn,
interval_seconds: int = 300,
):
"""周期性漂移检查并更新 Prometheus 指标"""
reference = pd.read_parquet(reference_path)
while True:
try:
start_time = time.time()
# 加载最近数据
current = current_query_fn()
if current is None or len(current) < 50:
logger.warning(f"当前数据不足: {len(current) if current is not None else 0} 条")
time.sleep(interval_seconds)
continue
# 仅过滤特征列(排除目标列与元数据列)
feature_cols = [c for c in reference.columns if c not in ["target", "id", "timestamp"]]
ref_features = reference[feature_cols]
cur_features = current[feature_cols]
# 漂移分析
report = Report(metrics=[DatasetDriftMetric(), DataDriftTable()])
report.run(reference_data=ref_features, current_data=cur_features)
result = report.as_dict()
drift_result = result["metrics"][0]["result"]
column_results = result["metrics"][1]["result"]["drift_by_columns"]
# 更新 Prometheus 指标
DRIFT_DETECTED.labels(MODEL_NAME, MODEL_VERSION).set(
int(drift_result["dataset_drift"])
)
DRIFT_SHARE.labels(MODEL_NAME, MODEL_VERSION).set(
drift_result["share_of_drifted_columns"]
)
for col_name, col_info in column_results.items():
COLUMN_DRIFT_SCORE.labels(MODEL_NAME, MODEL_VERSION, col_name).set(
col_info.get("drift_score", 0.0)
)
DRIFT_CHECK_TOTAL.labels(MODEL_NAME).inc()
duration = time.time() - start_time
DRIFT_CHECK_DURATION.labels(MODEL_NAME).observe(duration)
logger.info(
f"漂移检查完成: drift={drift_result['dataset_drift']}, "
f"share={drift_result['share_of_drifted_columns']:.2%}, "
f"duration={duration:.1f}s"
)
except Exception as e:
logger.error(f"漂移检查失败: {e}", exc_info=True)
time.sleep(interval_seconds)
if __name__ == "__main__":
# 启动 Prometheus 指标 HTTP 服务器(端口 8000)
start_http_server(8000)
logger.info("Prometheus 指标导出器已启动(端口 8000)")
# 启动周期性漂移检查(间隔 5 分钟)
run_periodic_drift_check(
reference_path="/data/reference/fraud_features_v3.parquet",
current_query_fn=lambda: pd.read_parquet("/data/serving/latest_batch.parquet"),
interval_seconds=300,
)
Grafana 仪表盘组成
在 Grafana 中配置以下面板,综合监控 ML 模型的健康状态。
| 面板 | 指标 | 可视化类型 | 告警规则 |
|---|---|---|---|
| 漂移状态 | ml_model_drift_detected | Stat(最新值) | 值为 1 时触发 Critical 告警 |
| 漂移列比例趋势 | ml_model_drift_column_share | Time Series | 超过 30% 时触发 Warning |
| 各列漂移分数 | ml_model_column_drift_score | Heatmap | 高亮超过阈值的列 |
| 检查耗时 | ml_model_drift_check_duration_seconds | Histogram | 超过 60 秒时触发 Warning |
| 检查执行次数 | rate(ml_model_drift_checks_total[1h]) | Time Series | 为 0 时告警检查已停止 |
Alertmanager 告警规则示例
# prometheus-alerts.yaml
groups:
- name: ml_model_drift_alerts
rules:
- alert: MLModelDriftDetected
expr: ml_model_drift_detected == 1
for: 5m
labels:
severity: warning
team: ml-platform
annotations:
summary: 'ML 模型数据漂移检测'
description: '模型 {{ $labels.model_name }} v{{ $labels.model_version }} 检测到数据漂移。漂移列比例: {{ $value }}'
- alert: MLModelCriticalDrift
expr: ml_model_drift_column_share > 0.5
for: 0m
labels:
severity: critical
team: ml-platform
annotations:
summary: 'ML 模型临界漂移 - 需要立即处理'
description: '模型 {{ $labels.model_name }} 的漂移列比例为 {{ $value | humanizePercentage }}。需要立即再训练或切换 fallback 模型。'
- alert: MLDriftCheckStalled
expr: rate(ml_model_drift_checks_total[1h]) == 0
for: 30m
labels:
severity: warning
team: ml-platform
annotations:
summary: 'ML 漂移检查已中断'
description: '模型 {{ $labels.model_name }} 的漂移检查已超过 30 分钟未执行。需要检查监控流水线。'
9. 运维注意事项
假阳性(False Positive)漂移管理
统计漂移检测最常见的陷阱就是假阳性。尤其在以下情况下,即便实际没有问题也可能被误判为漂移。
样本量效应:当前数据样本量非常大时,KS 检验或 Chi-squared 检验会把统计学上显著、但实际意义不大的差异也判定为漂移。应同时使用 PSI、Wasserstein 距离等基于效应量(effect size)的指标,以验证其实际意义上的显著性。
季节性(Seasonality):电商领域中黑色星期五期间的购买模式与平时明显不同。如果把这种情况判定为漂移,每年同一时期都会出现不必要的告警风暴。应将参考数据设置为同期的历史数据,或者应用季节性调整逻辑。
特征间相关性:仅在单个特征层面检测漂移,无法捕捉多变量分布的变化。有些情况下特征 A 与 B 各自的分布都很相似,但 A-B 之间的相关性发生了变化。Evidently 的 DatasetDriftMetric 提供的是整体数据集层面的判断,如果需要显式的多变量检测,应考虑 Alibi Detect 的 MMD(Maximum Mean Discrepancy) 方法。
参考数据管理策略
参考数据是漂移检测的基准线。错误的参考数据会使所有检测结果失效。
| 策略 | 说明 | 适用情况 | 注意事项 |
|---|---|---|---|
| 固定训练数据 | 将模型训练所用的数据固定为参考数据 | 稳定领域、变化较少的环境 | 随时间推移,参考数据本身会过时 |
| 滑动窗口 | 用最近 N 天/N 周的数据更新参考数据 | 渐进式变化属正常的环境 | 有可能错过渐进式漂移 |
| 再训练时更新 | 每次模型再训练时更新参考数据 | 具有定期再训练的流水线 | 依赖于再训练周期 |
| 双重基线 | 同时对比训练数据与最近稳定期数据 | 需要高准确度的环境 | 管理复杂度增加 |
关键点:应对参考数据进行版本管理,并与模型版本建立 1:1 对应关系,确保可追溯。建议以 MLflow 工件(artifact)的形式保存参考数据快照。
与特征存储的集成
如果离线训练时点与线上服务时点的特征计算逻辑不一致(Training-Serving Skew),就会出现并非真实漂移、而是由实现不一致造成的假漂移。使用 Feast 之类的特征存储来保证训练/服务之间的特征一致性,才是根本的解决方案。
10. 故障案例与恢复流程
案例 1:静默的模型性能退化(Silent Model Degradation)
情况:某电商推荐模型在 3 个月内逐渐性能退化,CTR 从 12% 降至 7%,但由于漂移监控只设置在单个特征层面,未能检测出来。
原因:用户行为模式发生了多变量变化。单个特征(浏览次数、停留时长、品类占比)各自的分布都没有大幅变化,但特征之间的相关性发生了改变。尤其是"停留时长-购买转化"这一关系,因短视频内容消费模式的变化而被削弱。
恢复流程:
- 增加多变量漂移检测(比较特征相关性矩阵)
- 增加直接监控业务 KPI(CTR、转化率)的概念漂移监控层
- 用最近 2 周的数据再训练模型,并通过 A/B 测试部署
- 将再训练周期从每月一次缩短为每周一次
教训:仅靠数据漂移难以捕捉概念漂移,必须同时并行监控业务指标。
案例 2:数据管道故障导致的假漂移
情况:某个周五深夜,漂移 Critical 告警集中爆发,3 个模型同时检测到超过 80% 的列漂移。
原因:上游数据管道的 ETL 作业失败,导致服务特征表中的部分列被填充为默认值(0 或 null)。这是一起数据质量问题被误判为漂移的案例。
恢复流程:
- 在 Evidently TestSuite 中,把
TestNumberOfMissingValues与TestShareOfOutRangeValues放在漂移检查之前的阶段 - 数据质量检查失败时跳过漂移检查,并单独发送数据管道告警
- 在上游 ETL 中增加数据完整性校验门禁
- 在漂移告警中附上"最近数据质量检查结果"信息
教训:漂移检测流水线之前必须放置数据质量校验环节。区分数据质量问题与真实的分布变化,是运维的核心。
案例 3:参考数据污染
情况:模型再训练后,用新数据更新了参考数据。此后完全检测不到任何漂移,导致监控形同虚设。
原因:用于再训练的数据本身已经含有漂移,而这份被污染的数据成为了新的参考数据。结果就是漂移被"正常化",基准线被重新设定成了错误的状态。
恢复流程:
- 在更新参考数据时,自动对比新旧参考数据之间的漂移
- 增加门禁:当漂移比例超过一定水平时,阻止参考数据更新
- 将参考数据的变更历史以 MLflow 工件形式做版本管理
- 定期与黄金数据集(经人工校验的高质量数据)进行比较
教训:参考数据是监控系统的基准线,任何变更都必须经过校验流程。
11. 生产监控检查清单
部署前检查清单
- 参考数据是否与模型版本一起做了版本管理
- Evidently Report/TestSuite 是否已集成进部署流水线
- 漂移阈值是否根据领域特性做了调整
- 数据质量校验是否放置在漂移检测之前的阶段
- fallback 模型是否已在注册表中注册
- Grafana 仪表盘与告警规则是否已配置
运行中检查清单
- 漂移检查是否按正常周期执行("监控者的监控")
- 假阳性比例是否处于可控水平(建议每月不超过 5 起)
- 参考数据是否在合适的时点得到更新
- 再训练触发是否正常运作,是否进行了 Champion/Challenger 评估
- 业务 KPI 与模型性能指标是否被一并追踪
- 收到告警后的平均响应时间(MTTR)是否在 SLA 范围内
概念漂移应对检查清单
- 是否搭建了标签获取流水线(含延迟标签)
- 是否在监控模型性能指标(Accuracy、F1、AUC)的时间序列走势
- 是否为没有标签的时间段定义了代理指标(proxy metric)
- A/B 测试基础设施是否已就绪
12. 参考资料
- Evidently AI - Data Drift 官方指南 - 涵盖数据漂移的概念、检测方法论与实际案例的综合指南。
- Evidently AI GitHub 仓库 - 开源代码、示例笔记本、社区讨论。
- MLflow Model Registry 官方文档 - 模型注册表 API、别名系统、部署工作流指南。
- Evidently AI 官方文档 - Report、TestSuite、Metric 的完整 API 参考与教程。
- Advanced ML Model Monitoring: Drift Detection, Explainability, and Automated Retraining - 漂移检测与自动再训练流水线的高级模式。
- Google - ML Technical Debt (Hidden Technical Debt in Machine Learning Systems) - 关于 ML 系统技术债务与监控必要性的奠基性论文。
- NannyML - Estimating Model Performance without Ground Truth - 无需标签即可估计模型性能的 CBPE 方法论。
测验
Q1:《ML 模型监控与漂移检测:Evidently AI + MLflow 生产运维指南》一文主要讨论的内容是什么?
一份综合指南,涵盖用 Evidently AI 与 MLflow 构建 ML 模型生产监控流水线、数据/概念漂移检测、自动再训练触发, 直至运维故障排查。
Q2:漂移的种类:变化的是什么,指的是什么?
漂移(Drift)指的是模型训练时的数据分布与实际服务(serving)时点的数据分布之间的不一致。漂移根据发生位置与
性质,大致可分为三类。
Q3:请说明 Evidently AI 的架构与核心功能。
Evidently AI 是一个用于 ML 模型监控与数据质量校验的开源库。它运行在 Python 原生环境中,内置了 20 多种
统计漂移检测方法。核心组成 Report:一次性数据分析报告。
Q4:Evidently AI 实战用法有哪些要点?
安装与基础设置 用 Test Suite 实现自动化数据质量校验
Q5:MLflow 模型注册表与监控集成是如何运作的?
MLflow 提供实验追踪、模型打包与模型注册表功能。通过将 Evidently 的漂移检测结果记录到 MLflow,可以在
同一平台上追踪各模型版本的性能历史与漂移状态。