☰
从零构建AI工程:小模型训练、微调与部署全链路实战
2026/10/1 4:24:22 网站建设 项目流程

“ai-engineering-from-scratch”这个仓库名乍一看,像那种收藏了不会再翻第二次的学习清单。但真正把它当成一个工程目标,从头到尾走一遍“AI从零构建”,你获得的远不止一个能跑的模型,而是一整套对数据、训练、推理、产品化的底层直觉。这篇博文就围绕这样一个最小可行项目展开:不靠现成API,从零设计并训练一个百MB级别的小模型,再把它包装成带Prompt工程和Agent能力的完整服务。适合刚入坑深度学习、想搞懂大模型内部到底在发生什么的人,也适合已经会用框架但总觉得“少了点什么”的工程师。

1. 从零开始前,先想清楚这四件事

1.1 核心不是“写代码”,而是“定义问题”

很多人第一次接触AI工程,第一反应是去找模型、跑代码、调参数。但我带过不少项目,也见过无数半途而废的仓库,发现最大的拦路虎不是显卡不够,而是问题没定义清楚。

你说“我想做一个AI”,这句话本身没有信息量。你是想要一个会聊天的机器人,还是要一个能自动读论文摘要的助手?是想要一个能分析Excel报表的工具,还是想要一个能根据代码注释生成单元测试的插件?这些需求落到技术方案上,可能是预训练、微调、检索增强、Agent编排四种完全不同的路线。

所以在写任何代码之前,我会把需求拆成四问:

  • 输入是什么格式?纯文本、文件、图片、多轮对话?
  • 输出是什么期望?一段话、结构化JSON、还是需要调工具的动作序列?
  • 质量怎么衡量?答案准不准?风格像不像?延迟高不高?
  • 约束条件是什么?显卡显存、预算、时间、团队水平?

这四个问题一旦回答清楚,项目的70%其实已经定了。剩下的才是调模型、部署、调优这些“看起来很难但实际上有成熟套路”的部分。

1.2 为什么选“from scratch”,而不是直接调API

市面上已经有很成熟的模型API,填入Key就能用,为什么要费劲从零搭一个?这是我在项目开始前反复问自己的问题。

直接调API的效率确实高,典型场景下几个小时就能做出一版可用原型。但从工程成长的角度看,那种方式叫“消费AI”,不叫“构建AI”。from scratch的价值主要体现在四个维度:

第一是原理可见。当你亲手完成了数据清洗、BPE分词、训练循环、推理服务这一整套链路,模型在你眼里就不再是一个“神秘黑盒”,而是一套由清晰部件组成的系统。以后遇到问题,你至少知道该去Debug哪一层。第二是可定制性。用API时,模型权重不在你手里,你不能针对特定领域数据做真正的定制;自己训练的模型,权重、Tokenizer、采样逻辑全部可控。第三是性能可控。你可以为了低延迟裁剪模型尺寸,为了高吞吐上量化,为了离线部署去掉外部依赖。第四是成本曲线。在持续大规模调用的前提下,自建模型的边际成本要比按量计费低得多。

但我也得说实话:from scratch不是用来做业务MVP的最优解。如果你只是想快速验证一个产品想法,直接调API是对的。这个项目的意义在于“练兵”——用一套小模型把全链路打通,之后无论你用什么API、什么框架,都能站在工程视角去评估方案。

1.3 技术选型的整体判断

我最终选定的技术栈,基本代表了目前从零开始做AI工程的主流配置:

  • Python 3.11 + PyTorch 2.x
  • HuggingFace Transformers / Datasets / Tokenizers
  • vLLM + FastAPI 做推理服务
  • Weights & Biases 或 TensorBoard 做训练监控
  • DVC 或简单的Shell脚本做数据版本管理

为什么是PyTorch?因为它的生态最完整,不管是训练脚本、模型库、推理框架还是量化工具,围绕PyTorch的工具链是最好的。为什么不直接从模型库拉一个GPT权重?因为还是要完整走过数据处理和训练流程,这部分手写才能真正建立手感。

