MLOps与AI系统运维完全教程

教程简介

全面讲解MLOps与AI系统运维的核心技术,涵盖实验追踪与版本管理、特征工程与特征存储、CI/CD/CT流水线构建、模型部署策略、推理服务框架、模型监控与数据漂移检测、LLMOps专项等核心内容,帮助开发者构建企业级AI运维体系。

MLOps与AI系统运维完全教程

教程简介

MLOps(Machine Learning Operations)是将机器学习模型从实验阶段推向生产环境的工程化实践体系。本教程系统讲解MLOps的核心理念、工具链和最佳实践,帮助团队构建高效、可靠的AI系统运维体系。


第一章:MLOps成熟度模型

1.1 MLOps三个级别

Google提出的MLOps成熟度模型将AI运维分为三个级别:

Level 0 - 手动流程

  • 手动训练模型、手动部署
  • 无CI/CD、无监控
  • 适合实验阶段

Level 1 - ML管道自动化

  • 自动化训练管道
  • 持续训练(CT)
  • 特征存储
  • 实验追踪

Level 2 - CI/CD/CT全自动化

  • 完整的CI/CD/CT流水线
  • 自动化测试与验证
  • 模型监控与漂移检测
  • 自动回滚机制

1.2 MLOps vs DevOps

# MLOps与DevOps的核心区别
comparison = {
    "代码版本管理": "Git (两者相同)",
    "数据版本管理": "DVC/Delta Lake (MLOps独有)",
    "模型版本管理": "MLflow Model Registry (MLOps独有)",
    "测试": "单元测试→模型质量测试 (MLOps扩展)",
    "部署": "容器化部署→A/B测试+影子模式 (MLOps扩展)",
    "监控": "系统指标→数据漂移+模型衰减 (MLOps扩展)",
}

第二章:实验追踪与版本管理

2.1 MLflow实验追踪

import mlflow
import mlflow.sklearn
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score

class ExperimentTracker:
    """实验追踪器"""
    
    def __init__(self, experiment_name: str):
        mlflow.set_experiment(experiment_name)
    
    def track_training(self, X_train, y_train, X_test, y_test, params: dict):
        """追踪训练过程"""
        with mlflow.start_run():
            # 记录参数
            mlflow.log_params(params)
            
            # 训练模型
            model = RandomForestClassifier(**params)
            model.fit(X_train, y_train)
            
            # 评估
            y_pred = model.predict(X_test)
            accuracy = accuracy_score(y_test, y_pred)
            f1 = f1_score(y_test, y_pred, average='weighted')
            
            # 记录指标
            mlflow.log_metric("accuracy", accuracy)
            mlflow.log_metric("f1_score", f1)
            
            # 记录模型
            mlflow.sklearn.log_model(
                model, 
                "model",
                registered_model_name="my_classifier"
            )
            
            # 记录数据版本
            mlflow.log_artifact("data/train.csv")
            
            return accuracy, f1
    
    def compare_experiments(self, experiment_name: str):
        """对比实验结果"""
        experiment = mlflow.get_experiment_by_name(experiment_name)
        runs = mlflow.search_runs(experiment_ids=[experiment.experiment_id])
        
        # 按F1分数排序
        runs_sorted = runs.sort_values("metrics.f1_score", ascending=False)
        return runs_sorted[["run_id", "metrics.accuracy", "metrics.f1_score", "params"]]

2.2 Weights & Biases追踪

import wandb

