
1. 从零搭建AI工程能力为什么“会用模型”和“会做工程”是两回事很多人第一次接触AI项目时都会经历一个相似的阶段在笔记本里跑通一个模型准确率看着还行于是觉得“AI不过如此”。可真要把这套东西放到线上让成百上千人同时用问题就全冒出来了——推理延迟忽高忽低、显存说爆就爆、模型更新一次要停服半小时、日志里全是看不懂的报错。这时候你才意识到训练一个模型和交付一个AI系统中间隔着一整条工程链路。ai-engineering-from-scratch这个标题说的正是这条链路。它不是教你调某个库的API也不是带你刷某个榜单而是从最底层开始把AI工程里那些绕不开的环节一个个搭起来。适合谁看我认为有三类人最该认真读一是刚转行做AI、只会写训练脚本的算法同学二是后端出身、被拉来做推理服务的工程师三是想搞清楚“模型上线到底发生了什么”的产品或项目负责人。这篇文章我会按真实项目推进的顺序来写从环境与依赖管理讲起到推理服务的封装、性能压测、模型版本管理再到监控与回滚。每一块我都会说清楚“为什么这么做”而不只是“怎么做”。因为AI工程最坑的地方往往不是某个命令写错了而是你根本不知道为什么要加那个参数。提示本文涉及的代码和配置都基于常见开源技术栈具体版本请以你本地实际环境为准不要盲目复制版本号。2. 环境与依赖AI项目里最容易被低估的“地基工程”2.1 为什么AI项目的依赖管理比普通后端更麻烦普通Web服务的依赖无非是框架、数据库驱动、几个工具库版本冲突相对好排查。AI项目不一样它的依赖链条又长又深深度学习框架本身、CUDA驱动、cuDNN、算子库、Python版本、编译工具链任何一环对不上轻则警告重则直接段错误。我见过太多人卡在“ImportError: libcudart.so.11.0: cannot open shared object file”这种问题上耗掉一整天。所以从零做AI工程第一件事不是写模型而是把环境固定下来。我的习惯是三层隔离操作系统层用容器镜像固定基础环境Python层用虚拟环境或conda隔离包项目层用锁文件固定精确版本。三层各司其职出问题时能快速定位是哪一层变了。2.2 用锁文件而不是requirements.txt裸列表很多人写依赖就一个requirements.txt里面写torch2.0。这在个人项目里没问题但在团队协作里是灾难——今天装是2.0.1明天装可能就变成2.1.0行为可能完全不同。正确做法是区分“直接依赖”和“锁定依赖”requirements.in只写你直接用的包比如torch、fastapi、uvicorn不写版本或只写大版本。requirements.txt由工具如pip-tools、uv根据.in文件解析出完整依赖树并锁定每个包的精确版本和哈希。这样别人拿到你的项目pip install -r requirements.txt装出来的环境和你一模一样。我实测下来这一步能省掉至少一半的“在我机器上能跑”的扯皮。2.3 容器镜像的层缓存技巧写Dockerfile时顺序很关键。把变化频率低的操作放前面变化频率高的放后面能最大化利用层缓存。典型顺序是装系统依赖 → 装Python依赖 → 复制代码 → 启动。如果你把复制代码放在装依赖之前那每次改一行代码整个依赖层都要重建构建时间从几十秒变成十几分钟。FROM python:3.11-slim RUN apt-get update apt-get install -y --no-install-recommends \ libgomp1 rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [uvicorn, app.main:app, --host, 0.0.0.0, --port, 8000]注意--no-install-recommends和清理apt缓存这两步能让镜像小几百MB。镜像小不只是省磁盘拉取和启动也更快在弹性扩容场景下这点时间很值钱。3. 推理服务封装把模型变成一个能扛流量的接口3.1 模型加载放在哪里决定了服务的启动行为新手常犯的错是在每个请求的处理函数里加载模型。这样每个请求都要读一遍权重文件延迟高得离谱显存也会被反复占用。正确做法是在服务启动时加载一次常驻内存。用FastAPI的话可以放在lifespan事件里from contextlib import asynccontextmanager from fastapi import FastAPI ml_models {} asynccontextmanager async def lifespan(app: FastAPI): ml_models[encoder] load_model(encoder) yield ml_models.clear() app FastAPI(lifespanlifespan)这里有个细节加载模型是阻塞操作如果放在异步事件循环里直接调用会卡住整个启动过程。如果加载时间很长比如几十秒建议用线程池包一层或者干脆接受启动慢一点但要在健康检查里区分“启动中”和“就绪”两种状态。3.2 批处理吞吐量和延迟的跷跷板单条推理往往浪费算力因为GPU擅长并行。把多个请求攒成一批一起算吞吐量能提升几倍甚至十几倍。但攒批会引入等待时间延迟上升。这个平衡点怎么找我的经验是先测出单条推理的耗时再测不同批大小下的总耗时算出每条的平均耗时。通常批大小从1增加到8单条平均耗时下降最明显再往上收益递减。实现上可以用一个队列加一个后台worker请求进来先入队worker每隔几毫秒或队列达到一定长度就取一批出来推理。要注意设置最大等待时间否则低峰期请求会一直等不到足够的批。3.3 输入校验和超时控制不能省模型服务最怕的是畸形输入。一张超大图片、一段超长文本可能直接把显存打满导致整个服务崩溃。所以入口处必须做校验图片限制尺寸和格式文本限制长度数值限制范围。校验不通过直接返回明确错误不要让它进到模型里。超时控制同样重要。推理可能因为各种原因变慢如果没有超时请求会一直挂着连接池被占满服务雪崩。在服务端设置推理超时超时后返回降级结果或错误同时记录日志用于排查。4. 性能压测别等上线了才知道能扛多少4.1 压测要模拟真实流量而不是狂刷同一个请求用ab或wrk对同一个接口狂发请求测出来的数字很好看但没意义。真实流量是多样的输入长度不同、请求类型不同、有冷有热。压测脚本应该准备一批有代表性的样本随机抽取发送。如果条件允许把线上真实流量的采样脱敏后作为压测输入效果最好。4.2 关注P99而不是平均值平均延迟200毫秒听起来不错但如果P99是5秒那意味着每100个用户就有1个要等5秒体验很差。AI服务的延迟分布往往是长尾的因为偶尔会遇到特别大的输入或特别复杂的样本。压测报告里必须包含P50、P90、P99甚至P999。优化时优先砍长尾比如对超大输入做截断或分流。4.3 显存和GPU利用率要一起看光看QPS不够还要看GPU利用率。如果利用率只有30%说明算力没吃满可能是批处理没做好或数据预处理成了瓶颈。如果利用率接近100%但QPS上不去说明算力到顶了该考虑加卡或换更高效的模型。显存方面要留出余量别跑到95%以上否则一个稍大的请求就可能OOM。指标健康范围异常时的排查方向GPU利用率60%-85%过低查预处理过高查算力瓶颈显存占用低于90%过高查批大小和输入尺寸P99延迟低于SLA的80%过高查长尾输入和GC错误率低于0.1%过高查输入校验和超时5. 模型版本管理与灰度发布让更新不再提心吊胆5.1 模型文件也要有版本号不能靠文件名区分我见过用model_final_v2_real_final.pt这种命名的过两周谁也不知道哪个是哪个。正确做法是给每次训练产出一个唯一版本号比如用时间戳加git commit短哈希同时记录训练数据版本、超参数、评估指标。这些元信息存到一个清单文件或数据库里模型文件本身按版本号存放。5.2 灰度发布先给小流量再逐步放大新模型上线不要一次性全量替换。先切5%的流量到新版本观察错误率、延迟、业务指标。如果一切正常再逐步扩大到20%、50%、100%。如果发现异常立刻切回旧版本。这个过程要能在一分钟内完成所以模型加载和切换机制要提前设计好不能靠重启服务。实现上可以在服务里同时加载新旧两个模型用一个配置开关控制流量分配。配置可以放在配置中心或环境变量里支持热更新。这样切换只是改一个比例不用重新部署。5.3 回滚要像切换一样简单回滚不是“出事了再想办法”而是要在设计阶段就准备好。旧版本的模型文件要保留旧版本的配置要能一键恢复。我建议至少保留最近三个版本并且定期做回滚演练确保真出事时手不生。6. 监控与日志出问题时能快速定位才是真本事6.1 日志要结构化别打印一堆字符串print(model inference done)这种日志出问题时毫无用处。应该用结构化日志至少包含请求ID、模型版本、输入长度、推理耗时、是否命中缓存、错误码。这样你可以按请求ID串起整个链路也可以按模型版本对比性能。import logging import json logger logging.getLogger(inference) def log_inference(request_id, model_version, input_len, latency_ms, status): logger.info(json.dumps({ request_id: request_id, model_version: model_version, input_len: input_len, latency_ms: latency_ms, status: status }))6.2 关键指标要设告警但别设太多告警太多会让人麻木最后真出事反而没人看。我的做法是只对几个核心指标设告警错误率超过阈值、P99延迟超过SLA、GPU显存超过90%、服务实例数低于预期。其他指标先看板展示观察一段时间后再决定要不要加告警。6.3 保留原始输入样本用于复现出问题时光看日志往往不够还需要拿到当时的输入才能复现。可以在日志里记录输入样本的存储路径脱敏后或者对输入做哈希后存一份采样。这样排查时可以快速复现问题而不是靠猜。7. 一些踩过坑才明白的实操心得第一别在推理服务里做训练。有人图省事把微调逻辑也塞进服务里结果一个请求进来触发训练整个服务卡死。训练和推理要彻底分开训练用离线任务推理用在线服务。第二模型预热很重要。服务刚启动时第一次推理往往特别慢因为要初始化各种算子。可以在启动后主动跑几条样例请求做预热让后续请求稳定。第三注意Python的GIL。如果推理是CPU密集型的多线程并不能提升吞吐反而可能因为GIL切换变慢。这种情况用多进程或者把计算密集部分放到C扩展里。第四缓存能省很多算力。如果同样的输入反复出现缓存推理结果能大幅降低负载。但要注意缓存失效策略模型更新后旧缓存要清掉。第五文档和注释要写清楚“为什么”。AI工程里很多参数是经验值比如批大小、超时时间、缓存容量。写清楚当初为什么选这个值后来的人调整时才有依据而不是瞎试。这套从零搭建的流程我在几个项目里反复用过每次都能在早期发现不少隐藏问题。AI工程没有银弹靠的就是把每个环节做扎实然后留好退路。希望这些内容能帮你少走一些弯路。