一个关键认识是:from scratch不等于不用框架。你不用自己去实现反向传播、不用手写CUDA kernel、不用从零写Attention。你要做的是理解框架封装的每层概念,并且有能力在需要的时候绕开框架提供的默认行为。比如训练脚本里那个for batch in dataloader,你得知道背后是怎么组batch、怎么做padding的。

1.4 最小可行目标的设定

一个很容易犯的错误是目标定得太大。比如想训练一个能取代GPT-4的模型,先不说数据量和算力,光是训练稳定性就够折腾几个月的。所以我把这个项目的目标压到足够小,但又能覆盖完整工程链路:

从零训练一个约100M参数的Decoder-only小模型,让它能生成语法正确、语义连贯的短故事;然后用LoRA在特定领域数据上微调;最后把它部署成HTTP接口,并封装一个能调用外部工具的Agent demo。

为什么是100M左右?因为一张消费级显卡就能训练,迭代速度快,方便反复试错。为什么是短故事?因为故事类文本对连贯性要求高,能明显暴露出模型是否学明白了,而纯代码或数学类文本对模型能力要求太高,不利于初版跑通。整个项目做下来,大概三天到一周,时间上完全可控。

2. 数据:AI工程的地基

2.1 数据从哪来,怎么洗

我把数据比喻成AI工程的地基:模型结构再先进,训练代码写得再漂亮,喂进去的是垃圾,出来的必然是垃圾。大模型训练“数据质量比数据数量重要”这句话,几乎每个项目都能验证。

数据来源我建议从三个方向入手。第一是公开数据集,比如HuggingFace上的OpenWebText、The Pile、TinyStories这类,它们的清洗程度较高,适合直接用来做初版。第二是你自己的业务数据,比如工单记录、客服对话、代码仓库、产品文档,这类数据数量不大但价值密度极高。第三是合成数据,可以先用一个通用模型生成一批符合目标分布的文本,再人工抽检修正,这在小项目里尤其好用。

但不管数据从哪来,清洗这步省不掉。我的清洗流程通常包含这几个环节:

# 常规清洗流水线 1. 按行读入原始文本,丢弃空行和过短片段 2. 用正则过滤HTML标签、URL、乱码字符 3. 做MinHash去重,减少重复段落对训练的干扰 4. 按长度过滤:句子少于10个字符或超过2000字符的,酌情丢弃 5. 用简单的启发式规则过滤广告、无意义内容 6. 最终人工抽检100条,确认质量后再入训练集

这个过程我用过很多次,实测下来最大的坑是“看起来干净但实际很脏”。比如爬下来的网页文本里充满了导航栏、版权声明,这类重复内容如果不去重,模型会学到“每隔几句话就输出一次版权信息”的坏习惯。所以我会专门花时间做字面去重和MinHash去重,并且在所有清洗步骤之后,随机抽一批样本人工看一遍。

2.2 分词与Tokenizer,为什么它常被忽略

Tokenizer是很多AI工程新手最容易忽略的环节,但它直接影响模型的训练效率和生成质量。简单说,Tokenizer就是把文本切成一串token编号,模型看到的实际上是这些编号而不是原始字符。

目前主流方案是BPE(Byte Pair Encoding),基本思路是这样的:一开始每个字符都是一个独立的token,统计相邻字符对的出现频率,每次把出现频率最高的一对合并成一个新token,不断重复这个过程,直到token数量达到你设定的词汇表大小。比如“university”这个词,可能会被切成"uni"、"versity"这样的子词,这种切分方式既能覆盖常见词,又能合理处理生僻词。