class WandBTracker:
    """W&B实验追踪器"""
    
    def __init__(self, project: str, config: dict):
        self.run = wandb.init(project=project, config=config)
    
    def log_epoch(self, epoch: int, train_loss: float, val_loss: float, metrics: dict):
        """记录每个epoch的指标"""
        wandb.log({
            "epoch": epoch,
            "train_loss": train_loss,
            "val_loss": val_loss,
            **metrics
        })
    
    def log_model(self, model_path: str, name: str):
        """记录模型"""
        artifact = wandb.Artifact(name, type="model")
        artifact.add_file(model_path)
        self.run.log_artifact(artifact)
    
    def log_confusion_matrix(self, y_true, y_pred, class_names):
        """记录混淆矩阵"""
        wandb.log({
            "confusion_matrix": wandb.plot.confusion_matrix(
                y_true=y_true, preds=y_pred, class_names=class_names
            )
        })

2.3 DVC数据版本管理

# DVC初始化
dvc init

# 追踪数据文件
dvc add data/train.csv data/test.csv

# 数据版本管理
git add data/train.csv.dvc data/test.csv.dvc
git commit - "Add training data v1"

# 配置远程存储
dvc remote add -d myremote s3://my-bucket/dvc-store

# 推送数据到远程
dvc push

# 切换到历史版本
git checkout v1.0 -- data/train.csv.dvc
dvc checkout

# 数据管道
dvc repro
# dvc.yaml - 定义数据管道
stages:
  prepare:
    cmd: python src/prepare.py
    deps:
      - src/prepare.py
      - data/raw.csv
    outs:
      - data/train.csv
      - data/test.csv
  
  train:
    cmd: python src/train.py
    deps:
      - src/train.py
      - data/train.csv
    outs:
      - models/model.pkl
    metrics:
      - metrics.json

第三章:特征工程与特征存储

3.1 Feast特征存储

from feast import FeatureStore, Entity, FeatureView, Field
from feast.types import Float32, Int64, String
from feast.infra.offline_stores.file_source import FileSource
from datetime import timedelta

# 定义实体
user = Entity(
    name="user_id",
    join_keys=["user_id"],
    description="用户ID"
)

# 定义特征视图
user_features = FeatureView(
    name="user_features",
    entities=[user],
    schema=[
        Field(name="age", dtype=Int64),
        Field(name="total_orders", dtype=Int64),
        Field(name="avg_order_amount", dtype=Float32),
        Field(name="membership_level", dtype=String),
    ],
    source=FileSource(
        path="data/user_features.parquet",
        timestamp_field="event_timestamp"
    ),
    ttl=timedelta(days=1),
)

# 注册特征
store = FeatureStore(repo_path=".")
store.apply([user, user_features])

# 在线检索
feature_vector = store.get_online_features(
    features=[
        "user_features:age",
        "user_features:total_orders",
        "user_features:avg_order_amount"
    ],
    entity_rows=[{"user_id": "U001"}]
).to_dict()

# 离线训练数据集
training_df = store.get_historical_features(
    entity_df=entity_df,
    features=[
        "user_features:age",
        "user_features:total_orders",
        "user_features:avg_order_amount"
    ]
).to_df()

第四章:CI/CD/CT流水线构建

4.1 模型训练自动化

# 使用 Prefect 构建训练管道
from prefect import task, flow
from prefect.tasks import task_input_hash
from datetime import timedelta

@task(cache_key_fn=task_input_hash, cache_expiration=timedelta(hours=1))
def load_data(data_path: str):
    """加载数据"""
    import pandas as pd
    return pd.read_csv(data_path)

@task
def preprocess(df):
    """数据预处理"""
    from sklearn.preprocessing import StandardScaler
    scaler = StandardScaler()
    features = df.drop("target", axis=1)
    scaled = scaler.fit_transform(features)
    return scaled, df["target"], scaler

@task(retries=3, retry_delay_seconds=60)
def train_model(X_train, y_train, params: dict):
    """训练模型"""
    from sklearn.ensemble import GradientBoostingClassifier
    model = GradientBoostingClassifier(**params)
    model.fit(X_train, y_train)
    return model

