☰
从零搭建AI工程:120M语言模型全链路训练与部署实战
2026/10/1 12:13:41 网站建设 项目流程

1. 为什么我坚持从零搭建完整AI工程,而不是直接调API

过去一年,圈子里有个现象很有意思:AI开发者的数量暴涨,但真正“做过完整AI工程”的人反而变少了。大部分人拿到需求的第一反应是——有没有现成的API?哪个模型的接口便宜?然后写几百行调用代码,接个提示词,项目就算“交付”了。

不是说这个路径不行,而是它掩盖了太多东西。你会发现,一旦需要做微调、优化推理延迟、控制成本、排查生成质量波动,那些只会调API的人就彻底卡住了。他们不知道模型内部在算什么,不知道温度参数背后到底是什么机制,不知道显存占用为什么突然暴涨。我之前带过一个小团队,同事把幻觉问题归咎于“模型不够聪明”,但实际查下来,是数据管线的编码错误把语料搞脏了——这个层面的问题,不亲手从零搭一遍工程链路,根本不可能有直觉去定位。

所以当“ai-engineering-from-scratch”这个主题出现时,我第一时间就想把它拆成一个能落地的实战项目:从零训练并部署一个小规模语言模型,完整走一遍数据、训练、评估、推理、上线的全链路。这篇文章就是那次项目的完整记录。适合谁看?想做LLM但不想只当“API搬运工”的工程师,对模型内部工作机制有好奇心、想建立系统认知的开发者,以及正在做技术选型、想评估“从零做”到底值不值的技术管理者。

2. 先泼冷水:从零不等于从“零”开始写代码

很多人一听“from scratch”就以为要从矩阵乘法手写Transformer。真没必要。我的理解是:放弃现成的端到端方案,但保留成熟的底层工具库。也就是说不碰那些帮你把训练、部署全部封装好的黑盒框架,但PyTorch、HuggingFace的tokenizer库、flash-attention这些底层组件该用就用。你要“亲手搭建”的是整条链路的工程设计与实现,而不是重新发明轮子。

2.1 为什么选“训练一个小模型”作为从零项目

市面上真正在做的“from scratch”项目,多数是拿现成的大模型做二次开发,微调一下、套个RAG、接个Agent就完事。这确实省事,但它绕开了AI工程里最核心、也最考验功底的几个环节:

  • 数据管线的建设——语料从哪来、怎么清洗、怎么构造训练样本
  • 训练过程的稳定性控制——loss为什么不降、梯度为什么爆炸、显存为什么OOM
  • 评估体系的设计——怎么科学衡量模型能力,而不是只看一两个指标
  • 推理侧的工程优化——量化、批处理、缓存、延迟控制

训练小模型的好处就是:这些环节一个都躲不掉,但资源成本可控。我用约1.2亿参数(120M)的Decoder-only架构,大概相当于GPT-2的缩小版。单卡A100就能跑,误差预算几十块钱电费出头。虽然模型不大,但整条工程链路是完整的,里面踩的坑和大模型项目百分之九十九相同。

2.2 先明确边界:what this article covers / what it doesn't

我给这个项目划了几条硬边界,建议你也这么干,不然很容易陷入“什么都想做,什么都不精”的泥潭:

  • 不做:从零编写反向传播、从零实现Attention机制的数学细节。这些我建议你通过《build a large language model from scratch》这类经典资料补理论,但不适合作为工程项目的起步点。
  • 做:从裸数据开始,一步步构建出可训练、可评估、可部署的完整系统。这一个过程跑下来,你对“AI工程”四个字的理解会和以前完全不一样。

3. 架构选型与核心参数:1.2亿参数模型如何炼成

项目开始前最纠结的一件事就是模型规模。想做大一点,训练时间太长,迭代效率低;做太小,能力和背后的问题体现不出来,达不到练手的目的。最后我定了个折中方案——120M参数,这个规模在推理上非常轻量,同时训练时的各项工程技术问题(分布式、精度、显存优化)都能完整暴露。

3.1 模型架构配置参考

配置项数值选择理由
参数量约1.2亿单卡训练可行,问题链路完整
隐藏层维度768与GPT-2 small一致,参考成熟经验
Transformer层数12层深度适中,梯度稳定性好控制
注意力头数12头768可以整除12,多头注意力参数分配均匀
上下文长度512 tokens兼顾效果与训练速度,降低自注意力计算压力
词表大小32K中文场景下BPE词表32K覆盖率高且不至于过大
参数量决定公式每层约 12768² + 7684*768,12层累计可以通过常见LLM参数量估算公式复核

