刚看到“ai-engineering-from-scratch”这个项目标题时,我第一反应是:又一个把“调API”包装成“AI工程”的项目。但仔细把这几年带团队、做落地的经验捋了一遍之后,我得说,这个标题其实点到的是整个行业最缺的那批人——不是会训练模型的研究员,也不是只会写requests.post的调用工,而是真正能把模型装进业务系统、让它稳定跑起来、出问题能定位、性能扛得住压力的AI工程师。
这个项目从名字看就是“从零开始做AI工程”,意味着它不假设你有大厂基建,也不默认你能调用内部推理平台,而是逼着你用最朴素的手段,从第一行代码到完整系统走一遍。这篇文章我就围绕这个标题,把我认为一个合格的AI工程师必须具备的能力拆开揉碎讲清楚,再给出一条我自己验证过的学习路径和实操方案。里面的工具选型、架构取舍、踩坑记录,都是我真实跑过的,可以直接拿去用。
1. 内容整体设计与思路拆解
1.1 为什么“会写模型”不等于“会做AI工程”
先说一个我在面试里反复遇到的场景:候选人简历上写着精通TensorFlow、PyTorch,论文也发过,让他现场说一个“用户点击率预测”从数据到上线的完整链路,他能把模型结构讲得头头是道,但一问到“特征怎么上线”“模型怎么更新”“延迟超标了怎么办”,就开始含糊。
这不是个例。AI工程和“写模型”是两个物种。写模型关心的是在固定数据集上把指标刷高,AI工程关心的是在真实流量、真实数据分布漂移、真实硬件限制下,让系统持续稳定地产出价值。用一个生活类比:前者是米其林大厨,给你一个干净厨房,他能做出满汉全席;后者是野战炊事班,给你一个汽油桶几把野菜,他也能让一个连队吃上热饭。AI工程要的就是后面这种能力——资源受限、环境复杂、需求多变,但你得保证系统活着、转着、有效果。
“ai-engineering-from-scratch”这个名字里的“from scratch”特别关键。它暗示了一条逻辑:不依赖任何黑盒平台,自己动手把每一层都打通。这意味着你需要理解数据怎么流、模型怎么部署、服务怎么编排、监控怎么设计,而不是只会按平台的向导点按钮。
1.2 AI工程化的两条主线:模型全生命周期 + 系统稳定性
如果让我用一个框架来概括AI工程的核心,我会画成两条交叉的主线。
第一条线是模型全生命周期管理。从一个业务问题被提出开始,你要做数据采集、数据清洗、特征工程、模型训练、模型评估、模型部署、线上监控、模型迭代。这条线听起来和MLOps课程讲的一样,但真正的工程难点在于:每一环都不是线性的。数据质量出问题,你要回溯到采集端;线上效果衰减,你要判断是数据漂移还是业务环境变了。
第二条线是系统稳定性与性能。模型本身只是一个函数,但你把它暴露给用户时,你就得考虑QPS、延迟、并发、容灾、降级。上一周我刚好有个朋友吐槽,他们团队花三个月训了一个效果很好的推荐模型,上线当天就因为缓存击穿把数据库打挂了——这就是典型的“模型很强,系统很脆”。
AI工程的全部工作,本质上就是这两条线的交汇。你不能只盯着AUC和Loss,你得知道模型在线上每秒被调用多少次、P99延迟是多少、出错了怎么兜底。这也是我看到“from scratch”这个项目名时,最希望它能覆盖的部分。
2. 核心能力拆解:你要掌握的四大模块
2.1 数据处理能力:工程化的地基
别一上来就学模型架构。AI工程的地基是数据。我的经验是,如果数据处理能力不过关,后面所有环节都会返工。
这里说的数据处理,不止是pandas里dropna()和fillna()这种基础操作。工程化场景下的数据处理,至少要包含三层:第一,数据管道——你要能从业务数据库、日志文件、埋点系统里把数据抽出来,做清洗转换,落到特征存储或数据仓库;第二,数据质量校验——字段缺失率、取值分布是否合理、时间戳是否对齐,这些都要有自动化检查;第三,数据版本管理——训练数据变了,模型效果变了,你必须能追溯是哪一批数据导致的。
我在这个项目里最推荐的实践是:哪怕你只是自己学习,也要从一个真实的、脏的数据集开始。不要用Kaggle上已经清洗好的数据。
2.2 模型服务化与部署能力
模型训练出来只是起点,真正考验工程能力的是怎么把模型变成线上服务。
模型部署有两条常见路线,一条是传统方案:把模型序列化成pkl或onnx格式,写一个Flask/FastAPI服务包一层HTTP接口;另一条是专用推理框架:比如TorchServe、Triton Inference Server,它们帮你处理模型加载、动态批处理、并发控制。
我强烈建议初学者先把第一条路线彻底走通。原因很简单:你只有在手写服务接口的过程中,才会理解模型加载为什么耗内存、为什么多线程调用有风险、为什么需要对输入做预处理。直接上Triton这类框架,很多底层问题会被框架掩盖,出了故障你反而不知道从哪里排查。
部署环节有四个坑是新手必踩的:其一,模型加载和推理要分离——不能每次请求都重新加载模型;其二,输入数据要和服务端预处理逻辑保持一致——训练时做的归一化参数必须同步到服务端;其三,服务要设置超时和熔断——第三方依赖出问题时不能把整个服务拖死;其四,CPU和GPU推理的结果可能有细微差异——如果对数值一致性要求高,要注意算子精度设置。
2.3 性能优化与资源管控
一个模型服务的性能,不是由“模型跑得有多快”单独决定的。它由一组指标共同定义:吞吐量、延迟、资源占用、成本。
举个例子,我优化过的一个意图识别服务,最初是同步调用方式,单次请求平均耗时80ms,QPS一高CPU直接飙到90%。后来做了三个改动:把模型从PyTorch转换成ONNX推理,单次耗时降到50ms;加了一层缓存,对重复请求直接命中,整体耗时降到15ms;用异步任务队列替代同步阻塞,削峰填谷。
这三个改动没有一个是“重新训练模型”,但带来的收益比精细调参高得多。
2.4 监控、评估与持续迭代
模型上线之后,工程挑战才真正开始。线上环境不比离线评估集,数据分布会漂移,用户行为会变化,外部环境会干扰模型表现。
所以你必须建立一套监控体系。最低限度要包含三类指标:业务指标(这个模型有没有真的带来业务收益)、模型质量指标(预测分布是否异常、置信度是否下降)、系统指标(延迟、QPS、错误率)。这三类任何一类出问题,都要能及时告警。
3. 实操过程:一个人从零搭建一个AI服务
3.1 场景定义与数据集准备
我采用一个几乎每个团队都用得上的场景:垃圾评论识别。它足够简单——模型可以用文本分类;也足够工程化——涉及数据标注、文本预处理、在线推理、性能调优、监控告警。
数据我用的是公开的文本分类数据集,然后把训练集和线上真实分布故意做了偏移——这是工程实践里很重要的一步,你必须在离线阶段就模拟“数据漂移”,因为你如果从来没有处理过分布差异,上线后遇到真实漂移会手足无措。
3.2 离线训练的工程化规范
很多初学者训练模型时,代码风格是“能跑就行”——所有逻辑写在同一个train.py里,参数直接硬编码。这种写法在项目标题叫“from scratch”的语境下是可以接受的,但如果你想让它变成一份“工程化”的代码,至少要分开几个模块:数据处理、模型定义、训练流程、评估流程。
我用一个极简的config.py来统一管理参数,它记录模型名称、学习率、批次大小、训练轮数、数据路径等。把这套配置固化下来之后,整个训练流程就具备可复现性。
这个阶段我还做了一件很重要的事:给数据集做完整性校验,确保每一条样本都有它对应的标签;检查正负样本比例;对文本长度分布做统计。这些信息不仅帮助我清理数据,也决定了我后续怎么设计预处理逻辑。
3.3 模型服务化的三种方案对比
我实际用三种方案部署了同一个模型,把结果整理成了一个对比表:
| 方案 | 部署方式 | 平均延迟 | QPS | 适用场景 |
|---|---|---|---|---|
| Flask + pickle | 单机同步服务 | ~10ms | 100 | 原型验证、内部工具 |
| FastAPI + onnxruntime | 单机异步服务 | ~4ms | 350 | 中小流量、延迟敏感型 |
| Triton Inference Server | GPU/多副本部署 | ~3ms | 800+ | 高并发、多模型管理 |
这个对比实验做完,你会对“部署方式如何影响性能”有非常直观的认识。同一个模型,就因为序列化格式不同、服务框架不同,吞吐量可以差三倍以上。
3.4 完整可复现的FastAPI部署代码
下面这个代码是基于onnxruntime的FastAPI部署,它是我用的最顺的一个组合,兼顾性能和开发效率。整个文件就是一个可以独立运行的服务模块。
import io import numpy as np from fastapi import FastAPI, UploadFile, File from pydantic import BaseModel import onnxruntime as ort import re app = FastAPI(title="Comment Classifier Service") # 1. 加载ONNX模型 sess = ort.InferenceSession("models/comment_clf.onnx", providers=["CPUExecutionProvider"]) input_name = sess.get_inputs()[0].name label_map = {0: "normal", 1: "spam"} # 2. 预处理函数 —— 训练和服务必须保持一致! def preprocess(text: str, max_len: int = 128) -> dict: # 简化版本:实际项目中可用jieba/tokenizer tokens = re.findall(r"[\w\u4e00-\u9fa5]+", text.lower()) ids = [hash(t) % 1000 for t in tokens[:max_len]] ids = ids + [0] * (max_len - len(ids)) mask = [1 if i > 0 else 0 for i in ids] return { "input_ids": np.array([ids], dtype=np.int64), "attention_mask": np.array([mask], dtype=np.int64) } class Item(BaseModel): text: str @app.post("/predict") def predict(item: Item): inputs = preprocess(item.text) logits = sess.run(None, {input_name: inputs["input_ids"]})[0] pred = int(np.argmax(logits, axis=1)[0]) prob = float(np.max(logits, axis=1)[0]) return {"label": label_map[pred], "confidence": prob, "text": item.text[:50]} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)这里有一个必须强调的点:预处理逻辑要和训练时保持一致。我见过太多线上事故,就是服务端的文本清洗规则和训练时差了一个strip(),导致预测结果系统性偏差。
3.5 性能压测与调优记录
部署完成之后,我用了两款工具做压测:locust和wrk。wrk适合快速查看极限吞吐,locust更适合模拟更真实的用户行为。
压测时我会盯三个指标:P50延迟、P99延迟和错误率。线上用户体验基本由P99决定。最初我这套服务P99是150ms,原因是每个请求重新做了一次正则匹配和哈希计算,CPU占用率很高。后来我把预处理结果加了一个LRU缓存,命中率大约30%,P99直接降到30ms。
4. 工具链选型与架构取舍
4.1 三种功能定位的模型部署工具
现在市面上的AI部署工具很多,很容易挑花眼。我的选型建议是看功能定位,不要跟风。
- 纯服务框架:Flask、FastAPI。适合你只有一个模型、以HTTP接口方式暴露的场景。优点是轻量、灵活、和Python生态无缝结合。
- 专业推理服务:TorchServe、Triton Inference Server。适合多个模型、需要动态批处理、要用GPU做高并发推理的场景。功能强大,但运维成本也高。
- Serverless/托管平台:各类云函数、托管的模型服务。适合不想管服务器的场景,但要注意冷启动延迟和单次调用超时限制。
我个人的建议是:如果你做一个“from scratch”的完整项目,至少手写一次FastAPI方案,再体验一次Triton方案。这样你才会知道工具到底帮你解决了什么问题、引入了什么新麻烦。
4.2 特征与模型仓库:规模化避不开的基建
当你的模型数量超过五个,特征和模型的管理就变成痛点。特征不统一会导致训练和线上不一致,模型版本管理混乱则导致无法回滚。
我不建议初学者一开始就上Feature Store和Model Registry这样的重型组件。更务实的路径是:先用一套文件命名规范来管理,比如特征文件命名规则是{feature_group}_{date}.parquet,模型文件名带版本号和训练时间。等到项目复杂度真的撑不住了,再引入MLflow或Feast这类系统。
5. 常见问题与排查技巧实录
5.1 模型加载在每次请求时都重复执行
这个问题出现得非常高频。新手习惯在predict函数里写加载模型的逻辑,结果每次请求都重新读文件、重新占内存,服务一启动就卡死。解决办法很简单——把模型加载放到模块级别,只在进程启动时加载一次。如果你有模型热更新的需求,再单独做一个reload机制。
5.2 线上预测与离线评估结果差异过大
排查思路按优先级排序:先看预处理逻辑是否一致,再看特征工程是否一致,最后再看推理框架的数值精度。我遇到过的案例里,九成以上是前两个原因。
5.3 服务高并发时内存持续上涨
这个问题的锅,很大程度上要由Python的GC机制和推理框架的缓存策略背。可以尝试用objgraph或tracemalloc定位内存占用来源,也可以直接对推理部分做进程隔离。真实生产环境里,用独立推理进程或容器来隔离内存隐患,是完全值得的成本投入。
5.4 模型效果上线后逐日衰减
线上效果衰减通常不是代码Bug,而是数据环境变了。你可以设计一个分布监控脚本,每天跑一遍线上特征分布和训练集特征分布的相似度指标(比如KL散度或PSI)。一旦指标连续几天超阈值,就要触发告警并考虑重新训练了。
| 问题 | 常见原因 | 推荐排查手段 |
|---|---|---|
| 服务启动慢 | 模型在请求时加载 | 模块级单次加载 |
| 线上离线不一致 | 预处理不一致 | 代码走查 + 黄金样本集回归 |
| 高并发内存上涨 | 推理框架/GC问题 | tracemalloc + 进程隔离 |
| 效果衰减 | 数据漂移 | 分布监控 + 定期重训 |
6. 关于学习路径与心态的最终建议
现在回过头看“ai-engineering-from-scratch”这个项目,我最想强调的是:不要一上来就追求新框架,先把一条最简单的链路打通——数据进来,模型出去,服务上线,指标可见,问题可查。你每完成一个闭环,AI工程能力就扎实一分。
我个人实际做项目的顺序是:先写丑陋但能跑的代码,再把“能跑”变成“能上线”,最后再谈“能规模化”。很多卡在AI工程门槛上的人,不是缺模型知识,而是缺“把一个东西交付出去”的工程素养。这个项目名里的“from scratch”,恰恰就是在补这门课。
如果你也想实践一遍,我建议你从今天开始,找一个小而真实的痛点场景,别用现成的清洗好的数据,别跳过部署和监控,完整地走一遍这条链路。等你走完,回头再看那些招聘JD上写的“熟悉模型部署上线与性能优化”,你会发现自己已经能和面试官聊出真正有细节的内容了。