☰
AI工程化从零到一:全链路设计与踩坑复盘
2026/9/30 4:10:44 网站建设 项目流程

说到“ai-engineering”,我最早是在一次内部技术分享上听到这个词,当时第一反应是“这不就是把模型训练完扔上线吗”。真正动手做下去才发现,从零开始把一个AI项目做成能稳定运行的服务,和跑通一个Jupyter Notebook完全是两码事。这篇文章就当是我把一套从零启动的AI工程化项目重新梳理后的复盘,给准备入坑的朋友一份可以照着走的路线图。我会尽量把设计思路、技术选型、踩坑记录都说清楚,不会只丢一堆名词。

1. 先把“AI工程化”这个词拆明白

1.1 它解决的并不是模型效果问题

很多人以为AI工程化的核心是“把模型效果做得更好”,但工作几年之后我越来越觉得,模型效果只是其中一环,工程化真正解决的是“可靠交付”的问题。学术项目里,只要离线指标达标就算成功;生产环境里,一个模型从训练完成到被业务方稳定调用,中间还隔着数据处理、版本管理、模型部署、服务监控、持续迭代这些环节。任何一个环节掉链子,离线Acc再高也白搭。

打个比方,算法工程师交出的是“一道研发好的菜”,而AI工程化要做的是“把这道菜变成任意一家门店都能稳定复制的标准出品”。你要考虑的不是某一次做得好吃,而是能不能每次都在同样时间内出餐,原材料变了怎么办,顾客投诉怎么追踪,口味怎么定期更新。放到AI场景里,就是数据变了怎么办,模型精度下降怎么发现,服务宕了怎么回滚,新版本怎么灰度上线。

所以“ai-engineering-from-scratch”这个项目,本质上是在搭一套完整的、可重复的AI交付体系。从零开始,意味着没有现成的平台基建,也没有成熟的团队分工,所有环节都要自己先跑通。这个过程很难一步到位,但一旦把骨架搭起来,后续再往上加模型、加业务场景,都会顺手非常多。

1.2 很多团队容易卡在哪

我见过不少团队,模型训练已经跑得很溜,但工程化推进却极其痛苦。最典型的卡点是:代码能跑但别人跑不起来,模型能出结果但没人说得清是拿哪份数据训的,服务上线后一更新就出问题,出现问题后排查日志像大海捞针。

这些问题的根源往往不是某个人技术不行,而是整个项目缺少“纪律性”。模型训练代码里写死了数据路径,换台机器就崩;数据集在某个同事的电脑上,没人知道是哪个版本;模型文件有十几个命名“final_final_v2”,谁也分不清哪个才是线上正在用的。这些细节在Notebook里也许不算致命,一旦要上线,每一个都是事故。

所以从零开始做AI工程化,第一步不是选多牛的模型,而是先建立一套约束。约束不是限制效率,而是保证所有人都能在同一个基础上协作。后面我会具体说怎么落地这个约束体系。

2. 从零开始的全链路设计思路

2.1 先定义可度量的业务目标

开工之前,第一件事是问清楚:这个AI能力上线之后,业务上到底要改变什么?是客服平均响应时长降低30%,是搜索点击率提升5%,还是审核通过率提高10%?没有业务指标的技术指标没有任何意义。

我自己犯过一个错误:一开始只顾着优化模型的F1值,认为分数够高就万事大吉。结果模型上线后业务方反馈“没有用”,因为离线指标和线上真实的用户行为有很大偏差。后来才意识到,离线F1是模型视角,业务方关心的却是单位成本、处理时效、用户体验。AI工程化项目一定要从第一天就定义好业务口径,并且建立离线指标和在线业务指标之间的映射关系。

通常我会把指标分成两层。第一层是模型指标,比如准确率、召回率、AUC;第二层是业务指标,比如转化率、留存率、成本节约幅度。每一轮模型迭代,都要同时回答两个问题:模型指标涨了多少?业务指标有没有可预期的正向变化?如果模型指标涨了但业务指标推不出来,那这个迭代宁可不上。

