1. 从零搭建AI工程能力:为什么我劝你别再“调包”了
这两年带过不少新人,也帮朋友面过几十场AI方向的岗位,一个特别明显的感受是:简历上写着“熟悉深度学习”“做过大模型应用”的人越来越多,但真正能把一个模型从数据到上线跑通的人,少得可怜。大部分人所谓的“做过”,其实就是在Jupyter Notebook里import torch,然后调一个预训练模型,跑个demo,截图发朋友圈。一旦问到“数据怎么清洗的”“推理延迟怎么优化的”“显存不够怎么办”,基本就卡壳了。
ai-engineering-from-scratch这个标题,我第一次看到的时候就觉得特别对味。它说的不是“AI从零入门”,也不是“大模型原理速成”,而是AI工程——工程这两个字才是重点。算法和模型是研究员的事,但把模型变成能稳定跑在服务器上、能扛住并发、能持续迭代的东西,是工程师的事。这两者之间的鸿沟,比很多人想象的要大得多。
我自己是从传统后端转过来的,踩过的坑可以说能写一本书。最开始我也觉得,AI嘛,不就是调个库的事?后来才发现,数据管道的设计、训练脚本的工程化、模型服务的部署、监控和回滚,每一个环节都有大量的细节,而且这些细节在论文里根本不会讲,在教程里也往往一笔带过。所以这篇内容,我想从一个一线从业者的角度,把“从零搭建AI工程能力”这件事拆开来讲,不讲虚的,只讲我实际做过、踩过、验证过的东西。
这篇文章适合谁看?如果你是刚入行的算法工程师,想补上工程这块短板;如果你是后端或运维,想转AI方向但不知道从哪下手;或者你是个独立开发者,想自己做一个AI产品但被各种工程问题卡住——那这篇内容应该能帮到你。我会从整体思路、核心环节、实操步骤到常见坑,尽量讲透。
2. 整体设计思路:AI工程到底在工程什么
2.1 先搞清楚AI工程和算法研究的边界
很多人一上来就想学Transformer架构、想手推反向传播,我觉得方向就偏了。不是说这些不重要,而是对于“工程”这个定位来说,优先级排错了。AI工程的核心任务,是让一个已经存在的模型(不管是自己训的还是开源的)能够在生产环境中稳定、高效、可维护地运行。它关心的问题是这样的:
- 数据从哪来,怎么保证质量,怎么版本化?
- 训练任务怎么调度,怎么复现,怎么管理实验?
- 模型怎么打包,怎么部署,怎么做到低延迟高吞吐?
- 线上出问题了怎么排查,怎么回滚,怎么持续迭代?
这些问题,没有一个能靠调包解决。它们需要的是扎实的软件工程能力:代码规范、版本控制、CI/CD、容器化、监控告警、日志系统。你可以不会推导注意力机制的公式,但你必须会写可维护的代码、会设计合理的接口、会排查线上问题。
我见过太多算法很强的同学,模型效果做得很好,但代码一团糟,训练脚本里硬编码路径,实验参数靠手改,模型文件命名靠日期。这种工作方式在实验室里可能还能凑合,一旦进入团队协作或者生产环境,就是灾难。所以ai-engineering-from-scratch的第一层含义,我认为是先把工程基础打好,再谈AI。
2.2 技术选型的核心逻辑:稳定优先,渐进式引入
在技术选型上,我的原则一直是:能用成熟方案就不用新方案,能简单实现就不搞复杂架构。这不是保守,而是被坑出来的经验。AI领域新技术层出不穷,每周都有新的框架、新的工具,但生产环境最怕的就是不稳定。
举个例子,模型服务这块,很多人一上来就想用Kubernetes加各种微服务框架,觉得这样才“专业”。但实际上,如果你的QPS只有几十,一个FastAPI加Gunicorn的简单服务完全够用,部署和维护成本低得多。等到真的扛不住了,再考虑上K8s也不迟。过早优化是万恶之源,这句话在AI工程里同样适用。
再比如实验管理,有人喜欢用各种花哨的平台,但我觉得最实用的还是最朴素的方案:Git管理代码,配置文件管理参数,MLflow或TensorBoard记录指标。这套组合简单、可靠、可迁移,不会因为某个平台倒闭或者改版就抓瞎。
我的建议是:在项目初期,把精力放在数据管道和代码规范上,这两块是地基。模型服务可以先跑通最小闭环,后面再逐步优化。不要一上来就追求“架构先进”,能跑通、能维护、能迭代,比什么都重要。
2.3 从零搭建的四个阶段
我把AI工程能力的搭建分成四个阶段,每个阶段有明确的目标和产出:
| 阶段 | 核心目标 | 关键产出 | 常见误区 |
|---|---|---|---|
| 第一阶段:基础工程 | 代码可维护、环境可复现 | 规范的代码仓库、依赖管理、Docker环境 | 忽视代码规范,环境靠手动配置 |
| 第二阶段:数据管道 | 数据可获取、可清洗、可版本化 | 自动化数据脚本、数据版本管理 | 数据靠手动下载,清洗逻辑散落各处 |
| 第三阶段:训练工程化 | 实验可复现、可对比、可调度 | 配置化训练脚本、实验记录系统 | 参数硬编码,实验结果靠记忆 |
| 第四阶段:部署与运维 | 服务稳定、可监控、可回滚 | 模型服务、监控告警、回滚机制 | 只关注上线,不关注线上表现 |
这四个阶段不是严格线性的,实际工作中会有交叉,但整体上应该按这个顺序来。跳过基础直接搞部署,后面一定会返工。
3. 核心环节拆解:每个阶段到底怎么做
3.1 基础工程:把代码和环境管起来
基础工程这块,说起来都是老生常谈,但真正做到位的人不多。我列几个必须做到的点:
代码规范。Python项目至少要用black做格式化,isort做import排序,flake8或ruff做静态检查。这些工具配置一次,后面提交代码前自动跑一遍,能省掉大量review时间。我见过太多项目,光是代码风格就吵得不可开交,最后谁也没说服谁。工具能解决的问题,不要用人来解决。
依赖管理。requirements.txt是最低要求,但更好的做法是用poetry或pipenv,能锁定版本、管理虚拟环境。AI项目特别容易遇到依赖冲突,比如torch和transformers的版本兼容问题,锁版本能避免很多麻烦。我的习惯是,每个项目单独一个虚拟环境,依赖文件提交到Git,别人clone下来一条命令就能装好。
Docker化。不要觉得Docker是运维的事,AI工程师也必须会。一个Dockerfile写清楚基础镜像、依赖安装、代码拷贝、启动命令,别人拿到就能跑,不用问“你环境怎么配的”。我一般会用多阶段构建,把训练环境和推理环境分开,推理镜像尽量小,减少部署时的传输和启动时间。
# 训练环境示例 FROM pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["python", "train.py"]Git工作流。至少要有main和dev两个分支,功能开发在feature分支上,通过PR合并。commit message写清楚做了什么,不要写“update”或者“fix bug”这种没信息量的。我自己的习惯是,每个commit只做一件事,方便回滚和排查。
3.2 数据管道:AI工程里最容易被低估的环节
数据这块,我的观点很明确:AI项目80%的问题出在数据上,但80%的精力被花在了模型上。这是极其不合理的。数据管道没做好,后面训练再花哨也是白搭。
数据管道要解决几个核心问题:
数据获取。数据从哪来?是数据库、文件、API还是爬虫?获取频率是多少?增量还是全量?这些都要想清楚。我一般会写一个独立的data_loader模块,把数据获取逻辑封装起来,上层训练代码不关心数据从哪来,只关心拿到的是干净的数据。
数据清洗。这是最耗时间也最考验经验的环节。文本数据要去重、去噪、处理编码问题;图像数据要检查损坏、统一尺寸、处理标注错误。我的经验是,清洗逻辑一定要写成可复用的函数,并且加上单元测试。不要在一个Notebook里手动清洗,那样没法复现,也没法维护。
数据版本化。数据变了,模型效果可能就变了。所以数据也要像代码一样版本化。小数据集可以直接用Git LFS管理,大数据集可以用DVC或者自己搭一个简单的版本管理系统。关键是要能回答“这个模型是用哪版数据训的”这个问题。
实操心得:数据清洗脚本一定要加日志,记录每一步处理了多少条、过滤了多少条、剩余多少条。这样出问题的时候能快速定位是哪一步出了岔子。我吃过亏,一个去重逻辑写错了,把正常数据也过滤了,结果模型效果怎么调都上不去,查了两天才发现是数据的问题。
3.3 训练工程化:让实验可复现、可对比
训练脚本的工程化,核心就一个词:配置化。不要把参数硬编码在代码里,不要靠改代码来调参。我推荐用Hydra或者简单的YAML配置文件,把所有超参数、路径、模型结构都放在配置文件里,代码只负责读取配置并执行。
这样做的好处是:
- 实验可复现:配置文件一存,随时能重现当时的实验
- 实验可对比:不同配置跑出来的结果,能清晰对比
- 代码更干净:训练逻辑和参数解耦,代码更易读易维护
# config/train.yaml model: name: bert-base-chinese num_labels: 10 dropout: 0.1 training: batch_size: 32 learning_rate: 2e-5 epochs: 5 warmup_ratio: 0.1 data: train_path: data/train.csv val_path: data/val.csv max_length: 128实验记录这块,MLflow是我用得最顺手的。每次训练自动记录参数、指标、模型文件,还能在UI上对比不同实验。如果不想引入额外依赖,TensorBoard加一个简单的CSV日志也能凑合,但MLflow的体验确实好很多。
还有一个容易被忽视的点:随机种子。AI训练有大量随机性,数据打乱、参数初始化、dropout都会引入随机。如果不固定种子,同样的配置跑两次结果可能差很多,根本没法对比。所以训练脚本开头一定要固定所有随机种子,包括Python、NumPy、PyTorch的。
3.4 部署与运维:上线只是开始
模型部署这块,我的建议是从简到繁。最开始用一个FastAPI服务把模型包起来,提供HTTP接口,就足够了。不要一上来就搞模型服务器、搞微服务、搞K8s,那些是规模上来之后才需要考虑的。
一个最简单的模型服务大概长这样:
from fastapi import FastAPI from pydantic import BaseModel import torch app = FastAPI() model = torch.load("model.pt", map_location="cpu") model.eval() class Request(BaseModel): text: str @app.post("/predict") def predict(req: Request): with torch.no_grad(): inputs = tokenizer(req.text, return_tensors="pt", truncation=True, max_length=128) outputs = model(**inputs) pred = outputs.logits.argmax(dim=-1).item() return {"label": pred}启动命令用gunicorn加uvicorn worker,能支持一定的并发。如果QPS再高,可以考虑用ONNX Runtime或者TensorRT做推理加速,或者上Triton Inference Server。但这些都是后话,先把最小闭环跑通。
监控这块,至少要记录每个请求的延迟、输入输出的长度分布、错误率。这些指标能帮你发现很多问题,比如某个输入特别长导致延迟飙升,或者某类输入总是预测错误。日志用结构化格式(JSON),方便后续分析。
回滚机制也很重要。模型上线后效果不好怎么办?要能快速切回上一个版本。我的做法是模型文件按版本号命名,服务启动时指定版本,回滚就是改个配置重启。简单但有效。
4. 实操过程:从零到一跑通一个完整项目
4.1 项目初始化:十分钟搭好骨架
假设我们要做一个文本分类项目,从零开始。第一步是搭骨架:
mkdir ai-project && cd ai-project git init mkdir -p src data configs models logs tests touch README.md .gitignore requirements.txt.gitignore要排除数据文件、模型文件、日志、虚拟环境这些不该进Git的东西:
data/ models/ logs/ __pycache__/ *.pyc .venv/ .env然后配置pyproject.toml或者requirements.txt,把核心依赖列清楚。我一般会分requirements.txt和requirements-dev.txt,训练和推理的依赖也尽量分开,减少镜像体积。
4.2 数据准备:写一个靠谱的清洗脚本
数据清洗脚本我一般放在src/data/下面,结构大概是:
src/data/ __init__.py loader.py # 数据加载 cleaner.py # 清洗逻辑 splitter.py # 训练验证集划分cleaner.py里每个清洗函数都要有明确的输入输出和日志:
import logging import re logger = logging.getLogger(__name__) def remove_duplicates(texts): seen = set() result = [] for t in texts: if t not in seen: seen.add(t) result.append(t) logger.info(f"去重: {len(texts)} -> {len(result)}") return result def clean_text(text): text = re.sub(r'\s+', ' ', text) text = text.strip() return text清洗完的数据存成parquet格式,比CSV读取快,而且能保留数据类型。划分训练验证集的时候固定随机种子,保证每次划分一致。
4.3 训练脚本:配置化加日志
训练脚本的核心结构:
import yaml import random import numpy as np import torch from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) def main(config_path): with open(config_path) as f: config = yaml.safe_load(f) set_seed(config["seed"]) # 加载数据、模型、训练...关键点:所有参数从配置读,随机种子固定,训练过程用Trainer的callbacks记录指标到MLflow或TensorBoard。训练完保存模型时,把配置文件和模型一起存,方便追溯。
4.4 模型服务:最小可用加监控
服务代码前面已经给了示例,补充几个实操细节:
- 模型加载放在服务启动时,不要每次请求都加载
- 输入要做长度限制和异常处理,防止恶意输入打挂服务
- 加一个
/health接口,方便健康检查 - 日志记录请求ID、输入长度、预测结果、耗时
import time import uuid import logging @app.post("/predict") def predict(req: Request): req_id = str(uuid.uuid4()) start = time.time() try: # 推理逻辑 result = {"label": pred} latency = time.time() - start logger.info(f"req_id={req_id} input_len={len(req.text)} label={pred} latency={latency:.3f}") return result except Exception as e: logger.error(f"req_id={req_id} error={str(e)}") raise4.5 部署上线:Docker加简单编排
推理服务的Dockerfile要尽量精简:
FROM python:3.10-slim WORKDIR /app COPY requirements-serve.txt . RUN pip install --no-cache-dir -r requirements-serve.txt COPY src/serve/ ./serve/ COPY models/ ./models/ EXPOSE 8000 CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-w", "2", "-b", "0.0.0.0:8000", "serve.app:app"]-w 2是两个worker,根据CPU核数调整。部署用docker-compose就够了,一个服务加一个监控,简单明了。等真的需要横向扩展了,再考虑K8s。
5. 常见问题与排查技巧实录
5.1 训练相关的高频问题
问题一:同样的配置,两次训练结果差很多。
原因基本是随机种子没固定全。除了Python、NumPy、PyTorch,还要注意cudnn的deterministic设置,以及DataLoader的worker_init_fn。如果用了多卡训练,还要固定每张卡的种子。
问题二:训练loss不下降或者震荡严重。
先检查数据有没有问题,标签对不对,输入格式对不对。然后看学习率是不是太大,batch size是不是太小。我遇到过一次,数据清洗时把标签列也当文本处理了,导致标签全错,loss当然不降。排查的时候,先打印几条数据和标签看看,往往能快速定位。
问题三:显存不够。
优先减小batch size,然后考虑梯度累积。如果还不够,用混合精度训练(torch.cuda.amp),能省不少显存。再不行就上梯度检查点(gradient_checkpointing),用时间换空间。
5.2 部署相关的高频问题
问题一:服务启动慢。
模型加载是大头。如果模型很大,可以考虑用torch.jit或者ONNX加速加载,或者用内存映射文件。另外,Docker镜像层要合理利用缓存,依赖安装和代码拷贝分开,改代码不用重装依赖。
问题二:推理延迟高。
先看是不是CPU推理,能用GPU就用GPU。然后看输入长度,长文本推理慢是正常的,可以在服务层做截断。再就是模型本身,可以考虑量化、蒸馏、剪枝这些优化手段。但优化之前,先用profiler定位瓶颈在哪,不要盲目优化。
问题三:线上效果和离线评估不一致。
这是最头疼的问题之一。常见原因有:训练和推理的预处理不一致(比如tokenizer参数不同)、数据分布变了、特征计算逻辑不同。排查方法是,把线上请求的输入存下来,离线跑一遍,对比结果。我一般会在服务里加一个采样日志,把部分请求的输入输出存下来,方便排查。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 训练结果不可复现 | 随机种子未固定 | 检查所有随机源 | 固定Python/NumPy/PyTorch/cudnn种子 |
| loss不下降 | 数据问题或学习率过大 | 打印数据和标签 | 检查数据质量,调小学习率 |
| 显存不足 | batch size过大或模型过大 | 查看显存占用 | 减小batch,梯度累积,混合精度 |
| 服务启动慢 | 模型加载耗时 | 计时模型加载 | 模型优化,镜像分层缓存 |
| 推理延迟高 | CPU推理或输入过长 | profiler定位 | GPU推理,输入截断,模型量化 |
| 线上线下不一致 | 预处理不一致 | 对比预处理逻辑 | 统一预处理代码,采样日志排查 |
独家避坑技巧:每次上线新模型前,一定要做A/B测试或者灰度发布。不要一次性全量替换,万一效果不好,影响面太大。我一般会先切10%的流量,观察一天,指标正常再逐步放大。这个习惯帮我避免了好几次线上事故。
6. 我踩过的那些坑和最后的小建议
说几个我印象最深的坑。第一个是数据泄露,做特征的时候不小心把标签相关的信息也放进去了,离线评估指标特别好,上线后一塌糊涂。这个坑让我养成了习惯:每次做完特征,都要检查一遍特征和标签的相关性,异常高的特征要警惕。
第二个是环境不一致,本地训练用的是PyTorch 1.12,服务器上是1.13,结果模型加载报错。从那以后,我所有项目都用Docker,本地和服务器用同一个镜像,彻底解决环境问题。
第三个是日志没打好,线上出问题的时候,日志里只有一句“error”,什么上下文都没有,排查全靠猜。后来我强制要求所有关键路径都要打结构化日志,包含请求ID、输入摘要、错误堆栈,排查效率提升了好几个档次。
最后分享一个小技巧:养成写README的习惯,把项目的环境配置、数据准备、训练命令、部署步骤都写清楚。不是为了别人,是为了三个月后的自己。我经常翻自己以前的项目,如果没有README,重新跑起来要花半天,有了README,十分钟就能恢复环境。这个投入产出比,高得离谱。
AI工程这条路,入门不难,但做好需要时间积累。不要急着追新框架新工具,把基础的数据管道、代码规范、部署流程做扎实,后面学什么都快。我到现在还在不断补工程方面的知识,这块没有捷径,就是多做、多踩坑、多总结。希望这篇内容能帮你少走点弯路。