
AI工程这个词这两年听得人耳朵起茧。但你要真问一句AI工程到底在做什么十个有九个答不利索。有人说就是把模型训练出来有人说是调参调出高精度还有人说是写接口给前端调用——都不全对。我当年从算法岗转到AI工程岗第一个项目就把我捶得鼻青脸肿。模型在Jupyter Notebook里跑得飞起准确率92%老板看完直点头。结果要上线了问题一串接一串模型怎么封装成服务QPS一上来就超时GPU显存泄露数据预处理在训练和推理时对不上模型版本跟代码版本对不上……这时候我才反应过来学校里教的、网上教程讲的全是怎么训练一个模型而工业界真正缺的是怎么把一个模型变成稳定运行的软件系统。这个ai-engineering-from-scratch的项目就是想系统梳理一条从零开始AI工程实践的路线。我摸索了大半年把踩过的坑、验证过的方法、能直接抄作业的配置都整理出来今天这篇就当是交个底给同样在这条路上折腾的人一个参考。1. 先想明白AI工程和算法岗到底差在哪儿我刚转岗那会儿满脑子还是把损失函数调得更低。后来被现实教育了才明白AI工程的关键命题不是模型有多聪明而是系统有多可靠。这两者之间隔着一整条生产链路的差距。1.1 训练代码和工程代码不是一回事很多人有个误解觉得算法工程师写的代码稍微改改就能上线。我在第一个项目里就是这么天真结果被QA怼得说不出话。训练代码的本质是探索——数据怎么洗、特征怎么组合、loss怎么调目标是得到一个好的模型权重。而工程代码的本质是交付——接口要稳定、响应要快、并发要扛得住、故障要能恢复。这是两套完全不同的思维模式。打个比方训练代码像是厨师研发新菜看重的是味道工程代码像是连锁餐厅的标准作业流程看重的是每一份出品的稳定性和出餐速度。你把研发厨房的配方直接丢给中央厨房生产不出事才怪。具体到代码层面区别更实在训练脚本可以容忍代码写得乱反正跑一次就完事工程代码是要长期维护的结构、注释、异常处理都是刚需。训练阶段处理的是离线数据格式不对重新清洗就行推理阶段处理的是线上实时数据脏数据直接进模型结果错了都不知道错在哪。训练代码跑在GPU服务器上挂了就重新跑工程服务跑在用户请求链路上挂了就是线上事故。所以我后来接手任何项目第一步就是问这个模型的交付形态是什么在线服务还是离线批量推理这决定了整个工程架构的走向。1.2 为什么从零开始反而是一条捷径市面上AI课程多如牛毛但绝大多数教你的是用框架搭模型而不是把系统搭起来。直接去学MLOps、Kubernetes这些大词新手往往被劝退——概念太碎、工具太多、不知道从哪儿下手。我的体会是从零开始手写一遍AI工程的最小闭环比一上来就学那些重型框架高效得多。原因有三个第一手写一遍能真正理解每个环节的痛点。你手动管理过模型版本才会懂得MLflow存在的意义你手动发过一次模型更新导致线上事故才会明白CI/CD在ML领域有多重要。没有痛过的工具选型都是盲目的。第二最小闭环能建立完整的系统观。你会自然理解数据、模型、代码、配置这些组件是怎么咬合在一起的而不是只看某一个点。第三从零开始便于搭建适合自己的脚手架。我后来做的几个项目底子都是第一次手撸闭环时沉淀下来的模板。这条路走下来最大的收获不是写出多少代码而是建立起一种条件反射——看到一个模型要上线脑子里自动浮现出服务化、容器、监控、版本管理这一整套流程。2. 搭建AI工程能力的核心底座从零开始不是让你把AI理论全部啃一遍再动手而是要够用就好边做边补。我总结下来真正的地基是三个东西工程化Python能力、数据版本管理意识、模型交付思维。2.1 Python工程能力别停留在写脚本阶段很多做算法的人Python底子停留在Notebook水平定义一堆函数全局变量满天飞各种魔法数字。这种代码在工程场景下就是灾难。AI工程对Python能力的要求是能写被他人维护的代码具体来说有几个硬指标面向对象设计模型封装成类预处理封装成类配置用dataclass而不是到处传dict。异常处理网络超时、数据格式非法、模型推理异常每一种都要有兜底逻辑。装饰器和上下文管理器实现推理超时控制、资源自动释放、日志记录这些横切逻辑。测试意识对预处理器写单元测试对接口写集成测试防止改了A功能弄坏B功能。我见过太多项目死在Python写得太自由上。有人一个文件写了3000行函数之间互相调用连自己都记不清数据流怎么走的。工程化的第一步就是把代码当成会被别人读的作品来写。我自己的习惯是任何项目一开始就建好包结构按照src/、tests/、configs/、models/、notebooks/分层。这样不管是调试还是后续扩展心里都有一张地图。2.2 数据版本和模型版本AI工程的账本普通软件工程管理的是代码版本AI工程要管的东西多得多数据集版本、模型权重版本、特征工程代码版本、超参数配置版本。哪个对不上复现就是一句空话。我早期做项目吃过一次大亏。训练时用了一个清洗过的数据集A后来数据集被重新处理成了B代码里没记录用哪个版本训练的。线上效果出了问题想回滚到之前的模型才发现根本不知道该配哪份数据。所以后来我的每个项目都强制做三件事数据集生成时记录哈希值或版本号放在training config里让每次训练都知道自己吃了什么数据。模型checkpoint命名带上实验ID和关键指标比如model_bert_acc899_epoch5.pt一目了然。每次实验写一个简短记录模型路径、数据版本、超参、指标、备注哪怕就几行markdown。在这个基础上MLflow这类工具才有用武之地。但我的经验是新人先别急着上工具手工把版本管理的习惯建立起来再上工具理解会深很多。2.3 模型交付从能跑到能扛模型交付是AI工程和算法研究最明显的分水岭。研究阶段模型能出结果就成工程阶段模型要过三关功能关、性能关、运维关。功能关指接口行为符合预期输入输出格式正确错误请求有合理的返回。性能关指响应延迟可接受、吞吐量达标、资源占用合理。运维关指服务可监控、可告警、可优雅上下线、出问题能快速定位。这三关每一关都有对应实操要点不过先不急着展开我下面用一个完整的最小项目来演示这条链路怎么跑通。3. 实操一个完整的AI工程闭环纸上谈兵没意思我拿一个非常经典的场景来走一遍全流程训练一个文本分类模型比如情感分类然后把它做成一个HTTP服务部署上线。这个项目麻雀虽小五脏俱全覆盖了AI工程的主干链路。3.1 项目目录结构设计先看目录结构这是我几次重构后沉淀下来的模板text-classifier/ ├── configs/ # 所有配置集中管理 │ └── config.yaml ├── src/ # 核心代码包 │ ├── data.py # 数据加载与预处理 │ ├── train.py # 模型训练脚本 │ ├── model.py # 模型定义 │ └── predict.py # 推理封装 ├── models/ # 模型权重存放 ├── tests/ # 单元测试 ├── serve/ # 服务化相关 │ └── app.py # FastAPI接口 ├── docker/ # 容器化配置 │ └── Dockerfile └── requirements.txt这个结构解决的核心问题是什么代码放在什么地方。新手最容易犯的错是全都塞一起最后变成一个大杂烩。我见过太多项目训练脚本、服务代码、数据清洗逻辑全都混在几个文件里维护起来简直要命。配置全部集中在configs目录好处是训练和推理用同一份配置避免训练时一个参数、上线时另一个参数的经典翻车。3.2 训练代码从Notebook风格进化到工程风格我以前写训练代码也是标准的Notebook思维直接加载数据直接定义模型直接循环训练然后保存权重。后来被坑了几次总结了几个关键升级点。第一所有超参数走配置不走硬编码。比如学习率、batch size、epoch数全部放在config.yaml里data: train_path: ./data/train.csv test_path: ./data/test.csv model: vocab_size: 10000 embedding_dim: 128 hidden_dim: 256 num_layers: 2 train: batch_size: 64 learning_rate: 0.001 epochs: 10 seed: 42 save_dir: ./models这样做的意义是跑实验时想调参直接改配置不用翻代码记录实验时把配置文件一copy就是完整的参数快照。第二固定随机种子。我第一次跑实验时没固定同样的代码两次训练结果不一样排查了半天还以为是bug。后来老老实实在训练脚本开头加了一段种子固定逻辑。import random import numpy as np import torch def set_seed(seed: int) - None: random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)第三保留checkpoint而不是只存最终模型。训练到第3轮和第8轮模型状态完全不同。磕磕绊绊训练出来的最终模型不一定是最好的。我在训练脚本里每个epoch都保存一次checkpoint保存时带上epoch和验证指标。torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), val_acc: val_acc, config: config, }, fmodels/checkpoint_epoch{epoch}_acc{val_acc:.4f}.pt)训练完成后还要写一个简单的评估脚本在独立的测试集上评估模型效果确保不是过拟合到验证集上。这个过程不用很复杂但一定不能省。3.3 推理封装把模型变成可调用的组件训练完之后模型权重躺在models目录里接下来是工程化的关键一步把模型封装成一个物件让任何调用方都不需要关心模型内部实现。我在predict.py里写了一个Classifier类核心点在于把预处理、模型推理、后处理全部封装在一起class Classifier: def __init__(self, model_path: str, config_path: str): self.config load_config(config_path) self.tokenizer build_tokenizer(self.config) self.model load_model(model_path, self.config) self.model.eval() if torch.cuda.is_available(): self.model.cuda() def predict(self, texts: list[str]) - list[dict]: inputs [self.tokenizer.encode(text) for text in texts] # 填充到相同长度 batch pad_sequence(inputs, batch_firstTrue).long() if torch.cuda.is_available(): batch batch.cuda() with torch.no_grad(): outputs self.model(batch) probs torch.softmax(outputs, dim-1) labels torch.argmax(probs, dim-1).cpu().tolist() results [] for text, label_id, prob in zip(texts, labels, probs): results.append({ text: text, label: self.config[id2label][str(label_id)], confidence: float(prob[label_id].cpu()) }) return results这里有几个工程细节值得强调with torch.no_grad()推理阶段不需要梯度计算不写这个会白白消耗大量显存。批量推理而不是单条推理接口层可能一次收到多条请求代码层面支持list输入性能完全不一样。model.eval()与model.train()切换Dropout和BatchNorm在两种模式下行为完全不同忘记设成eval模式会导致线上推理结果异常这是非常经典的bug。数据预处理封装在类内部训练和推理共用同一套tokenizer避免两边用了不同的处理逻辑导致特征分布漂移。3.4 用FastAPI搭一个模型服务模型封装好了接下来要暴露给外部调用。FastAPI是我用过最顺手的框架性能好、自动生成API文档、类型校验省心。serve/app.py的核心逻辑from fastapi import FastAPI from pydantic import BaseModel, Field import uvicorn app FastAPI(titleText Classification Service) class PredictRequest(BaseModel): texts: list[str] Field(..., min_length1, max_length32) class PredictResponse(BaseModel): results: list[dict] classifier Classifier( model_path./models/checkpoint_epoch9_acc0.9300.pt, config_path./configs/config.yaml ) app.get(/health) def health_check(): return {status: ok} app.post(/predict, response_modelPredictResponse) def predict(request: PredictRequest): results classifier.predict(request.texts) return PredictResponse(resultsresults) if __name__ __main__: uvicorn.run(app:app, host0.0.0.0, port8000, workers1)几个设计点是我踩坑之后沉淀下来的使用Pydantic的BaseModel做请求体校验texts最大32条防止恶意大请求打爆显存。之前我没有限制直接被一个大batch把显存干爆了。加上/health健康检查端点Kubernetes或Docker Compose做探针时会用到。没有健康检查的容器化服务调度平台根本没法自动帮你恢复。workers1模型加载到内存/显存里是有开销的多worker会导致模型被加载多份内存翻倍。需要增加吞吐时优先思考是否要换异步推理或加缓存而不是单纯加worker数。特别注意多次请求时会并发调用模型如果你的模型不是线程安全的某些自定义层有状态需要加锁或改用独立线程处理。这个坑不遇到一次根本意识不到。3.5 容器化部署让服务随处可跑Python环境依赖管理是个大坑。开发机跑得好好的换台机器就各种报错Python版本不对、依赖冲突、CUDA版本不匹配。容器化是解决这个问题的终极手段。我的Dockerfile长这样FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD [python, serve/app.py]构建镜像的命令很简单docker build -t text-classifier:v1.0 . docker run -d -p 8000:8000 --name classifier text-classifier:v1.0然后就能用http://localhost:8000/docs看到接口文档直接在线调试。容器化这块有两条经验分享第一镜像构建时.dockerignore一定要配好。如果不排除__pycache__、.git、数据文件这些镜像体积能膨胀好几倍。第二如果你用了GPU推理比如在线的BERT模型Docker跑的时候要加--gpus all参数同时在Dockerfile里装好CUDA相关的库。CPU可以推理的模型容器体积和启动速度会友好很多。模型服务化之后还有一步很关键测试一下真实请求的延迟和吞吐。我一般用hey或wrk做简单的压测看看服务在并发情况下的表现。这一步能提前发现很多性能瓶颈比如Python预处理太慢导致接口整体延迟飙高。4. 踩过的坑总结AI工程高频问题与排查实录整个流程走完说几个我真实踩过、也帮你验证过的经典问题。这些问题在教科书上很少提到但在生产环境里几乎必然遇到。4.1 训练和推理的数据预处理不一致这是我见过最多的翻车现场。训练时对文本做了一堆清理去HTML标签、转小写、去停用词、分词然后保存了个tokenizer但线上推理时只做了一部分。结果模型在线上看到的输入分布跟训练时对不上效果莫名下降。排查方法也不复杂拿同一条输入分别在训练管道和推理管道里跑一遍对比中间结果。这一步要固化到测试用例里每次改代码都要回归。我的原则是预处理函数写一次训练和推理都引用同一个实现。如果硬要分开写就必须写测试确保两者输出一致。血和泪证明这个测试绝对值回票价。4.2 推理服务的线程安全Python里跑模型推理新手最容易忽略线程安全问题。FastAPI默认是异步处理请求的多个请求同时进来会同时执行模型前向推理。如果你的模型用的是PyTorch大部分情况下是线程安全的因为底层的CUDA调用有全局锁保护。但如果你用了一些自定义的预处理逻辑里面有共享的可变状态比如全局计数器、缓存list并发请求下就会出现诡异的结果。我遇到过两次这种情况。一次是tokenizer里有个全局字典被多线程同时读写导致有时候分词结果错乱另一次是用了onnxruntime的session多个线程同时run导致延迟飙升。解决方案是按需加锁或者把推理部分放在一个独立的线程池里保证同一时间只有一个线程真正执行模型前向。4.3 服务起来后健康但推理全部报错这类问题往往藏在模型加载环节。模型加载失败时FastAPI依然能启动成功因为Classifier实例化发生在模块顶层如果抛出异常服务进程直接崩溃。但如果我把加载过程放在lifespan事件里加载失败时服务可能看起来是启动成功的只是所有推理请求都会异常。习惯性做法是启动时明确打印模型加载日志包括模型路径、加载耗时、是否使用GPU这些信息在排查问题时非常管用。加上/health接口除了返回ok还可以验证模型是否就绪。4.4 模型上线后发现效果崩了怎么回滚这个问题问的人最多。模型上线后发现线上效果大幅下降怎么快速恢复答案是模型版本和流量切分要做好。我在实践中发现最简单的回滚机制是把当前模型做成一个软链服务启动时读这个软链指向的模型路径。发布新模型时先指向新路径出问题就改回旧路径然后重启服务。这样不用重新构建镜像也不用改代码。更进一步的话可以上真正的A/B测试和多模型路由但对小团队来说先把软链回滚机制做好已经能解决90%的问题了。4.5 AI工程常见问题速查表问题现象可能原因排查方向解决方案训练正常线上效果差数据预处理不一致对比训练/推理中间结果统一预处理代码加测试服务一压测就超时预处理耗时过长用cProfile分析热点优化向量化操作加缓存GPU显存持续增长模型推理未释放显存监控显存曲线确保输入限制定期清理并发请求结果错乱线程安全问题压测时对比单线程结果加锁或独立线程池换机器就报错依赖环境不一致用容器复现Docker打包运行环境日志没有参考价值日志信息不全检查是否覆盖关键路径结构化日志记录请求ID、处理耗时重启后状态丢失模型加载逻辑有误看启动日志是否完整完善初始化逻辑和健康检查5. 下一步扩展这个最小闭环跑通之后AI工程还有很远的路。我自己是按照这四步逐步扩展的第一步加上实验跟踪工具。接入MLflow或Weights Biases把每次实验的参数、指标、产物都记录下来。手工记录总归会漏工具化之后才谈得上真正的高效迭代。第二步把训练流程自动化。用Airflow或Prefect把数据拉取、数据清洗、训练、评估、发布这几个阶段编排起来做成一个可重放的流水线。第三步引入特征存储。当模型开始依赖大量线上特征时单独管理特征逻辑会变得很重要。特征存储的价值在于一处定义训练和推理共用彻底解决训练/推理特征不一致的问题。第四步渐进式交付。金丝雀发布、A/B测试、模型监控告警让每一次模型更新都变得可控、可度量、可回滚。这条路没有终点但走完最小闭环之后你已经具备了一个AI工程师最基本的系统观。在我看来AI工程能力的核心不是会用多少工具而是面对一个需求时脑子里能浮现出完整的交付链路并且知道每个环节的坑在哪里。这需要的是大量实操不是看教程能学来的。最后分享一个小习惯我每次做完一个模型的上线都会写一份简短的复盘文档记录预期的风险、实际遇到的问题、排查过程和最终解法。半年之后翻看这些都是最宝贵的一手素材。AI工程这个领域还在快速演化但那些踩过的坑、沉淀下来的方法论永远值得留档。