ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零构建AI工程能力:数据管道、模型训练与推理服务化实战

从零构建AI工程能力:数据管道、模型训练与推理服务化实战 1. 从零搭建AI工程能力为什么大多数人卡在第一步就放弃了很多人对AI工程这四个字的理解还停留在调个API、写个提示词的阶段。我刚开始接触这个方向的时候也是这么想的——直到真正动手做一个完整项目才发现从数据准备到模型部署中间隔着的不是一两个知识点而是一整套工程化思维的鸿沟。ai-engineering-from-scratch这个标题本身就说明了一件事它面向的是那些想从底层理解AI工程全貌、而不是只会调包的人。你可能是刚转方向的开发者也可能是做了几年后端想往AI方向靠的工程师还可能是学生党想系统补齐工程能力。不管哪种身份核心诉求是一样的——我需要一条清晰的路径告诉我从零到能跑通一个完整AI项目到底要经历哪些环节、每个环节的关键决策是什么。这篇文章不会给你画一张学习路线图就完事。我想做的是把AI工程拆成几个真实的工程阶段每个阶段告诉你这一步在干什么、为什么需要它、常见的坑在哪里、以及我自己踩过之后总结出来的做法。全文围绕一个核心逻辑展开——AI工程的本质不是算法有多深而是你能不能把数据、模型、服务这三条线串成一条稳定可复现的流水线。先说一个反直觉的结论大部分AI项目失败的原因不是模型效果不好而是工程链路太脆弱。数据格式一变就崩、模型版本对不上、推理服务上线后延迟飙升——这些问题跟算法无关纯粹是工程能力的问题。所以从零构建AI工程能力第一步不是去学Transformer架构而是建立端到端可运行的意识。2. 数据管道的搭建AI工程真正的地基2.1 为什么数据管道比模型更值得花时间我见过太多人一上来就研究模型结构、损失函数、注意力机制结果数据加载那一步就写得乱七八糟。训练脚本里硬编码路径、数据预处理逻辑和训练逻辑混在一个文件里、换一个数据集就要改十几处代码——这些都是典型的研究型代码跑一次实验可以但作为工程项目完全不合格。数据管道在AI工程里的地位相当于地基之于房子。你后面模型换多少次、超参调多少轮数据管道是那个从头到尾都在跑的东西。它如果设计得不好你每次实验都要花大量时间在数据准备上实验迭代速度直接砍半。一个合格的数据管道应该做到三件事输入输出接口固定、预处理逻辑可配置、数据版本可追溯。听起来简单但实际做的时候很多人会忽略第三条。举个例子你今天用了一份清洗过的数据训练了一个模型效果不错两周后数据团队更新了原始数据你重新跑了一遍训练发现效果掉了——这时候如果你没有记录当时用的数据版本根本无从排查。2.2 从原始数据到训练样本的标准化流程我自己的做法是把数据管道拆成四层每层职责单一采集层负责从各种来源数据库、文件、接口把原始数据拉下来统一存成一种中间格式比如JSONL或Parquet。这一层不做任何清洗只做格式统一。清洗层处理缺失值、去重、格式校验、异常值过滤。这一层的输出是干净但未加工的数据。加工层做特征工程、分词、序列化、数据增强。这一层的输出直接是模型可以吃的格式。切分层划分训练集、验证集、测试集并且固定随机种子保证每次切分结果一致。为什么要分这么细因为每一层的变更频率不一样。采集层可能一周变一次清洗规则可能一个月调一次加工层可能每次实验都在变。分层之后你改加工层不会影响清洗层的稳定性排查问题也能快速定位是哪一层出了状况。具体到代码组织上我习惯给每一层定义一个抽象基类然后用配置文件驱动具体实现。比如清洗层可以定义一个BaseCleaner里面有clean()方法具体的DedupCleaner、NullFilterCleaner继承它。配置文件里写清楚用哪些cleaner、按什么顺序执行。这样换数据集的时候只需要改配置不用动代码。注意数据管道的每一层输出都应该落地成文件不要只在内存里传递。落地文件的好处是你可以随时检查中间结果出问题的时候能快速定位是哪一层的输出不对。我一般用Parquet格式存中间结果读取快、体积小、schema清晰。2.3 数据版本管理一个容易被忽视但极其关键的环节数据版本管理这件事很多人觉得我用git管理代码就够了但数据文件通常很大放git里不现实。我的做法是用一个简单的清单文件来记录每次数据变更# data_manifest.yaml version: 20240115 raw_data: s3://bucket/raw/20240115/ clean_data: s3://bucket/clean/20240115/ stats: total_samples: 1250000 after_dedup: 1180000 after_filter: 1150000 train: 1035000 val: 57500 test: 57500每次数据管道跑完自动生成这样一份清单。训练的时候模型配置文件里引用这个版本号。这样任何时候你都能追溯到某个模型用的是哪份数据。如果团队用MLflow或者类似的实验管理工具这份清单可以直接作为run的artifact存进去。这个做法看起来笨但实际用起来非常稳。我经历过一次线上模型效果突然下降的事故最后就是靠这份清单发现是数据团队更新了原始数据但没通知我们导致清洗规则失效、混入了大量脏数据。如果没有版本记录这个排查可能要花好几天。3. 模型训练工程化从能跑到可复现的跨越3.1 训练脚本的组织方式决定了你的迭代速度新手写训练脚本通常是一个train.py从头写到尾加载数据、定义模型、写训练循环、保存模型全在一个文件里。这种写法在第一次跑通的时候没问题但当你需要做以下任何一件事的时候就会非常痛苦换一个模型结构做对比实验调整数据预处理方式在不同的数据集上跑同一套训练逻辑复现一个月前某次实验的结果我的做法是把训练脚本拆成三个独立的部分配置、组件、流程。配置用YAML或dataclass来管理所有可变的东西——学习率、batch size、模型类型、数据路径、训练轮数——全部放在配置里。组件包括模型、数据集、优化器、学习率调度器每个组件都定义成可替换的类。流程就是训练循环本身它只依赖配置和组件接口不关心具体实现。这样组织之后做一个新实验只需要写一份新配置指定用哪个模型、哪份数据、什么超参然后启动训练就行。代码一行不用改。# 配置示例 dataclass class TrainConfig: model_name: str transformer_base data_version: str 20240115 batch_size: int 32 lr: float 1e-4 epochs: int 10 seed: int 42 output_dir: str experiments/exp_0013.2 随机种子与确定性可复现不是可选项为什么我两次跑出来的结果不一样——这是AI工程里最常见的问题之一。原因通常有三个随机种子没固定、GPU非确定性操作、数据加载顺序不稳定。固定随机种子要做的不只是torch.manual_seed(42)这一句。你需要固定Python内置的random、numpy的random、深度学习框架的random如果用了GPU还要设置cuDNN的确定性模式。数据加载器的worker也要设置seed否则多进程加载时顺序会变。import random import numpy as np import torch def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False提示开启cudnn.deterministic True会牺牲一点性能但换来的是可复现性。在实验阶段建议开启等模型定稿要上生产的时候再关掉换性能。但即使做了这些有些操作仍然是非确定性的比如某些GPU上的原子操作。所以我的经验是不要追求100%的bit-level复现追求的是统计意义上的可复现——同样的配置跑两次最终指标的差异在可接受范围内比如0.1%以内这就够了。如果差异很大那说明某个环节有隐藏的随机性没控制住。3.3 实验追踪别让自己三个月后看不懂自己的实验我刚开始做实验的时候用Excel记录每次跑的配置和结果。跑了大概五十组实验之后Excel彻底失控了——列太多、版本太多、有些实验跑到一半中断了不知道结果算不算数。后来换成结构化的实验追踪效率提升非常明显。实验追踪的核心是记录三样东西配置、指标、产物。配置就是你这次实验用的所有参数指标是训练过程中的loss、accuracy等产物是最终保存的模型文件、日志、图表。工具选择上轻量级可以用TensorBoard加一个配置文件快照重量级可以用MLflow或Weights Biases。我的建议是如果你是一个人做实验TensorBoard加本地文件管理就够了如果是团队协作直接上MLflow它的模型注册和版本管理功能在后期会省很多事。不管用什么工具有一个原则必须遵守每次实验的配置必须自动保存不能靠手动记录。我见过太多人手动记配置结果记错了一个参数导致后面复现不出来白白浪费几天时间。4. 推理服务化模型从实验室到线上的最后一公里4.1 推理服务和训练脚本的本质区别训练脚本可以慢、可以占内存、可以跑完就退出。推理服务不行——它要7x24小时运行、要控制延迟、要处理并发请求、要在出错时优雅降级。这两个场景对代码的要求完全不同。我见过很多团队直接把训练脚本里的模型定义复制到服务代码里结果上线后问题不断模型加载慢导致服务启动超时、单次推理占用内存太大导致OOM、没有批处理导致GPU利用率极低。正确的做法是把模型推理部分单独抽出来定义一个干净的接口class Predictor: def __init__(self, model_path: str, device: str cuda): self.model self._load_model(model_path) self.model.eval() self.device device def _load_model(self, path): # 模型加载逻辑 pass torch.no_grad() def predict(self, inputs: list) - list: # 预处理 - 推理 - 后处理 pass这个Predictor类不依赖任何训练相关的代码只负责加载模型和执行推理。服务层调用它的时候不需要知道模型是什么结构、用什么框架训练的。4.2 批处理与动态batching提升吞吐量的关键手段单条推理是最简单的但也是最浪费的。GPU的算力是按批处理的一次处理32条和一次处理1条耗时可能差不多。所以推理服务必须支持batching。最简单的做法是固定batch size攒够一批就推理。但实际请求量不均匀有时候请求多有时候请求少固定batch会导致延迟不稳定。更好的做法是动态batching设置一个最大batch size和一个最大等待时间哪个先到就触发推理。class DynamicBatcher: def __init__(self, max_batch_size32, max_wait_ms50): self.max_batch_size max_batch_size self.max_wait_ms max_wait_ms self.queue [] def add_request(self, request): self.queue.append(request) if len(self.queue) self.max_batch_size: return self._flush() # 否则等待超时或下一批 def _flush(self): batch self.queue[:self.max_batch_size] self.queue self.queue[self.max_batch_size:] return batch这个逻辑看起来简单但实际实现的时候要考虑并发安全、超时处理、异常请求的隔离。我建议如果团队没有专门的推理优化工程师直接用现成的推理框架比如Triton或TorchServe它们内置了动态batching配置一下就能用。4.3 服务监控上线只是开始不是结束推理服务上线之后你需要持续关注几个指标延迟P50/P95/P99、吞吐量QPS、错误率、GPU利用率、内存占用。这些指标任何一个异常都可能导致线上问题。延迟的P99比平均值重要得多。平均值看起来可能只有50ms但P99可能是500ms——意味着1%的请求用户体验很差。如果这是一个面向用户的产品这1%的用户可能会直接流失。我的做法是在服务里埋点每次推理都记录耗时然后定期上报到监控系统。同时设置告警阈值P99超过200ms告警、错误率超过1%告警、GPU利用率持续低于20%告警说明资源浪费。注意监控不仅要看服务本身的指标还要看模型输出的分布。如果输入数据的分布发生了变化比如用户行为变了模型的效果可能会下降但服务层面的指标完全正常。这种静默失效是最危险的需要额外做输入数据的统计监控。5. 那些让我踩过坑的工程细节5.1 配置文件管理别让配置成为事故源头配置文件看起来简单但它是很多事故的根源。我经历过一次因为配置文件里路径写错导致服务加载了旧版本的模型线上效果直接回退到三个月前的水平。还有一次是配置里的batch size设得太大服务一启动就OOM。我的经验是配置文件必须做schema校验。用Pydantic或类似工具定义配置的结构和类型加载的时候自动校验。如果配置里少了必填项、或者类型不对启动时直接报错而不是等到运行时才出问题。from pydantic import BaseModel, validator class ServiceConfig(BaseModel): model_path: str batch_size: int max_wait_ms: int device: str cuda validator(batch_size) def batch_size_must_be_positive(cls, v): if v 0: raise ValueError(batch_size must be positive) return v另外配置文件不要硬编码在代码里也不要用环境变量散落各处。统一放在一个目录下用环境名区分dev/staging/prod启动时通过参数指定用哪份配置。5.2 模型版本管理比代码版本管理更复杂代码可以用git管理模型文件通常很大放git不现实。但模型版本管理又极其重要——你需要知道线上跑的是哪个模型、这个模型是用哪份数据训练的、对应的代码是什么版本。我的做法是用一个模型注册表来管理。每次训练产出一个模型就注册一条记录字段说明model_id唯一标识比如mdl_20240115_001model_path模型文件存储路径data_version训练数据版本code_commit训练代码的git commitmetrics验证集上的指标statusstaging/production/archived线上服务加载模型的时候不直接指定文件路径而是指定model_id。这样切换模型只需要改一个ID而且随时可以回滚到之前的版本。5.3 日志与可观测性出问题时能快速定位AI服务的日志比普通后端服务更重要因为出问题的时候你不仅要看服务本身的日志还要看模型输入输出的日志。我的做法是分三层记录访问日志记录每次请求的元信息时间、耗时、状态码用于监控和告警。推理日志记录每次推理的输入摘要和输出摘要用于排查模型行为异常。调试日志记录详细的中间结果默认关闭需要时动态开启。推理日志要注意脱敏和采样。全量记录输入输出会占用大量存储而且可能包含敏感信息。我一般只记录输入的统计特征长度、分布和输出的置信度不记录原始内容。6. 从零构建AI工程能力的实操路径6.1 第一个月把一条最小链路跑通不要一上来就追求完美架构。第一个月的目标很简单用一份真实数据训练一个简单模型部署成一个能调用的服务。模型效果不重要重要的是整条链路是通的。具体来说你可以选一个经典任务比如文本分类或图像分类。数据用公开数据集模型用预训练模型微调服务用FastAPI包一层。这个阶段你会遇到很多工程问题环境依赖、数据格式、模型保存加载、服务启动——每一个问题解决掉你对AI工程的理解就深一层。我建议这个阶段不要用太高级的框架尽量用基础工具手动搭一遍。用PyTorch写训练循环、用Flask或FastAPI写服务、用Docker打包。手动搭一遍之后你再用高级框架就会知道它在帮你做什么。6.2 第二到三个月把链路做稳链路跑通之后下一步是让它稳定。这个阶段的重点是配置管理、实验追踪、数据版本管理、服务监控。把这四件事做好你的AI工程能力就从能跑升级到可靠了。具体动作包括把训练脚本重构成配置驱动、接入实验追踪工具、给数据管道加版本记录、给推理服务加监控指标。这些事情做起来不复杂但需要耐心和细致。我自己的经验是这个阶段花的时间越多后面做新项目的时候就越轻松。6.3 三个月之后优化与扩展当你的链路稳定运行了一段时间积累了真实的使用数据就可以开始做优化了。优化的方向包括推理加速量化、蒸馏、编译优化、成本优化GPU利用率提升、自动扩缩容、效果优化数据增强、模型迭代、在线学习。这个阶段没有标准答案取决于你的具体场景和约束。但有一个原则是不变的每次优化都要有数据支撑。不要凭感觉说这个优化应该有用而是先做baseline、再做对比实验、用数据说话。7. 一些个人体会做AI工程这几年我最大的感受是工程能力比算法能力更稀缺。算法方面开源社区有大量的论文、代码、预训练模型可以用但工程方面每个团队的场景不一样、数据不一样、约束不一样很难直接抄作业。ai-engineering-from-scratch这个方向的核心价值不是让你成为算法专家而是让你具备把AI能力落地成产品的工程素养。这种素养包括知道数据管道怎么设计、知道训练怎么组织、知道服务怎么部署、知道问题怎么排查。这些东西听起来不酷但它们是AI项目能不能成功的关键。如果你正在从零构建这方面的能力我的建议是动手做别只看。看一百篇教程不如自己跑通一个项目。遇到问题的时候先自己想、再查资料、最后问人。每一个你独立解决的问题都会变成你真正的能力。最后分享一个我常用的检查清单每次启动一个新AI项目的时候都会过一遍数据从哪来、怎么清洗、怎么版本化训练怎么配置、怎么复现、怎么追踪模型怎么保存、怎么加载、怎么切换版本服务怎么部署、怎么监控、怎么扩容出问题的时候怎么快速定位是哪一层的问题这五个问题如果能清晰回答这个项目的工程基础就是扎实的。
返回列表