简介:面向AI研发提效与LoRA微调实践人群的资源包,围绕Llama(Alpaca LoRA)与ChatGLM(ChatGLM Tuning)两条主流微调路线,覆盖用户故事生成、测试代码生成、代码辅助生成、文本转SQL、文本生成代码五类典型研发任务,既演示参数高效微调思路,也提供从数据准备、脚本调试到结果查看的完整学习路径,适合有深度学习基础、想通过低秩适配技术轻量微调大模型的开发者和研究者。资源包充分体现低秩适配与P-Tuning的差异,提供开箱即用的Alpaca LoRA训练Notebook与ChatGLM Tuning脚本,并配以JSONL格式训练数据、Python预处理工具和结果演示,便于对比两种微调方案的实现差异,在较小显存占用下实现针对性能力增强,整体适合在消费级GPU上尝试。全包共93个文件,约53.46MB,核心包括18个ipynb训练调优Notebook、20个jsonl标注数据集、8个py工具脚本、11个md说明文档、10个pdf参考材料,并附图片与配置示例;目录按数据、代码、文档分层组织,既有原始与合并数据集,也有运行日志、调试输出等辅助内容,方便按模块复用。目前已有442人学习/下载,内容编排兼顾原理讲解、案例拆解与动手实践,尤其适合刚接触LoRA的开发者按步骤复现。除可直接运行的训练Notebook与数据预处理脚本外,还可获得依赖冻结清单、JSONL合并工具、输出日志、效果演示、单元测试项目骨架与金融科技领域示例,支撑从环境搭建、数据准备、模型微调到结果验证的完整闭环,显著降低复现门槛,便于二次改造迁移到自身业务场景。
1. 自己动手训练 LoRA:一张消费卡就能把大模型调成懂业务的样子
做私有化 Agent 和知识库问答的团队,多半都卡在同一个环节:模型能跑起来,但回答太“通用”,放到业务里总觉得差点意思。全参微调一张 A100 都未必够,而 LoRA 用一张消费级显卡就能把大模型调成特定领域的形状。LoRA 想通以后并不玄学:冻结原权重,在旁路加两个低秩矩阵,训练时只更新旁路。这篇笔记就按 Llama(Alpaca LoRA)和 ChatGLM 两条主流路线的 LoRA 训练为主线,讲清楚数据准备、最小可跑脚本、显存参数和那些容易翻车的坑。目标是让刚开始接触 LoRA 微调的工程师在 16G 或 24G 显存上跑通从数据到发布的完整闭环;已经跑过一两次的人,也能对着参数表和避坑清单调出自己的版本。
2. LoRA 原理与选型:低秩适配靠什么省下显存和时间
2.1 冻结大模型权重,旁路里塞两个小矩阵
假设模型某一层权重是 W,形状是 d×k。前向计算时输出是 h = Wx。LoRA 不做任何结构改动,只在旁边加了一条分支:h' = Wx + BAx。其中 A 是 d×r 的矩阵,用高斯分布初始化;B 是 r×k 的矩阵,全零初始化。训练时 W 被冻结,梯度只走向 A 和 B,这就是“低秩适配”的全部含义。
为什么这样设计能work?因为微调本质上是对预训练权重的增量修正,而这个增量在某个具体任务上的“有效自由度”并不高。预训练已经给了模型强大的通用能力,领域适配只是把回答的分布往业务侧推一推,低秩分解足够承载这个偏移。这也是 LoRA 和全参微调在效果上最核心的差别:LoRA 不是换一个模型,而是给原模型加一个方向明确的推力。
参数量对比很直观。以 Llama 7B 的 q_proj 为例,权重形状是 4096×4096,单层就有 16M 参数;r 取 8 时,LoRA 分支只有 4096×8 + 8×4096 = 65536 个参数,只占原来的 0.4%。全参微调需要把所有层的梯度、优化器状态都放进显存,LoRA 只需要存两个小矩阵的梯度,这就是为什么一张消费卡能跑 7B 甚至 13B 模型的微调。r 越大表达能力越强,但显存和过拟合风险也同步上涨,r=8 是绝大多数场景的起步值。
2.2 什么时候该用 LoRA,什么时候该用全参微调
把 LoRA 和常见的几种微调方案放在一起看,选型会清晰很多。下面这个对比是我自己做选型时最常用的参考表。
| 方案 | 可训练参数量 | 显存需求(7B 级) | 推理延迟 | 适用场景 |
|---|---|---|---|---|
| 全参微调 | 全部 | 70G 以上,多卡才稳 | 无变化 | 数据量极大、任务与预训练分布差异大 |
| P-Tuning / Prefix Tuning | 极少(embedding 前缀) | 8G 左右 | 有额外计算 | 少量样本、只需调整输出风格 |
| LoRA | 0.1%~1% | 12G~16G(bf16),8G 内(4bit) | 可合并进原权重,零额外延迟 | 领域指令微调、对话风格调整、知识库问答适配 |
| QLoRA | 0.1%~1% | 6G~10G | 可合并 | 显存有限时的 LoRA 微调,效果略低于 LoRA |
这里有个容易误解的点:QLoRA 不是一种新微调方法,它是“4bit 量化基座 + LoRA”。量化的是被冻结的基座权重,LoRA 分支本身仍然用 bf16 训练,所以最终合并回原权重时精度损失基本可控。如果你的显卡刚好差一口气,优先试 QLoRA 而不是把 batch_size 压到 1 硬扛。
什么时候别用 LoRA,这个判断比学会用 LoRA 更重要。数据量到几十万条且能力缺口明显时,低秩增量带不动,该上全参就上全参;需要模型学会全新知识体系或全新语言(比如让英文模型直接学会中文对话),LoRA 的效果会明显打折扣;基座选错时 LoRA 也救不回来,中文业务场景用中文基座做 LoRA,比用英文基座加中文数据硬调靠谱得多。我的经验是:LoRA 负责“把模型已经会的知识,按你的格式和风格说出来”,它不负责“教模型它本来不会的东西”。
3. 指令数据准备:从 Alpaca 格式到可训练的文本列
3.1 三种常见数据形态和 Alpaca 归一化脚本
Alpaca 数据是斯坦福那套 self-instruct 生成出来的格式,一条样本三个字段:instruction(指令)、input(可选的背景输入)、output(期望回答)。这套格式后来成了开源社区的事实标准,也是标题里“Alpaca LoRA”的源头。实际清洗数据时,我遇到的更多是这三种形态混在一起:
- 单轮问答:instruction + input + output,Alpaca 原生格式。
- 多轮对话:conversations 列表,user 和 assistant 交替出现。
- 纯文本续写:没有明确指令,比如代码补全、文档改写,就是一段文本接一段文本。
这三种格式不能直接丢进同一个训练脚本,因为它们拼接出来的文本结构不一致,模型会学到混乱的对话规律。常见的做法是先归一化成一个 text 列,指令和回答之间用固定分隔符隔开。下面是我常用的转换函数,兼容三种形态。
# data_normalize.py import json def build_text(item): if "conversations" in item: # 多轮对话:只取最后一轮 user/assistant,前面的轮次拼进指令 turns = item["conversations"] history = "".join( f"用户:{turn['content']}\n" if turn["role"] == "user" else f"助手:{turn['content']}\n" for turn in turns[:-1] ) last = turns[-1] return {"text": f"历史:{history}\n用户:{last['content']}\n助手:"} if "instruction" in item: # 单轮问答:输入为空时省略背景 if item.get("input"): return {"text": f"指令:{item['instruction']}\n背景:{item['input']}\n回答:"} return {"text": f"指令:{item['instruction']}\n回答:"} if "text" in item: # 纯文本续写:直接当回答训练 return {"text": item["text"]} normalized = [build_text(json.loads(line)) for line in open("raw.jsonl", encoding="utf-8")] with open("train.jsonl", "w", encoding="utf-8") as f: for item in normalized: f.write(json.dumps(item, ensure_ascii=False) + "\n")归一化脚本的逻辑说明:多轮对话只保留到最后一轮,是为了避免训练时上下文过长导致显存失控,也避免了模型把历史轮次里用户的话当成自己要生成的内容。单轮问答把 input 作为背景拼在指令后面,回答统一用“回答:”开头,模型会在训练中学会看到这个前缀就输出业务答案。如果你的场景需要模型真的记住多轮上下文,就不要用这个脚本,改用下面的标签遮蔽方式配合长 max_length 训练。
这里要注意一个参数选择:指令和回答之间的分隔符要全文统一,不要有时用“回答:”有时用“助手:”。分隔符一旦混乱,模型生成的头部就会带着各种奇怪的前缀,这是我调整数据格式时踩得最频繁的坑。
3.2 拼接与标签遮蔽:让模型只对回答做学习
数据归一化成 text 列之后,下一步是把文本变成 input_ids,并且决定哪些 token 参与 loss 计算。很多新手直接拿 tokenizer 把整条文本编码,然后让模型把整条文本都当成预测目标,这样模型会花大量精力去背你的指令和背景,回答质量反而下降。正确的做法是对 label 做遮蔽:问题部分的 label 全设成 -100,只有回答部分的 token 参与 loss。
# tokenize_with_mask.py from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("/data/models/llama-2-7b-hf") tokenizer.pad_token = tokenizer.eos_token # Llama 没有 pad token,必须显式指定 def tokenize_with_mask(examples, max_length=512): texts = examples["text"] model_inputs = tokenizer(texts, max_length=max_length, truncation=True, return_tensors="pt") labels = [ids.copy() for ids in model_inputs["input_ids"]] # 找到回答起始位置“回答:”对应的 token,之前的全部屏蔽 answer_token_ids = tokenizer("回答:", add_special_tokens=False)["input_ids"] for i, input_ids in enumerate(model_inputs["input_ids"]): answer_pos = None for j in range(len(input_ids) - len(answer_token_ids) + 1): if input_ids[j:j + len(answer_token_ids)].tolist() == answer_token_ids: answer_pos = j break if answer_pos is not None: labels[i][:answer_pos + 1] = [-100] * (answer_pos + 1) model_inputs["labels"] = labels return model_inputs这段代码的逻辑说明:先把整条文本编码,再找到“回答:”这几个 token 的位置,把该位置之前的 label 全部替换成 -100。PyTorch 的 CrossEntropyLoss 会自动跳过 -100 的位置,所以模型只会从“回答:”之后开始学。max_length 这里设 512 是起步值,初版流程跑通后再根据业务上下文长度往上调。
数据量方面,我的建议是走“小而精”的路线:1k 条先验证全流程,5k~20k 条常见于垂直场景。超过 20k 条时先想想是不是数据噪声太多,而不是无脑加量。数据质量上重点看三点:指令多样性是否覆盖线上真实问题、输出是否稳定无格式噪声、有没有重复样本需要去重。有些团队从网上爬了一堆问答对,不清理就直接训,结果 LoRA 把爬虫里的 HTML 标签都学进去了,生成回答带着“
”这类尾巴,这种数据问题靠调参数救不回来。
4. Llama 与 ChatGLM 的 LoRA 训练实操:最小可跑脚本和参数对照
4.1 Llama(Alpaca LoRA)最小训练脚本
Llama 系的 LoRA 训练是生态里最成熟的,transformers + peft + datasets 三件套就能跑。下面这个脚本接第 3 章归一化后的 train.jsonl,是一个能直接跑通的最小实现。
# train_llama_lora.py import torch from datasets import load_dataset from transformers import ( AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer, DataCollatorForSeq2Seq ) from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training model_path = "/data/models/llama-2-7b-hf" tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) tokenizer.pad_token = tokenizer.eos_token model = AutoModelForCausalLM.from_pretrained( model_path, load_in_4bit=True, # QLoRA 加载方式,显存不够就开 torch_dtype=torch.bfloat16, device_map="auto", ) model = prepare_model_for_kbit_training(model) model.config.use_cache = False # 训练阶段关闭 KV cache,省显存 lora_config = LoraConfig( r=8, # 低秩维度,先 8 起步 lora_alpha=16, # 缩放系数,通常是 r 的两倍 lora_dropout=0.05, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], bias="none", task_type="CAUSAL_LM", ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() # 确认可训练参数量远小于原模型 dataset = load_dataset("json", data_files="train.jsonl")["train"] dataset = dataset.map( # build_text 和 tokenize_with_mask 参考第 3 章 lambda x: tokenize_with_mask(build_text(x), max_length=512) ) args = TrainingArguments( output_dir="./llama_lora_out", per_device_train_batch_size=1, gradient_accumulation_steps=8, # 等效 batch size = 8 learning_rate=2e-4, num_train_epochs=3, logging_steps=50, save_steps=500, fp16=True, # 消费卡建议开,V100 及以下用 fp16 remove_unused_columns=False, ) trainer = Trainer( model=model, args=args, train_dataset=dataset, data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True, label_pad_token_id=-100), ) trainer.train()脚本参数的逻辑说明:load_in_4bit 打开后,基座权重被量化到 4bit,显存占用直接掉到 bf16 的四分之一,这是 QLoRA 的核心。r=8 是低秩维度,控制旁路矩阵的大小;lora_alpha 是缩放系数,实际更新强度差不多是 alpha/r 倍,所以 alpha 取 r 的两倍是社区最常见的做法。target_modules 里列的是 Llama 注意力层的四个投影矩阵,如果用了某些 Llama 变体,模块名要对应调整,最稳妥的办法是打印 model.named_modules() 确认。fp16 和 bf16 二选一,A100 上优先 bf16,消费卡没有 bf16 优化就用 fp16。
跑之前先在命令行执行 python train_llama_lora.py,第一件事是看 print_trainable_parameters 的输出。正常情况下 trainable params 占总参数量的 0.5% 左右,如果这个比例异常高,说明冻结没生效,检查 prepare_model_for_kbit_training 是否执行。训练过程中如果 loss 从 1.5 左右起步并在几个 epoch 内降到 0.8 以下,基本算正常;如果 loss 开局就是 0.1,大概率标签遮蔽没生效,模型在背答案。
4.2 ChatGLM LoRA:加载方式、target_modules 和数据格式差异
ChatGLM 走的是另一套路子,最大的差异在三处:权重加载需要 trust_remote_code=True、注意力层结构不同导致 target_modules 不一样、对话格式里带“问/答”角色标记。下面是最小差异脚本,主体结构和 Llama 版本一致,只列关键差异。
# train_chatglm_lora.py model = AutoModelForCausalLM.from_pretrained( "THUDM/chatglm2-6b", # 或本地路径 trust_remote_code=True, # ChatGLM 依赖 remote code,必须开 load_in_4bit=True, torch_dtype=torch.bfloat16, device_map="auto", ) tokenizer = AutoTokenizer.from_pretrained( "THUDM/chatglm2-6b", trust_remote_code=True ) lora_config = LoraConfig( r=8, lora_alpha=32, # ChatGLM 场景稍大的 alpha 更稳 lora_dropout=0.05, target_modules=["query_key_value"], # ChatGLM 把 QKV 合在一个线性层 bias="none", task_type="CAUSAL_LM", )参数说明:ChatGLM 的自注意力把 query、key、value 三个投影合在同一个线性层里,模块名叫 query_key_value,这和 Llama 的 q_proj/k_proj/v_proj 是两套命名体系。如果按 Llama 的模块名直接套,peft 会报找不到模块,这是 ChatGLM 训练最常见的报错。lora_alpha 我习惯给到 32,因为 ChatGLM 的输出分布和 Llama 不太一样,需要稍微大一点的缩放系数才能看到明显变化,但这要根据你自己的数据实验调整。
ChatGLM 的数据格式还有一个特殊点:它的官方对话模板是“[Round 0] 问:… 答:…”,训练时如果不按这个模板拼接,模型在推理时按模板生成就会对不上。我的处理方式是统一在 build_text 阶段就把文本拼成“问:…\n答:…”的形态,这样生成阶段只需要沿用同样的前缀就能触发。多轮场景下,按第 3 章的思路保留历史轮次拼接进“问:”部分即可。
4.3 显存预算与超参对照表
显存是这个方向最现实的门槛,下面是我在 16G 和 24G 显卡上实际跑过的预算参考。注意这里的数值会因为 max_length、batch_size、是否开 gradient_checkpointing 产生明显浮动。
| 模型规模 | 加载方式 | 设备显存 | 建议 max_length | 是否开 gradient_checkpointing |
|---|---|---|---|---|
| 7B(Llama/ChatGLM) | QLoRA 4bit | 8G~10G | 512 | 否 |
| 7B | QLoRA 4bit | 10G~14G | 1024 | 是 |
| 13B | QLoRA 4bit | 16G~20G | 512~1024 | 是 |
| 7B | bf16 LoRA | 16G~20G | 512 | 否 |
超参对照表是另一个容易让人纠结的地方,直接给一组可用的起步值:
| 超参 | 推荐起步值 | 调整方向 |
|---|---|---|
| learning_rate | 1e-4~2e-4 | loss 震荡时降到 5e-5;数据量小也降 |
| num_train_epochs | 2~3 | loss 降到平台期就停,不是越多越好 |
| per_device_train_batch_size | 1 | 显存允许时升到 2,否则用梯度累积 |
| gradient_accumulation_steps | 8 | 等效 batch size 保持在 8~32 |
| r | 8 | 数据量大或任务复杂升到 16 |
| lora_alpha | 2×r | 效果弱时升到 3×r 试一版 |
gradient_checkpointing 是显存不够时的后悔药,打开后训练速度会慢 20%~30%,但显存占用能低三分之一。它和 gradient_accumulation_steps 不冲突,两个可以同时开。如果 4bit 加载后显存还是爆,优先砍 max_length,把 2048 降到 1024 往往比把 batch_size 从 2 降到 1 更有效,因为大部分输入 padding 到 max_length 时,显存被无意义的空格吃掉了。
5. LoRA 训练避坑:5 个高频问题的现象、原因和处理方法
5.1 训练 loss 一直在降,回答却变成复读机
现象:loss 掉到 0.7 以下,看着很漂亮,但生成阶段模型反复输出同一句话,或者把指令内容原样吐回来。这是 LoRA 新手最容易遇到,也最容易误判成“模型坏了”的情况。
原因:学习率偏大导致模型在低秩空间里震荡到某个局部极值;更常见的是标签遮蔽没生效,模型把整条文本包括你的指令都背下来了,生成时它只是在续写“指令部分”而不是回答。loss 下降只代表模型学会了预测下一个 token,不代表它学会了你的业务逻辑。
解决:先验证标签遮蔽,取一条训练样本,打印 labels 数组,确认指令部分全是 -100。然后学习率从 2e-4 降到 1e-4,重训一个短的 epoch 对比。如果复读机现象出现在训练后期,把 epoch 从 3 降到 2,LoRA 过拟合的典型表现就是后期 loss 极低但生成退化。
5.2 batch_size 稍微调大就爆显存
现象:per_device_train_batch_size 从 1 调到 2,直接 CUDA out of memory,但 nvidia-smi 看显存似乎还有剩余。
原因:transformers 的 DataCollator 默认会把 batch 内所有样本 padding 到当前 batch 的最大长度,而不是模型的最大长度。如果两条样本一条 200 token 一条 1500 token,显存按 1500 分配,200 的那条也在空转。很多人的数据里混着超长文本,截断设置又没生效。
解决:先确认 tokenize 时 max_length 有没有真正截断,可以在 tokenize 函数里打印统计信息看最长长度。训练参数里开 gradient_checkpointing,同时把 gradient_accumulation_steps 提到 16 来弥补 batch_size 下降带来的收敛变慢。如果单条样本本身就超过显存,优先缩短 max_length,这是比任何花式优化都直接的解法。
5.3 加载旧 LoRA 权重时维度对不上
现象:训练完保存了 adapter_model.bin,下次加载时报维度不匹配,说某个 key 的 shape 对不上,或者直接报找不到 query_key_value 这类模块名。
原因:LoRA 权重和 r、lora_alpha、target_modules 强绑定。你上次用 r=8 训完,这次想用 r=4 加载,维度当然对不上;或者换了 ChatGLM 版本,remote code 里的模块结构变了,同名的 query_key_value 实际形状也变了。
解决:每个 LoRA 训练任务保存时,把 lora_config.json 和 base model 的版本信息一起归档。加载前先对比当前 r 和 config 里的 r 是否一致,不一致就重新训练,不要妄图裁剪权重。ChatGLM 这类依赖 remote code 的模型,切换版本前先跑一次 model.named_modules() 看模块名有没有变。我把这当作一个惯例:LoRA 权重不跨 r 值复用,不跨模型版本复用。
5.4 训练完拿到新权重,生成结果和原来一模一样
现象:loss 正常下降,adapter 权重也保存了,但生成时用 PeftModel 加载后输出和 base model 完全一样,仿佛 LoRA 没训过。
原因:最常见的是推理时忘了加 adapter,直接加载了 base model;还有一种情况是训练脚本里 model.requires_grad_(False) 和 get_peft_model 的执行顺序错了,LoRA 参数实际没被优化,loss 下降只是巧合。另外有些推理框架需要显式传 adapter_name,不传就默认走原始权重。
解决:训练阶段看 print_trainable_parameters 确认 trainable params 不为 0。推理阶段用 PeftModel.from_pretrained(base_model, "lora_out") 加载,并在 generate 前打印 model.active_adapter 确认 adapter 处于激活状态。合并权重时如果文件大小和 base model 几乎一样,说明合并成功;如果文件大小没变,说明 merge 没有生效,需要检查 peft 版本。
5.5 ChatGLM 多轮数据把系统提示当成了对话内容
现象:用 ChatGLM 训练多轮对话数据后,模型生成时会把“系统提示”里的内容当成上一轮用户的话复述出来,或者角色混乱,助手开始替用户提问。
原因:数据拼接时没有按 ChatGLM 的“[Round 0] 问:… 答:…”模板组织,也没有区分角色。模型在训练里见到的规律是“所有文本都是它要预测的内容”,推理时系统的角色标记它根本不认识,自然当成普通文本处理。
解决:回到第 3 章的 build_text,把多轮数据按“问:用户输入\n答:助手输出”的格式重组,系统提示固定放在“问:”之前,并在标签遮蔽时把系统提示部分也设成 -100。如果你用的是 ChatGLM3,它的官方 tokenizer 自带 apply_chat_template 方法,直接用它来构造训练文本,比自己拼模板更稳。这种角色混淆问题,本质上是数据和生成模板没有对齐,别在训练参数上浪费时间。
6. 把 LoRA 合并回原模型:发布前必须做的导出与回归验证
6.1 合并权重与保存完整模型的一个技巧
训练产物是 adapter 权重,而部署环境往往只认完整模型文件。peft 提供 merge_and_unload 可以直接把 LoRA 分支合并进冻结权重,得到一个脱离 peft 依赖的完整模型。一个关键习惯:永远在副本上合并,不要在训练机上直接覆盖原始权重。LoRA 微调本质是给原权重做增量修改,合并操作不可逆,没有备份就没有后悔药。
# merge_and_save.py from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer base_model = AutoModelForCausalLM.from_pretrained( "/data/models/llama-2-7b-hf", torch_dtype="auto", device_map="auto" ) lora_model = PeftModel.from_pretrained(base_model, "./llama_lora_out") merged_model = lora_model.merge_and_unload() merged_model.save_pretrained("/data/models/llama-2-7b-lora-merged") tokenizer = AutoTokenizer.from_pretrained("/data/models/llama-2-7b-hf") tokenizer.save_pretrained("/data/models/llama-2-7b-lora-merged")这段代码里,merge_and_unload 会把 BA 矩阵乘进原始权重并释放 LoRA 训练相关的额外结构,保存出来的模型文件大小应该和原模型基本一致。文件大小明显变小说明合并没有完整执行,检查 peft 和 transformers 版本是否匹配。合并后建议用 transformers 的原生接口做一次生成验证,而不是直接依赖你训练时的 notebook 环境,因为部署环境往往没有 peft。
6.2 固定 case 清单做回归,别只看 loss
训练时盯着 loss 看是本能,但 loss 下降不等于业务效果达标。我现在每训一版 LoRA 都会维护一份固定的 case 清单:20 到 50 条,覆盖三类样本——垂直场景的典型问题、通用能力的保留测试、容易越界的边界问题。每条 case 记录生成结果,和 base model 的输出并排对比。
具体做法是写一个批量评估脚本,把 case 清单逐条输入合并后的模型,输出到 markdown 文件里做人工打标。业务问题看回答是否准确且格式符合要求;通用能力问题看模型有没有“变笨”,比如原来会算的简单逻辑题现在还对不对;边界问题看它会不会输出不该输出的内容。这三个维度缺一不可。我见过太多项目在垂直指标上刷得漂亮,一上线发现通用对话能力崩了,原因就是回归清单里没有保留通用测试样本。
量化导出是合并后的下一步选项,常见路径是转成 GGUF 格式给 llama.cpp 类推理框架用,这块工具链比较成熟,转换后模型体积能再压一半。我自己的发布习惯是:先备份原始权重副本,再合并新副本,最后固定 case 清单跑完一遍回归才允许进部署。这套流程走了十几个项目,翻车率显著下降。希望帮到你。
本文还有配套的精品资源,点击获取