"ai-engineering-from-scratch",这个标题在技术社区里越来越常见,我第一次看到时其实有点好奇——它不是"learn-ai"或者"ai-for-beginners",而是特意强调了"engineering"。两者的差别非常大,前者可能只是让你会用工具,后者是真的在教你如何像工程师一样思考、设计和交付一个能扛住真实压力的AI系统。这篇文章我想把自己从零搭建AI工程能力的过程、踩过的坑、以及最终沉淀下来的那套方法论完整拆开来讲,内容比较长,但都是实操过的,希望能帮你少走弯路。
这个"from scratch"到底是从哪条线开始?我的理解是:不依赖任何现成的端到端平台,不靠拖拽式工具,而是从Python基础、数据处理、模型训练、服务部署一路亲手做下来。目标是让你在读完、练完这套内容之后,即使把你扔进一个完全没有AI基础设施的团队,你也能自己从零搭出一套可用、可维护的AI服务。
适合谁看?如果你刚工作一两年,被安排去做AI相关项目但之前主要写业务代码;或者你是学生,想系统性地进入AI工程这个方向;又或者你已经能做模型训练,但一说到上线就头疼——这篇文章和这套实践路径,就是冲着你的需求来的。
1. 内容整体设计与思路拆解
1.1 为什么强调"engineering"而不是"modeling"
我的第一个观点可能会让不少人意外:在真实的生产环境里,模型训练代码通常只占整个系统代码量的10%-20%。剩下的是什么?是数据管道、验证逻辑、监控告警、容器编排、接口服务、配置管理、测试覆盖,还有最容易被低估的——文档。
很多自学AI的人陷入一个误区,以为"会训练模型"就等于"会AI工程"。结果面试造火箭,入职拧螺丝,发现自己真正要写的代码全是高速缓存、队列、重试机制、多进程调度这些东西。这就是标题里"engineering"的深意,它要求你把AI当作一个软件工程问题来对待,而不是一个算法问题。
所以我做这套"ai-engineering-from-scratch"实践时,设计原则非常明确:
- 每个项目都必须从真实业务场景出发,而不是用玩具数据集刷精度
- 每一步都要有可运行、可测试的代码,而不是只写伪代码
- 模型能力只是中间产物,交付物永远是一个服务、一个系统或一条完整链路
注意:这里不是说算法不重要,而是说算法之外的那部分工程能力,才是大多数公司真正缺的、也是你个人竞争力的核心护城河。
1.2 从零构建的核心路线图
如果给"ai-engineering-from-scratch"画一条学习路径,结合我自己的经验,应该是这样的:
第一层是根基,Python高级语法、类型标注、单元测试、虚拟环境管理。这部分很多人跳过了,后面才会不断返工。
第二层是数据工程思维,包括清洗、特征工程、数据版本管理、数据校验。现实中的数据永远是脏的、乱的、有偏的,处理它的能力直接决定模型上限。
第三层是模型训练全流程,这里不只是跑通一个notebook,而是要用脚本化方式管理训练、评估和实验记录,保证每次实验结果可复现。
第四层是模型服务化,包括REST API设计、性能优化、并发处理、模型版本管理等。
第五层是MLOps闭环,包含CI/CD、监控、告警、模型漂移检测和自动重训。
很多人学AI只停留在第三层,而"from scratch"的意义,恰恰是把第四层、第五层也亲手搭建一遍。你可能觉得这些不太酷,但在招聘市场上,能完整走完五层的人,比只会训练模型的人值钱得多,因为你可以独立负责一整条AI产品线。
1.3 工具选型:为什么是这套技术栈
我的开源技术栈选择,经过几次项目沉淀后基本固定了:Python 3.10+作为主力语言,Poetry做依赖管理和打包,Pandas和Polars做数据处理,Scikit-learn和PyTorch做建模,FastAPI做服务层,Docker做环境一致性,MLflow做实验追踪和模型注册,GitHub Actions做CI/CD,Prometheus加Grafana做监控。
这个选型的逻辑很简单:
- 每个工具都经过大规模生产验证,不是一堆GitHub上几千star但没人敢上线的玩具项目
- 它们之间有非常活跃的社区连接,比如MLflow原生支持FastAPI模型的部署
- 学习资料质量高,遇到问题更容易搜索到解决方案
有人可能会问为什么不用Airflow或Kubeflow这类重型工具。我的回答是:新手阶段用重型工具弊大于利,你需要先理解编排、调度、状态管理这些底层概念是怎么工作的,被它们虐过一遍,之后再用框架就是降维打击了。
2. 核心细节解析与实操要点
2.1 一个"AI工程"项目的完整构成是什么
我习惯把一个AI工程项目拆成五个核心包,这也是我每次从零搭建新项目时的标准骨架:
data/:数据获取、清洗、校验、版本管理的代码features/:特征工程的代码models/:模型定义、训练脚本、评估脚本api/:模型服务层代码infra/:Docker配置、部署脚本、监控配置
这个分层看起来简单,但每加一层约束,就能在后面的迭代中省掉大量问题。比如data/和models/严格分离,数据集的改动就不会污染模型代码的git历史,回滚某个坏版本就特别干净。
对应到实际工作中的体会是,很多人喜欢把所有脚本塞进一个src/目录里,前期方便,后期灾难。当你的代码超过5000行、模型迭代超过20个版本之后,没有一个清晰分层,你会花大量时间在"找代码、猜依赖"上,而不是真正的算法调优。
2.2 数据模块:比算法重要十倍的工程环节
任何AI工程新手,第一个真正的拦路虎都是数据处理。我在"from-scratch"实践项目的第一个模块就放了一道非常真实的题:给定一份带缺失值、异常值、重复样本和不平衡类别的真实数据集,要求产出一个干净、可复用的训练集。
这里的"工程"体现在几个关键决策上:
数据版本管理。训练数据不是一成不变的,如果某天最新拉取的数据集质量下降,你要能明确知道是哪一批数据引入的问题。我给每个数据集加一个带哈希值的元数据文件,类似Git commit的机制,每次数据变更都生成新的版本号,后面模型训练时把数据版本记录到MLflow中,问题溯源就是几秒钟的事。
数据校验。很多人会把校验放在训练之后,等模型效果崩了才发现是数据出了问题。正确的做法是在数据管道的出口就设置校验规则,比如字段类型、取值范围、缺失率上限、分布偏移阈值。我用pydantic加自定义断言实现了一套轻量级校验,每次跑数据管道时,如果数据分布和上一个版本偏差太大,管道直接fail。
避免数据泄漏。这个坑几乎所有新手都踩过,包括我。最典型的错误是:在做特征工程之前就把全量数据的统计量算好并填充到特征里,而正确的做法是只用训练集的统计量来填充验证集和测试集。在工程项目里,我规定所有fit操作只能在训练数据上执行,transform则可以应用到任何数据,这个约束用抽象基类来强制保证。
注意:数据泄漏不会让你的模型效果变差,反而会让你的模型效果异常好——好到不真实。上线后立马原形毕露,这比效果差更可怕,因为它给了你虚假的安全感。
2.3 模型训练模块:脚本化是基本门槛
"from-scratch"实践项目要求训练代码必须是可脚本化运行的,禁止在Jupyter Notebook里跑完整训练。原因很简单:Notebook天然不可复现,你很难保证"上次运行时的环境状态"和"这次运行时完全一致"。
我的训练脚本框架长这样:
python -m train \ --experiment-name icefall_forecast \ --data-version 2024.06.01 \ --model-type gradient_boosting \ --n-estimators 500 \ --cross-validation folds=5每次实验前,需要先想清楚三个问题:
- 这次实验要验证什么假设?(比如换用新的特征A能否提升准确率)
- 基线是什么?(必须跑过之前至少一个模型作为对照)
- 成功标准是什么?(比如线上指标提升至少2个百分点才算有效)
这就是工程化训练和玩耍式训练的本质区别:前者每次实验都在为一个明确的问题寻找答案,后者只是不断调参数碰运气。
2.4 模型部署模块:从notebook到生产环境
在这里,我来拆解一下模型部署的完整心法。很多人在这个环节翻车,不是模型推理写错了,而是根本不知道生产环境对模型服务的要求远不止"能返回预测结果"。
第一是延迟SLA。你的API必须保证P99延迟在200ms以内,这意味着你不能每次请求都重新加载模型权重,而是在服务启动时预加载到内存中。第二是吞吐量。你需要考虑并发请求场景,FastAPI的async特性配合进程池通常能解决大多数问题。第三是可观测性。一个没有指标监控的模型服务等于裸奔,请求量、延迟、错误率、CPU和内存使用率这些基础指标必须一应俱全。
我部署AI服务的基本流程:
- 模型导出:训练结束后把模型权重和预处理参数一起打包为
model.joblib或model.pt文件 - 服务封装:编写FastAPI服务,加载模型并进行推理
- 容器化:写Dockerfile,保证开发环境和生产环境的Python版本和依赖完全一致
- 本地压测:用
locust做并发测试,验证延迟和吞吐量是否达标 - 灰度发布:新模型先切5%流量观察,确认稳定后再放量
2.5 评估指标:不要只看准确率
在"ai-engineering-from-scratch"实践里,我特意花了大量篇幅讲评估体系,因为这是很多人轻视的部分。不同业务场景需要不同的指标组合:
- 分类问题:准确率(Accuracy)、精确率(Precision)、召回率(Recall)、F1值、AUC
- 回归问题:MAE、MSE、RMSE、R-squared
- 排序问题:NDCG、MAP、MRR
- 业务最终指标:转化率、留存率、收入提升
工程项目的关键在于:建立离线指标和线上指标的相关性追踪。你离线评估提升了,上线后用户指标真的提升了吗?如果相关,说明你的评估体系建得对;如果不相关,说明你评估的方式有刻舟求剑的问题,需要尽快调整。
3. 实操过程与核心环节实现
接下来这一部分比较"硬核",讲的是我如何从零完整实现一个真实项目的全过程。选的项目是:供应链需求预测服务。这个项目既有时间序列处理、特征工程、模型选择的复杂度,也有部署监控的实际业务价值,非常适合作为"ai-engineering-from-scratch"的压轴实践。
3.1 项目骨架初始化和环境搭建
环境搭建这步看似简单,但很多人就是在这里翻车。我用Poetry做依赖管理的理由很简单:能把虚拟环境、依赖版本、锁定文件统一起来,配合Docker就能实现"我本机能跑,线上也一定能跑"的效果。
初始化项目:
poetry new supply-demand-forecast cd supply-demand-forecast poetry add pandas polars scikit-learn pydantic fastapi uvicorn mlflow docker这里有个细节值得说:为什么需要Polars?Pandas处理百万行数据时明显卡顿,而Polars使用Rust内核和惰性计算,性能提升好几倍。在真实业务里,数据量动辄上亿行,Pandas会成为瓶颈,Polars是更优的选择。当然,如果团队主要用Pandas,你也可以保留。工具是死的,业务是活的。
注意:Poetry生成项目结构后,需要手动创建
data/、features/、models/、api/、infra/这几个目录,因为在AI工程项目中,按职能分包比按技术分包更符合实际情况。
3.2 数据管道实现:从CSV到特征矩阵
我拿到的原始数据是某零售品牌过去3年的SKU级日销量,包含促销活动、节假日、天气、库存水平等信息。数据结构长这样:
| 字段 | 类型 | 示例 |
|---|---|---|
| store_id | string | ST3847 |
| sku_id | string | SKU88421 |
| date | date | 2024-03-15 |
| sales_quantity | int | 156 |
| promo_flag | bool | True |
| holiday_flag | bool | False |
| weather_condition | string | sunny |
| inventory_level | float | 0.78 |
第一步,我用Polars写了一个数据加载和清洗的管道,指定好每列的数据类型,遇到无法解析的日期直接fail,避免脏数据悄悄溜进后面的流程。
清洗规则体现了那个"宁可挂掉也不要产出错误数据"的取舍:缺失率超过20%的特征直接丢弃;缺失率低于20%的特征用训练集统计量填充,这主要是为了保证后面推理时的一致性;重复样本按日期和SKU去重。跑完清洗,原始2000万行数据剩下1750万行,13个特征保留到11个。
第二步,窗口特征工程。需求预测的难点在于:单一SKU的销售序列非常稀疏且有强季节性,直接喂给模型效果很差。我采用的做法是加滞后特征、滚动统计特征和外部特征。
def create_features(df, config): # 滞后特征:过去7/14/28天的销量 for window in config.lag_windows: df = df.with_columns( pl.col("sales_quantity") .shift(window) .over(["store_id", "sku_id"]) .alias(f"sales_lag_{window}") ) # 滚动统计:7/14天的均值、标准差、最小值、最大值 for window in config.rolling_windows: df = df.with_columns( pl.col("sales_quantity") .rolling_mean(window) .over(["store_id", "sku_id"]) .alias(f"rolling_mean_{window}") ) return df特征工程的核心理念是"人机协同":让机器做重复性、大规模的计算,让人做特征选择、业务理解的判断。加特征要多,但要想想特征的实际意义。比如"季度末促销"这个特征是我观察数据分布时发现的规律,这种带有业务洞察的特征,往往比单纯加更多窗口特征更有效。
第三步,数据集划分。为了避免数据泄漏,我严格按时序切分:训练集取自项目前两年的数据,验证集和测试集分别是最后两个独立月份。这样做模拟了真实的预测场景:训练过去的,预测未来的。
3.3 模型训练和调优流程
模型选型上,我对比了三类算法:线性回归、梯度提升树(LightGBM/XGBoost)、以及一个简单的时间序列基准模型。没有选择深度学习方法的原因有两个:数据量还不够支撑复杂模型,"杀鸡用牛刀"且难以维护;第二个是业务上需要模型可解释性,每一条预测都能追到特征归因上,树模型在这方面有天然优势。
训练的核心代码用MLflow进行实验追踪:
with mlflow.start_run(run_name=f"xgboost_{timestamp}"): mlflow.log_params(params) model = XGBRegressor(**params) model.fit(X_train, y_train, eval_set=[(X_val, y_val)]) # 记录离线指标 for metric_name, value in eval_metrics.items(): mlflow.log_metric(metric_name, value) # 记录模型和预处理流程 mlflow.sklearn.log_model(model, "model") mlflow.log_artifact("preprocessor.pkl")调优过程的经验是:不要一开始就Random Search或者Grid Search,先手动跑几组极端参数,理解参数方向和指标变化趋势,然后再用贝叶斯优化收窄搜索空间。手动探索加自动搜索的组合,能在时间成本可控的前提下拿到更好的结果。
最终模型的表现:验证集RMSE比基线下降了23%,准确率提升到89.4%。但注意,这只是线下数据上的表现,真正的考验在线上。
3.4 模型服务化和容器化部署
训练完成之后,进入我反复强调过的第四层——模型服务化。我选择了FastAPI,性能好、文档自动生成、用起来很顺手,而且对新手友好。核心文件代码如下:
from fastapi import FastAPI, HTTPException from pydantic import BaseModel import joblib app = FastAPI() model = joblib.load("models/model.joblib") scaler = joblib.load("models/scaler.pkl") class PredictionInput(BaseModel): store_id: str sku_id: str date: str # 其他特征字段 class PredictionOutput(BaseModel): prediction: float skus: list[str] @app.post("/predict", response_model=PredictionOutput) async def predict(input_data: PredictionInput): try: features = preprocessor.transform(input_data) prediction = model.predict(features) return PredictionOutput(prediction=prediction) except Exception as e: raise HTTPException(status_code=500, detail=str(e))把模型服务封装成API后,就轮到Docker上场了。很多新手只知道"用Docker是为了环境一致",但具体怎么写Dockerfile就没概念了。一个高效的Dockerfile应该把依赖层和代码层分开,因为依赖层很少变更,可以直接用缓存,镜像构建速度会快很多。
FROM python:3.10-slim as builder WORKDIR /app COPY pyproject.toml poetry.lock ./ RUN pip install poetry && poetry config virtualenvs.create false && poetry install --no-dev FROM python:3.10-slim WORKDIR /app COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages COPY . . EXPOSE 8000 CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"]部署到测试环境后,我做的第一件事不是发预测请求,而是跑压测。用locust模拟50个并发用户,持续5分钟,观察P99延迟是否在SLA以内。实测结果,QPS达到800,P99延迟156ms,平均延迟38ms,完全达标。这才是能上生产的模型服务,而不是一个"看起来能跑"的脚本。
3.5 监控和告警链路搭建
模型上线之后,你以为就结束了?恰恰相反,真正的工作才刚刚开始。我搭建了一个"模型健康监控仪表盘",盯四个方面:
请求数据:监控每天的调用量、单位时间内的请求分布。如果某天突然跌掉80%,大概率是上游系统出了问题,可能与模型本身无关。
响应质量:离线评估是"死"的,线上反馈才是"活"的。我设计了一个异步反馈闭环,用户侧会把实际需求和预测结果的差值回传,我们实时计算线上MAE,一旦连续3天超过阈值,就自动触发告警。
数据漂移:生产环境的输入数据分布和训练数据的分布,可能随着时间推移而逐渐偏离。我用PSI(群体稳定性指数)实时监测关键特征的漂移程度。当PSI超过0.2时,就会在仪表盘上标红,提示需要重新培训模型。
模型切换开关:在FastAPI服务里内置一个配置开关,可以将流量指向新模型或回滚到旧模型。这个开关配合CI/CD,让模型版本的发布和回滚像日常发版一样简单。
4. 常见问题与排查技巧实录
4.1 冷启动和特征依赖
实话说,做AI工程最容易翻车的地方往往不在模型本身,而在"特征对齐"这个环节。我遇到过一种非常经典的问题:训练代码里的特征名,和推理API里收到的特征名对不上。训练阶段用"days_since_last_order",部署阶段传成了"days_since_last_purchase",差一个字母,模型静默返回了一个随机结果,根本没有任何报错。
这个问题的根源在于训练和推理的特征管道用两套代码维护,后期一改动必然不同步。我的解决方案是把特征工程代码抽成一个独立的包,训练和推理都import同一个包,从源头锁死不一致的可能,再在代码里加上特征完整性校验,上线前跑一次"训练实列与推理实列对齐测试"。
4.2 模型性能下降但没人发现
这种情况比较隐蔽。你的模型在跑,API也没有报错,但是业务方开始抱怨预测越来越不准。当你去查看监控时发现模型指标确实在下降,但要追溯到是"什么时候开始下降的",就成了一个让人头疼的问题。
解决这个问题最好的方式是上线之初就记录基线预测分布。在模型第一次部署时,把线上输入特征分布和预测结果分布保存下来,之后每周自动对比一次。一旦当前分布和基线分布的差异超过阈值,就发告警,让模型训练团队开始准备新数据。这是我用"血的教训"换来的经验,一开始没做,后面排查问题全部靠猜,痛苦不堪。
注意:模型漂移不是一个异常现象,而是必然现象。数据分布会变,用户行为会变,环境会变,模型效果劣化是时间问题。工程上的重点是让"发现劣化"的反应时间尽量短。
4.3 依赖地狱
AI项目的依赖管理比传统Web项目要痛苦得多:Pandas、NumPy、Scikit-learn这些核心库之间互相钳制版本,PyTorch和TensorFlow还会争夺GPU资源,同时你的服务端还要求Python版本不能太新以免某个库不兼容。
我给出的建议是:项目一开始就锁定所有依赖版本,永远用Poetry管理而不是手工pip安装。每次更新依赖的时候单独跑一次完整的回归测试,确保核心流程不受影响。升级依赖是"立项级"操作,不要顺手就做,特别是涉及NumPy和Pandas的大版本升级。
4.4 FastAPI部署时的坑
如果直接用uvicorn main:app --reload命令启动服务,并把它放到生产环境,那就埋下了一颗雷。--reload是为开发设计的,每次保存代码都会自动重启,启动前还有代码扫描开销,在生产环境既慢又不稳定。生产环境应该用uvicorn main:app --workers 4(根据CPU核心数调整)来充分利用多核。
另一个容易踩的坑:Docker内部运行FastAPI时,默认绑定127.0.0.1,但这个地址在容器外部是访问不到的。必须指定--host 0.0.0.0,否则你本地压测全部失败,还以为代码有问题。
4.5 在线评估:业务指标连着天
离线模型评估做得再好,最后还是要接受真实业务的检验。我在项目上线一个月后,做了一次线上评估,结果令人警醒:线上MAE和离线验证结果相比,高了将近30个百分点。原因让人意外,业务方在旺季加大了促销力度,导致销量分布偏离训练集,模型完全没跟上节奏。
从那以后我建立了一个机制:每月固定做一次线上数据分布回顾,结合业务日历(大促、节假日、清仓等)提前预判数据跳变,必要时提前用最新数据重新训练模型。AI工程不是一次性交付,它是一条持续运营的链路。
5. 学习路径建议与扩展方向
5.1 每周动手计划
如果你完全是零基础,想通过"ai-engineering-from-scratch"这条路径走下来,我推荐一个13周的计划:
- 第1-2周:Python基础知识补漏,重点掌握类型、装饰器、生成器、上下文管理器
- 第3-4周:Pandas/Polars数据处理,完成10个练习题
- 第5周:探索性数据分析,学会画直方图、箱线图、相关性热力图
- 第6-7周:Scikit-learn建模,逻辑回归、决策树、随机森林
- 第8周:特征工程专题,处理缺失值、编码、缩放、特征选择
- 第9周:模型评估体系,交叉验证、混淆矩阵、PR曲线、ROC曲线
- 第10周:FastAPI部署,把第9周的模型封装成API
- 第11周:Docker容器化
- 第12周:MLflow实验追踪
- 第13周:完成一个完整项目,从数据到模型到部署到监控全链路打通
每个阶段都要有输出物,可以是代码仓库、部署链接、或者技术博客。没有输出物的学习通通是"假装学习"。
5.2 项目选题推荐
练习AI工程最好的项目,必须满足"数据真实、流程完整、有部署价值"三条标准。我的推荐:
- 电商销售预测(可以在Kaggle上找真实数据)
- 客户流失预警(电信/银行领域的数据集很丰富)
- 房价预测(训练集干净,适合练习特征工程)
- IT日志异常检测(和运维结合,实战价值很高)
做项目的过程中,切记不要只追求精度数字。把模型效果做到"不差"之后,立刻把精力转向部署、监控、自动重训等工程环节,你的收获会大得多。
结尾:一些值得分享的个人体会
做了这么多年AI工程,我最大的体会是,这个领域最稀缺的不是懂算法的科学家,也不是只会敲代码的普通程序员,而是能把算法变成稳定在线业务系统的复合型人才。
"from-scratch"的价值恰恰在于它逼着你把每一块地基都亲手打一遍。你用Docker部署服务的经历,会帮助你理解为什么公司需要一套统一的运行时环境;你手动搭监控告警的经历,会帮助你理解平台团队为什么要维护一套监控体系;你被数据分布偏移折磨过的经历,会帮助你更珍惜数据治理和模型生命周期管理这些平时不起眼的基建工作。
如果你正在走这条路,我的建议很简单:先把一个端到端的小项目完整跑通,哪怕它只是一个连GUI都没有的命令行工具。只有当你亲身体验过"从一团乱麻的数据到上线一个被真实用户调用的服务"的全过程,你才会真正理解AI工程到底是什么,也才能在未来面对更复杂系统时游刃有余。
希望这些踩坑经验和实操细节对你有一点帮助。祝你在AI工程这条路上,少踩坑,多沉淀,持续进步。