
1. 为什么AI工程不是“跑通模型就完事”1.1 算法工程师和AI工程师的分工差异很多人刚开始接触AI工程时都会把注意力放在模型效果上用哪个预训练模型怎么调参精度能到多少。这当然重要但真正让你在真实业务中站稳脚跟的其实是把数据、训练、部署、监控一整套链路跑通的能力也就是常说的 ai-engineering-from-scratch——从零开始构建可用的AI系统。这篇文章不讲大道理只讲我亲身趟过的路从环境搭建到项目落地再到避坑适合那些算法基础尚可、但没正经做过完整AI项目的朋友也适合准备转岗的工程师。先厘清一个常见的误区算法工程师和AI工程师并不是完全等同的称谓。算法工程师的核心任务是“怎么让模型在评测集上变得更好”比如设计网络结构、引入新的loss、做数据增强、调整超参数而AI工程师的核心任务是把“模型效果”变成“用户可用的功能”这意味着你还需要处理数据管道、服务接口、资源调度、异常处理、监控告警。换个说法算法研究像是在实验室里做实验求解一个由评测指标定义的“正确率最大化”问题而AI工程像是在建筑工地上盖楼结构稳固、水电通畅、能经得住风雨这些决定了最终能不能交付。很多从算法转向工程的人第一次踩坑就是把“跑通了notebook”当成“项目完成”。notebook里模型精度80%但一旦要接受外部请求、并发访问、未知输入、突发流量问题全出来了。真正意义上的 from-scratch 训练远不止fit和predict两个方法那么简单。1.2 工程化要解决的三件事数据、模型、服务从零开始做一个AI项目我习惯把整件事拆成三个部分数据层、模型层、服务层。数据层要回答的问题包括数据从哪里来数据格式统一了吗标签质量如何有没有脏数据用户请求实际上拿到的是什么格式和训练时用的格式是否一致很多团队在原型阶段用爬虫抓了数据、简单清洗就开训结果上线后才发现生产环境中有一堆“空白字符串”“特殊字符”“超长文本”没有处理模型直接乱掉了。模型层不仅是“训练一个高精度模型”还包括模型文件怎么保存、版本怎么管理、如何加载、推理时会不会出现显存泄漏、如何对模型做一次快速的验证而不是等到整个训练跑完才发现bug。这些经验如果没有人提醒真的会耗掉大量时间。服务层则更偏向常规后端工程接HTTP请求、校验参数、对输入做预处理、调用模型推理、把输出转成业务需要的格式、设置超时和重试、记录日志。AI工程的难点在于模型有“随机性”同一个输入在模型更新前后可能给出不同结果这会带来业务上的信任和排查问题。所以服务层从一开始就要设计成可观测的而不是“反正模型能返回结果就行”。1.3 从零起步的认知地图如果你完全没有相关经验我可以把从零开始的路线画成一张认知地图方便你确定自己处于哪个阶段第一阶段能运行别人写好的推理脚本会调用训练好的模型文件第二阶段能准备数据集、跑通训练脚本、保存参数并重新加载第三阶段能把训练好的模型封装成接口供前端或后端调用第四阶段能对模型进行监控、评估、定期更新并处理长时间运行中出现的异常。我见过不少人一上来就抱着“微调一个大型语言模型”的目标卡了两个月还没摸到门槛。原因不是模型本身难而是前面四个阶段的基础环节全都略过了。如果老老实实地用一个公开文本分类数据集把第二和第三阶段完整走通再去碰复杂模型效率反而高得多。ai-engineering-from-scratch 这条路没有捷径但也不是走得越“超前”就越快先跑通一条最简单完整的链路比什么都重要。2. 从零搭建AI工程环境选型与血泪教训2.1 Python环境到底该怎么装环境搭建几乎是每个AI起步者最郁闷的早晨刚装好的库突然冲突升级了一下依赖训练代码就崩了。我的建议是不要在系统全局Python里装任何项目依赖。无论你实验多小都请从一开始就用虚拟环境隔离。具体操作上Miniconda和Python自带的venv都可以但我个人更推荐Miniconda。不是因为它有多先进而是因为在Windows、Linux、macOS上的行为一致而且可以优雅地管理不同版本的Python。你不需要很多concepts只需要知道两个命令就够了conda create -n ai-project python3.10 -y conda activate ai-project这个操作的过程相当于给这个项目准备了一间独立的小房间你在里面装什么都不会影响别人。之后安装依赖就用pip注意固定版本而不是装最新版。举个例子我一般会这样写requirementspip install torch2.1.2 transformers4.37.0 scikit-learn1.4.0 fastapi0.110.0 uvicorn[standard]0.29.0踩过一次大坑之后我明白了如果只是写“torch transformers fastapi”过三个月再来跑和当时的环境早已不是同一个世界。2.2 GPU和CUDA版本匹配的坑如果说虚拟环境是第一个坑那么GPU和CUDA就是第二个大坑而且是那种炸起来能让你怀疑人生的坑。很多教程会让你直接执行pip install torch但当你的机器上有老显卡、或者CUDA版本偏旧时下载的变形金刚预编译包可能根本跑不动启动时会报类似 CUDA driver initialization failed 的错。为什么会出现这个问题因为现代深度学习框架通常会预先构建好针对特定CUDA版本的二进制包比如PyTorch的cu118、cu121等。你的驱动版本决定了它能支持的最大CUDA运行时版本但驱动只要不低于某个版本就能向下兼容一部分CUDA工具包。所以正确做法是先运行nvidia-smi查看驱动支持的CUDA版本再根据它选择对应预编译的torch。nvidia-smi # 最上面有一行 CUDA Version: 12.2这表示驱动支持的最高CUDA版本是12.2然后在PyTorch官网选择对应的安装命令比如pip install torch --index-url https://download.pytorch.org/whl/cu121。如果没GPU用CPU版跑小demo也完全没问题不要因为没显卡就卡在第一步代码逻辑才是核心。2.3 版本管理比想象中重要在一个项目存活几个月之后“刚才还能跑现在怎么就崩了”这类问题几乎必然发生。原因千奇百怪某个间接依赖被自动升级了、某个包的新版本改了口径、甚至一个无关的库占用了“torch”的名字。要解决这个问题光靠requirements.txt并不够因为requirements.txt只列出直接依赖不锁定间接依赖的版本。我的建议是项目里同时保留一份精确的锁定文件如果你用Conda可以用conda env export environment.yml如果你用pip则可以用pip freeze requirements-lock.txt。前者更完整但文件很大后者适合恢复精确环境。另外如果你的项目需要跑在同事或服务器上强烈建议从一开始就使用Docker。虽然刚开始多一个概念显得麻烦但Dockerfile会把所有环境配置固化在文件里让任何机器都能复现完全一致的环境。我给一个最简示例FROM pytorch/pytorch:2.1.2-cuda12.1-cudnn8-runtime WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [python, train.py]这样就算换一台机器执行docker build也能得到基本一致的环境省掉无数“我这边可以跑”的扯皮。3. 跑通第一个端到端AI项目以文本分类为例3.1 数据准备没有数据集怎么办理论讲再多不带大家走一遍总会显得空。我先以文本分类为例展示一个完整的端到端流程。为什么不选视觉任务因为文本数据更好理解处理流程更直接而且自然语言处理里很多工程坑同样适用于其他模态。第一步是数据。很多新手卡在“不知道该用什么数据”。公开数据集有很多Hugging Face的datasets库可以直接加载比如IMDB影评情感分类。但为了体验数据工程师的日常我建议你手动下载一个CSV或JSON文件然后自己写清洗逻辑。加载后第一件事不是建模而是看数据的形态和标签分布。用pandas读取import pandas as pd df pd.read_csv(imdb_reviews.csv) print(df.head()) print(df[label].value_counts())你会看到正负样本大概率均衡但真实数据几乎不可能这么干净。我当时处理过一个客服短文本分类任务正负样本比例是1:20还有大量空数据、重复数据、错别字和HTML标签残留。处理思路是去重根据文本内容去重避免模型把重复样本当作有效知识清洗去掉HTML标签、多余空白、特殊符号截断设定最大长度超出部分截断避免单条数据把整个batch撑爆检查标签一致性如果数据里同一个文本有两个不同label需要判断是否保留。这些看起来简单但每一条都有真实事故。比如不去重模型在训练集上效果很好但实际生产遇到稍有不同的文本效果就直线下降因为模型只是在“背答案”。3.2 训练脚本的骨架训练脚本是项目的骨架。很多人喜欢把所有逻辑堆在一个文件里虽然能跑但不利于改动。我给一个非常基础但结构清晰的PyTorch训练脚本骨架你可以先照着“抄作业”跑通后再剥离成模块。import torch from torch.utils.data import DataLoader, Dataset from transformers import AutoTokenizer, AutoModelForSequenceClassification class TextDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len): self.texts texts self.labels labels self.tokenizer tokenizer self.max_len max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): encoding self.tokenizer( self.texts[idx], truncationTrue, max_lengthself.max_len, paddingmax_length, ) return { input_ids: torch.tensor(encoding[input_ids]), attention_mask: torch.tensor(encoding[attention_mask]), labels: torch.tensor(self.labels[idx]), } model AutoModelForSequenceClassification.from_pretrained( bert-base-uncased, num_labels2 ) tokenizer AutoTokenizer.from_pretrained(bert-base-uncased) train_dataset TextDataset(train_texts, train_labels, tokenizer, max_len128) train_loader DataLoader(train_dataset, batch_size16, shuffleTrue) optimizer torch.optim.AdamW(model.parameters(), lr2e-5)在训练循环里有两个细节我特别提醒你。第一固定随机种子。PyTorch、Python、NumPy的种子都要设定否则你会发现每次结果都不一样。固定它们是为了让实验可复现至少能定位“到底是代码变了才导致效果变还是仅因为随机抖动”。import random import numpy as np def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) set_seed(42)第二训练时不要每个epoch都从头打印一次损失就完事我建议每个batch打印一次并且用滑动平均来平滑曲线这样能更早发现梯度爆炸或者学习率不合适。如果loss突然变成NaN大概率是学习率过大或数据里有异常值。训练结束后不要只保存最后一个checkpoint。我习惯保留best.pt和last.pt两个文件best.pt根据验证集指标选择last.pt用于出问题时回溯。保存模型时不要把整个训练器对象直接序列化而是单独保存模型state_dict和optimizer state这样文件更小、更稳定。3.3 模型部署与接口封装训练完成只是第一步真正让ai-engineering-from-scratch落到实处的是把模型变成一个能对外服务的接口。我通常会选FastAPI因为写起来快、自带Swagger文档、性能也过得去。试着写一个最简单的推理服务from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class PredictRequest(BaseModel): text: str class PredictResponse(BaseModel): label: int probability: float model load_model() # 这里需要把训练好的模型加载进来 tokenizer load_tokenizer() app.post(/predict, response_modelPredictResponse) def predict(req: PredictRequest): encoding tokenizer(req.text, truncationTrue, max_length128, return_tensorspt) with torch.no_grad(): logits model(**encoding).logits prob torch.softmax(logits, dim-1)[0, 1].item() return {label: 1 if prob 0.5 else 0, probability: prob}是不是很简单但我在实际项目里很快就发现了几个问题一是模型加载的位置。如果每个请求都重新加载模型显存会不足、推理速度也会非常慢。应该把模型放到全局变量服务启动时加载一次之后每个请求复用。二是batch处理。如果多个请求同时进来一个个推理非常浪费GPU。工程上可以把请求放到一个队列里凑够batch后一起推理。这个听起来复杂但跑通了会给你很大的性能提升。三是超时和并发。真实服务里用户可不会规规矩矩排队等太久。Uvicorn默认是单worker的一个推理卡住整个接口就全堵死。可以先用--workers 2启动多进程但要注意如果模型放在GPU上多个进程共享一个GPU也会导致显存竞争。3.4 评测与效果验证很多新手只看准确率但准确率在类别不平衡场景里非常具有欺骗性。比如正负样本比1:9模型全部预测为负类准确率是90%但一个正样本都没找出来。所以对分类任务至少要看Precision、Recall、F1这组指标。我举一个实际例子当时业务要求“尽可能找出所有异常用户”这意味着Recall必须高哪怕误报一些也可以接受。于是我调整了判定阈值从默认的0.5改成0.3。效果如何异常用户召回率从60%升到了82%但误报率也从前5%升到了前15%。这个博弈不是模型参数调出来的而是和业务方一起敲定的。评测的目的不是给自己打分而是在上线前让所有人知道模型的“行为边界”和“潜在成本”。除了指标还要看badcase。我一般会抽20-30条预测错误的样本逐条人工看。比如模型为什么分错因为措辞模糊、标签本身就有争议、还是遇到了一些训练期间没有覆盖到的表达方式这类分析是提升模型质量的真正来源也是算法和工程结合得最紧密的地方。4. 工程化过程中最容易被忽略的四个问题4.1 可复现性如果你只在本地跑过一次实验可能意识不到可复现性的价值。一旦模型要交给别人维护或者三个月之后回看当时的实验记录你会感谢自己多写了一个配置文件。固定随机种子只是基础。我建议把训练的关键输入hash值也记录下来比如训练数据集的摘要值。这样如果数据被意外改动至少能通过hash检测出来。最简单的方法是生成一个SHA256sha256sum train.csv data_version.txt同时模型文件的命名建议带上训练日期、数据版本、主要超参数比如bert_text_cls_d20240220_v1_lr2e5.pt。虽然文件名长一点但比那个model_final_v2_真的最终版.pt要靠谱得多。4.2 模型漂移上线一段时间后模型效果下滑几乎是必然的。不是你的代码写错了而是现实世界的数据分布一直在变化。比如训练数据里用户评论长度大多在100字以内后来流行起短视频口播评论很多人写到几千字或者出现了新的网络流行语模型根本没见过。这种“训练和推断时数据分布不一致”的情况在机器学习领域专门有个词叫covariate shift。应对策略是监控输入数据的特征分布。我通常会在推理日志里记录每条文本的长度、来源渠道、预测概率。每周跑一个汇总平均值、分位数、类别频次。如果发现某新增渠道的请求占比突然升高就要警惕了。同时设定一个“模型警戒线”比如平均置信度低于0.55说明模型对近期输入越来越拿不准该通知数据团队去更新数据了。还有一种是业务规则变化导致标签漂移。比如客服系统原来把所有“退货”需求算作负面情绪但后来业务调整了退货也被看作正常操作。如果继续用旧标签训练模型模型会一直把退货相关文本预测为负面情绪。所以每次业务规则变化都要同步考虑训练数据的标签是否仍然有效。4.3 日志与监控AI服务出问题时最让人头疼的不是模型效果差而是你根本不知道它为什么差。没有日志就没有任何线索。我给自己的服务定的最低标准是每个请求至少记录三样东西——输入摘要、模型预测结果、耗时。注意不能记录完整的个人隐私数据否则会带来隐私风险文本可以截断到前几十个字符或者只记录长度和关键词分布。一个简单但有效的监控方案是使用结构化日志。Python自带的logging也可以但我更推荐用JSON格式。举个例子{event: predict, input_len: 56, label: 1, prob: 0.87, duration_ms: 12.4, timestamp: 2024-01-20T10:30:00Z}把这些日志收集到Elasticsearch或者直接写入文件再定时扫描至少能让你在出问题时回放当时的场景。更进一步可以在推理耗时突然增加时设置告警因为这可能意味着GPU被占满、请求参数异常或者并发暴涨。没有监控的模型上线等于闭着眼开车早晚要出事。4.4 成本与资源优化很多团队第一版AI服务根本不考虑成本等月底账单出来才意识到GPU不是白送的。成本优化通常可以从几个角度入手。第一是模型结构选择。如果不是必须用大模型可以考虑distilled版本比如BERT常会用distill或tiny版本。我做过一个中文文本分类项目用bert-base效果虽然最好但换成bert-tiny后F1只下降了2%推理速度快了三倍多显存占用少了近四倍。这对在线服务场景来说是非常划算的权衡。第二是推理优化。比较常用的是混合精度推理和量化。用torch.float16代替float32在很多GPU上推理速度会明显提升显存占用直接减半。当然float16偶尔也会改变精度需要在验证集上复测一遍确认指标能接受。第三是batch调度。有些推理框架支持动态batch比如每次来了若干请求后积攒到一定数量再统一推理。这个做法能有效提高GPU利用率但会增加单次请求的等待时间。如果延迟要求不高比如异步任务这是省钱利器。如果业务对延迟敏感则要小心权衡。成本优化永远是在“效果、延迟、费用”三个维度里做取舍没有一种方案能在所有场景下都最优。5. 继续深入的方向与个人建议5.1 学习路线从“能用”到“稳定”再到“高效”如果你已经能完整跑通一个端到端的文本分类项目恭喜你你已经跨过了from-scratch的第一个门槛。但距离一个负责任的AI工程师还有一段路。我建议接下来按这个顺序扩展能力第一学会用实验管理工具比如MLflow或者WB。它们能记录每一次实验的超参数、指标、模型文件让项目管理变得透明。第二个方向是容器化与编排把推理服务打包成Docker镜像再通过Kubernetes部署。这会让服务具备自动扩缩容的能力而不是一台机器跑到崩溃。第三个方向是推理性能优化包括TensorRT、ONNX Runtime、vLLM之类的推理加速框架。我个人的经验是每学一个新工具都把它应用到一个已有项目里而不是单独去刷教程。比如学Docker时就把上一个文本分类服务容器化学Kubernetes时就尝试把它部署到单机的K3s上。这样学到的每个概念都有了实际载体记忆更深刻。5.2 用项目驱动能力提升很多人问“我应该做多少个小项目才够”我见过有人做了十几个差不多同类型的toy project简历上还是显得空。因为小项目只展示了“你会用库”没有体现“你会解决实际问题”。更有价值的做法是选一个你关心的垂直领域深入做下去。比如你是做电商运营的可以做一个评论情感分析自动打标系统你是做运维的可以做一个日志异常检测告警。找到真实场景后你会自然地遇到数据不干净、服务不稳定、效果被人质疑这些问题而这些才是AI工程中最值钱的经验。如果你想参与开源项目可以先从Hugging Face社区的代码库、或者某个模型推理示例仓库的issue开始。试着修复一个bug、补充一段文档、或者加一个测试都能帮你理解优秀工程项目的组织方式。5.3 我的几点体会最后分享几条最实在的体会。第一条永远不要等到模型“足够好”才开始工程化。模型效果60%的时候可以先把接口部署起来真实反馈会让你知道问题到底出在算法还是产品设计。第二条给未来留好调试入口。无论模型还是服务都要有日志、版本号、可重现的环境这些不会直接提升精度但会在出问题的时候救你命。第三条不要盲目追逐大模型。这几年AI领域的热点一轮接一轮但工程化的底层能力从来都是通用的底子打牢了新模型出来时只需要多读几篇文档就能接上。我刚开始做ai-engineering-from-scratch的时候连CUDA版本和显卡驱动都搞不清楚硬着头皮一趟趟踩坑走了不少弯路。如果你也是在从零摸索我希望这篇文章能让你少踩几个我踩过的坑把时间花在真正重要的事情上把模型做成一个能稳定跑、能方便改、能被别人理解和维护的系统。这就是AI工程最朴素也最有挑战性的目标。