ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零手搓AI工程:数据、训练到部署的完整实践指南

从零手搓AI工程:数据、训练到部署的完整实践指南 1. 为什么我仍然推荐“从零手搓”的方式来学AI工程先交代一下背景。我接触AI工程这条路是从几年前拿着一个模型推理项目开始的——那时候市面上还没有这么多开箱即用的平台想调通一条推理链路得自己处理数据、自己写训练脚本、自己排查显存溢出每一步都是硬功夫。如今AI工程这个词被各类工具、框架、平台推得很高很多新人一上来就套着现成的能力去“组装产品”我见过太多简历上写着“熟练使用大模型API”“做过Prompt调优”的人遇到一个需要自己训练、自己部署、自己优化性能的场景就直接懵掉了。这让我越来越确信一件事尽管现在造的轮子很多但从零完整走一遍AI工程的全流程仍然是判断一个工程师是否真正理解这套体系的最佳方式。我不否认直接调用成熟能力可以快速交付业务价值但如果你没有亲手搭建过数据管线、没有写过训练循环、没有亲自处理过推理延迟和模型体积的取舍问题那你的认知边界就永远停在“能跑”而不是“知道它为什么能跑、怎么才能跑得更好”。我接触到“ai-engineering-from-scratch”这个实践路径时第一反应就是它把整个学习过程重新拉回了本质——数据、模型、训练、评估、部署五个环节全都自己做不跳过任何一个。这篇文章我想用自己的实操经历把这条路径拆开来讲环境怎么搭、数据怎么准备、模型怎么从零构建、训练要注意什么、部署有哪些暗坑。适合对AI工程有好奇心、但还没完整跑通过一条链路的人参考当然如果你已经在用别人的框架和平台这篇文章也能帮你补上底层视角之后再做技术选型时会更有底气。很多人质疑“从零搭建模型”在海量开源模型面前还有什么意义我的看法是意义不在最终产出的模型比开源模型更强而在于你知道每一步为什么这样做。就像学汽车维修的人不会因为现在造车技术成熟就跳过拆发动机的练习。你拆过、装过才知道引擎盖下面到底发生了什么。AI工程也是一样别人给你一个微调好的模型你只有亲手把训练流程跑通一遍才有能力判断它适合不适合你的业务、还能不能再优化一点。下面我按一条完整的落地路径来讲从环境准备开始逐层深入最后落回到实际部署和避坑总结。每一节的内容都是我在实际操作里沉淀过的经验不是教科书式的概念堆叠。2. 本地环境搭建CUDA、Python环境和训练框架的取舍2.1 硬件选型显存大小直接决定你能碰多大的模型先说硬件。很多人一上来就想训练大模型但手里只有一张消费级显卡这就得先面对现实显存决定了模型规模的上限。拿我自己的实践来说最开始我用的是12GB显存的卡训练一个参数量在1亿以内的Transformer模型勉强够用但再往上加层数或序列长度就得靠梯度累积和混合精度来硬撑训练速度慢得让人怀疑人生。如果只是跑通流程、验证学习链路有个8GB到12GB显存的GPU就已经能覆盖大部分练手场景了。如果你打算做一些中小规模的生成模型或稍微大一点的分类模型24GB以上的显存会更舒服。实在没有GPU用云GPU实例也是一种选择只是要注意费用控制别让一次练手跑掉几百块。显存计算有个粗略公式模型参数量乘以参数精度字节数再乘以优化器状态和梯度占用的倍数大致就能估算出单卡训练最低需求。比如一个1亿参数的FP16模型参数本身约200MB但Adam优化器需要额外保存状态算下来可能就要占用1GB以上的显存再加上激活值实际占用还会成倍上涨。实践里我习惯先用一个很小的批次把模型和数据加载起来观察显存占用再逐步调大批次直到接近上限。2.2 开发环境配置conda、CUDA和PyTorch的版本匹配是关键说句实话AI工程里最容易让人崩溃的环节不是模型设计而是环境配置。CUDA、cuDNN、PyTorch、Python版本之间的兼容关系稍微错一个版本就能让你在import的时候直接报错报错信息还往往看不懂。我的做法是固定一套经过验证的版本组合。以我当前的主力环境为例组件版本说明Python3.10兼容性最稳妥的版本区间CUDA12.1驱动版本需要支持该CUDA版本PyTorch2.1.x自带对应CUDA的预编译包训练框架原生PyTorch HuggingFace生态灵活且可控包管理conda环境隔离避免互相污染关键坑在于PyTorch的安装命令里要带正确CUDA的index-url否则它会默认安装CPU版本训练慢到让你怀疑代码有问题。我第一次没注意这个直接pip install torch结果训练一个epoch要两天后来才发现装的是CPU版。所以装完先跑一句torch.cuda.is_available()确认输出是True再往下走。我推荐用conda建独立的虚拟环境不要一股脑把项目依赖装进base环境。因为不同项目可能依赖不同版本的numpy、pandas、甚至PyTorch混在一起迟早出事。我后来养成的习惯是每个项目新建一个环境虽然磁盘占用多一点但排查问题的时间省了不止一半。2.3 选原生PyTorch还是封装好的框架现在训练框架很丰富HuggingFace的Trainer、PyTorch Lightning、FastAI都提供了很友好的抽象层。但对于“from scratch”的学习目的我强烈建议至少完整手写一次训练循环不要一上来就用Trainer把细节都隐藏掉。手写训练循环其实没有想象中那么难核心就是这几步遍历数据、前向传播、计算损失、反向传播、更新参数、查看指标。把这几步写明白你才算真正理解一个模型是怎么被“训练”出来的。之后再切换到Trainer之类的封装工具遇到问题也知道它内部在做什么不会一脸黑线地乱改参数。当然我也不是让大家永远手写。等跑通一两条链路后切换到高级封装可以明显提高实验效率。关键是顺序不能颠倒先理解底层再用高层工具。这个观点可能有人不同意觉得有省力的工具为什么不用但我的亲身体会告诉我省力工具省的是重复劳动的力不是理解原理的力直接用封装工具最后往往会陷入“调参靠猜、报错靠百度”的尴尬局面。3. 数据是整个工程的地基从公开数据集到清洗管线3.1 别把数据当配角它的优先级应该是最高的很多人学AI工程时把大部分注意力放在模型结构上——今天Transformer、明天注意力机制、后天扩散模型研究得很上头。但真实项目里模型占比可能只有20%的时间投入数据清洗、标注、管线建设反而吃掉了一大半工作量。甚至可以说决定一个AI系统上限的往往不是模型而是数据质量。我印象很深的一次经历训练一个文本分类模型用了同一个模型结构、同一套超参数只是换了一批数据一个在验证集上F1达到0.89另一个只有0.72。差距全在数据质量上——一份干净、均衡、贴合业务分布另一份包含大量错误标签和重复样本。从那以后我再也不在做数据这件事上压缩时间。3.2 从哪里拿数据、怎么判断数据能不能用公开数据集平台主要有几个Kaggle、HuggingFace Datasets、UCI等。HuggingFace Datasets的优势在于它有一个统一的加载接口很多数据集可以用一行代码下载并预处理成标准格式对练手项目非常友好。不过公开数据集在真实项目里往往不够用这时候就得自己动手收集和构造数据。比如做一个客服意图分类公开数据集里可能没有你的业务场景你需要从历史工单、聊天记录里抽样本再人工打标。这个过程很苦但也是数据工程能力的核心——你如何把一堆非结构化的原始文本变成干净的训练语料直接决定了后续模型表现。数据检查的四个基本维度数量够不够分类任务每个类别至少几百条起步生成任务则需要更大规模标签准不准随机抽几十条人工复核标签错误率超过5%就要考虑清洗或重新标注分布均不均衡类别严重倾斜时考虑过采样、欠采样或重新设计评估指标有没有泄漏训练集和验证集里出现重复样本或者验证集包含了训练时才能看到的信息都会导致评估结果虚高3.3 构建一条可以复用的数据清洗管线数据清洗不是一次性脚本而是应该构建成可以反复执行的处理管线尤其是当数据源持续更新时。我的习惯是分几步处理去重同一文本重复出现会加剧过拟合用哈希或SimHash做第一层去重过滤长度异常、包含乱码、明显非目标语言的样本一律剔除标准化统一大小写、处理多余空格和换行文本类任务通常还需要分词或子词切分标签清洗对不一致的标签做规则批处理再人工抽检划分数据集按训练集、验证集、测试集划分划分顺序要以文本ID或哈希值为依据避免随机种子带来的不稳定做完管线后我强烈建议把每个阶段的样本数量变化记录下来——原始数据多少条、去重后多少条、过滤后多少条。一方面方便排查管线哪里吞了不该吞的数据另一方面也能让你对数据质量有一个量化的感知。很多工程师忽略这个细节等模型效果差的时候复盘根本不知道是哪一步处理出了问题。3.4 数据增强和少样本场景的补数技巧在真实工业场景里高质量数据永远是稀缺的。少样本场景下有几个简单实用的补数技巧。第一是回译增强把中文文本翻译成英文再翻译回中文虽然有时会改变语义但能得到一大批语义相似、表达不同的样本。第二是关键词替换在保留原句结构的前提下把核心词替换为同义词或关联词。还有一种是基于语言模型的重写借助一个预训练语言模型生成同义改写但需要注意人工审核防止生成出错误标签或矛盾信息。不过我要强调一个底线数据增强不能改变标签语义。一个“客户投诉物流慢”的样本增强后变成了“客户抱怨配送延迟”标签依然是“投诉”没问题但如果改成了“客户询问物流时长”那就变成“咨询”了硬用原标签训练等于在教模型理解错误关系。这是最典型的坑之一。4. 模型从零构建以“推理模型”为例的完整搭建过程4.1 “推理模型”到底是什么从LLM到reasoning model的演进逻辑最近“build a reasoning model from scratch”这个话题在AI社区里热度相当高。所谓推理模型简单来说就是让模型不只生成“看起来流畅”的文本还要具备一步步推导的过程能力。比如你问一个大语言模型“为什么天空是蓝色的”普通模型可能直接给出一段百科式答案而推理模型会先内部拆解问题分步骤组织答案链路最终给出的内容更接近人的思考过程。从技术角度拆解推理能力的核心来自两个层面。一是模型本身的参数规模与训练数据是否足够丰富让它可以隐含学到“多步推理”的模式二是在训练策略上做了强化学习或监督微调引导模型在推理路径上做优化。很多推理模型在基础生成能力之上加入了类似“思维链”的训练数据让模型学会把自己内部的推导过程逐步呈现出来。对于从零构建一个推理模型的练手项目我建议不要一上来就挑战几百亿参数的大规模模型。一个参数量在几亿到十几亿的模型已经足够用来理解核心机制。重点在于你会不会构造推理类型的训练数据、能不能设计合理的训练目标、有没有能力评估模型到底在“推理”而不是“背答案”。4.2 准备“推理数据”思维链数据怎么构造、怎么清洗训练一个推理模型数据构造的复杂度比普通文本生成高一个量级。普通文本生成只需要“文本”作为训练目标推理模型需要“问题-推理过程-最终答案”这样的三元组结构。目前公开的推理数据源并不多比较可行的做法有三种。第一种是从问答社区抓取带详细解析的题目例如数学类解答、逻辑推理类题目把解析过程拆解成结构化的推理链。第二种是用现成的强推理模型生成“教学式”推理过程比如让一个大模型对一道题目输出步骤推导再人工校验后作为训练数据。第三种是手工构造一整套带有明确规则的小规模推理任务比如数学应用题、多跳问答、符号推演等每个样本都由人工编写推理步骤。数据清洗在推理任务里尤其重要原因很直接推理过程只要错了一步最终结果就不可信。我处理这类文本时有三条硬规则推理过程的每一步必须与最终答案严格一致不允许存在前后矛盾的步骤推理链的中间步骤必须完整不能跳跃关键推导否则模型学到的就是“结果正确但过程残缺”同类任务的数据格式要尽量统一比如数学题就统一用“设局→列式→计算→验算”的格式减少模型在格式映射上的负担4.3 搭建Transformer主干从零实现的代码骨架这里我用一段可以实际运行的PyTorch代码来演示从零搭建一个小型Transformer模型。这个框架既可以用来做文本分类也可以稍作改造用于序列生成。核心是理解几个关键组件的组装方式嵌入层、位置编码、多头注意力、前馈网络、残差连接和层归一化。先看看我实际跑通过的一个小型Transformer代码骨架import torch import torch.nn as nn import math class PositionalEncoding(nn.Module): def __init__(self, d_model: int, max_len: int 512): super().__init__() pe torch.zeros(max_len, d_model) position torch.arange(0, max_len, dtypetorch.float).unsqueeze(1) div_term torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) self.register_buffer(pe, pe.unsqueeze(0)) def forward(self, x: torch.Tensor) - torch.Tensor: return x self.pe[:, : x.size(1), :] class SelfAttention(nn.Module): def __init__(self, d_model: int, n_heads: int): super().__init__() assert d_model % n_heads 0, d_model must be divisible by n_heads self.d_model d_model self.n_heads n_heads self.head_dim d_model // n_heads self.wq nn.Linear(d_model, d_model) self.wk nn.Linear(d_model, d_model) self.wv nn.Linear(d_model, d_model) self.wo nn.Linear(d_model, d_model) def forward(self, x: torch.Tensor, mask: torch.Tensor None) - torch.Tensor: batch_size, seq_len, _ x.size() q self.wq(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) k self.wk(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) v self.wv(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) scores torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim) if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) attn torch.softmax(scores, dim-1) out torch.matmul(attn, v).transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model) return self.wo(out) class TransformerBlock(nn.Module): def __init__(self, d_model: int, n_heads: int, d_ff: int, dropout: float 0.1): super().__init__() self.attn SelfAttention(d_model, n_heads) self.ff nn.Sequential( nn.Linear(d_model, d_ff), nn.ReLU(), nn.Linear(d_ff, d_model), ) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.dropout nn.Dropout(dropout) def forward(self, x: torch.Tensor, mask: torch.Tensor None) - torch.Tensor: x x self.dropout(self.attn(self.norm1(x), mask)) x x self.dropout(self.ff(self.norm2(x))) return x class TinyTransformer(nn.Module): def __init__(self, vocab_size: int, d_model: int, n_layers: int, n_heads: int, d_ff: int, max_len: int 512, dropout: float 0.1): super().__init__() self.embed nn.Embedding(vocab_size, d_model) self.pe PositionalEncoding(d_model, max_len) self.blocks nn.ModuleList([TransformerBlock(d_model, n_heads, d_ff, dropout) for _ in range(n_layers)]) self.norm nn.LayerNorm(d_model) self.head nn.Linear(d_model, vocab_size) def forward(self, x: torch.Tensor, mask: torch.Tensor None) - torch.Tensor: x self.embed(x) * math.sqrt(self.embed.embedding_dim) x self.pe(x) for block in self.blocks: x block(x, mask) return self.head(self.norm(x))这里要特别解释几个看起来不起眼但很容易出错的设计决策。位置编码用的是经典的sin/cos函数式而不是可学习的位置编码。原因是它在序列长度超出训练长度时有一定的泛化能力对练手项目来说接口也更稳定。如果你的任务里序列长度经常变这个设计会比可学习的版本更省心。多头注意力的实现里有一个常见细节先做线性变换得到Q、K、V然后拆分头在拆分后做注意力计算最后合并头再经过输出线性层。很多初学者把“多头”错误理解成多套独立的QKV权重其实是在同一个线性层输出上拆分维度这样既省参数又达到了让人关注不同子空间的目的。残差连接和层归一化的顺序也有讲究。代码里采用的是“先归一化再进子层、残差连接在外”的写法这种“Pre-LN”结构在训练稳定性上明显优于“Post-LN”。如果我在练手时把顺序写反很容易遇到深层模型训练不稳定、loss震荡甚至直接发散的情况。注意力掩码在自回归推理任务里是关键中的关键。训练时我们不允许模型看到未来的token所以要用一个上三角矩阵把未来位置掩掉。代码里通过masked_fill把掩码位置置为负无穷再通过softmax之后这些位置的概率就无限接近零。如果漏掉这一步模型训练时就会看到答案表面上loss很好看实际生成时完全崩溃。这种“训练时偷看答案”的问题在不带掩码的练手项目里非常隐蔽我踩过一次花了两天才定位到根因。4.4 训练循环怎么写梯度下降、优化器和损失函数的配合逻辑模型的骨架搭好了接下来就是训练循环。一个完整的训练循环模板大概长这样def train_one_epoch(model, dataloader, optimizer, criterion, device, clip_grad1.0): model.train() total_loss, total_tokens 0.0, 0 for batch in dataloader: input_ids batch[input_ids].to(device) labels batch[labels].to(device) logits model(input_ids) loss criterion(logits.view(-1, logits.size(-1)), labels.view(-1)) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), clip_grad) optimizer.step() total_loss loss.item() * input_ids.size(0) total_tokens input_ids.size(0) return total_loss / total_tokens这段代码里有两个容易被忽略但极其重要的设计。第一个是梯度裁剪。训练Transformer模型时梯度范数在某些batch上会突然变得很大导致参数更新幅度剧烈、loss爆炸。加上梯度裁剪后更新的步长被限制了训练稳定性会明显提升。我自己的经验是对于中小型Transformer梯度裁剪阈值设在1.0附近是个比较稳的起步值。第二个是优化器的选择。文本生成任务里最常用的是AdamW而不是普通的Adam区别在于AdamW把权重衰减从梯度更新中解耦出来效果更好且对学习率的敏感度更低。学习率设置也需要注意Transformer模型通常需要一个预热阶段也就是先从一个很小的学习率线性涨到目标值再按余弦函数衰减。这个warmup机制我一开始不太理解后来才明白深层模型在训练初期很脆弱参数剧烈更新容易让模型陷入坏的局部最优预热相当于给模型一个“缓慢起步热身”的过程。4.5 评估怎么判断推理模型是真的学会推理还是只会背答案评估是所有人最容易糊弄过去、但恰恰最不该偷懒的环节。对于推理模型准确率或BLEU这类单值指标远远不够至少要加上这么几层检查。第一层是最终答案的正确率。这个不用多说答案对就是对错就是错。第二层是中间推理过程的连贯性。我习惯随机抽取预测结果人工看推理链的每一步是否和上一步逻辑衔接。如果出现“前一步还在计算A后一步直接跳到结论B”说明模型并没有真正学会推导只是在模仿推理文本的格式。第三层是对抗性测试。把训练集里常见的数字换掉、把问题换个说法但逻辑结构保持一致看模型是否还能答对。这叫分布外泛化测试。如果模型只在训练数据上表现好换一个说法就崩说明它并没有泛化出“推理能力”只是在记忆训练样本。这一点对推理模型尤其致命。我个人的经验法则是一个能被称作“推理能力”的模型必须能在未见过的问题表述上达到至少接近训练时水平的正确率。如果只能背训练样本那它跟一个检索系统没什么本质区别。5. 训练与调优过程中那些容易忽略的关键配置5.1 批次大小、学习率和序列长度之间的联动关系训练一个模型表面上看超参数似乎可以独立调节实际它们是互相牵制的。批次大小、学习率和序列长度这三个变量经常被单独调整忽略联动关系最终导致loss不降或显存瓶颈。先说批次大小与学习率的关系。批次越大每个batch的梯度越稳定可以适当把学习率调高批次越小梯度越嘈杂学习率应该相应降低。标准做法是在改变批次大小时学习率跟着线性或平方根缩放。如果固定学习率不变只是把批次调大常常会出现训练初期loss剧烈震荡的问题。再说序列长度对有效批次大小的影响。同样是“batch_size32”序列长度是64还是512每个batch实际的token数是4倍差异显存占用和训练步数完全不同。我的习惯是以“每个batch的token总数”为单位来配置一批超参数而不是盯着batch_size这个数字。这样可以避免不同实验之间的对比失真。5.2 过拟合还是欠拟合先观察哪边训练中判断模型状态我从不直接看训练集loss而是同时监控训练集和验证集的曲线。如果训练loss持续下降、验证loss在某个点后开始回升那就是典型的过拟合如果训练loss和验证loss都很高且不下降通常是模型容量不够或者学习率设置不合理属于欠拟合。针对过拟合最直接的调整是加Dropout、增强数据、减小模型容量或增加权重衰减。针对欠拟合则需要增大模型容量、调高学习率、或者检查数据质量。有一类情况我特别提醒一下有些项目测试集指标上不去不是过拟合也不是欠拟合而是训练集和验证集的分布不一致。比如训练数据大多是短文本验证数据全是长文本模型在长文本上表现差是完全正常的。这时候改模型参数没用要处理的是数据分布问题。5.3 用早停法和检查点管理训练过程训练大模型动辄几个小时甚至几天如果中途挂了或者跑到后面才发现loss已经发散没有及时存储检查点那前面所有时间都白费了。我的实践经验是每跑完一个epoch就把模型参数存到磁盘文件名带上epoch数和验证集指标保留表现最好的几个检查点不要只存最新的因为验证集指标最好的epoch往往不是最后一个开启早停逻辑当验证集指标连续多个epoch不再提升时主动终止训练返回最优检查点早停的阈值设置也有讲究。设置太严格可能在指标还在缓慢上升的阶段就提前停了设置太宽松又浪费训练时间。我通常看验证loss是否在连续5到10个epoch内不再下降作为判断依据。如果曲线波动大还可以选择在平滑后的指标上做早停判断。5.4 混合精度训练为什么每张卡都能靠它“多装一倍的模型”混合精度训练已经成为AI工程的标配技巧原理很简单前向和反向计算用FP16来加速和减少显存占用但优化器更新参数时用FP32来保持精度。实现上PyTorch自带的torch.autocast和GradScaler已经封装好了大部分细节代码改动量很小。我自己的实际体验是在支持的GPU上开混合精度后显存占用大约能下降40%左右训练速度提升30%以上。但要注意两个坑第一个是有一些算子对FP16非常敏感计算结果可能溢出尤其是涉及大数值范围的操作第二个是首次启用混合精度时loss可能会出现罕见的NaN这时需要检查GradScaler是否有收到“inf/nan”的标记并考虑是不是学习率过高导致的。实践中跑通第一轮后我通常会对比一下FP32和混合精度在验证集上的指标差异如果差异很小比如0.01以内就放心用混合精度继续跑。6. 部署上线把训练好的模型真正变成可用的服务6.1 部署方式选型是直接起API服务还是走推理优化框架模型训练完成之后很多人就认为项目收工了但真实世界里“训练好”和“能用上”之间的距离相当远。部署一个Transformer模型到线上环境要考虑推理延迟、并发量、显存资源和模型体积每一步都有坑。最简单的部署方式是把模型加载进一个Web服务框架比如用FastAPI包一层接口pipeline直接调用模型推理。这种方式的优点是代码量小、改动快适合原型验证和流量很小的场景。缺点也很明显模型加载到内存后不会自动释放并发量上来时GPU显存可能直接被打满推理延迟也会随之升高。当流量变大后就需要考虑更专业的推理方案。目前主流的做法有几个方向一是用ONNX Runtime对模型做导出和加速推理模型体积通常能压缩推理速度也有明显提升二是用TensorRT做深度优化但其对模型结构的支持有限不是所有模型都能顺利转换三是用专门的推理服务框架比如vLLM它对语言模型的在线推理做了大量优化包括连续批处理、PagedAttention机制等。选型的核心依据是你的业务规模和模型类型不要为了炫技把简单问题复杂化。6.2 与训练服务化相关的几个容易被忽视的工程点部署不只是把模型文件放到服务器上然后跑起来那么简单有几个问题很值得关注。第一个是把模型结构定义和权重文件管理在一起. 训练时你可能在Jupyter Notebook里定义了模型类但部署时又要重新写一份两边稍有偏差就会导致state_dict加载不匹配。我的习惯是训练之初就把模型类放到独立的.py文件里统一import部署时直接复用同一个类定义。第二个是输入数据的预处理链路要和训练时完全一致。训练时你用了什么分词器、什么长度截断策略、什么padding方式部署时的预处理管线必须严格复刻。这里最容易出bug的地方是tokenizer的版本不一致。同一个tokenizer不同版本的结果可能不同上线前一定要在测试数据上逐条对比部署环境和训练环境预处理后的ID是否完全一致。这一步我吃过亏曾因为tokenizer版本更新导致线上推理结果和线下评测结果对不上排查了很久才发现是预处理不一致。第三个是推理时的长度控制。生成式模型在线上推理时最大生成长度、温度参数、采样策略都要显式设置不能依赖训练时的默认参数。不然用户输入一个超长问题模型可能生成一段又臭又长的回答既浪费算力又影响体验。通常我会根据业务需求设定一个合理的max_new_tokens并配合温度参数做一定的随机性控制。6.3 量化与模型压缩当显存放不下完整模型时怎么办部署时经常遇到显存不足的问题尤其是当模型规模超过显卡容量。这时候量化是一个有效的方案。量化就是把模型的权重参数从FP32/FP16压缩到INT8甚至INT4从而大幅减少模型体积和显存占用。代价是推理精度的轻微下降。我对量化的态度是先用但要验证。不是所有模型量化后都还能保持原有表现。对于分类型任务INT8量化通常几乎无损对于生成型任务尤其是推理模型量化后生成质量可能会下降必须用评估集做量化前后对比。除了量化另一个思路是蒸馏——用一个大的教师模型训练一个小一点的学生模型让小模型模仿大模型的输出。蒸馏训练本身需要额外的算力投入但换来的是一个体积小、推理快且性能接近大模型的部署实体。6.4 线上监控与效果回溯部署后才是真正的开始模型部署上线之后还远没到可以高枕无忧的时候。我在实际工程中深刻体会到一个问题离线评估结果好不代表线上效果好。因为线上遇到的输入分布可能和训练集、评估集都有差异用户的新问法、新术语、错别字、超长输入层出不穷。所以部署后至少要建立三套基础监控第一是服务层监控包括请求延迟、吞吐量、错误率第二是输入分布监控记录线上输入的文本长度分布、高频词汇变化及时发现和训练分布差距拉大的趋势第三是输出质量抽检定期对模型的线上预测结果做人工抽检留出“bad case池子”为下一轮迭代收集训练数据。这三个监控里最容易被忽视但价值最高的其实是第三个。很多团队部署后只看系统指标不看业务指标结果模型悄悄变差了几个月都没人发现。把线上的bad case沉淀下来定期做模型微调或提示词优化整个系统才能持续进化而不是一锤子买卖。7. 复盘从零到一最值得避开的几个典型深坑7.1 坑一数据泄漏让评估结果虚高上线后惨遭打脸这是我在早期项目中犯过的、也是我见过新人最容易犯的错误。数据泄漏的表现形式很多样最常见的是训练集和验证集之间存在重复或高度相似的样本导致验证集指标远高于实际可达到的水平。还有一种隐蔽的情况是数据清洗时用了目标变量的信息来过滤样本比如把标签为“投诉”的样本手动剔除了一部分然后模型在验证集上又看到了这些本不该存在的分布规律。规避方法其实不复杂但要求从一开始就养成习惯划分数据集时按ID或哈希值而不是随机种子、重复样本必须先整体去重再划分、清洗规则里禁止使用目标变量信息。上线前做一次数据泄漏专项自查非常有必要把训练集和验证集的相似度算一遍能让你少走很多弯路。7.2 坑二对tokenizer的改动不做验证导致模型效果神秘下降文本类AI项目里tokenizer是牵一发而动全身的组件。有人升级了tokenizer库版本、有人换了预训练tokenizer、有人在代码里改了归一化规则这些改动都会直接影响输入token的切分方式进而影响模型效果。有一次我把一个预训练的中文tokenizer换成了另一个分词效果更好的版本所有离线指标都涨了但模型的序列长度需求也变了导致部分长文本被截断线上出现大量“回答不完整”的bad case。那次之后我养成了一个习惯任何与tokenizer相关的改动都要先在固定样本集上比对token序列和模型输出确认无异常后再全面推开。7.3 坑三前期不做消融实验后期根本定位不了有效改进很多工程师在训练模型时喜欢一次性加很多技巧——混合精度、梯度裁剪、warmup、学习率衰减、数据增强全开。结果模型效果好了却不知道是哪个改动起了作用模型效果差了也不知道是哪个环节拖的后腿。这就是典型的没有做消融实验。我的建议是每加一个改动跑一组对照实验。对照组是最简单的基线配置实验组只在基线上加一个变量。虽然这样做会消耗一些算力但换来的是对每一次改动效果的确切认知长期来看反而是省算力的做法。尤其是你在写博客或做技术分享时“哪个改动带来了多少提升”这种数据比“我最终效果很好”有说服力得多。7.4 坑四拿着训练代码直接部署推理延迟高到无法接受还有一个高频翻车点把训练阶段用的模型对象直接搬到线上做推理。训练阶段的模型带着dropout、batch normalization这些在训练时有用的机制推理阶段应该关闭或调整训练阶段的代码还会保存优化器状态、梯度信息这些在部署时都是完全无用的负担。线上推理至少要单独写一套推理代码关闭dropout、走批量推理来提升吞吐、移掉优化器状态、合理设置KV缓存复用。直接用训练代码起服务的人我几乎可以肯定会在延迟上踩坑只是时间早晚的问题。8. 写在最后从我踩过的坑里总结的几条经验回顾整个从零构建AI工程的实践过程我最深的感受是这条路没有捷径但也不是盲目地一步一个脚印走到底。环境配置阶段的枯燥重复、数据清洗阶段的无聊琐碎、训练调参阶段的反复试错这些看似低效的过程恰恰是建立技术直觉最有效的途径。如果让我给后来者三条最具体的建议我会说第一从最小的闭环开始——哪怕模型很小、数据很少也要把“数据→训练→评估→部署”完整跑通一遍再逐步放大规模第二把每一次失败都记录成文档——很多报错和现象在第二次遇到时如果有一份排查日志解决问题的时间可以缩短十倍第三不要跳步骤——跳过数据清洗去做模型调优、跳过模型评估去做部署优化最终都会以更昂贵的方式重新补课。最后再分享一个小技巧每次项目结束后花半小时把项目里“改对了但不知道为什么对”的配置整理成一个清单标注当时的实验结果下次遇到类似场景直接查阅。这几年下来这个清单已经成为我最宝贵的技术资产比任何教程都有用。如果你也准备踏上从零开始的AI工程之路希望这些经验能帮你少踩几个坑把更多时间花在真正有趣的部分——理解模型、理解数据、理解系统如何协同工作。
返回列表