这里贴一个我自己用的参数量估算逻辑,方便你随时估算:
对于一个Decoder-only的Transformer,每层主要由两部分构成——自注意力(Q/K/V/O四个矩阵)和MLP(上投影+下投影两个矩阵)。以768维为例:

  • 自注意力部分:4个项目分别是 768×768 的矩阵,那就是 4 × 768² ≈ 236万参数
  • MLP部分:通常上投影是 768×3072,下投影是 3072×768,总计约 2 × 768×3072 ≈ 472万参数
  • 再加上LayerNorm、位置编码等零头,每层大概 750万参数左右

12层就是9000万,再加上token embedding矩阵(词表32K × 768维 ≈2500万)。一部分参数是共享的(权重绑定),最终总参数落在1.2亿上下,和直接跑model.num_parameters()的输出基本吻合。这个估算能力在训练大模型时是基本功,因为你得先判断一个模型结构在某个GPU上放不放得下。

3.2 训练规模与计算量的估算

按Chinchilla法则,一个理想训练规模的参考值是“参数量 × 20”个token。这样算下来1.2亿参数对应约24亿token,对个人项目来说量太大了。我实际只准备了约10亿token的中文语料(约3.1GB纯文本),做了打折处理。理由是:小模型吃不满最优数据比,多跑几个epoch反而更容易看到过拟合曲线,这对理解训练动态反而有帮助。

以10亿token、批量大小约100万token(即单batch约2000条512长度样本)来估算,一个step是20万token,10亿token约折合5000个step。在单卡A100上,120M模型每秒大概能处理2万token,一个step大约10秒,5000个step就是14小时左右,实际加日志、断点保存、波动,约20小时跑完。这个时间预算对一个练手项目比较友好,不至于等得失去耐心。

4. 数据管线的完整搭建:从乱糟糟的语料到能吃的训练样本

很多人轻视数据工程,觉得不就是拿点文本喂进去嘛。真做起来你才知道,数据管线才是整个项目里花费时间最多的环节,我大概60%的时间都耗在这了。语料获取相对容易——网上开源的清洗过的中文语料不少,但那是“相对干净”,距离“可直接训练”仍有距离。

4.1 语料清洗的三个隐藏大坑

第一个坑是重复内容。爬虫语料里常见整段重复、模板重复,如果不做去重,模型会疯狂“复读”,论文里管这个叫重复惩罚。我处理10亿token的语料,跑了一遍MinHash去重(重点去的是文档与文档级重复),能肉眼看到复读现象大幅下降。

第二个坑是语言混杂与噪声字符。中英文混杂还好说,难的是全角半角标点不统一、HTML残留、base64乱码、社交媒体里的@符号和URL。我的清洗pipeline非常简单粗暴,但有效:

# 核心清洗逻辑(简化版) import re def clean_text(text: str) -> str: # 去除HTML标签 text = re.sub(r'<[^>]+>', '', text) # 统一全角半角标点(最简单的方式是转换成全角) text = text.replace(',', ',').replace('!', '!').replace('?', '?') # 压缩连续空行 text = re.sub(r'\n{3,}', '\n\n', text) # 过滤过短或比例异常的"乱码" lines = [ln for ln in text.split('\n') if len(ln) > 2] return '\n'.join(lines)

第三个坑是质量过滤门槛别定太高。把“质量”作为单一指标,很容易把一些带方言、口语化表达的文本全删光,反而让语料变得寡淡。我评估后保留了一个较宽的过滤底线:去掉广告、赌博、政治激进言论等明显垃圾,其他内容保留多样性。毕竟10亿token的数据量,靠过滤筛掉10%,不如想办法多找数据源扩充50%。

4.2 Tokenizer训练:为什么我自己训而不是直接用现成的

本来想直接用某个开源的中文BPE词表,省事。结果测试后发现:词表对代码、数学符号和网络用语的覆盖极差,一个表情符号被切成五六个token,训练效率打折扣。后来我决定自己训一个Byte-level BPE分词器,词表大小32K,用SentencePiece工具,训练语料就是上面清洗完的10亿token。

# SentencePiece训练命令(精简参数) spm.SentencePieceTrainer.train( input='corpus.txt', # 清洗后的纯文本语料 model_prefix='my_tokenizer', # 输出模型前缀 vocab_size=32000, # 词表大小 model_type='bpe', byte_fallback=True, # 遇到OOV时回退到字节级 character_coverage=1.0, # 中文覆盖,1.0表示全覆盖 max_sentencepiece_length=16 )

