
1. 从零搭建AI工程能力为什么大多数人卡在“会调包”这一步“ai-engineering-from-scratch”这个标题第一次看到的时候我就觉得它戳中了一个很真实的痛点。现在网上讲AI的教程铺天盖地但绝大多数都在教你“怎么调用某个库”“怎么跑通某个Demo”真正从工程角度把一套AI系统从零搭起来的内容少得可怜。我自己带过几个刚入行的同学他们能熟练地写出model.fit()和model.predict()但一旦问到“这个模型上线后怎么处理并发请求”“训练数据怎么版本管理”“推理延迟怎么压到50毫秒以内”基本就答不上来了。这就是“从零做AI工程”和“从零学AI算法”之间的巨大鸿沟。算法层面你调包能跑出结果就算成功工程层面你要考虑的是数据管道、特征存储、模型服务、监控告警、灰度发布这一整条链路。我见过太多团队算法同学在Jupyter Notebook里把准确率刷到95%结果工程同学接手后花了三个月还没上线最后项目黄了。问题不出在模型本身出在工程能力上。这篇文章我想聊的就是怎么从零开始建立一套真正能落地的AI工程能力。不是教你调包而是教你理解每个环节为什么这么设计、有哪些坑、怎么用最小的成本跑通全流程。适合两类人看一类是算法背景想补工程短板的一类是后端背景想切入AI方向的。我会尽量用大白话把每个环节讲透该给代码给代码该给配置给配置让你看完能直接上手搭一套自己的东西。2. 先搞清楚AI工程到底包含哪些环节别一上来就写代码2.1 一张图看清AI工程的全链路很多人一提到AI工程脑子里第一反应就是“训练模型”。但实际上训练只是中间的一个环节前后还有大量工作要做。我习惯把整条链路拆成六个阶段数据获取与清洗从各种来源把数据捞回来去重、去噪、格式化特征工程与存储把原始数据加工成模型能吃的特征并且存好方便复用模型训练与调优选模型、调超参、做实验管理模型评估与验证不只看准确率还要看业务指标、公平性、鲁棒性模型部署与服务把模型变成API处理并发、做批处理、控制延迟监控与迭代上线后盯着数据漂移、模型衰减定期更新这六个阶段里真正跟“算法”强相关的只有第三和第四阶段剩下四个全是工程活。而实际项目中工程活占的时间往往超过70%。我做过一个统计在一个典型的推荐系统项目里数据清洗和特征工程花了将近两个月模型训练调参只花了两周部署和监控又花了一个月。所以如果你只盯着模型看项目进度根本推不动。2.2 为什么“从零”比“用现成平台”更值得学现在市面上有很多一站式AI平台点几下鼠标就能完成训练和部署。对于快速验证想法来说这些平台确实好用。但如果你想真正理解AI系统是怎么运转的我强烈建议你至少从零搭一次。原因很简单平台帮你屏蔽了所有细节一旦出问题你根本不知道从哪查。举个例子某次我用一个平台部署模型推理接口突然变慢从50毫秒涨到2秒。平台上什么日志都看不到只能干等客服。后来我自己用Flask加Gunicorn搭了一套同样的问题出现时我直接看Gunicorn的worker日志发现是某个请求触发了模型重新加载把内存打满了。定位问题只花了十分钟。这种排查能力只有你自己亲手搭过才能获得。另外从零搭建能让你对每个环节的成本有直观感受。比如你会知道一次全量训练要烧多少GPU小时一次特征回填要跑多久这些数字直接影响你的技术方案选型。用平台的话你只看到一个账单数字根本不知道钱花在哪了。2.3 最小可行AI工程栈的选型思路从零开始不等于什么都自己造。我的原则是核心逻辑自己写通用组件用成熟轮子。下面是我推荐的一套最小可行栈适合个人开发者和小团队环节推荐工具选择理由数据清洗Pandas PolarsPolars处理大数据集比Pandas快很多特征存储Feast或自己用Redis搭小规模用Redis足够大规模再上Feast实验管理MLflow开源、轻量、跟主流框架都兼容模型训练PyTorch或LightGBM深度学习用PyTorch表格数据用LightGBM模型服务FastAPI ONNX RuntimeFastAPI开发快ONNX Runtime推理快监控Prometheus Grafana生态成熟社区资源多这套栈的好处是每个组件都足够简单你能看懂它内部在干什么。等业务量上来了再逐步替换成更重的方案。我特别不建议一上来就上Kubernetes加TF Serving那一套光是环境配置就能耗掉你两周而且出了问题排查链路太长。3. 数据管道AI工程里最脏最累但最重要的部分3.1 数据清洗的常见陷阱与处理策略数据清洗这件事说起来简单做起来恶心。我总结下来新手最容易踩的坑有三个重复数据、缺失值处理不当、类别不平衡。重复数据的问题比想象中严重。有一次我做一个文本分类任务训练集准确率冲到99%我差点以为模型成神了。结果一查训练集和验证集之间有大量重复样本模型等于在背答案。后来我写了一个基于SimHash的去重脚本把重复率从30%降到2%准确率回落到87%这才是真实水平。去重的代码不复杂from datasketch import MinHash, MinHashLSH def deduplicate(texts, threshold0.8): lsh MinHashLSH(thresholdthreshold, num_perm128) minhashes {} for i, text in enumerate(texts): m MinHash(num_perm128) for word in text.split(): m.update(word.encode(utf8)) lsh.insert(fdoc_{i}, m) minhashes[fdoc_{i}] m unique_ids set() for key, m in minhashes.items(): result lsh.query(m) if len(result) 1: unique_ids.add(key) return [texts[int(k.split(_)[1])] for k in unique_ids]缺失值处理也有讲究。数值型特征直接填0或者均值是常见做法但如果缺失本身有信息量就应该把“是否缺失”作为一个单独的特征。比如用户年龄缺失可能意味着这个用户是新注册的这个信息比年龄本身还有用。类别型特征缺失我一般单独设一个“unknown”类别而不是用众数填充。类别不平衡是另一个大坑。正样本只有1%的时候模型全预测负样本也能有99%的准确率但这个模型毫无用处。处理方式有几种过采样少数类、欠采样多数类、调整损失函数权重。我一般先用class_weightbalanced试试效果不好再上SMOTE过采样。但要注意过采样一定要在训练集上做验证集和测试集必须保持原始分布否则评估结果会严重偏乐观。3.2 特征存储为什么你的模型上线后效果会打折特征存储这个概念很多做算法的人不重视但它恰恰是训练和推理一致性的关键。我见过一个真实案例训练时用Pandas算了一个“用户过去7天平均下单金额”的特征上线时工程同学用SQL重新实现了一遍结果SQL里用了BETWEEN导致边界条件跟Pandas不一致线上效果直接掉了5个百分点。解决这个问题的核心思路是训练和推理用同一套特征计算逻辑。小规模场景下我会把特征计算逻辑封装成一个Python函数训练时批量调用推理时单条调用。比如class FeatureComputer: def compute_user_avg_amount(self, user_id, orders): if not orders: return 0.0 return sum(o[amount] for o in orders) / len(orders) def compute_features(self, user_id, orders, context): return { user_avg_amount: self.compute_user_avg_amount(user_id, orders), user_order_count: len(orders), context_hour: context[hour], }训练时把历史数据按用户分组批量算推理时实时算。这样逻辑完全一致不会出现偏差。如果特征计算特别复杂、耗时很长那就需要引入离线特征存储加在线特征缓存的两级架构。离线用Hive或Spark算好存到Redis在线直接查Redis。但这时候要特别注意特征的新鲜度离线特征通常T1更新如果业务对实时性要求高就得做实时特征管道。3.3 数据版本管理别让“数据变了”成为玄学问题数据版本管理是另一个容易被忽视的环节。模型效果突然下降你怀疑是数据问题但如果没有版本记录你连“上周的数据长什么样”都不知道排查就变成了玄学。我的做法很简单每次训练前把训练数据的元信息记录到MLflow里包括数据路径、样本数量、特征列表、时间范围、MD5校验和。这样任何时候都能回溯到具体是哪份数据训出了哪个模型。如果数据存在对象存储上可以用路径加时间戳的方式做版本比如s3://bucket/training_data/2024-01-15/。每次训练脚本自动读取最新日期的数据同时把路径写进实验记录。对于特征本身如果特征计算逻辑改了一定要升版本号。比如user_avg_amount_v1改成user_avg_amount_v2不要直接覆盖。这样旧模型还能继续用旧特征新模型用新特征互不干扰。等旧模型全部下线了再清理旧特征。4. 模型训练与实验管理让每一次尝试都可追溯4.1 实验管理不是可选项是必选项我刚开始做AI项目的时候最头疼的就是“上次那个效果好的参数是什么来着”。那时候用Excel记记着记着就乱了。后来用了MLflow整个世界清净了。MLflow的核心概念就三个实验、运行、指标。一个实验对应一个任务比如“用户流失预测”一次运行对应一次训练记录超参、指标、模型文件指标就是准确率、AUC这些。用起来特别简单几行代码就能接入import mlflow import mlflow.sklearn mlflow.set_experiment(churn_prediction) with mlflow.start_run(): mlflow.log_param(learning_rate, 0.01) mlflow.log_param(max_depth, 6) model train_model(X_train, y_train, lr0.01, depth6) auc evaluate(model, X_val, y_val) mlflow.log_metric(val_auc, auc) mlflow.sklearn.log_model(model, model)跑完在MLflow UI里就能看到所有运行记录按AUC排序点进去看具体参数。更关键的是模型文件也存下来了随时能加载回来做推理。我现在的习惯是任何一次训练都必须走MLflow哪怕只是改了一个小参数。因为很多时候你以为没用的那次尝试过两周突然发现它才是最优解。4.2 超参搜索的实用策略别一上来就网格搜索超参搜索的方法有很多网格搜索、随机搜索、贝叶斯优化。我的经验是先用随机搜索快速摸清大致范围再用贝叶斯优化精细调优。网格搜索看起来全面但维度一高就爆炸。比如5个参数各取5个值就是3125次训练根本跑不完。随机搜索的好处是在同样的计算预算下它能覆盖更大的参数空间。我一般会跑50到100次随机搜索看看哪些参数组合的指标比较好。然后在这个基础上用Optuna做贝叶斯优化通常再跑30到50次就能找到不错的配置。import optuna def objective(trial): lr trial.suggest_float(lr, 1e-4, 1e-1, logTrue) depth trial.suggest_int(depth, 3, 12) subsample trial.suggest_float(subsample, 0.5, 1.0) model train_model(X_train, y_train, lrlr, depthdepth, subsamplesubsample) return evaluate(model, X_val, y_val) study optuna.create_study(directionmaximize) study.optimize(objective, n_trials50)这里有个小技巧把每次trial的结果也记到MLflow里这样Optuna的可视化加上MLflow的记录整个调参过程就非常清晰了。另外如果计算资源有限可以用早停策略验证集指标连续几轮不提升就中断训练省下来的时间跑更多trial。4.3 训练加速的几种低成本手段不是每个人都有A100集群大部分时候我们只有一张消费级显卡甚至CPU。这种情况下训练加速就很重要了。我总结了几种低成本手段混合精度训练是最容易见效的。PyTorch里加几行代码就能开启速度能提升30%到50%显存占用还减半from torch.cuda.amp import autocast, GradScaler scaler GradScaler() for batch in dataloader: optimizer.zero_grad() with autocast(): output model(batch) loss criterion(output, target) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()梯度累积适合显存不够的情况。把batch size设小一点累积几次梯度再更新一次参数效果等价于大batchaccumulation_steps 4 for i, batch in enumerate(dataloader): loss model(batch) / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()数据加载优化也经常被忽略。把num_workers设成CPU核心数开启pin_memory用prefetch_factor预取数据这些都能显著减少GPU等待时间。我实测下来光是把num_workers从0改成8训练速度就快了将近一倍。5. 模型部署从Notebook到生产环境的惊险一跃5.1 为什么你的模型API响应这么慢模型部署最常被吐槽的就是延迟高。一个在Notebook里推理只要10毫秒的模型部署成API后变成500毫秒用户根本没法用。问题通常出在几个地方模型加载方式不对。很多人把模型加载写在请求处理函数里每次请求都重新加载一遍模型。这是灾难性的。正确的做法是在服务启动时加载一次全局复用。用FastAPI的话可以放在startup事件里from fastapi import FastAPI import onnxruntime as ort app FastAPI() model_session None app.on_event(startup) def load_model(): global model_session model_session ort.InferenceSession(model.onnx) app.post(/predict) def predict(features: dict): input_array preprocess(features) output model_session.run(None, {input: input_array}) return {prediction: output[0].tolist()}预处理和后处理太重。有时候模型推理只要5毫秒但特征预处理花了200毫秒。这时候要把预处理逻辑也优化比如用NumPy向量化操作代替循环用Cython或Numba加速热点代码。并发模型选错了。Python有GIL多线程对CPU密集型任务没用。如果推理是CPU密集的应该用多进程比如Gunicorn配多个worker。如果推理是IO密集的比如要查数据库那多线程或者异步IO更合适。我一般用Gunicorn加Uvicorn workerworker数量设成CPU核心数的2到4倍。5.2 模型格式转换ONNX为什么能提速PyTorch模型直接部署推理速度往往不理想。转成ONNX格式后用ONNX Runtime推理速度通常能提升2到5倍。原因是ONNX Runtime做了大量图优化比如算子融合、常量折叠、内存复用。转换过程也不复杂import torch import torch.onnx model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, opset_version13 )这里有几个坑要注意dynamic_axes一定要设否则batch size被固定成1没法做批处理。opset_version不要设太高有些推理环境不支持最新版本。导出后一定要用ONNX Runtime跑一遍对比PyTorch的输出确保数值误差在可接受范围内。5.3 批处理与动态批处理的取舍批处理能大幅提升吞吐量但会增加单次请求的延迟。因为要等凑够一个batch才能推理。这个取舍取决于业务场景。如果是离线任务批处理越大越好如果是在线服务就要做动态批处理设置一个最大等待时间比如10毫秒超时了就用当前已有的请求组成一个batch。动态批处理的实现可以用一个队列加一个后台线程import queue import threading import time request_queue queue.Queue() BATCH_SIZE 32 MAX_WAIT 0.01 def batch_worker(): while True: batch [] start time.time() while len(batch) BATCH_SIZE and (time.time() - start) MAX_WAIT: try: item request_queue.get(timeout0.001) batch.append(item) except queue.Empty: continue if batch: inputs [item[input] for item in batch] outputs model_session.run(None, {input: inputs}) for item, output in zip(batch, outputs[0]): item[future].set_result(output) threading.Thread(targetbatch_worker, daemonTrue).start()这样既能利用批处理的吞吐优势又不会让单个请求等太久。实测下来QPS能从50提升到300以上而P99延迟只增加了不到20毫秒。6. 上线后的监控与迭代模型不是部署完就没事了6.1 数据漂移检测模型效果下降的早期信号模型上线后效果会慢慢衰减这是必然的。因为训练数据是历史数据而线上数据分布会随时间变化。数据漂移检测就是要在效果明显下降之前发出预警。最简单的检测方法是监控输入特征的统计量。比如训练时用户平均年龄是30岁线上跑了一个月变成35岁这就是漂移信号。我会对每个数值特征计算均值、方差、分位数对类别特征计算各取值占比然后跟训练时的基准做对比。偏差超过阈值就告警。import numpy as np def detect_drift(train_stats, online_data, threshold0.1): drift_scores {} for feature in train_stats: train_mean train_stats[feature][mean] online_mean np.mean(online_data[feature]) relative_change abs(online_mean - train_mean) / (abs(train_mean) 1e-8) drift_scores[feature] relative_change if relative_change threshold: print(f警告特征 {feature} 发生漂移变化幅度 {relative_change:.2%}) return drift_scores除了特征漂移还要监控预测结果的分布。如果模型突然把90%的样本都预测成正类而训练时只有10%那肯定有问题。这种监控用Prometheus加Grafana做可视化特别方便每天看一眼仪表盘就能掌握模型状态。6.2 模型重训练的触发条件与自动化模型重训练不能拍脑袋决定要有明确的触发条件。我一般设三个定时触发比如每周日凌晨自动跑一次全量训练保证模型不过期漂移触发数据漂移分数超过阈值时触发效果触发线上AUC或业务指标连续三天低于基线时触发自动化重训练的关键是训练管道要可复现。我会把整个训练流程写成一个脚本输入是数据路径和配置参数输出是模型文件和评估报告。然后用Airflow或简单的Cron定时调度。训练完成后自动跑评估如果新模型比旧模型好就推送到模型仓库等待人工审核后上线。#!/bin/bash # retrain.sh DATA_PATH$(date %Y-%m-%d) python train.py --data_path s3://bucket/data/$DATA_PATH --config config.yaml python evaluate.py --model_path output/model.pkl --baseline_auc 0.85 if [ $? -eq 0 ]; then aws s3 cp output/model.pkl s3://bucket/models/$(date %Y%m%d)/ fi这里有个经验新模型上线前一定要做影子模式测试。就是让新模型跟旧模型同时跑但新模型的结果不返回给用户只记录日志。对比一周的数据确认新模型确实更好再切换。我见过太多次新模型离线指标好但线上效果差的情况影子模式能避免这种翻车。6.3 反馈闭环让线上数据反哺训练AI系统跟传统软件最大的区别是它需要持续的数据反馈来迭代。线上用户的每一次点击、每一次购买、每一次停留都是宝贵的训练数据。建立反馈闭环的关键是把线上行为日志跟模型预测关联起来。具体做法是每次推理时生成一个唯一的request_id把request_id、输入特征、模型版本、预测结果都记到日志里。用户产生行为后把行为跟request_id关联形成一条完整的训练样本。这样积累一段时间后就能用线上真实数据做增量训练。import uuid import json def log_prediction(features, prediction, model_version): request_id str(uuid.uuid4()) log_entry { request_id: request_id, features: features, prediction: prediction, model_version: model_version, timestamp: time.time() } with open(prediction_log.jsonl, a) as f: f.write(json.dumps(log_entry) \n) return request_id def log_feedback(request_id, label): feedback_entry { request_id: request_id, label: label, timestamp: time.time() } with open(feedback_log.jsonl, a) as f: f.write(json.dumps(feedback_entry) \n)这个闭环建起来之后模型迭代速度会快很多。以前可能要等一个月才能收集到足够的标注数据现在一周就能用线上反馈训一版新模型。而且线上数据分布跟实际业务完全一致训出来的模型效果更稳。7. 一些踩坑之后的真心话做AI工程这几年踩过的坑比写过的代码还多。有几个教训我觉得特别值得分享。第一不要过早优化。我刚开始的时候总想着一步到位搭一套完美的架构结果光设计就花了两周代码一行没写。后来学乖了先用最土的办法跑通全流程哪怕是用Flask单进程加SQLite只要能跑起来就行。跑通之后再逐步替换瓶颈环节。这样你至少有一个能工作的系统而不是一堆设计文档。第二日志和监控要提前做。不要等出了问题才想起来加日志。我现在的习惯是任何新服务上线前先把关键路径的日志和核心指标的监控配好。哪怕只是打印请求耗时和模型版本出问题的时候也能救命。有一次线上模型效果突然下降就是靠日志发现某个特征的值全是0追查下去发现是上游数据源改了字段名。第三版本管理要严格。代码用Git数据用路径版本模型用MLflow配置用YAML文件加版本号。任何变更都要有记录。我吃过亏有一次手动改了一行配置没记录后来怎么都复现不出之前的实验结果白白浪费了一周。第四测试要覆盖边界情况。模型对空输入、超长输入、异常值输入的处理一定要写测试用例。我见过模型因为输入了一个空字符串直接崩溃导致整个服务不可用。后来我养成了习惯每个预处理函数都加边界检查空值、超长、非法字符都要有兜底逻辑。第五别忽视文档。不是写给别人的是写给三个月后的自己的。我现在每个项目都会维护一个README记录架构图、部署步骤、常见问题、关键决策的原因。三个月后回头看能省下大量回忆的时间。这套从零搭建AI工程能力的思路我自己走了一遍也带着几个同学走了一遍。最大的感受是工程能力不是看几篇文章就能学会的必须亲手搭、亲手踩坑、亲手修。但一旦搭起来一套后面再做新项目就是复制粘贴加微调效率会高很多。希望这些经验能帮你少走点弯路。