
1. 从零搭建AI工程能力为什么我劝你别再收藏夹吃灰了ai-engineering-from-scratch这个标题第一次看到的时候我愣了一下。不是因为它有多高深恰恰相反——它太直白了直白到像一句废话。AI工程从零开始谁不知道要从零开始但真正让我停下来琢磨的是from scratch这四个字背后藏着的那层意思不是从入门到精通不是快速上手而是从一片空白的地基开始把每一块砖都亲手砌上去。我做了十多年一线开发带过不少新人也见过太多人在AI工程这条路上反复横跳。有人上来就调API跑通了几个demo就觉得自己会了有人一头扎进论文堆里三个月后连一个能跑的服务都没部署出来还有人收藏了上百个AI工程学习路线硬盘里躺着几十个G的教程实际动手写过的代码不超过五百行。这些坑我都踩过或者说我围观过别人踩。所以当我看到ai-engineering-from-scratch这个项目标题时我脑子里第一反应是终于有人愿意把从零这两个字当真了。这个项目本质上是一个面向AI工程化落地的系统性实践框架它要解决的问题不是怎么训练一个大模型——那是算法工程师的事——而是怎么把一个AI能力变成稳定、可维护、可扩展的线上服务。说得再直白一点模型跑在notebook里是一回事跑在千万级用户的请求链路里是另一回事。中间隔着的就是AI工程要填的坑。适合谁来参考如果你已经会写Python能看懂基本的模型推理代码但一提到部署监控版本管理灰度发布就头皮发麻那这个方向就是为你准备的。如果你还在纠结要不要学AI那可以先划走这篇内容对你来说太早了。我接下来要拆的是基于这个标题所指向的AI工程从零构建的完整路径。不是那种三天速成的爽文而是一个从业者真正从零开始搭一套AI工程体系时会遇到的选型逻辑、实操细节和那些文档里不会写的坑。我会尽量把每个为什么讲透让你看完能直接抄作业而不是又往收藏夹里塞一篇以后再看。2. 整体设计思路为什么从零不等于从底层造轮子2.1 先搞清楚零的起点在哪里很多人对from scratch有误解以为要从零实现一个Transformer、从零写一个推理引擎、从零搭一个Kubernetes集群。我见过一个哥们儿花了两个月时间用C手写了一个简易的矩阵运算库结果发现PyTorch一行代码就能搞定而且性能是他的几百倍。这不是从零这是从负开始。AI工程里的从零起点应该是你有一个能跑的模型但不知道怎么让它稳定服务。这个起点很关键因为它决定了你后面所有工作的重心。你的核心任务不是造模型而是造一套让模型能持续产生价值的工程体系。这套体系包括但不限于模型服务的封装、请求链路的编排、版本管理与回滚、性能监控与告警、数据回流与迭代。这些东西才是AI工程师和算法工程师的分水岭。所以整体设计的第一条原则就是能用成熟工具解决的绝不自己造。这不是偷懒这是工程效率的基本素养。你花三个月造一个轮子别人用现成方案三天就上线了三个月后别人的系统已经迭代了五个版本你的轮子还在修bug。AI工程这个领域时间窗口比什么都重要。2.2 分层架构把复杂问题切成能一口吃掉的小块我习惯把AI工程体系分成四层从下往上依次是基础设施层、模型服务层、应用编排层、业务接入层。这个分层不是教科书上的标准答案是我自己在实际项目中反复调整后觉得最顺手的切法。基础设施层管的是算力、存储、网络这些底层资源。这一层你基本不用从零写但你要知道怎么用。比如GPU资源怎么调度、模型文件怎么存储和分发、服务之间的网络怎么隔离。我见过太多人在这一层翻车模型文件放在本地磁盘上服务扩容的时候新节点拉不到模型GPU显存不够多个模型抢资源导致服务雪崩。这些问题不是算法问题是工程问题但杀伤力一点不比算法问题小。模型服务层是核心管的是怎么把模型变成一个能接收请求、返回结果的HTTP或gRPC服务。这一层的关键词是封装和隔离。封装是指把模型的加载、推理、后处理逻辑打包成一个独立的服务单元隔离是指不同模型、不同版本之间不能互相干扰。我试过在一个进程里加载多个模型结果一个模型的内存泄漏把整个服务拖垮了。从那以后我坚持一个模型一个服务实例宁可多占点资源也要保证故障隔离。应用编排层管的是多个模型服务之间的协作。一个真实的AI应用往往不是单个模型能搞定的比如一个智能客服系统可能需要意图识别模型、实体抽取模型、情感分析模型、回复生成模型协同工作。这一层要解决的是请求怎么在多个服务之间流转、超时怎么处理、某个服务挂了怎么降级。这一层是最能体现AI工程功力的地方也是从零搭建时最容易忽略的地方。业务接入层管的是怎么让业务方用起来。这一层要提供统一的API网关、鉴权、限流、计费、日志。很多技术出身的AI工程师觉得这一层不够硬核不愿意花时间结果业务方接入的时候发现连个像样的文档都没有最后项目黄了。我的经验是业务接入层的体验直接决定了你的AI能力能不能真正被用起来。2.3 技术选型的核心逻辑别追新追稳AI工程领域的技术栈更新速度堪比时装周今天流行这个框架明天流行那个工具。我见过太多团队被新技术牵着鼻子走半年换一套架构最后什么都没沉淀下来。我的选型逻辑很简单优先选社区活跃、文档齐全、有大规模生产验证的方案。具体来说模型服务化我首选FastAPI加Uvicorn不是因为它性能最好而是因为它足够简单、足够稳定、生态足够丰富。你可能会说Triton Inference Server性能更好没错但Triton的学习曲线和运维复杂度也更高。如果你的团队没有专门的推理优化工程师FastAPI是更务实的选择。等你的QPS真的到了FastAPI扛不住的时候再换Triton也不迟而且那时候你已经有了足够的工程积累换起来反而更快。容器化我首选Docker加Docker Compose而不是一上来就上Kubernetes。K8s很强但它的复杂度对于从零开始的团队来说往往是负担大于收益。我见过一个五人团队花了两个月搭K8s集群结果模型服务还没上线。Docker Compose足够支撑你从零到日请求百万级的场景等真的到了瓶颈再迁移迁移成本远小于你一开始就硬上K8s的维护成本。监控我首选Prometheus加Grafana日志我首选ELK或Loki。这些方案都是经过大规模验证的社区里遇到问题一搜就有答案。不要自己造监控系统那是一个无底洞。3. 核心细节解析从零搭建AI工程体系的五个关键环节3.1 模型服务化把notebook里的代码变成能扛住流量的服务模型服务化是从零搭建AI工程体系的第一道坎。很多人觉得这事很简单把notebook里的推理代码复制出来包一个Flask接口完事。我一开始也这么想直到线上服务在压测的时候直接崩了。问题出在几个地方。第一模型加载时机。如果你在每次请求里都加载一次模型那延迟会高到无法接受。正确的做法是在服务启动时加载模型并常驻内存。但这里有个坑如果你的服务是多worker的每个worker都会加载一份模型显存直接翻倍。我试过用Gunicorn起4个worker结果24G显存的卡直接OOM。解决方案是用单worker多线程或者用共享内存的方式加载模型。第二请求批处理。单个请求推理一次GPU利用率极低。你需要把短时间内到达的多个请求合并成一个batch一次性推理然后拆分结果返回。这个逻辑听起来简单但实现起来要考虑超时控制、batch大小动态调整、不同请求的输入长度差异等问题。我建议用现成的方案比如NVIDIA的Triton或者Ray Serve它们内置了动态批处理能力。如果你非要自己写记住一个原则batch等待时间不能超过业务可接受的额外延迟。比如你的业务要求P99延迟在200ms以内那batch等待时间最多设20ms。第三健康检查与优雅退出。服务要能告诉负载均衡器我还活着也要能在收到终止信号时把手头的请求处理完再退出。我见过一个服务滚动更新的时候直接kill进程导致正在处理的请求全部失败。这种问题在测试环境很难发现一到生产环境就是事故。下面是一个我常用的FastAPI模型服务模板你可以直接参考from fastapi import FastAPI, HTTPException from pydantic import BaseModel import torch import asyncio from concurrent.futures import ThreadPoolExecutor import signal import sys app FastAPI() model None executor ThreadPoolExecutor(max_workers4) is_ready False class PredictRequest(BaseModel): text: str max_length: int 128 class PredictResponse(BaseModel): result: str latency_ms: float app.on_event(startup) async def load_model(): global model, is_ready # 模型加载放在启动阶段避免请求时加载 model torch.load(/models/my_model.pt, map_locationcuda) model.eval() is_ready True app.get(/health) async def health(): if not is_ready: raise HTTPException(status_code503, detailModel not ready) return {status: ok} app.post(/predict, response_modelPredictResponse) async def predict(req: PredictRequest): if not is_ready: raise HTTPException(status_code503, detailModel not ready) loop asyncio.get_event_loop() # 推理是CPU/GPU密集型操作放到线程池避免阻塞事件循环 result await loop.run_in_executor( executor, run_inference, req.text, req.max_length ) return result def run_inference(text, max_length): import time start time.time() with torch.no_grad(): output model(text, max_lengthmax_length) latency (time.time() - start) * 1000 return PredictResponse(resultoutput, latency_mslatency) def graceful_shutdown(signum, frame): global is_ready is_ready False # 等待正在处理的请求完成 executor.shutdown(waitTrue) sys.exit(0) signal.signal(signal.SIGTERM, graceful_shutdown)这个模板里有几个细节值得说。is_ready标志位是为了让健康检查在模型加载完成前返回503避免流量打到还没准备好的实例上。run_in_executor是为了不让推理阻塞FastAPI的事件循环否则并发能力会大打折扣。graceful_shutdown是为了在收到终止信号时先停止接收新请求再等待已有请求完成。注意模型加载时间可能很长如果你的模型有几十个G启动可能要几分钟。这时候要确保健康检查的初始延迟设置得足够长否则负载均衡器会在服务还没准备好的时候就把它踢掉。3.2 版本管理与灰度发布别让新模型成为定时炸弹模型版本管理是AI工程里最容易被低估的环节。我见过太多团队用文件名来区分版本比如model_v1.pt、model_v2.pt然后手动改配置切换。这种做法在模型少的时候还能凑合一旦模型数量上来了就是灾难。正确的做法是把模型版本当成代码版本来管理。每个模型版本有唯一的标识符有完整的元数据训练数据、超参数、评估指标、上线时间有可追溯的变更记录。我推荐用MLflow或者DVC来管理模型版本它们能帮你把模型文件和元数据关联起来还能和Git提交挂钩。但版本管理只是第一步更关键的是灰度发布。新模型上线你不能直接把所有流量切过去万一效果变差了呢我一般的做法是先切1%的流量到新模型观察核心指标准确率、延迟、错误率24小时如果没问题切10%再观察24小时然后50%最后100%。整个过程大概需要一周。听起来很慢但比起新模型上线导致线上事故这一周的时间成本几乎可以忽略不计。灰度发布的实现方式有很多种。最简单的是在网关层做流量切分根据请求的某个特征比如用户ID的哈希值决定走新模型还是旧模型。这种方式对模型服务本身没有侵入但需要网关支持。另一种方式是在模型服务内部做同一个服务加载新旧两个模型根据请求头里的版本号决定用哪个。这种方式更灵活但会占用双倍资源。我倾向于第一种方式因为模型服务应该保持纯粹不应该承担流量调度的职责。下面是一个用Nginx做灰度发布的配置示例upstream model_v1 { server model-v1:8000; } upstream model_v2 { server model-v2:8000; } split_clients ${remote_addr}${http_user_agent} $model_version { 1% v2; * v1; } server { listen 80; location /predict { if ($model_version v2) { proxy_pass http://model_v2; } if ($model_version v1) { proxy_pass http://model_v1; } } }这个配置用split_clients模块根据客户端特征做流量切分1%的流量走v299%走v1。调整百分比只需要改一个数字非常方便。实操心得灰度发布期间一定要做好指标对比。我一般会在Grafana上放两个面板一个显示新模型的指标一个显示旧模型的指标用同样的时间轴。这样一旦新模型有问题一眼就能看出来。另外灰度期间要确保日志里能区分请求走的是哪个版本否则出了问题都不知道该查哪个模型的日志。3.3 监控与告警让问题在你发现之前就被发现AI工程的监控和传统后端监控有一个很大的区别除了系统指标你还要监控模型指标。系统指标包括QPS、延迟、错误率、CPU/GPU利用率、内存占用模型指标包括预测分布、置信度分布、输入数据分布。后者往往更重要因为模型效果下降不一定体现在系统指标上。我见过一个案例一个推荐模型上线后系统指标一切正常延迟没变错误率没变但业务方反馈点击率下降了15%。查了半天才发现模型对某一类用户的预测结果全部偏向同一个类别导致推荐多样性大幅下降。这个问题在系统监控里完全看不出来只有监控了预测分布才能发现。所以我的监控体系里模型指标和系统指标是同等重要的。具体来说我会监控以下几类指标指标类别具体指标监控目的告警阈值建议系统指标QPS、P50/P95/P99延迟评估服务性能P99延迟超过基线50%系统指标错误率、超时率发现服务异常错误率超过1%资源指标GPU利用率、显存占用评估资源瓶颈显存占用超过90%模型指标预测类别分布发现模型退化分布偏移超过阈值模型指标置信度分布发现模型不确定性低置信度请求占比突增业务指标点击率、转化率评估业务效果下降超过10%监控工具我用的是Prometheus加Grafana。Prometheus负责采集和存储指标Grafana负责展示和告警。模型指标的采集需要在推理代码里埋点比如每次推理后把预测结果的分布上报到Prometheus。下面是一个简单的埋点示例from prometheus_client import Counter, Histogram, Gauge import numpy as np # 定义指标 prediction_counter Counter( model_predictions_total, Total predictions, [predicted_class] ) confidence_histogram Histogram( model_confidence, Prediction confidence, buckets[0.1, 0.3, 0.5, 0.7, 0.9, 0.95, 0.99] ) input_length_gauge Gauge( model_input_length, Input text length ) def run_inference_with_metrics(text): # 推理 result model(text) predicted_class result.argmax().item() confidence result.softmax(dim-1).max().item() # 上报指标 prediction_counter.labels(predicted_classstr(predicted_class)).inc() confidence_histogram.observe(confidence) input_length_gauge.set(len(text)) return result注意模型指标的告警阈值不能拍脑袋定要用历史数据来算。我一般的做法是上线后先观察一周记录指标的均值和标准差然后把告警阈值设为均值加减三倍标准差。这样既能发现异常又不会因为正常波动频繁告警。3.4 数据回流与持续迭代让模型越用越聪明AI工程和传统软件工程最大的区别在于传统软件上线后行为是确定的AI模型上线后行为会随着数据分布的变化而漂移。今天效果好的模型三个月后可能就退化了。所以数据回流和持续迭代是AI工程体系里不可或缺的一环。数据回流的核心是把线上推理的输入数据和业务反馈的结果收集起来用于后续的模型迭代。这里有几个关键点。第一数据采集要合规。用户隐私数据不能随便存该脱敏的脱敏该加密的加密。第二数据存储要结构化。不能把请求日志随便扔到一个文件里要存到数据库或者数据湖里方便后续查询和处理。第三反馈闭环要建立。光收集数据没用要有机制把数据转化成训练样本定期触发模型迭代。我一般的做法是在推理服务里加一个异步的数据上报逻辑把请求的输入、输出、时间戳、模型版本等信息发到Kafka然后由一个消费程序写入数据仓库。业务侧的反馈比如用户点击、评分通过另一个通道写入最后在数据仓库里做join生成训练样本。from kafka import KafkaProducer import json import hashlib producer KafkaProducer( bootstrap_servers[kafka:9092], value_serializerlambda v: json.dumps(v).encode(utf-8) ) def report_inference_data(text, result, model_version): # 脱敏处理 text_hash hashlib.sha256(text.encode()).hexdigest() data { text_hash: text_hash, text_length: len(text), predicted_class: str(result.argmax().item()), confidence: float(result.softmax(dim-1).max().item()), model_version: model_version, timestamp: time.time() } producer.send(inference_logs, valuedata)这个逻辑是异步的不会阻塞推理请求。Kafka的吞吐量足够大不用担心成为瓶颈。实操心得数据回流最容易犯的错误是只收集不处理。我见过一个团队收集了半年的推理数据结果要用的时候发现数据格式乱七八糟清洗就花了一个月。所以从一开始就要定好数据schema并且写一个校验程序确保写入的数据符合预期格式。另外数据量大的时候要考虑冷热分离最近一个月的数据放热存储方便查询更早的数据归档到冷存储降低成本。3.5 成本控制AI工程不只是技术问题更是经济问题从零搭建AI工程体系成本是一个绕不开的话题。GPU资源贵、存储贵、带宽贵如果不加控制账单能吓死人。我见过一个团队模型服务用的是最高配的GPU实例结果利用率只有5%钱全浪费了。成本控制的核心思路是按需分配、弹性伸缩。具体来说有几个手段。第一模型量化。把FP32的模型量化成FP16或者INT8显存占用能减少一半到四分之三推理速度还能提升。精度损失通常在可接受范围内我实测下来大部分模型量化后准确率下降不超过1%。第二动态扩缩容。根据QPS自动调整实例数量高峰期多开低峰期少开。K8s的HPA或者云厂商的弹性伸缩服务都能做这个。第三混合部署。把延迟不敏感的离线任务和延迟敏感的在线服务混部在同一张卡上提高GPU利用率。模型量化我用的是ONNX Runtime或者TensorRT它们都支持训练后量化不需要重新训练模型。下面是一个用ONNX Runtime做FP16量化的示例import onnx from onnxruntime.transformers import float16 # 加载原始FP32模型 model onnx.load(model_fp32.onnx) # 转换为FP16 model_fp16 float16.convert_float_to_float16(model) # 保存 onnx.save(model_fp16, model_fp16.onnx)量化后的模型推理速度通常能提升1.5到2倍显存占用减少一半。如果你的模型是Transformer架构还可以用ONNX Runtime的优化工具做算子融合和图层优化进一步提升性能。注意量化不是万能的。有些模型对数值精度非常敏感量化后效果会明显下降。所以量化后一定要做完整的评估对比量化前后的指标。如果下降超过可接受范围就要考虑混合精度量化只量化对精度不敏感的部分。4. 实操过程从零搭建一个完整的AI工程Demo4.1 环境准备与项目结构说了这么多理论接下来我带你走一遍完整的实操流程。我们以一个文本分类模型为例从零搭建一个包含模型服务、监控、灰度发布、数据回流的完整AI工程Demo。先看项目结构ai-engineering-demo/ ├── docker-compose.yml ├── model-service/ │ ├── Dockerfile │ ├── requirements.txt │ ├── app.py │ ├── model_loader.py │ └── metrics.py ├── gateway/ │ └── nginx.conf ├── monitoring/ │ ├── prometheus.yml │ └── grafana-dashboard.json ├──>import torch import torch.nn as nn from transformers import AutoTokenizer, AutoModelForSequenceClassification import logging logger logging.getLogger(__name__) class ModelLoader: def __init__(self, model_path, deviceNone): self.model_path model_path self.device device or (cuda if torch.cuda.is_available() else cpu) self.model None self.tokenizer None def load(self): logger.info(fLoading model from {self.model_path} on {self.device}) self.tokenizer AutoTokenizer.from_pretrained(self.model_path) self.model AutoModelForSequenceClassification.from_pretrained( self.model_path ) self.model.to(self.device) self.model.eval() logger.info(Model loaded successfully) return self def predict(self, text, max_length128): inputs self.tokenizer( text, return_tensorspt, max_lengthmax_length, truncationTrue, paddingTrue ) inputs {k: v.to(self.device) for k, v in inputs.items()} with torch.no_grad(): outputs self.model(**inputs) logits outputs.logits probs torch.softmax(logits, dim-1) return { predicted_class: int(probs.argmax().item()), confidence: float(probs.max().item()), probabilities: probs.cpu().numpy().tolist()[0] }这个类封装了模型加载和推理逻辑。注意几个细节model.eval()是必须的否则Dropout和BatchNorm的行为会不一致torch.no_grad()能减少显存占用输入数据要移到和模型相同的设备上。再看metrics.pyfrom prometheus_client import Counter, Histogram, Gauge, generate_latest import time PREDICTION_COUNTER Counter( model_predictions_total, Total number of predictions, [predicted_class, model_version] ) PREDICTION_LATENCY Histogram( model_prediction_latency_seconds, Prediction latency in seconds, buckets[0.01, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0] ) CONFIDENCE_HISTOGRAM Histogram( model_confidence, Prediction confidence distribution, buckets[0.1, 0.3, 0.5, 0.7, 0.9, 0.95, 0.99] ) MODEL_READY Gauge( model_ready, Whether the model is ready to serve ) def record_prediction(predicted_class, confidence, latency, model_version): PREDICTION_COUNTER.labels( predicted_classstr(predicted_class), model_versionmodel_version ).inc() PREDICTION_LATENCY.observe(latency) CONFIDENCE_HISTOGRAM.observe(confidence)这些指标覆盖了系统性能和模型行为两个维度。PREDICTION_COUNTER按预测类别和模型版本分组能看出模型是否偏向某个类别CONFIDENCE_HISTOGRAM能看出模型的不确定性分布PREDICTION_LATENCY能看出服务性能。最后是app.pyfrom fastapi import FastAPI, HTTPException, Request from fastapi.responses import Response from pydantic import BaseModel from prometheus_client import generate_latest, CONTENT_TYPE_LATEST import asyncio from concurrent.futures import ThreadPoolExecutor import time import signal import sys import os import logging from model_loader import ModelLoader from metrics import record_prediction, MODEL_READY logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) app FastAPI(titleAI Model Service) executor ThreadPoolExecutor(max_workers4) model_loader None is_ready False MODEL_VERSION os.getenv(MODEL_VERSION, v1) class PredictRequest(BaseModel): text: str max_length: int 128 class PredictResponse(BaseModel): predicted_class: int confidence: float probabilities: list model_version: str latency_ms: float app.on_event(startup) async def startup(): global model_loader, is_ready model_path os.getenv(MODEL_PATH, /models/text-classifier) model_loader ModelLoader(model_path).load() is_ready True MODEL_READY.set(1) logger.info(fService ready with model version {MODEL_VERSION}) app.get(/health) async def health(): if not is_ready: raise HTTPException(status_code503, detailModel not ready) return {status: ok, model_version: MODEL_VERSION} app.get(/metrics) async def metrics(): return Response( contentgenerate_latest(), media_typeCONTENT_TYPE_LATEST ) app.post(/predict, response_modelPredictResponse) async def predict(req: PredictRequest): if not is_ready: raise HTTPException(status_code503, detailModel not ready) start time.time() loop asyncio.get_event_loop() try: result await loop.run_in_executor( executor, model_loader.predict, req.text, req.max_length ) except Exception as e: logger.error(fPrediction failed: {e}) raise HTTPException(status_code500, detailPrediction failed) latency time.time() - start record_prediction( result[predicted_class], result[confidence], latency, MODEL_VERSION ) return PredictResponse( predicted_classresult[predicted_class], confidenceresult[confidence], probabilitiesresult[probabilities], model_versionMODEL_VERSION, latency_mslatency * 1000 ) def graceful_shutdown(signum, frame): global is_ready logger.info(Shutting down gracefully...) is_ready False MODEL_READY.set(0) executor.shutdown(waitTrue) sys.exit(0) signal.signal(signal.SIGTERM, graceful_shutdown)这个服务提供了三个接口/health用于健康检查/metrics用于Prometheus采集指标/predict用于推理。MODEL_VERSION从环境变量读取方便在部署时指定版本。4.3 Docker Compose编排与网关配置docker-compose.yml把各个组件串起来version: 3.8 services: model-v1: build: ./model-service environment: - MODEL_PATH/models/text-classifier-v1 - MODEL_VERSIONv1 volumes: - ./models:/models deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] networks: - ai-net model-v2: build: ./model-service environment: - MODEL_PATH/models/text-classifier-v2 - MODEL_VERSIONv2 volumes: - ./models:/models deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] networks: - ai-net gateway: image: nginx:alpine ports: - 80:80 volumes: - ./gateway/nginx.conf:/etc/nginx/nginx.conf:ro depends_on: - model-v1 - model-v2 networks: - ai-net prometheus: image: prom/prometheus:latest ports: - 9090:9090 volumes: - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro networks: - ai-net grafana: image: grafana/grafana:latest ports: - 3000:3000 environment: - GF_SECURITY_ADMIN_PASSWORDadmin volumes: - ./monitoring/grafana-dashboard.json:/etc/grafana/provisioning/dashboards/dashboard.json networks: - ai-net networks: ai-net: driver: bridge这个编排文件起了五个服务两个模型服务v1和v2、一个网关、一个Prometheus、一个Grafana。两个模型服务分别加载不同版本的模型用于灰度发布。gateway/nginx.conf的配置前面已经给过了这里不再重复。核心就是用split_clients做流量切分。monitoring/prometheus.yml配置采集目标global: scrape_interval: 15s scrape_configs: - job_name: model-v1 static_configs: - targets: [model-v1:8000] - job_name: model-v2 static_configs: - targets: [model-v2:8000]4.4 部署与验证一切就绪后执行部署脚本#!/bin/bash # scripts/deploy.sh set -e echo Building model service image... docker-compose build echo Starting services... docker-compose up -d echo Waiting for services to be ready... sleep 30 echo Checking health... curl -f http://localhost/health || exit 1 echo Running smoke test... curl -X POST http://localhost/predict \ -H Content-Type: application/json \ -d {text: This is a test input for the model} \ | python -m json.tool echo Deployment completed successfully部署完成后你可以通过以下地址访问各个服务服务地址用途模型服务http://localhost/predict推理接口健康检查http://localhost/health服务状态Prometheushttp://localhost:9090指标查询Grafanahttp://localhost:3000监控面板验证灰度发布是否生效多次调用/predict接口观察返回的model_version字段。大约1%的请求会返回v299%返回v1。如果比例不对检查Nginx配置里的split_clients百分比。验证监控是否生效打开Prometheus的Graph页面查询model_predictions_total应该能看到按predicted_class和model_version分组的计数。查询model_prediction_latency_seconds_bucket能看到延迟分布。5. 常见问题与排查技巧实录5.1 模型服务启动慢、健康检查失败这是最常见的问题。模型加载需要时间如果健康检查的初始延迟设置得太短负载均衡器会在服务还没准备好的时候就判定为不健康导致服务刚启动就被踢掉。排查思路先看服务日志确认模型加载花了多长时间。然后在Docker Compose或者K8s的配置里把健康检查的initialDelaySeconds设置为模型加载时间的1.5倍。比如模型加载需要60秒就设90秒。另一个可能的原因是模型文件太大从磁盘加载慢。解决方案是把模型文件放到内存文件系统tmpfs里或者用更快的存储。我试过把模型从机械硬盘移到SSD加载时间从3分钟降到了30秒。5.2 GPU显存不足导致服务崩溃多worker加载模型、batch size太大、显存泄漏都可能导致OOM。排查方法是在服务启动后用nvidia-smi观察显存占用。如果启动后就占满了说明模型加载有问题如果运行一段时间后逐渐占满说明有显存泄漏。显存泄漏最常见的原因是推理时没有用torch.no_grad()导致计算图被保留。另一个原因是把中间结果存到了全局变量里没有及时释放。我一般的做法是在推理函数里用with torch.no_grad():包裹所有计算并且避免在全局作用域保存任何张量。5.3 灰度发布流量比例不对split_clients的百分比是基于请求特征的哈希值理论上大样本下比例是准确的但小样本下会有波动。如果你发现1%的配置实际切了5%的流量先确认请求量是否足够大。如果请求量只有几百波动是正常的。另一个可能的原因是Nginx的split_clients模块没有正确加载。检查Nginx版本是否支持该模块以及配置语法是否正确。可以用nginx -t测试配置。5.4 监控指标缺失或不准Prometheus采集不到指标最常见的原因是网络不通或者采集路径不对。检查Prometheus的Targets页面看目标是否处于UP状态。如果是DOWN看错误信息。常见错误包括服务没启动、端口不对、路径不对。指标不准的另一个原因是埋点位置不对。比如你把延迟埋点放在了推理函数外面那测出来的延迟就包含了网络传输和序列化的时间比实际推理延迟大。我一般的做法是在推理函数内部埋点只测模型前向传播的时间。5.5 数据回流管道堵塞Kafka消费者处理速度跟不上生产速度导致消息积压。排查方法是看Kafka的Consumer Lag指标。如果Lag持续增长说明消费能力不足。解决方案有几个增加消费者实例数、提高消费者的处理效率、批量消费。我一般的做法是批量消费一次拉100条消息批量写入数据仓库这样能大幅提升吞吐量。但要注意批量消费会增加延迟如果业务对实时性要求高就要权衡。实操心得数据回流管道一定要加监控和告警。我见过一个团队Kafka积压了几千万条消息直到磁盘满了才发现。从那以后我坚持给所有数据管道加Lag监控超过阈值就告警。5.6 模型效果下降但系统指标正常这是最棘手的问题因为系统监控看不出来。排查思路是对比新旧模型的预测分布看是否有明显偏移。如果新模型对某一类输入的预测结果全部偏向同一个类别说明模型可能过拟合了或者训练数据有偏。另一个排查方向是看输入数据分布。如果线上输入的数据分布和训练数据分布差异很大模型效果下降是必然的。这时候需要做数据回流用线上数据重新训练模型。我一般的做法是每周跑一次模型效果评估用最近的线上数据做测试集对比模型的准确率、召回率、F1。如果指标下降超过5%就触发模型迭代流程。问题现象可能原因排查方法解决方案服务启动慢模型加载耗时看启动日志增加健康检查初始延迟GPU OOM多worker加载/显存泄漏nvidia-smi观察单worker/加no_grad灰度比例不对请求量小/配置错误检查Nginx配置增大样本量/修正配置指标缺失网络不通/路径错误看Prometheus Targets修正采集配置数据积压消费能力不足看Consumer Lag增加消费者/批量消费效果下降数据漂移/模型退化对比预测分布数据回流/重新训练6. 我踩过的坑和给你的建议从零搭建AI工程体系我踩过的坑比写过的代码还多。挑几个最有代表性的说说。第一个坑是过早优化。我一开始就想着要上K8s、上Service Mesh、上全套的CI/CD结果花了两个月搭基础设施模型服务一行代码没写。后来我学乖了先用Docker Compose把核心链路跑通等业务量上来了再逐步替换。能跑起来的简单方案永远优于跑不起来的完美方案。第二个坑是忽略业务反馈。我一度沉迷于优化模型的技术指标准确率从92%提升到94%结果业务方说没感觉。后来我才明白业务方关心的是转化率、留存率这些业务指标不是技术指标。技术指标提升1%可能对业务指标毫无影响而业务指标的微小提升可能需要技术指标大幅改进。所以做AI工程一定要盯着业务指标别自嗨。第三个坑是不做容量规划。有一次大促流量涨了10倍模型服务直接被打挂。事后复盘发现我们从来没有做过容量测试不知道系统能扛多少QPS。从那以后我坚持每个季度做一次全链路压测摸清系统的容量上限并预留30%的buffer。第四个坑是文档缺失。我接手过一个项目前任工程师离职后没人知道模型服务怎么部署、怎么回滚、怎么排查问题。我花了整整两周才把系统摸清楚。所以我现在要求团队任何服务上线前必须有一份完整的运维手册包括部署步骤、回滚步骤、常见问题排查、联系人。这份文档可能永远用不上但用上的时候能救命。最后分享一个小技巧给每个模型版本起一个有意义的名字不要用v1、v2这种无意义的编号。比如2024-01-15-bert-base-lr2e5包含了日期、模型架构、学习率。这样你看到版本号就能大致知道这个模型是什么时候训练的、用了什么配置。这个习惯能帮你在排查问题时快速定位到相关版本。AI工程这个领域技术更新快但核心逻辑变化慢。把基础打牢把工程习惯养好比追新技术重要得多。我从零搭建过三套AI工程体系每一套都比上一套更简单、更稳定。不是因为我技术退步了而是因为我学会了做减法。