1. 这不是“跑通模型”就完事的活儿:为什么第4部分专讲真实世界部署

你训练出一个AUC 0.98的模型,Jupyter里画出完美ROC曲线,保存成 .pkl 文件,发给工程团队——然后呢?然后就没有然后了。项目卡在“下一步”整整三个月,数据科学家开始写新论文,后端工程师在等API文档,运维同事盯着空荡荡的Kubernetes集群发呆。这就是“From Notebook to Production”系列走到Part 4的核心真相: Notebook是起点,不是终点;模型是资产,不是成品;部署不是复制粘贴,而是一整套工程契约的落地。 我做过的27个上线项目里,有19个卡点不在算法调优,而在Part 4——那个被多数教程轻描淡写带过的“最后一步”。它不涉及反向传播公式,但要你懂Docker镜像分层原理;不需要推导梯度下降收敛性,但得会看Prometheus里 http_request_duration_seconds_bucket 的直方图分布;不考你Transformer的attention矩阵维度,但必须能解释为什么把 model.predict() 包进FastAPI路由后,P99延迟从80ms飙到1.2s。关键词—— ML in the Real World ——这里的“Real World”三个字,指的是有监控告警、有灰度策略、有回滚机制、有资源配额、有审计日志、有业务兜底的真实生产环境。它拒绝“在我机器上能跑”的模糊地带,只认“在SLO 99.95% SLA下稳定服务30天”的硬指标。适合谁?不是刚学完scikit-learn的新人,而是已经能把模型训出来、正被老板问“什么时候能上线”的中级数据科学家;不是纯写CRUD的后端,而是需要和算法团队对齐接口规范、设计请求熔断逻辑的全栈工程师;更不是只管买服务器的IT采购,而是要为GPU节点规划Taint/Toleration、为模型服务配置HorizontalPodAutoscaler的云平台负责人。Part 4不是锦上添花,它是把实验室成果变成公司营收流水线的关键一环。

2. 从Notebook到Production的完整链路拆解:为什么跳过任何一环都会崩

2.1 不是“模型导出”,而是“服务契约定义”

很多人以为Part 4第一步是 joblib.dump(model, 'model.pkl') ,大错特错。真正的起点是 服务契约(Service Contract)的书面确认 。我见过最惨的案例:算法团队交付了一个PyTorch模型,输入要求是 [batch_size, 3, 224, 224] 的Tensor,类型 torch.float32 ,但没说明是否已归一化(ImageNet mean/std还是自定义?)。工程团队按常规流程做了 torch.jit.script ,上线后首日凌晨三点报警:所有请求返回 CUDA out of memory 。排查发现,前端传来的base64图片经OpenCV解码后是 uint8 ,直接转Tensor没除255,导致数值范围0-255压进float32显存,显存占用翻倍。问题根源不在代码,而在契约缺失。服务契约必须明确四项铁律:

  1. 输入规范 :数据格式(JSON/Protobuf)、字段名( "image_base64" 还是 "img_data" )、编码方式(base64还是raw bytes)、数值范围(0-1 or 0-255)、尺寸约束( max_width=1920 )、缺失值处理(null报错 or 默认填充);
  2. 输出规范 :结构( {"score": 0.92, "class": "cat"} )、精度( score 保留3位小数)、置信度阈值( class 仅当 score>0.5 才返回)、多标签场景的排序规则(按score降序 or 按class字母序);
  3. 非功能需求 :P95延迟≤200ms、并发QPS≥500、错误率<0.1%、支持HTTP/HTTPS双协议、健康检查端点路径( /healthz );
  4. 运维边界 :谁负责证书更新(算法团队 or SRE)、模型版本升级是否需停机(滚动更新 or 蓝绿发布)、日志字段必须包含 request_id model_version

这份契约不是Word文档,而是用OpenAPI 3.0 YAML写的接口定义,由算法、工程、SRE三方签字确认。我坚持用 swagger-codegen 从YAML自动生成FastAPI的Pydantic模型,强制类型校验——这比任何口头约定都可靠。

