
1. 从零搭建AI工程体系为什么我劝你别急着调包ai-engineering-from-scratch这个标题第一次看到的时候我愣了一下。市面上讲AI的教程铺天盖地但绝大多数都是教你pip install一个库然后调个API就完事了。真正从工程角度把AI系统当成一个需要设计、需要架构、需要运维的软件项目来对待的内容少得可怜。我自己在这个领域摸爬滚打了几年踩过的坑比写过的代码还多。最开始我也是那种能跑就行的心态模型加载出来推理结果对了就觉得大功告成。直到有一次线上服务在高峰期直接雪崩排查了整整两天才发现是显存碎片化导致的OOM那一刻我才意识到AI工程和传统软件工程一样甚至比传统软件工程更需要系统性的思维。这个项目标题的核心价值在于from scratch这四个字。它不是让你从零开始造轮子而是让你从零开始理解每一个工程决策背后的逻辑。你为什么要用这个推理框架而不是那个为什么要做模型量化而不是直接部署原始权重为什么你的批处理策略在测试环境跑得好好的一到生产就拉胯这些问题调包侠永远回答不了。适合看这个内容的人我大致分三类。第一类是有一定编程基础想把AI能力集成到自己项目里的开发者你可能写过一些Python但对AI系统的工程化没有概念。第二类是做过后端或者运维现在转方向做AI平台的同学你的工程底子很好但需要补上AI特有的那些坑。第三类是对AI感兴趣的学生或者爱好者你不想只停留在跑通demo的层面而是想真正理解一个AI系统是怎么从代码变成服务的。接下来的内容我会按照一个AI工程项目的完整生命周期来展开。从最基础的环境搭建和依赖管理到模型选型和推理优化再到服务化部署和监控运维每一步我都会告诉你为什么这么做以及不这么做会死在哪里。这些经验有些是我自己踩坑踩出来的有些是跟同行交流时偷师来的都是实打实能落地的干货。2. 环境搭建与依赖管理别让你的项目死在第一步2.1 为什么虚拟环境不是可选项而是必选项我见过太多人拿到一个新项目上来就是pip install -r requirements.txt装完发现版本冲突然后开始pip install --upgrade一通乱搞最后把系统Python环境搞得一团糟。更可怕的是有些AI框架对CUDA版本、cuDNN版本、Python版本有严格的对应关系你随便升级一个包可能整个推理链路就崩了。虚拟环境这件事在AI工程里不是最佳实践而是生存底线。我推荐用conda来管理AI项目的环境原因很简单AI生态里很多包不是纯Python的它们依赖底层的C库、CUDA运行时、数学加速库。conda能帮你把这些非Python依赖也管起来而venv只能管Python包。具体操作上我习惯这样建环境conda create -n ai-eng python3.10 conda activate ai-engPython版本的选择有讲究。3.10是目前AI生态兼容性最好的版本3.11和3.12虽然更新但有些推理框架的预编译包还没跟上。你如果非要追新就要做好自己编译某些依赖的心理准备那个过程会让你怀疑人生。环境建好之后第一件事不是装框架而是装pip-tools或者poetry这样的依赖锁定工具。为什么因为requirements.txt里写torch2.0这种写法今天装和明天装可能装到不同的版本。AI框架的版本迭代非常快小版本之间行为不一致是常有的事。你需要把实际安装的精确版本锁定下来生成一个requirements.lock文件这样团队里每个人、每台机器上装出来的环境才是一致的。2.2 依赖冲突的排查思路与实战技巧依赖冲突是AI工程里最常见的第一天问题。我遇到过的典型场景是你装了一个推理框架它依赖numpy1.24但你之前装的另一个数据处理库要求numpy1.26。pip不会告诉你冲突了它会直接装一个版本然后运行时给你一个莫名其妙的报错。排查依赖冲突我有一套自己的流程。第一步用pip check命令它会列出所有不满足的依赖关系。第二步如果pip check没发现问题但运行时报错用pipdeptree这个工具把依赖树打出来看看是哪个包引入了冲突的版本。第三步如果冲突无法调和考虑用conda安装那些有复杂依赖的包因为conda的依赖解析器比pip强很多。注意千万不要在同一个环境里混用conda install和pip install来装同一个包。conda和pip各自维护一套依赖记录混用会导致依赖状态不一致后面出问题你根本查不出来。还有一个坑是CUDA版本。如果你要用GPU做推理PyTorch、TensorFlow这些框架的GPU版本对CUDA版本有严格要求。我的建议是先确定你的显卡驱动支持的最高CUDA版本然后去框架官网查对应关系表选一个框架版本再根据框架版本去装对应版本的CUDA运行时。顺序不能反反了就要重装。2.3 项目目录结构的设计逻辑一个AI工程项目如果目录结构设计得不好后期维护成本会指数级上升。我见过把所有代码都堆在一个main.py里的项目也见过目录层级深到from a.b.c.d.e import f这种程度的项目。这两种都是极端。我推荐的目录结构是这样的project/ ├── configs/ # 配置文件 ├── data/ # 数据相关 │ ├── raw/ # 原始数据 │ ├── processed/ # 处理后数据 │ └── schemas/ # 数据模式定义 ├── src/ # 源代码 │ ├── models/ # 模型定义 │ ├── inference/ # 推理逻辑 │ ├── training/ # 训练逻辑 │ └── utils/ # 工具函数 ├── tests/ # 测试代码 ├── scripts/ # 运维脚本 ├── notebooks/ # 实验性代码 └── requirements/ # 依赖管理这个结构的关键在于把实验性代码和生产代码分开。notebooks/目录里的东西可以乱可以随便改但src/目录里的代码必须有测试、有文档、有类型注解。很多AI项目烂尾就是因为实验代码和生产代码混在一起改着改着就不知道哪个版本是对的了。configs/目录也很重要。AI项目有大量的超参数、路径、模型配置这些东西绝对不能硬编码在代码里。我习惯用hydra或者pydantic-settings来管理配置支持从YAML文件、环境变量、命令行参数多个来源读取优先级明确覆盖逻辑清晰。3. 模型选型与推理优化把每一毫秒都花在刀刃上3.1 模型选型的决策框架模型选型是AI工程里最容易被忽视但影响最大的决策。很多人选模型的标准就是排行榜上哪个分高选哪个这是典型的学术思维不是工程思维。工程选型要考虑的维度多得多。我一般从五个维度来评估精度、延迟、吞吐、显存占用、部署复杂度。这五个维度往往是互相矛盾的。精度高的模型通常更大、更慢吞吐高的模型通常需要更复杂的批处理逻辑显存占用小的模型可能需要更多的量化工作。具体怎么权衡取决于你的业务场景。如果是离线批处理任务延迟不敏感那就选精度最高的模型用最大的批处理尺寸把吞吐拉满。如果是实时交互场景延迟是硬指标那就得在精度和延迟之间找平衡点可能需要用蒸馏后的小模型或者用模型量化来压缩。我做过一个文本分类的项目最初选了一个精度高但推理慢的大模型单条推理要200毫秒。后来换成一个小模型加蒸馏精度只掉了1.5个百分点但推理时间降到了15毫秒。对于那个业务场景来说这1.5个百分点的精度损失完全可以接受但延迟的改善是质变的。3.2 推理优化的三板斧量化、批处理、缓存推理优化这件事说复杂很复杂说简单也简单。核心就三板斧量化、批处理、缓存。把这三件事做好性能提升个三到五倍是常态。量化是把模型权重从高精度浮点数转换成低精度表示的过程。最常见的做法是把FP32转成FP16或者INT8。FP16量化几乎不损失精度但能省一半显存推理速度也能提升30%左右。INT8量化更激进显存省四分之三速度提升更多但精度损失需要评估。我一般先用FP16如果显存还是不够再考虑INT8。量化的实操上PyTorch有原生的量化工具但用起来比较繁琐。我推荐用bitsandbytes这个库它支持在加载模型时直接做8位或4位量化代码改动极小from transformers import AutoModelForCausalLM import bitsandbytes as bnb model AutoModelForCausalLM.from_pretrained( model_name, load_in_8bitTrue, device_mapauto )批处理是把多个推理请求合并成一个批次一起送进模型。GPU的并行计算能力很强单条推理和批量推理的耗时差距远小于批量大小的差距。比如单条推理10毫秒批量32条可能只要50毫秒平均每条1.5毫秒吞吐提升近7倍。但批处理不是越大越好。批处理尺寸受限于显存而且太大会导致首条请求的延迟增加。我通常的做法是设置一个动态批处理窗口比如最多等10毫秒攒够32条或者超时了就送一批。这样在吞吐和延迟之间取得平衡。缓存是最容易被忽视的优化手段。很多AI应用有大量的重复请求比如相同的用户查询、相同的图片。你可以用Redis或者内存缓存把推理结果存起来下次相同请求直接返回。缓存命中率哪怕只有20%整体性能也能提升不少。实操心得缓存的key设计很关键。对于文本输入不要直接用原始文本做key因为空格、大小写、标点的微小差异会导致缓存不命中。我一般会先做归一化处理然后对归一化后的文本做哈希用哈希值做key。3.3 显存管理的那些坑显存管理是AI工程里最让人头疼的问题之一。你可能会遇到这样的情况模型加载后显存占用正常但跑了一段时间后显存逐渐增长最后OOM。这就是显存泄漏。显存泄漏的常见原因有几个。一是PyTorch的计算图没有释放特别是在训练循环里如果忘记调loss.backward()或者optimizer.zero_grad()计算图会一直累积。二是缓存没有清理比如你把中间结果存在了一个全局列表里越存越多。三是CUDA上下文没有正确管理多线程环境下容易出现。排查显存泄漏我一般用torch.cuda.memory_summary()来看显存分配情况配合nvidia-smi观察显存变化趋势。如果发现显存只增不减那基本就是泄漏了。预防显存泄漏有几个习惯要养成。第一推理时用torch.no_grad()上下文管理器它不会构建计算图能省大量显存。第二定期调torch.cuda.empty_cache()清理未使用的显存缓存但注意这个操作有性能开销不要频繁调用。第三用del显式删除不再需要的张量然后手动触发垃圾回收。还有一个坑是显存碎片化。即使总显存够用但如果碎片太多大的张量可能分配不到连续显存。解决办法是设置PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True环境变量让PyTorch使用可扩展的显存段减少碎片。4. 服务化部署与性能调优让模型真正跑起来4.1 推理服务的架构设计把模型跑通和把模型做成服务中间隔着一道巨大的鸿沟。跑通只需要一个Python脚本做成服务要考虑并发、容错、监控、扩缩容等一系列问题。我推荐的推理服务架构是分层的。最底层是模型推理层负责加载模型、执行推理。中间是服务层负责请求接收、批处理调度、结果返回。最上层是网关层负责鉴权、限流、路由。服务层我一般用FastAPI或者Triton Inference Server。FastAPI适合快速搭建代码可控性强适合中小规模的服务。Triton是NVIDIA出的专业推理服务框架支持多模型、多框架、动态批处理适合大规模生产环境。如果选FastAPI有几个关键点要注意。第一模型加载要在应用启动时完成不能每个请求都加载一次。用FastAPI的lifespan事件或者startup事件来做模型初始化。第二推理是CPU密集型操作会阻塞事件循环要用run_in_executor把推理放到线程池里执行。第三要设置合理的超时和重试策略防止单个慢请求拖垮整个服务。from fastapi import FastAPI from concurrent.futures import ThreadPoolExecutor import asyncio app FastAPI() executor ThreadPoolExecutor(max_workers4) model None app.on_event(startup) async def load_model(): global model model load_your_model() app.post(/predict) async def predict(request: PredictRequest): loop asyncio.get_event_loop() result await loop.run_in_executor( executor, model.inference, request.text ) return {result: result}4.2 动态批处理的实现细节动态批处理是提升推理吞吐的核心技术。它的基本思想是不立即处理每个到达的请求而是等一小段时间把这段时间内到达的请求攒成一个批次一起处理。实现动态批处理关键参数有三个最大批处理尺寸、最大等待时间、批处理队列长度。最大批处理尺寸取决于显存和模型需要实测确定。最大等待时间决定了延迟的上限一般设10到50毫秒。批处理队列长度决定了系统能承受的突发流量队列满了之后新请求要么等待要么被拒绝。我实现过一个简单的动态批处理调度器核心逻辑是这样的import asyncio from collections import deque class BatchScheduler: def __init__(self, max_batch_size, max_wait_ms): self.max_batch_size max_batch_size self.max_wait max_wait_ms / 1000 self.queue deque() self.lock asyncio.Lock() async def add_request(self, request): future asyncio.Future() async with self.lock: self.queue.append((request, future)) if len(self.queue) self.max_batch_size: await self._process_batch() return await future async def _process_batch(self): batch list(self.queue) self.queue.clear() requests [r for r, _ in batch] futures [f for _, f in batch] results await self._inference(requests) for future, result in zip(futures, results): future.set_result(result)这个调度器有个问题如果请求量很小队列一直攒不满请求就会一直等。所以还需要一个定时器每隔max_wait时间强制处理一次队列。这个逻辑可以用asyncio.create_task来实现。注意动态批处理会改变请求的响应时间分布。P50延迟可能很低但P99延迟会明显升高因为有些请求运气不好刚好在批次快满的时候到达等了很久。如果你的业务对P99延迟敏感要谨慎使用动态批处理或者把最大等待时间设小一点。4.3 服务监控与告警体系服务上线只是开始真正的挑战在于运维。没有监控的服务就像没有仪表盘的飞机你不知道它什么时候会掉下来。AI服务的监控指标分几类。第一类是系统指标CPU使用率、内存使用率、GPU使用率、显存使用率、网络IO。这些用Prometheus加node_exporter就能采集。第二类是应用指标请求量、响应时间、错误率、批处理尺寸分布。这些需要在代码里埋点用prometheus_client库暴露出来。第三类是模型指标推理延迟、显存占用、缓存命中率。这些是AI服务特有的需要专门监控。告警策略上我建议设置多级告警。警告级别GPU使用率持续超过80%超过5分钟。严重级别错误率超过1%持续1分钟。致命级别服务不可用或者显存OOM。不同级别走不同的通知渠道警告发邮件严重发即时消息致命直接打电话。还有一个容易被忽视的监控点是数据漂移。AI模型的输入数据分布会随时间变化如果输入分布和训练分布差异越来越大模型效果会下降。你可以定期采样线上请求计算输入特征的统计量和训练集的统计量做对比。如果差异超过阈值就要考虑重新训练模型了。5. 常见问题与排查技巧实录5.1 推理结果不一致的排查思路同一个输入两次推理结果不一样这是AI工程里最诡异的问题之一。我遇到过好几次每次原因都不一样。第一次是模型没有切换到评估模式。PyTorch的nn.Module有train()和eval()两种模式train()模式下Dropout和BatchNorm的行为和eval()不一样。如果你加载模型后忘记调model.eval()推理结果就会随机变化。这个坑很隐蔽因为模型加载后默认是train()模式。第二次是随机种子没有固定。有些模型在推理时会用到随机数比如某些采样策略。如果你没有设置随机种子每次推理的随机数序列不一样结果自然不一样。解决办法是在推理前调torch.manual_seed(42)和numpy.random.seed(42)。第三次是浮点数精度问题。GPU和CPU的浮点运算精度有细微差异同一份代码在GPU上跑和在CPU上跑结果可能在小数点后好几位才不一样。如果你的业务对精度极其敏感要么统一用同一种设备要么在比较结果时设置一个容差。排查这类问题我的建议是先用一个极简的输入比如一个单词或者一张纯色图片跑两次看结果是否一致。如果一致说明问题出在特定输入上可能是数据预处理的问题。如果不一致那就是模型或者环境的问题按上面说的几个方向逐一排查。5.2 性能不达预期的优化路径性能优化最忌讳盲目调参。我见过有人一上来就把批处理尺寸调到最大结果OOM了然后开始怀疑人生。正确的做法是先定位瓶颈再针对性优化。定位瓶颈的工具我推荐py-spy和nsight-systems。py-spy能采样Python代码的调用栈告诉你时间花在哪个函数上。nsight-systems是NVIDIA出的性能分析工具能看到GPU kernel的执行时间、显存拷贝时间、CPU-GPU同步时间。常见的性能瓶颈和优化方向我整理了一个速查表瓶颈现象可能原因优化方向GPU利用率低数据加载慢用多进程预加载数据或用GPU加速数据预处理推理延迟高批处理尺寸太小增大批处理尺寸或启用动态批处理显存占用高模型精度太高做FP16或INT8量化吞吐上不去CPU-GPU同步频繁减少.item()和.cpu()调用用异步方式首条请求慢模型冷启动预热模型启动时先跑几次推理优化的顺序也很重要。先做量化这是收益最大、改动最小的优化。再做批处理这需要改服务架构但收益也很明显。最后做缓存这需要业务逻辑配合但能解决重复请求的问题。5.3 模型更新与版本管理模型不是一成不变的你需要定期更新模型来提升效果或者修复问题。但模型更新比代码更新复杂得多因为模型文件很大而且更新过程中服务不能中断。我推荐的模型更新流程是这样的。第一步新模型在离线环境验证确保精度和性能达标。第二步把新模型上传到模型仓库打上版本标签。第三步在预发布环境部署新模型用线上流量的一小部分做A/B测试。第四步A/B测试通过后逐步把线上流量切换到新模型。第五步观察一段时间确认没问题后下线旧模型。模型版本管理我建议用MLflow或者DVC。MLflow能记录每次实验的参数、指标、模型文件方便回溯。DVC更适合管理大文件它把模型文件存在对象存储里Git仓库里只存指针文件。实操心得模型文件一定要做校验。我遇到过模型文件在传输过程中损坏的情况加载时不报错但推理结果全是乱码。后来我养成了习惯每次加载模型前先校验MD5不匹配就重新下载。5.4 线上服务的容错与降级线上服务不可能永远不出问题。GPU可能挂掉模型可能加载失败依赖的服务可能超时。你需要提前设计好容错和降级策略。容错的第一道防线是健康检查。用/health接口检查模型是否加载成功、GPU是否可用、依赖服务是否正常。Kubernetes的liveness probe和readiness probe都配上不健康的实例自动重启或者从负载均衡里摘除。降级的策略取决于业务场景。如果模型推理超时可以返回一个默认结果或者缓存结果。如果GPU不可用可以降级到CPU推理虽然慢但至少能用。如果整个服务不可用可以返回一个友好的错误提示而不是让用户看到一堆堆栈信息。我做过一个项目模型服务挂了之后降级逻辑是返回一个基于规则的简单结果。虽然效果差很多但至少服务没挂用户体验没有完全崩溃。这个降级逻辑平时用不到但关键时刻能救命。6. 从项目到产品AI工程的最后一公里6.1 测试策略AI项目怎么测AI项目的测试和传统软件测试有很大不同。传统软件测试是确定性的输入A必然得到输出B。AI项目的输出是概率性的同样的输入可能得到不同的输出你没法用assertEqual来断言。我的测试策略分三层。第一层是单元测试测数据预处理、后处理、工具函数这些确定性逻辑。这些可以用传统的测试框架pytest就很好。第二层是集成测试测模型加载、推理流程、服务接口。这些测试不检查具体输出值而是检查输出的形状、范围、类型是否符合预期。第三层是效果测试用一批标注数据评估模型的精度、召回率等指标确保模型更新后效果没有下降。效果测试是最重要的也是最容易被忽视的。我建议每次模型更新都跑一遍效果测试把指标记录下来和上一个版本对比。如果指标下降超过阈值就阻止这次更新。这个流程可以集成到CI/CD里自动化执行。6.2 文档与知识沉淀AI项目的人员流动率很高今天写代码的人明天可能就离职了。如果没有好的文档接手的人会非常痛苦。我见过一个项目模型训练脚本里有一个魔法参数没人知道为什么设成那个值后来有人改了一下模型效果直接崩了。文档要写什么第一环境搭建步骤包括依赖版本、CUDA版本、环境变量。第二模型架构和训练细节包括超参数选择理由、数据来源、预处理步骤。第三服务部署和运维手册包括启动命令、配置项说明、常见问题排查。第四实验记录包括每次实验的参数、指标、结论。文档的维护比编写更重要。我建议把文档和代码放在同一个仓库里代码更新时同步更新文档。用mkdocs或者sphinx把文档生成静态网站方便查阅。6.3 团队协作与代码规范AI项目的团队协作有个特殊挑战数据科学家和工程师的工作方式差异很大。数据科学家习惯用Jupyter Notebook代码风格随意注重快速实验。工程师习惯用IDE代码风格严谨注重可维护性。我的经验是不要试图改变数据科学家的工作方式而是建立一套交接规范。数据科学家在Notebook里做实验实验成功后把核心逻辑提取成Python模块加上类型注解和文档字符串交给工程师集成到生产代码里。这个交接过程要有代码审查确保代码质量达标。代码规范上我建议用black做格式化isort做import排序mypy做类型检查ruff做lint。这些工具都可以集成到pre-commit hook里提交代码时自动执行。一开始可能会觉得麻烦但习惯之后会发现这些工具能帮你避免很多低级错误。6.4 成本控制AI服务烧钱的那些事AI服务是出了名的烧钱。GPU实例按小时计费一个A100实例每小时可能要几十块钱。如果你的服务24小时运行一个月下来就是几万块。成本控制不是可选项是生存必需。成本控制的手段有几个。第一用竞价实例或者抢占式实例价格能便宜一半以上代价是可能被随时回收需要做好容错。第二自动扩缩容流量低的时候缩容到最小实例数流量高的时候自动扩容。第三用Serverless推理服务按实际调用次数计费没有请求的时候不花钱。第四模型量化和小型化用更小的模型达到可接受的精度减少GPU需求。我做过一个成本优化把模型从FP32量化到INT8显存占用降到四分之一原来需要一张A100的活现在一张T4就能干成本直接降了80%。精度只掉了不到1个百分点业务方完全能接受。实操心得成本优化一定要算ROI。花一周时间优化每月省100块那不值得。花一天时间优化每月省10000块那必须做。先算账再动手。6.5 安全与合规AI工程的底线AI服务的安全问题比传统服务更复杂。除了常规的网络安全、访问控制还要考虑模型安全、数据隐私、输出合规。模型安全方面要防止模型被窃取。模型文件要加密存储推理服务要鉴权不要暴露模型的内部结构。对抗样本攻击也要考虑虽然普通业务不太会遇到但如果你的服务面向公众恶意用户可能会构造特殊输入来让模型出错。数据隐私方面如果模型训练用了用户数据要确保数据脱敏符合隐私保护要求。推理时如果输入包含敏感信息要考虑是否记录日志记录的话要做脱敏处理。输出合规方面要过滤模型的输出防止生成不当内容。这个可以用关键词过滤也可以用另一个模型来做内容审核。我建议至少做一层关键词过滤成本低效果好。这些安全措施会增加一些开发工作量但它们是底线不能省。我见过因为输出不合规导致服务被下架的案例损失远大于做安全措施的成本。7. 我踩过的那些坑希望你别再踩写了这么多最后分享几个我印象最深的坑都是真金白银换来的教训。第一个坑是环境不一致。开发环境用Python 3.9生产环境用Python 3.10结果一个依赖包在3.10上行为不一样导致线上推理结果全错。后来我强制要求开发、测试、生产环境用完全相同的Docker镜像这个问题才彻底解决。第二个坑是显存泄漏。服务跑了一周后突然OOM排查发现是一个全局缓存字典没有清理每个请求都往里塞数据越塞越多。后来加了LRU淘汰策略问题解决。这个坑的教训是任何全局状态都要有清理机制。第三个坑是模型版本混乱。线上同时跑了三个版本的模型流量分配是1:1:1但没人知道哪个版本是哪个。后来出了bug排查了半天才发现是其中一个旧版本的问题。现在我的做法是模型文件命名必须包含版本号和日期服务启动时打印模型版本日志里也记录版本信息。第四个坑是监控缺失。服务上线后没有监控挂了都不知道直到用户投诉才发现。现在我的做法是服务上线前必须配好监控和告警没有监控的服务不允许上线。这些坑看起来都是低级错误但在实际项目中它们发生的概率远比你想象的高。AI工程和传统软件工程一样细节决定成败。把每一个细节做好你的AI项目才能从demo变成真正可用的产品。