标题里带“from scratch”是很要命的。见过太多人一上来就追求最前沿的大模型微调方案,结果连自己的数据长什么样都没搞明白;也有同学简历里写了三四个AI项目,一问到“模型怎么上线的”就沉默。我叫它“AI工程”,不是因为它听起来比“算法工程师”高级,而是因为它的本质正在于此:让模型从想法走到可用,走完一条完整的、可重复的、不靠运气的路。
这篇文章就是围绕“ai-engineering-from-scratch”这个话题展开的。我会从AI工程的能力地图讲起,聊清楚工程化和“调参实验”之间的边界,然后逐步拆开环境搭建、数据工程、训练评估、部署监控这几个核心环节,中间穿插具体参数、命令、排查思路,以及我个人踩坑后的经验。适合谁看?一种是已经在做算法或后端开发,但觉得AI项目“跑通就完了”之后没下文的人;另一种是想转行AI工程方向、希望系统梳理知识体系的同学。文章不会从线性代数重新讲起,但会解释很多课堂不会告诉你的事情。
1. AI工程到底是什么,以及为什么值得从零盘一遍
1.1 科学与工程的分界线
学术界关心“这个方法能不能work”,工程界关心“这个方法能不能一直work,并且work得可控”。在AI语境里,区别尤其明显。
你可能在一个Notebook里跑通了一个效果不错的分类模型,准确率98%,于是你觉得任务完成了。但工程会追问:这98%是在哪一批测试数据上得到的?这批数据和线上真实分布一致吗?如果明天数据分布变了,模型还在跑吗?重训一次需要多长时间,谁触发重训?模型推理延迟能不能扛住线上流量?如果有人误用了这个模型,本地复现不出你的结果,怎么办?
这些问题单个看起来都不难,难的是它们之间的耦合。数据、代码、模型、配置、监控,任何一环出问题,都会让整个系统毫无征兆地退化。AI工程就是把这些环节当成一个完整的生命周期来管理,而不是每次临时救火。这也是为什么“from scratch”值得做——不是从零重复造轮子,而是从零理解每一个轮子为什么需要存在、在什么条件下会转不动。
1.2 端到端全链路应该长什么样
我习惯把AI工程拆成六个环节:需求与指标定义、数据与特征管线、模型训练与评估、部署与服务化、监控与反馈闭环、迭代与治理。很多人把注意力全放在第三个环节,但实际上前三者往往是瓶颈。
举个具体的例子,需要做一个垃圾评论识别系统。需求阶段要定的不是“用BERT还是用CNN”,而是“误杀率允许多少”和“漏过率允许多少”。这两个指标直接决定了数据标注的倾向、模型阈值的选择、以及后期人审的工作量。数据工程师根据这两个指标去设计采集规则、清洗策略、标签体系。训练工程师拿到的应该是一个有明确分布说明、版本可回溯的数据集,而不是一堆就躺在共享目录里的CSV。
模型上线后,监控系统要回答的也不只是“模型还活着吗”,而是“模型在最近一周的预测置信度分布是否发生了变化”、“新增样本里有没有训练阶段没见过的新话术模式”。这一环如果缺失,前面所有的努力都会在某个凌晨突然失效,而你没有任何可用的日志去定位。整个链路串起来,才是AI工程的全貌。
2. 从零搭建一套靠谱的AI工程环境
2.1 Python环境、依赖管理和容器化的口味差异
AI项目几乎没有不依赖一堆第三方库的,PyTorch、NumPy、pandas、scikit-learn,再加上各类工具链。问题从来不是“安装一个库”,而是“让所有人在任何机器上都能复现同一个环境”。
我见过很多团队的环境管理是用了一个requirements.txt,写死了几个大版本,然后靠“在我机器上能跑”来混日子。直到某天别人clone代码,装完依赖,训练出来的指标少了两三个点,排查了两天才发现是NumPy版本差异导致随机数序列变了。
聊聊我现在的标配做法。Python版本管理用pyenv,项目环境管理用venv,团队统一用Docker固定可复现环境。关键点在于:pyenv负责“装不同版本的Python”,venv负责“隔离依赖”,docker负责“镜像分发”。三者职责不同,不要混用。
如果项目里要用GPU训练,Docker里还得注意NVIDIA容器工具包(nvidia-container-toolkit)的配置,否则容器里看不到卡。具体的启动命令大概长这样:
docker build -t ai-train-env:latest . docker run --gpus all -v $(pwd):/workspace -it ai-train-env:latest bash第一次写Dockerfile时容易把镜像体积搞得巨大,什么依赖都往里面塞。我的习惯是分阶段安装:基础镜像固定CUDA版本,然后单独安装训练依赖,再单独装部署推理依赖。这样最终镜像体积能压缩30%到50%,对后续分发很有利。
2.2 依赖锁定与精确复现的细节
requirements.txt写版本号时,大多数人写的是“numpy>=1.20”,但你不知道1.20到1.26之间某个版本会改变某些函数的默认行为。严谨做法是锁定精确版本,最好再加哈希校验。pip可以用pip freeze生成当前环境的精确版本清单,但freeze的输出不一定适合作为上层依赖。
更可靠的做法是分两个文件:一个requirements.in放顶层直接依赖,一个requirements.txt放锁定后的完整依赖树。配合pip-tools或poetry这类工具做约束解析。Poetry的lock文件机制我个人比较喜欢,它能同时维护项目依赖和开发依赖。
需要说明的是,Docker镜像本身也不是完全一劳永逸的。镜像构建过程中会从PyPI等源拉包,如果没有把下载源替换成公司内部镜像源,每次构建的时机不同,拉到的包版本可能也不同。所以我会把构建依赖源的步骤写进Dockerfile,同时在CI里固定基础镜像的digest,避免“基础镜像悄悄变了”这种隐性复现问题。
2.3 环境之外的项目骨架与配置管理
环境搭好只是第一步,代码组织同样影响工程效率。AI项目的代码结构我推荐按功能域划分:
- configs/ # yaml配置文件,环境无关 - data/ # 数据脚本与数据说明 - features/ # 特征工程代码 - models/ # 模型定义与训练入口 - evaluations/ # 评估脚本 - deployments/ # 部署与推理服务 - tests/ # 单元测试与数据校验配置管理有个核心原则:代码和环境分离,配置和代码分离。环境相关的东西,比如数据路径、API密钥,永远不要写进代码里,而是通过环境变量或单独的config文件注入。训练超参数习惯上放yaml文件里,这样能方便做多组实验对比,也能把每次实验的完整参数记录到日志中。
另外要提一下随机种子。很多项目复现不齐,未必是代码bug,而是随机种子没有完全固定。PyTorch里需要同时设置torch.manual_seed、numpy.random.seed,还要关掉cuDNN的确定性模式开关torch.backends.cudnn.deterministic = True。但要注意,开确定性模式会降低训练速度,所以只在需要精确复现时才开。这个取舍,工程上要心里有数。
3. 数据工程:AI工程里最容易被低估的环节
3.1 数据版本管理比代码版本管理更麻烦
代码可以用Git管理,数据呢?数据集动不动几十GB甚至TB,不可能直接塞进Git仓库。但训练结果的可复现性,恰恰又极度依赖“当时用的到底是哪一批数据”。
现在通行的做法是用专门的数据版本管理工具,DVC是其中典型。它不把真实数据存进Git,而是记录数据文件的哈希值,以及在远程存储上的位置。训练时,DVC会根据配置还原到对应版本的数据文件。
听起来很顺理成章,但实际操作中有个坑:DVC的远程存储需要在团队里提前配置好,可能是对象存储,也可能是NFS或NAS。如果远程存储的访问权限不一致,或者网络带宽不一致,队友拉数据时的体验会非常割裂。所以我在项目启动时,第一件事就是把远程存储通道路通,并且把“拉取数据的命令”写进项目README,当作日常操作,而不是一次性部署任务。
3.2 数据清洗和基础质量校验的工程化
很多人的清洗逻辑是一堆脚本,跑完一次就不再维护。工程化的做法,是把清洗阶段提炼成可复用的管道步骤,每一步都带校验逻辑。
我记得有一次接手一批用户行为日志,清洗流程里有一行代码是“过滤掉时长小于1秒的样本”,但没写为什么。结果后来业务方说短时长样本在某个频道里恰恰是重要信号。因为没有记录过滤规则的产生背景,后续团队不敢动这行代码,模型效果一直上不去,直到追溯到源头才解决。所以不管清洗规则多简单,都要在代码注释里写清楚“为什么过滤”“依据是什么”“阈值怎么定出来的”。
数据质量校验可以靠Great Expectations这类工具来保持活文档,它对数据集的列名、数据类型、缺失值比例、值域范围等做断言。训练前跑一遍,线上数据异常时预警能提前很多。哪怕团队初期没有这类工具,至少也要在读取数据后加assert断言,把“数据不符合预期”暴露在训练前,而不是训练出一个奇怪模型之后才反过来怀疑数据。
3.3 数据漂移与线上分布对齐
训练数据和线上真实数据分布不一致,是AI工程里最隐蔽的坑。写训练代码时,拿到的往往是历史样本;线上服务时,用户行为可能已经变了。如果只关注训练集上指标好看,上线之后翻车是大概率事件。
所以成熟的AI工程一定会做数据漂移检测。做法一般分两步:统计层面用PSI(群体稳定性指数)或KL散度对比特征分布;采样层面用“属性相似度”判断当前线上的样本是否落在训练分布的高密度区域。这些指标不是模型上线后才开始计算,而是在数据管线里定期输出,和模型的业务指标一起看板展示。
我见过一个团队,他们的模型线上A/B测试没通过,但离线评测很好。后来排查发现,原因是线上请求的特征填充率比训练时低了8个百分点,某些特征大量为空。这就是典型的数据对齐问题,和模型本身的算法能力无关。
4. 模型训练与评估:从“跑通”到“可控”
4.1 训练脚本的正确姿态
初学者的训练代码往往是把Notebook里的一段顺序执行搬成一个py文件,变量名沿用data1、data2,参数硬编码在头部。这能跑,但只支持“一次性跑”。
工程化训练脚本的关键在于:以配置文件驱动,以命令行覆盖,以日志和实验记录为最终结果。我用Hydra或配置文件管理超参数,训练脚本本身不包含任何固定数值。必要的时候,命令行参数可以覆盖配置文件中的值,方便做快速消融实验。
训练循环里,检查点保存也是门学问。不要每次epoch都保存完整模型,那样磁盘很快就不够用了;也不要只在最终epoch保存,因为可能最后几步loss飙高。正确做法是:监控验证集指标,在指标最优时保存检查点,同时保留最近N个检查点以便回滚。实际开发时我用PyTorch Lightning的ModelCheckpoint回调,设置monitor参数监控验证集指标,mode设为max或min。
4.2 评估指标应该跟着业务走
“准确率96%”这类指标在工程评审中几乎没有信息量。业务关心的是误杀率、召回率、覆盖率、时延成本和运营人工成本。模型评估必须回到需求阶段定的指标上。
我建议团队的评估模板里至少包含三类指标:
- 核心性能指标:准确率、精确率、召回率、F1等,按业务分群分别看,不要只看总平均。
- 鲁棒性指标:在不同时间段、不同用户群体、不同渠道上的表现差异。
- 工程指标:推理延迟、显存占用、单次推理成本。
有一个点容易被忽略:阈值的选择。很多库的默认预测阈值是0.5,模型输出的概率分数达到0.5就判正。但业务场景往往要求不同的误杀和漏过权衡。这时候要做阈值扫描,选择在业务损失函数意义下的最优阈值,而不是简单接受默认值。Sklearn有precision_recall_curve可以辅助做这件事。
我做过一个文本分类项目,业务要求误杀率不超过1%,一开始阈值设为0.5时误杀率是3%。通过扫描阈值,找到0.82附近时误杀率降到0.8%,但召回率从75%掉到了62%。这时不是简单地选阈值,而是要和业务方沟通清楚,可能的动作包括调整模型结构、增加特征、或者接受更低的召回。这就是评估指标服务于业务决策的真实过程。
4.3 训练调试中的“玄学”与基本法
loss不降、loss炸了、验证集指标和训练集差距过大……这些问题是每个AI工程师日常都会遇到的。我的排查顺序一般固定为:先看数据,再看实现,最后才怀疑超参。
先看数据是指:训练集里是否存在标签错误、特征泄漏、异常分布。特征泄漏是最难查的,典型例子是时序预测里把未来信息当特征用了。再看实现是指:损失函数公式方向、梯度是否被正确传播、数据预处理是否和推理时一致、模型保存和加载时是否有状态丢失。最后才考虑超参,比如学习率过大导致loss震荡、batch size过小导致梯度噪声大。
有一个经验技巧值得分享:先从一个小数据集上把模型跑过拟合。如果你在几十条样本上连过拟合都做不到,那大概率代码哪里有bug。能过拟合说明模型有足够容量学习和记忆数据,然后再逐步扩大数据量,观察泛化表现。这条路径比直接上全量数据盲调高效得多。
5. 部署与服务化:模型从研发到生产的临门一脚
5.1 模型服务化选型与适配
训练完的模型是要给人用的,无论是实时API还是批量离线计算,都要走服务化。最简单的方案是用Flask/FastAPI包一个HTTP接口,把模型加载进内存做推理。对原型验证来说够用,但对生产环境,还要考虑并发、超时、排队、抢锁、批处理等问题。
对于工业级场景,我一般按需求分三层选型:
- 轻量且依赖简单的模型:直接用ONNX Runtime或者TensorRT做推理加速,不必引入额外的推理框架。
- 中等规模模型:TensorFlow Serving或TorchServe,能自动管理多模型版本和多worker。
- 大模型或生成式模型:还需要考虑KV cache、连续批处理等能力,这时考虑vLLM、Triton这类推理服务框架更合适。
选型时要评估一个细节:模型输出的内容和中间过程,是否需要注册到外部系统。很多推荐类或风控类的线上推理,还需要保留每个请求的特征快照和决策日志。这些不是模型推理框架原生支持的,需要服务层自治。
5.2 推理性能优化与资源评估
在线服务的优化核心是延迟和吞吐。延迟指单个请求从发出到收到响应的时间,吞吐指单位时间能处理的请求数。两者往往有冲突:通过动态批处理可以提升吞吐,但单个请求会等积累更多样本一起推理,延迟会变大。
对于GPU推理,我习惯先做一次显存占用量化估算。比如一个BERT-base模型参数量约110M,FP32权重占440MB,加上激活值、优化器状态和CUDA context,实际峰值占用会远超这个数。如果不够就换FP16,或者做模型量化。工程上要反复权衡成本和效果,不要盲目追求用大模型。
实际生产里,请求排队打满显存的情况很常见。我会在服务入口加信号量或并发队列控制,防止流量冲击时把服务打挂;同时为每个模型部署配置“最大batch size”和“最大队列长度”两个参数,两者都不能取太大。这几个参数是运维事故中救人的关键。
5.3 线上监控与模型回退机制
模型部署上线只是一个起点。监控要做的第一件事是存活探活,第二件事是质量监控。存活探活容易理解,周期性地发一个心跳请求,确认服务还能响应。质量监控复杂很多,需要区分数据漂移、概念漂移和业务指标异常。
数据漂移:线上输入特征的分布与训练集差异变大。概念漂移:输入分布没变,但输入到输出的映射关系发生了变化,老模型不再适用。比如用户兴趣整体迁移,同样特征对应的点击概率完全不同。
监控方案会提前把阈值定好,比如PSI在0.1以内正常,0.1到0.25需要关注,超过0.25就要触发告警。告警不是直接通知人就行了,还要伴随一个预案:到底是继续观察、切回上一个稳定版本、还是触发自动重训。如果没有预案,告警只是把问题从“没人知道”变成“知道了但不知道怎么处理”。
6. 常见问题排查与避坑经验速查
6.1 典型问题排查对照
| 症状 | 可能原因 | 排查思路 |
|---|---|---|
| 训练loss不降 | 学习率过大/过小,数据标签噪声高,特征没有归一化 | 先在小数据上试过拟合,检查loss曲线的下降趋势 |
| 训练损失降,验证损失升 | 过拟合训练集,数据分布不一致 | 增加正则化、数据增强、检查验证集的构成 |
| 推理延迟波动大 | 动态批处理策略激进、网络带宽抖动、GPU共享 | 压测定位瓶颈,分别测试CPU与GPU耗时 |
| 线上线下性能差异大 | 特征线上填充率低、pipeline处理不一致 | diff特征值,对比线上请求日志与训练数据 |
| 模型偶发返回空结果 | 输入长度超限、服务并发竞争、超时切分异常 | 在服务层加超时与异常捕获,记录完整请求与响应 |
| 镜像构建时GPU驱动不匹配 | CUDA基础镜像和宿主机驱动不兼容 | 用nvidia-smi查驱动对应的CUDA版本,匹配基础镜像 |
| 复现不出历史训练结果 | 数千随机种子未固定、依赖版本漂移、数据版本错乱 | 确认训练哈希、代码提交号和配置文件的完整性 |
这个对照表是我多年项目运维的沉淀,但真正上手排查时,不要贪图快速靠经验定论。按“先数据、再代码、后超参”的顺序来,大多数问题都能在半小时内定位到方向。
6.2 工程效率相关的非技术坑
AI工程里有一类坑和技术无关,但破坏力极大——团队协作方式。代码评审时大家大概率会关注模型结构和指标,却很少人review数据管道脚本、标签定义文档、配置变更记录。我后来养成一个习惯:每次提PR时,必须附上“数据影响说明”和“配置变更说明”。不需要长篇大论,三五行讲清楚“加了什么过滤条件”“哪个阈值变了”“为什么变”就行。这几行说明救过我好多次,让后续接手的人能快速理解改动的意图。
另外,实验记录也要工程化。不要只记录“改了个模型”,“把学习率从1e-4调到3e-5”这种描述没有索引价值。理想的做法是每次实验自动记录:git提交哈希、配置文件全文、数据版本、环境依赖快照、训练日志关键指标、模型产物路径。现在MLflow这类工具可以直接省掉这些工作,最好从第一个实验就用起来,不要等实验多了再补。
6.3 从“项目能跑”到“系统可靠”的经验补全
在AI项目里,我学到的最朴素的经验是:把不确定性显式化,不要掩盖在代码里。数据漂移检测不是“锦上添花”,而是“计时炸弹的拆除工具”;随机种子的管理不是“仪式感”,而是“可复现工程的底线”;线上监控不是“运维部门的事”,而是“模型质量的一部分”。
从零搭建AI工程最大的难点不是任何单个技术,而是如何让每个环节都保持“可解释、可重复、可调试”的状态。模型可以是复黑盒,但工程链路不能是黑盒。
结尾:关于“从零开始”的最后一句话
做AI工程越久,我越觉得“from scratch”真正的含义不是从零发明轮子,而是从零建立一套属于自己的判断标准。别人推荐的框架、模板和最佳实践都可以抄,但遇到“这个模型的阈值到底定多少”“这周的漂移指标升了要不要干预”“这个数据质量问题是不是该阻塞版本发布”这类问题时,唯有你自己对整条链路的理解,才能给出负责任的答案。
如果你正准备开始一个AI项目,我的建议是:先不要着急写模型,花一个周末把环境、数据版本和配置管理搭好,然后用一个最小模型把从训练到部署的完整流程走一遍。这个过程可能会很枯燥,但它会帮你省掉后面无数个“为什么我的模型烂得莫名其妙”的夜晚。踩过几次坑之后,你自然会发现所谓的AI工程不是某个炫酷的工具,而是一种把不确定事物管理起来的习惯。