
1. 为什么把「从零开始」当成 AI 工程的核心先说一个我观察到的现象很多团队做 AI 项目把八成精力全压在「调模型」上模型在离线集上刷到 98%以为大功告成结果一上线就崩。推理延迟飙红、特征对不上、跑了一周指标不升反降最后整个项目变成「模型好但没人能用」。这就是典型地把 AI 工程理解窄了。AI engineering 的本质是围绕模型周边一整条链路的稳定性、可复现性和可迭代性——数据从哪来、怎么校验、训练参数怎么记录、模型怎么部署、上线之后怎么监测每一个节点但凡松动系统就会以各种意想不到的方式打你的脸。所以我特别认这个标题「ai-engineering-from-scratch」。做 AI 工程最好的方式不是直接抱一套现成的 AI 平台在那点点点而是自己从零开始把每一块地基都亲手搭一遍。搭过的人和使用过的人判断力完全不同。这篇文章就是我在这方面做 AI 工程实践的完整复盘不是那种「点一下按钮就出结果」的教程。我会顺着一个最小可用 AI 系统的生命周期从环境、数据、训练、评估、部署到监控把每一步为什么要这么设计、怎么操作、踩过什么坑都交代清楚。适合刚入行、想系统建立 AI 工程体系的算法工程师也适合需要使用 AI 能力但不想依赖黑盒平台的业务技术团队。1.1 AI 工程 ≠ 调模型AI 工程这个词经常被误解成「把模型调好」。但模型调参是研究行为工程是可靠性行为。两者的目标函数完全不一样。研究追求的是「在某个数据集上达到更高的准确率、更低的损失」。而工程追求的是「在真实环境里以可接受的成本、稳定地把模型能力交付出去」。一个模型在 Python 脚本里跑通了只是万里长征的第一步真正难的是让这条链路在没人手动干预的情况下持续运行三个月不出乱子。举一个我亲历的例子。之前做一个推荐相关的模型算法同学在离线测试里效果很好但线上点击率怎么都上不去。排查了三天最后发现是特征计算方式不一致——训练脚本里对用户特征的聚合逻辑和服务端实时算的逻辑有细微差异一个用的是「最近 7 天点击次数」另一个用的是「最近 30 天点击次数」。这不是模型的问题是工程链路的特征一致性问题但它的破坏力比模型烂还大。所以做 AI 工程第一件事是转变思维不要只盯着模型的 loss而是要盯着整条流水线的输入、输出、版本、依赖、可观测性。任何一个环节出现鬼影最终都会反馈成线上指标的下降而且你很难定位到具体原因。1.2 为什么我坚持自建而不是直接套平台市面上并不缺 AI 平台很多 MLOps 工具号称能一键完成训练、部署、监控。但我的建议是如果你的目的是真正理解 AI 工程最好先不要过度依赖平台至少要从零开始搭一遍核心链路。理由有三点。第一平台是黑盒。出了问题你只能在平台提供的日志面板里翻来翻去却不知道底层到底发生了什么。比如特征服务为什么延迟突然变高、某个依赖库为什么和线上环境冲突、模型热加载为什么失败这些底层细节平台不会告诉你但恰恰是线上事故的根源。第二平台绑定成本高。一旦你的数据格式、训练流程、部署方式都深度依赖某个平台的私有协议后面想迁移或者定制就会非常痛苦。AI 领域变化快今天的主流框架可能两年后就边缘化了如果工程底座是自建的你可以跟着生态平滑迁移如果是绑定平台的你就只能跟着平台走。第三自建能逼你理解每一层的取舍。比如你会意识到数据版本管理要解决的不只是「存一份数据」还包括「新旧数据的血缘关系」「回滚之后指标怎么对比」。这些认知只有亲手实现过一遍才真正长在脑子里。当然我不是反对使用平台。团队规模小、业务验证期短的时候用现成工具提速完全没问题。但从学习、从掌握控制力、从长期可维护性的角度我强烈建议至少在练手项目里从 scratch 走一遍。1.3 这套体系到底能解决什么问题我给自己搭这套 AI 工程体系目标很明确让 AI 系统的每一次变更都可预期、可追踪、可回滚。具体来说这套体系解决四类问题。数据层面解决「数据版本混乱、线上和线下特征不一致、脏数据流入训练集」的问题。方法包括数据版本管理、数据集校验、特征口径统一。实验层面解决「跑了很多次但说不清哪个配置最优、代码版本和数据对不上」的问题。方法包括结构化实验记录、统一指标口径、checkpoint 完整保存。部署层面解决「模型是模型、服务是服务两者是脱节的两张皮」的问题。方法包括标准化模型产物、统一推理接口、做性能压测。上线之后解决「模型退化没人知道、数据漂移没人感知、迭代全靠手动」的问题。方法包括推理日志采集、分布监控、预警和自动重训的闭环。我见过太多项目死在这四类问题上。模型被训练得很好却被工程问题拖垮了。一个 AI 系统能稳定跑下去靠的不是某个天才模型而是这些不起眼的工程细节。下面我从头开始把每一步细节都展开讲。2. 环境与工具链第一道最容易被忽略的坎如果你以为 AI 工程的第一件事是写模型那你就错了。第一件大事是把运行环境搞干净。无数项目毁在环境问题上A 同事的代码跑得好好的B 同事拉下来一跑报错一堆最后发现是 Python 版本和依赖库不一致。环境问题的本质是「你的代码运行在什么上下文里」没有被确定下来。做 AI 开发这一点的复杂度比普通后端开发高很多因为涉及 Python 解释器、CUDA 驱动、GPU 计算库、深度学习框架之间的复杂兼容关系。2.1 环境隔离是底线我见过最野的做法是直接在服务器的全局环境里 pip install 一堆包。刚开始确实快但装到第 N 个包的时候某个库把另一个库的依赖覆盖了原来的项目跑不起来了你根本不知道是什么时候坏的也不知道怎么恢复。环境隔离是底线没有商量余地。我常用的方案是 conda 或 python 自带的 venv关键是把每个项目的依赖关进各自的笼子里。python3 -m venv .venv source .venv/bin/activate pip install --upgrade pip看起来非常简单但就是这四行命令能避免后面无数个「我本地可以你那边怎么不行」的尴尬。如果是做深度学习相关的工作我建议直接上 conda 或者 micromamba它对 CUDA 相关依赖的管理比 pip 省心一些。尤其是不同项目可能要不同版本的 CUDA toolkit用 conda 环境隔离是最干净的方式。conda create -n ai-eng python3.10 conda activate ai-eng2.2 依赖追踪requirements 不是摆设环境隔离做完之后第二步就是依赖追踪。这里的核心不是在你的 requirements.txt 里写一句torch2.0就完事而是要锁定精确版本。直接写宽容版本号的做法等于把环境的确定性交给了运气。半年之后你重新装依赖torch 可能已经升级到 2.6 了API 变了你的代码可能就坏了。而你的项目代码本身并没有变过。我现在的做法是分层管理依赖requirements.in记录顶层依赖也就是你代码里直接 import 的库比如 torch、transformers、fastapi。requirements.txt锁定完整解析结果包含每个传递依赖的精确版本用 pip-tools 或 uv 生成。requirements-dev.txt开发环境专用的包比如 pytest、ruff、jupyter。这样每次从零还原环境都用pip install -r requirements.txt保证和原始开发环境完全一致。推荐用pip-compile或者uv lock来生成锁文件手写锁定文件维护成本太高了。2.3 GPU 与 CUDA 版本管理Conda 环境能解决 Python 层面的依赖但 GPU 计算链路的坑更多而且往往藏得很深。先说几个概念显卡驱动Driver、CUDA Toolkit、cuDNN、深度学习框架PyTorch/TensorFlow。很多人搞不清它们的层次关系。打个比方驱动是操作系统和 GPU 硬件之间的翻译官CUDA Toolkit 是一套 GPU 并行计算的开发库cuDNN 是在 CUDA 之上的深度神经网络加速库PyTorch 是调用这些库的顶层框架。每一层都有自己的版本号而且相邻层之间有兼容性要求。常见的错误是nvidia-smi显示的 CUDA 版本是 12.4就以为 PyTorch 也要装 cu124 版本。其实 nvidia-smi 那个 CUDA 版本是驱动支持的「最高版本」你完全可以用 PyTorch 官方编译的 cu118 版本跑在驱动 12.4 上因为驱动是向后兼容的。我在实际项目中一般这样处理先查驱动版本nvidia-smi确认驱动足够新。再确认 PyTorch 需要什么 CUDA 版本torch.version.cuda。用 conda 装对应版本的 cudatoolkit或者在 PyTorch 官方索引安装带预编译 CUDA 的版本。整个团队用同一套组合记录在文档里不要各自装各自的。如果条件允许docker 是更彻底的方案。用 nvidia/cuda 官方镜像或者 PyTorch 官方镜像连驱动以外的环境都固定住团队内共享同一个镜像能彻底消除「环境不同」带来的问题。3. 数据工程AI 系统的隐形主干模型跑得再快没有可靠的数据喂进来也是白搭。数据工程是 AI 系统里最不性感、但最决定成败的部分。很多团队把数据工程当成 ETL 的别名跑个脚本把数据存下来就算完事。但 AI 工程里的数据问题要微妙得多——不是「数据有没有」而是「数据对不对、版本是否匹配、分布是否变化」。3.1 数据版本管理代码有版本管理数据同样要有。你的模型是用 v7 版本数据训练的结果三个月后想复现当时的实验发现数据目录已经被覆盖了那就什么都说不清了。数据版本管理不需要一开始就上 DVC 这类重量级工具可以先用最简单的方式跑起来但必须跑起来。我习惯的做法是每次生成数据集时同时生成一个 manifest 文件记录三件事原始数据来源是从哪个表、哪个文件、什么时间点抽出来的。数据处理脚本的版本用什么代码、什么参数生成的这份数据。数据统计指纹行数、标签分布、数值列的基本统计量包括均值、标准差、缺失比例。有了 manifest任何一次训练用的什么数据一目了然。出问题时可以快速定位是数据变了还是模型变了。{ dataset_id: rec-2025-03-15-v7, source: hdfs://cluster/raw/events/2025-03-14, generated_by: preprocess.pygit-6f8a91c, num_rows: 2345678, num_features: 42, label_distribution: {positive: 0.18, negative: 0.82}, checksum: a1b2c3... }3.2 数据质量检查你的模型不会告诉你数据是脏的只会默默学坏。所以在数据进入训练流程之前一定要设一道闸门把明显有问题的数据拦下来。我总结的最低限度检查项有这些缺失值检查关键特征的缺失比例是否超过阈值。唯一性检查主键是否有重复重复样本会不会造成标签泄漏。分布检查标签分布、数值特征分布与上一版数据集相比是否有显著变化。未来信息检查特征中是否存在时间上来自「未来」的字段这是最隐蔽的泄漏方式。在项目里我用一个统一的validate_dataset函数把这些检查串起来返回一份报告不通过就拒绝训练。这个函数在本地跑得快在训练流水线里也能直接调用。def validate_dataset(df, expected_cols, label_col, max_missing_ratio0.05): report {} missing_ratio df[expected_cols].isnull().mean() report[exceed_missing_cols] ( missing_ratio[missing_ratio max_missing_ratio].index.tolist() ) duplicate_cnt df.duplicated(subset[row_id]).sum() report[duplicate_rows] duplicate_cnt label_dist df[label_col].value_counts(normalizeTrue).to_dict() report[label_distribution] label_dist return report这里不要求把函数写得完美但逻辑要清晰、可扩展。重点是数据质量检查做的是「拦截」不是「补救」。等脏数据进入训练集再补救成本会高很多。3.3 特征工程与特征存储特征工程的细节多到可以单独写一本书我在这里只讲一个最关键的原则特征计算逻辑必须统一训练和线上跑同一个代码。特征不一致是线上效果崩盘最常见的隐形杀手。拿前面推荐场景的例子来说训练时算「用户过去 7 天点击品牌数」线上服务可能是另一套代码用同样的名字但实际算的是「用户过去 30 天点击品牌数」。模型看到的数据和训练时不一致效果自然就崩了。解决这个问题的最好方式是把特征计算逻辑收敛到一个模块里训练和线上服务都调用同一个函数。不管这个特征是在 Spark 里批量算的还是在服务端实时算的底层逻辑必须从一个源头定义。更进一步的做法是把特征定义化用配置文件描述每个特征的名称、类型、参考窗口、聚合方式然后由一份代码根据配置生成对应的特征。这样训练和线上天然保持一致因为用的是同一份配置、同一份实现。有条件的话可以引入特征存储把特征集中管理但小团队一开始不用上那么重先做到训练和线上共用一套特征计算代码已经能规避大部分灾难了。4. 实验管理与模型训练把「跑通」变成「可复现」数据准备好了终于可以训练模型了。但训练这个环节工程化的重点不是「把模型调好」而是「让训练过程可以被复现、被比较、被管理」。在 AI 工程里训练只是一个环节不应该是全部。你需要问自己的问题是如果三个月后拿这版代码和数据重新训练能不能得到一模一样的结果如果跑过 50 次实验能不能准确说出每个版本之间的差异4.1 训练循环的封装先说不封装的做法在一个几百行的脚本里从读数据到建模到训练到保存全部写在一起。一开始确实灵活想改哪里改哪里但随着实验次数增加你会发现这个「方便」变成了最大的坑。我自己做训练代码会把训练循环封装成一个尽量简洁的 Trainer 对象它只做三件事接收配置和数据、执行训练、产出产物。核心训练逻辑在内部是稳定的外部只需要传入不同的配置就能调整行为。class SimpleTrainer: def __init__(self, model, optimizer, scheduler, config): self.model model self.optimizer optimizer self.scheduler scheduler self.config config def train_one_epoch(self, dataloader): self.model.train() total_loss 0 for batch in dataloader: loss self._compute_loss(batch) loss.backward() self.optimizer.step() self.scheduler.step() self.optimizer.zero_grad() total_loss loss.item() return total_loss / len(dataloader) def save_checkpoint(self, path): torch.save({ model_state: self.model.state_dict(), optimizer_state: self.optimizer.state_dict(), config: self.config, }, path)封装的关键点不是代码更「优雅」而是让实验变异的范围可控。你在几十次实验里真正改来改去的就是配置、数据和模型结构很少会改训练循环本身的逻辑。把稳定的部分固定下来把多变的部分收进配置才方便管理。4.2 实验追踪别靠记忆跑实验的次数一多人的记忆就不靠谱了。哪个版本 AUC 最高当时用的什么学习率数据是哪一版代码是哪个 commit如果你答不上来说明你的实验追踪没做到位。实验追踪的核心是建立一个「实验记录」它至少包含配置参数所有超参数、数据路径、模型结构参数。运行环境代码所在 commit、依赖版本、Python 版本。数据版本对应数据集的 manifest 信息。结果指标训练和验证的 loss、核心业务指标。产物位置模型 checkpoint、日志、配置文件的路径。我建议不用急着上复杂的实验管理平台先用一个简单的约定每一次实验的产物都放进一个独立的目录目录名包含时间戳和实验标识同时把一份 JSON 格式的实验元信息写进同一目录。experiments/ 20250601_1430_lr3e4_bs256/ config.json metrics.json model.pt train.log这样做的成本几乎为零但收益立竿见影。任何时候回看实验记录你都能知道当时跑了什么、结果如何。后面如果实验多了、团队大了再平滑迁移到 MLflow、wandb 之类的工具也不迟数据结构是一致的。4.3 超参数与 checkpoint 管理训练过程中的 checkpoint 管理也是工程化容易被忽略的一环。很多人的习惯是训练结束保存一个model_final.pt但遇到 OOM、训练中断需要续跑就傻眼了。我的经验是保存 checkpoint 时至少保存四样东西模型权重、优化器状态、随机数生成器的状态、配置信息。前两个是为了续跑第三个是为了复现第四个是为了追溯。分类说一下如果只是推理线上模型权重就够了轻量、加载快。如果是加速开发、中断续训必须包含优化器状态和随机状态。如果是为了提出一份可复现的实验那完整 checkpoint 加配置全都要。checkpoint 保存策略也有讲究不要每一轮都存硬盘会爆。我一般用「保存最优 定期保存」结合的策略保留验证指标最好的权重同时每 N 个 epoch 保存一个最近 checkpoint用于中断恢复。5. 评估与部署从模型到服务的最后一公里模型训练完不等于能交付。从模型文件到一个稳定在线的服务这中间的距离往往比想象的远。我见过不少团队离线评估做得挺热闹各种指标报告非常好看结果一上线就出乱子延迟直接打到几秒、显存爆掉、并发一高就超时。这些都是部署工程化没有做好的表现。5.1 离线评估的局限首先要正视一个现实离线评估的能力是有限的。你在测试集上刷到的指标只是真实场景的一个有偏抽样不能代表线上表现。既然如此离线评估的重点就不应该只是「看一个分数」而是应该分层第一层是对比性评估同一份测试集上新模型和线上模型的指标对比。这里的核心是统一口径两边的评估集、预处理流程必须完全一致否则对比没有意义。第二层是压力性评估把边界情况、长尾场景、少样本类别单独拿出来看而不是只看平均值。平均数会被高频正常样本拉高边缘样本的差劲表现往往被掩盖。第三层是业务指标评估如果技术指标和业务指标不联动评估就是空转。推荐场景可以量化成「预估命中率」风控场景可以量化成「误杀率」要让模型的分数能翻译成业务语言。5.2 模型部署从 estimator 到 API部署方式的取舍核心是速度和运维复杂度的平衡。最轻量的方式是用 FastAPI 包一个推理服务适合模型不大、并发不高的场景。它的优点是简单直接Python 生态的代码可以直接用缺点是需要自己处理并发、超时、扩容等问题。from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class PredictRequest(BaseModel): features: list[float] app.post(/predict) def predict(req: PredictRequest): result model.predict([req.features]) return {label: result[0]}需要特别注意几个生产级细节设置超时时间、限制请求体大小、添加请求量保护、记录推理日志。在代码里实现这些不就是几行配置的事但在压测环境里它们决定服务能不能扛住真实流量。如果模型更复杂比如大模型或者高并发诉求那就需要更专业的推理服务比如 Triton Inference Server 或 TorchServe。它们支持动态批处理、多模型管理、GPU 调度性能更强但相对的运维复杂度也更高。小项目先不用等遇到真实的吞吐瓶颈再上。5.3 推理性能工程模型部署最经常被低估的是推理性能的优化。CPU 还是 GPU要不要量化要不要批处理这些问题直接在线上表现为 p95 和 p99 延迟用户在业务上的体感就是「卡不卡」。先说几个基本概念。平均延迟对系统调优没有参考意义因为长尾请求才能暴露瓶颈所以我一般看 p95 和 p99。你压测时记录每个请求的延迟然后按百分位切出来就知道线上用户的真实体验。做性能优化的时候优先级大概是这样的从便宜到贵排列模型预热很多框架首次推理会触发初始化和显存申请延迟暴涨所以部署后要先发一个请求预热。请求批处理如果单请求推理很快但吞吐上不去可以把多个请求合并成一个 batch摊薄固定开销。模型量化从 FP32 降到 FP16 或者 INT8精度损失通常可控但延迟和显存占用能显著降低。缓存高频结果对重复请求做缓存能直接省掉重复计算的成本适合有大量相似输入的场景。每一项优化做之前和之后都要压测用数据说话不要凭感觉调。6. 线上监控与迭代闭环真正拉开差距的地方模型上线之后工作才刚刚开始。很多团队把「上线」当成项目的终点但对于 AI 工程来说上线只是把模型放到了一个会不断变化的环境里后面才是真正的考验。线上模型会老化、数据会漂移、业务会调整没有监控和迭代闭环的 AI 系统本质上是在裸奔。6.1 线上可观测性监控不是「看服务活着没」而是要能回答这几个问题当前线上模型是哪个版本请求量有没有异常波动延迟是否恶化输入数据的分布和训练时的分布还一致吗预测结果有没有异常集中要达到这个程度最简单的手段是记录推理日志。每次请求都记录下输入特征、模型版本号、预测结果和耗时定期分析。这些日志不仅是监控的基础也是后续分析线上问题、做样本回流的一手材料。{ timestamp: 2025-06-01T12:00:00Z, model_version: rec-v12, input_features: {user_id: u123, item_id: i456}, prediction: 0.87, latency_ms: 23.4 }有条件的团队可以上一个观测平台把请求量、延迟、错误率、特征分布四类指标做成仪表盘。没有条件时最简单的方案是把日志落到文件用脚本加定时任务做分析告警也比完全没有强。6.2 数据漂移与模型退化数据漂移是个大词但理解起来并不难线上真实输入的数据分布和你训练模型时的数据分布开始不一样了。比如你的推荐系统训练用的是冬季的数据到了夏季用户的行为模式变了模型的效果就会退化。检测数据漂移不需要多高深的算法先跑起来再说。我常用的两个指标特征均值/方差对比把近期线上特征和训练集特征做对比看数值分布是否出现明显偏移。分类特征的分布距离用 PSIPopulation Stability Index或者 KL 散度衡量分布差异设置一个阈值超过就告警。这两项都可以做成定时任务每天跑一次输出一份分布对比报告。重要发现不是所有漂移都需要立刻处理。有些漂移是季节性的、暂时的过一阵子自己就恢复了贸然重训可能打乱线上稳定。正确的做法是「先观察、再归因、后行动」。6.3 反馈闭环与自动重训成熟的 AI 系统一定要有一个「从线上回到训练」的反馈闭环。否则模型上线之后就是一条单行道只能等它越来越糟然后人工手动去救火。我理想中的闭环是这样的线上产生预测数据记录推理日志。定期从推理日志中筛选「有价值的新样本」进入待标注队列。经过清洗和标注后样本加入训练集触发一轮新的训练。新模型通过离线评估后进入灰度发布逐步替代旧模型。这个闭环不需要一开始就全自动可以先做半自动定时生成候选样本清单由人确认后触发训练。跑顺了再把确认规则自动化。不要一上来就搞全自动重训AI 系统最怕的就是自动化了一堆你还没想清楚的决策最后引入的问题比解决的还多。7. 避坑速查我踩过的十三个 AI 工程陷阱到最后分享一些实战总结。这部分内容很杂但每一件都是真实踩过坑之后的血泪记录希望你能绕开。7.1 问题速查表症状根因解法换了台机器就复现不了结果依赖版本没有锁定用锁文件固定全量依赖不要用宽版本号训练到一半进程被杀白跑两小时checkpoint 只保存在最后定期保存保存优化器状态线上特征和训练特征总对不上训练和推理用两套特征代码收敛到同一份特征计算模块模型上线后效果暴跌数据漂移或 batch size 变化部署前做特征分布检查上线后监控漂移显存突然爆掉多个进程共享 GPU 显存显存隔离每进程限制使用比例并发一高就超时没有做批处理和预热加模型预热、动态批处理、压测验证同样的代码今天跑明天结果不同随机种子没固定固定 python、numpy、pytorch 的随机种子实验记录乱七八糟说不清哪个最好没有统一的实验目录每个实验一个目录写 config metrics训练时用了未来数据特征里有时间泄漏做数据质量校验检查特征时间窗口测试集和训练集有重叠数据划分不合理按时间或主键划集做记录模型服务一段时间后变慢内存泄漏或缓存膨胀定期检查内存设置缓存上限新模型离线很好、线上更差评估集分布和线上不一致用线上日志构造评估集告警邮件太多没人看了告警阈值不合理分级告警只对 actionable 事件推送这张表我建议收藏起来遇到类似性能问题时对照排查能省掉很多走弯路的时间。7.2 最后再分享一个小技巧最后一个个人经验对我自己的帮助非常大从零开始做 AI 工程不要一上来就追求完美体系。我见过很多同行兴致勃勃地规划「完整的 MLOps 平台」结果搭了两个星期还没开始训练热情先耗尽了。正确路线是先搭一条最短的、能跑通的链路——哪怕它只有一个 Python 脚本加一个 requirements.txt——然后把链路跑熟再逐步把版本管理、实验追踪、监控这些补进来。我自己现在的习惯是任何新的 AI 项目动工前先强制自己回答四个问题。数据从哪来、如何验证训练用什么配置、如何记录模型如何部署、如何服务线上如何监控、如何回流回答不上来的地方就是工程化要优先补的地方。「ai-engineering-from-scratch」这个思路对我的价值不是把每个工具都从轮子造起而是让我对整条链路的每一个环节都有掌控力。我可以随时回答依赖是什么、数据是什么、模型是什么、服务是什么、变化在哪里。这种掌控感是直接用平台时感受不到的。在 AI 工程这条路上我也是边做边踩坑边总结但这些经过验证的基本功哪怕未来技术框架换了几轮依然会持续发挥作用。希望你也能从底层开始搭起属于自己的那一套体系。