@task
def evaluate_model(model, X_test, y_test) -> dict:
    """评估模型"""
    from sklearn.metrics import accuracy_score, f1_score, classification_report
    y_pred = model.predict(X_test)
    return {
        "accuracy": accuracy_score(y_test, y_pred),
        "f1_score": f1_score(y_test, y_pred, average='weighted'),
        "report": classification_report(y_test, y_pred)
    }

@task
def validate_model(metrics: dict, threshold: float = 0.85) -> bool:
    """验证模型是否达标"""
    return metrics["f1_score"] >= threshold

@task
def register_model(model, model_name: str):
    """注册模型"""
    import mlflow
    mlflow.sklearn.log_model(model, "model", registered_model_name=model_name)

@flow(name="training-pipeline")
def training_pipeline(data_path: str, params: dict):
    """完整训练管道"""
    # 加载数据
    df = load_data(data_path)
    
    # 预处理
    X, y, scaler = preprocess(df)
    
    # 划分数据集
    from sklearn.model_selection import train_test_split
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
    
    # 训练
    model = train_model(X_train, y_train, params)
    
    # 评估
    metrics = evaluate_model(model, X_test, y_test)
    print(f"评估结果: {metrics}")
    
    # 验证
    if validate_model(metrics):
        register_model(model, "production_model")
        print("模型已注册")
    else:
        print("模型未达标,跳过注册")

4.2 模型部署策略

class ModelDeployer:
    """模型部署器"""
    
    def blue_green_deploy(self, model_name: str, model_version: str):
        """蓝绿部署"""
        # 1. 在Green环境部署新模型
        self._deploy_to_green(model_name, model_version)
        
        # 2. 运行健康检查
        if self._health_check("green"):
            # 3. 切换流量
            self._switch_traffic("green")
            # 4. 停止Blue环境
            self._stop_blue()
        else:
            # 回滚
            self._stop_green()
            raise Exception("Green环境健康检查失败")
    
    def canary_deploy(self, model_name: str, model_version: str, 
                      canary_ratio: float = 0.1):
        """金丝雀部署"""
        # 1. 部署新版本到少量实例
        self._deploy_canary(model_name, model_version)
        
        # 2. 分流指定比例的流量
        self._set_traffic_split(canary_ratio)
        
        # 3. 监控新版本指标
        metrics = self._monitor_canary(duration_minutes=30)
        
        # 4. 判断是否全量发布
        if self._evaluate_canary_metrics(metrics):
            self._promote_canary_to_production()
        else:
            self._rollback_canary()
    
    def shadow_deploy(self, model_name: str, model_version: str):
        """影子部署(用于验证)"""
        # 1. 部署新模型但不服务真实流量
        self._deploy_shadow(model_name, model_version)
        
        # 2. 复制生产流量到影子模型
        self._mirror_traffic("shadow")
        
        # 3. 对比新旧模型的预测结果
        comparison = self._compare_predictions(duration_hours=24)
        
        return comparison

第五章:推理服务框架

5.1 vLLM高性能推理

from vllm import LLM, SamplingParams

class VLLMInferenceService:
    """vLLM推理服务"""
    
    def __init__(self, model_name: str, **kwargs):
        self.llm = LLM(
            model=model_name,
            tensor_parallel_size=kwargs.get("tp_size", 1),
            gpu_memory_utilization=kwargs.get("gpu_util", 0.9),
            max_num_batched_tokens=kwargs.get("max_batch_tokens", 8192),
            max_num_seqs=kwargs.get("max_seqs", 256),
            dtype="auto",
            trust_remote_code=True
        )
    
    def generate(self, prompts: list, **kwargs) -> list:
        """批量生成"""
        sampling_params = SamplingParams(
            temperature=kwargs.get("temperature", 0.7),
            top_p=kwargs.get("top_p", 0.9),
            max_tokens=kwargs.get("max_tokens", 512),
            stop=kwargs.get("stop", None)
        )
        
        outputs = self.llm.generate(prompts, sampling_params)
        return [output.outputs[0].text for output in outputs]
    
    def chat(self, messages: list, **kwargs) -> str:
        """对话接口"""
        from vllm import ChatCompletionRequest
        
        request = ChatCompletionRequest(
            model=self.llm.model_config.model,
            messages=messages,
            temperature=kwargs.get("temperature", 0.7),
            max_tokens=kwargs.get("max_tokens", 512)
        )
        
        response = self.llm.chat(request)
        return response.choices[0].message.content

