
简介这是一套面向自然语言处理与大模型微调实践的压缩包聚焦法律领域垂直应用。基于 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-Pair2.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_len2048适用场景Full Fine-tuning100%多卡 48G 以上数据量大、需要全量知识迁移LoRA0.1% ~ 1%单卡 24G 可跑领域风格迁移、问答指令微调QLoRA0.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, encodingutf-8) as fin, \ open(dst, w, encodingutf-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_asciiFalse) \n)这里处理的是 SFT-Pair如果使用的是 Triplet 文件DISC-Law-SFT-Triplet-released.jsonl中通常还包含一个positive/negative或情境描述字段可以拼到input字段里让模型在回答前先读一段案情摘要。ensure_asciiFalse保证中文以明文写入避免训练时每个字符被转成\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, encodingutf-8) as fin, \ path_out.open(w, encodingutf-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_asciiFalse) \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, encodingutf-8).readlines() random.shuffle(lines) with open(data/train.jsonl, w, encodingutf-8) as f: f.writelines(lines[:int(len(lines)*0.9)]) with open(data/val.jsonl, w, encodingutf-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 论文里的经验默认值如果改成alpha64, rank64更新幅度会缩小一半模型收敛更慢但会更稳。运行训练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参数结果生成时没有系统提示法律答案变得口语化。配置原始 LoRAQLoRA显存占用7B, bs2, seq2048约 22G约 14G训练速度较快略慢反量化开销适合显卡RTX 4090 24GRTX 3090 24G / 16G效果差异接近全参与 LoRA 差距极小QLoRA 的 4bit 权重在前向传播时需要反量化到 bf16因此单步训练时间比 LoRA 长约 10% 到 15%但对显存的降低很可观。如果在 16G 的卡上训练把per_device_train_batch_size降到 1再配合gradient_accumulation_steps16同样能得到稳定效果。4.3 训练日志、断点续训与常见排错训练中最大的问题是“没有观察指标只看最终 loss”。LLaMA-Factory 在saves/目录下会按实验名生成文件夹里面保存trainer_log.jsonl和 checkpoint。我们可以直接用命令查看实时 losstail -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 foundtemplate与模型不匹配时生成效果会异常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这样不同工程师之间的评测结果才可以横向对比。本文还有配套的精品资源点击获取