实操层面有几点可以直接抄作业:

  • 词汇表大小建议32K到128K之间。小项目用32K足够,太大反而增加Embedding层的参数量和训练成本。
  • 必须自行保留特殊token的位置,比如<|endoftext|>用来标记文本边界,<|pad|>用来对齐batch内不同长度的样本。后续微调和推理时,这套token映射要原封不动地搬过去。
  • 训练Tokenzier和训练模型必须用同一份文本数据分布,否则推理时,模型可能频繁遇到OOV或异常分词,导致生成质量崩掉。

我吃过一次亏:训练阶段用了A数据集训练的Tokenizer,推理阶段图省事直接用了开源的GPT-2 Tokenizer。结果生成文本里频繁出现奇怪的断裂,排查了很久才发现是两边Tokenizer不一致。后来我把Tokenizer的vocab.json和merges.txt直接打进模型目录,每次加载都校验哈希,才彻底解决这个问题。

2.3 数据配比与采样策略

如果你的语料来源不止一种,比如有通用文本、代码、还有业务数据,那么怎么按比例混合就很关键。我常用的是多项式采样:给每个数据集一个权重,每次组batch的时候先按权重抽数据集,再从该数据集中抽文本。这样能精确控制各数据源的占比。

实际经验里,通用文本和代码的比例,我习惯控制在7:3左右。代码样本能显著提升模型对逻辑结构的理解能力,即便你的最终任务和代码无关,也能感受到生成逻辑的改善。业务数据的占比通常不高,一般10%~20%就够,因为业务数据量小、歧义多,占比太高模型容易过拟合到样本上,反而丢失泛化能力。

还有一个我反复强调的细节:验证集必须从训练集的分布中独立抽取,并且保证不和训练集有交叠。如果不做去重就粗暴划分,验证集里会出现和训练集高度重复的句子,导致loss虽然漂亮,但模型生成质量的真实水平被严重高估。等到一上线就露馅。

3. 模型训练:从随机权重到V1版本

3.1 架构选择与参数配置

模型架构我直接选了Decoder-only的GPT式结构,这基本也是当前大模型领域的主流选择。它的核心由几部分组成:Token Embedding、位置编码(或RoPE)、多层Transformer Block(每层包括多头注意力、前馈网络、残差连接、归一化)、最后的输出投影层。

有人会问:为什么不试试BERT那种Encoder-only?如果任务主要是分类、检索这类理解型场景,BERT确实高效。但我要构建的是一个能自主生成文本并接入Agent的系统,生成能力是刚需,Decoder-only是最省力的选择。

下面给出我实际跑通的100M模型配置,可以直接作为起步参考:

# 模型配置示例 model_config = { "vocab_size": 32768, "n_layer": 8, "n_head": 8, "n_embd": 768, "block_size": 256, "dropout": 0.0, "use_bias": False, "rms_norm_eps": 1e-6, }

这个配置算下来参数量在100M附近。为什么block_size只设256而不是2048?因为小模型的建模能力有限,太长的上下文反而会稀释注意力,让模型学不到稳定的语法和语义模式。先把短文本做扎实,后期如果需要长文本能力,再通过位置编码外推或继续预训练来扩展,这样路径更稳。

3.2 最小GPT训练脚本,关键就那么几行

很多教学代码会把训练脚本写成几百行,看着吓人,但核心逻辑粗到只有四步:取batch、算logits、算loss、反传更新。我通常会用一个极简的复现来跑通流程,再替换成正式的数据加载器。

下面这个训练循环,是我反复用来做基础设施验证的版本。它拿一段连续文本作为输入,目标是文本的下一个token,也就是用前256个token预测下一个token,不断地滑动窗口完成训练。

import torch from torch.utils.data import Dataset class TextDataset(Dataset): def __init__(self, tokens, block_size): self.tokens = tokens self.block_size = block_size def __len__(self): return len(self.tokens) - self.block_size - 1 def __getitem__(self, idx): x = torch.tensor(self.tokens[idx:idx + self.block_size]) y = torch.tensor(self.tokens[idx + 1:idx + 1 + self.block_size]) return x, y # 训练核心循环 for step, (x, y) in enumerate(dataloader): logits = model(x) # 前向,输出(batch, block_size, vocab_size) loss = F.cross_entropy( logits.view(-1, logits.size(-1)), y.view(-1) ) # 把每个位置都当成一个分类任务 optimizer.zero_grad() loss.backward() optimizer.step()