5.2 Triton Inference Server

# 模型仓库结构
# model_repository/
# └── my_model/
#     ├── config.pbtxt
#     ├── 1/
#     │   └── model.py
#     └── 2/
#         └── model.py

# config.pbtxt 示例
"""
name: "my_llm_model"
platform: "python"
max_batch_size: 64
input [
  {
    name: "input_text"
    data_type: TYPE_STRING
    dims: [1]
  }
]
output [
  {
    name: "output_text"
    data_type: TYPE_STRING
    dims: [1]
  }
]
instance_group [
  {
    count: 2
    kind: KIND_GPU
  }
]
dynamic_batching {
  preferred_batch_size: [8, 16, 32]
  max_queue_delay_microseconds: 100000
}
"""

# Python模型实现 (model.py)
class TritonPythonModel:
    def initialize(self, args):
        """初始化模型"""
        import torch
        from transformers import AutoModelForCausalLM, AutoTokenizer
        
        model_path = "/models/my_llm_model/1/"
        self.tokenizer = AutoTokenizer.from_pretrained(model_path)
        self.model = AutoModelForCausalLM.from_pretrained(
            model_path, 
            torch_dtype=torch.float16,
            device_map="auto"
        )
    
    def execute(self, requests):
        """处理推理请求"""
        responses = []
        for request in requests:
            input_text = pb_utils.get_input_tensor_by_name(request, "input_text").as_numpy()
            
            # 推理
            inputs = self.tokenizer(input_text[0].decode(), return_tensors="pt").to("cuda")
            outputs = self.model.generate(**inputs, max_new_tokens=256)
            result = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
            
            # 构建响应
            output_tensor = pb_utils.Tensor("output_text", np.array([result.encode()]))
            responses.append(pb_utils.InferenceResponse([output_tensor]))
        
        return responses

第六章:模型监控与数据漂移检测

6.1 数据漂移检测

import numpy as np
from scipy import stats

class DriftDetector:
    """数据漂移检测器"""
    
    def __init__(self, reference_data: np.ndarray):
        self.reference = reference_data
        self.reference_stats = self._compute_stats(reference_data)
    
    def _compute_stats(self, data: np.ndarray) -> dict:
        """计算数据统计特征"""
        return {
            "mean": np.mean(data, axis=0),
            "std": np.std(data, axis=0),
            "median": np.median(data, axis=0),
            "q25": np.percentile(data, 25, axis=0),
            "q75": np.percentile(data, 75, axis=0)
        }
    
    def ks_test(self, new_data: np.ndarray, threshold: float = 0.05) -> dict:
        """KS检验:检测分布变化"""
        results = {}
        for i in range(new_data.shape[1]):
            statistic, p_value = stats.ks_2samp(
                self.reference[:, i], new_data[:, i]
            )
            results[f"feature_{i}"] = {
                "statistic": float(statistic),
                "p_value": float(p_value),
                "drift_detected": p_value < threshold
            }
        return results
    
    def psi(self, new_data: np.ndarray, bins: int = 10) -> dict:
        """PSI(Population Stability Index):量化分布偏移"""
        results = {}
        for i in range(new_data.shape[1]):
            # 计算分箱边界
            breakpoints = np.percentile(self.reference[:, i], 
                                       np.linspace(0, 100, bins + 1))
            
            # 计算各箱的占比
            ref_counts = np.histogram(self.reference[:, i], bins=breakpoints)[0]
            new_counts = np.histogram(new_data[:, i], bins=breakpoints)[0]
            
            ref_pct = ref_counts / len(self.reference) + 1e-10
            new_pct = new_counts / len(new_data) + 1e-10
            
            # 计算PSI
            psi_value = np.sum((new_pct - ref_pct) * np.log(new_pct / ref_pct))
            
            results[f"feature_{i}"] = {
                "psi": float(psi_value),
                "status": "stable" if psi_value < 0.1 else "moderate" if psi_value < 0.25 else "significant"
            }
        
        return results
    
    def monitor_prediction_drift(self, reference_preds: list, 
                                  new_preds: list) -> dict:
        """监控预测结果漂移"""
        ref_dist = np.bincount(reference_preds) / len(reference_preds)
        new_dist = np.bincount(new_preds, minlength=len(ref_dist)) / len(new_preds)
        
        # 卡方检验
        chi2, p_value = stats.chisquare(new_dist * len(new_preds), 
                                         ref_dist * len(new_preds))
        
        return {
            "chi2_statistic": float(chi2),
            "p_value": float(p_value),
            "drift_detected": p_value < 0.05
        }