2.2 数据、模型、交付三条线并行

从项目实施的角度,我会把工作拆成三条线:数据线、模型线、交付线。三条线不是先后顺序,而是并行推进。数据线负责把原始数据变成高质量的、可版本化的训练样本;模型线负责实验、调参、评估、产出模型产物;交付线负责把模型封装成服务,部署到对应环境,并接上监控和告警。

常见的问题是把这三条线串行来做,先等数据攒齐,再训练模型,最后才考虑部署。这种方式在前期看起来稳妥,但周期太长,而且到了交付阶段才会发现很多问题,比如数据格式不满足服务要求、模型推理速度跟不上业务高峰、接口设计不满足调用方习惯。

最佳做法是第一条迭代就用最小数据集、最小模型走通全链路。哪怕效果很差,也要先把“数据到服务”的管道打通。管道通了,后面换更好的数据、换更强的模型,都只是替换其中的一个环节,而不是重新做一遍。

2.3 技术选型:优先通用框架,而不是什么都自己造

技术选型上,我的原则很朴素:社区越活跃、生态越成熟的方案优先选。自己造轮子只发生在“实在没有合适方案”的时候,而不是“为了显得我们团队有技术深度”。

具体到这套从零项目,我的基本选型如下:

环节选型理由
开发语言PythonAI生态最全,团队上手成本低
深度学习框架PyTorch调试友好,社区资源多
数据处理HuggingFace Datasets自带缓存、切片、随机种子控制,比手写Dataset省心
实验追踪MLflow参数、指标、模型统一记录,部署流程成熟
推理服务FastAPI轻量、文档自动生成、异步支持好
容器化Docker + Compose环境一致性最好,本地切换成本低
监控Prometheus + Grafana业界标准,后续扩展告警方便

这套组合不是唯一正确答案,但它能覆盖从实验到上线的绝大多数需求。我不建议一开始就引入Kubernetes。单机服务和容器编排还没有跑通的情况下,Kubernetes只会带来额外的复杂度。先把最小闭环跑起来,确认瓶颈在某一个环节之后,再针对性地引入更重的组件。

3. 核心实操:搭一个最小可用的AI服务

3.1 环境准备与项目结构

我习惯用一个非常工程化的目录结构来启动项目,哪怕最初只是单个模型实验也保持这个架子。目录结构如下:

ai-engineering-from-scratch/ ├── configs/ # 配置文件 ├── data/ │ ├── raw/ # 原始数据,只读不写 │ └── processed/ # 清洗后的标准数据 ├── models/ # 模型产物,按版本存放 ├── notebooks/ # 探索性分析脚本 ├── src/ │ ├── data/ # 数据加载与清洗逻辑 │ ├── models/ # 训练与推理逻辑 │ └── serving/ # API服务相关代码 ├── tests/ # 单元测试和集成测试 ├── pyproject.toml └── README.md

这个结构看起来很普通,但每个目录都有边界:data/raw只放原始文件,任何人都不要直接修改;data/processed由脚本生成,脚本要做重复性验证;models下面每个模型都必须带版本号,不允许出现含义不明的名称;notebooks只做探索,不能成为最终代码的载体。

环境管理我会直接上Poetry或uv,不推荐裸放在全局Python里。项目依赖必须锁定,否则三个月后就复现不了当时的训练环境。依赖锁定不是做给别人看的,是为了让你自己在大版本升级时少踩坑。

环境准备好后,第一步写一个简单的数据加载脚本,确认整条数据链路是通的。不要一上来就加载大模型、跑大规模训练,那样一旦出错,定位成本会很高。

3.2 数据准备与特征工程

我以文本分类任务为例,这个任务最典型,也最容易扩展到其他场景。数据来源可能是业务工单、商品评论、日志文本中的某一段。原始数据往往有大量噪声,比如空文本、重复样本、URL、乱码符号等。清洗规则不能拍脑袋,要一边探查数据分布一边确认。