我把这段代码放进数据管线里,每2小时语言模型的外围环境一变(比如新词出现频率变化),就重新训一次,保证词表跟得上语料形态。这件事的意义在后来的推理阶段体现出来了:模型对网络新词、代码片段的生成质量明显比用现成词表的版本好。

4.3 训练样本构造与batch策略

  • 把清洗后的文本按512 token切块,天然就构造成训练样本,不需要额外的监督标签。
  • 我用的是完整文本切块而不是滑动窗口切块。滑动窗口会有大量重叠样本,训练效率低;完整切块信息密度更高。
  • 为了避免一个样本跨越语义边界(比如上一篇文章结尾和下一篇文章开头被拼在一起),切块时按文档边界做填充处理,不足512的部分用<pad>补上,同时在attention mask里把padding区域屏蔽掉。

最后用DataLoader加载时,我额外加了个采样策略:喂给模型的batch里,每条的来源尽量分散,避免同一篇文章被频繁抽到导致模型“背题”。做完这步再回头看,整个数据管线从原始语料到dataloader,已经是一个完整的可复用工程模块了。

5. 分布式训练实操:单卡也跑分布式,值不值

前面已经确定了整体训练时间约为单卡20小时。但我不想就这么盯着进度条,一个重要原因是:如果训练跑到一半就因为某个隐藏bug崩了,单卡从头再来,心理防线容易直接崩溃。更好的方案是搭一个支持多卡断点续训的训练脚本——单卡的时候跑起来,等有空闲多卡时也能平滑扩展。

5.1 训练框架选择与配置

我是基于PyTorch + DeepSpeed实现的训练脚本。核心训练循环长这样(代码逻辑省略了大量细节):

# 训练脚本核心框架(伪代码) import torch import deepspeed def train(): # 初始化Deepspeed引擎 model_engine, optimizer, _, _ = deepspeed.initialize( args=ds_args, model=model, model_parameters=model.parameters() ) for step, batch in enumerate(dataloader): loss = model_engine(batch) model_engine.backward(loss) model_engine.step() if step % 100 == 0: train_state.save_checkpoint(...)

关于为什么用DeepSpeed而不用原生PyTorch DDP,我的理由是:DeepSpeed的ZeRO阶段2能把优化器状态切分到多卡,同样一批数据,显存占用明显更低。120M模型单卡其实是能塞下的,但我故意开ZeRO Stage 2,为的是提前熟悉这套机制,后面迁移到更大模型时可以无缝衔接。

5.2 混合精度训练:速度翻倍但有两个附加事项

为了提速,我开了FP16混合精度。实测收益非常直观:纯FP32训练大概每step要14秒,开混合精度后压到8~9秒。代价是loss曲线里会出现一些毛刺,偶尔会冒出一个特别大的loss值。解决方案:

  • 开loss scaler的自动调整(DeepSpeed默认会处理)
  • 在配置里开启prescale_gradients和gradient_accumulation_steps(梯度累积)

梯度累积是我强推的一个设置:虽然单卡理论显存塞得下2000条样本,但为了稳定我keep住了batch size在256条,用8个step累积成一个大batch(等效batch size 2048条)。效果是梯度方向更稳,loss曲线更平滑,对训练初期防止发散很有帮助。

5.3 训练监控:不要只盯loss曲线

loss不降、loss震荡、loss骤降后又反弹,每种现象的原因都不同。我搭了个轻量监控面板,除了loss之外还额外跟踪了梯度范数和学习率曲线的实际变化。梯度范数一旦持续大于5,说明出现了梯度爆炸的前兆,这时候value clipping和调整lr要两手抓。这里贴一下我实际用的loss下降节奏:

  • 第0~200步:loss从10.8快速掉到6.3,这个是embedding层在快速收敛
  • 第200~1500步:loss从6.3降到4.2,进入平稳下降期
  • 第1500~3000步:降到3.6左右,下降速度开始放缓
  • 第3000步以后:每1000步只降0.1~0.2,说明模型接近容量上限,继续训练纯亏时间

到第5000步左右,验证集loss和训练集loss开始出现几个百分点的差距,说明模型开始过拟合。小模型+固定数据量的情况下这个现象非常正常,反而让我能提前规划评估策略,而不是漫无目的训到天荒地老。

6. 评估体系设计:为什么不能只看loss,还要搞“考试”

训练完的模型,表面看loss很低(最终3.2左右),但loss低不等于“聪明”。一个只会背训练集文本的复读机,loss也可以降得很低。我设计了一套三层评估体系,从浅到深,每一层都能暴露不同问题。

6.1 第一层:基础指标——困惑度与生成多样性