这个代码里值得留意的点是logits.view(-1, vocab_size),它把batch和序列长度两个维度合并了,相当于把每一个token位置都独立成一个分类样本,然后再做交叉熵。这样设计的直觉是:语言模型的任务就是让每个token的概率分布尽量贴近真实下一个token。

优化器我建议直接用AdamW,学习率用小规模实验里常见的3e-4左右。为什么AdamW比普通Adam更稳?因为AdamW把权重衰减和梯度更新解耦了,在不失偏置修正的前提下,能有效抑制过拟合。这个区别在小模型上看着不明显,但规模一大,训练后期会明显省心。

3.3 从预训练到LoRA微调,降本增效的必走之路

预训练之后,模型已经学会了基本的语法和语义结构,可以生成通顺的文本。但如果你要它做特定任务,比如按特定风格写日报、抽取工单里的关键字段,就会发现指令理解能力和输出格式都不对路。这个阶段就需要微调。

全参数微调在我的消费级显卡上显存很不友好,所以我直接上了LoRA。LoRA的原理说起来不复杂:冻结原始权重不变,在每一层Transformer的权重矩阵旁边插入两个低秩小矩阵,训练时只更新这两个小矩阵。低秩意味着参数量大幅压缩,同时因为冻结了原权重,整体显存占用也显著下降。

常用的LoRA配置参数如下:

  • rank:8到16之间,效果和成本比较均衡
  • alpha:建议取rank的2倍,控制LoRA分支的影响强度
  • 学习率:1e-4到3e-4,太高会覆盖原权重学到的通用表示
  • 作用模块:attention层的q_proj、v_proj必加,k_proj和out_proj可选,FFN层也可以插

我实测下来,rank=8、alpha=16、学习率2e-4这组配置在绝大多数文本生成任务上都够用。如果微调后模型出现严重的“灾难性遗忘”,可以先把学习率降到1e-4,再看是不是alpha太大压过了原始知识。

3.4 评估:不要只盯着loss看

训练日志里的loss一路下行,确实很爽,但这只代表模型在训练集分布上的拟合程度,不代表它“会用了”。我在项目里采用了三层评估结构:

第一层是困惑度(Perplexity),可以在验证集上算,用来快速判断模型是否还在正常优化。第二层是任务级评测,针对你的目标场景准备一批固定问题,看模型能不能给出符合要求的输出。第三层是人工评估,让模型生成一段长度适中的文本,肉眼判断是否连贯、是否复读、是否符合风格要求。

一个小技巧是准备几组固定模板的“探针问题”,比如“讲一个关于狐狸和乌鸦的短故事”“用一句话介绍树懒”,每隔几百步训练就生成一次看效果。这种定性观察比数值更直观,能让你在loss没有明显异常时,提前发现模型退化、重复、偏离指令这些信号。

另一个重要原则是评测集要和训练集严格隔离,并且评测集一旦定下来就不要频繁改动。否则你其实在做“对着答案找题”,评估结果也就失去参考价值了。

3.5 训练中的常见失败模式

我把自己在训练过程中遇到最多的三类问题列出来,方便你对照排查。

Loss不降或下降极慢,最先怀疑学习率。学习率太高,loss会在初期冲高后震荡;学习率太低,loss会无聊地平躺。你可以先跑一个50步的小实验,把学习率从1e-5到1e-3各试一档,看看哪个区间能稳定下降。其次怀疑数据问题,比如文本里混入了大量重复片段,或者Tokenizer异常导致id分布大面积断层。

过拟合,也就是训练loss持续下降但验证loss反弹,多半是数据规模太小或者模型容量超出需求。对策包括增加数据多样性、加大dropout、早停。还有一种隐蔽情况是验证集和训练集没有彻底去重,导致“假过拟合”。

输出重复死循环。模型一直输出同一段话,通常和采样设置有关。复用默认的top_k=50、top_p=0.95、temperature=0.8是个不错的起点。如果模型本身训练不充分,也会更容易陷入重复循环,这时候首先应该回头检查训练步数是否足够、block_size是否太短。

4. 让模型变成“工程”:推理、服务化与Agent

4.1 从checkpoint到推理服务,vLLM是吞吐利器

训练完的checkpoint只是一个权重文件,要真正让别人能调用,还得把它部署成一个推理服务。我第一版直接用PyTorch自带的生成接口,写了个FastAPI应用,结果并发一上来,GPU利用率上不去,排队时间却暴涨。后来切到vLLM,同样的模型,吞吐量能提升好几倍。

为什么vLLM能快这么多?核心是它用了PagedAttention,把KV Cache按页管理,显存碎片化问题大幅缓解,同时它内部做了continuous batching,一个batch里的请求可以动态进出,不用等整批结束再处理下一批。这些机制对短文本生成尤其友好。

我用的是一个很常规的部署结构:

# 1. 把checkpoint转成HuggingFace格式 # 2. 用vLLM启动离线推理服务 vllm serve ./model_output \ --host 0.0.0.0 \ --port 8000 \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.8 # 3. 再用FastAPI包一层统一入口

上线前还有一个步骤容易被忽略:模型预热。也就是服务启动后,先用几个固定请求跑一遍,把显存里的CUDA kernel和缓存激活,否则第一个真实请求的响应时间会非常难看。另外生产环境建议限制单请求最大生成token数,并设置合理的超时时间,避免个别异常请求把GPU拖死。

4.2 给模型加“大脑”:Prompt Engineering基础

部署完模型,很多人的第一反应是直接用,然后发现模型的回答“又空又飘”。这里的问题通常不在模型,而在于你没有把任务描述清楚。这就进入Prompt Engineering的范畴了。

模型的本质仍然是一个next token预测器,它生成什么,高度依赖于你给它的上下文。你要在Prompt里把任务约束、输入格式、输出格式、示例都对齐。我最常用的一个模板结构是这样:

你是(角色),任务是(描述)。 约束条件: 1. ... 2. ... 输入内容: {输入} 请按照如下格式输出: {JSON格式} 示例: {示例}

看起来机械,但有效。它本质上是在降低模型的输出空间不确定性,把“开放式回答”收敛到“结构化完成”。Few-shot(给几个示例)和CoT(让模型先写推理过程再给答案)是两种最有效的增强手段。前者适合格式控制,后者适合复杂推理任务。

一个常见的误区是“Prompt越长越好”。并不是。Prompt过长会挤占模型的有效上下文窗口,也可能引入干扰信息。我的经验是,在能说清需求的前提下,Prompt越短越好,只保留必要的约束和示例。

4.3 Agent工作流,让模型学会调用工具

模型能生成文本,但它不能查数据库、不能调API、不能打开网页,这时就需要Agent层面的编排。我实现了一个极简的ReAct流程:模型交替进行“Thought(思考)”“Action(调用工具)”“Observation(观察结果)”,最终输出Answer。

用“论文搜索助手”来举例:你问“最近有哪些关于LoRA的论文”,模型发现自己不知道答案,于是决定调用搜索工具。工具返回一列论文标题和摘要,模型阅读后整理出回答。如果第一轮搜索结果不够,它会再次发起搜索,直到信息足够。

极简实现的核心逻辑可以这样理解:

# 伪代码:ReAct Agent def run_agent(prompt, tools, max_iterations=5): messages = [{"role": "user", "content": prompt}] for _ in range(max_iterations): reply = llm(messages) action = parse_action(reply) # 从输出中提取工具名和参数 if action is None: return parse_answer(reply) result = tools[action["name"]](**action["args"]) messages.append({"role": "assistant", "content": reply}) messages.append({"role": "tool", "content": result}) return "达到最大迭代次数"