from datasets import load_dataset # 假设你已经把原始数据放到了 data/raw dataset = load_dataset("csv", data_files="data/raw/samples.csv") # 简单清洗 def clean_text(example): text = example["text"].strip() text = text.replace("\n", " ") return {"text": text} dataset = dataset.map(clean_text, num_proc=4) dataset = dataset.filter(lambda x: len(x["text"]) > 0) # 观察类别分布 print(dataset["train"].features) print(dataset["train"].to_pandas()["label"].value_counts())

这里有个容易忽略的点:数据集切分。很多新手直接用train_test_split随机切,但生产中如果数据带有时间属性,必须按时间切,否则很容易高估模型能力。比如前面的数据是1月到5月,测试集随机从中间抽了一些,模型的所谓“未来预测能力”就虚高了。正确做法是训练集用1月到4月,验证集用5月,或者至少保证分组时避免同源样本跨集合。

文本特征方面,除非你有充分理由,否则不要一开始就自己写一整套分词流程。直接用HuggingFace的Tokenizer会更稳健。这里有一个我踩过的坑:训练时和推理时如果用了不同版本的Tokenizer资源文件,或者用了不同的清理函数,线上效果会和张口闭口“预训练模型能力不够”产生奇妙的偏差。所以清洗逻辑必须封装成一个函数,训练和推理复用同一份代码,不要各写一套。

3.3 训练与实验记录

训练脚本不要写成一个巨大的notebook,而是拆成可配置的模块。我自己会写一个train.py,所有可变参数通过配置或命令行传入。配置信息和最终结果一定要记录到同一个地方,我用的是MLflow。

import mlflow from transformers import ( AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments ) mlflow.set_experiment("cls_baseline") with mlflow.start_run(): model_name = "bert-base-chinese" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained( model_name, num_labels=2 ) training_args = TrainingArguments( output_dir="./checkpoints", num_train_epochs=3, per_device_train_batch_size=16, per_device_eval_batch_size=32, evaluation_strategy="epoch", logging_dir="./logs", save_total_limit=2, seed=42, fp16=True, ) trainer = Trainer( model=model, args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["valid"], tokenizer=tokenizer, ) trainer.train() trainer.save_model("./models/cls_v0.1") mlflow.log_param("model_name", model_name) mlflow.log_param("num_train_epochs", 3) mlflow.log_metric("eval_accuracy", ...)

这段代码里最重要的不是我选了什么模型,而是我把“随机种子固定为42”“混合精度开启”这些训练细节固化下来。固定随机种子是因为模型实验需要可复现,否则同样代码跑两次指标不同,你根本分不清是改进还是噪声。混合精度是因为显存受限,后面小节还会具体说。

实验记录不能只记最后的指标。训练数据版本、代码commit号、模型文件路径、推理延迟和模型大小,这些都要一并落库。没有这些信息的模型文件就像没有标签的罐头,你根本不敢吃,哪怕味道再香。

3.4 构建推理服务与容器化

训练完模型,接下来是把它变成一个可以被业务方调用的服务。我一般用FastAPI写一个最小服务,接口信息清晰,自带Swagger文档,联调非常方便。

from fastapi import FastAPI from pydantic import BaseModel from transformers import pipeline app = FastAPI(title="cls-service") pipe = pipeline("text-classification", model="./models/cls_v0.1") class PredictRequest(BaseModel): text: str class PredictResponse(BaseModel): label: str score: float @app.post("/predict") def predict(req: PredictRequest): result = pipe(req.text)[0] return PredictResponse(label=result["label"], score=result["score"])

这里有个重要细节:模型加载路径必须通过配置注入,不要硬编码。因为不同环境下的模型目录可能完全不同,本地可能是./models,测试环境可能是/data/models,线上还可能从一个对象存储下载到本地缓存。

