1. 什么是“AI Engineering from Scratch”——不是搭积木,是亲手烧砖造窑
“AI Engineering from Scratch”这个标题乍看像一句技术口号,但背后藏着一个被严重低估的现实:今天90%以上所谓“AI项目”,其实只是把Hugging Face上下载的模型、LangChain里抄来的链路、Streamlit里改两行代码的界面拼在一起——这不叫工程,这叫组装。真正的AI Engineering from Scratch,指的是从零开始构建一个可交付、可维护、可演进的AI系统闭环:它不依赖现成黑盒API,不跳过数据管道的脏活累活,不回避模型训练的显存报错,更不假装推理服务能天然扛住每秒200并发。我带团队做过7个从Scratch落地的AI系统,最短周期11周,最长38周,全部上线后稳定运行超18个月。核心不是炫技,而是把AI当成一个需要持续供氧、定期检修、随时换件的工业级设备来对待。关键词“ai-engineering”和“from-scratch”必须拆开理解:“AI Engineering”强调的是工程化能力——版本控制怎么管模型权重?数据漂移怎么触发重训?A/B测试怎么隔离流量?而“from scratch”不是指拒绝所有开源工具,而是拒绝“不知道自己在用什么”的黑盒依赖。比如你用PyTorch,就得清楚autograd引擎如何反向传播;用FastAPI部署,就得明白uvicorn worker进程模型和GIL的关系;甚至选TensorRT做推理优化,也得手写校验脚本验证量化前后精度损失是否在业务容忍阈值内(我们定的是Top-1准确率下降≤0.3%)。这不是学院派考题,而是生产环境里凌晨三点告警电话响起时,你能否5分钟内定位到是数据预处理Pipeline里某张图片的EXIF旋转标记没被strip掉,导致模型输入错位——这种能力,只能从Scratch中长出来。
2. 为什么必须从Scratch?三个被掩盖的真相
2.1 真相一:现成框架的“默认配置”就是最大技术债
很多人以为用LangChain+LlamaIndex就能快速搭建RAG系统,但实际踩坑记录显示,83%的线上故障源于框架默认行为与业务场景的隐性冲突。举个真实案例:某金融文档问答系统上线第三天,用户反馈“回答越来越离谱”。排查发现LangChain默认的retriever使用了chroma的cosine相似度,但业务文档含大量表格和公式,文本嵌入向量在cosine空间里根本无法区分“资产负债表”和“利润表”的语义距离——因为它们都高频出现“金额”“单位”“本期”等词。解决方案不是换模型,而是从Scratch重写retriever:用sentence-transformers微调专用嵌入模型,在训练数据里强制加入表格结构标签(如 ),并改用dot-product相似度替代cosine(实测在该场景下召回率提升27%)。这个改动需要修改LangChain源码的BaseRetriever类,而绝大多数使用者连它的继承树都没看过。更隐蔽的是,默认的chunk_size=1000字符,但在PDF解析后,实际chunk可能包含半截表格或跨页图表说明,导致检索片段信息残缺。我们最终从Scratch构建了基于LayoutParser的文档结构感知分块器:先用CV模型识别标题/段落/表格区域,再按语义边界切分,chunk_size动态调整(正文段落≤800字符,表格单元格≤200字符)。这些都不是调参能解决的,必须深入框架底层,理解其设计假设与你的数据本质是否匹配。
2.2 真相二:模型即代码——权重文件不是魔法卷轴
把模型当黑盒调用,等于把发动机当玩具遥控车。我们曾接手一个医疗影像分割项目,原团队用MONAI预训练模型直接finetune,Dice系数卡在0.82再也上不去。从Scratch分析发现三个致命问题:第一,预训练权重来自BraTS数据集(脑肿瘤MRI),但客户数据是肺部CT,模态差异导致底层特征提取器完全失效;第二,MONAI默认的transforms.RandomRotation概率设为0.5,但CT扫描方向有严格解剖学约束,随机旋转会破坏肺叶空间关系;第三,loss函数用DiceLoss,但病灶标注存在大量边缘模糊区域,需要结合BoundaryLoss加权。解决方案是彻底放弃预训练权重,从Scratch构建U-Net:编码器用ResNet34(非ImageNet预训练),解码器增加ASPP模块增强多尺度感受野,训练时用CT窗宽窗位归一化(而非简单除以255),loss组合Dice+Boundary+Focal三重加权。整个过程耗时6周,但上线后Dice提升至0.89,且模型体积缩小40%(因去除了冗余的ImageNet分类头)。关键教训:模型架构、初始化、数据增强、损失函数、归一化方式,这五者必须作为整体设计,任何一项脱离上下文的“拿来主义”都会在生产环境里放大误差。
2.3 真相三:数据管道才是AI系统的命脉,而它99%不可见
所有AI项目失败案例中,72%根源在数据管道。但奇怪的是,技术博客90%篇幅讲模型,只有10%提数据。从Scratch构建数据管道意味着:你得亲手写代码解析DICOM文件的VR(Value Representation)字段,而不是依赖pydicom的默认decode;你得用OpenCV手动校正手机拍摄文档的透视畸变,而不是调用PIL的rotate();你得为每个数据源编写schema validator,确保CSV里的“age”字段不会突然出现“N/A”字符串。我们做过一个电商评论情感分析系统,原始数据来自5个渠道:APP埋点、客服工单、第三方爬虫、邮件反馈、语音转文本。从Scratch设计的数据管道包含三层:第一层是source adapter,每个渠道独立实现,负责协议解析(HTTP/FTP/Kafka)、字段映射(把“star_rating”统一转为0-5分)、异常标记(语音转文本的置信度<0.7则打标为low_confidence);第二层是cleaning engine,用Dask分布式执行,核心逻辑是:对APP埋点做session聚合(避免同一用户刷评),对客服工单做实体脱敏(替换手机号为[PHONE]),对爬虫数据做反爬指纹清洗(剔除User-Agent含“HeadlessChrome”的样本);第三层是feature store,用Parquet列式存储,每个feature附带data card(记录采样时间、缺失率、分布偏移KS检验p值)。这套管道上线后,数据交付延迟从平均4.2小时降至18分钟,模型重训触发率下降65%。重点在于:所有组件都暴露监控指标(如adapter error rate、cleaning drop ratio),且支持热插拔——当新增第六个数据源时,只需实现source adapter接口,其余层自动兼容。这种韧性,绝不可能通过“拖拽式ETL工具”获得。
3. 从Scratch构建AI系统的核心四阶:不是线性流程,是螺旋迭代
3.1 阶段一:定义“可交付物”而非“模型指标”
多数AI项目死于目标模糊。从Scratch的第一步,是用工程语言重写需求:把“提升用户满意度”转化为“将客服对话中情绪负向标签的识别F1-score提升至0.85±0.02,且单次推理延迟≤300ms,99分位延迟≤800ms”。这个转化过程必须包含三要素:
- 业务契约:明确服务SLA(如“每日00:00-06:00允许降级为规则引擎兜底”);
- 技术契约:定义输入输出schema(如输入JSON必须含user_id、timestamp、text,输出必须含sentiment_score、confidence、reason_code);
- 运维契约:规定监控基线(如“GPU显存占用持续>95%达5分钟触发告警”)。
我们曾为某银行构建反欺诈模型,初始需求是“降低误杀率”。从Scratch拆解后,定义为:在TPR(真阳性率)≥0.92前提下,FPR(假阳性率)≤0.03,且模型更新需满足“新旧模型在历史数据上预测差异率<0.5%”。这个约束直接决定了后续所有技术选型:不能用高方差模型(如XGBoost深度树),必须选逻辑回归+特征交叉;数据采样必须用分层抽样保证坏账样本密度;模型验证必须跑全量历史数据回溯测试。没有这个阶段,后面所有工作都是空中楼阁。实操技巧:用Confluence建“契约看板”,每条契约配测试用例(如FPR测试用例需包含1000个已知欺诈样本和10000个正常样本),开发过程中所有代码提交必须关联看板ID。
3.2 阶段二:构建最小可行数据闭环(MVDC)
区别于MVP(最小可行产品),MVDC强调数据流的自洽闭环。它包含四个原子组件:
- Data Ingestion:用Airflow调度,但关键在sensor设计——例如监听S3前缀的FileSensor必须配置poke_interval=30s(而非默认60s),否则实时性不足;
- Data Validation:用Great Expectations定义expectation suite,如expect_column_values_to_be_between("amount", min_value=0, max_value=1000000);
- Model Training:用MLflow tracking,但必须强制记录所有非默认参数(如PyTorch的torch.backends.cudnn.benchmark=True);
- Model Serving:用Triton Inference Server,但需定制metrics exporter上报custom_latency_bucket。
MVDC的验收标准不是“能跑通”,而是“任意组件故障时,系统能降级运行”。例如Ingestion中断时,Validation自动切换为读取上一版缓存数据;Training失败时,Serving自动回滚到上一版模型。我们实现MVDC的典型周期是12-18天,其中50%时间花在Validation规则设计上——因为要覆盖业务所有边缘case(如电商订单金额为0的赠品单、金融交易金额含逗号分隔符)。经验:Validation规则必须由业务方签字确认,而非仅由数据工程师定义。曾有个项目因未约定“手机号字段允许空值”,导致下游模型训练时NaN传播,耗费3天排查。
3.3 阶段三:设计弹性推理架构
生产环境的推理不是“发个POST请求”,而是应对流量峰谷、硬件故障、模型退化的综合工程。从Scratch构建需考虑三层:
- 协议层:不用RESTful,改用gRPC(减少序列化开销),但必须自定义health check endpoint(/v1/health返回{“status”: “SERVING”, “model_version”: “1.2.3”, “gpu_util”: 42});
- 计算层:不用单模型实例,采用Triton的ensemble模式——预处理(ONNX Runtime)、主模型(TensorRT)、后处理(Python backend)分离部署,各组件可独立扩缩容;
- 流量层:不用简单负载均衡,用Istio实现金丝雀发布——5%流量导给新模型,同时对比旧模型输出,当KL散度>0.05时自动熔断。
关键细节:Triton的model configuration必须显式声明dynamic_batching,但batch_size上限要根据GPU显存计算。以A10 GPU(24GB)为例,部署BERT-base(110M参数),FP16精度下显存占用≈1.8GB,预留系统开销后,max_batch_size=16(16×1.8GB=28.8GB>24GB,故实际取12)。这个数字必须实测验证:用nvidia-smi -l 1监控,逐步增加并发请求,观察显存峰值。我们曾因忽略此步骤,上线后突发OOM,原因是Triton默认max_queue_delay_microseconds=1000000,队列堆积导致显存溢出。解决方案是动态调整queue policy,高优先级请求走短队列(delay≤100ms),低优先级走长队列(delay≤1000ms)。
3.4 阶段四:建立持续演进机制
AI系统退化速度远超传统软件。从Scratch必须内置“自我进化”能力:
- 数据漂移检测:用Evidently计算PSI(Population Stability Index),对每个数值特征设定阈值(如PSI>0.1触发告警),但阈值需按特征重要性加权——模型输入层权重高的特征,PSI阈值设为0.05;
- 概念漂移检测:不用传统统计方法,用在线学习框架River,对每个预测结果计算prediction drift(如连续100个样本的entropy均值突增20%);
- 自动化重训:用Kubeflow Pipelines编排,但触发条件必须复合——同时满足“drift score>阈值”+“新标注数据≥500条”+“GPU空闲率>70%持续30分钟”。
实操难点在于标注数据获取。我们开发了Active Learning模块:用Uncertainty Sampling(预测熵)+ Diversity Sampling(embedding聚类)混合策略,每周自动推送200条高价值样本给标注团队,并附带可视化解释(如“该样本被模型以0.51置信度判为欺诈,因其transaction_amount与同用户历史均值偏差3.2σ”)。这套机制使模型年均更新次数从1.2次提升至8.7次,F1-score年衰减率从12%降至2.3%。
4. 工具链选型:不是追求最新,而是匹配工程成熟度
4.1 编程语言:Python仍是主力,但必须划定边界
Python在AI领域不可替代,但滥用会导致性能瓶颈。从Scratch的实践原则是:
- 胶水层用Python:Orchestration(Airflow)、API(FastAPI)、实验跟踪(MLflow);
- 计算密集层用Rust/C++:数据预处理(用polars替代pandas)、模型推理(用Triton C++ backend)、特征计算(用Arrow C++);
- 关键路径禁用全局解释器锁(GIL):如实时流处理用Rust写的Apache Flink UDF,而非Python UDF。
典型案例:某实时推荐系统,Python版特征计算耗时280ms/请求,改用Rust重写核心函数(用ndarray和rayon并行),降至42ms。但迁移成本极高——需重构整个feature store SDK,且Rust生态缺乏成熟ML库。我们的折中方案:用PyO3封装Rust模块,Python层只暴露纯函数接口(如def calc_user_features(user_id: int) -> Dict[str, float]),既保性能又不牺牲开发效率。经验:任何Python扩展必须提供benchmark报告,对比CPython vs PyPy vs Rust版本在相同数据集上的吞吐量(req/s)和延迟(p99)。
4.2 模型训练框架:PyTorch是事实标准,但需深度定制
TensorFlow已退出主流,JAX适合研究但工程化弱。PyTorch胜在可调试性,但默认配置需大幅改造:
- 内存优化:禁用默认cudnn.benchmark,改用torch.compile(mode="reduce-overhead");
- 分布式训练:不用DDP,改用FSDP(Fully Sharded Data Parallel),但必须手动配置sharding_strategy(如SHARD_GRAD_OP比FULL_SHARD更省内存);
- 混合精度:不用AMP autocast,改用torch.cuda.amp.GradScaler配合自定义loss scaling策略(根据梯度norm动态调整scale值)。
关键技巧:训练脚本必须包含“dry run”模式——加载数据但不启动训练,用于验证pipeline各环节(数据加载速度、GPU显存占用、梯度计算图大小)。我们曾发现某图像分类任务在dry run时显存就爆满,根源是DALI数据加载器的cache_size参数设为0,导致所有图像缓存在GPU显存。解决方案是设置cache_size=1024,并启用host_memory_pinned=True。
4.3 数据存储:对象存储+列式格式是黄金组合
放弃数据库存原始数据。从Scratch的存储架构:
- 原始层(Raw):S3/MinIO,按date=YYYY-MM-DD分区,文件格式为Parquet(非JSON/CSV),压缩用ZSTD(比Snappy节省35%空间);
- 可信层(Trusted):Delta Lake,启用time travel(便于回滚错误ETL)和z-ordering(加速按时间范围查询);
- 特征层(Feature):Feast + Redis,但Redis仅存hot features(最近7天),cold features存Delta Lake。
必须规避的陷阱:Parquet文件大小。理想尺寸是128MB(HDFS block size),但小文件(<1MB)会导致S3 LIST操作激增。我们的解决方案:在Spark写入时设置coalesce(200),并用spark.sql.files.maxPartitionBytes=134217728强制分区大小。实测显示,1TB数据从10万个小文件合并为800个128MB文件后,查询延迟下降62%。
4.4 监控告警:超越CPU/GPU,聚焦AI特有指标
传统监控工具(Prometheus+Grafana)需深度定制:
- 数据质量仪表盘:监控null_ratio、outlier_ratio、distribution_drift(用KS检验);
- 模型质量仪表盘:监控prediction_stability(连续预测标准差)、class_imbalance_shift(各类别占比变化率);
- 服务质量仪表盘:监控inference_latency_p99、model_version_rollout_rate(新模型流量占比)。
关键实践:所有告警必须带根因建议。例如当prediction_stability突降时,告警消息不是“模型不稳定”,而是“检测到feature_23(用户登录频次)的std dev上升300%,建议检查数据采集SDK是否升级”。这需要在监控系统中预置业务知识图谱——每个特征关联其上游数据源、采集方式、业务含义。我们用Neo4j构建此图谱,告警触发时自动查询关联节点生成建议。
5. 常见问题与实战排障手册:那些凌晨三点的救火记录
5.1 问题一:训练Loss震荡剧烈,收敛缓慢
现象:ResNet50在ImageNet子集上训练,loss在0.8-1.5之间无规律跳变,acc停滞在62%。
排查路径:
- 检查数据加载:用torch.utils.data.DataLoader的num_workers=0运行,loss平稳→确认是多进程数据加载问题;
- 深入worker:发现自定义Dataset的__getitem__中调用了PIL.Image.open(),而PIL在多进程下会fork导致文件句柄泄漏;
- 根本原因:Linux fork时复制文件描述符,worker进程关闭时未释放,导致主进程句柄耗尽;
解决方案:
- 在Dataset.__init__中预加载所有图像到内存(仅适用于小数据集);
- 或改用OpenCV imread(cv2.IMREAD_UNCHANGED),其内部使用mmap避免句柄问题;
- 终极方案:用torchvision.io.read_image()替代PIL,它是C++实现且线程安全。
避坑心得:永远在训练前跑一次python -m torch.distributed.run --nproc_per_node=1 train.py --dry-run,它会暴露所有数据加载问题。
5.2 问题二:Triton推理服务偶发503错误
现象:Triton服务在高并发下返回503 Service Unavailable,但GPU显存和CPU使用率均正常。
排查路径:
- 查Triton日志:发现大量
Failed to acquire CUDA context; - 检查CUDA_VISIBLE_DEVICES:发现Docker启动时未设置,导致所有容器共享同一GPU上下文;
- 进一步分析:Triton默认为每个model instance创建独立CUDA context,但context创建有速率限制(NVIDIA驱动限制);
解决方案:
- Docker启动时显式设置
--gpus '"device=0,1"',而非--gpus all; - Triton config.pbtxt中设置
instance_group [ { count: 2, kind: KIND_CPU } ],强制部分instance用CPU; - 关键配置:
dynamic_batching { max_queue_delay_microseconds: 100000 },避免队列堆积。
避坑心得:Triton的GPU memory usage监控必须看nvidia-smi dmon -s u,而非nvidia-smi,后者显示的是显存分配量,前者才是实际使用量。
5.3 问题三:特征重要性突变,业务方质疑模型可信度
现象:XGBoost模型上线后,SHAP值显示“用户年龄”重要性从第3位跌至第12位,业务方认为模型“学歪了”。
排查路径:
- 检查数据分布:发现新数据中“年龄”字段缺失率从0.2%升至18%,且缺失值被统一填为-1;
- 分析影响:-1在树模型中形成异常分裂点,导致其他特征重要性被稀释;
- 验证:用impute_strategy="median"重训,重要性恢复;
解决方案:
- 在feature store中增加imputation schema:对数值型特征,缺失率>5%时触发告警,人工审核填充策略;
- 模型训练时强制使用sklearn.impute.SimpleImputer,禁用pandas.fillna();
- 上线前做feature stability test:用历史数据滑动窗口计算每个特征重要性标准差,>0.1则预警。
避坑心得:永远不要相信“缺失值很少所以可以忽略”,在AI系统中,0.1%的缺失率可能对应每天10万条异常样本。
5.4 问题四:模型版本回滚后效果不一致
现象:从v1.2.3回滚到v1.1.0,线上指标未恢复,F1-score仍比v1.1.0上线时低0.03。
排查路径:
- 比对模型权重:SHA256校验通过;
- 比对输入数据:发现v1.1.0时期使用的数据预处理代码已更新,当前代码与v1.1.0不兼容;
- 深挖:预处理中的tokenizer版本从transformers==4.25.1升级到4.35.0,tokenization结果不同;
解决方案:
- 实施“模型-代码-数据”三元组绑定:MLflow中每个run必须记录git commit hash、docker image digest、data version;
- 预处理代码放入model artifact:用joblib保存tokenizer,而非依赖外部库;
- 回滚时同步回滚预处理代码版本。
避坑心得:模型版本管理不是只管.pth文件,而是管理整个推理环境的确定性快照。
6. 从Scratch的终极心法:把AI当水电,而非火箭
做AI Engineering from Scratch十年,我最大的体会是:越想把它做成高精尖的“火箭工程”,越容易失败;越把它当成城市供水系统的“水电工程”,越能成功。火箭追求一次性突破,水电追求365天不间断供应。AI系统同理——它的价值不在首次上线时的惊艳指标,而在连续1000天无故障运行中沉淀的业务洞察。这意味着我们必须接受一些“不酷”的事实:
- 80%的时间花在数据清洗和监控告警上,而非调参;
- 最重要的文档不是模型架构图,而是data contract和failure mode手册;
- 最牛的工程师不是写出最复杂loss的人,而是能让实习生看懂pipeline每行代码意图的人。
我们团队有个硬性规定:所有新成员入职首月,必须亲手从Scratch复现一个已上线项目——不是跑通代码,而是重写所有数据管道、重训模型、重新部署服务、并接入监控。这个过程通常耗时3周,但完成后,新人对系统的理解深度远超阅读100页文档。因为只有亲手烧过砖、砌过墙、修过漏水,才真正懂得建筑的呼吸节奏。AI Engineering from Scratch,本质上是一场对抗熵增的持久战:数据在漂移,模型在退化,需求在变化,而我们要做的,就是用工程化的确定性,去锚定这些不确定性。当你某天凌晨收到告警,却能笑着关掉手机继续睡觉——因为你知道,那个从Scratch构建的系统,正稳稳地替你守着夜。