2.2 镜像构建不是“pip install”,而是分层缓存的艺术

很多团队用 Dockerfile 第一行就 COPY . /app ,然后 RUN pip install -r requirements.txt ,结果每次改一行Python代码,整个镜像重建,基础镜像层(CUDA、PyTorch)全被重复拉取。Part 4的镜像构建必须遵循 分层缓存黄金法则 :越稳定的内容越靠前,越易变的内容越靠后。以一个典型推理服务为例,我的标准分层是:

# 第一层:操作系统与CUDA(半年一更)
FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04

# 第二层:系统级依赖(季度一更)
RUN apt-get update && apt-get install -y \
    libglib2.0-0 \
    libsm6 \
    libxext6 \
    && rm -rf /var/lib/apt/lists/*

# 第三层:Python与核心框架(月度更新)
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
RUN curl -sSL https://install.python-poetry.com | POETRY_HOME=/opt/poetry sh
ENV PATH="/opt/poetry/bin:$PATH"
RUN poetry config virtualenvs.create false
COPY pyproject.toml poetry.lock ./
# 关键:只安装依赖,不COPY代码!
# poetry install --no-root --no-dev 会自动解析lock文件,复现精确版本
RUN poetry install --no-root --no-dev

# 第四层:模型权重与预处理资产(周级更新)
# 注意:模型文件通常>100MB,放这里可利用CDN加速分发
COPY models/ /app/models/
COPY assets/ /app/assets/

# 第五层:应用代码(每日更新)
COPY src/ /app/src/
WORKDIR /app

这个结构让镜像构建时间从12分钟降到90秒——因为90%的变更只触发第五层重建。更重要的是,它让安全扫描变得可行:SRE团队只需扫描前三层(OS/CUDA/Python),就能确认无高危CVE;模型层单独做哈希校验,确保生产环境加载的权重和测试环境一致;代码层最小化,降低漏洞攻击面。我曾用 docker history 对比两个镜像,发现某团队因把 requirements.txt 和代码混在同一层,导致每次 git commit 都让镜像ID变化,无法做精准diff审计——这是Part 4里最常被忽视的合规风险。

2.3 API网关不是“加个Nginx”,而是流量治理中枢

把模型包装成FastAPI服务后,直接暴露公网IP?那是自杀行为。Part 4必须引入API网关作为 流量治理中枢 。它不只是反向代理,更是模型服务的“交通警察”:限流防刷、熔断保底、鉴权控权、日志审计、协议转换。我们不用Kong或Traefik,而是用AWS ALB + Lambda Authorizer组合,原因很实在——ALB原生支持WebSocket(用于实时推理流)、自动TLS卸载(省去Let's Encrypt轮换)、内置WAF规则(防SQLi/XSS),而Lambda Authorizer能无缝集成公司IAM系统,避免维护独立用户数据库。关键配置有三处:

  1. 限流策略 :不是简单设 1000 req/sec ,而是按用户等级分层。VIP客户走 /v1/predict/vip 路径,QPS配额5000;普通客户走 /v1/predict/standard ,QPS配额200。ALB的Target Group Health Check必须设为 /healthz?timeout=5 ,且超时时间严格匹配模型warmup耗时(实测ResNet50首次推理需1.8s,Health Check timeout设2s,避免误判宕机);
  2. 熔断机制 :当ALB监测到目标组5xx错误率连续3分钟>5%,自动触发熔断,将流量切至备用静态响应( {"error": "service_unavailable", "fallback": true} ),同时触发PagerDuty告警。这个fallback不是返回503,而是返回预计算的兜底结果——比如推荐系统熔断时,返回热门商品列表,保证用户体验不中断;
  3. 请求透传 :ALB默认会Strip掉 X-Forwarded-For 头,但模型服务需要真实客户端IP做风控(防羊毛党)。必须在ALB Listener Rule里启用 X-Forwarded-For 透传,并在FastAPI中间件里用 request.headers.get("X-Forwarded-For", "").split(",")[0] 提取IP——注意是第一个IP,因为可能经过多层代理。

这套设计让我们的模型服务在黑五期间扛住峰值12万QPS,错误率维持在0.03%,而没扩容一台GPU服务器。因为流量治理在网关层完成,模型服务本身只专注推理。

3. 核心环节实现:从本地验证到生产就绪的七步法

3.1 步骤1:本地沙盒验证——用Docker Compose模拟生产网络

别急着推K8s。Part 4的第一步,是在本地用 docker-compose.yml 搭建最小化生产环境镜像。这不是为了“看起来像”,而是为了 提前暴露网络和权限问题 。我的标准沙盒包含四个服务:

version: '3.8'
services:
  # 模型服务:完全复刻生产Dockerfile
  model-api:
    build: .
    ports: ["8000:8000"]
    environment:
      - MODEL_PATH=/app/models/resnet50_v2.pth
      - LOG_LEVEL=INFO
    # 关键:挂载host网络,模拟真实容器间通信
    network_mode: "host"

  # Mock数据库:替代真实Redis/Mongo,用sqlite模拟缓存逻辑
  mock-cache:
    image: python:3.9-slim
    volumes: ["./cache.db:/app/cache.db"]
    command: ["python", "-m", "http.server", "8001"]

  # 日志收集器:Fluent Bit,配置和生产一致
  fluent-bit:
    image: fluent/fluent-bit:2.1.11
    volumes: ["/var/log:/var/log", "./fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf"]
    command: ["-c", "/fluent-bit/etc/fluent-bit.conf"]

  # 健康检查工具:curl循环调用,模拟ALB探针
  health-check:
    image: curlimages/curl:8.4.0
    depends_on: [model-api]
    command: ["sh", "-c", "while true; do curl -f http://localhost:8000/healthz || exit 1; sleep 5; done"]

运行 docker-compose up --build 后,重点验证三件事:1) model-api 启动时能否正确加载 /app/models/ 下的权重(检查日志是否有 Model loaded from /app/models/resnet50_v2.pth );2) health-check 容器是否持续收到200响应(证明健康检查端点可用);3) fluent-bit 是否生成 /var/log/model-api.log 且包含 request_id 字段。这一步卡住最多的是路径权限——Docker默认以root运行,但模型文件在host上是 chmod 600 ,容器内读取失败。解决方案是 docker-compose.yml 里加 user: "1001:1001" ,并在Dockerfile里 RUN chown -R 1001:1001 /app/models 。这个细节,90%的教程不会提,但线上必踩。

3.2 步骤2:CI/CD流水线——GitOps驱动的自动化发布

手工 docker push 到ECR?那是Part 1的做法。Part 4必须用GitOps: 代码提交即部署,分支即环境 。我们的GitHub Actions流水线分三级:

触发条件 执行动作 目标环境 关键检查
push to dev branch 构建镜像 → 推送至ECR → 部署到EKS dev cluster 开发环境 curl -s http://dev-api.example.com/healthz | jq .status == "ok"
push to staging branch 同上 + 运行混沌测试(注入500ms延迟) 预发环境 k6 run --vus 100 --duration 30s staging-test.js | grep "http_req_failed.*0%"
merge PR to main 同上 + 自动创建Argo CD Application manifest 生产环境 kubectl get app model-api-prod -n argocd | grep SyncStatus.*Synced

其中最关键的不是部署,而是 混沌测试 staging-test.js 脚本用k6模拟100并发,但故意在请求头加 X-Chaos-Delay: 500 ,触发服务网格的延迟注入规则。如果P95延迟超过300ms,流水线自动失败。这比任何单元测试都真实——它验证的是“在故障条件下,服务是否仍满足SLA”。我坚持让算法同学也参与写混沌测试用例,比如针对NLP模型,专门构造含特殊Unicode字符(如零宽空格)的输入,验证预处理模块是否鲁棒。这种协作,让算法和工程的边界从“交付物交接”变成“质量共担”。

3.3 步骤3:K8s部署——不是 kubectl apply ,而是声明式资源编排

deployment.yaml 扔进K8s就完事?太天真。Part 4的K8s部署必须解决三个核心矛盾:

  1. 资源争抢矛盾 :GPU节点上多个模型服务共享显存,但PyTorch默认不释放显存。解决方案是 deployment.yaml 里加 resources.limits.nvidia.com/gpu: 1 ,并设置 env

    env:
      - name: PYTORCH_CUDA_ALLOC_CONF
        value: "max_split_size_mb:128"
    

    这强制PyTorch内存分配器更激进地回收碎片,实测让单卡并发能力提升3倍。

  2. 冷启动矛盾 :新Pod启动后首次推理慢。我们在 initContainers 里预热:

    initContainers:
      - name: warmup-model
        image: <our-registry>/model-api:latest
        command: ['sh', '-c']
        args: ['curl -X POST http://localhost:8000/predict -H "Content-Type: application/json" -d "{\"image_base64\":\"...\"}" > /dev/null']
        resources:
          limits:
            nvidia.com/gpu: 1
    

    注意:initContainer必须申请GPU资源,否则无法访问 /dev/nvidia* 设备。

  3. 配置漂移矛盾 :环境变量 MODEL_VERSION=v2.1 在代码里硬编码?绝对不行。我们用K8s ConfigMap + Downward API:

    env:
      - name: MODEL_VERSION
        valueFrom:
          configMapKeyRef:
            name: model-config
            key: version
    

    并通过Argo CD同步ConfigMap,确保配置变更和代码发布原子性。

3.4 步骤4:可观测性埋点——不是“加个Prometheus”,而是业务指标驱动

很多团队只监控 cpu_usage_percent ,但Part 4必须监控 业务语义指标 。我在FastAPI里埋了三类指标:

  1. 推理性能指标

    # 使用prometheus_client
    from prometheus_client import Histogram, Counter
    PREDICT_DURATION = Histogram(
        'model_predict_duration_seconds',
        'Model prediction duration',
        ['model_name', 'status']  # status: success/fail
    )
    @app.post("/predict")
    async def predict(request: PredictRequest):
        start_time = time.time()
        try:
            result = model.predict(request.image_base64)
            PREDICT_DURATION.labels(model_name="resnet50", status="success").observe(time.time() - start_time)
            return result
        except Exception as e:
            PREDICT_DURATION.labels(model_name="resnet50", status="fail").observe(time.time() - start_time)
            raise e
    
  2. 数据漂移指标 :每1000次请求,采样100个输入,计算像素均值分布,用KS检验对比训练集分布,若p-value<0.01则告警:

    # 在后台任务中执行
    if request_count % 1000 == 0:
        drift_score = ks_2samp(train_pixel_mean, current_batch_mean).pvalue
        DRIFT_SCORE.set(drift_score)
        if drift_score < 0.01:
            alert_manager.send("Data drift detected on resnet50 input!")
    
  3. 业务效果指标 :不是模型准确率,而是线上AB测试的转化率。在 /predict 返回体里加 "ab_test_group": "control" 字段,前端上报点击行为,BI系统关联计算CTR提升。

这些指标统一推送到Prometheus,Grafana看板按“服务健康”、“模型性能”、“数据质量”、“业务影响”四象限组织。运维不再问“CPU高不高”,而是问“今天数据漂移告警几次?对应哪个业务渠道?”——这才是Part 4该有的观测深度。

3.5 步骤5:灰度发布——不是“先发10%”,而是基于特征的渐进式放量

kubectl set image deployment/model-api model-api=<new-image> ?那是裸奔。Part 4的灰度必须 基于请求特征 ,而非简单流量比例。我们用Istio VirtualService实现:

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: model-api
spec:
  hosts:
  - model-api.example.com
  http:
  - match:
    - headers:
        x-user-tier:
          exact: "vip"  # VIP用户直通新版本
    route:
    - destination:
        host: model-api-v2
        subset: v2
  - match:
    - headers:
        x-canary:
          exact: "true"  # 内部测试流量
    route:
    - destination:
        host: model-api-v2
        subset: v2
  - route:  # 其余流量走旧版
    - destination:
        host: model-api-v1
        subset: v1

关键在于 x-user-tier 头由前端SDK根据用户积分等级自动注入,无需后端改造。灰度策略分三阶段:1)内部员工( x-canary:true )全量;2)VIP用户( x-user-tier:vip )100%;3)随机1%普通用户(用Envoy Filter按 request_id 哈希分流)。每阶段观察2小时,重点看 model_predict_duration_seconds_bucket{le="0.2"} 占比是否下降——P95进入200ms桶才是真稳定。我们曾因忽略这点,在第二阶段放量后发现新模型在低分辨率图片上延迟飙升,及时回滚,避免影响普通用户。

3.6 步骤6:回滚机制——不是 kubectl rollout undo ,而是秒级服务恢复

“回滚”不是删除新Deployment,而是 流量切换+状态隔离 。我们的回滚方案分两步:

  1. 流量秒切 :Istio VirtualService里预置两个 http.route 规则,用 weight 控制流量:

    http:
    - route:
      - destination:
          host: model-api-v1
          subset: v1
        weight: 100  # 初始100%切旧版
      - destination:
          host: model-api-v2
          subset: v2
        weight: 0
    

    回滚时只需 kubectl patch vs model-api -p '{"spec":{"http":[{"route":[{"weight":100},{"weight":0}]}]}}' ,耗时<200ms,用户无感。

  2. 状态隔离 :新版本服务启动时,自动向Redis写入 model-api:v2:status=deploying ,健康检查端点 /healthz 读取此key,若值为 deploying 则返回 503 Service Unavailable ,确保ALB不将流量导给未就绪实例。回滚后,旧版本Pod的 /healthz 返回 200 ,新版本Pod因key不存在或值为 rollback 而持续返回503,直到被K8s自动驱逐。

这套机制让我们回滚平均耗时1.8秒,远低于K8s默认的 minReadySeconds=10s 限制。根本原因是:我们把“服务可用性”判断从K8s的 Readiness Probe (只检查端口)升级为业务语义检查(检查Redis状态)。

3.7 步骤7:生产就绪检查清单——不是“上线了”,而是“签收了”

Part 4的终点不是 kubectl get pods 看到Running,而是完成一份 生产就绪检查清单(Production Readiness Checklist) ,由SRE、安全、合规三方签字。这份清单共21项,我挑最关键的7项说:

序号 检查项 验证方法 不通过后果
1 模型权重哈希与训练环境一致 sha256sum /app/models/*.pth 对比CI流水线存档 阻止部署,触发审计
2 所有敏感配置(API Key、DB密码)通过K8s Secret注入,非环境变量 kubectl get secret model-api-secrets -o yaml | grep -q "password" 重新设计配置方案
3 日志包含 request_id 且全链路透传(ALB→K8s→FastAPI→下游服务) 抽样10个 request_id ,验证各服务日志是否完整 补全OpenTelemetry SDK配置
4 健康检查端点 /healthz 响应时间<100ms,且不依赖下游服务 ab -n 1000 -c 100 http://prod-api/healthz 重构健康检查逻辑
5 模型服务OOMKill次数为0(过去7天) kubectl top pods | grep model-api | awk '{print $3}' | grep Mi 调整 resources.requests.memory
6 数据输入符合OpenAPI契约,非法输入返回400而非500 Postman批量发送 {"image_base64":"invalid"} 修复Pydantic模型校验
7 每日自动备份模型权重至S3,且备份文件可下载验证 aws s3 ls s3://model-backup/resnet50/ | tail -1 配置S3 Lifecycle策略

这份清单不是形式主义。去年我们因第2项未通过(API Key硬编码在ConfigMap里),被安全团队叫停上线,最终发现该Key已被泄露在某GitHub公开仓库——正是这份清单救了我们。Part 4的终极意义,就是把“能跑”变成“敢交”。

4. 真实踩坑记录:那些让Part 4延期的隐形炸弹

4.1 坑1:GPU显存“幽灵泄漏”——你以为释放了,其实没释放

现象:模型服务运行24小时后, nvidia-smi 显示显存占用从1.2GB涨到3.8GB, torch.cuda.memory_allocated() 却只报1.5GB。重启Pod后立即回落,但几小时后又爬升。
根因:PyTorch的CUDA缓存机制。 torch.cuda.empty_cache() 只清空缓存池,不释放给OS;而某些第三方库(如 albumentations 的GPU版)在 __del__ 里没调用 cudaFree
解决方案:

  1. 在FastAPI的 /predict 函数末尾强制清理:
    import gc
    torch.cuda.empty_cache()
    gc.collect()  # 强制Python垃圾回收
    
  2. 更治本的是用 nvidia-docker --memory 参数限制容器显存:
    docker run --gpus all --memory=4g --memory-swap=4g <image>
    
    当容器内显存超限时,OOM Killer会杀掉泄漏进程,比等服务崩溃强。

提示:别信“PyTorch 2.0已修复”,我们实测2.1.2仍有此问题,必须双保险。

4.2 坑2:时区混乱——UTC时间戳被当成本地时间

现象:模型服务日志里 2023-10-05T02:30:00Z 的请求,在BI报表里显示为“昨天18:30”,导致AB测试数据错乱。
根因:FastAPI默认用 datetime.now() ,而Docker基础镜像 nvidia/cuda 的时区是 UTC ,但公司BI系统按 Asia/Shanghai 解析时间戳。
解决方案:

  1. Dockerfile里显式设置时区:
    RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
        echo "Asia/Shanghai" > /etc/timezone
    
  2. FastAPI里统一用 datetime.now(timezone.utc) 生成时间戳,前端解析时指定时区:
    // 前端JS
    new Date("2023-10-05T02:30:00Z").toLocaleString('zh-CN', {timeZone: 'Asia/Shanghai'})
    

注意:不要用 datetime.utcnow() ,它返回naive datetime,无时区信息,是Python 3.12已弃用的危险操作。

4.3 坑3:gRPC over HTTP/2的连接复用失效

现象:用gRPC Python client调用模型服务,QPS超200后大量 StatusCode.UNAVAILABLE 错误, grpc-status: 14
根因:gRPC默认启用HTTP/2连接复用,但ALB的HTTP/2支持有缺陷——它会重置空闲连接,而gRPC client未及时探测。
解决方案:

  1. 客户端配置心跳:
    channel = grpc.insecure_channel(
        'model-api.example.com:443',
        options=[
            ('grpc.keepalive_time_ms', 30000),
            ('grpc.keepalive_timeout_ms', 10000),
            ('grpc.http2.max_pings_without_data', 0),
        ]
    )
    
  2. ALB侧禁用HTTP/2,强制HTTP/1.1(牺牲一点性能,换稳定性):
    aws elbv2 modify-listener --listener-arn <arn> --default-actions Type=forward,TargetGroupArn=<tg-arn> --protocol HTTP --port 80
    

实测HTTP/1.1下P99延迟增加12ms,但错误率从5%降至0.02%,值得。

4.4 坑4:模型版本管理的“薛定谔状态”

现象:SRE反馈“线上跑的是v2.1,但日志里打印 model_version=v2.0 ”,查代码发现 __version__ 写死在 pyproject.toml 里,而Docker镜像构建时没注入Git tag。
根因:版本号来源不唯一。 pyproject.toml Dockerfile K8s manifest OpenAPI spec 四份文件各自维护版本,必然不一致。
解决方案: Git tag驱动一切 。CI流水线第一步:

# GitHub Actions
- name: Extract version from git tag
  id: version
  run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Build and push
  run: |
    docker build -t ${{ secrets.ECR_REPO }}:${{ env.VERSION }} .
    docker push ${{ secrets.ECR_REPO }}:${{ env.VERSION }}
- name: Deploy to K8s
  run: |
    sed -i "s/{{MODEL_VERSION}}/${{ env.VERSION }}/g" k8s/deployment.yaml
    kubectl apply -f k8s/deployment.yaml

同时在FastAPI里动态读取:

from importlib.metadata import version
@app.get("/info")
def get_info():
    return {"model_version": version("my-model-package")}

这样所有地方的版本号都来自同一个Git tag,杜绝“薛定谔版本”。

4.5 坑5:CI流水线里的“幽灵依赖”

现象:本地 poetry install 成功,CI里 poetry install --no-dev 失败,报 ModuleNotFoundError: No module named 'sklearn'
根因: pyproject.toml sklearn [tool.poetry.dependencies] 下,但 poetry.lock 文件未提交到Git,CI每次重新生成lock,而 sklearn 的最新版(1.3.0)要求 numpy>=1.24.0 ,但 pyproject.toml 里锁的是 numpy=1.23.5 ,冲突。
解决方案:

  1. 强制提交 poetry.lock :这是Poetry最佳实践,但很多团队忽略;
  2. CI里用 poetry install --no-dev --sync --sync 会删掉lock文件里没有的包,确保环境纯净;
  3. 更彻底的是用 pip-tools 替代Poetry: pip-compile requirements.in 生成 requirements.txt ,再 pip install -r requirements.txt ,虽失去Poetry的虚拟环境管理,但lock文件确定性100%。

我现在所有新项目都用pip-tools,因为Part 4要的是确定性,不是开发便利性。

5. Part 4之后:当模型服务成为业务基础设施

Part 4结束,不等于故事终结。真正考验在上线后30天——当模型服务从“新功能”变成“业务基础设施”,它开始暴露更深层的问题。我见过太多团队在Part 4欢呼雀跃,三个月后却默默下线服务,原因惊人一致: 没人负责持续迭代 。模型不是静态艺术品,它是活的系统。上周我们监控到 model_predict_duration_seconds_bucket{le="0.2"} 占比从92%跌到85%,排查发现是上游图片服务升级后,JPEG压缩率提高,导致解码后Tensor尺寸变大。这不是算法问题,也不是工程问题,而是 跨团队SLA缺失 ——图片服务承诺“输出尺寸≤1920x1080”,但没承诺“解码后内存占用≤5MB”。Part 4教会我们一件事:部署不是终点,而是建立 模型服务生命周期管理(MLSM) 的起点。这包括:每周自动运行数据漂移检测,每月人工审核特征重要性变化,每季度用新数据重训并AB测试,每年审计模型偏见(bias audit)报告。这些工作不能靠个人自觉,必须写进SRE的oncall手册,纳入产品经理的OKR。所以Part 4的真正产出,不该是一个能跑的API,而是一份《模型服务SLO协议》,里面白纸黑字写着:“当P95延迟>200ms持续15分钟,SRE自动扩容;当数据漂移p-value<0.001,算法团队48小时内响应;当业务指标(如CTR)下降5%,触发模型重训流程。”——这才是“ML in the Real World”的终极形态:不是让模型跑起来,而是让它像电力、网络一样,成为公司可信的基础设施。我在实际操作中发现,最难的不是技术实现,而是推动法务部在SLO协议上盖章。但一旦签了字,Part 4才算真正落地。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