
1. 为什么在 InternLM 上选 XTuner 做低成本微调先说我遇到的最典型的场景手上有一批垂直行业的问答数据几百到几千条不等想让 InternLM 这类开源大模型说话更像这个领域的专家但手里的卡只有一张 24GB 甚至 16GB 显存的消费级显卡租云卡也不想一天烧掉几百块。这个前提下低成本微调不是一句营销话术它对应三个非常具体的约束显存要压得住、训练时间要可控、产出的权重能直接部署。XTuner 大模型微调实战之所以值得单独拿出来讲就是因为这套工具链恰好把这三件事串起来了——配置文件驱动、原生支持 LoRA/QLoRA、一条命令完成训练到合并。我先把结论摆在这里在单卡或双卡的小规模场景下用 XTuner 对 InternLM2 系列做 QLoRA 微调是目前门槛最低、出错率最低的一条路。它的价值不在于功能最多而在于默认配置就是能跑通的。全参微调听起来更正统但 7B 模型全参至少要 4 张 A100 级别的卡才谈得上舒服光是权重、梯度、优化器状态三份拷贝就吃掉 80GB 以上显存这对绝大多数个人开发者和中小团队来说是直接劝退的门槛。QLoRA 把基座权重压到 4bit只训练插进去的低秩矩阵显存需求能压到原来的十分之一左右效果损失在垂直领域问答这种任务上实测差异远小于数据质量带来的差异。这篇文章适合三类人看一是手里有数据但没跑过大模型微调的新手照着做能出第一个可用模型二是跑过全参微调、被显存和成本折磨过的工程师想换成性价比更高的方案三是做应用落地、需要评估微调到底值不值的产品侧同学里面关于数据量和效果上限的部分可以直接参考。1.1 从能不能跑起来倒推方案选型选微调方案我的习惯是先算显存再看效果最后才看工具好不好用。顺序不能反因为显存不够的话后面全是空谈。大模型训练时显存主要被四块吃掉模型权重、梯度、优化器状态、激活值。以 7B 模型、fp16 精度为例权重 14GB梯度 14GBAdam 优化器要存一阶动量和二阶动量共两份 fp32约 56GB再加上前向传播中间产生的激活值跟 batch size 和序列长度正相关通常几 GB 到十几 GB加起来 90GB 起步。这就是为什么全参微调的门槛那么高。LoRA 的思路很聪明冻结原来的权重不动在旁边挂两个小矩阵 A 和 B前向时把B A加到原权重上。因为原权重不需要梯度梯度和优化器状态就只剩这两个小矩阵的量。7B 模型在 rank16 的情况下LoRA 参数大概在千万量级占原模型的千分之几这部分的开销几乎可以忽略。但权重本身还是 fp16 的 14GB激活值也还在。QLoRA 在 LoRA 基础上再进一步把冻结的基座权重直接量化成 4bit 存储计算时反量化回 bf16。14GB 瞬间变成 4GB 左右这才是把门槛从一张专业卡拉到一张游戏卡的关键一步。所以低成本微调的标准组合就是4bit 基座 LoRA 适配器 梯度检查点 小 batch 配梯度累积。1.2 LoRA 与 QLoRA 到底省在哪一笔显存账我习惯在动手前先估一遍估算是防止白跑一晚上的最好办法。下面这张表是我按 InternLM2 系列常见规格整理的粗略量级实际会因序列长度、框架版本有浮动方案7B 模型显存占用1.8B 模型显存占用适用显卡训练速度全参 fp1680GB 以上24GB 左右多卡专业卡慢卡间通信开销大LoRA fp1620GB 至 26GB8GB 至 12GB单卡 24GB中等QLoRA 4bit9GB 至 14GB4GB 至 6GB单卡 16GB 甚至 12GB比 LoRA 慢 20% 左右省下来的显存代价是什么量化本身有精度损失但 4bit NF4 量化对基座能力的影响在垂直任务上基本感知不到因为微调的目标是改说话方式、补领域知识不是重建语言能力。真正的代价是速度——每次前向都要做反量化训练吞吐大概比 fp16 LoRA 低两成。这个交换我认为完全值得因为省下来的是能不能跑的问题而慢两成只是多等两小时的问题。还有一个容易被忽略的点gradient_checkpointing。它把前向的中间激活值丢掉反向传播时重新算一遍显存能再省 30% 到 50%代价是训练速度慢 20% 到 30%。在显存吃紧的时候这个开关是必须开的XTuner 的默认配置里一般已经打开了。1.3 XTuner 的配置文件驱动模式适合谁XTuner 最舒服的设计是把所有东西塞进一个 Python 配置文件里模型路径、数据路径、batch size、学习率、LoRA 的 rank 和 alpha、优化器、学习率调度、日志间隔、保存间隔全在一个文件里。训练命令就一行不用记一堆命令行参数。这种模式的好处是可复现。你把 config 文件丢进 git三个月后回来还能一模一样地重跑。坏处是初次接触时不知道从哪下手——配置文件动辄两三百行看着头大。我的建议是别从零写直接抄官方模板改三个地方模型路径、数据路径、输出目录。剩下的先别动跑通了再说。很多人第一次失败就是因为一上来就大改参数改出问题又不知道是哪一处引起的。另外 XTuner 内置了一堆开箱即用的模板按模型规格和微调方式排列组合internlm2_7b_qlora_xxx这种命名规则很好认。找到跟你模型规模匹配的那个复制一份出来改这是最省时间的路径。2. 微调前的三件准备工作动手之前有三件事必须确认清楚顺序错了后面全是返工。这三件事分别是硬件和环境是否匹配、数据格式是否符合要求、基座模型和 tokenizer 是否完整。听起来是废话但我见过太多人卡在这三步中的某一步上折腾一整天。2.1 硬件与环境的现实评估先看卡。单卡 24GB比如 3090、4090、A10跑 7B 的 QLoRA 是很宽裕的甚至可以把max_length拉到 4096。单卡 16GB 跑 7B QLoRA 要小心序列长度别超过 2048batch size 必须是 1。单卡 8GB 建议直接上 1.8B 模型7B 就算勉强塞进去稍微长一点的样本就会 OOM训练过程会非常痛苦。再看磁盘。基座模型 7B 的 fp16 权重约 14GB加上 tokenizer、配置文件下载下来接近 15GB。训练过程中每个 checkpoint 又是几 GB 到十几 GBLoRA 的 checkpoint 只有适配器几百 MB这点很友好。合并后的完整权重再占 15GB。预留 100GB 空间是稳妥的。然后是环境。Python 3.10 是比较稳的选择PyTorch 版本要和 CUDA 驱动匹配。我踩过的坑是装完 XTuner 之后发现它依赖的 transformers 版本跟环境里已有的版本冲突把别的项目搞崩了。所以强烈建议用 conda 建独立环境不要往系统环境里装。conda create -n xtuner_env python3.10 -y conda activate xtuner_env显卡驱动版本这块用nvidia-smi看一眼右上角的 CUDA Version装 PyTorch 时选不高于这个版本的 CUDA 版本就行。2.2 数据准备格式比数量更重要低成本微调里数据质量对最终效果的贡献远大于任何超参数调整。我一直跟人说三千条干净数据的效果好过三万条脏数据。脏数据包括格式错乱、答案自相矛盾、答案过长导致被截断、以及大量我不知道式的敷衍回答。XTuner 支持的对话数据格式主要两种。一种是conversation格式也就是一组多轮对话{conversation: [{system: 你是一个专业的设备故障诊断助手, input: 机器运行时有异响怎么排查, output: 先确认异响的位置...}]}另一种是单轮的 alpaca 式字段是instruction、input、output。垂直问答任务用单轮就够了多轮反而会拉长序列、增加显存压力。格式上有几个硬性要求违反了会直接报错或者静默出错一行一个 JSON 对象不能有换行符跨行这是 JSONL 的基本规则。文件里不能有空行空行会导致解析时抛异常。每个样本的 output 字段必须非空否则 loss 计算出 NaN。中文不需要转义但如果你用脚本生成数据ensure_asciiFalse记得加上否则中文全变成\uXXXX虽然能解析但人工检查时可读性极差。数据量怎么定我的经验是单一任务、风格迁移类500 到 1000 条就能看到明显效果知识注入类要让模型记住具体的产品参数、流程规范2000 到 5000 条比较稳再往上收益递减明显不如把精力花在清洗上。如果数据少于 200 条微调很容易过拟合模型会开始复读训练集不如用提示词工程prompt engineering解决。还有一个必须做的动作统计 token 长度分布。用 tokenizer 把每条数据编码一遍算出 95 分位数max_length设成比这个值稍大一点。设太小会截断答案最要命的是把 output 截没了模型只学到前半句就结束设太大会浪费显存。from transformers import AutoTokenizer import json tok AutoTokenizer.from_pretrained(你的模型路径, trust_remote_codeTrue) lens [] with open(train.jsonl) as f: for line in f: d json.loads(line) text .join([t[input] t[output] for t in d[conversation]]) lens.append(len(tok(text).input_ids)) lens.sort() print(median:, lens[len(lens)//2], p95:, lens[int(len(lens)*0.95)], max:, lens[-1])2.3 基座模型与 tokenizer 的确认模型下载下来之后先确认目录结构完整。至少要包含config.json、tokenizer.model或tokenizer.json、pytorch_model.bin或者分片的*.safetensors。少一个 tokenizer 文件训练到中途才会报错非常浪费时间。我习惯在配置文件里用绝对路径而不是相对路径。相对路径在xtuner train启动后工作目录可能被切换到work_dirs下导致找不到模型。这个坑我踩过一次报错信息还不够明确查了半小时才发现是路径问题。路径末尾的斜杠也别加/path/to/model和/path/to/model/在某些拼接逻辑下会出问题。另外确认一下模型的对话模板。InternLM2 系列有自己的特殊 token 体系微调时 XTuner 会自动套用对应的模板你不需要手动拼|im_start|这类标记。如果你用的是别的基座一定要在 config 里确认prompt_template配置对上了模板错了模型学到的就是一堆乱码。3. 配置文件逐行拆解与关键参数计算配置文件是整个微调过程的中枢看懂它基本上就掌握了 XTuner。这一节我把关键字段拆开讲重点解释每个数字背后的逻辑而不是简单告诉你填这个值。3.1 从官方模板继承一份 configXTuner 的 config 体系基于 mmengine支持继承。官方模板一般长这样我以 InternLM2 的 QLoRA 配置为例说明结构字段名以你本地安装的版本为准版本间会有调整from mmengine.config import read_base with read_base(): from .internlm2_7b_qlora_alpaca_e3 import * model_name_or_path /path/to/internlm2_7b data_path /path/to/train.jsonl max_length 2048 batch_size 1 accumulative_counts 16 max_epochs 3 lr 2e-4 evaluation_inputs [介绍一下你的能力, 这个故障怎么处理]继承的好处是基础配置里那些model、tokenizer、train_dataloader、optim_wrapper的复杂结构你都不用管只覆盖顶层变量就行。XTuner 的模板里这些变量都用大写或者特定命名暴露出来了覆盖它们就能改变深层配置。需要改的只有三处模型路径、数据路径、工作目录。第一次跑通之前其他参数一个都别动。这份配置能进官方仓库说明是被验证过的先跑起来再说优化的事。如果要改输出位置在训练命令里加--work-dir参数即可不用改 configxtuner train ./my_config.py --work-dir ./work_dirs/run_001 --deepspeed deepspeed_zero23.2 LoRA 参数怎么定LoRA 有三个核心参数rrank、lora_alpha、lora_dropout。它们在 config 的model字段下或者在lora子配置里具体位置看模板。r是低秩矩阵的秩决定了适配器的容量。r 越大能表达的变化越复杂参数和显存也越多。常见的取值rank 取值参数量级7B适用场景8约千万级风格微调、简单格式对齐16约千万级通用推荐垂直问答首选32约两千万级知识注入、任务较复杂64约四千万级数据量大、任务差异大我一般从 16 起步。任务简单比如让模型统一用某个格式回答用 8 就够数据量上万、任务跟基座差异很大再往上加到 32。再大就没必要了不如考虑全参微调。lora_alpha是缩放系数实际生效的缩放是alpha / r。常见做法是让 alpha 等于 2 倍的 r这样缩放比例是 2。有人喜欢让 alpha 等于 r缩放比例为 1。这个参数的影响没有 r 大我一般就用模板默认值不动它。lora_dropout是防止过拟合的数据量小低于 1000 条时可以设 0.05 到 0.1数据量大的时候设 0 更利于收敛。默认通常是 0我建议保持。还有一个隐藏的关键点LoRA 作用在哪些层上。默认一般覆盖 attention 的 q、k、v、o 和 MLP 的 gate、up、down 全部线性层。有人为了省参数只挂在 q、v 上这样效果会打折扣不建议。全部挂上的参数量也就千万级没必要省。3.3 batch size、梯度累积与学习率的联动显存有限的时候batch_size只能设 1但梯度下降需要一定的有效批量才有稳定的方向。解决办法是梯度累积连续算 16 次前向反向把梯度攒着第 16 次才更新一次参数。有效批量 batch_size × accumulative_counts × 显卡数单卡、batch_size1、accum16 的情况下有效批量是 16。这是比较保守的设置。如果你数据量在几千条这个级别有效批量 16 到 32 都合适。数据量特别小几百条累积次数可以降到 8让每个 epoch 有更多更新步数否则一个 epoch 才几十步模型没学到什么就结束了。这里有个容易算错的地方总步数。比如 3000 条数据batch_size1、accum16、单卡那么一个 epoch 的步数是3000 / 16 ≈ 188步。三个 epoch 就是 564 步。如果save_steps设成 500那整个训练只会存一次 checkpoint。建议save_steps设在总步数的三分之一到四分之一左右方便观察中间效果也方便回滚。学习率方面LoRA 的学习率比全参微调高一个数量级。全参常用 1e-5 到 2e-5LoRA 常用 1e-4 到 3e-4。原因是 LoRA 参数是从零初始化B 矩阵初始为 0刚开始梯度很小需要大一点的学习率推起来。QLoRA 因为量化误差学习率可以稍微再大一点2e-4 是个稳妥的起点。如果训练过程中 loss 出现明显震荡上下跳动幅度超过 0.3说明学习率偏大降到 1e-4。如果 loss 几乎不动一直卡在 2.5 左右说明学习率偏小或者数据有问题先检查数据格式再考虑调大到 4e-4。还有一个参数必须提max_epochs。垂直任务微调 1 到 3 个 epoch 就够了。超过 3 个 epoch模型开始背训练集表现为在训练集上回答得完美在没见过的问法上直接崩。判断方法是训练集 loss 持续下降但验证集 loss 开始上升这时候就该停了。4. 完整实操流程从装环境到跑出第一个模型这一节是纯操作记录按顺序走一遍。每一步我都标注了大概耗时和容易出错的位置你照着做应该能在一个下午内出结果。4.1 环境搭建与依赖安装conda create -n xtuner_env python3.10 -y conda activate xtuner_env # 按你的 CUDA 版本装对应的 torch下面以常见版本示意 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装 xtuner pip install -U xtuner装完之后验证一下xtuner list-cfg | head -20能列出配置模板名字就说明装好了。如果报command not found检查是不是没激活环境或者pip装的路径和PATH不匹配。装flash-attn这个可选依赖时容易踩坑它需要编译跟 CUDA 版本和 torch 版本强绑定。我的建议是先不装用默认的 attention 实现也能跑只是慢一点、显存多一点。等基础流程跑通了再考虑装它提速。下载模型这一步7B 的 fp16 权重十几 GB网速慢的话要等很久。建议用支持断点续传的方式。下载完成后ls -lh看一眼文件大小分片文件应该是几个 4GB 到 8GB 的 safetensors缺片的话训练加载时会报 key 不匹配。4.2 数据转换与数据集注册假设你手上是 Excel 或者 CSV需要转成 JSONL。转换脚本我一般这么写import json rows [] # 假设这里是从 csv 读出来的 [问题, 答案] 列表 with open(train.jsonl, w, encodingutf-8) as f: for q, a in rows: q, a q.strip(), a.strip() if not q or not a or len(a) 10: continue # 丢掉空答案和过短答案 item {conversation: [{system: 你是一个专业的行业问答助手, input: q, output: a}]} f.write(json.dumps(item, ensure_asciiFalse) \n) print(总条数:, sum(1 for _ in open(train.jsonl)))几个细节值得强调。第一system字段建议统一训练和推理时保持一致否则模型对 system 的响应会不稳定。第二过滤掉长度小于 10 的答案这些通常是是、好的这类无信息量的回复留着只会让模型学会敷衍。第三转换完一定要人工抽检 20 条肉眼看看有没有乱码、字段错位、答案对错了问题——这一步能省掉后面几小时的困惑。如果数据需要划分验证集建议 95/5 或者 90/10。XTuner 的 config 里可以同时配train_dataset和eval_dataset也可以只配训练集训练完手动拿几条新问题测。垂直任务数据本来就不多我一般只留 20 条做人工验证剩下的全用来训练。数据集在 config 里通过dataset字段指定不同版本字段名可能是train_dataset具体看模板。如果用的是dataset_map_fn之类的映射函数注意它默认按某种格式解析格式对不上会静默产出空样本——这是最阴险的坑表现为训练能跑但 loss 一直是常数。4.3 启动训练与过程监控xtuner train ./my_config.py \ --work-dir ./work_dirs/run_001 \ --deepspeed deepspeed_zero2单卡训练的话其实可以不加--deepspeed但加上也没坏处Zero-2 会把优化器状态分片多卡时是必需的单卡时开销可以忽略。启动后前几分钟会看到加载模型、构建数据集的日志。正常跑起来之后日志里会周期性打印Epoch 1/3, Iter 20/188, Loss: 1.8234, LR: 0.000200看 loss 是判断训练是否正常最直接的手段。健康的曲线是前 50 步快速从 2.5 左右掉到 1.5然后缓慢下降到 0.8 到 1.0 之间趋于平稳。如果前 100 步 loss 一直贴着 2.5 不动基本可以确定是数据格式问题停下来检查。训练过程中可以另开一个终端nvidia-smi看显存。稳定在某个值附近波动是正常的如果缓慢上涨然后 OOM多半是显存碎片问题试着把PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128加到环境变量里。checkpoint 会存在work_dirs/run_001/下命名是iter_XXX.pth。LoRA 的 checkpoint 只有几百 MB存起来很轻松所以save_steps可以设小一点多存几个方便对比。4.4 权重转换、合并与推理验证训练结束后iter_XXX.pth是 XTuner 自己的格式需要先转成 HuggingFace 格式再合并到基座模型上。# 第一步pth 转 HF 适配器 xtuner convert pth_to_hf ./my_config.py ./work_dirs/run_001/iter_564.pth ./hf_adapter # 第二步合并适配器与基座 xtuner convert merge /path/to/internlm2_7b ./hf_adapter ./merged_model --max-shard-size 4GB第一步产出的hf_adapter里就是标准的adapter_model.bin和adapter_config.json这个目录可以直接用 PEFT 加载也可以走第二步合并。合并的好处是推理时不需要额外加载适配器部署简单坏处是占空间。如果空间紧张不合并、用 PEFT 动态加载也行。合并完直接对话测试xtuner chat ./merged_model --prompts 机器运行时有异响怎么排查 --temperature 0.1temperature设低一点0.1 到 0.3输出的稳定性更好方便判断模型是不是真的学到了东西。如果输出跟基座模型差不多说明微调没生效回去检查数据。如果输出开始复读训练集原句说明过拟合了减少 epoch 或者增加数据多样性。验证一定要用训练集里没有的新问法。我见过有人拿训练集里的原题去问模型答得一字不差就以为成功了实际上只是在背诵。构造 10 到 20 条同义但表述不同的问法看模型的回答是否保持了同样的业务逻辑这才是真实效果。5. 常见问题与排查实录这一节是我和自己踩过的坑、以及帮别人排查时反复遇到的问题汇总。按发生频率排序越靠前越常见。5.1 显存类问题OOM 的排查顺序OOM 是最高频的问题没有之一。排查要有顺序别一上来就乱改参数。第一个检查点max_length是不是设太大了。2048 和 4096 的显存差距接近一倍。先用 1024 跑通再往上加。第二个检查点batch_size是不是大于 1。单卡 16GB 跑 7Bbatch_size必须是 1靠accumulative_counts补有效批量。第三个检查点gradient_checkpointing开了没有。这个开关能省 30% 以上显存代价是慢一点显存紧张时必须开。第四个检查点换更小的模型。7B 在 8GB 卡上硬撑是不明智的1.8B 的 QLoRA 在 6GB 显存就能跑先把流程跑通建立信心再上大模型。还有一个隐蔽的 OOM 原因数据和模型不在同一张卡上。多卡环境下如果只指定了一张卡却加载了完整模型或者CUDA_VISIBLE_DEVICES设错会出现明明卡是空的却报 OOM。跑之前nvidia-smi确认进程占用的卡是不是你预期的那张。5.2 训练效果类问题loss 不降和过拟合loss 卡在 2.5 附近不动九成是数据没被正确读入。具体表现是每步的 loss 值几乎完全相同。原因通常有两种dataset_map_fn跟你的数据格式不匹配解析出空样本或者data_path指向了错误的文件。排查方法是在 config 里临时加一句打印或者在 Python 里单独跑一遍数据集的构建逻辑看看第一条样本长什么样。loss 降到很低但推理效果差这是过拟合。判断标准是训练集 loss 低于 0.5 但新问法答得乱七八糟。处理方式是减 epoch、加数据、提高lora_dropout。1 到 3 个 epoch 是安全区超过 5 个 epoch 基本都会过拟合。还有一个特殊情况loss 正常下降但模型输出开始夹杂奇怪的符号或者中英混杂。这通常是对话模板不匹配导致的。模板里的特殊 token 跟基座模型训练时用的不一致模型会把它们当成普通文本学习。解决办法是确认 config 里的prompt_template字段跟模型规格匹配用错模板比如拿 Chat 版模板配 Base 模型就会出现这种症状。5.3 数据与格式类问题速查我把常见的报错和对应原因整理成一张表出问题先来这里对照报错或现象可能原因处理办法JSONDecodeErrorJSONL 里有空行或跨行对象删空行确认每条样本单行KeyError: conversation字段名跟模板预期不符检查数据字段或改用对应格式训练能跑但 loss 恒定样本被解析成空检查 map_fn 与数据格式匹配CUDA out of memorymax_length 或 batch 过大降长度、降 batch、开 checkpointing合并后推理报 key 不匹配分片文件缺失或版本不兼容检查模型目录完整性加 shard-size 参数输出全是重复句子过拟合或 temperature 过低减 epoch调高 temperature 到 0.7中文输出乱码编码问题读写统一encodingutf-8训练极慢每秒不足一步未装 flash-attn 或数据加载阻塞装 flash-attn调大 dataloader workers数据加载阻塞这个坑值得单独说。默认num_workers设得小GPU 一直在等数据利用率上不去。日志里会看到 GPU 利用率在 0 到 100 之间反复跳。把num_workers调到 4 或者 8根据 CPU 核数吞吐能提升明显。但也别设太大超过 CPU 核数反而会拖慢。5.4 几条不写在文档里的经验第一条先用 100 条数据跑通全流程再用全量数据跑。这个习惯帮我省了无数次时间。100 条数据跑 10 步就能验证环境、配置、格式是不是全对成本几分钟。直接上全量数据跑到一半发现格式错了白等两小时。第二条checkpoint 宁可多存。LoRA 的 checkpoint 才几百 MB存 10 个也就几 GB。存少了发现第 3 个 epoch 效果最好却回不去只能重跑。第三条评估要固定一批测试题。每次微调都拿同样 20 条问题去问横向对比不同配置的效果才有意义。凭感觉判断好像好了一点非常不可靠人对生成文本的质量判断波动很大。第四条合并后的模型可以再用量化工具压缩。如果最终是要部署到显存受限的机器上合并完再用 4bit 量化7B 模型能压到 4GB 左右推理速度也更快。但要注意量化推理和 QLoRA 训练叠加可能有精度累积损失部署前一定要在真实测试集上验一遍效果。6. 关于成本与效果的几句实话低成本微调这件事工具层面已经把门槛降得很低了真正决定成败的是数据和对任务的拆解。我见过花一周调参效果平平的也见过半天微调效果惊艳的差别全在数据质量上。有那时间研究 rank 从 16 改成 32 能提升多少不如把训练集里那几十条答非所问的样本挑出来删掉。另外提醒一句期望管理微调不是万能的。它能改风格、补知识、对齐格式但它不能让一个 7B 模型具备 70B 模型的推理能力。如果你的需求是复杂逻辑推理先考虑提示词工程和检索增强别指望微调能解决。微调最擅长的场景是模型本来就会说话只是说得不像我们这行的——把这句话想明白很多方案选择就清楚了。按我自己的经验从零到跑出第一个可用的垂直问答模型熟练之后一个下午够用第一次做留出两天比较现实。卡住的地方通常不是技术本身而是数据准备和环境配置这些琐碎环节。把这两块摸熟了后面每次微调都是一条命令的事。