1. 从零搭建AI工程能力:为什么我劝你别一上来就调包
这两年AI应用开发的门槛肉眼可见地降低了,随便拉个框架、调个API就能跑出一个能对话的Demo。但我在团队里带过不少新人,也面试过上百个号称“做过AI项目”的候选人,发现一个很普遍的问题:大家会用工具,但不知道工具背后发生了什么。模型输出不稳定,不知道从哪查;推理速度慢,不知道瓶颈在哪;换个场景效果崩了,只能反复试提示词,试到怀疑人生。
ai-engineering-from-scratch这个方向,说白了就是把AI工程当成一门手艺来练,而不是当成一堆API来调。它适合那些不满足于“能跑就行”、想真正理解从数据到模型再到服务这条链路上每个环节的人。不管你是刚转行想入局AI的开发者,还是已经在做应用但总觉得根基不牢的工程师,从零搭建一套自己的AI工程能力,都是绕不开的一步。
我自己的经历比较典型:最早做推荐系统,后来转做NLP应用,踩过不少坑。最开始也是调包侠,后来一次线上事故逼着我从头读了一遍Transformer的源码,才发现很多“玄学”问题其实都有明确的工程解释。从那以后,我开始有意识地补全自己在AI工程方面的短板,从数据处理、模型训练、推理优化到服务部署,一个环节一个环节地啃。这篇文章就是把我这些年积累的经验和教训整理出来,给想走这条路的朋友一个参考。
2. 整体思路拆解:从零搭建AI工程能力到底在搭什么
2.1 先搞清楚“AI工程”和“AI研究”的区别
很多人把这两个概念混在一起,导致学习路径完全跑偏。AI研究的目标是探索新方法、刷新榜单指标,关注的是“能不能更好”;AI工程的目标是把已有的方法稳定、高效、低成本地跑在生产环境里,关注的是“能不能一直好用”。这两个目标对能力的要求差别很大。
举个例子,研究场景下你可以在Jupyter Notebook里跑一个模型,数据加载慢一点无所谓,推理时间长一点也能忍,反正只跑一次。但工程场景下,你的服务可能要同时处理上千个请求,每个请求的延迟必须控制在几百毫秒以内,还要考虑内存占用、并发安全、故障恢复。这些在研究里基本不会涉及,但在工程里是每天都要面对的问题。
所以从零搭建AI工程能力,第一步不是去学最新的模型架构,而是把数据管道、训练流程、推理服务、监控体系这四个基础模块搞清楚。这四个模块构成了AI工程的主干,其他所有技术都是围绕它们展开的。
2.2 为什么选择“从零手写”而不是直接上框架
现在成熟的AI框架很多,功能也很全,为什么还要强调“from scratch”?我的理由有三个。
第一,框架会掩盖细节,而细节决定成败。比如你用某个高层API做文本分类,一行代码就能搞定。但如果效果不好,你根本不知道是分词出了问题、embedding没对齐、还是损失函数选错了。手写一遍之后,每个环节的输入输出你都清清楚楚,排查问题的时候就能快速定位。
第二,手写能帮你建立正确的直觉。很多工程决策依赖直觉,比如batch size设多大、学习率怎么调、要不要做梯度裁剪。这些直觉不是看书能看出来的,必须自己动手跑过、调过、错过,才能形成。我见过太多人调参全靠随机搜索,问他为什么选这个范围,答不上来。这就是缺乏直觉的表现。
第三,手写过的代码才是真正属于你的。调包调多了,你会发现自己的能力停留在“知道有这个工具”的层面,一旦工具更新或者换了个场景,又得重新学。但如果你理解底层原理,不管上层工具怎么变,你都能快速适应。
当然,我不是说所有东西都要手写。实际工作中,该用框架的地方还是要用,效率优先。但学习阶段,手写一遍是值得的。我的建议是:核心模块手写一遍,辅助工具直接用现成的。比如数据加载你可以用现成的库,但模型的前向传播和反向传播最好自己实现一次。
2.3 能力搭建的四个阶段
根据我的经验,从零搭建AI工程能力可以分成四个阶段,每个阶段的目标和重点不一样。
| 阶段 | 核心目标 | 关键能力 | 建议投入时间 |
|---|---|---|---|
| 基础夯实 | 理解AI工程全貌 | 数据处理、模型训练基础 | 1-2个月 |
| 动手实践 | 能独立完成小项目 | 端到端流程搭建 | 2-3个月 |
| 性能优化 | 让服务跑得又快又稳 | 推理加速、资源管理 | 3-6个月 |
| 体系化 | 形成自己的方法论 | 架构设计、故障排查 | 持续积累 |
这个阶段划分不是绝对的,每个人基础不同,节奏也不一样。但大方向是这样:先广后深,先跑通再优化。
3. 核心细节解析:数据、模型、服务三个环节的关键点
3.1 数据处理:AI工程里最容易被低估的环节
我在面试的时候经常问一个问题:“你觉得AI项目里最耗时间的是哪个环节?”大部分人回答模型训练或者调参。但实际上,数据处理往往占了整个项目60%以上的时间。而且数据处理出问题,后面所有环节都白搭。
数据处理的核心任务包括:数据清洗、格式转换、特征提取、数据增强、划分训练验证测试集。每个任务都有不少坑。
先说数据清洗。真实场景的数据往往很脏,有缺失值、异常值、重复值、格式不一致等等。我见过一个项目,模型训练效果一直不好,排查了两周才发现是数据里有大量重复样本,导致模型过拟合。所以清洗这一步一定要做扎实,该去重的去重,该修正的修正。
再说格式转换。不同来源的数据格式可能完全不同,有的是CSV,有的是JSON,有的是数据库导出的。你需要把它们统一成模型能吃的格式。这里有个经验:尽量早做格式统一,不要等到训练的时候再转换。因为训练时转换会拖慢速度,而且容易出错。
特征提取是另一个关键点。对于文本数据,你需要决定用词袋、TF-IDF还是embedding;对于图像数据,你需要决定用原始像素还是预训练特征。这个选择直接影响模型效果和训练速度。我的建议是:先从简单的方法开始,跑通之后再尝试复杂方法。不要一上来就用最先进的embedding,因为你不一定需要,而且调试成本高。
数据增强在数据量不足的时候特别有用。文本可以用同义词替换、回译、随机插入删除;图像可以用旋转、裁剪、颜色抖动。但要注意,增强方法要和任务匹配。比如做情感分类,你把“我喜欢这个”改成“我不喜欢这个”,那就适得其反了。
注意:划分数据集的时候一定要保证分布一致。我见过有人随机划分,结果训练集和测试集的类别比例差很多,导致评估结果不可信。正确做法是分层采样,确保每个类别在训练集和测试集里的比例接近。
3.2 模型训练:从能跑到跑得好
模型训练这个环节,新手最容易犯的错误是“一把梭”——把所有数据丢进去,设个学习率,跑完看结果。这样跑出来的模型,效果好不好全看运气。
正确的做法是小步快跑,逐步调优。具体来说,可以分成几步:
第一步,用一小部分数据跑通流程。这一步的目标不是效果,而是确认代码没有bug,数据能正常流入流出,损失能正常下降。我一般会取1%的数据,跑几个epoch,看看loss曲线是不是正常。
第二步,用全量数据跑一个baseline。这个baseline不需要调参,用默认配置就行。它的作用是给你一个参照点,后面所有优化都跟它比。
第三步,逐步调优。调优的顺序建议是:先调学习率,再调batch size,然后调模型结构,最后调正则化。学习率是最重要的超参数,一般从1e-3开始试,如果loss震荡就调小,如果下降太慢就调大。batch size影响训练速度和内存占用,一般设成2的幂次,比如32、64、128。模型结构可以从简单到复杂,先试小的,效果不够再加层。正则化包括dropout、weight decay等,主要在过拟合的时候用。
这里有个经验:不要同时调多个参数。因为如果效果变好了,你不知道是哪个参数起了作用;如果变差了,你也不知道该回退哪个。每次只调一个,记录结果,这样才能积累经验。
还有一个容易被忽视的点是随机种子。深度学习训练有随机性,同样的配置跑两次结果可能不一样。所以做对比实验的时候,一定要固定随机种子,否则结论不可靠。我一般会跑三次取平均,减少随机性的影响。
3.3 推理服务:让模型真正产生价值
模型训练完只是第一步,把它部署成服务、让业务方能用起来,才是产生价值的地方。推理服务这个环节,核心关注点是延迟、吞吐、稳定性。
延迟是指单个请求的处理时间。对于实时交互场景,延迟一般要求控制在几百毫秒以内。影响延迟的因素包括模型大小、输入长度、硬件性能等。优化延迟的方法有:模型量化、剪枝、蒸馏、使用更快的推理引擎等。
吞吐是指单位时间内能处理的请求数。对于批量处理场景,吞吐比延迟更重要。提高吞吐的方法有:批处理、异步推理、多实例部署等。
稳定性是指服务在长时间运行和高负载下的表现。这里有几个常见的坑:内存泄漏、线程安全问题、模型加载失败等。我建议在服务上线前做压力测试,模拟高并发场景,看看服务能不能扛住。
提示:推理服务一定要加监控。至少监控三个指标:请求延迟、错误率、资源使用率。这样出问题的时候能快速定位。我见过一个服务,上线后偶尔超时,查了半天才发现是某个请求的输入特别长,导致处理时间暴涨。如果早点加监控,这个问题几分钟就能发现。
4. 实操过程:手把手搭建一个文本分类服务
4.1 环境准备与依赖安装
这一节我以文本分类为例,完整走一遍从数据处理到服务部署的流程。选择文本分类是因为它足够简单,适合练手,同时又能覆盖AI工程的主要环节。
环境方面,我建议用Python 3.9以上版本,主要依赖包括:numpy、pandas、scikit-learn、torch、flask。如果你有GPU更好,没有的话CPU也能跑,只是慢一点。
pip install numpy pandas scikit-learn torch flask这里解释一下为什么选这些库。numpy和pandas是数据处理的基础,scikit-learn提供了很多现成的工具函数,torch用来实现模型,flask用来搭建服务。这些都是很成熟的库,文档齐全,遇到问题容易找到答案。
注意:安装torch的时候要根据你的硬件选择版本。如果有NVIDIA显卡,装CUDA版本;如果没有,装CPU版本。具体命令可以去官网查,不要直接pip install torch,那样可能装到不匹配的版本。
4.2 数据准备与预处理
我用的数据集是一个公开的中文情感分类数据集,包含正面和负面两类评论。数据格式是CSV,两列:text和label。
import pandas as pd from sklearn.model_selection import train_test_split # 读取数据 df = pd.read_csv('sentiment_data.csv') print(f'数据总量: {len(df)}') print(f'类别分布:\n{df["label"].value_counts()}') # 划分训练集和测试集,分层采样 train_df, test_df = train_test_split( df, test_size=0.2, stratify=df['label'], random_state=42 ) print(f'训练集: {len(train_df)}, 测试集: {len(test_df)}')这段代码做了两件事:读取数据、划分数据集。注意stratify参数,它保证训练集和测试集的类别比例一致。random_state固定随机种子,保证结果可复现。
接下来做文本预处理。中文文本需要分词,我用jieba来做。
import jieba def preprocess(text): # 分词 words = jieba.lcut(text) # 去除停用词和标点 words = [w for w in words if w.strip() and w not in stopwords] return words train_df['tokens'] = train_df['text'].apply(preprocess) test_df['tokens'] = test_df['text'].apply(preprocess)停用词表可以自己整理,也可以用现成的。我建议自己整理一份,因为不同场景的停用词不一样。比如做情感分类,“不”这个词就不能去掉,因为它会反转情感。
4.3 模型实现与训练
模型方面,我用一个简单的TextCNN。选择TextCNN是因为它结构简单、训练快、效果还不错,适合入门。
import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim, num_classes): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim) self.convs = nn.ModuleList([ nn.Conv2d(1, 128, (k, embed_dim)) for k in [2, 3, 4] ]) self.fc = nn.Linear(128 * 3, num_classes) self.dropout = nn.Dropout(0.5) def forward(self, x): x = self.embedding(x) # (batch, seq_len, embed_dim) x = x.unsqueeze(1) # (batch, 1, seq_len, embed_dim) x = [torch.relu(conv(x)).squeeze(3) for conv in self.convs] x = [torch.max_pool1d(i, i.size(2)).squeeze(2) for i in x] x = torch.cat(x, 1) x = self.dropout(x) return self.fc(x)这个模型的核心思想是用不同大小的卷积核提取不同长度的n-gram特征,然后拼接起来做分类。卷积核大小选了2、3、4,分别对应bigram、trigram、4-gram。每个卷积核输出128个特征图,最后拼接成384维向量。
训练循环的代码比较长,这里说几个关键点。损失函数用交叉熵,优化器用Adam,学习率设1e-3。每个epoch结束后在验证集上评估,保存效果最好的模型。
criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(10): model.train() for batch in train_loader: optimizer.zero_grad() outputs = model(batch['input_ids']) loss = criterion(outputs, batch['label']) loss.backward() optimizer.step() # 验证 model.eval() with torch.no_grad(): # 计算验证集准确率 pass训练过程中要关注loss曲线。正常的loss曲线应该是先快速下降,然后逐渐平缓。如果loss震荡厉害,说明学习率太大;如果loss下降很慢,说明学习率太小;如果loss先降后升,说明过拟合了。
4.4 推理服务搭建与测试
模型训练好之后,用flask搭一个简单的HTTP服务。
from flask import Flask, request, jsonify import torch app = Flask(__name__) model = load_model('best_model.pt') model.eval() @app.route('/predict', methods=['POST']) def predict(): text = request.json['text'] tokens = preprocess(text) input_ids = convert_to_ids(tokens) with torch.no_grad(): output = model(input_ids) pred = torch.argmax(output, dim=1).item() return jsonify({'label': pred}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)这个服务很简单,接收POST请求,返回预测结果。实际生产环境还需要考虑更多东西,比如批处理、超时控制、错误处理等。但作为练手,这个已经够了。
测试的时候可以用curl或者Python的requests库。
curl -X POST http://localhost:5000/predict \ -H "Content-Type: application/json" \ -d '{"text": "这个产品非常好用"}'如果返回{"label": 1},说明服务正常。
5. 常见问题与排查技巧实录
5.1 训练不收敛怎么办
训练不收敛是最常见的问题,表现是loss不下降或者震荡厉害。排查思路如下:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| loss完全不降 | 学习率太小、数据有问题、模型结构错误 | 调大学习率、检查数据、打印中间输出 |
| loss震荡厉害 | 学习率太大、batch size太小 | 调小学习率、增大batch size |
| loss先降后升 | 过拟合 | 加正则化、早停、增加数据 |
| loss降但指标不升 | 评估方式有问题、数据泄漏 | 检查评估代码、检查数据划分 |
我遇到最多的情况是学习率设得不对。很多人直接用默认的1e-3,但这个值不一定适合你的任务。我的经验是:先用一个较大的学习率跑几步,看看loss有没有爆炸,然后逐步调小。比如从1e-2开始,如果loss变成nan,就降到1e-3,再试。
还有一个容易被忽视的原因是数据有问题。比如标签错了、特征全是0、样本顺序有问题等。排查方法很简单:打印几个batch的数据看看,人工检查一下。
5.2 推理速度慢怎么优化
推理速度慢的原因可能有很多,需要逐步排查。
首先确认瓶颈在哪。用profiler工具跑一下,看看时间花在哪个环节。常见瓶颈包括:数据预处理、模型前向传播、后处理。
如果是模型前向传播慢,可以考虑:减小模型、量化、用更快的推理引擎。量化是把float32转成int8,能显著减少计算量和内存占用,但可能损失一点精度。我一般先试动态量化,效果不够再试静态量化。
如果是数据预处理慢,可以考虑:预处理好数据、用更快的分词工具、并行处理。我见过一个服务,瓶颈居然在分词上,因为用的分词工具是单线程的。换成多线程之后,吞吐直接翻倍。
如果是后处理慢,比如排序、过滤,可以考虑用numpy替代Python循环,或者用Cython加速。
提示:优化之前一定要先测量,不要凭感觉猜。我见过有人花了一周优化模型,最后发现瓶颈在数据加载上。用profiler跑一下,几分钟就能定位问题。
5.3 服务上线后不稳定怎么办
服务不稳定表现为:偶尔超时、内存持续增长、并发高了就崩。这些问题往往和代码质量有关。
内存泄漏是最常见的。Python虽然有垃圾回收,但如果对象被全局变量引用,就不会被回收。比如你把模型加载到全局变量里,每次请求都往里面加东西,内存就会一直涨。解决方法是:请求级别的数据用完就释放,不要挂在全局对象上。
线程安全问题也常见。如果多个请求共享同一个模型实例,而模型内部有可变状态,就可能出问题。解决方法是:要么加锁,要么每个请求用独立的模型副本。加锁会影响并发性能,独立副本会占更多内存,需要权衡。
超时问题一般是某个请求处理时间特别长导致的。解决方法有:设置超时时间、限制输入长度、异步处理。我一般会设置一个合理的超时时间,超过就返回错误,避免拖垮整个服务。
5.4 效果不好怎么排查
效果不好是最难排查的,因为原因可能有很多。我的排查顺序是:数据、模型、训练、评估。
先看数据。数据量够不够?标签对不对?分布有没有问题?我见过一个项目,效果一直上不去,最后发现是数据里有30%的标签是错的。所以数据检查一定要做,而且要认真做。
再看模型。模型容量够不够?结构合不合理?对于文本分类,TextCNN一般够用,但如果任务复杂,可能需要BERT之类的预训练模型。
然后看训练。训练充分了吗?有没有过拟合?学习率调好了吗?这些可以通过loss曲线和验证集指标来判断。
最后看评估。评估方式对不对?测试集有没有泄漏?指标选得合不合适?我见过有人用准确率评估不平衡数据,结果模型全预测多数类,准确率还有90%,但实际没用。
6. 我踩过的坑和总结的经验
6.1 不要过早优化
我刚开始做AI工程的时候,总想着一步到位,用最先进的模型、最复杂的架构。结果往往是:花了很多时间,效果还不如简单方法。后来我学乖了,先用最简单的方法跑通,确认流程没问题,再逐步优化。这样即使出问题,排查范围也小。
比如做文本分类,先用TF-IDF加逻辑回归跑一个baseline,可能就能达到不错的效果。如果不够,再上深度学习。这样既省时间,又能建立一个参照点。
6.2 日志和监控比你想的重要
我早期做服务的时候,不太重视日志和监控,觉得能跑就行。后来一次线上事故,服务突然挂了,我查了半天不知道问题在哪,因为没有日志。从那以后,我养成了习惯:关键环节一定要打日志,核心指标一定要监控。
日志要包含:请求ID、输入摘要、处理时间、输出摘要、错误信息。监控要包含:QPS、延迟分布、错误率、资源使用率。这些数据不仅能帮你排查问题,还能帮你发现优化点。
6.3 版本管理不只是代码
AI项目里,除了代码,数据和模型也需要版本管理。我见过一个项目,模型效果突然变差,查了半天发现是数据更新了,但没人记录。所以数据版本、模型版本、配置版本都要管理起来。
简单的方法是用文件名加日期和版本号,比如model_v1.2_20240101.pt。复杂一点可以用专门的工具,比如DVC。但不管用什么方法,关键是要有记录,能追溯。
6.4 测试要覆盖边界情况
AI服务的测试和普通服务不太一样,因为输入是自然语言,边界情况特别多。空输入、超长输入、特殊字符、多语言混合,这些都要测。我一般会准备一个测试集,包含各种边界情况,每次上线前跑一遍。
还有一个容易忽视的是模型加载失败的测试。如果模型文件损坏或者路径不对,服务应该能优雅地报错,而不是直接崩掉。这个也要测。
6.5 持续学习,但不要盲目追新
AI领域发展很快,新模型、新工具层出不穷。保持学习是必要的,但不要盲目追新。我的原则是:先把基础打牢,再关注新东西。基础包括:数据处理、模型训练、推理优化、服务部署。这些是相对稳定的,学会了长期有用。新东西可以关注,但不要每个都跟,选几个和你的方向相关的深入就好。
最后分享一个我自己的习惯:我会定期回顾自己做过的项目,把遇到的问题和解决方法整理成文档。这个习惯帮我积累了很多经验,也让我在遇到类似问题的时候能快速找到答案。如果你也在做AI工程,建议你也试试。