我一直觉得,AI工程这个圈子里最害人的一句话是:“模型不都开源了吗?直接把仓库拉下来跑通不就行了?”
我刚上手做项目那会儿也这么想,结果被现实结结实实教育了一轮。三年前我花了整整两周,把别人训练好的模型跑通了推理Demo,效果看起来相当惊艳,但真往业务里一放,全是问题:数据格式对不上、线上数据和训练数据分布不一样、某天半夜模型开始对空字符串输出一堆废话、想要回滚才发现上一版参数根本没记录……那段时间我才明白,“跑通一个模型”和“交付一个AI系统”之间,隔着一条巨大的河。
所以当我看到“ai-engineering-from-scratch”这个项目名时,第一反应是:这名字取得真准。它不是让你从零手写Transformer,也不是让你从零预训练一个大模型,而是要把AI系统的完整工程链路——数据管线的搭建、模型选型、评测体系、部署上线、监控告警——从第一行代码开始,一块一块亲手搭起来。这篇东西就是我自己在“从零搭AI工程”这条路上攒下来的实操经验,写给那些不想只会“调包装毕”,想真正把AI系统落地、稳住、能修的人。
1. 你以为的“从零”和工程里的“从零”不是一回事
1.1 “跑通模型”只是全程的最后一小段
网上99%的教程都长一个样:一段代码加载开源模型,一行prompt,回车,模型吐出一段一百个字的回复,然后教程结束。
但一个真实生产环境下的AI系统长什么样?用户从App或者接口发来一条请求,这条请求要先过鉴权、限流、防重;系统要决定这条请求是走缓存、走规则、走小模型还是走重量级大模型;模型返回之后要检查输出格式、安全过滤、兜底逻辑;整个过程要有trace、有日志、有成本统计;模型上线要灰度,效果变差要能滚回上一版,线上数据分布变了要能收到告警。
这些东西,绝大多数教程一个字都不会写。你照着教程跑通一个Demo,大概只走完了“加载模型+推理”这几十行代码。而整个AI工程,是围绕这几十行代码长出来的一个完整的系统:数据、训练、评估、部署、监控、迭代。Demo是发动机点火的那一下,工程是整车——底盘、刹车、仪表盘、油箱管路、备用轮胎,缺一样都上不了路。
1.2 一个可交付AI系统的真实构成
我接手过好几个半路夭折的AI项目,也观察过不少团队的项目复盘,发现一件很有意思的事:大家几乎都高估了“模型训练”的占比。一个从零到上线、能稳定跑的AI项目,实际工时分布大概是这样的(基于常见项目实践的经验估计,不是精确科学,但足够说明问题):
| 环节 | 占比 | 说明 |
|---|---|---|
| 数据采集与清洗 | 30%-40% | 脏活累活全在这,而且永远比预想的多 |
| 评测集建设与回归机制 | 20%-25% | 没有评测就没有迭代依据,越早期越省不了 |
| 模型训练/微调/服务化 | 15%-20% | 真正的“炼丹”环节,反而不是时间大头 |
| 部署、监控、发布策略 | 20%-25% | 上线之后的事,决定了系统能不能长期活着 |
为什么要把这个比例摆出来?因为“跑通Demo”给你产生的错觉是:模型是核心,模型搞定一切就搞定。实际上模型训练在系统里只是一小段,数据才是地基,评测才是方向盘,部署监控才是仪表盘和安全带。地基不稳,模型再厉害也只是在一堆烂泥上盖高楼。
所以,“ai-engineering-from-scratch”这个项目真正让人兴奋的点,不是“我下载了一个模型”,而是“我不躲避任何一块工程拼图”——从第一个空目录开始,把数据、训练、评估、部署、监控整条链路亲手搭起来。
2. 动工之前,先把“成功标准”钉死在文档里
2.1 一份问题定义文档必须写清楚的六件事
很多人做AI项目有个坏习惯:一开始就在找数据集、调模型,跑了两周才被人问“你到底做到什么程度算成功?”——然后陷入沉默。
我现在的习惯是,第一周不写任何训练代码,先写一份问题定义文档。这份文档不需要多花哨,但它必须把六件事写清楚:
- 业务背景:这个东西是给谁用的?解决什么具体痛点?
- 任务类型:是文本分类、抽取、生成、排序、还是RAG检索?这决定了后面所有技术选型。
- 输入Schema:输入数据的字段、类型、范围、来源。线上请求长什么样,必须有明确的json或字段定义。
- 输出Schema:模型输出什么格式,怎么解析,什么算合法输出,什么算非法输出。
- 成功指标:离线指标是什么(准确率、召回率、格式合规率),在线指标是什么(延迟、打平率、成本、badcase率)。
- 约束条件:延迟预算是多少(比如p99必须小于2秒)、显存和成本上限多少、数据权限和隐私边界在哪。
其中第5条最关键,也最容易被新手忽略。我见过太多项目,团队特别努力地把某个指标从89%干到92%,结果一问业务方,人家要的根本不是这个指标。先定指标,再动手,是所有AI工程的第一条铁律。
2.2 为什么我建议先用一个“笨基线”跑通整条链路
第二个反直觉的建议:不要一上来就微调最强模型,先做一个“笨得掉渣”的基线版本,把整个系统链路跑通。
什么叫笨基线?对于一个垃圾评论分类任务,先用关键词规则做一个分类器;对于一个问答系统,先把所有文档缓存进去,命中关键词就返回对应段落。这些方案无论从哪个角度看都很笨,但它们有一个不可替代的价值——它们能把整条工程链路逼着走一遍。
你需要真实的数据输入输出接口,需要写评估脚本,需要部署一个服务,需要加日志和监控。这些活儿和模型智商无关,就是绕不过去的体力活。先把这条“大象肠”拉通,后面把中间的“模型内脏”换掉,就只是局部改动了。
我自己第一次搭笨基线时,发现80%的时间花在数据格式和版本对接上,模型本身十分钟就接好了。这个经历本身就是一个重要提醒:AI系统的复杂度大头在集成,不在模型。
2.3 第一周必须立起来的三个工程习惯
从零开始的项目,前面三天是习惯养成期。我用血的教训换来三条必须第一天就立起来的规矩:
- 一切代码进Git,数据用DVC或manifest管理。宁可数据文件先放本地,也要把“数据版本+处理代码commit+配置”这套绑定关系记录下来。没版本的数据等于没数据。
- 每次实验必须留下记录。至少记四样:训练配置、代码commit号、数据版本、跑出来的指标。用MLflow、wandb都行,没有一个像样的工具之前,哪怕写在Markdown表格里也比不记要好。
- 环境锁定。用Docker镜像或者完整的依赖lock文件,保证三个月后还能复现今天的结果。“我不能复现上周跑出来的结果”这句话,会吃掉整个AI项目的可信度。
这三条不是“锦上添花”,而是“安身立命”。没有它们,后面的每一次调参、每一版模型,都是在流沙上盖房子。
3. 数据管线:地基里的脏活,决定天花板
3.1 数据版本:没版本的数据等于没数据
做AI工程的人迟早会撞上同一个噩梦:上个月用一批数据训出的模型效果很好,这个月想复现,发现原始数据已经被覆盖了;或者处理脚本改了三版,谁也记不清当时用的是哪一版。
解决办法不是玄学,就是给数据上版本。DVC是最常用的工具,但它的核心思想你不用DVC也能实现:任何时候,一个实验报告必须能追溯到它当时用的数据源、处理脚本版本、采样规则和校验结果。
我现在每个数据集的manifest大致长这样:
{ "data_path": "s3://bucket/dataset/raw_v3.parquet", "schema_version": "v2", "pipeline_commit": "a3f9c1d", "sampling": "stratified_by_label", "checksum": "sha256:8f9a1b..." }每次训练、每次微调、每次评估,我都把这一条manifest直接贴在实验记录里。这样以后不管谁来问“你这个结果是怎么跑出来的”,我都能在十分钟内给出可复现的完整答案。数据版本化的本质,是给“实验结果”这个结论加上可追溯的证明链。
3.2 随机划分、时间划分、按组划分,选错会高估成绩
很多人在划分训练集和测试集时,闭着眼train_test_split(random_state=42)。但数据划分方式选错,评估结果可以虚高得一塌糊涂。
三种划分方式和使用场景:
- 随机划分:适合样本之间完全独立的场景,比如一张张图片的分类。但如果样本之间存在关联,随机划分会泄漏。最典型的是同一个用户贡献了多条数据时,模型在训练集里见过这个人,测试时再遇到他,等于“开卷考试”。
- 时间划分:适合时序类数据,比如商品评论、行情预测。按时间前80%训练、后20%测试,模拟的是“用过去预测未来”的真实场景。
- 按组划分(group split):当数据里存在用户、店铺、文章这类“组”的概念时,必须把同一组的所有样本全部放进同一侧(要么全在训练、要么全在测试)。
我实际遇到过的一件事:做一个工单分类模型,一开始随机划分,准确率93%,模型看起来优秀得离谱。后来改成按用户ID分组划分,准确率立刻掉到81%。差距不是模型变笨了,而是之前的评估在“作弊”——测试集里大量样本的用户已经在训练集里出现过了。如果你做的是用户维度、时间维度的数据,随机划分就是给自己喂一颗定心丸,这颗定心丸会在上线后变成毒药。
3.3 清洗脚本必须进仓库,notebook只配做探索
Jupyter Notebook是探索工具,不是生产工具。这一点我吃过大亏:在notebook里“顺手”改了几个单元格清洗逻辑,把空值填充方式改了,跑出结果,然后关了笔记本。两周后再想复现,连自己都看不懂当时的操作步骤,更别提换人了。
正确做法是:探索和可视化可以在notebook里做,但所有清洗逻辑和特征工程逻辑必须写成正式脚本(Python文件或dbt之类的工具),纳入Git管理。脚本要做到可重跑、可追溯、有输入输出定义。想象一下半年后的你或者新来的同事,他们不看notebook,他们只看仓库里的代码和数据manifest——他们能不能完全复现你的数据?如果不能,这个项目就是一次性的,不可维护,也谈不上工程。
3.4 一套廉价的自动数据校验清单
数据问题不会只在清洗时报错,更多时候是悄悄发生的。线上数据和训练数据分布漂移,是AI模型静默失效的头号原因,很多团队是等到badcase集中爆发才反应过来。
我现在每个项目都会在数据进入训练和上线这两个环节,各跑一遍自动校验,清单很朴素:
- 列名和列数是否匹配预期Schema
- 每列的字段类型是否符合定义
- 关键字段的非空率是否低于阈值
- 唯一性约束是否满足(比如ID列)
- 枚举值是否在合法集合内
- 标签分布是否与历史版本发生明显偏移
- 线上推理输入的特征分布与训练集分布是否漂移(用简单的均值、方差和分位数组对比就行)
这些校验用几十行Python就能写完,但它们能在训练开始前就拦住“数据错位”这种最贵的错误。我见过有人因为爬虫字段错位,导致训练数据标签整体偏了一列,模型还傻乎乎地训了一整晚,第二天看loss低得离谱,一查才知道数据在搞笑。廉价的自动校验,永远是AI工程里性价比最高的投资。
4. 模型选择:API、微调、从零训练,到底该走哪条路
4.1 三条路线的成本与控制权对比
到了模型这一步,很多人会陷入一个误区:既然项目叫“from-scratch”,那是不是必须从零训练模型?
不是。“AI工程从零开始”的意思是工程链路从零搭起,不是模型训练从零开始。在绝大多数业务场景下,用现成的托管API或者开源模型微调,才是理性选择。从零预训练一个模型,不管是从数据、算力还是时间成本看,对普通团队来说都是天文数字。
三条路线的实际权衡:
| 路线 | 数据要求 | 成本 | 控制力 | 适合场景 |
|---|---|---|---|---|
| 托管API(调用现成模型) | 几乎为零 | 按量付费,成本可控 | 低,无法修改模型本身 | 快速验证、长期效果优先于成本 |
| 开源模型微调 | 几百到几千条高质量样本 | 需要少量GPU,一次训练几十到几百元 | 高,模型归你掌控 | 领域术语多、输出格式固定、需要私有化部署 |
| 从零预训练 | 几十亿到万亿Token级别 | 极其昂贵 | 最高 | 学术研究、特殊语种/特殊模态,普通业务基本不用考虑 |
这个项目语境下的“from-scratch”,是指你从空白工程出发,亲手做选型、亲手验证、亲手做权衡。选哪条路,是工程决策的一部分。
4.2 什么时候才值得微调
很多人一上来就微调,这是一种“手里有锤子看什么都像钉子”的冲动。我的建议是,微调这件事必须排在整个流程的后面,前提条件满足再考虑:
- 先用提示词工程和RAG试过。把文档塞进上下文、把prompt调到位,看效果是否已经满足业务需求。很多任务在这里就解决了,不需要动模型本身。
- 评测数据证明“差距”确实存在。拿一组有代表性的badcase出来,明确看到模型在这些case上的错误模式——是领域知识缺失?格式要求无法满足?还是逻辑推理不足?不同错误模式对应不同解法,微调不是万能药。
- 高质量样本已经备好。所谓高质量,不是从网上爬一堆相关文本丢进去。而是针对目标场景,标注出“理想输入-理想输出”对,至少几百条,越多越好。用脏数据微调,等于给模型喂错了方向的强化学习。
微调还有一个容易忽略的副作用:灾难性遗忘。模型可能在新任务上表现好了,但通用能力下降。所以微调之后必须在保留评测集上做校验,确认没有把老能力弄坏。
4.3 小规模微调的最低配置参考
如果你的任务确实走到了微调这一步,被广泛验证、成本最低的起点是LoRA。我在这里给一个基于常见实践组合的参考配置,它不一定最优,但作为第一天就能跑起来的起点很可靠:
- 基础模型选7B或更小的开源模型(按显存和任务难度决定)
- LoRA rank=8到16
- 学习率1e-4到2e-4,配合warmup和cosine衰减
- 训练1到3个epoch
- 最大序列长度按任务设,短文本任务1024足够
- 使用bf16混合精度,梯度累积设8左右,让单卡能吃得下
- 数据用chat template组织,保持和推理时一致的格式
写到这里必须再强调:微调前和微调后,必须跑同一份评测集,用数据说话,而不是“感觉变聪明了”。没有评测跑的微调,和掷骰子没有区别。
5. 评测体系:把“感觉变聪明了”变成可回归的指标
5.1 离线评测集的三层结构
AI工程和普通软件开发最大的不同在于:普通软件改代码可以用单测断言来守护,AI系统改一个prompt改一版模型,效果变化是连续分布的、不好断言的。所以评测集就是AI工程的“单测”,而且是必须反复跑、持续跑的“单测”。
我搭建离线评测集时,分三层:
- 第一层:客观自动判分。分类任务算准确率/召回率,检索任务算命中率/Recall@K,生成任务算格式合规率。这类指标不依赖任何外部模型,简单、快、稳定。
- 第二层:规则或LLM-as-judge打分。对于“这段回答是否更好”这类需要主观判断的问题,用规则先做一遍硬约束(有没有关键信息、有没有违反禁止项),然后用另一个LLM对回答做质量评分或排序。
- 第三层:人工抽检。每周抽一批评测集里的案例,人眼过一遍,记录“机器认为好但人觉得差”的情况。人工抽检是评测体系的校准层,不要省略。
三层结构不是一次性的。每次模型版本变更、prompt调整、RAG检索逻辑修改,都要重新跑一遍,并产出对比报告。
5.2 LLM-as-judge 的使用边界
现在很多人喜欢让大模型给大模型打分,省时省力,但这里有两个坑:
第一,法官模型本身是有偏好的。它可能偏爱更长的回答、更喜欢某种行文风格,而这些偏好和真实用户需求不一定一致。我见过一个案例,judge给了某个模型更高的分,但人工抽检时发现那个模型只是在“自信地胡说八道”。
第二,LLM-as-judge适合排序和质量打分,不适合事实性判断。回答里有没有虚构的数字、有没有编造引用来源,这种场景要用检索比对、数据库核对,而不是让LLM自己评自己。
用LLM-as-judge之前,务必拿一小批人工标注好的样本去校准:judge的打分和人类的判断,相关性至少要达到一个可接受水平(比如用一致性比例或排序相关性来算)。校准不过关,就不能用。
5.3 每次改动都要跑一遍回归
很多团队把评测当成“上线前一次性工作”,这是大错。真实情况是:prompt改了一个词,可能让模型从一个坏case变好,同时弄坏另外20个case。没有回归评测,你根本不知道。
我把核心评测集当成CI来跑。任何变更——prompt调整、微调权重、RAG的切分策略、重排逻辑——都必须触发同一套评测集,跑出对比报告,才能合并。报告里同时展示“提升案例”和“退化案例”两个方向,只看平均分很容易掩盖局部回退。
生产环境侧同样不能漏,常用的三种手段:
- 影子模式(shadow mode):复制一份线上请求给新模型跑,输出不进生产、只做记录。低风险收集新模型在真实流量上的表现。
- 流量回放:把历史请求重新发给新模型,看它在旧问题上的表现。
- 线上A/B:小比例流量切给新模型,用在线指标(延迟、badcase率、用户反馈率)做最终裁决。
没有这套机制,你在线上做的每一次模型调整,本质都是赌博。赌徒偶尔会赢,但工程系统不能靠偶尔。
6. 把模型推到线上:部署、压测与可观测性
6.1 推理服务的骨架与性能关键点
模型部署这个环节,最容易犯的错是用Demo代码直接当服务代码。一个稍微像样的推理服务,至少包含这些模块:
请求进来后的处理顺序是:鉴权与限流 → 输入校验 → 查缓存 → 规则/小模型兜底 → 模型推理 → 输出校验 → 返回。其中输入校验和输出校验经常被省略,但恰恰是它们决定了系统在遇到脏数据时会不会静默乱来。输入不符合Schema就拒绝,输出不符合预期就重试或返回兜底结果,这两道闸门成本极低、收益极高。
性能层面的关键点:
- 动态批处理(dynamic batching):把并发请求攒成batch再喂给模型,吞吐能提升数倍,是实现高吞吐的主流手段。
- 量化:FP16、INT8、INT4逐级降低显存和延迟,但也逐级带来精度损失。我的习惯是先在FP16上建立正确性基线,再逐步量化并跑评测集确认精度不垮。
- 推理框架:成熟的开源推理服务(比如vLLM这类带PagedAttention、连续批处理能力的框架)能省掉大量底层优化工作,比手写推理循环靠谱得多。
选型层面,我没有写过任何框架的安装命令,因为版本变化太快,但思路是固定的:先确认框架支持你的模型架构,再小流量压测,不要一上来就接生产。
6.2 用压测暴露问题,而不是上线后让用户发现
部署完服务,别急着“测试一下通不通”。要压测。
压测的目的不是测出一个漂亮的并发数,而是摸清服务的真实承载边界:在多大QPS下延迟开始恶化,显存是否撑得住,CPU/GPU利用率的瓶颈在哪。我常用的工具是k6或wrk,压测脚本可以简单到十几行:
import http from 'k6/http'; import { sleep } from 'k6'; export const options = { scenarios: { load: { executor: 'ramping-vus', stages: [ { duration: '2m', target: 20 }, { duration: '2m', target: 50 }, { duration: '2m', target: 100 }, ], }, }, }; export default function () { const payload = JSON.stringify({ query: '压测样例输入' }); http.post('http://localhost:8000/inference', payload, { headers: { 'Content-Type': 'application/json' }, }); }跑完压测看三个数:p50/p95/p99延迟、错误率、GPU利用率。如果p99在某个并发下突然飙高,说明服务已经到瓶颈了;如果GPU利用率不到50%而延迟已经顶不住了,说明瓶颈在调度或者推理框架配置,不在显卡。
经验之谈:没压测就上线的AI服务,第一次流量高峰就是第一次故障时刻,从不例外。
6.3 AI服务要盯的三个特殊监控维度
普通Web服务监控看的是QPS、错误率、响应时间、CPU/内存。AI服务在此基础上,还要额外盯三个维度:
- Token消耗与成本曲线。大模型服务是按Token算钱的,Token用量会和输入长度、输出长度强相关。监控Token消耗,本质是在监控成本。一旦某天Token消耗异常上涨,通常意味着有用户在恶意灌长文,或者某个prompt被改得出乎意料地啰嗦。
- 生成长度与空回复异常。模型吐出一篇几百字废话、或者输出为空、输出无法解析,这类badcase在日志里必须单独标记和告警。它们不会导致HTTP 500,但它们会直接毒害用户体验——比宕机更隐蔽。
- 输入数据分布漂移。把线上推理的输入特征与训练集分布做周期性对比。输入分布漂了,模型表现必然漂。普通监控盯不住这个,需要单独写漂移检测任务,每天跑一次。
这三点,是判断一个AI服务是“活着”还是“活着但已经傻了”的分水岭。很多团队只知道服务200返回正常,却完全看不见模型已经在用陈旧认知回答新问题了。
7. 一条90天练兵路线:从零做一个完整项目
7.1 阶段一(1到6周):垃圾评论分类器闭环
如果只有一个项目要做,我最推荐从“垃圾评论/垃圾短信分类器”开始。理由很朴素:公开数据集容易找、任务边界清晰、评测指标明确,而且它逼着你走完整个闭环。
任务明确后,做这几件事:收集原始数据,写清洗脚本入库;按策略划分训练/验证/测试集(可以考虑按用户分组防止泄漏);微调一个小模型或者训练一个传统机器学习模型作为基线;写一套自动评测脚本,输出准确率、召回率、混淆矩阵;用FastAPI起一个推理服务,加输入校验、限流;跑一轮压测,记录延迟和错误率;加请求日志和基础监控。
这六周不追求模型效果全球第一,只追求一件事:把“数据到上线”的每一个环节亲手过一遍。做完之后,你脑子里会生成一张完整的工程地图,再往后学什么都快。
7.2 阶段二(7到10周):RAG知识问答系统
第二阶段,进入大模型应用的主流形态:RAG知识问答。用你自己的文档库(技术笔记、手册、或者”某个领域的说明文档“)做一个问答机器人。
这个项目的关键动作:文档解析与切分策略、向量库索引建立、检索逻辑(先粗排再重排)、生成prompt设计、缓存设计、评测集构建、调用链追踪。RAG系统的坑比纯分类任务多得多——切分太碎检索不到,切分太大上下文超限,检索到的内容与问题不匹配时模型会一本正经地瞎编。我强烈建议从一开始就为RAG建一个小评测集:准备20到50个“问题-标准答案-来源文档”,每次改检索逻辑或切分策略都跑一遍,看命中率变化。
7.3 阶段三(11到12周):微调小模型,和托管API算一笔账
第三阶段,把第二阶段的知识问答“重做”一遍,但这次换一条路线:微调一个3B到7B的开源小模型,在同一个评测集上,和托管API方案做一次完整对比。
对比维度别只看效果:延迟、成本、数据隐私、离线部署的难度、后续迭代维护成本,全部列出来。很多团队在实际项目中卡住,不是模型效果不行,而是没法做决策——因为他们从来不知道“不同选型的真实代价差多少”。这个阶段练的就是决策能力。
到这一步,你已经不是“会跑模型的人”了。你手里有数据管线、评测体系、部署脚本、监控告警,换一个任务、换一个模型,你都能在两周内把整条链路重新支起来。这才是AI工程“from-scratch”这个词真正的意义。
如果让我给这条路线排优先级,我会把数据管线、评测体系、监控告警这三块放得比模型调参更靠前。模型效果差可以靠优化追上,数据不可复现、评测不清晰、线上出了故障发现不了,才是真正致命的。那些不可见的环节,才是把模型变成产品的护城河。