中文法律大模型微调实战:从词表扩展到LoRA训练全流程 简介这份资源面向希望将大语言模型落地到中文法律场景的开发者与算法学习者围绕法律问答、法条理解与指令微调等任务提供了一套可复现的工程实践材料。包内共42个文件以Python脚本、JSON配置与数据、Shell运行脚本为主辅以少量图片、说明文档与许可证文件压缩包约3.41MB整体结构按工具、数据、模型、脚本与模板等模块划分便于按需查阅与二次开发。资源涵盖法律词表构建、指令数据示例、模型微调与推理、权重合并及Web界面调用等环节并配有法律提示模板与评估脚本能帮助读者理解从数据处理到部署上线的完整链路。目前已有120人学习下载适合具备一定深度学习基础、想切入法律垂直领域大模型应用的中高级开发者参考。1. 法律大模型落地从一份中文法律知识微调包说起法律咨询场景里通用大模型最让人头疼的不是知识广度而是它敢编。你问它“帮信罪和掩饰隐瞒犯罪所得罪怎么区分”它能给你说得头头是道但引用的法条编号、司法解释年份经常是错的。这种“一本正经胡说八道”在通用闲聊里无伤大雅放到法律场景就是事故。这份《AI大模型应用》-基于中文法律知识的大语言模型.zip解决的就是这个问题它把中文法律语料、指令微调数据、词表扩展脚本和一套完整的训练/推理/合并/WebUI 流程打包在一起让你能在本地把一个大语言模型往法律领域拽一把。适合谁做垂直领域 AI 大模型应用开发的工程师、想跑通 LoRA 微调全流程的算法同学以及需要给法律问答产品做原型验证的团队。它不是一个开箱即用的法律问答 API而是一套可复现的微调工程脚手架。2. 拆包看结构法律语料、指令数据与词表扩展怎么配合2.1 数据层三份 JSON 和一份法律词表解压后先看resources/目录这是整个项目的燃料仓。example_instruction_train.json和example_instruction_tune.json是两份指令微调数据格式是常见的 instruction-input-output 三元组前者用于训练集后者用于验证或调参对比。example_infer_data.json是推理测试样例用来在训练前后做效果对照。criminal_charges.json是罪名数据结构上应该是罪名名称与法条描述的映射适合做分类或检索增强的辅助数据。legal_vocab.txt是法律领域词表这是中文法律知识注入的关键——通用分词器会把“帮助信息网络犯罪活动罪”切成碎片而这份词表让模型在 token 层面就认识法律术语。templates/下有两个模板文件alpaca.json和law_template.json。前者是 Alpaca 风格的通用指令模板后者是法律场景专用模板。模板的作用是决定原始数据怎么拼成模型输入比如是否加“### 指令”“### 回答”这类分隔符。法律场景下模板设计直接影响模型对“问-答”边界的理解law_template.json大概率针对法条引用和案情描述做了格式适配。2.2 工具层词表合并与数据清洗tools/目录下有两个脚本clear_law.py和merge_vocabulary.py。前者做数据清洗法律语料里常见的噪声包括 HTML 标签、多余空白、重复段落、乱码字符清洗质量直接决定微调后的输出是否干净。后者做词表合并把legal_vocab.txt里的法律术语合并进基础模型的分词器词表。合并词表不是简单拼接需要处理 token 冲突和 ID 映射。常见做法是读取基础模型的 tokenizer检查法律词表中每个词是否已被现有词表覆盖对未覆盖的词分配新 ID然后同步更新模型 embedding 层的维度。如果只扩词表不扩 embedding推理时会直接报维度不匹配。merge_vocabulary.py应该封装了这个流程但具体实现需要看代码确认是否同时处理了 embedding 矩阵的 resize。# 词表合并的核心逻辑示意基于常见做法 from transformers import AutoTokenizer, AutoModelForCausalLM base_model_path models/base_models legal_vocab_path resources/legal_vocab.txt tokenizer AutoTokenizer.from_pretrained(base_model_path) model AutoModelForCausalLM.from_pretrained(base_model_path) # 读取法律词表 with open(legal_vocab_path, r, encodingutf-8) as f: legal_words [line.strip() for line in f if line.strip()] # 找出未在现有词表中的词 new_tokens [w for w in legal_words if w not in tokenizer.get_vocab()] print(f待添加新词数量: {len(new_tokens)}) # 添加新词并扩展 embedding tokenizer.add_tokens(new_tokens) model.resize_token_embeddings(len(tokenizer)) # 保存合并后的模型和分词器 tokenizer.save_pretrained(models/merged_tokenizer) model.save_pretrained(models/merged_model)这段代码的关键参数是resize_token_embeddings的入参必须是扩展后的词表大小。如果只保存 tokenizer 不保存 model后续加载时 embedding 维度对不上推理会直接崩。另一个坑是新增 token 的 embedding 初始化默认是随机初始化如果新增词数量大而微调数据少这些新词可能训练不充分反而拉低效果。稳妥做法是新增词数量控制在几百以内并且确保微调数据里这些法律术语有足够出现频次。2.3 训练层LoRA 微调与全量微调的取舍finetune.py和train_clm.py是两个训练入口。finetune.py大概率走 LoRA 路线因为models/lora_weights/目录是空的占位说明设计上支持把 LoRA 权重单独存放。train_clm.py可能是全量微调或继续预训练的入口配合scripts/train_clm.sh使用。LoRA 的优势是显存占用低、训练快、权重文件小适合法律这种垂直领域的数据量级。全量微调效果上限更高但需要多卡环境和更大的显存预算。对于个人开发者或小团队LoRA 是更现实的选择。finetune.py里需要关注的参数包括lora_rank、lora_alpha、target_modules。法律领域微调rank 一般设 8 或 16 就够alpha 通常是 rank 的两倍。target_modules要覆盖 attention 的 q_proj、v_proj如果显存允许把 k_proj、o_proj 也加上效果更稳。# 基于 scripts/finetune.sh 的典型调用方式 python finetune.py \ --base_model models/base_models \ --data_path resources/example_instruction_train.json \ --output_dir outputs/lora_law \ --lora_rank 16 \ --lora_alpha 32 \ --target_modules q_proj,v_proj,k_proj,o_proj \ --num_epochs 3 \ --batch_size 4 \ --learning_rate 2e-4 \ --cutoff_len 512cutoff_len是法律场景的敏感参数。法律文本往往较长案情描述加法条引用很容易超过 512 token。如果截断太短模型学不到完整的法律推理链条设太长则显存吃紧。建议先统计训练数据里 instructionoutput 的 token 长度分布取 95 分位数作为 cutoff_len常见值在 768 到 1024 之间。learning_rate用 2e-4 是 LoRA 的常规起点如果 loss 震荡明显降到 1e-4 再试。3. 从零跑通环境、数据准备与训练启动3.1 环境依赖与基础模型放置requirements.txt里应该锁定了 transformers、peft、datasets、accelerate 等核心库。建议用 conda 建独立环境Python 版本选 3.10这是目前大语言模型工具链兼容性最好的版本。CUDA 版本要和 PyTorch 匹配如果用的是 40 系显卡CUDA 11.8 以上。models/base_models/目录是空的只有一个.gitkeep。这意味着基础模型需要自己下载后放进去。选择哪个基础模型项目没有指定但从中文法律场景出发常见选择是 Baichuan、Qwen 或 ChatGLM 系列的中文底座。选型时看三点中文能力、是否支持商用许可、社区微调案例是否丰富。模型放进去后目录结构应该是models/base_models/下直接是config.json、pytorch_model.bin或分片文件、tokenizer.json等不要多套一层文件夹。# 环境准备 conda create -n law_llm python3.10 -y conda activate law_llm pip install -r requirements.txt # 确认 GPU 可用 python -c import torch; print(torch.cuda.is_available(), torch.cuda.device_count())如果torch.cuda.is_available()返回 False先查驱动版本和 CUDA 版本是否匹配不要急着改代码。另一个常见问题是peft版本和transformers版本不兼容表现为 import 报错或 LoRA 配置参数不识别。requirements.txt如果没锁死小版本建议手动固定peft0.6.0以上、transformers4.36.0以上。3.2 数据格式校验与模板对齐在启动训练前必须确认example_instruction_train.json的字段名和finetune.py里读取的 key 一致。常见字段是instruction、input、output但有些项目用prompt、response。如果字段对不上训练脚本会静默跳过所有数据loss 一直是初始值这种翻车很隐蔽。# 数据格式快速校验 import json with open(resources/example_instruction_train.json, r, encodingutf-8) as f: data json.load(f) print(f样本总数: {len(data)}) print(f第一条数据的 keys: {list(data[0].keys())}) print(f第一条 instruction 前 100 字: {data[0].get(instruction, )[:100]}) print(f第一条 output 前 100 字: {data[0].get(output, )[:100]}) # 统计 output 长度分布辅助设定 cutoff_len lengths [len(item.get(output, )) for item in data] lengths.sort() print(foutput 长度中位数: {lengths[len(lengths)//2]}) print(foutput 长度 95 分位: {lengths[int(len(lengths)*0.95)]})模板对齐同样关键。templates/law_template.json定义了数据拼接格式如果训练时用一套模板、推理时用另一套模型输出会带出训练模板里的分隔符残留比如回答末尾多出“###”或“指令”。我一般会在训练前手动构造一条样本走一遍 tokenizer 的 encode把 token 序列打印出来看模板拼接是否符合预期。3.3 启动训练与显存监控scripts/finetune.sh是封装好的启动脚本但直接跑之前建议先看一遍里面的参数。常见需要改的是CUDA_VISIBLE_DEVICES、batch_size、gradient_accumulation_steps。单卡 24G 显存跑 7B 模型的 LoRAbatch_size 设 4、gradient_accumulation_steps 设 4等效 batch 是 16比较稳。如果 OOM优先降 batch_size其次降 cutoff_len最后才考虑换更小的底座模型。# 启动训练并记录显存 nvidia-smi --query-gpumemory.used,memory.total --formatcsv -l 5 gpu_log.csv bash scripts/finetune.sh训练过程中看 loss 曲线法律领域微调的 loss 通常从 2.0 左右开始下降3 个 epoch 后能到 0.8 到 1.2 区间。如果 loss 降到 0.3 以下大概率过拟合了模型会开始复读训练数据里的原句泛化能力反而下降。这时候要么减 epoch要么加 dropout要么补充更多样的法律问答数据。outputs/目录下会保存 checkpointLoRA 权重文件通常只有几十 MB方便后续合并。4. 推理、合并与 WebUI把微调结果变成能用的服务4.1 LoRA 权重合并与模型导出训练完的 LoRA 权重不能直接当独立模型用需要和基础模型合并。merge.py和scripts/merge.sh负责这件事。合并的逻辑是把 LoRA 的 A、B 矩阵乘回原权重W_new W_base (alpha / rank) * B A。合并后的模型是一个完整的 HuggingFace 模型可以直接用AutoModelForCausalLM.from_pretrained加载。# 合并 LoRA 权重 python merge.py \ --base_model models/base_models \ --lora_model outputs/lora_law \ --output_dir outputs/merged_law_model合并时注意alpha和rank要和训练时一致否则权重缩放比例错了模型输出会变味。合并后的模型体积和基础模型一样大7B 模型约 13GB 到 14GBfp16。如果只想保留 LoRA 权重做动态加载推理时用PeftModel.from_pretrained也可以省硬盘空间但推理速度略慢。4.2 推理脚本与生成参数调优infer.py是推理入口scripts/infer.sh是封装脚本。法律问答场景下生成参数比模型本身还影响体验。temperature设 0.1 到 0.3法律回答要稳不能太发散。top_p设 0.8 到 0.9repetition_penalty设 1.1 到 1.2防止模型复读法条。max_new_tokens根据场景设简单咨询 256 够用案情分析建议 512 以上。# 推理参数配置示例 from transformers import AutoModelForCausalLM, AutoTokenizer model_path outputs/merged_law_model tokenizer AutoTokenizer.from_pretrained(model_path) model AutoModelForCausalLM.from_pretrained(model_path, device_mapauto) prompt 帮信罪的构成要件是什么 inputs tokenizer(prompt, return_tensorspt).to(model.device) outputs model.generate( **inputs, max_new_tokens512, temperature0.2, top_p0.85, repetition_penalty1.15, do_sampleTrue, pad_token_idtokenizer.eos_token_id ) print(tokenizer.decode(outputs[0], skip_special_tokensTrue))pad_token_id必须显式设置很多中文底座模型的 pad token 和 eos token 不一致不设置会报警告甚至生成异常。如果输出里出现大量重复的“根据《刑法》第…条”把repetition_penalty往上调到 1.3 试试但别超过 1.5否则语句会变得不连贯。4.3 WebUI 部署与接口封装webui.py和scripts/webui.sh提供了一个 Gradio 界面适合做演示和内部测试。启动后默认监听本地端口浏览器打开就能对话。如果要做成 API 服务可以在webui.py基础上改或者用 FastAPI 包一层。法律问答产品通常需要流式输出Gradio 的ChatInterface支持streamTrue但需要模型 generate 时加TextIteratorStreamer。# 流式输出核心逻辑示意 from transformers import TextIteratorStreamer from threading import Thread streamer TextIteratorStreamer(tokenizer, skip_promptTrue, skip_special_tokensTrue) generation_kwargs dict( **inputs, max_new_tokens512, temperature0.2, streamerstreamer, do_sampleTrue ) thread Thread(targetmodel.generate, kwargsgeneration_kwargs) thread.start() for new_text in streamer: print(new_text, end, flushTrue)流式输出在 WebUI 里体验提升明显但要注意TextIteratorStreamer和do_sampleFalse不兼容必须开采样。另外多用户并发时模型推理要加锁或走队列否则显存会爆。webui.py如果没做并发控制生产环境需要自己补。5. 避坑排查法律大模型微调里那些血泪经验5.1 词表合并后推理报维度错误现象合并词表后加载模型推理报RuntimeError: size mismatch for embedding weight。原因只调用了tokenizer.add_tokens但没调model.resize_token_embeddings或者保存模型时没保存 resize 后的 embedding。解决合并词表后必须同时保存 tokenizer 和 model且加载时用同一个目录。如果已经训练了 LoRA合并权重前先确认基础模型 embedding 维度是否和 tokenizer 词表大小一致。5.2 训练 loss 不下降或直接 NaN现象启动训练后 loss 一直停在初始值或者几个 step 后变成 NaN。原因数据字段名和脚本读取的 key 不匹配导致所有样本被跳过或者学习率设太大fp16 训练溢出。解决先跑一条数据的前向传播确认 loss 能正常计算。学习率从 2e-4 降到 1e-4加warmup_ratio0.03开fp16时如果 NaN 持续换bf16试试40 系显卡对 bf16 支持更好。5.3 模型输出复读训练数据原句现象推理时模型直接吐出训练集里的某条 output一字不差。原因过拟合训练 epoch 太多或数据多样性不足。解决减 epoch 到 2 甚至 1加lora_dropout0.1补充更多不同表述的法律问答数据。法律场景下同一个法条最好有 3 到 5 种不同问法否则模型会记住固定搭配。5.4 WebUI 并发请求导致显存溢出现象单用户测试正常两个用户同时提问就 OOM。原因Gradio 默认多线程处理请求多个 generate 同时跑显存叠加。解决在webui.py里加全局锁或者用queue()限制并发数为 1。如果要做多用户上 vLLM 或 TGI 这类推理框架它们内置了批处理和显存管理。5.5 合并后的模型回答风格突变现象LoRA 权重单独加载时回答正常合并后回答变得啰嗦或格式混乱。原因合并时alpha和rank参数和训练时不一致或者合并脚本里权重缩放系数写错。解决核对训练脚本和合并脚本里的lora_alpha、lora_rank是否一致。合并后先用example_infer_data.json里的样例跑一遍和合并前的输出做对比差异大就回查参数。6. 进阶技巧用评估脚本量化法律回答质量utils/evaluate.py这个文件容易被忽略但它其实是把微调从“感觉还行”推到“可量化对比”的关键。法律问答的评估不能只看 loss要看生成内容里法条引用准确率、罪名匹配度、回答完整性。我一般会构造一个小的评测集从example_infer_data.json里抽 50 条人工标注标准答案里的关键法条编号和罪名然后用脚本做关键词命中率统计。# 法律回答关键信息命中率评估示意 import json import re def extract_legal_refs(text): 提取回答中的法条引用如《刑法》第287条之二 pattern r《[^》]》第[\d一二三四五六七八九十百]条(?:之[一二三四五六七八九十])? return set(re.findall(pattern, text)) def evaluate(preds, refs): hit, total 0, 0 for pred, ref in zip(preds, refs): pred_refs extract_legal_refs(pred) ref_refs extract_legal_refs(ref) if ref_refs: total 1 if pred_refs ref_refs: # 有交集即算命中 hit 1 return hit / total if total else 0.0 # 加载推理结果和标准答案 with open(outputs/infer_results.json, r, encodingutf-8) as f: results json.load(f) preds [r[prediction] for r in results] refs [r[reference] for r in results] print(f法条引用命中率: {evaluate(preds, refs):.2%})这个评估脚本的阈值怎么定法条引用命中率低于 60% 说明模型还没学会准确引用需要补充更多带法条编号的训练数据。罪名匹配可以用criminal_charges.json里的罪名列表做实体识别看模型回答里是否出现了正确的罪名。回答完整性可以统计平均生成长度和人工抽检。评估不是为了刷分是为了在换底座模型、调 LoRA rank、改数据配比时有一个稳定的对比基准。从那以后我每次微调法律模型都会先跑一遍评估脚本再决定要不要合并权重不然光看 loss 曲线很容易被“虚假下降”骗过去。希望帮到你。本文还有配套的精品资源点击获取