困惑度(Perplexity)是语言模型最基础的评估指标,反映模型对测试语料的“惊讶程度”。我拿模型在验证集上的困惑度大约45,比随机猜测(困惑度≈词表大小32000)少了两三个数量级——说明模型确实学到了语料的统计规律。

但困惑度只能看“概率贴不贴”,不能看“内容是否合理”。我额外测了一个指标叫生成多样性,用模型自己生成100条文本,统计其中不同n-gram的重复率。重复率太高说明模型没学到内容生成的灵活性,只会绕着一两个高频短语打转。

6.2 第二层:任务级评估——仿照考试题出场景

为了测试模型真实可用程度,我design了三个任务:

  1. 完形填空:中文语境下抠掉一个词,让模型预测,看准确率。这个接近语言理解的底线能力。
  2. 对话连贯性:让两个模型实例互相对话5轮,从“语义连贯”“信息密度”“逻辑一致性”三个维度人工打分。我直接拉了两个同事盲评(他们不知道哪个是哪个),结论是:3分制下平均2.2分,比随机拼接强很多,但距离“仿佛一个真人”还很远。
  3. 指令跟随测试:给模型一些简单的指令,如“写一段招聘启事,要求包含岗位职责、任职资格和薪资待遇三段”。观察生成结果是否完整执行了指令。这里暴露了一个预期内的问题——120M模型受限于参数规模,指令跟随能力非常弱,经常写着写着就忘了格式要求。

6.3 与基座大模型的对比,给自己的模型定位

我自己做了个对照实验,把三个任务在某个开源7B模型上跑了一轮,对比结果很有意思:7B模型(没有微调,直接zero-shot)在完形填空上只比120M模型高8个百分点,但指令跟随和对话连贯性上直接拉开一倍差距。这说明“参数规模决定能力天花板”这句话在大模型领域尤其准确。但反过来,120M模型足以做很多场景下的基础文本生成任务(比如标题生成、短段落扩写),在成本控制上优势明显。

这轮评估跑完,可以得出的结论是:这个项目不是为了得到一个多强大的模型,而是给你一套评估模型的工具箱——以后不管你是用开源权重还是自己训,都能有理有据地衡量模型好坏,而不是靠感觉拍板。

7. 推理优化与部署实战:从训练到上线,最后一公里最有技术含量

模型训完了,评估通过了,但距离真正能被调用还有一段路——推理侧的工程优化。这一步直接决定模型的实用价值。慢吞吞的推理接口,就算模型再聪明也没人用。

7.1 量化与格式转换:把FP16变成INT8,实测性能对比

模型默认是FP16权重,显存占用差不多240MB(1.2亿参数 × 2字节),单看不大,但推理时的KV Cache和中间激活值才是显存大头。我用GPTQ做了INT8量化:

  • FP16权重:240MB,推理时显存占用约1.2GB
  • INT8量化后:120MB,推理显存占用约600MB
  • 加速效果:生成速度从每token约35ms降到22ms,速度提升约37%

让我意外的是精度损失非常小,困惑度从45.3变成46.1,几乎可忽略。这给了一个经验:对中小规模模型,INT8量化是性价比极高的优化手段,基本的损失可以接受。

7.2 KV Cache与连续批处理:吞吐量翻倍的秘密

很多人跑推理是一遍一遍来的——一条请求,模型做一次前向传播,结束后再处理下一条。这在流量上来时会直接拖垮速度。我换成了连续批处理(Continuous Batching):不同请求在不同时间点到达,但服务端会把多个请求拼进同一个batch里推理,同时每个请求内的KV Cache独立保存,互不干扰。

直接效果:相同硬件条件下,服务吞吐量从每秒2.5条请求涨到4.8条,接近翻倍。具体实现是用vLLM框架,它原生支持Continuous Batching和PagedAttention,我要做的就是把量化后的模型格式转换成vLLM可加载的格式(safetensors + config.json),改改yaml配置文件即可。这块的工程细节属于“说到就懂、做起来全是坑”,但踩完后收益巨大。

7.3 部署方式选择:自托管 vs Serverless 函数

模型小有小的好处——它可以塞进一个小内存实例里常驻,不需要大规模GPU部署。我最终选了一个中间方案:模型常驻在一个有8GB显存的GPU实例上,配一个简单的HTTP接口服务。

接口实现的要点:

  • 用FastAPI写一个异步接口,接收文本输入和生成参数(max_tokens、temperature、top_p)
  • 服务端预处理(tokenize)和后处理(decode)用同步方式挂在线程池里,避免阻塞事件循环
  • 输出加一个简单的流式接口(SSE),前端可按token实时打印生成结果,体验比一次性等全部生成完要好得多
