ARTICLE DETAIL

资讯详情

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

从零构建AI工程:推理模型与LLM的完整技术路线

从零构建AI工程:推理模型与LLM的完整技术路线 1. 为什么值得放弃现成API从零开始构建AI工程先说一个我观察到的现象这两年后台私信里问得最多的不是怎么调大模型接口而是我想把AI能力真正掌握在自己手里应该从哪里下手。这个问题的背后其实是整个行业在经历一次角色转换——从用AI变成造AI。而ai-engineering-from-scratch这个标题所指向的恰恰就是这条从Use到Build的路径。1.1 调包和造轮子之间隔着一条真正的能力鸿沟如果只是调用现成的模型API你实际上在做什么你在消费别人已经封装好的决策系统。打个比方这就像你买了一辆整车你只需要踩油门和打方向盘。可一旦车辆出现异常、需要改装、需要调整某个部件的性能参数你如果不懂发动机原理就只能原封不动送回4S店。我见过太多团队在项目初期依赖现成API跑Demo一切都很顺。等到进入生产环境就麻烦不断模型输出不稳定、延迟太高、成本爆炸、无法针对特定领域微调、数据隐私根本过不了合规审查。这时候你才发现封闭的黑盒解放不了生产力反而会把你的系统死死地锁在原地。从零开始构建AI能力不是因为你闲得慌而是因为你要掌握的是整个系统的命脉数据怎么处理、模型怎么训练、推理怎么优化、效果怎么评估。这是调包侠永远接触不到的层次。1.2 From Scratch并不意味着重复造轮子很多时候大家误解了从零开始这四个字。它不是说你要从写矩阵乘法、手写反向传播开始尽管做一遍确实有益处——而是说你要理解每一层抽象下面发生了什么并且在需要的地方亲手构建自己的实现。拿最近的趋势来看build a reasoning model from scratch和build a large language model from scratch两股热流几乎同时涌现这绝不是什么巧合。前者关注的是模型的推理能力怎么练出来后者关注的是LLM本身的底层架构怎么搭起来。两条路最终交汇于同一个核心能力对模型全生命周期的掌控力。我自己实际带项目的体会是如果你能完整地从数据准备、词表构建、模型结构设计、训练循环、推理部署全部走通一遍哪怕模型的参数量只有几百万你对AI工程的理解深度也远比那些调了两年API的人要扎实得多。后面的章节我把我最近几个月从零构建的经验完整拆出来每一块都会给出可以落地复现的细节。2. 从零开始的完整技术路线推理模型与语言模型的双线拆解ai-engineering-from-scratch看上去是个大而全的工程方向但你拆开看就会发现它实际上由三条主要技术路线交织而成推理模型的构建、语言模型从零训练以及把它们串起来的工程化能力。这三条线缺一条你手里的项目就跑不完整。2.1 先搞清楚推理模型到底在推理什么Build a reasoning model from scratch最近在圈子里热度很高源于推理模型Reasoning Model已经成为继基础LLM之后的下一站。要构建这类模型你首先要理解一个关键点推理模型和我们平时用的Chat模型在行为范式上有本质区别。普通语言模型的学习目标是预测下一个词它的底层逻辑是拟合人类文本的统计分布。而推理模型要在预测下一个词之上增加一条推演链——模型在给出最终答案之前会显式地生成中间推理步骤这些步骤相当于把一个大问题拆解成若干个可以串行或并行解决的子问题。在工程实现上这和思维链Chain-of-Thought提示技巧一脉相承但区别在于提示技巧是在推理时临时引导模型而训练推理模型是在训练阶段就让模型学会这样的思考模式。从零构建时我建议的第一原则是先不要一上来就设计复杂的多阶段强化学习流程而是先把推理数据这件事做扎实。你要收集或者合成一批带有显式推理步骤的问答对让模型在监督微调阶段就见过大量推导过程最终答案的样本先把推理的行为模式刻进参数里。这是后面一切强化学习流程的地基。2.2 从零构建LLM的经典学习框架分模块逐个击破再来看《Build a Large Language Model from Scratch》这本书带火的学习路线。这本书之所以受欢迎是因为它提供了一条清晰的主线文本分词tokenization→ 嵌入表示embeddings→ 注意力机制attention mechanism→ 多层Transformer块 → 预训练 → 微调。这条路线本质上就是现代LLM的最小复现路径。我把这条路线映射到实操层面拆成六个关卡文本预处理与BPE分词决定你的模型能看到什么粒度的人类语言词嵌入与位置编码把离散词元映射为连续向量空间并注入位置信息多头自注意力让每个token能看见上下文中的其他tokenTransformer解码器块堆叠注意力层与前馈网络加深模型的理解能力预训练循环与损失函数用海量文本做自监督学习让模型学到语言规律监督微调与对齐把预训练模型改造成能回答问题、遵循指令的助手每过一关你都要亲手写代码把它实现一遍而不是直接调用现成的Transformer库。你只有自己实现过注意力矩阵的计算才能真正理解为什么需要缩放点积、为什么要做多头、为什么必须加因果掩码。这些东西在面试场合你可能能背出来但只有动手写过一遍遇到显存爆炸、loss为NaN、生成重复这类问题时你才知道问题出在哪一层。2.3 工程化能力模型构建之外的隐形骨架还有一点很容易被初学者忽略AI工程不只是模型训练还包括数据流水线、评测框架、部署优化、监控告警。我甚至认为from scratch里最难的部分不是模型而是围绕模型的那一圈工程基础设施。举个例子你从零训练一个模型跑了一周loss下降得很漂亮。但这个模型能用吗不能。因为你还缺一套评测方案来判断它到底好不好。loss低不代表生成质量高困惑度perplexity降低也不一定意味着回答更准确。你需要准备一套针对具体任务中文问答、代码生成、逻辑推理的评测集还需要一个统一的评测脚本才能量化模型的每一次改动带来的变化。这些工程环节要考虑的问题不复杂但极其琐碎恰恰最考验工程师的综合能力。3. 实操实录手把手从零训练一个微型语言模型讲完路线进入实操环节。我用一个可以在一张普通消费级显卡8GB显存左右上跑完的项目为例子完整演示一下从零构建LLM的核心步骤。这套流程我是完整跑过的中途踩了不少坑整理出来给大家当参考。3.1 第一步搭建最小的Transformer解码器模型结构我选择了一个极度精简的GPT风格解码器4层Transformer块、8个注意力头、嵌入维度256。这个配置的参数量大概在800万到1000万之间放在今天的标准下算是个玩具但作为理解完整训练链路的载体绰绰有余。import torch import torch.nn as nn class LayerNorm(nn.Module): def __init__(self, emb_dim, eps1e-5): super().__init__() self.eps eps self.scale nn.Parameter(torch.ones(emb_dim)) self.shift nn.Parameter(torch.zeros(emb_dim)) def forward(self, x): mean x.mean(dim-1, keepdimTrue) var x.var(dim-1, keepdimTrue, unbiasedFalse) norm_x (x - mean) / torch.sqrt(var self.eps) return self.scale * norm_x self.shift class GELU(nn.Module): def forward(self, x): return 0.5 * x * (1.0 torch.erf(x / torch.sqrt(torch.tensor(2.0)))) class FeedForward(nn.Module): def __init__(self, emb_dim): super().__init__() self.layers nn.Sequential( nn.Linear(emb_dim, 4 * emb_dim), GELU(), nn.Linear(4 * emb_dim, emb_dim) ) def forward(self, x): return self.layers(x)这里有一个关键细节LayerNorm的实现中unbiasedFalse一定要设置因为Transformer训练时使用的是批内统计方差除以n而不是n-1。这个小差别平时发现不了一旦你要和预训练权重对齐、做精确的推理复现时数值差异就会暴露出来。3.2 第二步实现多头因果自注意力注意力机制是整个模型的核心。因果掩码的作用是让模型在预测第i个token时只能看到它之前的token——这是语言模型从左到右逐个生成的根本保证。class MultiHeadAttention(nn.Module): def __init__(self, emb_dim, n_heads, dropout0.0): super().__init__() self.n_heads n_heads self.head_dim emb_dim // n_heads self.W_query nn.Linear(emb_dim, emb_dim, biasFalse) self.W_key nn.Linear(emb_dim, emb_dim, biasFalse) self.W_value nn.Linear(emb_dim, emb_dim, biasFalse) self.out_proj nn.Linear(emb_dim, emb_dim) self.dropout nn.Dropout(dropout) def forward(self, x, maskNone): batch, seq_len, _ x.shape queries self.W_query(x).view(batch, seq_len, self.n_heads, self.head_dim).transpose(1, 2) keys self.W_key(x).view(batch, seq_len, self.n_heads, self.head_dim).transpose(1, 2) values self.W_value(x).view(batch, seq_len, self.n_heads, self.head_dim).transpose(1, 2) scores queries keys.transpose(-2, -1) / (self.head_dim ** 0.5) if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) weights torch.softmax(scores, dim-1) weights self.dropout(weights) context weights values context context.transpose(1, 2).contiguous().view(batch, seq_len, -1) return self.out_proj(context)缩放因子用head_dim ** 0.5而不是emb_dim ** 0.5这是一个非常容易写错但影响很大的细节。缩放的作用是防止点积结果过大导致softmax进入饱和区梯度消失。当嵌入维度变大时不缩放或者缩放错维度训练很快就会发散。3.3 第三步文本数据与BPE分词器的构建模型结构只是骨架数据才是血肉。我这次用的训练数据是一份约50MB的中文语料新闻、百科段落混合。预处理的第一步是构建词表。实践中很多人会直接使用HuggingFace的AutoTokenizer加载现成词表但既然要from scratch我建议自己实现一个简单的BPE分词器。你不用从零手写BPE算法核心可以借用tokenizers库的底层组件这个并不违背从零构建的初衷——关键是你得理解词表是怎么从语料里长出来的。from tokenizers import Tokenizer, models, trainers tokenizer Tokenizer(models.BPE()) trainer trainers.BpeTrainer(vocab_size32000, special_tokens[[PAD], [UNK], [CLS], [SEP], [MASK]]) # files 指向你的原始语料文件列表 tokenizer.train(files, trainer) tokenizer.save(tokenizer.json)词表大小选32000是个平衡点。太小比如5000会导致很多词被切成碎片序列过长训练效率低太大比如100万会让嵌入层参数量爆炸小模型根本训练不动。对今天这个微型项目来说32000恰好合适。3.4 第四步训练循环的关键参数与loss观察数据准备好之后训练循环本身其实很朴素前向传播、算交叉熵损失、反向传播、AdamW更新。真正考验人的是超参数的选择和训练状态的判断。超参数取值选择依据上下文长度256 tokens小模型学不了太长的依赖长序列会显著增加显存批大小328GB显存下较安全梯度比较稳学习率3e-4使用AdamW时的常见起点峰值学习率不宜超过1e-3训练步数10,000步50MB语料下大概能看4-5个epoch学习率调度余弦衰减前500步预热避免初期剧烈震荡后期平稳收敛权重衰减0.1对Transformer效果显著训练过程中我重点盯着两个信号第一个是初始loss。语料词表32000随机初始化的模型在第一步的交叉熵应该接近log(32000)≈10.37。如果你的初始loss远低于这个值说明模型初始化有问题或者数据管道有泄漏如果远高于说明数值不稳定。第二个是loss的下降趋势。正常训练下loss应该在前1000步内快速从10.3降到7以下之后进入平台期逐渐下行。要是2000步了还在10以上先别调模型去检查数据预处理和掩码逻辑。3.5 第五步模型生成与推理体验训练完成后保存权重并写一个自回归生成函数这是最直观的验收方式。def generate(model, tokenizer, prompt, max_new_tokens100, temperature0.8, top_k40): model.eval() input_ids tokenizer.encode(prompt).ids input_tensor torch.tensor([input_ids]) with torch.no_grad(): for _ in range(max_new_tokens): # 只保留最后 context_len 个token防止位置编码越界 input_tensor input_tensor[:, -context_len:] logits model(input_tensor) next_token_logits logits[0, -1, :] / temperature # top-k 采样只从概率最高的k个token里抽样 top_k_logits, top_k_indices torch.topk(next_token_logits, top_k) probs torch.softmax(top_k_logits, dim-1) next_token top_k_indices[torch.multinomial(probs, num_samples1)] input_tensor torch.cat([input_tensor, next_token.unsqueeze(0)], dim1) return tokenizer.decode(input_tensor[0].tolist())temperature参数的作用是控制概率分布的锐利程度。temperature0.8意味着比原始分布略保守一点生成结果稳定又不至于太过机械。top_k40则直接把那些概率极低的冷门token拦在门外减少随机抽到乱码的可能。这两个参数是所有生成类应用的基础值得多花时间感受它们对输出风格的影响。4. 常踩的坑与排查技巧我替你趟过的浑水从零构建的过程本质上是和意料之外的错误作斗争的过程。这里把我这几个月踩过的坑按频率从高到低整理成一张速查表每一个都是真实原因不是网上随便抄的。4.1 典型问题速查表问题现象真正原因解决方法训练loss一直是NaN学习率过大或数据里有特殊字符先降到1e-4试跑100步检查语料清洗是否彻底初始loss远低于理论值数据预处理时混入了标签信息检查是否把目标token提前拼进了输入序列生成结果全是重复词模型欠训练或temperature过低加大训练步数调高temperature到1.0以上再对比显存不足注意力分数的形状是batch×heads×seq×seq缩短上下文长度或用梯度检查点技术训练速度极慢没有用torch.compile或未开启TF32在训练脚本开头设置torch.backends.cuda.matmul.allow_tf32True多个GPU性能反而不如单卡通信开销远大于计算收益小模型不需要分布式单卡训练反而更高效4.2 loss不下降的逆向排查法如果loss卡在某个高位纹丝不动我的排查顺序是固定的第一步检查数据管道。把第一个batch的输入和标签打印出来人工看一遍很多数据对齐问题一眼就能发现。第二步关掉所有高级技巧把模型换成单层、单头上下文缩短到64先确保最小系统能收敛。如果最小系统还不行那就是代码bug如果最小系统能行那问题出在模型规模或超参数上。这套排查法几乎可以解决90%的loss不下降问题。核心思想是把变量逐个减少直到问题自然暴露。很多新手习惯一上来就怀疑模型结构但真正的病根往往藏在数据管道里。4.3 两个容易被忽视的工程细节第一个是随机种子。PyTorch默认的初始化带有随机性两次训练即使完全相同的配置结果也会有差异。如果你要对比不同超参数的效果务必在数据和模型初始化处固定种子。第二个是推理阶段的model.eval()。很多人在生成时忘了切换模型状态导致Dropout层还在工作生成结果时好时坏还以为是采样参数的问题。这两个细节每个都会浪费你至少半天时间写下来供各位参考。5. 工具链选型与时间路线规划建议在积累了从零构建的实操经验之后你会发现工具选型和时间规划对整个项目的成败影响巨大。这里我给出经过验证的推荐方案。5.1 不同阶段用什么工具最省力阶段推荐工具理由数据处理Pandas Datasets库内存友好支持流式读取大文件分词HuggingFace Tokenizers底层是Rust实现速度比纯Python快几个量级模型开发PyTorch动态图调试方便生态最成熟训练加速torch.compile一行代码提升20%-40%训练吞吐日志监控Weights Biases或TensorBoard远程监控loss曲线避免蹲在电脑前盯部署推理FastAPI ONNX Runtime接口轻量性能接近原生PyTorch的2-3倍5.2 建议按周推进的路线图如果每周能投入8-10小时我建议按下面的节奏走第1-2周搭建环境完成BPE分词器和数据管道能打印出第一个batch的input和label第3-4周实现Transformer解码器全部模块包括LayerNorm、注意力、前馈网络第5-6周完成训练循环让模型在1000步内跑通loss正常下降第7-8周完成生成函数和基础评测脚本开始调参、对比实验第9-10周加入推理数据与监督微调走一遍预训练微调全流程第11-12周做部署封装写一个简单的API服务端到端跑通这只是参考节奏因人而异。重要的是每个阶段结束时都要有一个能跑的东西第二周末你手里有一份可复现的数据集第四周末你手里有一个前向传播正常的模型第六周末你有了一条完整的训练曲线。没有可运行产物的学习学过即忘。5.3 宏观路线的最终权衡说到最后我还想强调一个容易被忽略的问题什么时候该走From Scratch的路线什么时候该走现成工具链的路线这本身就是一个工程决策。如果团队的核心竞争力不在模型层那盲目追求从零构建反而会拖慢业务进度。我见过不少团队在用现成模型已经能解决80%问题的情况下耗时三个月造出性能更差的轮子。但如果模式识别到你的业务长期依赖模型定制能力、需要深入调整结构、训练数据有特殊格式、或者推理成本在总成本中占比过高那么从零构建所积累的控制力会转换成长期的成本优势和技术壁垒。我的建议是先花一周时间做一次小规模验证用迷你模型跑通全链路再决定要不要投入大规模资源。这条路本身就是AI工程中最重要的实践——用小成本验证高风险的假设。总的来说从零构建一个AI系统真正锻炼的并不是背熟某些框架API的能力而是当一个系统出问题时你能通过拆解每个环节找到问题根源的能力。这份能力如果只靠调接口再练几年也积累不起来。
返回列表