简介:这是一套面向自然语言处理与大模型微调实践的压缩包,聚焦法律领域垂直应用。基于 Qwen2.5-7B-Instruct 架构与 LLaMA-Factory 框架,使用 DISC-Law-SFT-Pair 专业法律数据集,适合 NLP 开发者、算法工程师以及希望将大模型落地到法律咨询、案例研究、合同审查等场景的研究人员参考。资源共 11 个文件,整体约 35KB,包含 3 个 yaml 微调/合并配置、3 个 jsonl 法律数据样例、Python 对话脚本、说明文件及附赠文档,目录结构清晰,便于按用途查阅。目前已有 97 人浏览学习。借助该压缩包可以快速了解 LoRA 与 QLoRA 微调参数设计、法律指令数据组织方式,以及模型合并与推理调用流程,尤其适合比对不同微调模式的效果差异;结合其中的说明文件和辅助脚本,能直接复现完整微调链路,为领域模型适配提供可操作的参考。
1. 为什么法律 NLP 场景绕不开大语言模型微调
在真实的合同审查和法规问答场景中,直接用 Qwen2.5-7B-Instruct 做推理,常常会遇到术语混淆、法条引用不准确的问题。通用模型虽然掌握了自然语言处理的基础规则,却没有经过法律语料的专门训练,它并不知道“要约”和“要约邀请”在实务里怎么区分,更不会把《劳动合同法》的条文按“适用条件—法律后果”的结构组织答案。将 DISC-Law-SFT-Pair 这份中文法律问答对作为训练数据,用 LLaMA-Factory 框架在 Qwen2.5-7B-Instruct 上执行一次 LoRA 微调,能让模型在单卡条件下获得法律领域语义,而不需要重训全部参数。下面会一路写到数据集清理、LoRA/QLoRA 配置、模型合并和对话验证,适合已经具备 NLP 基础、想把大语言模型微调落地到法律项目的工程师。
2. 选型拆解:Qwen2.5-7B-Instruct、LLaMA-Factory 与 DISC-Law-SFT-Pair
2.1 为什么基准模型选 Qwen2.5-7B-Instruct
微调的效果主要由三件事决定:基线模型、训练数据和参数更新方式。Qwen2.5-7B-Instruct 属于 decoder-only 的指令微调模型,7B 表示大约 70 亿参数,这个体量既保留了较强的中文理解和生成能力,又能在消费级显卡上做高效微调。文件名里的 Qwen2.5 是版本号,不是参数量,初次接触的人容易把“25”和“7B”搞混,理解为 250 亿参数,这其实是对模型命名规则的误读。
指令跟随能力上,Instruct 版已经经过 SFT 和偏好对齐,回答问题时天然会生成“开头说明—具体分析—结论建议”的文本结构。法律问答需要的恰恰是这种结构化输出。另外,与同规模 Llama 系列相比,Qwen2.5 的中文词表覆盖更完整,法律文本里的长句、古籍引文和顿号列表不容易被 tokenizer 切碎。做法律 NLP 项目时,中文分词不劣化意味着后续微调能把有限的训练预算花在语义关系上,而不是修修补补错误切分。
选型边界也要讲清楚:如果换成 72B 模型,训练成本呈几何级上升,而且 DISC-Law-SFT-Pair 是专业领域数据,规模通常在几万条以内,用大模型全参微调既容易过拟合,也没有足够的算力去做多次实验。7B 配 LoRA 是这个场景下性价比最高的组合,后面的所有命令都基于这一选型展开。
2.2 LoRA、QLoRA 与全参微调的取舍
大语言模型微调不只是把数据喂进去跑几步,它意味着更新一组庞大的权重矩阵。全参微调要让 7B 模型的所有参数参与更新,单卡 24G 基本操作不了,需要张量并行和梯度检查点配合;而 LoRA 把权重更新约束为低秩矩阵,冻结原始参数,只训练很小的一部分。QLoRA 则在 LoRA 基础上再把原始权重量化到 4bit,进一步压低显存占用。
| 微调方式 | 可训练参数占比 | 显存占用(7B, seq_len=2048) | 适用场景 |
|---|---|---|---|
| Full Fine-tuning | 100% | 多卡 48G 以上 | 数据量大、需要全量知识迁移 |
| LoRA | 0.1% ~ 1% | 单卡 24G 可跑 | 领域风格迁移、问答指令微调 |
| QLoRA | 0.1% ~ 1% | 单卡 16G 可跑 | 显存受限,追求快速试错 |
项目里的qwen2.5-7b-lora-sft.yaml和qwen2.5-7b-qlora-sft.yaml分别对应后两列。需要注意,LoRA 的可训练参数量虽然小,但不代表效果一定弱于全参微调;在专业领域数据集上,LoRA 反而因为约束了低秩空间,降低了过拟合风险。选择一个 rank 值就像选择一张“工作台”,rank 太小装不下领域知识,rank 太大失去低秩约束,通常从 64 起步观察效果。
2.3 DISC-Law-SFT-Pair 数据结构与 LLaMA-Factory 的格式对接
DISC-Law-SFT-Pair 是面向中文法律问答的成对数据,每一行由“问题”和“法律回答”组成。LLaMA-Factory 的 SFT 流程需要的是 instruction / input / output 三元组,其中 instruction 放问题主体,input 放可选的补充材料,output 放期望模型生成的标准答案。原始 Pair 数据一般只有 question 和 answer 两个字段,所以第一步是做字段映射。
import json src = "data/DISC-Law-SFT-Pair.jsonl" dst = "data/disc_law_sft_pair.jsonl" with open(src, "r", encoding="utf-8") as fin, \ open(dst, "w", encoding="utf-8") as fout: for line in fin: line = line.strip() if not line: continue item = json.loads(line) # LLaMA-Factory 标准 SFT 格式:instruction / input / output record = { "instruction": item.get("question") or item.get("q"), "input": "", "output": item.get("answer") or item.get("a"), } fout.write(json.dumps(record, ensure_ascii=False) + "\n")这里处理的是 SFT-Pair,如果使用的是 Triplet 文件,DISC-Law-SFT-Triplet-released.jsonl中通常还包含一个positive/negative或情境描述字段,可以拼到input字段里,让模型在回答前先读一段案情摘要。ensure_ascii=False保证中文以明文写入,避免训练时每个字符被转成\uXXXX,既不利于检查数据,也会让 token 统计失真。随后要在 LLaMA-Factory 的data/dataset_info.json里注册这个新数据集,否则训练配置里的dataset参数找不到文件。
{ "disc_law_sft_pair": { "file_name": "disc_law_sft_pair.jsonl", "columns": { "prompt": "instruction", "query": "input", "response": "output" } } }prompt和query的区分是 LLaMA-Factory 的约定;如果原始数据里有历史对话,还需要配history字段。注册完成后,训练脚本才能通过dataset: disc_law_sft_pair定位到数据。
3. 环境准备与数据预处理实操
3.1 安装 LLaMA-Factory 并准备模型权重
这个阶段先把训练环境搭起来。LLaMA-Factory 是一个以“配置驱动”为设计目标的训练框架,安装完成后不需要写训练循环,所有训练策略都写在 yaml 文件里。常见做法是克隆仓库并安装可编辑模式:
git clone https://github.com/hiyouga/LLaMA-Factory.git cd LLaMA-Factory pip install -e .[torch].[torch]会拉取 PyTorch 训练所需的依赖,如果显卡驱动有 CUDA 版本要求,建议先手动安装对应版本的 PyTorch,再执行这条命令,避免自动安装的 torch 与系统 CUDA 版本不匹配。装完之后还要下载 Qwen2.5-7B-Instruct 权重,可以先用 Hugging Face CLI 拉取:
huggingface-cli download Qwen/Qwen2.5-7B-Instruct \ --local-dir models/Qwen2.5-7B-Instruct也可以从 ModelScope 镜像下载,环境变量MODELSCOPE_CACHE可以指定缓存目录。下载完成后,确认config.json里的model_type是qwen2,这个字段决定后续 LLaMA-Factory 是否启用正确的template。
3.2 把法律 Pair 数据转成标准 SFT 格式
下载回来的压缩包里已经带了DISC-Law-SFT-Pair.jsonl和DISC-Law-SFT-Triplet-released.jsonl,但它们的字段命名不一定直接匹配 LLaMA-Factory。我们需要先观察数据形状,再做转换。以下脚本做了三件事:解析 JSONL、合并 question/answer 两个字段、校验空值。
import json from pathlib import Path input_path = Path("data/DISC-Law-SFT-Pair.jsonl") output_path = Path("data/disc_law_sft_pair.jsonl") def convert_pair(path_in: Path, path_out: Path): with path_in.open("r", encoding="utf-8") as fin, \ path_out.open("w", encoding="utf-8") as fout: for line in fin: line = line.strip() if not line: continue obj = json.loads(line) q = obj.get("question") or obj.get("q") or obj.get("instruction") a = obj.get("answer") or obj.get("a") or obj.get("output") if not q or not a: continue # 跳过没有答案的噪声样本 record = { "instruction": q.strip(), "input": "", "output": a.strip(), } fout.write(json.dumps(record, ensure_ascii=False) + "\n") if __name__ == "__main__": convert_pair(input_path, output_path)q和a是 Pair 文件里可能存在的短字段名,instruction/output是 LLaMA-Factory 的标准字段名。使用or而不是直接get("question"),是为了兼容不同版本的数据集格式。跳过空样本时可以直接打印缺失行号,方便倒回去核对源文件。转换完成后,用wc -l看行数:
wc -l data/disc_law_sft_pair.jsonl法律数据很长,单条答案经常超过 2000 字。这里有个容易被忽略的问题:控制cutoff_len。如果一条数据被截断,模型只学了前半段,输出也会被截断。常见做法是在清洗时过滤掉超长样本,或者把超长样本按段落切分,而不是放任模型看到一截残缺的法条。
3.3 划分训练集与验证集
训练前还要留出一部分数据做验证,否则 loss 曲线没有任何参考意义。LLaMA-Factory 支持在 yaml 里直接设置val_size,但在数据量较大的情况下,显式计算划分更可控。下面这个命令按 9:1 比例切分:
python -c " import json, random lines = open('data/disc_law_sft_pair.jsonl', encoding='utf-8').readlines() random.shuffle(lines) with open('data/train.jsonl', 'w', encoding='utf-8') as f: f.writelines(lines[:int(len(lines)*0.9)]) with open('data/val.jsonl', 'w', encoding='utf-8') as f: f.writelines(lines[int(len(lines)*0.9):]) "这里用手动 shuffle 而不是val_size,是为了保证后面每次试验用的验证集一致,方便对比不同 LoRA rank 的差异。划分后两者都是一行一条 JSON,没有覆盖性问题。接下来编辑data/dataset_info.json,加入两条记录:
{ "disc_law_sft_pair": { "file_name": "train.jsonl", "columns": { "prompt": "instruction", "query": "input", "response": "output" } }, "disc_law_sft_pair_val": { "file_name": "val.jsonl", "columns": { "prompt": "instruction", "query": "input", "response": "output" } } }dataset_info.json是 LLaMA-Factory 的数据入口,训练配置里的dataset和eval_dataset都指向这里注册的名字。如果注册了disc_law_sft_pair_val,训练时eval_dataset才会生效。注意,验证集不参与梯度更新,只用于计算 eval loss。
4. 从 LoRA 到 QLoRA:训练参数细节与排错
4.1 标准 LoRA 配置与关键参数
LLaMA-Factory 的 yaml 配置非常直接,它就是一份“训练计划”。打开项目的qwen2.5-7b-lora-sft.yaml,核心内容如下:
model_name_or_path: models/Qwen2.5-7B-Instruct template: qwen stage: sft finetuning_type: lora dataset: disc_law_sft_pair val_size: 0 learning_rate: 1.0e-4 num_train_epochs: 3.0 lr_scheduler_type: cosine per_device_train_batch_size: 2 gradient_accumulation_steps: 8 cutoff_len: 2048 lora_rank: 64 lora_alpha: 128 lora_dropout: 0.05 logging_steps: 10 save_steps: 500 warmup_ratio: 0.05model_name_or_path指向第 3 章下载的模型目录;template: qwen告诉框架使用 Qwen 的 chat 模板;stage: sft是监督微调阶段;finetuning_type: lora选择低秩适应方案。learning_rate初始值1.0e-4是 LoRA 微调常见配置,如果数据量小,可以降到5.0e-5。cutoff_len需要根据数据长度调整,过短会截断法条,过长会浪费显存。
per_device_train_batch_size为 2 的情况下,配合gradient_accumulation_steps为 8,等效 batch size 为 16。在 7B 模型上,这个 batch 规模对于法律指令数据是比较稳定的起点。lora_alpha与lora_rank的比例alpha / rank = 2,是 LoRA 论文里的经验默认值;如果改成alpha=64, rank=64,更新幅度会缩小一半,模型收敛更慢但会更稳。运行训练:
llamafactory-cli train qwen2.5-7b-lora-sft.yaml该命令会读取同目录的 yaml 文件,开始加载模型并训练。日志里的loss应该在前 100 步内从高位快速下降,然后进入缓慢降低阶段。如果 loss 一直震荡,优先检查cutoff_len和数据清洗是否有空值。
4.2 QLoRA 的 4bit 量化配置与显存优化
法律文本通常很长,cutoff_len如果设置到 4096,原来的 LoRA 配置在 24G 卡上很可能 OOM。QLoRA 通过把 Qwen2.5-7B 的原始权重量化到 4bit,把大部分权重放进只读缓存,训练过程中只有 LoRA 适配器会在反向传播里更新。在qwen2.5-7b-qlora-sft.yaml中,需要显式打开量化开关:
finetuning_type: qlora quantization_bit: 4 quantization_dataset: data/disc_law_sft_pair.jsonlqlora会启用 transformers 的 BitsAndBytes 4bit 配置,此时lora_rank、lora_alpha仍需要保留,因为训练更新的仍然是低秩矩阵。quantization_dataset用于计算激活值统计信息,一般取一小段训练数据即可。很多人在这一步漏掉template参数,结果生成时没有系统提示,法律答案变得口语化。
| 配置 | 原始 LoRA | QLoRA |
|---|---|---|
| 显存占用(7B, bs=2, seq=2048) | 约 22G | 约 14G |
| 训练速度 | 较快 | 略慢(反量化开销) |
| 适合显卡 | RTX 4090 24G | RTX 3090 24G / 16G |
| 效果差异 | 接近全参 | 与 LoRA 差距极小 |
QLoRA 的 4bit 权重在前向传播时需要反量化到 bf16,因此单步训练时间比 LoRA 长约 10% 到 15%,但对显存的降低很可观。如果在 16G 的卡上训练,把per_device_train_batch_size降到 1,再配合gradient_accumulation_steps=16,同样能得到稳定效果。
4.3 训练日志、断点续训与常见排错
训练中最大的问题是“没有观察指标,只看最终 loss”。LLaMA-Factory 在saves/目录下会按实验名生成文件夹,里面保存trainer_log.jsonl和 checkpoint。我们可以直接用命令查看实时 loss:
tail -f saves/qwen2.5-7b-lora-sft/trainer_log.jsonltrainer_log.jsonl里的loss是每一步的均值,如果出现nan,多半是显存溢出或者学习率过高,调低learning_rate到3e-5,同时检查数据文件里有没有混入NaN字符。另一个常见错误是model_name_or_path没有找到本地目录时,框架会尝试从 Hugging Face 下载,网络波动容易导致中断;dataset名字写错时会在加载阶段报Dataset not found;template与模型不匹配时生成效果会异常,Qwen2.5 必须用qwen模板。
断点续训是微调项目的刚需。LLaMA-Factory 的train命令支持--resume_from_checkpoint参数:
llamafactory-cli train qwen2.5-7b-lora-sft.yaml \ --resume_from_checkpoint saves/qwen2.5-7b-lora-sft/checkpoint-500续训时优化器状态会继承,不需要重新加载全量数据集,学习率也会从 checkpoint 保存时的步数继续衰减。注意不要随意改lora_rank,续训时 rank 不一致会导致适配器无法加载。若改 rank,需要重新训练。
5. 合并 LoRA 权重并验证法律问答效果
5.1 用 merge 配置导出完整模型
LoRA 训练产出的只是一个小适配器,正式推理时要么用 PEFT 加载适配器,要么把适配器合并进原始模型。项目里的qwen2.5-7b-merge-lora.yaml就是干这个事的:
model_name_or_path: models/Qwen2.5-7B-Instruct adapter_name_or_path: saves/qwen2.5-7b-lora-sft/checkpoint-1500 finetuning_type: lora export_dir: models/Qwen2.5-7B-Instruct-Law运行导出命令:
llamafactory-cli export qwen2.5-7b-merge-lora.yaml导出后models/Qwen2.5-7B-Instruct-Law就是完整权重,不再依赖 LoRA 适配器文件。这个目录可以直接被transformers、vLLM 或 Ollama 加载,部署时少了peft依赖。合并时注意选择效果最好的一轮 checkpoint,不要直接合并最后一个,最好根据验证集 loss 确定。
5.2 用 chat.py 做法律问答测试
项目包里的chat.py是一个轻量推理入口,可以用来快速验证微调效果。它的常见调用方式是指向合并后的模型目录:
python chat.py --model_path models/Qwen2.5-7B-Instruct-Law --template qwen进入交互后输入:“劳动合同到期后公司不续签,需要支付经济补偿金吗?”观察模型输出。未微调的底座模型通常会给出“不一定”这类模糊回答;经过 DISC-Law-SFT-Pair 微调后的模型应当主动区分“无固定期限劳动合同”“续签条件”“经济补偿金计算基数”等概念。如果回答太啰嗦,可以把max_new_tokens设到 512,并在提示词里加上“请用列表分点回答”。
5.3 一个验证技巧:用温度参数控制法律答案的稳定性
法律问答和闲聊不同,生成结果的方差要尽量小。测试时可以在chat.py的生成参数里设置temperature: 0.2, top_p: 0.85,让解码过程更接近确定性。对于“某法条是否适用”这类问题,temperature 越低,模型越倾向选择概率最高的表达路径。
| 场景 | temperature | 说明 |
|---|---|---|
| 合同条款解释 | 0.1 ~ 0.2 | 强确定性,避免法律结论漂移 |
| 法律知识问答 | 0.2 ~ 0.4 | 保留结构化表达,降低废话 |
| 模拟案例讨论 | 0.7 以上 | 生成多个候选回答,不做最终法律判断 |
建议在chat.py加载模型后打印model.config.temperature确认生效,或者直接把生成参数暴露成命令行参数,每次测试时固定 seed 和 temperature,这样不同工程师之间的评测结果才可以横向对比。
本文还有配套的精品资源,点击获取