尧图精选

从零搭建AI工程能力:数据管道、推理优化与线上服务实战

🕒 发布时间:2026/10/2 9:33:13 📁 来源:尧图网络
1. 从零搭建AI工程能力到底在搭什么很多人第一次看到“ai-engineering-from-scratch”这个标题会下意识理解成“从零手搓一个大模型”。我一开始也这么以为直到真正把这条路线走了一遍才发现它要解决的根本不是“怎么训练模型”而是“怎么让模型在真实业务里稳定跑起来”。这两件事之间的距离比大多数人想象的要远得多。先把概念说清楚。AI工程AI Engineering指的是围绕模型构建可交付、可维护、可迭代的软件系统的一整套工程实践。它包含数据管道、特征处理、模型服务、推理优化、监控告警、版本管理、成本控制这些环节。而“from scratch”强调的是不依赖现成的高级封装从最基础的组件开始把每一层的原理和边界都摸清楚。适合谁来参考我认为有三类人一是会写业务代码但没碰过模型部署的后端工程师二是做过算法实验但没做过线上服务的算法同学三是想系统理解AI系统全貌的技术负责人。为什么值得从零做一遍因为现在大量教程教你调一个库、跑一个demo代码能跑通但一旦流量上来、数据分布变了、延迟要求收紧了整个系统就散架。我见过太多团队模型在notebook里准确率95%上线后接口超时率30%最后发现是预处理逻辑在批量推理时没有做向量化。这类问题只有你亲手把每一层搭过一遍才会形成肌肉记忆。这篇文章我会按照真实的搭建顺序来讲先明确一个最小可用的AI系统边界再逐层拆解数据层、推理层、服务层、可观测层最后讲我在实际落地中踩过的坑和总结出的取舍逻辑。全程不依赖任何特定云厂商的高级托管服务所有组件都可以在你自己的机器上复现。2. 先划边界一个最小可用AI系统长什么样2.1 为什么不能一上来就堆组件新手最容易犯的错是一开始就上Kubernetes、上特征存储、上向量数据库、上模型注册中心。结果两周过去环境还没配好热情已经耗光了。我的建议是先用一个脚本跑通端到端再逐步替换其中的组件。这个思路在工程上叫“演进式架构”核心是让系统始终处于可运行状态。一个最小可用的AI系统其实只需要四个部分一个能读数据的地方、一个能加载模型并做推理的进程、一个能接收请求并返回结果的接口、一个能看到运行状态的日志。就这四样。你可以用本地文件当数据源用Python脚本加载模型用Flask起一个接口用标准输出打日志。这套东西半小时就能跑起来但它已经具备了AI系统的完整骨架。我实际带团队时会让每个人先用这套最小系统跑通一个真实任务比如文本分类或者图像打标。跑通之后再问哪里会成为瓶颈数据量大了怎么办并发上来了怎么办模型更新了怎么不中断服务这些问题会自然引导你去引入更专业的组件而不是为了用而用。2.2 四个核心模块的职责划分把最小系统拆开看四个模块的职责必须清晰否则后期维护会非常痛苦。数据层负责把原始数据变成模型能吃的张量。这里的关键是“确定性”——同样的输入必须产生同样的输出。我见过因为随机种子没固定导致离线评估和线上推理结果对不上的案例排查了整整两天。推理层负责加载模型权重、执行前向计算、返回原始输出。这一层要关注的是批处理、设备管理CPU/GPU、内存复用。服务层负责协议转换、请求校验、并发控制、超时处理。它不应该关心模型内部长什么样只负责把请求安全地送进推理层。可观测层负责记录延迟、吞吐、错误率、输入输出分布。没有这一层你就是在盲飞。提示这四个模块之间的接口要尽早用数据结构固定下来比如用dataclass或者pydantic模型定义请求和响应的schema。后期换实现时只要接口不变替换成本极低。2.3 一个容易被忽略的起点环境可复现从零搭建最容易翻车的地方其实是环境。我建议在写第一行业务代码之前先把依赖管理做好。用conda或者venv创建独立环境把所有依赖写进requirements.txt或者pyproject.toml并且锁定版本号。不要用“latest”因为三个月后你重装环境很可能因为某个底层库升级导致行为变化。更进一步如果你有条件用Docker把整个运行环境打包。这样从开发机到测试机到生产机行为完全一致。我自己的习惯是哪怕只是本地跑一个demo也会写一个最简Dockerfile。多花十分钟省下后面几小时的“在我机器上是好的”扯皮时间。3. 数据管道从原始文本到模型输入的确定性转换3.1 预处理为什么必须和训练时完全一致这是AI工程里最经典也最致命的坑。训练时你用了一套分词规则、一套归一化逻辑、一套截断策略上线时如果任何一处不一致模型效果就会断崖式下跌。而且这种下跌往往不是报错而是“结果看起来还行但就是不对”排查起来极其痛苦。我的做法是把预处理逻辑写成一个独立的、无状态的函数或类训练和推理共用同一份代码。不要训练时用A库、推理时用B库。如果必须用不同的实现那就写一套对照测试用一批样本同时跑两个实现逐字段比对输出。这个测试要纳入CI流程每次改动都跑。具体到文本任务预处理通常包括去除控制字符、统一Unicode编码、处理全半角、分词、添加特殊token、截断或填充到固定长度。每一步都要有明确的参数记录比如最大长度是128还是256截断是从左边还是右边。这些参数要和模型权重一起版本化管理。3.2 批处理与流式处理的取舍离线批量推理和在线实时推理对数据管道的要求完全不同。批量场景下你追求的是吞吐量可以一次加载几万条数据做成大batch喂给模型。在线场景下你追求的是延迟通常batch size是1或者很小的数字。这里有个实操技巧在线服务里可以做“微批处理”micro-batching。也就是在很短的窗口内比如10毫秒把多个请求攒在一起凑成一个batch做推理然后再拆开返回。这样既能利用GPU的并行能力又不会让单个请求等太久。我实测下来在GPU上做微批处理吞吐量能提升3到5倍而P99延迟只增加几毫秒。但微批处理会引入复杂度你需要一个队列来暂存请求需要一个调度器来决定什么时候触发推理还需要处理超时和异常。如果你的QPS很低比如每秒几个请求直接用batch size1更简单没必要过度设计。3.3 数据校验别让脏数据悄悄进模型线上系统一定要有输入校验。模型对异常输入的容忍度往往很低一个空字符串、一个超长文本、一个非法字符都可能导致推理报错或者输出垃圾。我习惯在服务层做三层校验类型校验是不是字符串、长度校验是否超过模型最大长度、内容校验是否包含非法字符。校验失败时不要直接把异常抛给用户。返回一个明确的错误码和可读的错误信息同时记录到日志里。如果某类校验失败率突然升高那往往意味着上游数据源出了问题这是一个很有价值的告警信号。另外对于数值型特征要做范围检查。我遇到过因为上游传了一个NaN导致整个batch的推理结果都变成NaN的情况。后来在预处理里加了一行np.nan_to_num问题解决。这种小细节文档里不会写但线上跑久了必然会遇到。4. 推理层模型加载、设备管理与性能压榨4.1 模型加载的时机与内存布局模型加载看似简单其实有很多讲究。首先是什么时候加载是在服务启动时一次性加载还是每次请求时加载显然要一次性加载否则每次请求都要读几百MB的文件延迟无法接受。但一次性加载意味着服务启动会变慢如果你的模型很大比如几个GB启动可能需要几十秒甚至几分钟。这时候可以考虑“预热”策略服务启动后先加载模型但暂时不接收流量等加载完成并跑完一次推理后再注册到负载均衡器。这样用户不会感知到启动过程。Kubernetes里的readiness probe就是干这个的。内存布局方面如果用的是PyTorch注意model.eval()和torch.no_grad()要一起用。前者切换dropout和batchnorm的行为后者关闭梯度计算能省下大量显存。我见过有人忘了加no_grad结果显存占用是正常情况的三倍batch size被迫调小吞吐量上不去。4.2 CPU与GPU的选型逻辑不是所有场景都需要GPU。如果你的模型很小比如几MB的线性模型或者小型TransformerCPU推理可能更快因为省去了数据传输的开销。GPU的优势在于大规模矩阵运算模型越大、batch越大优势越明显。我一般用这个经验法则如果单次推理在CPU上超过50毫秒且QPS要求超过10就考虑上GPU。否则先用CPU跑着等真的成为瓶颈再迁移。迁移时要注意CPU和GPU的数值精度可能有细微差异可能导致输出不完全一致。对于分类任务通常不影响最终结果对于回归任务要评估这个差异是否可接受。设备管理还有一个坑多进程或多线程环境下GPU上下文的管理。如果你用多个worker进程每个进程都会尝试初始化CUDA可能导致显存爆炸。解决方案是用一个单独的推理进程其他进程通过IPC把请求发过去。或者用torch.multiprocessing的spawn模式配合显存共享策略。4.3 推理优化的几个实用手段在不改变模型结构的前提下有几个立竿见影的优化手段。第一是算子融合。PyTorch 2.0之后的torch.compile可以自动做这件事把多个小算子合并成一个大算子减少kernel launch的开销。我实测在Transformer类模型上开启compile后推理速度提升20%到40%。但要注意compile有编译时间第一次推理会特别慢所以一定要在服务启动时做预热。第二是量化。把FP32的权重转成INT8模型体积缩小4倍推理速度提升2到3倍精度损失通常在1%以内。PyTorch提供了动态量化和静态量化两种方式。动态量化最简单一行代码就能用静态量化需要校准数据但效果更好。我的建议是先用动态量化试水如果精度不达标再考虑静态量化。第三是ONNX Runtime。把PyTorch模型导出成ONNX格式用ONNX Runtime推理通常比原生PyTorch快10%到30%尤其是在CPU上。导出时要注意opset版本和动态轴设置否则可能导出失败或者行为不一致。注意任何优化手段都要做A/B测试。用同一批输入分别跑优化前后的模型逐条比对输出差异。差异在可接受范围内才能上线。5. 服务层接口设计、并发控制与优雅降级5.1 接口协议的选择与请求校验AI服务的接口通常用HTTP/REST或者gRPC。REST简单通用调试方便适合对外暴露gRPC性能好支持流式传输适合内部服务间调用。我的习惯是对外REST、对内gRPC用同一个推理后端。接口设计上请求体要包含足够的元信息输入数据、请求ID用于追踪、可选的参数覆盖比如临时调整最大长度。响应体要包含结果、模型版本、耗时、请求ID。请求ID特别重要它能把一次请求在日志、监控、追踪系统里串起来。请求校验要严格。用pydantic定义请求模型自动做类型检查和范围检查。对于不合法的请求返回400错误并在响应体里说明具体哪个字段有问题。不要返回500那会让人以为是服务端bug。5.2 并发模型同步、异步还是多进程Python的GIL让并发变得复杂。对于AI推理这种计算密集型任务多线程并不能真正并行。所以通常有三种方案同步多进程用gunicorn起多个worker进程每个进程独立加载模型。简单粗暴但显存占用是worker数的倍数。异步单进程用FastAPI的async接口配合一个线程池或进程池做推理。适合IO密集但推理量不大的场景。专用推理服务用Triton Inference Server或者自己写一个推理进程通过HTTP/gRPC接收请求内部做批处理和调度。这是最专业的方案但复杂度也最高。我一般从同步多进程开始worker数设为CPU核数或者GPU数的整数倍。等QPS上来了再迁移到专用推理服务。不要一开始就上最复杂的方案那是给自己找麻烦。5.3 超时、重试与降级策略线上服务必须有超时控制。推理超时通常设为P99延迟的2到3倍。超时后要返回明确的错误而不是让请求一直挂着。重试要谨慎对于幂等的推理请求可以重试一次对于非幂等的不要自动重试让客户端决定。降级策略是保命手段。当推理服务不可用时可以返回一个默认结果、一个缓存结果或者一个简化模型的结果。比如一个推荐系统主模型挂了可以降级到热门榜单。降级逻辑要提前写好并测试不要等故障发生了才临时想。我自己的经验是降级开关要能动态配置不需要重启服务就能生效。可以用配置中心或者环境变量来实现。每次上线新模型时都要问自己如果这个模型挂了系统还能提供什么价值6. 可观测层延迟、吞吐与数据漂移的监控6.1 必须监控的四个黄金指标AI服务的监控和普通Web服务有重叠也有差异。四个黄金指标是延迟Latency、吞吐Throughput、错误率Error Rate、饱和度Saturation。延迟要分P50、P90、P99来看平均值会骗人。吞吐要区分请求数和处理的数据量。错误率要区分客户端错误和服务端错误。饱和度要看CPU、内存、显存、队列长度。除了这些通用指标AI服务还要额外监控输入数据分布比如文本长度分布、类别分布、输出分布比如预测类别的比例、模型版本。这些指标能帮你发现数据漂移和模型退化。我习惯用Prometheus收集指标用Grafana做面板。每个服务暴露一个/metrics接口用prometheus_client库打点。面板上至少要有QPS曲线、P99延迟曲线、错误率曲线、显存占用曲线。这四张图放在最上面一眼就能看出系统是否健康。6.2 日志记录记什么、怎么记、存多久日志是排查问题的第一手资料。AI服务的日志要记录请求ID、输入摘要不要记完整输入注意隐私、输出摘要、模型版本、耗时、是否命中缓存、是否降级。输入摘要可以记长度、哈希值或者前N个字符。日志级别要合理使用。DEBUG级别记录详细输入输出只在排查问题时临时开启。INFO级别记录每次请求的关键信息。WARN级别记录降级、重试、校验失败。ERROR级别记录异常和超时。不要所有东西都打INFO否则日志量会爆炸。存储方面本地文件适合开发环境生产环境建议用集中式日志系统。保留时间根据合规要求和排查需求来定通常至少保留7天重要服务保留30天。6.3 数据漂移检测的简易实现数据漂移是指线上输入数据的分布和训练数据不一致。这是模型效果下降的主要原因之一。检测方法有很多最简单的就是统计输入特征的均值和方差和训练时的基准做对比。如果偏离超过阈值就告警。对于文本任务可以监控文本长度的分布、词汇表的覆盖率、OOV未登录词的比例。对于图像任务可以监控亮度、对比度、尺寸的分布。这些统计量计算成本很低可以定期比如每小时跑一次。发现漂移后怎么办不一定要立刻重新训练模型。先分析原因是上游数据源变了还是用户行为变了如果是暂时的波动可能不需要处理。如果是持续漂移那就需要收集新数据重新训练或微调模型。这个过程要有预案不要等效果崩了才手忙脚乱。7. 踩坑实录那些让我熬夜的典型问题7.1 预处理不一致导致的“灵异事件”有一次上线一个新模型离线评估准确率92%上线后只有70%。排查了一整天最后发现是训练时用的分词器版本和推理时用的版本不一致。训练时用的是某个库的2.3版本推理环境里装的是2.4版本两个版本对某些特殊字符的处理逻辑变了。就这么一个小差异导致大量样本的分词结果不同。教训所有预处理依赖都要锁定版本并且训练和推理共用同一个环境镜像。如果做不到就写对照测试用一批固定样本验证两边输出完全一致。7.2 显存泄漏一个不容易发现的慢性病显存泄漏不会立刻让服务崩溃但会逐渐蚕食可用显存最终导致OOM。常见原因有在推理循环里不断创建新的张量而没有释放、把中间结果存到了全局变量、用了torch.no_grad()但某些操作仍然保留了计算图。排查方法是定期打印显存占用观察是否随时间增长。如果是就用torch.cuda.memory_summary()看详细分配情况。修复方法通常是确保所有中间张量都在循环内部创建和释放不要跨请求持有引用。7.3 批处理大小与延迟的非线性关系很多人以为batch size翻倍延迟也翻倍。实际上在GPU上batch size从1增加到8延迟可能只增加20%因为GPU的并行能力被充分利用了。但从8增加到32延迟可能增加一倍因为显存带宽或计算单元饱和了。所以调batch size时要做实测找到延迟和吞吐的平衡点。我的做法是画一条曲线横轴是batch size纵轴是P99延迟和每秒处理样本数。选择那个“吞吐接近峰值但延迟还在可接受范围”的点。7.4 模型版本管理混乱引发的回滚困难没有版本管理时模型文件通常叫model.pt或者best_model.pt。一旦需要回滚你根本不知道哪个文件对应哪个版本。我现在的做法是模型文件名包含版本号、训练日期、git commit hash比如model_v1.2.3_20240501_abc123.pt。同时在数据库或配置文件里记录当前线上版本回滚时只需要改配置。更进一步可以用模型注册中心来管理但那是后话。至少先把命名规范建立起来这个成本极低收益极高。8. 从能跑到好用我的迭代路线建议如果你已经跟着上面的思路把最小系统跑起来了接下来怎么迭代我的建议是按这个顺序来第一步把日志和监控补齐。没有可观测性后面所有优化都是盲人摸象。先能看清系统在干什么再谈优化。第二步做性能压测。用locust或者wrk给服务加压找到当前的瓶颈在哪里。是CPU、GPU、内存还是IO瓶颈不同优化手段完全不同。第三步引入缓存。如果同样的输入反复出现缓存结果能大幅降低推理压力。缓存可以用内存字典、Redis或者本地文件。注意设置合理的过期时间和容量上限。第四步做模型量化或编译优化。这是提升推理速度最直接的手段但要在监控完备的前提下做否则出了问题不知道是优化导致的还是其他原因。第五步考虑多模型或多版本并行。比如A/B测试两个模型或者根据请求特征路由到不同的模型。这需要服务层支持动态路由复杂度较高但业务价值也大。每一步都要有回滚方案。我自己的原则是任何改动如果不能在5分钟内回滚就不要上线。这个原则帮我避免了很多次事故。最后分享一个我经常用的检查清单每次上线新模型或新服务前过一遍预处理代码是否和训练一致模型版本是否记录超时和降级是否配置监控面板是否能看到关键指标回滚步骤是否验证过这五个问题都能回答“是”才允许上线。看起来简单但能挡住80%的低级故障。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →