1. 这个标题到底在说什么
很多人第一次看到"ai-engineering-from-scratch"这个项目名,第一反应是"又要学AI了",然后可能就划走了。但我可以明确告诉你,这个标题真正想表达的,不是让你去啃Transformer源码,也不是让你调个MNIST手写识别就算完事,而是如何在没有任何现成AI基础设施的前提下,一个人从零开始搭起一套能落地的AI工程体系。
从我接触到的实际情况来看,身边至少一半的人卡在了"会用API"和"能做工程"之间那条巨大的鸿沟里。你用OpenAI的接口写个聊天机器人,那不算AI工程;你在Colab里跑通一个扩散模型,也不算AI工程。真正的AI工程,是你把模型、数据、算力、推理服务、评测、监控、成本控制这些东西全部串成一条完整链路,并且在生产环境里能稳定跑起来。
这个项目值得关注的点在于,它锁定的是"from scratch"这个视角。也就是说,不依赖任何公司的内部平台、不假设你有现成的MLOps团队、甚至不默认你有GPU集群。你要解决的是最基础的问题:环境怎么搭、数据怎么管、实验怎么跟踪、模型怎么部署、效果怎么评估、线上怎么观测。这些问题单独看都不难,但把它们组合在一起,并且一个人搞定,就是另一回事了。
如果你正在做AI应用开发,或者你所在的团队连个正经的MLOps流程都没有,又或者你就是想搞清楚一个AI项目从零到上线到底要走多少步,那这篇内容会给你一条清晰得多的路径。我尽可能把每一步为什么要这样做、常见的坑在哪里都说清楚,方便你直接照着做,或者结合自己的场景去改。
2. 项目结构设计与整体思路拆解
2.1 从"能跑的脚本"到"可维护的工程"之间缺了什么
先说我观察到的普遍现象。很多自学AI的人,作品集里躺着一堆notebook,每一个都能跑通,每一个也都止步于"能跑通"。为什么?因为notebook天然不适合做工程。你的数据读取路径是写死的,模型参数是随手调的,训练日志散落在输出单元格里,至于推理接口、评估脚本、监控逻辑,根本不存在。
而完整的AI工程,应该包含这样几层东西:数据层(数据采集、清洗、版本管理)、实验层(环境隔离、参数追踪、结果对比)、服务层(模型加载、推理接口、并发处理)、评估层(离线评测、回归测试、线上指标监控)、运维层(日志、告警、成本监控)。这五层看起来复杂,但你从零开始搭的时候,每一步都有非常成熟的开源工具可以用,难点不在于工具本身,而在于知道什么时候该引入哪个工具。
这个项目名里最关键的一个词是"engineering",它强调的不是算法创新,而是工程化交付能力。一个模型哪怕效果再惊艳,如果推理延迟波动大、并发一高就超时、出错时候没有日志可查,那它在生产环境里就是不可用的。这就像一个厨师能做出米其林级别的菜,但出餐速度忽快忽慢,厨房流程一团糟,餐厅照样开不下去。
2.2 为什么"从零开始"意味着要从最小闭环做起
我做这类项目时,最忌讳的一件事就是一开始就想搭一个终极版的MLOps平台。Kubeflow、MLflow、Airflow、Feast这些工具全家桶一股脑全上,最后光调试这些工具之间的配合就耗掉几周时间,真正的模型工作一点没推进。
所以我的核心思路是:先跑通一个最小闭环,再逐层加厚。最小闭环长这样——你有一个数据集,一个训练脚本,一个最简单的FastAPI推理服务,然后你把服务跑起来,发一个请求,拿到结果。就这么简单。全程你只需要一台能联网的电脑,甚至不需要GPU。
为什么一定要从最小闭环开始?因为AI工程的特殊性在于,它的瓶颈往往不在单个环节,而在环节之间的衔接处。你训练脚本里数据预处理的逻辑,跟推理服务里数据预处理的逻辑是不是一致?训练时长尾样本的分布,跟线上实际请求的分布是不是一致?这些衔接问题不跑通一次全流程是根本暴露不出来的。
跑通最小闭环之后,再逐层扩展:给数据加上版本号,给实验加上参数记录,给推理服务加上并发和超时控制,给评估脚本加上自动化回归。每一步扩展都是独立的,出了问题也容易定位。这种"先通后优"的做法,是我在所有工程实践里验证过无数次的节奏。
3. 环境准备与工具链选型
3.1 硬性基础:Python环境管理到底该怎么搞
不管你用什么框架,Python环境永远是第一道坎。我见过太多人因为环境冲突浪费掉整整一天。这里我直接给你一套我自己用下来最省心的方案。
首先,抛弃直接把依赖装进全局环境的做法。不管你是用conda还是venv,都建议给每一个项目建独立环境。我当前用的组合是pyenv管Python版本 + uv管虚拟环境和依赖。uv是目前解析依赖速度最快的工具,可以平替pip,而且它能生成锁文件,确保你在三个月后重装项目时,依赖版本跟现在完全一致。
具体操作简单说一下。装好pyenv之后,一条命令就能装指定版本的Python:
pyenv install 3.11.8然后在项目目录里初始化虚拟环境并用uv装依赖:
uv venv uv pip install -r requirements.txt这里有个容易被忽略的细节:requirements.txt里面一定要锁死传递依赖的版本,而不是只锁直接依赖。为什么?因为很多底层库的版本号是互相牵制的,比如numpy和torch之间就有严格的版本匹配关系。你不锁传递依赖,今天装出来的环境和三个月后装出来的环境可能完全是两个世界,到时候排查问题你会排查到怀疑人生。
3.2 模型训练的最小硬件方案与框架选择
对于"from scratch"这个定位,我不建议一开始就上多卡分布式训练。单张消费级显卡完全够用。你如果是做NLP,一张24GB显存的卡可以跑大多数开源模型的全参数微调(比如7B级别的模型用LoRA方式,或者13B以上模型用QLoRA)。你如果是做CV,一张卡做分类、检测、分割的数据集实验也绰绰有余。
框架选择上,我推荐直接学PyTorch,不要绕道。为什么?因为现在整个开源生态基本都围绕PyTorch展开,HuggingFace的transformers、diffusers、Peft,以及各种推理框架,全部默认支持PyTorch。你实在遇到底层算子需要优化的时候,PyTorch的社区和海量教程也能帮你更快找到答案。
顺便提一下依赖安装的一个坑。如果你的机器有NVIDIA显卡,装PyTorch的时候千万别直接用默认的PyPI源,它会给你装CPU版本,白白浪费你的显卡。要这样装:
uv pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121cu121代表CUDA 12.1版本,你得先看下自己显卡驱动的CUDA版本,用nvidia-smi查,右上角就有。装错版本也不会报错,但训练速度会慢到让你怀疑人生,而且这种问题特别隐蔽,很多人排查了很久都没想明白为什么自己的GPU利用率是0%。
3.3 数据管理:一开始就想到版本问题
很多人做项目时,数据管理是最被忽视的环节。文件夹里躺着data_final_v2.csv、data_final_v3.csv、data_final_v3_real.csv这种奇葩命名,过了一周自己都分不清哪个是哪个。
这里我不推荐一上来就上DVC这类重量级工具,因为它需要配置远程存储,对个人项目来说有点杀鸡用牛刀。我的建议是养成两个习惯就好:第一,数据文件用日期或版本号命名,比如news_20250401.parquet;第二,在实验记录里写下你用的是哪个版本的数据。做到这两点,你至少规避掉80%由数据混乱引发的问题。
如果你的项目数据量开始变大,超过几个GB,建议把数据从CSV换成Parquet格式。Parquet是列式存储,读写速度比CSV快好几个数量级,而且天然支持压缩,同样是100万行数据,Parquet文件可能只有CSV的三分之一大小。
4. 模型训练的工程化改造
4.1 训练脚本应该拆成哪几个模块
还是那句话,不要写一个几千行的单文件训练脚本。单文件写到后面,改一个参数都像在拆炸弹。我更推荐的目录结构是这样的:
src/ ├── data.py # 数据集加载与预处理 ├── model.py # 模型定义 ├── train.py # 训练主循环 ├── evaluate.py # 评估逻辑 └── config.yaml # 所有超参数这样拆的好处是每个模块的职责单一,改数据逻辑不会影响模型结构,调参也不会动到训练代码。特别是config.yaml,用YAML而不是命令行参数管超参数,最大的优势是可复现。你跑完一个实验,配置文件就是一份完整的记录,里面包含了学习率、批次大小、epoch数、模型参数等所有关键信息。命令行参数太容易漏记了,可能你跑完实验就忘了当时用了什么学习率。
4.2 训练过程怎么盯:损失曲线和日志是关键
训练模型时,如果只看终端打印的loss数值,你会错过很多关键信息。我的习惯是至少盯三样东西:训练损失曲线、验证损失曲线、学习率变化。
损失曲线能告诉你很多信息。训练损失持续下降、验证损失也下降,说明训练正常;训练损失下降但验证损失开始上升,说明过拟合;两个都不降,可能是学习率设置不对,也可能是数据预处理有问题。这些判断必须靠可视化曲线才能快速察觉,只看数字列表你根本看不出来趋势。
可视化工具我用的是TensorBoard或者WandB,个人项目我反而推荐TensorBoard,因为它完全本地运行、不需要注册账号,一条命令就能启动:
tensorboard --logdir=./runs在训练脚本里只需要增加几行代码即可记录:
from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter(log_dir=f"./runs/experiment_v1") for step, batch in enumerate(train_loader): loss = train_one_step(batch) writer.add_scalar("train/loss", loss, step)4.3 第一次全参数微调与LoRA,我推荐先学哪个
如果你要微调大语言模型,我强烈建议第一个项目就跑一遍LoRA低秩微调。为什么?因为全参数微调不仅显存开销大,而且对数据量和调参技巧要求都很高。一个很小的学习率设置失误,就可能导致模型灾难性遗忘,把原本的通用能力毁掉。
LoRA的做法是冻结原始模型的全部参数,只训练额外加入的低秩适配器。这就像在原来的神经网络边上插了几个小插件,训练成本大幅降低,效果很多时候跟全参数微调差不了太多。代码上,用HuggingFace的Peft库,十几行就能搞定:
from peft import LoraConfig, get_peft_model lora_config = LoraConfig( r=8, lora_alpha=16, lora_dropout=0.1, bias="none", task_type="CAUSAL_LM" ) model = get_peft_model(model, lora_config)这里r=8是低秩矩阵的维度,lora_alpha=16是缩放系数,这两个参数的比值大概决定了LoRA在最终模型中的影响权重。我的经验是,r设8起步,效果不够再加到16、32,但r过大会增加过拟合风险,而且推理时还是会有一点额外开销。微调完成后,你保存的模型文件比原始模型小了好几个数量级,一个7B模型的LoRA权重往往才几十MB,这在工程交付上非常友好。
5. 推理服务的构建与部署
5.1 FastAPI是纯推理服务的最佳起点
模型训练完,它还是一个静态的产物,你得把它变成能对外提供服务的东西,才算是真正完成了"工程"这一步。这就像你做了一道菜,放在后厨没人看得到,必须端到餐桌上客人才能吃。
我推荐用FastAPI来做推理服务的骨架,它是目前Python生态里性能最好、开发体验最舒服的Web框架,原生支持异步和自动生成API文档。一个最基础的模型推理服务大概长这样:
from fastapi import FastAPI, HTTPException from pydantic import BaseModel app = FastAPI() class PredictRequest(BaseModel): text: str class PredictResponse(BaseModel): label: str confidence: float @app.post("/predict") async def predict(req: PredictRequest): # 这里调用模型推理 label, confidence = inference(req.text) return PredictResponse(label=label, confidence=confidence)注意一点,模型在服务启动时加载一次,之后所有请求共享同一份模型实例,千万不要在每次请求时重新加载模型,那会直接把服务拖垮。
5.2 推理服务的并发与延迟到底怎么调
很多人写完推理服务,本地测试一两个请求觉得挺快,就以为完事了。结果一上生产环境,并发量稍微上来,延迟直接飙升到不可接受。核心原因是他们没有理解并发模型的差异。
FastAPI本身是异步框架,但如果你的模型推理是同步的,比如PyTorch的model(input)默认就是同步操作,那么请求实际上是排队处理的。异步框架只能帮你避免I/O等待,比如等待数据库查询、等待下游API返回,但它不能让一个死循环或一个GPU推理并行执行。
要让服务真正支持并发推理,你有几条路可以走。最简单的是给服务挂上多个worker进程,用uvicorn启动时可以指定:
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4每个worker进程是独立的,承载着同一个模型的独立副本,这能充分利用多核CPU,或者多块GPU。但要注意,多个worker意味着显存占用翻倍,你得算清楚显存能不能扛得住。另一个思路是引入消息队列,把推理请求先丢进队列,由单独的后台消费者进程去消费并调用GPU推理,这适合推理耗时长、无法靠增加worker解决的场景。
5.3 服务上线前必须要测的几个指标
我这里给你一份我自己项目里一定会跑的测试清单:
- 单请求延迟P99:注意不是平均延迟,P99意味着最差的那1%请求的表现,平均延迟再好看也掩盖不了长尾问题。
- 并发压力测试:用
locust或wrk模拟并发用户,观察TPS峰值、延迟拐点出现在多少并发。 - 错误率:压测时统计5xx和超时请求的比例,系统要在高负载下仍然保持低错误率。
- 显存和内存占用:持续请求一段时间后观察资源占用是否稳定,有没有内存泄漏。
这些测试的核心目的,是找到系统的真实瓶颈。很多时候,瓶颈不在模型本身,而在数据预处理、序列化、网络传输这些外围环节。把这些瓶颈提前测出来,比上线后收到告警再救火要省心得多。
6. 评估体系的搭建:让模型效果"可证明"
6.1 一张静态的测试集,和一套评估流水线是两码事
模型训练完了,随便跑几个测试样本看效果,觉得"看起来还行",这不能叫评估。真正的评估体系,是一套可重复运行的流水线,它能在每一次模型更新、每个候选模型之间,给出可量化的对比结论。
最小可用版本的评估流水线应该包含:一个固定的测试集、一组统一的评测指标、一段自动跑评测的脚本。测试集要尽早从训练集里分出来,而且可以故意往里面放一些典型的困难样本,比如用户最有可能遇到的极端情况。以后每次训练出新模型,只需要跑一遍评测脚本,就能得到一组指标,用来跟历史版本做对比。
我建议把评估脚本固化到项目的根目录,比如命名成eval.py,跟train.py平级。因为评估不是一次性工作,你后续每迭代一版模型,都要跑一遍它。把它当成项目的一等公民来对待,而不是临时写个脚本用完就扔。
6.2 离线指标与线上表现之间为什么会存在鸿沟
很多人会困惑一个问题:离线测试分数看起来很高,为什么一上线上用户的反馈还是不行?这里要理解,离线指标与线上真实表现之间天然存在偏差。
原因也不复杂。离线测试集是从历史数据中抽样出来的,它反映的是过去,而线上请求代表的是未来。用户的需求在不断变化,测试集却是一个固定的快照。另外,离线评测往往只看单一维度指标,比如准确率,但线上用户感知到的是完整的交互体验,包括响应速度、输出格式是否符合预期、极端输入时的容错表现等。
我个人的做法是两套指标并行:离线跑标准分,线上再做抽样人工抽检。抽检时不要只看模型回答得正确与否,还要看答案的可读性、安全性、稳定性,以及面对不同表达方式的同一问题时是否会出现答案矛盾。这些维度很难用自动化指标涵盖,但它们在工程交付中的重要性往往不低于准确率。
7. 运维与调优的实操复盘
7.1 推理日志里该记哪些字段
我在运营AI服务时,踩过最大的一次坑是线上用户反馈服务异常,但日志里什么都查不到,因为当时只打了"请求来了"和"请求处理完"两条log,完全不够定位问题。
后来我把日志字段标准化,形成了一套固定模板:
- 请求ID:每次请求生成唯一ID,方便串联上下游日志
- 模型版本号:当前服务加载的是哪个版本模型
- 输入大小与摘要:比如文本长度、图片分辨率
- 推理耗时:毫秒级
- 返回状态:成功、超时、还是抛异常
- 异常堆栈:出错时的完整追溯信息
这套日志模板的威力在于,它让你有能力做回溯排查。当线上出问题时,你能从一条日志串起整个过程,快速定位是模型推理慢、还是上游数据传输出问题、还是服务被杀导致请求丢失。
7.2 成本监控:token用量是一笔隐形开销
如果你用的是付费API,或者自己托管大模型,成本监控就不能忽略。很多人在开发阶段完全不管token消耗,上线之后看着账单才知道心疼。但实际上成本控制应该从设计阶段就开始。
一个简单的做法:给服务的日志加上prompt_tokens和completion_tokens字段,每天统计一次总消耗和人均消耗。这样你能直观看到,哪些功能接口在吞噬预算,然后决定是优化prompt、减少冗余字数,还是把调用频率降下来。另外,模型输入长度要设置上限,防止用户发超长文本把你的budget一次性烧光。
7.3 上线后性能变慢,最常见的三个原因
服务部署完之后,性能不能一劳永逸。我遇到过几次"昨晚还挺好"但今天突然变慢的情况,排查完基本是下面这几种原因:
第一个原因是模型更新没有做AB对比。新模型可能效果更好,但在推理耗时上却明显增加了,如果你没有做AB测试,用户就会不知不觉地享受到更慢的服务。
第二个原因是并发用户自然增长。服务刚上线时一天只有几百请求,跑得很轻松;上线一个月后日均请求破万,之前压测时掩盖的瓶颈就会集中爆发。
第三个原因是日志和监控拖慢了主流程。日志写文件、指标上报这些操作如果在主线程里同步执行,在高并发下会显著增加请求延迟。解决方法是把这些旁路操作改成异步,或者单独丢到后台线程处理。
8. 新人最容易掉进去的十个坑
一路看下来,你会发现AI工程核心其实就这几件事:环境、数据、训练、部署、评估、监控。但每一项都有新手容易踩的坑,我这里整理一份速查表给你,可以收藏着慢慢看。
| 问题领域 | 典型问题 | 我的建议 |
|---|---|---|
| 环境搭建 | 依赖版本冲突导致无法复现 | 用uv锁文件,锁定全部依赖版本 |
| 数据管理 | 训练/评估数据混用 | 训练集、验证集、测试集严格分离 |
| 数据预处理 | 训练与推理时的预处理逻辑不一致 | 把预处理逻辑封装成同一个函数 |
| 训练监控 | 只盯着loss数值不看曲线 | 用TensorBoard/WandB可视化训练过程 |
| 模型保存 | 只保存最终模型不保存配置文件 | 配置文件与模型权重同时存档 |
| 推理服务 | 每个请求都重新加载模型 | 服务启动时一次性加载模型到内存 |
| 并发处理 | 没压测就上线 | 用locust做至少100并发持续测试 |
| 评估体系 | 只看离线指标不看线上反馈 | 离线指标+抽样人工质检双轨并行 |
| 日志规范 | 日志信息不足无法排查 | 固定请求ID、模型版本、耗时等字段 |
| 成本控制 | 上线后才发现token消耗巨大 | 日志记录token用量,设立成本告警 |
9. 实际动手路线:一周搭建一个完整的AI服务
如果你看完前面这些内容,心里大概有了概念,但不知道从哪里开始。我直接给你一个七天路线图,照着走一遍,你就拥有一个端到端的AI工程样板。
第1天:搭环境,装好Python、PyTorch、FastAPI,跑通一个最简单的模型训练demo,确保GPU可用。
第2天:准备一份小型数据集并划分好训练/测试集,写数据加载和预处理代码,做一次完整训练,记录损失曲线。
第3天:学习LoRA微调,在一个开源小模型上完成微调,保存LoRA权重和配置文件。
第4天:用FastAPI搭建推理服务,加载微调后的模型,写好可预测的API接口,本地联调通过。
第5天:做压测,用并发工具模拟至少100路请求,记录延迟分布和错误率。
第6天:搭建自动化评估脚本,把测试集过一遍,输出量化指标。
第7天:完善日志、添加基本监控告警,写下项目README和部署文档,总结踩过的坑和解决方案。
七天走下来,你获得的不是一个玩具项目,而是一套完整的、可扩展的AI服务工程框架。之后的迭代,都是在这个骨架上增减模块而已。
我个人在实际操作中的体会是,AI工程最大的门槛从来不是某个具体的技术点,而是"全局视角"——你要能在头脑里同时装着链路中的每一个环节,并且理解它们之间的相互影响。这需要时间和实践积累,但一旦你完整地从头到尾做过一次,这套能力就会非常牢固地长在你身上。