1. 从零搭建AI工程体系为什么我劝你别急着调包这两年AI应用开发的门槛肉眼可见地在降低随便拉个框架、调几个API一个能跑通的Demo半天就能出来。但如果你真在团队里扛过从原型到上线的完整链路就会明白“能跑”和“能扛”之间隔着一条巨大的鸿沟。ai-engineering-from-scratch这个项目标题在我看来戳中的正是这条鸿沟——它强调的不是“用AI”而是“从零把AI工程化”。这两件事的差别就像会开车和会造车一样大。我见过太多团队在项目初期直接堆砌现成组件模型加载、推理服务、缓存、监控全靠第三方库一把梭结果一到线上流量波动、显存吃紧、响应延迟飙升的时候整个系统就像多米诺骨牌一样倒下去排查起来毫无头绪。原因很简单你对系统里每一层的边界和代价没有感知。从零搭建的意义不是让你重复造轮子而是让你亲手摸一遍每个轮子的承重极限知道哪里该省、哪里不能省。这篇文章适合三类人一是刚入行AI应用开发、想搞清楚一个完整推理服务到底由哪些部分拼起来的工程师二是带团队做AI产品、需要一套可复现工程骨架的技术负责人三是自己折腾过Demo、但一上量就翻车的独立开发者。我会围绕“从零构建”这个核心把整体设计思路、关键模块的实现要点、实操流程和踩坑经验完整拆一遍尽量给到可以直接抄作业的方案。2. 整体架构设计从零不等于从原始社会开始2.1 先想清楚“从零”的边界在哪里很多人一听到“from scratch”第一反应是什么都自己写连矩阵乘法都手撸。这是对工程化的误解。工程语境下的“从零”指的是从需求出发自主决定每一层的技术选型和边界划分而不是拒绝一切现成工具。你可以用PyTorch做推理可以用FastAPI做服务层可以用Redis做缓存但你必须清楚为什么用它们、它们在你的系统里承担什么角色、出问题时你从哪里下手。我在实际项目里总结出一条判断标准凡是影响系统稳定性、性能上限和成本结构的关键路径必须自己掌控凡是纯功能性、可替换的辅助组件可以大胆用现成的。比如推理引擎的批处理策略、显存管理、请求队列调度这些直接决定你的QPS和单次推理成本必须自己设计或深度定制而日志采集、配置读取这类东西用成熟库完全没问题。基于这个原则一个从零搭建的AI工程骨架通常包含这几层接入层负责协议解析和限流调度层负责请求排队和批处理推理层负责模型加载和前向计算资源层负责显存和算力分配观测层负责指标采集和告警。每一层之间通过明确的接口通信任何一层出问题都能被快速定位和隔离。2.2 为什么我选择“分层解耦异步流水线”这套组合早期我试过把推理逻辑直接写在Web框架的请求处理函数里简单直接几十行代码就能跑。但一旦并发上来问题立刻暴露请求处理线程被推理计算阻塞新的请求进不来超时雪崩。后来改成同步多线程又遇到GIL和显存竞争的问题。折腾了几轮之后我固定下来一套分层解耦加异步流水线的架构。具体来说接入层用异步IO接收请求把请求封装成任务对象丢进队列调度层从队列里按批次取出任务凑够一个batch或者等到超时阈值就触发推理推理层在独立的执行器里跑模型算完把结果写回每个任务的future接入层再异步地把结果返回给客户端。整条链路没有一处是阻塞的每一层都可以独立扩容。这套设计的好处在于批处理让GPU利用率从个位数拉到60%以上异步让单机并发能力提升一个数量级分层让故障排查从“大海捞针”变成“按层定位”。代价是代码复杂度上去了需要处理任务超时、批次拆分、结果乱序等问题。但这些都是可控的复杂度比起线上事故带来的损失完全值得。2.3 关键参数的计算过程批次大小和超时阈值怎么定批处理是这套架构的性能核心但批次大小不是拍脑袋定的。我通常用这个公式估算理论最大批次 可用显存 / 单样本峰值显存。假设你的模型单样本推理峰值占用1.2GB显存GPU可用显存20GB那理论最大批次约16。但实际不能拉满要留20%余量给框架开销和显存碎片所以实际批次上限设在12左右。超时阈值则取决于你的延迟要求。如果业务要求P99延迟不超过500ms而单批次推理耗时约200ms那排队等待时间就不能超过300ms。假设请求到达速率是每秒50个批次大小12那凑满一批需要约240ms刚好卡在阈值边缘。这时候要么增大批次受显存限制要么接受部分批次不满就触发。我的经验是设置一个动态阈值队列长度超过批次上限的70%就立即触发否则等到超时这样在吞吐和延迟之间取得平衡。3. 核心模块拆解每个轮子都要知道它为什么转3.1 接入层别小看协议解析和限流接入层看起来最简单但坑不少。我见过有人直接用同步框架处理请求结果一个慢请求把整个线程池占满。也见过没做请求体大小限制被人传了个超大payload直接把内存打爆。从零搭建的话接入层至少要处理四件事协议解析、请求校验、限流、任务封装。协议解析建议用异步框架Python生态里FastAPI加Uvicorn是比较顺手的选择Go的话用Gin或Echo。请求校验要严格特别是输入长度、字段类型、必填项这些在入口拦住比在推理层报错要划算得多。限流我一般用令牌桶算法按客户端维度限速防止单个用户把队列打满。任务封装则是把请求转成一个带唯一ID、超时时间、回调future的对象丢进调度队列。注意限流阈值不要设得太死要留一定的突发余量。我通常设成平均QPS的1.5倍配合队列长度监控队列快满时自动降级拒绝新请求而不是让所有请求一起超时。3.2 调度层批处理策略决定性能上限调度层是整个系统的性能枢纽。核心逻辑是从队列里取任务凑批触发推理收集结果。听起来简单但细节很多。首先是凑批策略我前面提到的动态阈值法在实践中效果不错。其次是批次拆分如果某个任务特别大或者特别小可能需要单独处理避免拖累整批。还有一个容易被忽略的点是任务优先级。线上环境里不是所有请求都同等重要付费用户和免费用户的请求应该区别对待。我的做法是在任务对象里带一个优先级字段调度层用优先队列而不是普通队列高优先级任务可以插队。但要注意防止低优先级任务饿死可以设置一个最大等待时间超过就提升优先级。3.3 推理层模型加载和显存管理是重头戏推理层直接跟GPU打交道也是最容易出问题的地方。模型加载阶段我建议预加载所有需要的模型避免运行时动态加载因为加载过程会占用大量显存和时间放在请求路径里是灾难。如果模型太多显存放不下就要做模型卸载和换入换出但这会引入额外的延迟需要权衡。显存管理方面PyTorch的缓存分配器默认行为有时候会导致碎片化长时间运行后可能出现“明明有显存却分配失败”的情况。我的经验是定期调用显存整理或者在批次之间主动释放中间张量。另外推理时用torch.no_grad()和inference_mode()能省下不少显存这个细节很多人会忘。3.4 观测层没有指标就是盲人摸象观测层是很多从零搭建的项目最容易省略的部分但恰恰是上线后最需要的。我至少要采集这几类指标请求维度的QPS、延迟分布、错误率队列维度的队列长度、等待时间推理维度的批次大小、推理耗时、GPU利用率资源维度的显存占用、温度、功耗。这些指标用Prometheus采集Grafana展示关键阈值配告警。实操心得延迟指标一定要分位数不要只看平均值。平均值会掩盖长尾问题而AI服务的用户体验往往由P99决定。我一般同时看P50、P95、P99三个分位。4. 实操流程从空目录到可压测的服务4.1 环境准备与依赖锁定第一步是把环境搭起来。我习惯用conda或者venv创建独立环境然后把所有依赖版本精确锁定写进requirements.txt或者poetry.lock。AI工程最怕的就是依赖漂移今天能跑的代码明天换个环境就崩。CUDA版本、PyTorch版本、驱动版本三者要严格对应这个对应关系在官方文档里有矩阵表照着选就行。python -m venv venv source venv/bin/activate pip install torch2.1.0 --index-url https://download.pytorch.org/whl/cu118 pip install fastapi uvicorn redis prometheus-client pip freeze requirements.txt4.2 推理服务的骨架代码下面是一个最小可用的推理服务骨架包含接入、调度、推理三层。代码用Python写但思路可以迁移到任何语言。import asyncio import torch from fastapi import FastAPI, HTTPException from pydantic import BaseModel app FastAPI() class InferRequest(BaseModel): text: str priority: int 0 class Task: def __init__(self, req_id, data, priority): self.req_id req_id self.data data self.priority priority self.future asyncio.Future() class BatchScheduler: def __init__(self, max_batch12, timeout0.3): self.queue asyncio.PriorityQueue() self.max_batch max_batch self.timeout timeout self.model None def load_model(self): self.model torch.jit.load(model.pt) self.model.eval() async def schedule_loop(self): while True: batch [] try: task await asyncio.wait_for(self.queue.get(), timeoutself.timeout) batch.append(task) except asyncio.TimeoutError: continue while len(batch) self.max_batch: try: task self.queue.get_nowait() batch.append(task) except asyncio.QueueEmpty: break await self.run_batch(batch) async def run_batch(self, batch): inputs [t.data for t in batch] with torch.inference_mode(): outputs self.model(inputs) for task, out in zip(batch, outputs): task.future.set_result(out) scheduler BatchScheduler() app.on_event(startup) async def startup(): scheduler.load_model() asyncio.create_task(scheduler.schedule_loop()) app.post(/infer) async def infer(req: InferRequest): task Task(id(req), req.text, -req.priority) await scheduler.queue.put(task) try: result await asyncio.wait_for(task.future, timeout5.0) except asyncio.TimeoutError: raise HTTPException(status_code504, detailinference timeout) return {result: result}这段代码省略了错误处理、指标采集、模型预热等细节但核心的异步队列加批处理逻辑是完整的。你可以直接在此基础上扩展。4.3 压测与调优用数据说话服务跑起来之后别急着上线先压测。我用locust或者wrk做压力测试逐步增加并发观察QPS、延迟、GPU利用率的变化曲线。通常会出现一个拐点并发增加到某个值之后QPS不再上升延迟开始飙升。这个拐点就是系统的容量上限。调优的方向有几个增大批次能提升吞吐但增加延迟增加推理实例能提升并发但增加显存压力优化模型量化、剪枝、蒸馏能同时改善两者但需要重新训练。我的建议是先调批次和超时找到当前硬件下的最优组合再考虑模型层面的优化。调优手段吞吐影响延迟影响实现成本增大批次提升明显略微增加低增加实例提升明显基本不变中模型量化提升中等降低高请求优先级不变高优降低低5. 常见问题与排查技巧实录5.1 显存溢出最常见也最头疼显存溢出OOM是AI工程里出现频率最高的问题。排查思路是先看是加载阶段还是推理阶段溢出。加载阶段溢出通常是模型太大需要换更小的模型或者用模型并行。推理阶段溢出通常是批次太大或者输入太长需要动态调整批次大小。我遇到过一个隐蔽的案例模型本身不大但某个请求的输入特别长导致中间激活值暴涨直接把显存打满。解决办法是在接入层限制输入长度超长的请求直接拒绝或者截断。另外PyTorch的显存缓存机制会导致“已释放但未归还”的情况可以用torch.cuda.empty_cache()手动清理但不要频繁调用会影响性能。5.2 延迟毛刺长尾请求的元凶延迟毛刺通常来自几个地方批次里混入了超大请求拖慢整批GPU被其他进程抢占导致推理变慢Python GC触发造成短暂停顿。排查方法是给每个阶段打点看时间花在哪里。如果是批次问题可以做长度分桶把相似长度的请求放在一批如果是GC问题可以调整GC阈值或者用对象池减少分配。避坑技巧线上服务一定要关掉Python的自动GC改成手动定期触发。自动GC在高峰期触发会造成明显的延迟毛刺这个坑我踩过不止一次。5.3 队列积压雪崩的前兆队列积压是系统过载的最早信号。一旦队列长度持续增长说明请求到达速率超过了处理速率如果不干预很快就会演变成全面超时。我的做法是设置两级阈值队列长度超过容量的60%时告警超过80%时自动降级拒绝低优先级请求保证高优先级请求能正常处理。降级策略要提前设计好不能等出事了再想。常见的降级手段包括拒绝低优先级请求、返回缓存结果、降低推理精度、跳过非关键后处理。这些策略要能在不重启服务的情况下动态开关通常用配置中心或者环境变量控制。5.4 常见问题速查表现象可能原因排查方向解决手段显存OOM批次过大/输入过长看溢出时机和输入分布限制输入长度、动态批次延迟毛刺GC/批次混杂分阶段打点关自动GC、长度分桶队列积压处理能力不足看QPS和队列曲线降级、扩容、优化模型结果错误批次对齐问题检查输入输出映射加唯一ID校验服务崩溃未捕获异常看日志和core dump全局异常捕获、健康检查6. 工程化之外的思考从零搭建的真正价值6.1 你收获的不只是一套代码从零搭建AI工程体系表面上是写了一套推理服务实际上你收获的是对整条链路的掌控力。当线上出问题时你能快速定位到是哪一层、哪个参数、哪个资源出了问题而不是对着黑盒束手无策。这种掌控力在业务快速迭代、需求频繁变化的阶段尤其宝贵因为你知道改哪里、怎么改、改了之后影响什么。我自己的体会是亲手搭过一遍之后再看那些现成的推理框架能一眼看出它们的设计取舍和适用边界。比如某个框架默认用同步推理那它就不适合高并发场景某个框架不支持动态批次那它在流量波动大的业务里就会浪费算力。这些判断力是调包调不出来的。6.2 后续可以怎么扩展这套骨架搭好之后扩展方向很多。往性能方向走可以引入模型量化、算子融合、TensorRT加速往功能方向走可以加多模型路由、A/B测试、灰度发布往运维方向走可以加自动扩缩容、故障自愈、成本核算。每加一个模块都是对系统理解的一次加深。最后分享一个小技巧每次给系统加新功能之前先问自己“这个功能出问题了我怎么发现、怎么定位、怎么回滚”。如果三个问题都有明确答案那就放心加如果有一个答不上来那就先补观测和回滚机制。这个习惯帮我避免了好几次线上事故也让我对系统的每一处改动都心里有底。