6.2 模型性能监控

class ModelMonitor:
    """模型性能监控"""
    
    def __init__(self, alert_threshold: dict):
        self.threshold = alert_threshold
        self.metrics_history = []
    
    def log_prediction(self, input_data: dict, prediction: str, 
                       ground_truth: str = None, latency_ms: float = 0):
        """记录预测日志"""
        record = {
            "timestamp": datetime.now().isoformat(),
            "input": input_data,
            "prediction": prediction,
            "ground_truth": ground_truth,
            "latency_ms": latency_ms
        }
        self.metrics_history.append(record)
    
    def compute_metrics(self, window_minutes: int = 60) -> dict:
        """计算时间窗口内的指标"""
        cutoff = datetime.now() - timedelta(minutes=window_minutes)
        recent = [r for r in self.metrics_history 
                  if datetime.fromisoformat(r["timestamp"]) > cutoff]
        
        if not recent:
            return {}
        
        # 延迟统计
        latencies = [r["latency_ms"] for r in recent if r["latency_ms"] > 0]
        
        # 准确率(有ground truth的)
        labeled = [r for r in recent if r["ground_truth"] is not None]
        accuracy = None
        if labeled:
            correct = sum(1 for r in labeled if r["prediction"] == r["ground_truth"])
            accuracy = correct / len(labeled)
        
        return {
            "total_predictions": len(recent),
            "latency_p50": np.percentile(latencies, 50) if latencies else None,
            "latency_p95": np.percentile(latencies, 95) if latencies else None,
            "latency_p99": np.percentile(latencies, 99) if latencies else None,
            "accuracy": accuracy,
            "labeled_ratio": len(labeled) / len(recent) if recent else 0
        }
    
    def check_alerts(self, metrics: dict) -> list:
        """检查告警"""
        alerts = []
        
        if metrics.get("latency_p99") and metrics["latency_p99"] > self.threshold.get("max_latency_p99", 5000):
            alerts.append({
                "type": "high_latency",
                "message": f"P99延迟过高: {metrics['latency_p99']:.0f}ms",
                "severity": "warning"
            })
        
        if metrics.get("accuracy") and metrics["accuracy"] < self.threshold.get("min_accuracy", 0.8):
            alerts.append({
                "type": "low_accuracy",
                "message": f"准确率过低: {metrics['accuracy']:.2%}",
                "severity": "critical"
            })
        
        return alerts

第七章:LLMOps专项

7.1 LLM特有的运维挑战