服务写好后,容器化是让所有人都能跑起来的核心步骤。我会用一个简单的Dockerfile:

FROM python:3.10-slim WORKDIR /app COPY pyproject.toml . RUN pip install --no-cache-dir . COPY . . EXPOSE 8000 CMD ["uvicorn", "src.serving.app:app", "--host", "0.0.0.0", "--port", "8000"]

容器化解决的是“本地能跑,线上跑不起来”的经典问题。但要注意,Docker镜像不是越大越好。直接把整个训练环境打进镜像,镜像体积可能到几个GB,上线拉取都会非常痛苦。推理镜像只装需要推理的依赖,没必要包含训练框架全家桶。这也是为什么前面要强调“模型产物”和“训练代码”分离,模型可以放到独立存储中,推理镜像只负责加载和使用。

3.5 上线后的监控与反馈回路

服务能跑只是及格,真正决定项目生死的是上线之后的监控。监控分成两个维度:服务健康和模型健康。

服务健康看的是QPS、P95延迟、错误率、显存占用等。这些指标可以通过Prometheus暴露出来,配合Grafana看板一目了然。如果你没有专门的运维平台,也至少要在代码里加日志,并把日志统一收集。我见过太多服务因为某个样本格式特殊导致处理异常,但错误被静默吞掉,等到用户投诉才发现。

模型健康看的是模型输入分布和输出分布是否发生漂移。线上进来的样本不能直接打标,但你可以观察模型预测的概率分布。如果某一天开始,大量样本的预测概率突然都落在0.48到0.52之间,说明模型对这批数据已经很“不确定”了,这时候就该考虑收集样本重新训练。

反馈回路的意思是:线上不能只是流数据,还要设计一个机制回收“低置信度样本”和“被业务方纠错的样本”,定期汇总成新的训练集。没有反馈回路的AI服务是单向消耗品,效果只会越用越差。哪怕一开始的反馈机制很笨,比如每天导出一次CSV给标注团队人工标,也一定要有。

4. 常见问题与排查技巧实录

4.1 显存不够怎么处理

训练文本模型最常见的问题是单卡显存不够。很多人第一反应是买更多卡,但很多情况下单机多卡也未必划算。我的排查顺序通常是:先看是不是批次大小设置过大。batch_size=32跑不通,就调到16或8。如果调小batch后指标明显波动,可以考虑用gradient_accumulation_steps来模拟更大的batch,逻辑上等价于攒几个小batch再更新一次梯度。

再就是开启混合精度。PyTorch里一行torch.cuda.amp或HuggingFace的fp16=True就能把显存占用大幅降下来。混合精度不是只在低精度下牺牲效果,而是关键计算保持高精度,不会产生显著精度损失。我在文本分类任务中实测,开启fp16后同等设置下显存下降差不多四成,训练速度反而更快。

如果以上还不够,就要考虑模型层面的优化,比如使用更小的预训练模型、加gradient_checkpointing、或者用device_map="auto"把部分层放CPU。注意,这些都是有代价的,不是无脑开。gradient_checkpointing会显著降低训练速度,模型变小可能会牺牲效果,要带着目标去取舍。

实际排查建议先去终端执行watch nvidia-smi,把显存曲线录下来。不要只盯峰值,很多问题在于某一步显存突增,而不是全程爆满。找不到具体是哪一段代码导致显存增长,先二分法注释模块跑,通常很快能定位。

4.2 模型在测试集上表现好,上线却变差

这个问题我至少遇到过三次。每次第一反应都是“线上预处理的坑”,但排查下来原因往往不止一个。

最常见的原因之一是数据分布不一致。训练数据里文本来源集中、长度集中在100字以内,但上线后用户输入长度跨度极大,格式也五花八门。比如训练集里没有表情符号,而线上大量出现表情符号。模型没见过这种写法,输出自然不稳定。

