
说到“ai-engineering”我最早是在一次内部技术分享上听到这个词当时第一反应是“这不就是把模型训练完扔上线吗”。真正动手做下去才发现从零开始把一个AI项目做成能稳定运行的服务和跑通一个Jupyter Notebook完全是两码事。这篇文章就当是我把一套从零启动的AI工程化项目重新梳理后的复盘给准备入坑的朋友一份可以照着走的路线图。我会尽量把设计思路、技术选型、踩坑记录都说清楚不会只丢一堆名词。1. 先把“AI工程化”这个词拆明白1.1 它解决的并不是模型效果问题很多人以为AI工程化的核心是“把模型效果做得更好”但工作几年之后我越来越觉得模型效果只是其中一环工程化真正解决的是“可靠交付”的问题。学术项目里只要离线指标达标就算成功生产环境里一个模型从训练完成到被业务方稳定调用中间还隔着数据处理、版本管理、模型部署、服务监控、持续迭代这些环节。任何一个环节掉链子离线Acc再高也白搭。打个比方算法工程师交出的是“一道研发好的菜”而AI工程化要做的是“把这道菜变成任意一家门店都能稳定复制的标准出品”。你要考虑的不是某一次做得好吃而是能不能每次都在同样时间内出餐原材料变了怎么办顾客投诉怎么追踪口味怎么定期更新。放到AI场景里就是数据变了怎么办模型精度下降怎么发现服务宕了怎么回滚新版本怎么灰度上线。所以“ai-engineering-from-scratch”这个项目本质上是在搭一套完整的、可重复的AI交付体系。从零开始意味着没有现成的平台基建也没有成熟的团队分工所有环节都要自己先跑通。这个过程很难一步到位但一旦把骨架搭起来后续再往上加模型、加业务场景都会顺手非常多。1.2 很多团队容易卡在哪我见过不少团队模型训练已经跑得很溜但工程化推进却极其痛苦。最典型的卡点是代码能跑但别人跑不起来模型能出结果但没人说得清是拿哪份数据训的服务上线后一更新就出问题出现问题后排查日志像大海捞针。这些问题的根源往往不是某个人技术不行而是整个项目缺少“纪律性”。模型训练代码里写死了数据路径换台机器就崩数据集在某个同事的电脑上没人知道是哪个版本模型文件有十几个命名“final_final_v2”谁也分不清哪个才是线上正在用的。这些细节在Notebook里也许不算致命一旦要上线每一个都是事故。所以从零开始做AI工程化第一步不是选多牛的模型而是先建立一套约束。约束不是限制效率而是保证所有人都能在同一个基础上协作。后面我会具体说怎么落地这个约束体系。2. 从零开始的全链路设计思路2.1 先定义可度量的业务目标开工之前第一件事是问清楚这个AI能力上线之后业务上到底要改变什么是客服平均响应时长降低30%是搜索点击率提升5%还是审核通过率提高10%没有业务指标的技术指标没有任何意义。我自己犯过一个错误一开始只顾着优化模型的F1值认为分数够高就万事大吉。结果模型上线后业务方反馈“没有用”因为离线指标和线上真实的用户行为有很大偏差。后来才意识到离线F1是模型视角业务方关心的却是单位成本、处理时效、用户体验。AI工程化项目一定要从第一天就定义好业务口径并且建立离线指标和在线业务指标之间的映射关系。通常我会把指标分成两层。第一层是模型指标比如准确率、召回率、AUC第二层是业务指标比如转化率、留存率、成本节约幅度。每一轮模型迭代都要同时回答两个问题模型指标涨了多少业务指标有没有可预期的正向变化如果模型指标涨了但业务指标推不出来那这个迭代宁可不上。2.2 数据、模型、交付三条线并行从项目实施的角度我会把工作拆成三条线数据线、模型线、交付线。三条线不是先后顺序而是并行推进。数据线负责把原始数据变成高质量的、可版本化的训练样本模型线负责实验、调参、评估、产出模型产物交付线负责把模型封装成服务部署到对应环境并接上监控和告警。常见的问题是把这三条线串行来做先等数据攒齐再训练模型最后才考虑部署。这种方式在前期看起来稳妥但周期太长而且到了交付阶段才会发现很多问题比如数据格式不满足服务要求、模型推理速度跟不上业务高峰、接口设计不满足调用方习惯。最佳做法是第一条迭代就用最小数据集、最小模型走通全链路。哪怕效果很差也要先把“数据到服务”的管道打通。管道通了后面换更好的数据、换更强的模型都只是替换其中的一个环节而不是重新做一遍。2.3 技术选型优先通用框架而不是什么都自己造技术选型上我的原则很朴素社区越活跃、生态越成熟的方案优先选。自己造轮子只发生在“实在没有合适方案”的时候而不是“为了显得我们团队有技术深度”。具体到这套从零项目我的基本选型如下环节选型理由开发语言PythonAI生态最全团队上手成本低深度学习框架PyTorch调试友好社区资源多数据处理HuggingFace Datasets自带缓存、切片、随机种子控制比手写Dataset省心实验追踪MLflow参数、指标、模型统一记录部署流程成熟推理服务FastAPI轻量、文档自动生成、异步支持好容器化Docker Compose环境一致性最好本地切换成本低监控Prometheus Grafana业界标准后续扩展告警方便这套组合不是唯一正确答案但它能覆盖从实验到上线的绝大多数需求。我不建议一开始就引入Kubernetes。单机服务和容器编排还没有跑通的情况下Kubernetes只会带来额外的复杂度。先把最小闭环跑起来确认瓶颈在某一个环节之后再针对性地引入更重的组件。3. 核心实操搭一个最小可用的AI服务3.1 环境准备与项目结构我习惯用一个非常工程化的目录结构来启动项目哪怕最初只是单个模型实验也保持这个架子。目录结构如下ai-engineering-from-scratch/ ├── configs/ # 配置文件 ├── data/ │ ├── raw/ # 原始数据只读不写 │ └── processed/ # 清洗后的标准数据 ├── models/ # 模型产物按版本存放 ├── notebooks/ # 探索性分析脚本 ├── src/ │ ├── data/ # 数据加载与清洗逻辑 │ ├── models/ # 训练与推理逻辑 │ └── serving/ # API服务相关代码 ├── tests/ # 单元测试和集成测试 ├── pyproject.toml └── README.md这个结构看起来很普通但每个目录都有边界data/raw只放原始文件任何人都不要直接修改data/processed由脚本生成脚本要做重复性验证models下面每个模型都必须带版本号不允许出现含义不明的名称notebooks只做探索不能成为最终代码的载体。环境管理我会直接上Poetry或uv不推荐裸放在全局Python里。项目依赖必须锁定否则三个月后就复现不了当时的训练环境。依赖锁定不是做给别人看的是为了让你自己在大版本升级时少踩坑。环境准备好后第一步写一个简单的数据加载脚本确认整条数据链路是通的。不要一上来就加载大模型、跑大规模训练那样一旦出错定位成本会很高。3.2 数据准备与特征工程我以文本分类任务为例这个任务最典型也最容易扩展到其他场景。数据来源可能是业务工单、商品评论、日志文本中的某一段。原始数据往往有大量噪声比如空文本、重复样本、URL、乱码符号等。清洗规则不能拍脑袋要一边探查数据分布一边确认。from datasets import load_dataset # 假设你已经把原始数据放到了 data/raw dataset load_dataset(csv, data_filesdata/raw/samples.csv) # 简单清洗 def clean_text(example): text example[text].strip() text text.replace(\n, ) return {text: text} dataset dataset.map(clean_text, num_proc4) dataset dataset.filter(lambda x: len(x[text]) 0) # 观察类别分布 print(dataset[train].features) print(dataset[train].to_pandas()[label].value_counts())这里有个容易忽略的点数据集切分。很多新手直接用train_test_split随机切但生产中如果数据带有时间属性必须按时间切否则很容易高估模型能力。比如前面的数据是1月到5月测试集随机从中间抽了一些模型的所谓“未来预测能力”就虚高了。正确做法是训练集用1月到4月验证集用5月或者至少保证分组时避免同源样本跨集合。文本特征方面除非你有充分理由否则不要一开始就自己写一整套分词流程。直接用HuggingFace的Tokenizer会更稳健。这里有一个我踩过的坑训练时和推理时如果用了不同版本的Tokenizer资源文件或者用了不同的清理函数线上效果会和张口闭口“预训练模型能力不够”产生奇妙的偏差。所以清洗逻辑必须封装成一个函数训练和推理复用同一份代码不要各写一套。3.3 训练与实验记录训练脚本不要写成一个巨大的notebook而是拆成可配置的模块。我自己会写一个train.py所有可变参数通过配置或命令行传入。配置信息和最终结果一定要记录到同一个地方我用的是MLflow。import mlflow from transformers import ( AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments ) mlflow.set_experiment(cls_baseline) with mlflow.start_run(): model_name bert-base-chinese tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSequenceClassification.from_pretrained( model_name, num_labels2 ) training_args TrainingArguments( output_dir./checkpoints, num_train_epochs3, per_device_train_batch_size16, per_device_eval_batch_size32, evaluation_strategyepoch, logging_dir./logs, save_total_limit2, seed42, fp16True, ) trainer Trainer( modelmodel, argstraining_args, train_datasetdataset[train], eval_datasetdataset[valid], tokenizertokenizer, ) trainer.train() trainer.save_model(./models/cls_v0.1) mlflow.log_param(model_name, model_name) mlflow.log_param(num_train_epochs, 3) mlflow.log_metric(eval_accuracy, ...)这段代码里最重要的不是我选了什么模型而是我把“随机种子固定为42”“混合精度开启”这些训练细节固化下来。固定随机种子是因为模型实验需要可复现否则同样代码跑两次指标不同你根本分不清是改进还是噪声。混合精度是因为显存受限后面小节还会具体说。实验记录不能只记最后的指标。训练数据版本、代码commit号、模型文件路径、推理延迟和模型大小这些都要一并落库。没有这些信息的模型文件就像没有标签的罐头你根本不敢吃哪怕味道再香。3.4 构建推理服务与容器化训练完模型接下来是把它变成一个可以被业务方调用的服务。我一般用FastAPI写一个最小服务接口信息清晰自带Swagger文档联调非常方便。from fastapi import FastAPI from pydantic import BaseModel from transformers import pipeline app FastAPI(titlecls-service) pipe pipeline(text-classification, model./models/cls_v0.1) class PredictRequest(BaseModel): text: str class PredictResponse(BaseModel): label: str score: float app.post(/predict) def predict(req: PredictRequest): result pipe(req.text)[0] return PredictResponse(labelresult[label], scoreresult[score])这里有个重要细节模型加载路径必须通过配置注入不要硬编码。因为不同环境下的模型目录可能完全不同本地可能是./models测试环境可能是/data/models线上还可能从一个对象存储下载到本地缓存。服务写好后容器化是让所有人都能跑起来的核心步骤。我会用一个简单的DockerfileFROM python:3.10-slim WORKDIR /app COPY pyproject.toml . RUN pip install --no-cache-dir . COPY . . EXPOSE 8000 CMD [uvicorn, src.serving.app:app, --host, 0.0.0.0, --port, 8000]容器化解决的是“本地能跑线上跑不起来”的经典问题。但要注意Docker镜像不是越大越好。直接把整个训练环境打进镜像镜像体积可能到几个GB上线拉取都会非常痛苦。推理镜像只装需要推理的依赖没必要包含训练框架全家桶。这也是为什么前面要强调“模型产物”和“训练代码”分离模型可以放到独立存储中推理镜像只负责加载和使用。3.5 上线后的监控与反馈回路服务能跑只是及格真正决定项目生死的是上线之后的监控。监控分成两个维度服务健康和模型健康。服务健康看的是QPS、P95延迟、错误率、显存占用等。这些指标可以通过Prometheus暴露出来配合Grafana看板一目了然。如果你没有专门的运维平台也至少要在代码里加日志并把日志统一收集。我见过太多服务因为某个样本格式特殊导致处理异常但错误被静默吞掉等到用户投诉才发现。模型健康看的是模型输入分布和输出分布是否发生漂移。线上进来的样本不能直接打标但你可以观察模型预测的概率分布。如果某一天开始大量样本的预测概率突然都落在0.48到0.52之间说明模型对这批数据已经很“不确定”了这时候就该考虑收集样本重新训练。反馈回路的意思是线上不能只是流数据还要设计一个机制回收“低置信度样本”和“被业务方纠错的样本”定期汇总成新的训练集。没有反馈回路的AI服务是单向消耗品效果只会越用越差。哪怕一开始的反馈机制很笨比如每天导出一次CSV给标注团队人工标也一定要有。4. 常见问题与排查技巧实录4.1 显存不够怎么处理训练文本模型最常见的问题是单卡显存不够。很多人第一反应是买更多卡但很多情况下单机多卡也未必划算。我的排查顺序通常是先看是不是批次大小设置过大。batch_size32跑不通就调到16或8。如果调小batch后指标明显波动可以考虑用gradient_accumulation_steps来模拟更大的batch逻辑上等价于攒几个小batch再更新一次梯度。再就是开启混合精度。PyTorch里一行torch.cuda.amp或HuggingFace的fp16True就能把显存占用大幅降下来。混合精度不是只在低精度下牺牲效果而是关键计算保持高精度不会产生显著精度损失。我在文本分类任务中实测开启fp16后同等设置下显存下降差不多四成训练速度反而更快。如果以上还不够就要考虑模型层面的优化比如使用更小的预训练模型、加gradient_checkpointing、或者用device_mapauto把部分层放CPU。注意这些都是有代价的不是无脑开。gradient_checkpointing会显著降低训练速度模型变小可能会牺牲效果要带着目标去取舍。实际排查建议先去终端执行watch nvidia-smi把显存曲线录下来。不要只盯峰值很多问题在于某一步显存突增而不是全程爆满。找不到具体是哪一段代码导致显存增长先二分法注释模块跑通常很快能定位。4.2 模型在测试集上表现好上线却变差这个问题我至少遇到过三次。每次第一反应都是“线上预处理的坑”但排查下来原因往往不止一个。最常见的原因之一是数据分布不一致。训练数据里文本来源集中、长度集中在100字以内但上线后用户输入长度跨度极大格式也五花八门。比如训练集里没有表情符号而线上大量出现表情符号。模型没见过这种写法输出自然不稳定。还有一类原因就是“训练和推理的预处理逻辑不一致”。我踩过一次深刻的坑清洗脚本在处理连续英文字母时用了不同的正则规则离线测试时文本被清洗后喂给模型线上服务却漏掉了同一层清洗。结果模型看到的输入文本和训练时完全不同效果崩盘。现在我会把清洗逻辑写成一个独立函数并加一个单元测试来保证训练和推理两个入口用的完全一样。遇到线上效果变差先不要急着重训。先抽一批线上真实样本和测试集对比分布统计类别、长度、字符覆盖率。很多问题通过分析样本就能找到答案。找到原因后再决定是补数据、修预处理还是调整模型盲目重训往往只是把问题暂时掩盖。4.3 服务延迟高如何定位延迟高需要先定义高在哪一段。FastAPI服务通常分为三段时间网络传输、预处理模型推理、返回序列化。最简单的定位方式是在接口函数里用time.perf_counter()分段计时并打印日志。import time app.post(/predict) def predict(req: PredictRequest): t0 time.perf_counter() clean clean_text(req.text) t1 time.perf_counter() result pipe(clean)[0] t2 time.perf_counter() logging.info(fpreprocess: {t1-t0:.4f}s, inference: {t2-t1:.4f}s) return ...如果预处理耗时占比高检查是不是在请求内重复加载了停用词表、正则每次重新编译。如果是推理耗时高优先优化模型本身不要急着加机器。可以试一下把模型转换成ONNX或者用bettertransformer加速用蒸馏后的轻量模型替换大模型简单场景下直接缓存重复请求也是立竿见影的做法。还有一点容易忽略动态batch。线上请求往往是并发到达的如果每个请求单独走一次推理GPU利用率很低。用text-generation-inference这类框架或者自己在FastAPI里实现简单的并发合并都能把吞吐量拉高好几倍。不过动态batch会增加单请求延迟要结合业务场景取舍。4.4 模型更新与回滚策略模型上线后不是一劳永逸。每三个月甚至每月都可能重新训练一版。更新模型最忌讳的是直接覆盖线上模型文件一旦新模型效果不达标连回滚的机会都没有。我现在的做法是每个模型产物都有一个唯一ID存放在独立的模型目录或模型仓库中。线上服务配置指向具体的模型版本需要通过部署流程修改配置并重启而不是直接改文件。发布时先起一个临时实例加载新模型做几十个真实请求测试再把流量切过去。如果条件允许做金丝雀发布。简单说就是让5%的流量打到新模型上对比新旧两个版本的关键指标比如平均置信度、用户点击率、业务处理成功率。没有异常再逐步放量。没有负载均衡能力时至少先在一台测试机启动服务用脚本模拟线上请求做回归再切主服务。回滚策略就是保留至少最近N个可用的模型版本。容器环境里回滚可以是重新部署上一个镜像非容器环境就是切换配置并重启服务。操作越简单越好。我见过团队一更新服务就要同时改五六个地方出了事故半小时都回不到上一个状态这种架构再忙也得重做。最后一点实际体会做了一套完整的“ai-engineering-from-scratch”之后我对“工程化”三个字的理解彻底变了。它不是一个工具、一个框架甚至不是一个岗位能搞定的事而是一套需要持续投入纪律性的思维方式。对我个人来说最有用的改变是从“想快速看到模型效果”转向“让每一步结果都可追溯、可复现、可回滚”。后者的起步慢但积累越久越省时间。如果你现在正要启动自己的AI工程化项目我的建议是不要追求一步到位。先选一个简单的场景哪怕就是做一个文本分类也要把数据版本、实验记录、服务部署、监控告警全部走通。跑通之后你会发现后续加再复杂的模型和能力都只是在一条成熟管道上做替换。框架可以学工具可以换但这套从零到一建立起来的工程习惯才是整个项目最值钱的部分。