1. 从零搭建AI工程体系,为什么我劝你别急着调包
"ai-engineering-from-scratch"这个标题,第一次看到的时候我愣了一下。市面上讲AI的文章,十篇有八篇在教你pip install之后怎么调API,剩下两篇在讲Transformer的数学推导。但"从零做AI工程"这件事,恰恰卡在中间——它既不是纯算法研究,也不是简单的接口调用,而是把模型、数据、服务、监控这一整条链路真正跑通的那套手艺。
我自己带过几个小团队做AI应用落地,踩过的坑比想象中多得多。最开始我们也觉得,模型嘛,找个开源权重加载进来,包一层FastAPI就能上线。结果真到了生产环境,显存溢出、推理延迟抖动、数据漂移、版本回滚困难,一个接一个冒出来。那时候才明白,AI工程的核心难点从来不在模型本身,而在于围绕模型的那一整套工程化基础设施。
这篇内容我想聊的,就是怎么从零开始,把一套AI工程体系搭起来。不是那种"跑个demo就完事"的教程,而是从目录结构、数据管道、模型服务、推理优化到监控告警,一层一层往下拆。适合谁看?如果你已经会写Python、懂一点深度学习基础,但每次想把模型真正用起来就觉得处处是坑,那这篇就是写给你的。如果你是完全的新手,也没关系,我会尽量用生活化的类比把每个环节讲清楚,让你知道每一步"为什么这么做"。
我个人的习惯是,任何AI项目在写第一行代码之前,先把整个工程的骨架想清楚。因为AI工程和传统后端最大的区别在于:它的"不确定性"太多了。模型输出不确定、数据分布会变、GPU资源有限、推理成本随规模非线性增长。这些不确定性决定了你不能用传统CRUD那套思路来组织代码。下面我就按我自己实际搭项目的顺序,从整体设计开始讲。
2. 整体架构设计与技术选型思路
2.1 为什么AI工程需要独立的分层设计
传统后端项目,通常就是controller-service-dao三层,清晰明了。但AI项目如果照搬这套,很快就会乱。原因很简单:AI系统里多了一个"模型"这个特殊角色,它既像数据(有版本、需要存储),又像代码(有逻辑、需要测试),还像服务(需要部署、需要扩缩容)。你把它塞进任何一层都不合适。
我一般会把AI工程分成这么几层:数据层、模型层、服务层、编排层、可观测层。数据层负责原始数据的采集、清洗、版本管理;模型层管训练、微调、评估、注册;服务层负责推理接口、批处理、流式输出;编排层把多个模型或工具串成工作流;可观测层则盯着延迟、成本、质量这些指标。
这么分的好处是,每一层的变更不会互相污染。比如你换了个更强的基座模型,只需要动模型层和服务层的适配,数据管道和监控体系基本不用改。反过来,如果你把所有逻辑揉在一个main.py里,改一处崩三处,维护成本会指数级上升。
提示:分层不是目的,隔离变化才是。你在设计时问自己一句"这部分未来最可能因为什么而改",答案就是它应该被独立出来的理由。
2.2 技术栈选型的几个关键取舍
选型这块我踩过不少坑,说几个真实的取舍。
推理框架:早期我用Flask直接包模型,简单是简单,但并发一上来就崩。后来换成FastAPI,异步支持好很多,但对于大模型推理,真正的瓶颈在GPU调度而不是Web框架。再后来我引入了专门的推理服务器思路,把模型加载和请求处理分离,Web层只做路由和鉴权,推理层用队列消费请求。这个改动让我们的P99延迟从3秒多降到了800毫秒左右。
向量数据库:做RAG的时候,选型特别纠结。FAISS轻量、快,但不支持持久化和分布式;Milvus功能全,但运维成本高;pgvector直接挂在PostgreSQL上,省事,但数据量大了性能会掉。我的经验是,数据量在百万级以下、团队没有专职运维,优先选pgvector,因为你能少维护一个组件。等真到了千万级再迁移也不迟,迁移成本远小于一开始就上重型方案的维护成本。
模型服务化:这里有个反直觉的点。很多人觉得应该用最流行的推理加速框架,但实际选型要看你的场景。如果是离线批处理,吞吐优先,那就选支持动态批处理的方案;如果是在线交互,延迟优先,那量化和小模型可能比大模型更合适。我见过一个团队硬上70B模型做客服,结果单次响应要5秒,用户体验极差,最后换成7B加检索增强,效果反而更好。
配置管理:这个最容易被忽视。AI项目的配置特别多——模型路径、超参、提示词模板、阈值。我强烈建议用Hydra或者Pydantic Settings这类工具,把配置和代码分离。我们有一次因为提示词硬编码在代码里,改一个标点都要重新部署,效率极低。
2.3 目录结构:一个能撑住三年迭代的骨架
我现在的项目基本都用这套结构,你可以直接抄:
project/ ├── configs/ # 所有配置,按环境分 │ ├── base.yaml │ ├── dev.yaml │ └── prod.yaml ├── data/ # 数据相关 │ ├── raw/ # 原始数据,只读 │ ├── processed/ # 清洗后数据 │ └── schemas/ # 数据校验schema ├── src/ │ ├── data/ # 数据管道代码 │ ├── models/ # 模型定义与训练 │ ├── serving/ # 推理服务 │ ├── pipelines/ # 编排逻辑 │ └── utils/ # 通用工具 ├── tests/ # 测试,按模块对应 ├── notebooks/ # 探索性分析,不进生产 ├── scripts/ # 运维脚本 └── deploy/ # 部署配置关键点在于data/raw永远只读,任何清洗都输出到processed。这样你随时能回溯到原始数据重新处理,不会因为一次错误的清洗把数据毁了。notebooks目录明确标注"不进生产",避免有人把实验代码直接搬上线。
3. 数据管道:AI工程里最脏最累的活
3.1 数据清洗的实操要点与常见陷阱
数据这块,我吃过最大的亏就是"想当然"。有次做文本分类,训练集准确率98%,上线后惨不忍睹。排查半天发现,训练数据里正负样本的标点符号分布不一样——正样本句号多,负样本问号多。模型学的是标点,不是语义。
所以数据清洗第一步,永远是先做数据探查。我一般会写个小脚本,统计每个字段的分布、缺失率、唯一值数量、长度分布。文本数据还要看字符集、特殊符号比例。这些统计结果直接决定你后面怎么清洗。
清洗的具体操作,我按优先级排:
- 去重:精确去重用哈希,近似去重用MinHash或SimHash。重复数据会让模型过拟合,尤其是小数据集。
- 异常值处理:长度过短或过长的样本先标记,人工抽查后再决定删还是留。我一般把长度在P1和P99之外的先隔离。
- 格式统一:全角半角、大小写、空白字符,这些不统一会让tokenizer产生大量冗余token。
- 敏感内容过滤:这个必须做,而且要留审计日志。用规则加模型双重过滤,规则兜底,模型提召回。
注意:清洗规则一定要版本化。我们有一次改了清洗逻辑,但没记录,导致新旧数据混在一起训练,模型表现莫名其妙地波动了两周才找到原因。
3.2 数据版本管理:别再用文件名区分了
"data_v2_final_真的最终版.csv"——这种命名我见过太多次。数据版本管理必须工具化。小团队用DVC就够了,它把数据文件的哈希存在Git里,数据本身放对象存储。大一点可以用LakeFS或者直接上特征平台。
DVC的基本用法很简单:
dvc init dvc add data/raw/train.csv git add data/raw/train.csv.dvc .gitignore git commit -m "add training data v1"这样每次数据变更都会生成新的.dvc文件,和代码版本一一对应。回滚的时候git checkout到对应commit,再dvc checkout就能拿到当时的数据。这个习惯养成后,复现实验的成功率会大幅提升。
3.3 数据校验:上线前的最后一道防线
数据校验我推荐用Great Expectations或者Pydantic。核心是定义清楚"什么样的数据是合法的"。比如:
- 文本字段非空,长度在10到5000之间
- 标签字段必须在预定义的类别集合里
- 数值字段在合理范围内,超出范围的比例不超过1%
这些校验规则要作为数据管道的强制关卡,不通过就阻断下游。我们有一次因为上游数据源改了格式,没做校验,脏数据直接进了训练,浪费了两天GPU时间。后来加了校验,类似问题再没发生过。
校验规则本身也要版本化,并且和模型版本关联。因为模型迭代时,对数据的要求可能变化。比如新模型支持更长的输入,那长度上限就要调整。
4. 模型服务化:从能跑到跑得稳的距离
4.1 推理服务的三种形态与选择依据
模型服务化,我总结下来就三种形态,各有适用场景。
第一种:进程内加载。模型和Web服务在同一个进程,启动时加载模型,请求来了直接推理。优点是简单、延迟低(没有进程间通信)。缺点是模型更新要重启服务,GPU利用率低(一个模型占一张卡)。适合小模型、低并发、快速验证的场景。
第二种:独立推理服务。模型单独跑在一个服务里,Web层通过HTTP或gRPC调用。优点是模型可以独立扩缩容、独立更新。缺点是多了网络开销,需要处理服务发现问题。这是目前最主流的做法,适合大多数生产场景。
第三种:队列异步推理。请求先进队列,推理worker从队列消费,结果通过回调或轮询返回。优点是能削峰填谷、支持长任务。缺点是延迟高、架构复杂。适合批处理、离线任务、或者推理时间很长的场景。
我的选择逻辑是:先问延迟要求,再问并发量,最后问模型更新频率。延迟要求高且并发低,选第一种;延迟要求中等且并发高,选第二种;延迟不敏感且任务重,选第三种。
4.2 动态批处理:提升吞吐的关键手段
动态批处理是推理优化的核心技巧,值得单独讲。原理很简单:把短时间内到达的多个请求合并成一个batch一起推理,充分利用GPU的并行能力。GPU最怕的就是batch size为1,算力利用率可能只有10%。
实现上,我一般用Triton Inference Server或者自己写一个简单的批处理调度器。核心逻辑是:维护一个请求队列,设置最大batch size和最大等待时间。队列满了或者等待超时就触发一次推理。
参数怎么定?最大batch size取决于模型和显存,一般从8开始试,逐步加到显存占用80%左右。最大等待时间取决于你的延迟预算,如果P99要求500毫秒,那等待时间设50到100毫秒比较合适。这两个参数需要压测调优,没有万能值。
我实测过一个7B模型,batch size从1提到16,吞吐提升了近10倍,而单请求延迟只增加了30毫秒左右。这个投入产出比非常高。
4.3 模型版本管理与灰度发布
模型更新比代码更新风险更高,因为效果变化很难用单元测试覆盖。所以灰度发布是必须的。
我的做法是:新模型上线先接5%的流量,同时记录新旧模型的输出和关键指标。观察24小时,如果新模型的延迟、错误率、以及业务指标(比如点击率、满意度)都不差于旧模型,再逐步放量到20%、50%、100%。任何一步指标恶化,立即回滚。
技术上,这需要一个能按比例路由的网关。我们用过Istio的流量切分,也用过自己写的简单路由中间件。核心是请求要带一个稳定的分流标识,比如用户ID的哈希,这样同一个用户始终走同一个模型,避免体验不一致。
模型注册这块,MLflow或者Weights & Biases都能用。关键是要记录:模型文件、训练数据版本、超参、评估指标、上线时间、负责人。这些信息在出问题时是救命的。
5. 推理优化:把每一毫秒和每一分钱都抠出来
5.1 量化:精度与速度的平衡术
量化是我最推荐的优化手段,没有之一。它把模型权重从FP16降到INT8甚至INT4,显存占用直接减半或更多,推理速度也能提升。代价是精度可能下降,但很多时候下降幅度在可接受范围内。
量化的方式主要有两种:训练后量化(PTQ)和量化感知训练(QAT)。PTQ简单,拿训练好的模型直接量化,适合快速验证。QAT在训练时就模拟量化误差,精度保持更好,但需要重新训练。
我一般先用PTQ试,如果精度掉得太多(比如超过2个点),再考虑QAT。工具上,PyTorch的torch.quantization、Hugging Face的bitsandbytes、还有GPTQ、AWQ这些方案都很成熟。
提示:量化后一定要在验证集上重新评估,不能只看loss。有些任务对量化特别敏感,比如需要精细数值判断的回归任务。
5.2 KV Cache与显存优化
大模型推理时,KV Cache会占用大量显存,尤其是长文本场景。优化KV Cache有几个方向:
- PagedAttention:把KV Cache分页管理,减少碎片,vLLM就是靠这个把吞吐提升了好几倍。
- KV Cache量化:把Cache也量化到INT8,显存占用减半。
- 滑动窗口注意力:只保留最近N个token的KV,适合长对话但会损失远期记忆。
显存优化的另一个思路是模型并行。单卡放不下就切到多卡,张量并行切层内,流水线并行切层间。但并行会引入通信开销,卡越多效率越低。我的经验是,2到4卡的并行效率还能接受,再多就要仔细评估了。
5.3 成本核算:每次推理到底花多少钱
这个很多团队不算,但我觉得必须算。公式很简单:
单次推理成本 = (GPU小时成本 / 3600) × 单次推理耗时(秒) / 并发数
举个例子,一张A100按每小时10元算,单次推理200毫秒,batch size为8,那单次成本大约是 10/3600 × 0.2 / 8 ≈ 0.00007元。看起来很少,但如果每天有1000万次调用,一天就是700元,一个月两万多。
算清楚这个账,你才知道优化值不值得。比如量化能把延迟降30%,那一个月就省6000多,投入几天优化时间完全划算。反过来,如果调用量很小,那优化优先级就该往后放。
6. 可观测性:看不见的问题最致命
6.1 必须监控的四类指标
AI系统的监控和传统后端不一样,我一般盯四类指标。
第一类:系统指标。GPU利用率、显存占用、CPU、内存、网络。这些用Prometheus加Grafana就能搞定。GPU利用率特别重要,长期低于30%说明资源浪费,长期高于90%说明该扩容了。
第二类:性能指标。QPS、P50/P95/P99延迟、错误率、超时率。延迟要分阶段记录:排队时间、预处理时间、推理时间、后处理时间。这样出问题能快速定位瓶颈在哪。
第三类:质量指标。这个最难,但最重要。分类任务看置信度分布,生成任务看输出长度、重复率、以及人工抽检的满意度。我一般会定期采样一批线上请求,人工标注后算准确率,作为质量基线。
第四类:业务指标。最终还是要看业务效果,比如转化率、留存、客诉率。技术指标好但业务指标差,说明模型优化方向可能错了。
6.2 日志与追踪:出问题时怎么快速定位
日志我建议结构化,用JSON格式,每条日志带上request_id、user_id、model_version、各阶段耗时。这样出问题能按request_id串起整个链路。
分布式追踪用OpenTelemetry,把Web层、推理层、数据层的span连起来。有一次我们P99延迟突然飙升,靠追踪发现是某个下游特征服务变慢,导致推理前等待时间变长。没有追踪的话,可能要在几个服务间来回排查很久。
注意:日志里千万别记录完整的用户输入和模型输出,尤其是涉及隐私的场景。我一般只记录哈希值和长度,需要排查时再按request_id去加密存储里取。
6.3 告警策略:别让告警变成狼来了
告警设置不好,要么漏报要么误报。我的原则是:告警必须可行动。收到告警的人应该知道下一步做什么,否则这个告警就不该存在。
具体设置上,我用分层告警。P1告警(电话+短信)只给真正紧急的,比如服务完全不可用、错误率超过10%。P2告警(即时消息)给需要关注的,比如延迟超过阈值、GPU利用率持续过高。P3告警(邮件)给趋势性的,比如成本周环比上涨20%。
阈值不要拍脑袋定,用历史数据的P99再加一点余量。而且要设置告警抑制,避免一个故障触发几十条告警。我们有一次数据库抖动,结果触发了上百条告警,把真正重要的信息淹没了。
7. 常见问题与排查技巧实录
7.1 推理延迟突然飙升怎么查
这是最常见的问题,我总结了一个排查顺序:
- 看监控大盘:是全局慢还是个别实例慢?全局慢可能是流量突增或下游依赖问题,个别慢可能是那台机器有问题。
- 看GPU指标:利用率是不是满了?显存是不是快爆了?如果显存接近上限,会触发频繁的显存交换,延迟会飙升。
- 看输入分布:是不是突然来了很多长文本请求?长文本的推理时间可能是短文本的几十倍。
- 看队列长度:如果请求在排队,说明推理能力不足,需要扩容或优化。
- 看下游依赖:特征服务、数据库、缓存,任何一个变慢都会传导上来。
我遇到过一次,延迟从200毫秒涨到2秒,最后发现是某个请求带了超长输入,把batch撑大了,导致同批次其他请求都跟着等。后来加了输入长度限制和超长请求单独处理,问题解决。
7.2 模型效果线上不如线下怎么办
这个太常见了,原因通常有几个:
- 数据分布不一致:线下测试集和线上真实数据分布不同。解决办法是持续收集线上数据,定期更新测试集。
- 特征穿越:训练时用了未来信息,线下评估虚高。这个要仔细检查特征计算逻辑。
- 预处理不一致:线下和线上的预处理代码不同步。解决办法是把预处理逻辑封装成共享库,两边调用同一份代码。
- 评估指标不匹配:线下看准确率,线上业务看的是别的。要确保优化目标和业务目标一致。
我的经验是,线上线下效果差距在5%以内算正常,超过10%一定要查。
7.3 显存溢出(OOM)的排查与预防
OOM是AI工程的日常。排查思路:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 启动就OOM | 模型太大 | 量化、模型并行、换小模型 |
| 运行中OOM | 输入太长或batch太大 | 限制输入长度、动态batch |
| 逐渐OOM | 显存泄漏 | 检查是否有张量未释放、缓存未清理 |
| 偶发OOM | 峰值请求 | 加显存余量、请求排队 |
预防上,我一般留20%的显存余量,不把卡跑满。另外用torch.cuda.empty_cache()定期清理,但别频繁调用,会影响性能。
7.4 常见问题速查表
| 问题 | 首要排查点 | 快速验证方法 |
|---|---|---|
| 延迟高 | GPU利用率、队列长度 | 压测单请求延迟 |
| 吞吐低 | batch size、并发数 | 逐步增大batch看吞吐曲线 |
| 效果差 | 数据分布、预处理 | 对比线上线下同一批数据 |
| 成本高 | 单次成本、调用量 | 算成本公式,找优化空间 |
| 不稳定 | 错误日志、依赖服务 | 看错误率时间分布 |
8. 我踩过的坑和几条实在建议
搭AI工程这几年,坑踩了不少,说几条我觉得最有价值的。
第一,别过早优化。我见过团队一上来就搞微服务、搞K8s、搞特征平台,结果模型本身还没跑通。正确的顺序是:先让模型能跑,再让它跑得稳,最后才让它跑得快、跑得省。每一步都验证过再往下走。
第二,测试要覆盖"坏情况"。正常输入谁都能处理,关键是空输入、超长输入、特殊字符、并发冲突这些边界情况。我现在的测试用例里,边界情况占了一半以上。
第三,文档和注释要写"为什么"。代码本身能说明"做什么",但"为什么这么做"只有写的人知道。过三个月回来看,没有注释的代码就是天书。我一般要求关键决策点必须写清楚背景和取舍理由。
第四,保持简单。能用简单方案解决的,别上复杂方案。一个单体能搞定的事,别拆成三个服务。技术选型的第一原则是"够用就好",而不是"越先进越好"。复杂度的代价往往在后期才显现,那时候改起来就难了。
第五,定期回顾指标。我每个月会花半天时间,把延迟、成本、质量这些指标拉出来看趋势。很多问题不是突然出现的,而是慢慢恶化的。定期回顾能在问题变大之前发现它。
最后分享一个小技巧:给每个模型和数据集起个有意义的名字,并记录在统一的注册表里。别用model_final、data_new这种名字。我现在的命名规范是{任务}-{模型}-{版本}-{日期},比如intent-bert-v3-20240115。看起来是小事,但在多模型多版本的环境里,这个习惯能省下大量沟通成本。
这套东西不是一天搭起来的,我也是从一个main.py开始,遇到问题解决问题,慢慢演化成现在的结构。你不需要一开始就追求完美,但每一步都要知道自己在解决什么问题、为什么这么解决。这才是"从零做AI工程"的真正含义。