从零搭建AI工程化项目:数据管道、特征工程与模型服务实战
1. 从零搭建AI工程能力这个项目到底在解决什么问题第一次看到ai-engineering-from-scratch这个标题我脑子里蹦出来的第一个念头是又一个“从入门到放弃”的教程合集但翻了一圈社区讨论和实际动手跑过之后我发现它切中的是一个特别真实的痛点——市面上讲AI的资料要么是纯理论推导要么是调包侠式的API调用中间那层“工程化落地”的脏活累活几乎没人系统讲。你想想看一个推荐系统或者对话机器人从Jupyter Notebook里的demo到能扛住线上流量的服务中间要跨过多少坑数据管道怎么搭、特征怎么存、模型怎么打包、推理延迟怎么压、A/B测试怎么做、监控告警怎么配——这些东西论文里不写官方文档只给片段全靠自己在生产环境里踩出来。ai-engineering-from-scratch这个项目本质上就是想把这条从“会调模型”到“能交付AI系统”的路用可复现的方式铺一遍。它适合谁我梳理了三类人第一类是刚学完机器学习课程的学生手里有理论但没碰过真实数据管道第二类是从后端或数据开发转过来的工程师代码能力够但缺AI系统设计经验第三类是带小团队的技术负责人需要一套能快速对齐认知的参考架构。这个项目不教你推导反向传播也不教你调Transformer的超参它教的是怎么让一个AI功能在真实业务里稳定跑起来。核心关键词就一个ai-engineering-from-scratch。但拆开看它覆盖了数据工程、模型服务、实验管理、持续交付这几个工程子领域。我实测下来的感受是它更像一张“AI工程化地图”而不是一本“算法原理手册”。下面我按自己复现的顺序把里面的关键设计、实操细节和踩过的坑一块一块拆开讲。2. 整体架构设计为什么这样分层而不是一锅炖2.1 从“Notebook思维”到“服务思维”的转变很多AI项目死掉不是因为模型不准而是因为代码组织方式还停留在Notebook阶段。ai-engineering-from-scratch最核心的设计决策就是强制把项目拆成四层数据层、特征层、模型层、服务层。这个分层看起来平平无奇但它解决了一个致命问题——让每一层可以独立测试和替换。我见过太多团队把数据清洗、特征计算、模型推理全写在一个main.py里结果换个数据源要改三天模型升级要全量回归。这个项目的分层逻辑是数据层只负责“把原始数据变成干净的结构化数据”特征层只负责“把干净数据变成模型可用的向量”模型层只负责“训练和评估”服务层只负责“对外提供推理接口”。层与层之间用明确的接口契约连接比如特征层输出必须是一个pandas.DataFrame或者pyarrow.Table列名和类型在配置文件中定义。为什么这么设计因为AI系统的变更频率是不均匀的。数据源可能每周变特征逻辑可能每月调模型可能每天重训但服务接口最好半年不动。分层之后你改数据源不会影响模型训练代码换模型不会影响上游特征计算。这种变更隔离是工程化的第一原则。2.2 工具选型的取舍逻辑项目里没有用那些重型框架比如Airflow、Kubeflow、MLflow全家桶。它选的是轻量级组合pandaspyarrow做数据处理scikit-learn做基线模型FastAPI做服务DVC做数据版本控制pytest做测试。这个选型背后有明确的考量。第一降低启动门槛。一个刚转行的工程师让他先学Kubernetes和Helm再学Kubeflow Pipeline黄花菜都凉了。用FastAPI写个推理接口半小时能跑通正反馈来得快。第二避免过度抽象。很多ML平台把“训练”抽象成一个黑盒你传数据进去它吐模型出来中间发生了什么完全不可控。这个项目坚持用显式的Python脚本每一步都能打断点调试。第三可替换性。今天用scikit-learn明天要换PyTorch只要接口不变替换成本很低。如果一开始就绑死某个平台迁移就是噩梦。我自己的经验是小团队做AI工程最怕的就是“平台依赖”。你用了某个云厂商的托管训练服务一开始很爽等你要做自定义特征工程或者特殊推理优化时发现处处受限。这个项目的“从零”哲学其实是把控制权留在自己手里。2.3 目录结构背后的工程思维项目推荐的目录结构是这样的project/ ├── data/ │ ├── raw/ │ ├── processed/ │ └── features/ ├── src/ │ ├── data/ │ ├── features/ │ ├── models/ │ └── service/ ├── configs/ ├── tests/ ├── notebooks/ └── dvc.yaml这个结构里data/下面分raw、processed、features三级对应数据处理的三个阶段。src/下面按层分目录每个目录里都有__init__.py和对应的测试文件。configs/放YAML配置比如特征列定义、模型超参、服务端口。notebooks/只用来做探索性分析不允许把生产代码写在Notebook里。我特别想强调configs/这个设计。很多项目把参数硬编码在代码里改个学习率要翻半天。这个项目要求所有可变参数都抽到YAML里代码只读配置。这样做的好处是实验可追溯。你跑一次训练配置文件一存就知道当时用了什么参数。配合DVC数据和模型版本也能对上。这套组合拳下来复现一个三个月前的实验结果不再是玄学。3. 核心模块拆解数据管道、特征工程与模型服务3.1 数据管道从原始文件到可训练数据集数据管道这块项目给了一个很实用的模式分阶段落盘。原始数据放在data/raw/不做任何修改。第一次清洗后的数据放data/processed/特征计算后的数据放data/features/。每个阶段都有一个独立的脚本比如src/data/clean.py、src/features/build.py。为什么强调分阶段落盘因为调试成本。如果你把清洗和特征计算写在一个脚本里每次改特征逻辑都要重新跑一遍清洗。数据量小还好数据量上TB的时候等半小时才能看到特征对不对效率极低。分阶段之后清洗跑一次结果存下来后面调特征只读processed数据秒级迭代。具体操作上项目推荐用pyarrow的Parquet格式存中间数据。相比CSVParquet的读取速度快5到10倍而且自带schema列类型不会乱。我实测过一个100万行、50列的数据集CSV读取要12秒Parquet只要1.8秒。代码大概长这样import pyarrow.parquet as pq import pandas as pd # 写入 df pd.read_csv(data/raw/orders.csv) df.to_parquet(data/processed/orders.parquet, indexFalse) # 读取 df pq.read_table(data/processed/orders.parquet).to_pandas()注意Parquet不支持原地修改每次都是全量重写。所以中间数据要按日期或版本分目录比如data/processed/2024-01-15/orders.parquet避免覆盖导致无法回滚。还有一个细节是数据校验。项目在src/data/validate.py里用pandera做schema检查比如“用户ID不能为空”、“订单金额必须大于0”。这个步骤放在清洗之后、特征之前。我踩过的坑是有一次上游数据源改了字段类型用户ID从字符串变成整数特征计算直接报错但报错信息指向的是特征脚本排查了半天才发现是数据源的问题。加了校验之后错误在数据层就暴露了定位时间从半小时降到两分钟。3.2 特征工程可复用、可测试、可版本化特征工程是AI工程里最“脏”的部分也是最能体现工程水平的地方。ai-engineering-from-scratch对特征工程的要求是每个特征都是一个纯函数。输入是原始DataFrame输出是新的DataFrame不依赖外部状态不修改输入数据。举个例子计算“用户过去7天平均订单金额”这个特征def avg_order_amount_7d(df: pd.DataFrame) - pd.DataFrame: df df.sort_values([user_id, order_date]) df[avg_order_amount_7d] ( df.groupby(user_id)[order_amount] .rolling(7D, onorder_date) .mean() .reset_index(level0, dropTrue) ) return df这个函数的好处是你可以单独给它构造一个小DataFrame做单元测试验证边界情况比如新用户没有历史订单时这个特征应该是NaN还是0。项目在tests/test_features.py里给了测试模板我建议每个特征至少写三个测试正常情况、空值情况、极端值情况。特征版本化是另一个关键点。项目用DVC来管理特征文件每次dvc add data/features/train.parquetDVC会生成一个.dvc文件记录哈希值。这样你切到某个历史commitdvc checkout就能拿到当时的特征数据。我自己的实践是特征定义变更时必须同时更新特征版本和模型版本否则会出现“用新特征训旧模型”的错配。实操心得特征命名要有规范比如{聚合方式}_{字段}_{时间窗口}像avg_order_amount_7d、sum_click_count_30d。这样一眼能看出特征含义也方便自动化生成特征文档。3.3 模型服务FastAPI 批处理 缓存服务层用FastAPI搭推理接口这个选择很务实。FastAPI自带OpenAPI文档前端或后端同事可以直接在/docs页面测试接口省去写接口文档的功夫。项目给的示例是一个简单的/predict接口from fastapi import FastAPI from pydantic import BaseModel import joblib app FastAPI() model joblib.load(models/model.pkl) class PredictRequest(BaseModel): features: list[float] app.post(/predict) def predict(req: PredictRequest): X [req.features] proba model.predict_proba(X)[0][1] return {score: proba}这个接口能跑但直接上生产有几个问题。第一模型加载在启动时完成如果模型文件很大启动会慢。第二没有批处理每次请求只推理一条吞吐量低。第三没有缓存相同输入重复计算。项目在进阶部分给了优化方案用lifespan事件异步加载模型用asyncio做批量推理用functools.lru_cache缓存高频请求。我实测下来单条推理延迟从15ms降到3ms批大小32QPS从200提到1500。具体做法是维护一个请求队列每10ms或队列满32条时触发一次批量推理。代码稍微复杂一点但性能提升明显。不过要注意批处理会增加尾延迟如果业务对P99延迟敏感批大小要调小。4. 实操全流程从零跑通一个AI服务4.1 环境准备与依赖安装项目推荐用conda创建独立环境Python版本3.9以上。依赖清单在requirements.txt里核心包包括pandas、pyarrow、scikit-learn、fastapi、uvicorn、dvc、pytest、pandera。安装命令conda create -n ai-eng python3.10 conda activate ai-eng pip install -r requirements.txt这里有个坑pyarrow和pandas的版本要匹配。我遇到过pandas 2.0配pyarrow 8.0读取Parquet时报ArrowInvalid错误。后来锁定pandas2.0, pyarrow14.0就正常了。建议在requirements.txt里写死大版本号避免自动升级导致不兼容。4.2 数据准备与清洗假设我们有一个电商订单数据集orders.csv字段包括order_id、user_id、order_date、order_amount、product_id。第一步是清洗python src/data/clean.py --input data/raw/orders.csv --output data/processed/orders.parquetclean.py做的事情去掉order_amount为负的行把order_date转成datetime类型去掉重复的order_id。清洗完用validate.py校验python src/data/validate.py --input data/processed/orders.parquet --schema configs/orders_schema.yamlorders_schema.yaml里定义每列的类型和约束columns: order_id: type: string nullable: false unique: true order_amount: type: float min: 0 order_date: type: datetime nullable: false这一步能拦住大部分数据质量问题。我建议把校验做成CI的一部分每次数据更新自动跑不通过就阻断下游。4.3 特征计算与数据集拆分特征计算脚本src/features/build.py会读取processed数据计算所有特征输出到data/features/。项目里给了几个基础特征用户历史订单数、平均订单金额、最近一次订单距今天数。你可以按业务需要加更多。计算完之后按时间拆分训练集和测试集。不要随机拆分因为时间序列数据随机拆会导致未来信息泄露。正确做法是取某个时间点之前的数据做训练之后的数据做测试cutoff pd.Timestamp(2024-01-01) train df[df[order_date] cutoff] test df[df[order_date] cutoff]拆分比例大概是7:3或8:2看数据量。如果数据量小可以用滚动窗口做交叉验证。4.4 模型训练与评估训练脚本src/models/train.py用scikit-learn的LogisticRegression或RandomForest做基线。项目强调先跑通基线再优化。很多人一上来就上深度学习结果调参调了两周效果还不如逻辑回归。基线模型的好处是训练快、可解释、容易部署。训练完输出两个文件models/model.pkl模型权重和models/metrics.json评估指标。评估指标至少包括AUC、准确率、召回率。如果是分类问题还要看混淆矩阵。项目在src/models/evaluate.py里给了可视化脚本生成ROC曲线和PR曲线。注意模型文件要用joblib而不是pickle因为joblib对numpy数组的序列化更高效大模型加载快30%左右。4.5 服务启动与接口测试服务启动命令uvicorn src.service.main:app --host 0.0.0.0 --port 8000 --workers 4--workers 4表示起4个进程利用多核CPU。如果是GPU推理workers设为1靠批处理提吞吐。启动后访问http://localhost:8000/docs能看到自动生成的接口文档。测试请求curl -X POST http://localhost:8000/predict \ -H Content-Type: application/json \ -d {features: [1.2, 0.5, 3.0, 2.1]}返回{score: 0.87}就说明服务通了。我建议再写一个locust压测脚本模拟100并发看P99延迟和错误率。如果P99超过200ms就要考虑加缓存或优化模型。5. 常见问题与排查技巧实录5.1 数据管道类问题问题一Parquet读取报schema不一致。原因通常是上游数据源改了列类型比如user_id从string变成int。排查方法是先用pyarrow.parquet.read_schema()看实际schema再对比configs/里的定义。解决方式是加一层类型转换或者在数据校验阶段直接拦截。问题二特征计算内存溢出。大数据集做groupby.rolling时pandas会生成中间副本内存翻倍。解决办法是用pyarrow的Table做分组聚合或者分块处理。我试过用dask替代pandas内存占用降了60%但代码复杂度上升。小数据集没必要。问题三DVC缓存冲突。多人协作时如果两个人同时dvc add同一个文件会产生哈希冲突。解决方法是每个人用独立分支合并前先dvc pull同步缓存。或者用远程存储如S3兼容的对象存储做共享缓存。5.2 模型服务类问题问题四FastAPI启动慢。如果模型文件几百MBjoblib.load要好几秒。解决办法是用lifespan异步加载启动时先返回健康检查通过模型在后台加载。或者用onnxruntime把模型转成ONNX格式加载速度提升明显。问题五推理结果不一致。训练时用pandas做特征服务时用numpy做特征浮点数精度差异导致结果偏差。解决办法是训练和服务共用同一套特征计算代码把特征函数抽到src/features/里两边都调用同一个函数。问题六并发请求下模型状态污染。如果模型有内部状态比如在线学习多线程会出问题。解决办法是每个worker加载独立模型副本或者用锁保护。但最好避免有状态的模型用无状态推理。5.3 排查速查表现象可能原因排查步骤解决方案服务启动报ModuleNotFoundError依赖未安装或环境不对pip list检查包重新安装requirements.txt推理延迟突然升高批处理队列积压看日志队列长度调大批大小或加worker模型AUC下降特征分布漂移对比训练和线上特征统计重新训练或加特征监控DVC推送失败远程存储配置错误dvc remote list检查存储凭证和网络数据校验不通过上游数据源变更看校验错误详情更新schema或修复数据源独家避坑技巧每次模型上线前用pytest跑一遍端到端测试从原始数据到推理结果全链路验证。我见过太多“训练时好好的上线就崩”的案例都是因为中间某个环节的假设变了。6. 工程化进阶监控、CI/CD与持续训练6.1 模型监控不只是看准确率模型上线不是终点而是起点。ai-engineering-from-scratch在进阶部分强调了数据漂移监控。具体做法是每天统计线上推理请求的特征分布和训练集分布做对比。如果某个特征的均值偏移超过阈值比如2个标准差就触发告警。实现上可以用evidently库它提供了现成的漂移检测报告。代码大概这样from evidently.report import Report from evidently.metric_preset import DataDriftPreset report Report(metrics[DataDriftPreset()]) report.run(reference_datatrain_df, current_dataonline_df) report.save_html(drift_report.html)除了数据漂移还要监控预测分布。如果模型输出的正例比例突然从10%跳到50%要么是业务变了要么是模型出问题了。我建议把预测分布和业务指标如点击率、转化率放在同一个看板上方便关联分析。6.2 CI/CD让每次变更都可回滚AI项目的CI/CD比普通软件复杂因为多了数据和模型两个版本。项目推荐的流程是代码提交触发CI跑单元测试和集成测试测试通过后触发训练流水线产出新模型新模型在影子模式下跑一段时间对比线上模型效果效果不降级才切换流量。影子模式是关键。新模型和旧模型同时接收请求但只有旧模型的结果返回给用户新模型的结果只记录不生效。跑一天后对比两个模型的指标如果新模型更好再切流量。这样能避免“上线即事故”。回滚策略也要提前设计。模型文件、特征配置、服务代码都要版本化。出问题时一键回滚到上一个稳定版本。我自己的经验是回滚时间要控制在5分钟以内否则故障影响面会扩大。6.3 持续训练什么时候该重训不是所有场景都需要持续训练。如果数据分布稳定模型半年重训一次就行。如果数据变化快比如新闻推荐、广告竞价可能需要每天重训。判断标准是模型效果衰减速度。你可以每周评估一次线上模型的AUC如果连续三周下降超过5%就该重训了。重训的触发方式有两种定时触发比如每天凌晨和事件触发比如漂移告警。定时触发简单可靠事件触发更及时。我建议两者结合定时跑基线重训漂移告警时加急重训。重训完的新模型还是要走影子模式和灰度发布。不要直接全量替换风险太大。灰度比例从1%开始逐步加到10%、50%、100%每一步观察至少一小时。7. 我踩过的坑和最后的小建议这个项目我前前后后复现了三遍第一遍跑通流程第二遍优化性能第三遍加监控和CI。踩过的坑不少挑几个最有代表性的说说。第一个坑是特征计算的时间窗口对齐。训练时用“过去7天”算特征服务时也用“过去7天”但训练数据是离线批处理服务是实时请求两者的“7天”边界可能差几个小时。解决办法是统一用UTC时间戳并且在特征定义里明确写清楚窗口的起止规则。第二个坑是模型文件的依赖版本。训练时用scikit-learn 1.3服务环境装的是1.4加载模型时报AttributeError。后来我把scikit-learn版本写死在requirements.txt里并且用pip freeze生成完整依赖清单确保训练和服务环境一致。第三个坑是日志格式不统一。数据层用print服务层用logging排查问题时日志散落在不同地方。后来统一用structlog输出JSON格式日志每条日志带trace_id从数据清洗到推理结果全链路可追踪。这个改动花了两天但后面排查问题的效率提升了好几倍。最后分享一个小技巧给每个实验打标签。用DVC的exp功能每次训练自动记录参数、指标和模型哈希。跑了几十次实验后你可以用dvc exp show对比所有实验快速找到最佳配置。这个习惯让我少走了很多“重复调参”的弯路。这个项目后续还可以往两个方向扩展一是加ONNX推理优化把延迟再压一半二是加Feature Store让特征在多个模型间复用。不过那是另一个话题了先把基础流程跑稳比什么都重要。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →