☰
从零搭建AI工程能力:从Notebook到线上服务的完整路径
2026/10/2 6:54:19 网站建设 项目流程

从零搭建AI工程能力这件事,我前前后后折腾过好几轮。最早的时候我也以为"AI工程"就是调个API、写个prompt、跑个demo,直到真正把一个模型从笔记本上的实验推到能扛住真实流量、能持续迭代、能被人接手维护的状态,才发现中间隔着的根本不是一行pip install,而是一整套工程化的思维和工具链。ai-engineering-from-scratch这个标题之所以值得聊,是因为它戳中了一个很普遍的痛点:网上讲模型原理的资料铺天盖地,讲算法推导的课程一抓一大把,但真正告诉你"一个AI项目从空目录到上线该怎么一步步搭"的内容却少得可怜。这篇东西我想按我自己踩过的顺序,把从零构建AI工程能力这条路径拆开讲清楚,包括每个阶段该学什么、为什么这么排、哪些坑我替你踩过了。不管你是刚转行想入局的新人,还是写了几年业务代码想往AI方向靠的工程师,都能从里面找到能直接上手的东西。

1. 先搞清楚"AI工程"到底在工程什么

很多人一上来就去啃Transformer的论文,结果啃了两个月,连一个能跑的推理服务都搭不出来。这不是学习能力的问题,是路径选错了。在动手之前,得先把"AI工程"这个词拆开看,它到底包含哪些活儿。

1.1 模型训练只是冰山露出水面的那一角

我习惯用一个类比:把AI项目比作开一家餐厅。模型训练相当于研发菜谱,这只是整个餐厅运营里很小的一块。你还需要采购食材(数据管道)、建厨房(训练/推理基础设施)、定标准出餐流程(模型服务化)、保证每道菜口味一致(评测与监控)、根据客人反馈调整(迭代闭环)。一个只会研发菜谱的人,是开不起餐厅的。

落到具体工程上,一个完整的AI系统通常包含这么几层:

  • 数据层:数据的采集、清洗、标注、版本管理、特征存储。这一层最脏最累,但决定了模型效果的天花板。
  • 训练层:实验管理、超参调优、分布式训练、模型检查点管理。这一层拼的是工程效率和算力调度。
  • 服务层:模型推理服务、批处理与流式处理、GPU资源管理、延迟与吞吐优化。这一层直接面对线上流量。
  • 评测层:离线评测、在线A/B、回归测试、幻觉与安全检测。这一层是质量的守门人。
  • 运维层:监控告警、日志追踪、成本控制、版本回滚。这一层决定了系统能不能长期活下去。

你会发现,真正跟"调模型"直接相关的,可能只占整个工作量的两三成。剩下的全是传统软件工程加上数据工程的活儿。所以"from scratch"的第一步,不是学模型,而是建立这个全局地图。

1.2 为什么大多数人卡在"能跑demo"和"能上线"之间

我见过太多这样的情况:一个工程师在Jupyter Notebook里把模型跑通了,准确率也不错,然后老板说"上线吧",他就懵了。因为Notebook和线上服务之间,横着一堆他从来没考虑过的问题。

举个最典型的例子。Notebook里你写model.predict(x),输入是一个样本,输出是一个结果,简单直接。但线上环境里,请求是并发的,可能一秒钟来几百个;输入可能缺字段、可能是脏数据、可能长度超限;模型加载要占显存,你得考虑多个请求怎么共享一个模型实例;推理有延迟,你得考虑超时和降级;服务会挂,你得考虑健康检查和自动重启。这些没有一个是模型本身的问题,全是工程问题。

我自己的经验是,判断一个人是不是真的具备AI工程能力,就看他能不能回答这个问题:"你的模型服务在流量翻十倍的时候会发生什么?"能答上来的人,才算跨过了那道坎。

1.3 从零起步的能力清单该怎么排优先级

既然要"from scratch",就得有个学习顺序。我的建议是按"能最快跑通一个端到端闭环"来排,而不是按知识体系的完整性来排。因为正反馈对坚持学习太重要了。

我推荐的优先级顺序是这样的:

阶段核心能力为什么排这个位置
第一阶段Python工程化 + 基础API调用先能写出结构清晰的代码,能调通一个模型
第二阶段数据处理与向量化基础数据是AI的燃料,不会处理数据后面全白搭
第三阶段模型服务化与容器化把模型变成别人能调用的服务,这是分水岭
第四阶段评测体系与实验管理没有评测就没有迭代方向,全靠玄学
第五阶段监控、成本与迭代闭环让系统能长期活下去,这是专业和业余的区别

注意这个顺序里,我把"深入理解模型原理"放在了贯穿始终的位置,而不是第一阶段。原理要学,但不要等学完原理再动手,边做边补原理效率最高。我见过太多人倒在"先把数学补完再开始"这个执念上。

2. 环境与工具链:别在起跑线上反复折腾

环境配置这件事,看起来是体力活,但它决定了你后面几个月的开发体验。我见过有人光配CUDA环境就配了一周,最后心态崩了。这一章讲讲怎么用最少的折腾把地基打好。

2.1 Python环境管理:为什么我劝你别用系统Python

新手最容易犯的错,就是直接在系统自带的Python上pip install。装到后面依赖冲突,整个环境废掉,只能重装系统。这不是危言耸听,我早期就这么干过。

正确的做法是用虚拟环境隔离每个项目。工具选择上,我的建议是:

  • conda / mamba:如果你要装CUDA相关的库,conda系对二进制依赖的处理更省心,尤其是涉及GPU驱动版本匹配的时候。
  • uv:如果你追求速度和现代工作流,uv现在是我装纯Python包的首选,解析依赖快得离谱,几秒钟搞定以前要几分钟的事。
  • venv + pip:最轻量,适合纯CPU的小项目,不引入额外工具。

我现在的习惯是:涉及深度学习的项目用conda建环境,纯应用层的项目用uv。两者不冲突,各管各的。

具体操作上,一个干净的项目起步大概是这样:

# 用conda创建一个指定Python版本的环境 conda create -n aieng python=3.11 -y conda activate aieng # 或者用uv,速度更快 uv venv --python 3.11 source .venv/bin/activate

提示:Python版本别追最新。3.11或3.12是目前生态兼容性最好的区间,很多AI库对3.13的支持还不完整,踩这个坑不值得。

2.2 依赖锁定:requirements.txt不够用的时候

pip freeze > requirements.txt是很多人的默认操作,但它有个致命问题:它锁定了所有间接依赖,包括那些跟你的系统环境强绑定的包。换一台机器,或者换个操作系统,这个文件可能直接装不上。

更靠谱的做法是用分层依赖管理。我通常会把依赖分成几个文件:

  • requirements.txt:直接依赖,只写你真正import的包,版本用>=或~=给个范围。
  • requirements.lock:完整锁定,由工具自动生成,用于生产环境复现。
  • requirements-dev.txt:开发工具,比如pytest、ruff、mypy,不进生产镜像。

生成lock文件可以用pip-compile(来自pip-tools)或者uv自带的锁定功能。这样做的价值在于:开发时灵活,部署时确定。我吃过太多次"本地能跑线上报错"的亏,根源都是依赖没锁死。

2.3 容器化:把"在我机器上能跑"变成"在哪都能跑"

容器化是AI工程里绕不过去的一环。它的核心价值不是"时髦",而是消除环境差异。你的模型服务最终大概率要跑在服务器上,而服务器上不可能有你的conda环境。

写Dockerfile有几个针对AI场景的要点:

FROM python:3.11-slim # 系统依赖单独一层,利用缓存 RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ && rm -rf /var/lib/apt/lists/* WORKDIR /app # 先拷依赖文件再装,这样改代码不会触发重装依赖 COPY requirements.lock . RUN pip install --no-cache-dir -r requirements.lock # 最后拷代码 COPY . . CMD ["python", "-m", "app.server"]

这里有个关键技巧:把变化频率低的内容放在Dockerfile前面。依赖文件很少变,代码天天变,所以先装依赖再拷代码,能最大化利用镜像层缓存,构建时间能从几分钟降到几秒。

另外,AI镜像往往很大,动辄几个G。控制体积的手段包括:用slim基础镜像、多阶段构建、清理pip缓存、把模型权重挂载而不是打进镜像。模型权重打进镜像是个常见的坏习惯,改一次模型就要重新构建推送整个镜像,效率极低。

3. 数据管道:决定模型上限的隐形战场

模型效果不好,十有八九是数据的问题,而不是模型的问题。但数据这块恰恰是最不性感、最容易被跳过的部分。我见过团队花大价钱调模型,最后发现是训练数据里有一批标注错误的样本在拖后腿。

3.1 数据清洗里那些没人告诉你的脏活

真实世界的数据有多脏?我给你列几个我实际遇到过的:

  • 同一条数据在不同来源里字段名不一样,user_id、userId、uid混着来。
  • 文本里混着全角半角、不可见字符、HTML标签残留。
  • 时间戳有的用秒有的用毫秒,有的还是字符串格式。
  • 类别字段里"北京"和"北京市"被当成两个类别。
  • 重复数据,而且重复得还不完全一样,差一个空格。

处理这些没有银弹,只能靠一套标准化的清洗流程。我的做法是写一个可复用的清洗模块,每个处理步骤都是独立的函数,可以自由组合:

def normalize_text(s: str) -> str: # 统一全角半角、去除多余空白、剥离HTML s = unicodedata.normalize("NFKC", s) s = re.sub(r"<[^>]+>", "", s) s = re.sub(r"\s+", " ", s).strip() return s def dedup_by_similarity(records, threshold=0.95): # 用相似度去重,而不是精确匹配 ...

关键在于,每一步都要能单独测试、单独开关。因为不同数据源需要的清洗步骤不一样,硬编码成一个大函数,后面维护起来就是灾难。

3.2 数据版本管理:模型可复现的前提

这是很多人忽略的一点:你的模型是用哪一版数据训出来的?如果答不上来,那这个模型就没法复现,出了问题也没法追溯。

数据版本管理不需要搞得很复杂。小团队用DVC(Data Version Control)配合Git就够了,大文件存对象存储,Git里只存指针。核心原则是:每次训练用的数据集,都要有一个唯一的、不可变的标识。可以是哈希值,可以是版本号,但必须能精确定位到当时那份数据。

我踩过的坑是:早期直接用data/latest.csv这种路径,结果数据一更新,之前的实验结果全对不上了。后来改成data/v20240115/这种带日期的目录,再配合一个manifest文件记录每个版本的来源和处理脚本,才把可复现性建立起来。

3.3 从原始数据到训练样本的转换链路

原始数据到能喂给模型的样本,中间通常要经过好几道转换。这条链路的设计原则是:每一步都可检查、可回放。

一个典型的链路长这样:

  1. 原始数据落盘(对象存储或数据库)
  2. 清洗与标准化
  3. 切分训练/验证/测试集
  4. 构造特征或指令样本
  5. 格式化成模型需要的输入格式
  6. 打包成训练可读的格式

我强烈建议在每一步之间都落一次中间结果。有人觉得这样浪费存储,但当你发现最终样本有问题、需要回溯是哪一步出错时,有中间结果能救命。没有中间结果,你只能从头重跑整条链路,时间成本高得多。

还有一个经验:切分数据集要在清洗之后、构造样本之前做。如果先构造样本再切分,很容易出现数据泄漏——比如同一个用户的不同样本被分到了训练集和测试集,导致测试指标虚高。这个坑我在一个推荐项目里踩过,离线指标漂亮得不行,一上线就拉胯,排查了好久才发现是切分顺序错了。

4. 模型服务化:从Notebook到线上接口的关键一跃

这是整个AI工程里我最看重的一环,也是区分"会调模型"和"会做AI工程"的分水岭。一个模型在Notebook里跑通,和它作为一个服务稳定运行,中间隔着一整套工程实践。

4.1 推理服务的三种形态与选型逻辑

模型服务化大致有三种形态,各有适用场景:

形态特点适用场景
在线同步服务请求-响应,低延迟实时对话、搜索排序、推荐
批量离线推理定时跑大批量数据离线打标、报表生成、数据增强
流式推理持续消费数据流实时风控、监控告警

选型的核心是看你的业务对延迟的要求。如果用户点一下要等结果,那就是在线同步;如果是每天凌晨跑一遍全量数据,那就是批量。别把批量任务硬做成在线服务,也别把实时需求塞进批处理,方向错了后面全是坑。

在线服务的技术栈,Python生态里FastAPI是目前的主流选择,异步支持好、性能够用、开发效率高。如果你对性能有极致要求,可以考虑用Triton Inference Server这类专门的推理服务器,它支持动态批处理、多模型管理,但学习曲线更陡。

4.2 一个能扛住并发的最小服务长什么样

我见过太多"能跑但一并发就崩"的服务。一个真正能扛并发的推理服务,至少要处理好这几件事:

模型只加载一次。这是最基本的。如果你在每个请求里都加载模型,那显存瞬间就爆了。正确做法是在服务启动时加载,全局共享。

from fastapi import FastAPI from contextlib import asynccontextmanager model = None @asynccontextmanager async def lifespan(app: FastAPI): global model model = load_model() # 启动时加载一次 yield # 关闭时清理 app = FastAPI(lifespan=lifespan) @app.post("/predict") async def predict(req: PredictRequest): result = model.infer(req.text) return {"result": result}

输入校验要严格。线上什么脏数据都有,长度超限、字段缺失、类型不对,都得在入口拦住,不能让它们进到模型里。用Pydantic定义请求模型,校验自动完成。

要有超时和降级。模型推理偶尔会卡住,如果不设超时,一个卡住的请求会拖垮整个服务。设置合理的超时时间,超时后返回降级结果或明确错误。

批处理提升吞吐。单个请求推理GPU利用率很低,把短时间内到达的多个请求攒成一批一起推理,吞吐能提升好几倍。这就是所谓的动态批处理(dynamic batching)。实现上可以用一个队列加一个后台批处理协程。

4.3 显存管理与批处理参数的取舍

GPU显存是稀缺资源,管理不好就是OOM(显存溢出)。几个关键点:

  • 批大小不是越大越好。批大小受显存限制,也受延迟要求限制。批越大吞吐越高,但单个请求的等待时间也越长。要在吞吐和延迟之间找平衡点。
  • 序列长度直接影响显存。处理长文本时,显存占用是随长度平方增长的(注意力机制的原因)。如果你的服务要处理变长输入,最好按长度分桶,避免短请求被长请求拖累。
  • 及时释放中间张量。推理时产生的中间结果如果不及时释放,会累积占用显存。用torch.no_grad()包裹推理过程,能省下不少显存。

我实测下来的经验是:先确定你能接受的最大延迟,然后在这个延迟约束下把批大小调到最大。比如要求P99延迟不超过500ms,那就逐步增大批大小,直到延迟逼近这个阈值,取略小一点的值作为配置。

5. 评测与实验管理:让迭代有方向而不是靠玄学

"我觉得这个版本更好"——这句话在AI工程里是危险的。没有量化评测,所有的"更好"都是主观感受。评测体系是让AI项目从手工作坊走向工业化的关键。

5.1 离线评测指标怎么选才不骗自己

选指标这件事,坑特别多。最典型的就是指标和业务目标脱节。比如你做一个问答系统,用BLEU或ROUGE这种文本相似度指标,但用户真正在意的是答案对不对、有没有幻觉。指标高不代表用户满意。

我的建议是分层设计指标:

  • 基础指标:准确率、召回率、F1,用于快速筛选。
  • 任务指标:针对具体任务设计的指标,比如问答的答案准确率、检索的命中率。
  • 业务指标:最终反映业务价值的,比如用户点击率、转化率、人工评分。

离线阶段主要看前两层,但心里要清楚,最终决定成败的是第三层。离线指标只是筛选器,不是终点。

还有一个必须警惕的问题:测试集污染。如果你的测试集数据在训练时被模型见过了,那指标就是虚高的。要确保测试集和训练集严格隔离,而且测试集要定期更新,避免模型"背答案"。

5.2 实验追踪:别再用Excel记结果了

我早期用Excel记实验结果,记到几十个实验之后就彻底乱了:哪个实验用了什么参数、对应哪个数据版本、指标是多少,全靠翻聊天记录。后来换成专门的实验追踪工具,效率提升是数量级的。

主流的选择有MLflow、Weights & Biases、TensorBoard等。核心功能都差不多:记录每次实验的参数、指标、产物(模型文件、图表),支持对比和检索。

用起来其实很简单,核心就是在训练脚本里加几行记录代码:

import mlflow with mlflow.start_run(): mlflow.log_params({"lr": 1e-4, "batch_size": 32}) for epoch in range(epochs): loss = train_one_epoch() mlflow.log_metric("loss", loss, step=epoch) mlflow.log_artifact("model.pt")

关键不在于工具多高级,而在于养成记录的习惯。每次实验,不管成功失败,都记下来。失败的实验同样有价值,它能告诉你哪些方向走不通,避免重复踩坑。

5.3 回归测试:防止新版本悄悄变差

这是很多团队会忽略的一环。你优化了模型在A场景的表现,结果B场景悄悄退化了,如果没做回归测试,这个问题可能要等用户投诉才发现。

回归测试的做法是:维护一组"黄金测试用例",覆盖各种典型场景和边界情况,每次模型更新都跑一遍,确保没有明显退化。这组用例不需要很大,几十到几百条就够,但一定要有代表性。

我通常会把回归测试集成到CI流程里:模型训练完自动跑评测,指标低于阈值就阻断发布。这样能把问题拦在上线之前,而不是等线上出事再回滚。

6. 上线之后:监控、成本与持续迭代

模型上线不是终点,而是另一个起点。线上环境会暴露一堆离线阶段发现不了的问题,而这些问题只能靠监控和迭代来解决。

6.1 线上监控该盯哪些指标

监控分两个层面:系统层面和模型层面。

系统层面的指标比较标准:QPS、延迟(P50/P95/P99)、错误率、GPU利用率、显存占用。这些用Prometheus + Grafana就能搞定。

模型层面的指标才是AI服务特有的,也是最容易被忽略的:

  • 输入分布漂移:线上请求的输入分布和训练数据分布是否一致。如果用户开始问一些训练时没见过的问题,模型效果会下降。
  • 输出分布漂移:模型输出的分布是否稳定。比如突然大量输出"我不知道",可能意味着模型遇到了不擅长的输入。
  • 置信度分布:模型对自己输出的置信度。置信度普遍偏低,说明模型对当前输入没把握。
  • 业务反馈:用户的点赞、点踩、追问率等,这是最真实的信号。

我踩过的坑是:只监控了系统指标,服务一直很稳定,但模型效果在悄悄下降,因为用户的问题类型在慢慢变化。等发现的时候,已经积累了大量差评。后来加了输入分布监控,才及时捕捉到这种漂移。

6.2 推理成本控制:GPU不是免费的

GPU成本是AI服务的大头。控制成本的手段有这么几个:

  • 选择合适的模型规模。不是越大越好。很多任务用小模型加好的prompt工程,效果能接近大模型,但成本低一个数量级。上线前一定要做模型规模的性价比评估。
  • 量化与蒸馏。把模型量化成INT8或INT4,显存占用和推理延迟都能大幅下降,效果损失通常在可接受范围内。
  • 缓存。对于重复的请求,直接返回缓存结果。很多场景下请求重复率很高,缓存能省下大量算力。
  • 弹性伸缩。流量低谷时缩容,高峰时扩容。别让GPU在半夜空转。

我做过一个粗略的测算:一个7B参数的模型,用FP16推理,单张消费级显卡大概能支撑每秒几十个请求(取决于输入长度)。如果换成INT8量化,吞吐能翻倍。这个账在上线前一定要算清楚,不然账单会让你怀疑人生。

6.3 迭代闭环:从线上反馈回到训练数据

AI系统最理想的状态是形成一个闭环:线上收集反馈,反馈变成标注数据,标注数据进入训练集,训练出新模型,新模型再上线。这个闭环转得越快,系统进化得越快。

但闭环不是自动的,需要工程支撑。关键环节包括:

  1. 反馈采集:在服务里埋点,记录用户的各种反馈信号。
  2. 数据回流:把线上数据定期回流到数据仓库,和训练数据统一管理。
  3. 标注流程:对回流的数据进行标注,这步往往需要人工介入。
  4. 触发训练:当积累到一定量的新数据,触发新一轮训练。
  5. 评测与发布:新模型经过评测后,灰度发布,观察线上表现。

这个闭环里,最容易被低估的是标注环节的成本。人工标注又慢又贵,所以要想办法降低标注需求,比如用主动学习挑出最有价值的样本去标注,而不是全量标注。

7. 一些我踩过之后才明白的事

写到这里,想单独拎几条经验出来,都是我在实际项目里交了学费才明白的,可能比前面那些方法论更值钱。

第一条:先跑通端到端,再优化局部。我早期总想把每一层都做到完美再往下走,结果卡在数据清洗上一个月,整个项目毫无进展。后来学乖了,先用最粗糙的数据、最简单的模型、最简陋的服务,把整条链路跑通,然后再逐个环节优化。有了端到端的框架,优化才有参照系。

第二条:可复现性比性能更重要。一个跑得快但结果不可复现的系统,是没法迭代的。我宁愿要一个慢一点但每次结果都一样的系统,也不要一个快但结果飘忽的系统。可复现性来自:固定的随机种子、锁定的依赖版本、版本化的数据、记录完整的实验。

第三条:别迷信大模型。很多任务用小模型加好的工程手段就能解决,硬上大模型只会让成本和延迟失控。选模型要看任务需求,不是看排行榜。我见过用几十亿参数模型做简单分类任务的,纯属浪费。

第四条:监控要早做,不要等出事。监控这东西,平时觉得没用,出事的时候才知道它的价值。而且监控要覆盖到模型层面,不能只看系统指标。我现在的习惯是,服务上线第一天就把监控配好,哪怕指标还不全,先有个基础框架。

第五条:文档和注释是给未来的自己看的。AI项目迭代快,三个月后你回头看自己的代码,很可能完全不记得当时为什么这么写。所以关键决策、参数选择、踩过的坑,都要记下来。我现在每个项目都会维护一个DECISIONS.md,记录重要的技术决策和理由,这个习惯救过我好几次。

第六条:成本意识要贯穿始终。从选模型、选硬件到设计架构,每一步都要算成本账。AI项目烧钱快,没有成本意识,项目很容易因为预算问题被砍。把成本当成一个和效果同等重要的指标来优化。

第七条:别一个人闷头搞。AI工程涉及的面太广,数据、训练、服务、运维,一个人很难样样精通。找到能互补的伙伴,或者至少多和同行交流,能少走很多弯路。我很多关键的思路都是从和别人的闲聊里得到的。

最后分享一个我一直在用的小技巧:每学一个新东西,就强迫自己用它能跑通的最小例子写一遍,然后把这个例子存起来。日积月累,你就有了一个自己的"代码片段库",下次遇到类似问题,直接翻出来改改就能用。这比任何教程都管用,因为那是你自己踩过坑、验证过的东西。AI工程这条路没有捷径,但走对了方向,每一步都算数。

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

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

立即咨询