想要把 AI 项目从“能跑出结果”推进到“可靠地创造价值”,中间隔着的东西,远比你想象中多。我见过太多团队,模型在 Jupyter Notebook 里面跑得好好的,准确率 90%,一到线上就崩,或者训练成本高到公司养不起,又或者换了个人就没人能复现结果。这些问题,根子都不在算法,而在工程。这篇博文想聊的,就是当我决定从零开始做一套真正的 AI 工程体系时,我会怎么搭、为什么这么搭、哪些地方必须较真。它不是某本教材的目录,更像是我自己这些年踩坑之后攒下来的一套方法论,适合已经会训练模型、但想往更规范的工程化方向走的工程师,也适合小团队里要一个人扛起 AI 基础设施的“全能选手”。
1. 先搞清楚:AI 工程化到底是解决什么问题
1.1 为什么多数项目死在 POC 之后
很多团队对 AI 项目的理解,停留在“把模型训练出来,指标够了就上线”。但实际上,从 POC 到生产环境,是一个完全不同的命题。POC 只需要证明“这个方向在样本上可行”,而生产环境要求的是“在无人看管的条件下持续稳定地运行”。差别具体在哪?你可以看这几点:
- 数据持续变化:训练时的样本分布和上线后的真实输入永远有偏差,而且这个偏差会越来越大。
- 环境不可控:依赖库升级、GPU 驱动变更、第三方接口限流,任何一个都能让原本正常的服务挂掉。
- 资源有限:训练要钱,推理也要钱,GPU 不便宜,不加控制的推理集群分分钟烧掉预算。
- 协作复杂:算法工程师、数据工程师、后端工程师各管一段,如果没有统一的工程标准,任何一环都会成为瓶颈。
我以前也天真地以为,只要模型代码写得够好,工程只是顺带的事。直到一个推荐模型上线后,因为特征管道多算了一天的时间窗口,导致线上数据漂移,整个 CTR 掉了 15%,才意识到:工程不是模型的搬运工,工程是 AI 项目里真正决定成败的部分。
1.2 一个 AI 系统需要哪些环节才算完整
如果把一次完整的 AI 工程实践拆开,应该是这样一条链条:
- 需求定义:明确要解决什么业务问题,指标是什么(离线指标和在线指标要分开定)。
- 数据工程:采集、清洗、校验、版本化、特征计算。
- 模型开发:训练代码、超参搜索、实验结果管理。
- 部署与推理:模型上线、灰度、压测、推理优化。
- 监控与运维:性能监控、漂移检测、告警、自动回滚。
- 迭代机制:反馈数据回流、重新训练、重新部署。
这六层环环相扣,任何一个环节缺失,系统都会很脆。很多团队只盯着第三层“模型开发”,而市场真正稀缺的,是能把六层全部串起来的人。从零开始做 AI 工程,本质上就是把这六层逐步搭建起来,并让它们形成闭环。
2. 搭建第一套能进生产的训练骨架
2.1 选硬件和环境的思路:先算账,再选型
很多人上来就问“用 A100 还是 H800”,我觉得这种问题问早了。第一步应该算清楚两笔账:训练频次和单次训练成本。如果你的模型每周训练一次,单次训练用 A100 跑 8 小时,那么租按需实例比买机器划算;如果你的模型需要每天滚动训练,并且对延迟敏感,那就得考虑内网部署 GPU 集群。
对于个人从头搭建或者小团队,我推荐一条稳妥的起步路线:
- 开发环境用带 GPU 的单机,比如 4090 或者云上的 A10 实例,先用小规模数据把代码逻辑调通。
- 模型需要大规模训练时,再切换到按需租用的多卡实例,比如 8×A100,并用容器镜像保证环境一致。
- 推理环境用独立的 GPU 实例,与训练隔离,避免两者互相抢占资源。
不要在一开始就追求“一步到位建一个 Kubernetes 集群”。基础设施的复杂度上去了,调试的时间成本会翻倍。我见过最强的“AI 工程化”团队,一开始也就是一台机器加几个 Docker 容器,先把流程理顺了再逐步扩展。
2.2 项目目录结构的核心原则
我推荐目录结构必须遵循三个原则:代码与配置分离、数据与代码分离、实验与代码分离。这里的“实验”是指每一次训练的实验记录、日志和产物,它们不应该散落在代码目录里,否则跑完几十次实验以后,整个项目会乱成一锅粥。
一种我常用的结构大概是这样的:
project/ ├── configs/ # 所有模型和训练配置(YAML/JSON) │ ├── model/ │ ├── train/ │ └── deploy/ ├── data/ # 数据文件(一般不入 git,用 DVC 或云存储) │ ├── raw/ │ ├── processed/ │ └── features/ ├── src/ │ ├── data/ # 数据加载、清洗、特征工程 │ ├── models/ # 模型定义 │ ├── train.py # 训练入口 │ ├── evaluate.py # 评估入口 │ └── serve.py # 推理服务入口 ├── experiments/ # 每个实验的日志、指标、模型权重 ├── scripts/ # 数据同步、环境初始化等工具脚本 ├── tests/ ├── Makefile └── requirements.txt你可能会问:为什么train.py只留一个入口文件,而不是把训练流程拆成很多小脚本?因为训练流程是一个有状态的流程——加载数据、初始化模型、跑批次、保存 checkpoint,拆太散反而不好排错。相对地,scripts/里的工具脚本可以是零碎的,它们本身就是一次性操作。这个结构很朴素,但足够支持从单机训练平滑过渡到集群训练。
2.3 训练脚本里必须考虑的四个问题
写训练脚本的时候,不要只顾着“把模型训练出来”,你要默认自己在写一个需要无人值守运行 12 小时甚至更久的程序。所以在代码里至少要覆盖下面四件事:
- 可恢复性:每隔 N 步保存一次 checkpoint,并在启动时检测是否存在 checkpoint,有就从中恢复,而不是一切从头重跑。
- 配置注入:所有超参、路径、环境相关变量都从配置或环境变量读取,禁止硬编码。硬编码路径是一个常见的坑,换个环境跑就到处报错。
- 日志结构化:不要只 print,还要输出结构化的日志(比如 JSON 格式),包含时间戳、epoch、loss、learning rate、GPU 利用率等。这样后续不管是人工排错还是自动巡检,都有据可查。
- 终止策略:除了正常训练结束,还要考虑磁盘满、网络中断、GPU 掉卡等异常情况。关键路径上要有 try/except,保证异常时能保存现场。
我还习惯上传data/processed目录的校验和到实验记录里。这是个小习惯,但能在排查数据错误时帮你快速定位问题:到底是数据变了,还是模型代码变了,还是配置变了。
3. 数据管道:没人愿意做但决定一切的部分
3.1 数据清洗的几条硬性规范
数据工程在 AI 项目里往往占据了 60% 以上的工作量,但它的价值被严重低估。数据清洗我一般遵循几条硬性规范:
- 去重要留证据:做去重时,不要只输出一个去重后的文件,还要输出去重前后的统计信息和去重依据(比如按哪几个字段判定重复)。不然下游根本没法判断你的去重逻辑是否正确。
- 异常值不能静默处理:对超过合理范围的值,要么截断、要么剔除、要么单独标记,但必须留下处理记录,并在训练日志里体现。静默地“把所有负数置 0”是危险的做法。
- ID 统一:如果是用户维度的数据,必须保证用户在不同表中的 ID 和字段定义一致,否则后面做特征拼接时会非常痛苦。
- 时间戳统一:系统中可能混有服务器时间、客户端时间,要明确统一用哪个时间作为“业务时间”,并且存成统一时区。
听起来都不是什么高深技术,但恰恰是这些“脏活”决定了你模型的真实效果。我遇到过不止一次,模型离线指标很好,上线后效果不佳,最后查来查去,发现是训练数据里的标签有 40% 是过期渠道回传的,带有巨大的时间延迟。
3.2 数据版本化:比代码版本化还重要
代码有 Git 管着,数据却经常被忽略。训练数据和特征文件如果不做版本管理,你根本没法回答“上周那个效果好的模型的训练数据到底是什么样”这个问题。对小团队来说,不需要上多复杂的系统,一个简单的方案是:
- 每个数据版本用一个带时间戳的目录,例如
processed/20250112_1530/。 - 在版本目录里放一个
manifest.json,描述数据来源、清洗逻辑、文件校验和、统计信息。 - 用 DVC 或简单的对象存储,把历史版本数据一并保存,保证有回滚能力。
有人觉得这也太繁琐了,但等你需要回溯或者遇到线上数据事故的时候,这套“繁琐”会救命。数据版本化还有一个隐形的好处:它强迫你在生成数据的每一步都写清楚,而这些元信息恰恰是复现实验结果的关键。
3.3 分布式训练下的数据读取优化
当训练规模变大,数据读取会成为新的瓶颈。GPU 等数据,是分布式训练里最常见的浪费。这里有几个实操层面的建议:
- 打包成 TFRecord / webdataset / parquet:不要保存成千上万个小图片或小 JSON 文件,否则 I/O 会拖死训练。先把数据打包成大文件,能用顺序读就顺序读。
- 使用 DataLoader 的多进程预读取:
num_workers应该根据机器 CPU 核数和 I/O 速度调,不能一直用默认值。我常用的调试方法是把num_workers从 2 开始逐步增加,观察 GPU 利用率的变化,到利用率不再明显上升就是合适的值。 - 考虑缓存层:如果数据集不大(比如几十 GB),可以直接把数据拷到本地 NVMe 或内存文件系统里,训练速度可以提升不少。
- 特征预计算与在线一致性:离线训练时用的特征,必须和线上推理时用的特征保持同一套逻辑。这不是数据读取问题,但它是数据管道设计里“最贵”的一个教训——如果离线在线特征不一致,再牛的模型也是空中楼阁。
4. 实验追踪与模型注册:让调参不靠运气
4.1 一门心思练模型,却忘了记录,等于白练
我早期做深度学习时,跑实验全凭记忆,觉得“这个参数好像试过,好像效果好一点”。等到要复现,或者换个人接手,就只能抓瞎。后来我彻底转向实验追踪工具,最核心的收益不是那些漂亮的曲线图,而是每次实验的所有信息都被固定下来:超参数、代码版本、数据版本、环境依赖、训练日志、最终指标、模型文件位置。
方案上,我个人推荐 MLflow——它开源、轻量、和 PyTorch/TensorFlow 都很容易集成。你只需要在训练代码里写:
import mlflow mlflow.set_tracking_uri("http://localhost:5000") with mlflow.start_run() as run: mlflow.log_params({"lr": 0.001, "batch_size": 64}) # 训练过程... mlflow.log_metrics({"val_loss": 0.123}) mlflow.log_artifact("model.pt") mlflow.pytorch.log_model(model, "model")每跑一次实验就是一个 run,参数、指标、产物在 UI 里一目了然。不要小看这件事,它能把你从“碰运气调参”变成“有方向地搜索”。
4.2 模型注册到底在管什么
实验追踪解决的是“实验过程可回溯”,模型注册解决的是“哪个模型是当前公认的最好的模型”。在工程上,这两者必须区分开。你不能靠大家在群里喊一句“新模型效果好,大家用这个吧”,必须有一个唯一权威入口。
一般的做法是:
- 候选模型在 MLflow 里注册,并附上评估报告,包括离线指标、数据版本、训练代码版本。
- 注册时区分 stage:
Staging、Production、Archived。 - 只有达到准入标准的模型才能被标记为
Production。 - 部署系统从模型注册中心拉取指定 stage 的模型,而不是从某个人的电脑目录里拷。
这样一来,部署什么、回滚到哪个版本,都清晰可控。需要注意的是,模型文件的内部结构最好保持一致,比如都用model.pt加一个元信息文件;否则部署系统需要为每个模型写不同的加载逻辑,这样的耦合很不利于扩展。
4.3 如何设计指标记录表
做实验追踪时,指标不是记越多越好。我建议分成三类:
- 核心指标:和业务目标直接挂钩,比如准确率、召回率、线上预估的 CTR 等,每次实验都记录。
- 诊断指标:如 loss 曲线、梯度范数、学习率变化,这些不是最终交付物,但对调参非常有用。
- 系统指标:如 GPU 利用率、训练吞吐(samples/s)、显存占用,这类指标帮助你判断训练效率,和模型质量无关,但和成本直接相关。
我的经验是:每天至少花 10-20 分钟扫一遍实验看板,不是为了看曲线,而是为了发现“系统指标异常”。如果某次实验训练吞吐突然掉了一半,那大概率不是模型问题,而是环境或数据管道出了问题,越早发现越好。
5. 推理部署与在线优化:从“能跑”到“跑得好”
5.1 部署形态不是越高级越好
AI 模型的部署形态有很多种:HTTP API、批量离线任务、嵌入式模型、边缘推理。每次看到团队一上来就整 Kubernetes 服务,我都想问一句:你的调用方有多少?QPS 多少?时延要求多少?如果调用方就是几个内部系统,一个容器化的 FastAPI 服务加负载均衡就够了;如果模型要服务百万级用户,再考虑上完整的弹性扩缩容。
我在实践中总结出的选型逻辑是这样的:
- QPS 低,内部调用:FastAPI + Gunicorn,单机部署,加一层缓存,维护成本很低。
- QPS 高,在线服务:考虑 Triton Inference Server 或 TensorFlow Serving,它们对 GPU 推理做了很多优化,支持动态批处理,能显著提升吞吐。
- 需要低延迟:把模型转换到 TensorRT、ONNX Runtime,并考虑模型蒸馏或量化。
- 离线批量预测:用 Spark 或 Ray 跑分布式批量推理,不需要在线服务,效率优先。
从零开始的时候,不要迷信大厂技术选型。它们的大集群方案是它们的规模和成本的产物,对你未必合适。你更应该关心“当前业务规模下最可靠且成本最低的方案是什么”。
5.2 推理性能优化是持续调优的过程
很多人以为部署上线就完了,其实推理性能优化才刚刚开始。我常用的排查路径是:
- 先做性能基准测试:压测单实例的 QPS、P99 时延、GPU 利用率。
- 用 profiler 看瓶颈在算子层面、数据预处理,还是网络传输。
- 如果 GPU 利用率低,优先检查预处理是否在 CPU 上太耗时,尝试把预处理移到 GPU 上,或做异步流水线。
- 如果模型尺寸大、时延高,考虑动态批处理(dynamic batching),把多请求合并成一次前向推理。
- 最后再考虑精度上的优化,比如 FP16、INT8 量化。
动态批处理是推理优化中性价比最高的技术之一。我举个例子:某个文本分类模型单条推理平均时延是 15ms,开启动态批处理后,batch size 从 1 增加到 8,吞吐提升了 4 倍,平均时延只增加了 30%。因为 GPU 并行计算的能力在那里,一次算 8 条比算 1 条多不了多少时间。
5.3 在线监控:没有监控的模型就是没穿衣服出门
模型上线后的监控,至少包含四个维度:
- 系统指标:请求量、时延、错误率、GPU 利用率、排队长度。
- 业务指标:点击率、转化率、推荐多样性,或者其他和业务目标直接相关的指标。
- 模型指标:预测分布、置信度、特征缺失率。模型的预测均值发生异常波动,往往是数据分布漂移的早期信号。
- 数据质量指标:上游数据延迟、特征覆盖率、新鲜度,这些变化会直接传导到模型效果上。
告警规则不能只设“平均指标超过阈值就报警”,否则你会在不重要的波动里被噪音淹没。比较好用的方式是分阶梯:
- 观察级:某个指标连续 15 分钟偏离基线超过 20%,通知负责人在看板确认。
- 紧急级:错误率超过 5%,或者 P99 时延超过目标值,立即触发电话或群告警。
- 严重级:关键业务指标断崖式下跌,自动执行服务回滚或降级策略。
6. 反馈闭环与自动迭代:AI 系统的最高级形态
6.1 影子部署与 A/B 测试的工程细节
很多人一提“上线新模型”就想直接全量切换,这在 AI 项目里风险太大。你可以用影子部署和 A/B 测试来降低上线风险,而且它们的工程细节比想象中更讲究。
- 影子部署:把新模型的输入复制一份,和线上模型同时预测,但新模型的预测结果不下发,只保存日志。运行一段时间后,离线比较两个模型的预测差异和业务指标。这一步成本低、风险小,是检验新模型的好办法。
- A/B 测试:把线上流量按比例切分,例如 90% 旧模型、10% 新模型,观察业务指标的差异。注意要保证流量分桶的随机性,并且至少要运行一个完整的业务周期(比如一周),避免短期波动干扰判断。
- 灰度发布:如果新模型在 A/B 测试中胜出,再逐步增加其流量比例,比如 10% → 30% → 50% → 100%,每步观察一段时间,若指标回退则立即回滚。
这套机制看起来麻烦,但它能避免“一次全量上线失败导致整个业务受损”的局面。AI 工程进入成熟阶段的重要标志,就是从“赌模型上线成功”变成“确定性地验证模型才好上线”。
6.2 自动重训到底应该在什么条件下触发
很多人的第一反应是“每天定时重训”。这不能说是错的,但要是数据分布相对稳定,每天重训是很浪费算力的,成本连着涨。
更好的方式是为重训设置触发条件:
- 数据量条件:累积的新样本达到一个阈值,比如训练集大小的 10%,触发重训。
- 漂移条件:在线监控发现特征分布漂移指标超过阈值,触发重训。
- 业务条件:关键业务指标持续下滑,触发重训或模型回滚。
- 定期强制:即使一切正常,也每周或每月做一次周期性的重训,防止潜在漂移累积。
重训并不是简单地“用新数据跑一遍”。工程上要确保重训的代码版本、数据版本、配置版本都被记录,而且重训要有独立于实验训练的优先级和资源配额,不能因为重训任务抢占线上推理资源。
6.3 数据分布漂移检测的落地方式
提到数据漂移,我见过很多团队想得很复杂,实际上起步并不难。轻量级方案是:
- 对每个重要特征,记录训练时段的均值、方差、分位数。
- 在线实时计算当前窗口的特征均值和分位数,与训练基线比较,用 PSI(Population Stability Index)或者 KS 检验量化差异。
- 当漂移指标超过设定阈值,触发观察级或紧急级告警。
PSI 的计算方法是:
psi = sum((实际占比 - 期望占比) * ln(实际占比 / 期望占比))这个值如果小于 0.1 说明分布稳定,0.1-0.25 需要关注,大于 0.25 说明明显漂移。它不需要额外的模型,几行代码就可以算出来,非常适合作为漂移检测的第一道防线。
漂移检测的目的不是“ обнаруж it and panic”,而是让你在模型效果还没显著恶化之前,就提前知道数据环境已经变了。工程化的本质就是把“事后救火”变成“事前预警”。
7. 我在实战里反复踩到的坑和总结的经验
7.1 团队协作中的三条铁律
AI 工程不是一个人的事,团队协作里我吃过不少亏,总结出三条铁律:
- 任何模型产出必须能复现。不能复现的实验结果,等于不存在。哪怕过程繁琐,也要把所有环境依赖锁定到版本号,甚至用容器镜像固化环境。
- 环境是最大的隐形杀手。最常见的线上疑难杂症,不是模型逻辑错,而是某个依赖库版本不一致。用 Docker 或 Conda 锁定环境,是最便宜的保险。
- 变更必须有回滚方案。不管是模型、配置、还是数据管道,任何变更都得先想好“如果出事了怎么回到上一个状态”。
这三条看起来都很基础,但就是这些基础的东西决定了团队的稳定性和效率。锦上添花的优化可以以后再做,这三条铁律从第一天就要立好。
7.2 防呆设计比聪明逻辑更重要
AI 系统本来就是高度复杂的系统,如果还在工程层面搞各种晦涩的“聪明设计”,后续维护绝对是一场灾难。我更喜欢做防呆设计:
- 默认值要安全。所有配置项都有默认值,但默认值必须让系统以最保守的方式运行,而不是激进启动。
- 命令要可重复。尽量用 Makefile 或 Shell 脚本把复杂命令固化下来,不要让人记住一长串参数。
- 启动时自检。服务启动时自动检查配置完整性、数据目录是否存在、模型文件是否能正常加载,任何一个不满足就拒绝启动。
- 清晰的错误提示。报错信息要直接告诉人“哪里坏了、怎么修”,而不是输出一个晦涩的 traceback。
我再分享一个细节:很多训练脚本在中断后重启时,会重新走一遍数据预处理的流程,然后又从头开始训练。看似没问题,实际上可能白白浪费几小时。更合理的做法是,训练脚本启动时首先检查 checkpoint 和预处理产物的存在性,存在就跳过对应步骤。这种“懒加载”和“断点续传”思路,能在日积月累中省下大量算力。
7.3 被低估的老知识:可靠工程原则依然适用
这个标题是 AI,但我计算机工程里学到的那套老原则,放到 AI 工程里一样适用,而且几乎全部适用。
- 模块化:数据管道、模型代码、部署服务应该各自独立,能单独测试。
- 解耦:模型训练和推理服务不要共享进程,它们对稳定性要求完全不同。
- 幂等性:数据清洗、特征计算这些步骤最好是幂等的,重复执行结果相同。
- 可观测性:系统任何关键路径都要有日志、指标和链路追踪。
AI 项目之所以难做,是因为它叠加了“数据的不确定性”和“算法的不确定性”。正因为不确定性太多了,工程层面才更需要用这些确定性的、可靠的手段去对冲。你越早接受这个道理,越能在这个领域走得稳。
作为一个也踩过无数坑的人,我最后想说的是:AI 工程化的本质不是堆工具,不是把集群搞得越大越高级,而是让每一个环节都能被追踪、被验证、被回滚。这条路没有捷径,但每补上一块短板,系统的稳定性和团队的效率都会肉眼可见地提升。希望这篇从零开始梳理的工程思路,能帮你少走一些我走过的弯路。