这个流程对整个AI工程有很重要的启发:模型不再需要“什么都知道”,它只需要知道如何获取信息。在你的业务场景里,可以把搜索工具换成查工单、查库存、查代码等接口,Agent就变成一个灵活的业务机器人。

Agent的关键挑战不是模型有多聪明,而是你给它的工具边界是否清晰。我会在工具定义里写明参数类型、返回格式、超时时间,并且要求模型在必要时说“我不知道”,避免它为了凑答案而编造工具结果。

4.4 可观测性与版本管理

模型上线后最常听到的一句反馈是:“它好像变蠢了。”排查下来,70%的情况不是模型权重变了,而是输入分布变了、Prompt被改了、或者业务方换了一批数据。要让这些排查变得高效,必须在工程化初始阶段就埋好可观测性。

我要求所有推理请求都记录一份结构化日志,至少包含:请求ID、输入内容、输出内容、模型版本、Prompt版本、耗时、token数。这样任何一次线上反馈,我都能精确回溯到当时的输入输出,快速判断是模型问题还是Prompt问题,还是纯粹的用户预期问题。

模型版本管理也非常重要。模型文件必须和Tokenizer配置、GenerationConfig打包成一个不可变的版本。线上服务永远只加载固定版本,新版本必须先灰度、验证、再全量切换。这个习惯养成了,AI项目才真正算得上“工程化”。

5. 上线后我踩过的那些坑

5.1 常见问题速查表

下面这张表,基本是我过去几个月反复看到的问题集结,给出直接可查的解决方案:

问题现象常见原因解决方向
训练时显存OOMbatch太大或block_size太长减小batch,使用梯度累积;必要时降模型尺寸
Loss不降或震荡学习率不合适跑一个50步的学习率扫描,找到稳定区间
验证集loss明显低于训练loss验证集与训练集数据泄漏重建独立验证集,保证严格去重
生成结果老是复读temperature/top_p设置不当调高temperature到0.8~1.0,降低top_k
Tokenizer加载报错训练和推理用了不同tokenizer固定tokenizer文件并验证哈希一致
API响应极慢没有预热或并发配置不合理部署后先发固定请求预热,再调并发
某些用户请求频繁超时单请求生成token上限过大限制max_output_tokens,对长文本做截断策略
Agent调用工具结果错乱工具参数定义不清晰写好参数类型和返回格式,限制工具数量

5.2 在反复折腾中沉淀的经验

这个项目让我重新理解了“工程”两个字的份量。模型训练只是整个链路的一环,数据质量、部署稳定性、日志可追溯性,任何一环掉链子,最终呈现出来的都是一个“不靠谱的AI”。我个人的体会有三条,供你参考:

第一,先小后大是铁律。我见过一上来就想训7B模型的人,卡在环境配置和数据清洗上整整两周还没见到loss。不如先用100M模型把全链路跑通,再逐步扩大规模,这样每轮迭代都建立在已验证的基础上。第二,每次实验只改一个变量。AI实验中变量太多,同时改学习率和数据配比,一旦结果变差,你根本不知道是谁的锅。第三,保存模型权重时连同训练参数、数据版本、评估结果一起存下来。你永远猜不到哪个老版本会在两周后需要被召回。

这个项目做完以后,整个链路里每个环节的下一步都清晰可见:模型可以考虑做RLHF对齐,服务可以考虑上量化加速,Agent可以扩展成多工具并行和记忆机制。但比这些技术升级更重要的,是你从这条链路里建立起来的“定位直觉”——在一个AI系统出问题时,你知道该往哪里看。有了这种直觉,后续无论玩多大的模型、接多复杂的业务,都不会手足无措。

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

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

立即咨询