# FastAPI部署的核心结构(伪代码) from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class GenerateRequest(BaseModel): prompt: str max_tokens: int = 128 temperature: float = 0.8 top_p: float = 0.9 @app.post("/generate") async def generate(req: GenerateRequest): output = await run_model(req.prompt, req.max_tokens, req.temperature, req.top_p) return {"text": output}

部署上线之后,实测单实例可以扛住20个并发请求不超时,单条请求的平均响应时长700ms左右(输入长度10~50 token,输出长度100 token)。对一个120M模型来说,这个性能在轻量级文本生成场景里是很能打的。

8. 从零项目复盘:哪些钱值得花,哪些坑必须踩

整个项目从搭建到上线,前后花了大概三周。每天投入时间不固定,周中平均3~4小时/天,周末加起来能到10小时。算总账的话,GPU费用1200元左右,语料获取和数据工程基本零成本。这个预算在“从零训大模型”的范畴里,性价比已经非常高了。

8.1 预算与资源投入统计

项目成本说明
GPU租用约1200元A100单卡累计约120小时,按峰谷价格
数据获取与清洗0元全程使用开源语料+自写清洗脚本
Tokenizer训练0元SPM训练30分钟搞定
推理部署资源约200元8GB显存实例常驻一周
人工投入约40小时分散在数据工程、训练调参、评估和部署

8.2 拉低效率的三个时间黑洞

复盘时梳理了一下,真正吃掉时间的不是模型训练本身,而是几个非常隐蔽的工程问题:

  • 数据管线的脏数据排查:有一段时间loss始终降不到预期,查了一天半,最后发现是清洗脚本在处理某些语料时把换行符去掉了,导致一批文档内容全部粘连成一段。这个bug如果有好的数据可视化工具,本来几分钟就能定位。
  • 多卡训练的配置折磨:DeepSpeed的配置文件参数太多,一个参数设置不当(比如train_batch_size和gradient_accumulation_steps没配套),训练直接不收敛。建议从单卡跑通后再切入多卡,不要一开始就上多卡排错。
  • 评估的人肉打分:对话连贯性要人工打分,50条样本我拉了两个同事盲评,虽然结果有效,但沟通成本高。以后可以先用规则抽取几个浅层特征做初筛,再让人工只看小部分难例。

8.3 这些经验对做大模型项目有什么借鉴意义

这段从零经验最大的价值是老铁们一直说的“工程手感”。遇到问题的时候,你会先怀疑哪个环节?数据、模型、训练、推理——四个方向上各有一些高频故障模式,你经历过之后,排查的时候就有一种直觉。这种直觉没法通过看书获得,只能踩坑踩出来。

举个具体例子:如果你发现模型生成内容的语言风格发生突变,第一反应应该是我之前搞混过的问题——做了多次断点续训,但resume时dataloader的采样器没有重新设置随机种子,导致每个epoch的训练样本顺序和上个epoch完全一致,模型在大量“背题”的同时丢失了泛化性。现在我对所有seed相关的代码(dataloader、随机采样、dropout)都统一管控,这类问题再也没出现过。

9. 最后的实用建议:什么样的人适合从零搭一次AI工程链路

项目收尾后,我经常被问到“值不值得做”。我的答案是:取决于你想要什么。如果你只是想快速交付一个功能Demo,直接调用现有的API服务是最高效路径,没必要从零训练;如果你想真正理解AI系统背后发生了什么,想在团队里成为那个“什么都能解决”的工程师,那走一遍从零链路是绕不开的必修课。

这不是部分人口中的“重复造轮子”——造轮子能让你理解轮子为什么是圆的,以及什么情况下轮子会不圆。在AI这个快速迭代的行业里,拥有这种底层认知的人,面对新模型、新框架时的适应速度完全不是一个量级。

这次项目做完,我的tokenizer还留着、数据管线的脚本也没有删,偶尔有新想法了会再训一个小模型验证一下。比如我现在正在尝试用同样的链路训练一个面向代码补全的小模型,预期参数量更小,但要接住代码的结构性特征,数据管线需要重新设计。实践下来发现,把第一次链路踩过的坑都规避掉之后,第二次的边际成本低了很多。

如果你看完这篇文章也想自己动手试一次,我的建议是:第一次做,宁小勿大。参数规模小一点没关系,数据量少一点也能说明问题,关键是要把数据、训练、评估、部署四个环节完整体验。等你把这条线跑通一遍,再决定要不要往更大规模迈进,那时候你手里的工具和经验,能让你少走非常多弯路。

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

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

立即咨询