如果只说一句,AI engineering from scratch 最难的不是模型,而是“不知道从哪里下手”。我从第一次跑通手写识别的小 demo,到后来在真实业务里上线评论分析接口,中间隔的不只是一行 load_dataset,而是把数据、训练、部署、监控串起来的一整套工程方法。这篇文章不会有“30天精通”的承诺,我会按自己真实走过的路线,把为什么选这些工具、每一步该盯什么、以及那些容易让人整晚睡不着的坑,都摊开讲清楚。适合已经会基础 Python、能跑通教程却还不敢独立搭项目的人,也适合团队里刚接手 AI 工程的新人。
1. AI 工程先想清楚:你缺的不是模型,而是一条能自洽的链路
1.1 从“写出模型”到“跑起系统”,差距到底在哪
我见过不少简历上写着“会训练模型”的人,给他们一个真实需求——比如做一个商品评论情感分析接口,每天处理几万条文本,还要能上线持续运行——立刻就卡住了。卡住的点往往不是不会写 Transformer,而是心里默认 AI 工程 = 模型训练。实际上,模型只是整条流水线中间的一环。
用开餐厅打比方更直观:模型是菜谱,数据是食材,后厨是数据管道,传菜是推理服务,食客反馈是监控指标。菜谱再惊艳,没有洗菜、切菜、配菜、掌勺、上菜这一整套后厨体系,客人是不会满意的。AI 工程从零开始,说白了就是先学会经营这个后厨。
具体到落地上,一个能自洽的链路至少要覆盖四层:
- 数据层:原始数据怎么存、怎么清洗、怎么标注、怎么版本化。
- 实验层:特征怎么造、模型怎么训、指标怎么算、实验怎么记录。
- 服务层:模型怎么部署成接口、并发怎么扛、异常怎么兜底。
- 监控层:线上指标怎么看、数据漂移怎么发现、模型什么时候该重训。
我在最早搭项目时犯过一个典型错误:花大量时间在一个小数据集上调模型,把准确率从 0.94 提到 0.96,非常兴奋。可等我把它放进 API 里才发现,接口在压测下响应要 120ms,用户刷个页面都要等。调那 0.02 的准确率,在真实场景里根本感受不到,反而把整体健壮性忽略了。所以我说,真正符合“从零开始”的工程观不是把模型调到极致,而是先建立起一条能自洽的链路:数据可溯源、训练可重复、结果可对比、服务可回滚。这四条,比单点准确率稀缺得多。
1.2 先把三个容易走窄的念头排掉
误区一,总觉得要从底层数学推导开始。原理当然值得学,但如果目标是要尽早独立交付一个可用的 AI 服务,那“公式推导导向”会让你一直停在准备阶段。更好的节奏是由用到理:先跑通一个很小的任务,跑的过程中遇到瓶颈,再回头补相关数学。工程上的学习是需求驱动的,强行按“先磨刀再砍柴”来,留给你的大概率是刀磨了一年,柴一根没砍。
误区二,什么都想自己造。看到别人用开源框架,你非要自己实现一个推理引擎,这会占用大量本应投资在业务问题上的精力。真要自己动手创造的,应该是业务数据和模型之间那些难以标准化的部分,比如清洗逻辑、评估口径、异常处理。这些才会慢慢变成你真正的工程资产,而不是重复造轮子。
误区三,想一步到位。我见过不少人第一周就设计 K8s 集群、监控大盘、多任务调度,结果模型都还没跑起来,先被运维成本压垮。第一版应该朴素到可以用一条命令从数据跑到 API;当你发现这条链路的某个环节总是要人盯着,再针对它加自动化。架构是长出来的,不是一开始设计出来的。这三点理顺之后,再往下看路线和选型,你会笃定很多。
2. 动手之前,先把路线图和工具链定下来
2.1 四段式路线:从最小闭环到持续迭代
我把“从零开始”落地拆成四个阶段,每个阶段结束都要有一个“能跑的东西”拿来验收,而不是“我学完了某个章节”。
阶段一,打通最小闭环。用一个小型公开数据集,把“读数据 → 训练 → 保存模型 → 加载模型 → 写一个预测函数”完整跑一遍。这段代码可能只有几十行,也可能很粗糙,但闭环一旦形成,后面所有工程复杂度才有承载基础。不要小看这一步,很多人真的没耐心做完“保存模型再 load 回来”这件小事,跳过去之后,后面的选型基本都是空中楼阁。
阶段二,给脚本上工程骨架。把第一阶段的脚本拆成数据、模型、训练、评估、配置五个模块,超参数、数据路径、输出目录全部抽成配置文件。这个阶段开始产生“别人也看得懂”“换数据也能跑”的效果。
阶段三,做推理服务与自动化。用 FastAPI 或类似轻量框架把训练好的模型包成 HTTP 接口,加上基本的请求校验、超时、日志,再用 curl 把“从训练到部署”的整个流程串起来。
阶段四,反馈轮与持续迭代。为线上请求积累日志,定时统计数据分布、计算指标,当指标下降时触发重训。做到这一步,你才算是把 AI 工程自己驱动起来了。
这四段路线最大的特点是每步都可以验收。我后来带人入门,也是要求他们先按这个顺序做,做不完不许聊架构。
2.2 工具链怎么选,我的取舍标准
工具选择不是越火越好,而是要看你处于什么阶段。从零开始、可能只有一个人的项目,核心诉求是“生态好”“可替换”“能快速跑通”。框架之争不值得花太多时间,重要的是每个环节选一个够用的工具,并知道它能干什么、不能干什么。
我给出一个常用的起步选型表,供你参考:
| 环节 | 起步用 | 进阶可以换 | 为什么 |
|---|---|---|---|
| 数据清洗与转换 | pandas / polars | spark / dask | 单机先把逻辑写对,换大数据时要换的不只是工具,还有思维 |
| 模型实验 | scikit-learn + PyTorch | Lightning / Transformers | 起步阶段用原生 API,保留对训练细节的控制力 |
| 推理服务化 | FastAPI + ONNX Runtime | Triton / vLLM | FastAPI 部署简单,ONNX 是避免训练与部署环境不一致的好路径 |
| 实验与版本记录 | MLflow,甚至一个表格 | 自建记录平台 | 第一步其实只需要面向“未来的你”可复现 |
| 环境管理 | conda + requirements.txt | uv / poetry | 先把环境锁定,再谈优雅 |
这套选型不是所有场景都正确,但非常适合同样是从零开始且没有专职运维支持的个人项目。ONNX Runtime 不是必须的,如果你只在 GPU 上训练并直接用 PyTorch 做服务,很多项目也能跑起来。但一旦你想同时提供 CPU 降级、多语言客户端、批次提速,ONNX 的跨平台确定性会让你少掉不少头发。
另一个原则:能外包给框架的,就不要自己重复实现。工具链的意义在于,你在设计系统和排布流程时,知道哪些环节已经有人替你解决,哪些环节只能靠自己。像数据处理管道中的复杂逻辑,别人很难照抄;但模型推理引擎这种成熟领域,值得先信任社区。
2.3 容易被忽略的工程基础设施
路线上除了主线,还有三条容易被忽略的“基础设施”。
环境固定。虚拟环境加锁文件是必须的,训练结果必须能离线重现。依赖版本不一致是零起步团队返工的一大来源:训练机是 CUDA 11.8,部署机是 CUDA 12.x,实验结果可能悄悄就变了。你可以把 Python 版本、关键库版本、框架配置记成一个“环境指纹”,和 checkpoint 放在一起。
数据版本。小项目至少把原始数据按日期存,比如data/raw/20250601_review.csv,不要覆盖同名文件。中大型项目可以引入 DVC 之类工具。数据没有版本,一切实验结论都像沙子建塔,风一吹就散。
实验日志。哪怕只是“日期 + 数据版本 + 参数 JSON + 指标 JSON”四个文件名,也要有。你要具备“三周后还能说清当时为什么这个指标高”的能力。这些基础设施不花钱,花的是习惯。养成习惯后,你会发现那些看起来复杂的 AI 工程,不过是在这堆积木之上叠东西。
3. 从零搭一个能上线的 AI 工程:我用评论文本分类举例
3.1 最小项目结构长什么样
如果从零教一个人搭项目,我不会一上来端出六层架构,而是先给一个最简单的目录:
sentiment_ai/ ├── configs/ │ └── train.yml ├── data/ │ ├── raw/ │ └── processed/ ├── src/ │ ├── data_prep.py │ ├── train.py │ ├── evaluate.py │ └── serve.py ├── models/ │ └── checkpoints/ └── logs/每个目录职责明确:configs把可变参数隔离出来;data/raw坚持原始数据不可污染;src保持扁平源码,方便定位;models只放产物;logs放训练日志和指标曲线。真正的工程化不是目录数量多,而是修改一处不影响另一处。
我建议初次动手的人优先选择文本分类作为演练项目,因为文本不像图片那样需要大量增强和复杂预处理,可以把精力集中在这条工程链路上。以“商品评论情感分类”为例,数据就是两列,一列是label,一列是text,绝大多数模型都能直接吃进去。
3.2 数据预处理顺序,是工程基本功
处理数据时,最容易忽略的一步是“必须先拆分,再进入预处理”。很多人习惯拿到数据先做清洗、特征工程,最后才划分训练集和测试集。这在统计上会让测试集偷看到训练阶段的信息,是典型的评估泄漏。
比如文本向量化时,正确做法是把fit限制在训练集上:
from sklearn.model_selection import train_test_split train_df, test_df = train_test_split(df, test_size=0.2, random_state=42) # 先对训练集拟合,再对 train/test 分别 transform vectorizer.fit(train_df["text"]) X_train = vectorizer.transform(train_df["text"]) X_test = vectorizer.transform(test_df["text"])这段代码看起来极其简单,但很多“从零开始”的人写着写着就会变成vectorizer.fit_transform(df["text"])放在拆分前面。严格来说,这时的测试集已经不是真正的 unseen 数据了。如果后续模型效果高得不正常,先怀疑这里,再去怀疑模型。
3.3 训练脚本里我会坚持的五个检查点
写训练脚本时,架构可以借鉴开源,但下面这五个检查点建议自己盯紧。
第一,随机种子固定。Python、NumPy、PyTorch 要分别固定,同时把num_workers固定下来,否则多进程加载也可能带来不确定因素。种子不固定,同样的代码跑两遍,结果可以不一样。
第二,损失曲线落盘。每 N 步把 loss 和验证指标写入logs/train.log,不要只看最后两个 print。训练过程是否收敛,曲线比单点数字可靠得多。
第三,验证时要用model.eval()。这句话很多人真的会忘。没关 dropout 和 batch norm 的训练行为,会让验证 loss 虚高或虚低,看起来像玄学,其实是状态搞错了。
第四,设定 checkpoint 策略。每个 epoch 可以保存一份,或者至少保存“验证指标最好”和“最新”两份。不然进程崩溃之后,你只能重新开始算力之旅。
第五,早停要和最佳 checkpoint 对应。早停条件触发后,要确保你载入的是验证指标最优的那份参数,而不是最后一个 epoch 的参数。在脚本里这两者很容易错位。
另外一个很务实的建议:如果你还在“抄代码”阶段,就把日志系统和评估函数写得比模型架构更仔细。模型结构可以抄别人的,但日志和评估体现的是你对自己业务判断的把握。
3.4 推理接口:模型变成服务前的最后一道闸
从训练到推理,最考验人的是把模型重新封装成服务这一步。以 FastAPI 为例,核心是拆分“预处理”和“模型推理”两个阶段,并分别打日志:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): text: str @app.post("/predict") def predict(item: Item): # step1: 数据校验与预处理 tokens = preprocess(item.text) # step2: 模型推理 prob = run_model(tokens) return {"label": int(prob > 0.5), "prob": round(float(prob), 4)}这里有两个基本功:把输入为 None、超长文本、空白文本都当作异常分支处理;模型要全局加载一次,而不是每个请求都 load。我踩过最低级的坑,就是最初把模型加载放进每个predict函数里,单机压测只有 1 个 QPS,CPU 先爆了。正确做法是在服务启动阶段加载一次,模型常驻内存或显存,请求只做预处理和推理。
3.5 上线前必须过的五个检查项
模型在测试集上再好看都只是开始,真正上线前我建议过一遍这个表:
| 检查项 | 为什么 |
|---|---|
| 种子和环境指纹已记录 | 如果之后无法复现,线上出问题很难排查 |
| 预处理和训练时完全一致 | 很多线上效果下降,根源是部署时的预处理和训练时不一致 |
| 模型只加载一次且失败可恢复 | 避免启动即 OOM,或者加载失败后无法优雅退出 |
| 用真实请求做一次压测 | 找到内存、CPU、GPU 的真实瓶颈 |
| 有日志和指标系统 | 后续评估、告警、发布决策都依赖它 |
这就引出一个观点:“从零到一”收尾的标志,不是训练出了最好的模型,而是你能让一个简单模型在真实访问下稳定运行,出问题时有日志可查。
4. 从零开始路上,我替你踩过的几个坑
4.1 最隐蔽的元凶:数据泄漏
我印象很深的一次,情感分类项目的测试 AUC 到了 0.97,明显高得不正常。排查到半夜,发现原因是我在做分词过滤时,用整个数据集去构建了一个词表,train 和 test 共用同一个词表,测试集信息已经被模型提前看到了。
这种泄漏对新手最坑的地方在于:它不报错,反而让你的效果展示非常好,让人误以为自己是调参天才。修复之后,指标掉回 0.86,那才是真实水平。
同类陷阱还有:全量归一化、全量缺失值填充、全量目标编码。凡是用到整个数据集信息的预处理,都要在 train/test 拆分之后做。这是一个值得刻在工位上的原则。
4.2 训练时很好,上线后一塌糊涂
这多半不是玄学,通常是三种原因之一。
第一,线上预处理和训练不一致。训练时写了strip().lower(),部署时为了省事少了一步,或者中文分词器版本换了,文本切分结果随之变化,效果直接崩。
第二,推理状态错误。该用model.eval()的时候忘了关 dropout,把随机性带进了线上结果。这类问题很常见,排查方法就是固定一个种子,把你训练时的预处理输出和线上打印出来的特征做对比。
第三,线上输入分布偏移。真用户的输入往往比测试集更脏、更口语,导致特征分布漂移。解决方式不是祈祷,而是从上线第一天就保存推理日志,定期和训练数据分布做对比,再针对差异补样本。
4.3 环境不同,结果对不上
同一段代码,训练机是 GPU,部署机是 CPU,中间还做了 ONNX 转换,精度可能从 99.2% 掉到 98.6%。这不是算子 bug,而是浮点计算顺序不同导致的微小差异。应对方式很明确:转换之后在完整测试集上跑一遍,对比原模型输出,最大误差控制在 1e-4 量级;同时记录 tokenizer 版本,因为分词库升级后词表会变。
很多人以为这是大公司才会遇到的问题,其实个人项目换个云端环境就出现结果不一样,大概率也是环境没有完整复刻。建议把conda env export或 requirements-lock 文件和 checkpoint 放在一起保存。这种习惯一开始不值钱,等你撞过一次墙就知道值钱了。
4.4 没有实验记录,等于白跑
从零开始的项目里,最贵的是算力和调试时间。没有记录,两周后回头看自己调过什么,只剩下一堆 git 碎片。随手记一行“dataset V2 + max_len 256 + lr 3e-5”都是赚的。
我更推荐建一个experiments/目录,每个文件夹里放 config、指标日志和一句备注。三个月后你还能立刻说清为什么当时选中这个方案。这件事的价值在早期看不到,要等开始二次迭代时才爆发——我见过太多人调了一堆参,最后根本不知道哪个组合最有效,只能重新再来一遍,那才是真的浪费。
5. 如果回头,我会把“可复现的可靠基线”放在首位
5.1 用基线奠定整个项目的可比性
在把模型调到最好之前,先用一个标准模型加默认超参数做出一个“不丢人”的基线,并把产物保存好。随后的每次改动,只围绕一个变量变化。
我知道这听起来很无聊,但从零开始的人最容易犯的错,恰恰是在链路还没闭环时,试图同时解决“准确率不高”“推理慢”“接口不好看”三个问题,结果全崩。基线的存在就是用来区分“预期会变”和“回退版本”。没有它,所有实验判断都是自欺。
5.2 完整优先于漂亮:先能跑,再优化
在真实项目里,我看到最多人卡住的原因,是今天在纠结用什么框架、怎么封装才叫工程化。“能不能跑”和“跑得好”根本是两个阶段,应该有时间界线。
我给自己立的规则是:第一阶段最多花三天,三天内必须做到 train、serve、curl 全通。不管模型多普通。因为项目的掌控感来自完整可见,而不是局部精雕。先接受一个粗糙但完整的环节,再让它在一次次迭代里变漂亮。跳过完整闭环谈优化,大概率是空中楼阁。
5.3 我每次从零开始都要回答的五个问题
最后分享一个我坚持了很久的小习惯:项目启动当天,把下面五个问题写在笔记最上面,每完成一个版本就重新答一遍。
- 原始数据存在哪,能不能随时回滚?
- 如果现在关机或中断,这个项目还能继续吗?
- 从一条线上请求到预测返回,中间哪个环节是手写的、最不可靠?
- 这些指标是怎么算出来的,只看日志,未来的我能不能理解?
- 如果效果下降,我第一步会去看哪个日志?
这五个问题帮我挡掉了很多虚无的焦虑。很多新人的问题不是能力不够,而是没有一条清晰、可检验的链路兜底。如果你能把前面这些经验带进第一个项目,那你从零开始的起点,大概率比我当年要高不少。