
1. 为什么个人开发者现在值得认真做一次 LLM 全流程1.1 从“调 API”到“自己训一遍”的分水岭过去两年大部分个人开发者接触大语言模型的方式其实只有一种调接口。写个提示词接个 SDK套个前端一个“AI 应用”就出来了。这种方式当然能跑通产品但它有个致命问题——你对模型内部到底发生了什么几乎一无所知。一旦遇到领域适配效果差、输出不稳定、成本压不下来、延迟降不下去这类问题你能做的只有反复改提示词改到最后自己都不知道为什么有效。我自己真正下决心走一遍全流程是在做一个医疗文本结构化的小工具时。当时用现成接口处理中药处方审核相关的字段抽取通用模型对“君臣佐使”“十八反十九畏”这类领域概念经常给出似是而非的结果提示词写了十几版准确率卡在 70% 上下上不去。那一刻我意识到问题不在提示词而在模型本身没见过足够的领域语料。于是就有了这个项目用一张 RTX 3090从零走一遍预训练、微调、领域适配、部署的完整链路。这篇内容适合三类人一是想搞懂 LLM 训练到底怎么回事、但被论文和框架文档劝退的个人开发者二是手里有一张消费级显卡3090、4090 这个级别想把它榨干做点真东西的人三是已经在做 RAG、Agent 这类上层应用但想往下沉一层、理解模型行为边界的技术人。我不会假设你有分布式训练经验但会假设你会写 Python、看得懂基本的张量操作。1.2 一张 3090 能做什么、不能做什么先把预期摆正这是整个项目最重要的一步。RTX 3090 有 24GB 显存这个数字决定了你的能力边界。能做的从零预训练一个 GPT-2 级别1.24 亿参数的模型用混合精度加梯度累积在几十 GB 的领域语料上跑出可用的基座对 1B 到 7B 的模型做 LoRA 或 QLoRA 微调跑通完整的 tokenizer 训练、数据清洗、评估、量化部署链路。不能做的从零预训练 7B 以上的模型。算笔账就清楚了——7B 参数用 fp16 存权重就要 14GB加上优化器状态Adam 的动量和方差各一份 fp32就是 14 56 70GB还没算激活值和梯度。3090 的 24GB 连放都放不下。所以“个人开发者从预训练到领域适配”这个标题里预训练指的是小模型从零训练领域适配指的是在大模型上做参数高效微调这两件事的定位完全不同别混为一谈。我最终选的技术路线是GPT-2 架构从零预训练做基座理解 Qwen2.5-1.5B 做 LoRA 领域适配。前者让我彻底搞懂了训练动力学后者才是真正能落地的产物。下面按这个顺序展开。2. 预训练阶段从零训一个 GPT-2 到底在训什么2.1 架构选型为什么是 GPT-2 而不是别的很多人会问2024 年了还训 GPT-2 是不是有点过时。从产品角度是过时从学习角度它是最优解。原因有三第一1.24 亿参数的规模刚好卡在“单卡能训、又能体现真实训练问题”的甜点区再小比如 nanoGPT 的 1M 级别学不到 scaling 相关的经验再大就跑不动第二GPT-2 是纯 decoder-only 的因果语言模型结构干净没有 MoE、没有 GQA、没有 RoPE 这些后来才普及的组件你能把注意力机制、位置编码、层归一化这些基础件看得清清楚楚第三社区资料极其丰富Karpathy 的 nanoGPT 和 llm wiki 类项目基本都拿它当教学载体遇到问题好查。架构上我基本沿用了 GPT-2 的标准配置但做了两处调整。一是把位置编码从可学习绝对位置改成 RoPE旋转位置编码这样后续做长文本外推会方便很多二是把 LayerNorm 换成 RMSNorm省一点计算量也更贴近现代模型的做法。词表大小保持 50257 不变因为我要复用 GPT-2 的 tokenizer 做对比实验。这里有个关键决策点要不要自己训 tokenizer。我的建议是如果你的领域有大量专业术语比如中药名、化学式、法律条文编号一定要自己训一个 BPE tokenizer。通用 tokenizer 会把“炙甘草”切成“炙”“甘”“草”三个 token甚至更碎这会显著拉长序列、增加训练成本、损害语义完整性。我用领域语料训了一个 32000 词表的 BPE实测同样一段处方文本token 数从 187 降到 112压缩率提升 40%这个收益在训练和推理两端都能吃到。2.2 数据准备清洗比训练更耗时间预训练的数据质量直接决定模型上限这句话不是口号。我这次用的语料大约 38GB 纯文本来源包括公开的医药典籍、临床指南、药品说明书、以及一部分通用中文语料用来防止模型在领域数据上过拟合、丧失通用语言能力。整个数据 pipeline 我走了五步第一步是格式统一。把所有来源转成纯文本去掉 HTML 标签、PDF 的页眉页脚、扫描件的乱码。这一步我用的是datasets库加自定义的清洗函数重点处理全角半角混用、多余空白、以及 PDF 转换常见的连字符断词问题。第二步是去重。这一步最容易被忽略但收益巨大。我用 MinHash LSH 做近似去重阈值设在 0.8。实测下来原始语料里有大约 12% 是重复或高度相似的内容去重后训练 loss 曲线明显更平滑模型复读现象也少了。第三步是质量过滤。用困惑度perplexity做粗筛拿一个小的参考模型给每段文本打分把困惑度异常高的通常是乱码或机器翻译腔和异常低的通常是模板化文本都剔掉。这一步会损失大约 8% 的数据但留下的都是“像人话”的文本。第四步是长度过滤和分块。太短的片段少于 50 token信息量不足太长的超过 2048 token训练时要截断浪费。我把文档按语义边界切成 512 到 1024 token 的块块之间保留 64 token 的重叠避免语义被硬切断。第五步是打包。把所有文本块拼成一个大的一维 token 流训练时按固定长度滑窗采样。这样做的原因是避免 padding 浪费——如果按 batch 内最长序列 padding短序列会填充大量无意义的 tokenGPU 利用率会掉得很惨。打包之后每个 batch 都是满的实测吞吐能提升 30% 以上。注意数据清洗阶段一定要做小样本人工抽检。我一开始偷懒没看训到一半发现模型总输出一些奇怪的药品广告词回头查才发现某批语料里混进了营销文案。清洗脚本再智能也替代不了人眼抽查。2.3 训练配置显存、学习率与那些踩过的坑这是整个项目最硬核的部分我把关键参数和背后的计算逻辑都摊开讲。显存预算。GPT-2 1.24 亿参数fp16 权重约 248MBfp32 主权重 496MBAdam 的动量加方差两份 fp32 共 992MB梯度 fp16 约 248MB。加起来静态占用约 2GB。真正吃显存的是激活值。序列长度 1024、batch size 8 的情况下激活值大约占 8 到 10GB。所以 24GB 显存是够的但余量不算特别充裕我最终把 batch size 定在 12配合梯度累积 4 步等效 batch size 48。混合精度。用torch.cuda.amp做 fp16 前向反向配合 GradScaler 防止梯度下溢。这里有个坑LayerNorm我换成了 RMSNorm和 softmax 这类操作对数值精度敏感一定要在 fp32 下算。PyTorch 的 autocast 会自动处理大部分情况但自定义算子要手动加torch.cuda.amp.custom_fwd装饰器。学习率调度。我用的是 warmup cosine decay。warmup 步数设为总步数的 2%峰值学习率 6e-4。这个值不是拍脑袋来的——GPT-2 原论文用的是 1.5e-4 配 batch size 512我等效 batch size 只有 48按线性缩放规则学习率应该按比例调整但小 batch 下噪声大不能完全线性缩所以我取了 6e-4 这个偏保守的值。实测 loss 下降平稳没有出现尖峰。梯度裁剪。max norm 设 1.0。这个几乎是标配防止个别 batch 的异常梯度把模型带偏。我试过不裁剪训到 3000 步左右 loss 突然飙到 8 以上模型直接废掉只能回滚 checkpoint。优化器。AdamWweight decay 0.1beta 设 (0.9, 0.95)。beta2 从默认的 0.999 降到 0.95 是现在大模型训练的常见做法能让优化器对近期梯度更敏感收敛更快。整个预训练跑了大约 72 小时处理了约 12 亿 token最终 validation loss 收敛到 3.12perplexity 约 22.6。这个数字放在通用语料上不算惊艳但在我的领域语料上模型已经能生成语法正确、术语使用基本合理的文本了。2.4 训练过程监控loss 曲线会告诉你一切训练不是启动脚本就完事监控才是重头戏。我盯的主要是四个指标训练 loss、验证 loss、梯度范数、学习率。训练 loss 和验证 loss 的差值能告诉你有没有过拟合。我的经验是如果验证 loss 连续 500 步不降反升而训练 loss 还在降那就是过拟合了该停或者该加数据。梯度范数突然变大通常意味着遇到了异常样本或者学习率太高。学习率曲线要确认 warmup 和 decay 都按预期走我遇到过一次 scheduler 配置写错学习率一直卡在峰值没降模型后期完全没收敛。另外强烈建议接一个实验跟踪工具我用的是 TensorBoard轻量够用。每 100 步记录一次把 loss、grad norm、学习率、吞吐tokens/sec都打上去。出问题的时候回看曲线比翻日志快得多。3. 领域适配让基座模型真正懂你的业务3.1 为什么预训练之后还要做领域适配预训练出来的模型是个“通才”它学会了语言的统计规律但对你的具体任务一无所知。领域适配要解决的就是这个 gap。这里要区分两个概念继续预训练continued pretraining和指令微调instruction tuning。继续预训练是在领域语料上接着做因果语言建模让模型吸收领域知识比如中药的配伍规律、法律条文的表述习惯。指令微调是让模型学会“听懂人话”知道什么样的输入对应什么样的输出格式。两者顺序不能反——先灌知识再教格式。如果先做指令微调模型会过早收敛到特定输出模式后续再灌知识效果会大打折扣。我的做法是先用 1.5B 的基座模型在领域语料上做一轮继续预训练LoRA 方式rank 32再用构造好的指令数据做一轮 SFT。这样既省显存效果也够用。3.2 LoRA 与 QLoRA个人开发者的显存救星全参数微调 1.5B 模型光优化器状态就要 18GB 以上3090 扛不住。LoRA 的思路是不动原权重只在注意力层的 Q、V 投影矩阵旁边挂两个小矩阵 A 和 B训练时只更新这两个小矩阵。参数量能降到原来的 1% 以下。具体配置上rank 我选 32alpha 选 64alpha 通常是 rank 的 2 倍dropout 0.05。rank 越大表达能力越强但越容易过拟合32 是我在 1.5B 模型上试出来的平衡点。target_modules 我挂了 q_proj、k_proj、v_proj、o_proj 四个只挂 q、v 也能work但全挂上效果更稳。QLoRA 是在 LoRA 基础上再把基座模型量化成 4bitNF4 格式加载进一步省显存。1.5B 模型 4bit 量化后只占约 1GB加上 LoRA 参数和激活值总共 6GB 左右就能跑起来3090 绰绰有余。代价是训练速度慢一些量化反量化有开销以及极轻微的精度损失。我的实测是QLoRA 和 LoRA 在下游任务上的差距在 1 个百分点以内但显存占用差了一倍多所以个人开发者优先选 QLoRA。3.3 指令数据的构造质量远比数量重要SFT 阶段最花时间的不是训练是造数据。我的经验是1000 条高质量指令数据效果远好于 10000 条低质量数据。构造指令数据我遵循几个原则第一任务要覆盖真实使用场景。我的工具主要做处方字段抽取和配伍合理性判断所以指令数据就围绕这两类任务构造不掺无关的闲聊数据。第二输出格式要严格统一。比如字段抽取任务我要求模型输出 JSON字段名、嵌套结构都固定死。格式一致性越高模型学得越快。第三要有一定比例的“困难样本”。全是简单样本模型学不到边界情况。我特意构造了一批包含罕见药材、超长处方、以及故意写错的配伍的数据让模型学会处理异常输入。第四用强模型辅助生成但要人工审核。我拿一个更大的模型批量生成候选答案然后逐条人工核对修改。这一步很枯燥但省不得。我试过完全用生成数据不审核训出来的模型会一本正经地胡说八道因为它把生成模型编造的内容也学进去了。数据量上我最终用了约 3200 条指令样本训练 3 个 epoch。再多就过拟合了验证集 loss 会回升。3.4 训练参数与效果评估SFT 的超参和预训练不太一样。学习率要小得多我用 2e-4cosine 调度warmup 比例 3%。batch size 用 4 配梯度累积 8等效 32。训练 3 个 epoch大约 6 小时跑完。评估不能只看 loss。我设计了三层评估第一层是自动指标字段抽取任务看精确率、召回率、F1配伍判断看准确率第二层是格式合规率统计输出能被 JSON 解析的比例第三层是人工盲评随机抽 100 条让不参与训练的同事判断输出是否可用。实测结果字段抽取 F1 从基座的 0.71 提升到 0.93配伍判断准确率从 0.68 提升到 0.89格式合规率从 0.82 提升到 0.99。这个提升幅度说明领域适配确实起作用了尤其是格式合规率接近满分意味着模型真正学会了输出规范。4. 部署与推理优化让模型真正跑起来4.1 量化把模型塞进更小的显存训练完的模型要部署第一件事是量化。我把 LoRA 权重合并回基座后用 GPTQ 做了 4bit 量化。量化后模型体积从 3GB 降到约 900MB推理显存占用从 4GB 降到 1.5GB速度还快了约 20%因为显存带宽压力小了。量化的坑在于校准集的选择。GPTQ 需要一个校准数据集来统计激活值分布校准集必须和实际使用场景的输入分布接近。我一开始随便拿了几百条通用文本做校准结果量化后模型在领域任务上掉点严重。后来换成从验证集里抽的 512 条领域样本掉点控制在 0.5 个百分点以内。4.2 推理框架选型vLLM 还是 llama.cpp个人部署主要两个选择。vLLM 吞吐高、支持连续批处理适合有并发需求的场景但它对显存要求相对高且主要面向 GPU。llama.cpp 支持 CPU 推理和多种量化格式资源占用低适合单机低并发。我的场景是本地工具并发不高所以选了 llama.cpp 的 GGUF 格式。把量化后的模型转成 GGUF用 llama.cpp 加载CPU 上也能跑到每秒 15 个 token 左右GPU 上能到 60 以上。如果你的场景是服务多个用户那 vLLM 更合适它的 PagedAttention 能把显存利用率拉满。4.3 一个容易被忽略的环节提示词与模型的配合模型训好了不代表就能直接用。领域适配后的模型对提示词格式是有“偏好”的因为它在 SFT 阶段见到的都是特定格式。我的做法是把 SFT 时用的系统提示词原样搬到推理阶段保持格式一致。实测这样能让模型输出更稳定格式错误率进一步下降。另外推理时的 temperature 和 top_p 要按任务调。字段抽取这种确定性任务temperature 设 0.1top_p 设 0.9几乎不引入随机性。如果是生成类任务可以适当调高到 0.7 左右。这个没有标准答案得拿验证集试。5. 常见问题与排查实录5.1 训练阶段的典型故障现象可能原因排查与解决loss 变成 NaN学习率过高、fp16 溢出降学习率检查 GradScaler 是否生效关键算子强制 fp32loss 不下降数据有问题、学习率过低检查数据是否正常 tokenize打印几个 batch 看看适当提高学习率显存 OOMbatch size 太大、序列太长降 batch size开梯度检查点用梯度累积补回等效 batch验证 loss 回升过拟合早停加数据加 dropout降 LoRA rank吞吐突然下降数据加载瓶颈检查 dataloader 的 num_workers预取是否开启5.2 领域适配后的“怪毛病”最常见的是灾难性遗忘——模型学会了领域任务但通用能力退化问它简单常识都答不对。解决办法是在 SFT 数据里掺入 10% 到 20% 的通用指令数据让模型保持通用能力。我一开始没掺结果模型连“今天天气怎么样”都答得莫名其妙掺了之后恢复正常。第二个是格式漂移——训练时输出 JSON推理时偶尔输出 Markdown 或纯文本。这通常是提示词格式和训练时不一致导致的把系统提示词对齐就能解决。第三个是复读机现象——模型反复输出同一句话。这多半是解码策略问题调低 repetition_penalty 的阈值或者检查是不是训练数据里有大量重复内容。5.3 我踩过的三个大坑第一个坑是tokenizer 和模型不匹配。我中途换过一次 tokenizer忘了同步更新模型的 embedding 层大小结果训练时直接维度报错。教训是 tokenizer 一旦定了就别轻易换要换就得从头训。第二个坑是checkpoint 存太稀。我一开始每 5000 步存一次结果训到 4000 步时 loss 崩了只能回滚到起点重来。后来改成每 500 步存一次保留最近 5 个磁盘占用可控容错能力强很多。第三个坑是评估集泄漏。我构造指令数据时不小心把几条验证集样本也放进去了导致评估指标虚高。后来做了严格的数据划分和去重检查确保训练集和验证集零重叠。这个坑很隐蔽但影响极大一定要在数据准备阶段就做好隔离。6. 个人开发者做 LLM 全流程的现实建议6.1 时间与成本的真实账本把整个项目的时间成本摊开数据准备约 40 小时最耗时预训练调试约 20 小时实际训练 72 小时可以挂着跑领域适配数据构造约 30 小时SFT 训练 6 小时部署调试约 10 小时。总计约 180 小时其中真正需要人盯着的约 100 小时。电费方面3090 满载功耗约 350W训练 78 小时大约耗电 27 度按居民电价算不到 20 块钱。真正的成本是你的时间。所以我的建议是先想清楚你要解决的具体问题再决定投入多少。如果只是想做个小工具直接上 QLoRA 微调现成模型就够了预训练那部分当作学习练手别指望它产出生产级模型。6.2 什么情况下该走全流程什么情况下不该该走全流程的情况你的领域数据高度专有且量大几十 GB 以上通用模型和现成微调模型都搞不定你需要对模型行为有完全的控制权你是为了学习想彻底搞懂训练这件事。不该走全流程的情况你的数据量很小几千条那直接做 SFT 或 RAG 就行你的任务通用模型已经做得不错只是差一点格式那提示词工程加少量微调就够你只是想快速验证一个产品想法那用 API 最快。RAG 和微调也不是二选一。我的实际做法是两者结合模型负责理解和生成RAG 负责提供实时、准确的知识。领域适配让模型懂“怎么说”RAG 让模型知道“说什么”。这个组合在需要知识更新的场景下特别有效。6.3 后续可以继续深挖的方向这个项目跑通之后我接下来想试几个方向。一是把上下文长度从 1024 扩到 8192用 RoPE 的外推能力加少量长文本继续训练这样能处理整份病历或长合同。二是试试 DPO直接偏好优化用人工标注的偏好对进一步对齐模型输出让它更符合实际使用习惯。三是把整个 pipeline 脚本化、配置化做成一套可复用的模板下次换领域只改数据和配置不用重写代码。最后分享一个我自己的体会个人开发者做 LLM最大的优势不是算力是你能把整条链路都摸一遍。大厂里每个人只负责一小块你可能永远不知道数据清洗那一步到底发生了什么。而你自己走一遍从原始文本到能用的模型每个环节的坑都踩过这种理解是看多少篇论文都换不来的。3090 跑得慢没关系慢有慢的好处——每一步你都看得清楚。