还有一类原因就是“训练和推理的预处理逻辑不一致”。我踩过一次深刻的坑:清洗脚本在处理连续英文字母时用了不同的正则规则,离线测试时文本被清洗后喂给模型,线上服务却漏掉了同一层清洗。结果模型看到的输入文本和训练时完全不同,效果崩盘。现在我会把清洗逻辑写成一个独立函数,并加一个单元测试来保证训练和推理两个入口用的完全一样。

遇到线上效果变差,先不要急着重训。先抽一批线上真实样本和测试集对比分布,统计类别、长度、字符覆盖率。很多问题通过分析样本就能找到答案。找到原因后再决定是补数据、修预处理还是调整模型,盲目重训往往只是把问题暂时掩盖。

4.3 服务延迟高如何定位

延迟高需要先定义高在哪一段。FastAPI服务通常分为三段时间:网络传输、预处理+模型推理、返回序列化。最简单的定位方式是在接口函数里用time.perf_counter()分段计时并打印日志。

import time @app.post("/predict") def predict(req: PredictRequest): t0 = time.perf_counter() clean = clean_text(req.text) t1 = time.perf_counter() result = pipe(clean)[0] t2 = time.perf_counter() logging.info(f"preprocess: {t1-t0:.4f}s, inference: {t2-t1:.4f}s") return ...

如果预处理耗时占比高,检查是不是在请求内重复加载了停用词表、正则每次重新编译。如果是推理耗时高,优先优化模型本身,不要急着加机器。可以试一下把模型转换成ONNX或者用bettertransformer加速,用蒸馏后的轻量模型替换大模型,简单场景下直接缓存重复请求也是立竿见影的做法。

还有一点容易忽略:动态batch。线上请求往往是并发到达的,如果每个请求单独走一次推理,GPU利用率很低。用text-generation-inference这类框架,或者自己在FastAPI里实现简单的并发合并,都能把吞吐量拉高好几倍。不过动态batch会增加单请求延迟,要结合业务场景取舍。

4.4 模型更新与回滚策略

模型上线后不是一劳永逸。每三个月甚至每月都可能重新训练一版。更新模型最忌讳的是直接覆盖线上模型文件,一旦新模型效果不达标,连回滚的机会都没有。

我现在的做法是:每个模型产物都有一个唯一ID,存放在独立的模型目录或模型仓库中。线上服务配置指向具体的模型版本,需要通过部署流程修改配置并重启,而不是直接改文件。发布时先起一个临时实例,加载新模型,做几十个真实请求测试,再把流量切过去。

如果条件允许,做金丝雀发布。简单说就是让5%的流量打到新模型上,对比新旧两个版本的关键指标,比如平均置信度、用户点击率、业务处理成功率。没有异常再逐步放量。没有负载均衡能力时,至少先在一台测试机启动服务,用脚本模拟线上请求做回归,再切主服务。

回滚策略就是保留至少最近N个可用的模型版本。容器环境里,回滚可以是重新部署上一个镜像;非容器环境,就是切换配置并重启服务。操作越简单,越好。我见过团队一更新服务就要同时改五六个地方,出了事故半小时都回不到上一个状态,这种架构再忙也得重做。

最后一点实际体会

做了一套完整的“ai-engineering-from-scratch”之后,我对“工程化”三个字的理解彻底变了。它不是一个工具、一个框架,甚至不是一个岗位能搞定的事,而是一套需要持续投入纪律性的思维方式。对我个人来说,最有用的改变是从“想快速看到模型效果”转向“让每一步结果都可追溯、可复现、可回滚”。后者的起步慢,但积累越久越省时间。

如果你现在正要启动自己的AI工程化项目,我的建议是不要追求一步到位。先选一个简单的场景,哪怕就是做一个文本分类,也要把数据版本、实验记录、服务部署、监控告警全部走通。跑通之后你会发现,后续加再复杂的模型和能力,都只是在一条成熟管道上做替换。框架可以学,工具可以换,但这套从零到一建立起来的工程习惯,才是整个项目最值钱的部分。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询