class LLMOperations:
    """LLM运维管理"""
    
    def __init__(self, llm_client):
        self.llm = llm_client
        self.guardrails = []
    
    def add_guardrail(self, guardrail):
        """添加安全护栏"""
        self.guardrails.append(guardrail)
    
    async def safe_generate(self, prompt: str, **kwargs) -> dict:
        """带安全检查的生成"""
        # 输入检查
        for guard in self.guardrails:
            check_result = await guard.check_input(prompt)
            if not check_result["passed"]:
                return {
                    "blocked": True,
                    "reason": check_result["reason"],
                    "guardrail": guard.name
                }
        
        # 生成
        response = await self.llm.chat(
            messages=[{"role": "user", "content": prompt}],
            **kwargs
        )
        
        # 输出检查
        for guard in self.guardrails:
            check_result = await guard.check_output(response)
            if not check_result["passed"]:
                return {
                    "blocked": True,
                    "reason": check_result["reason"],
                    "guardrail": guard.name
                }
        
        return {"blocked": False, "response": response}
    
    def cost_tracker(self, input_tokens: int, output_tokens: int, 
                     model: str) -> float:
        """成本追踪"""
        pricing = {
            "gpt-4o": {"input": 2.5 / 1_000_000, "output": 10 / 1_000_000},
            "gpt-4o-mini": {"input": 0.15 / 1_000_000, "output": 0.6 / 1_000_000},
            "claude-3-5-sonnet": {"input": 3 / 1_000_000, "output": 15 / 1_000_000},
        }
        
        if model not in pricing:
            return 0.0
        
        cost = (input_tokens * pricing[model]["input"] + 
                output_tokens * pricing[model]["output"])
        return cost

7.2 Prompt版本管理

import hashlib
import json
from datetime import datetime

class PromptRegistry:
    """Prompt版本注册中心"""
    
    def __init__(self, storage_path: str = "prompts/"):
        self.storage_path = storage_path
        self.registry = {}
    
    def register(self, name: str, template: str, 
                 metadata: dict = None) -> str:
        """注册Prompt版本"""
        # 计算版本哈希
        version_hash = hashlib.sha256(template.encode()).hexdigest()[:8]
        
        entry = {
            "name": name,
            "version": version_hash,
            "template": template,
            "metadata": metadata or {},
            "created_at": datetime.now().isoformat(),
            "is_active": False
        }
        
        if name not in self.registry:
            self.registry[name] = []
        self.registry[name].append(entry)
        
        # 持久化
        self._save(name, entry)
        
        return version_hash
    
    def activate(self, name: str, version: str):
        """激活指定版本"""
        for entry in self.registry.get(name, []):
            entry["is_active"] = entry["version"] == version
    
    def get_active(self, name: str) -> str:
        """获取当前激活的Prompt"""
        for entry in self.registry.get(name, []):
            if entry["is_active"]:
                return entry["template"]
        return None
    
    def compare_versions(self, name: str, v1: str, v2: str) -> dict:
        """对比两个版本"""
        versions = {e["version"]: e for e in self.registry.get(name, [])}
        return {
            "v1": versions.get(v1),
            "v2": versions.get(v2),
            "diff": self._diff(versions[v1]["template"], versions[v2]["template"])
        }

最佳实践总结

  1. 从小处开始:先实现基本的实验追踪,再逐步完善CI/CD
  2. 数据版本化:数据和代码同等重要,必须版本管理
  3. 自动化测试:模型测试包括准确性、延迟、公平性等维度
  4. 渐进式部署:蓝绿部署→金丝雀部署→影子部署
  5. 持续监控:不仅监控系统指标,还要监控数据漂移和模型衰减
  6. 成本意识:追踪GPU使用和API调用成本,优化资源利用
  7. 安全优先:LLM应用必须有Guardrails安全护栏

总结

MLOps是AI应用从实验到生产的关键桥梁。本教程涵盖了从实验追踪、特征管理、CI/CD流水线、推理服务到监控告警的完整技术栈。随着LLM应用的兴起,LLMOps作为MLOps的子领域也在快速发展。掌握这些工程化实践,将帮助团队高效、可靠地将AI能力交付给用户。

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录