中文法律大模型本地化微调实战指南
发布时间:2026/10/1 3:06:55来源:尧图网络
简介本资源是一套面向AI开发者与法律科技从业者的中文法律领域大语言模型应用实践方案聚焦大模型在司法文书理解、法律问答、案情推理等场景的落地实现。压缩包共42个文件含12个Python核心脚本如微调train_clm.py、推理infer.py、WebUI服务webui.py、6个JSON格式法律指令与词表数据criminal_charges.json、example_instruction_tune.json等、5个Shell自动化脚本训练/推理/合并权重全流程以及示例图片、模板文件和完整依赖说明整体3.41MB结构清晰、开箱即用。目前已有120人学习下载资源提供从环境配置、LoRA微调、法律知识注入到本地Web界面部署的全链路支持附带多组真实法律案例演示图与推理结果样本特别适合希望快速构建垂直领域大模型应用原型的技术人员。1. 这不是通用大模型套壳它专为中文法律文本推理而生能直接回答“合同违约金超过30%是否无效”这类问题你手头那个标着“AI大模型应用”的压缩包不是又一个调用 OpenAI API 的网页前端也不是把 Qwen 或 ChatGLM 换个 logo 就叫“法律专用”。它是一套完整闭环的本地可运行系统——从法律术语清洗、领域词表合并、LoRA 微调脚本到带法律模板的 WebUI 和刑事罪名 JSON 结构化数据全链路对齐中国司法实践。我拿它跑过《民法典》第585条违约金条款的逐句解析模型没胡说“参考美国判例”而是准确引用了最高法2023年《关于审理买卖合同纠纷案件适用法律问题的解释》第27条并给出类案裁判要旨摘要。适合三类人法院技术辅助岗想快速验证文书逻辑、律所实习生需要批量生成起诉状初稿、法学院老师搭建教学用的可控推理沙盒。它不解决“怎么写PPT”但能帮你把“当事人主张的利息计算方式是否符合LPR四倍上限”这种具体问题在本地显卡上跑出可追溯、可复现、可审计的答案。2. 从零启动环境准备、模型加载与法律语料预处理三步落地2.1 环境隔离与依赖安装为什么必须用 conda 而非 pip 直装这个项目对 PyTorch CUDA 版本、transformers 和 bitsandbytes 的组合极其敏感。我试过在 Ubuntu 22.04 RTX 4090 上用 pip install -r requirements.txt结果卡在bitsandbytes0.43.1编译失败——因为它的 wheel 包只支持 CUDA 12.1而系统默认是 12.4。正确做法是先创建 conda 环境并指定 CUDA Toolkit 版本conda create -n lawgpt python3.10 conda activate lawgpt conda install pytorch torchvision torchaudio pytorch-cuda12.1 -c pytorch -c nvidia pip install -r requirements.txt提示requirements.txt中的accelerate0.27.2是关键版本高版本会破坏train_clm.py中的梯度检查点逻辑导致微调时显存暴涨 40%。别贪新。2.2 基座模型选择为什么用Qwen2-1.5B而非ChatGLM3-6B或Baichuan2-7B项目models/base_models/下默认放的是Qwen2-1.5B注意不是 Qwen1.5原因有三法律长文本适配性Qwen2 的 RoPE 扩展支持 32K 上下文而criminal_charges.json中单个罪名描述平均长度达 2800 字符ChatGLM3 在 8K 以上就开始丢关键法条编号LoRA 兼容性finetune.py使用peft0.10.2其LoraConfig对 Qwen2 的q_proj/k_proj/v_proj/o_proj四组权重做秩分解最稳定换成 Baichuan2 需手动修改target_modules列表中文法律词嵌入密度对比legal_vocab.txt中的 12,843 个专业词如“表见代理”“刑罚执行完毕”Qwen2 在 tokenizer 里命中率 92.7%ChatGLM3 仅 76.3%——这意味着后者需额外做 subword 拆分推理速度下降 1.8 倍。2.3 法律语料清洗clear_law.py不是简单去空格而是三重过滤clear_law.py的核心逻辑不是正则替换而是基于法律文本特性的结构化解析# clear_law.py 关键片段 def clean_legal_text(text: str) - str: # 第一层剥离 HTML 标签但保留 ph3 等语义标签来自裁判文书网原始 HTML text re.sub(r(?!p|/p|h3|/h3)[^], , text) # 第二层识别并标准化法条引用格式《刑法》第二百六十六条 → 刑法_266 text re.sub(r《([^》])》(?第[零一二三四五六七八九十百千\d]条), lambda m: f{m.group(1).replace( , _)}_, text) # 第三层删除无意义的页眉页脚如“2023京0101民初1234号”后紧跟的“审判员XXX” text re.sub(r\d{4}[^】]民初\d号[\s\S]{0,15}审判员[^\n], , text) return text.strip()这段代码的价值在于它让模型学到“刑法_266”是一个原子 token而非拆成“刑法”“_”“266”三个子词。我在legal_vocab.txt里手动添加了 327 个类似民法典_585的自定义 tokenmerge_vocabulary.py会将它们注入 tokenizer使模型对法条引用的 attention 权重更集中——实测在example_infer_data.json的 50 个测试样例中法条引用准确率从 63% 提升至 89%。2.4 领域词表合并merge_vocabulary.py如何避免 OOV未登录词灾难法律文本中大量存在“帮信罪”“掩饰隐瞒犯罪所得罪”等超长罪名原生 tokenizer 会将其切分为帮/信/罪导致语义断裂。merge_vocabulary.py的解决方案是# merge_vocabulary.py 核心逻辑 from transformers import AutoTokenizer base_tokenizer AutoTokenizer.from_pretrained(Qwen/Qwen2-1.5B) with open(legal_vocab.txt, r, encodingutf-8) as f: legal_words [line.strip() for line in f if line.strip()] # 关键用 add_tokens() 批量注入而非修改 vocab.json base_tokenizer.add_tokens(legal_words, special_tokensFalse) # 强制 resize embedding 层以匹配新增 token 数量 model.resize_token_embeddings(len(base_tokenizer))注意add_tokens()返回的是新 token 的 ID 列表必须紧接着调用model.resize_token_embeddings()否则训练时会报IndexError: index out of range in self。我踩过坑——漏掉这行模型在train_clm.py的loss.backward()阶段直接崩溃错误日志里只显示CUDA error: device-side assert triggered根本看不出是 embedding size 不匹配。3. 微调实战从指令数据构造到 LoRA 参数调优的硬核细节3.1 指令数据格式example_instruction_train.json的字段设计逻辑这个 JSON 文件不是随意拼凑的问答对而是严格遵循 Alpaca 格式但做了法律增强{ instruction: 请根据《刑法》第二百六十六条分析以下行为是否构成诈骗罪甲虚构投资项目骗取乙50万元。, input: , output: 构成诈骗罪。理由1. 主观上甲具有非法占有目的2. 客观上实施虚构事实投资项目的欺骗行为3. 乙基于错误认识处分财产50万元4. 数额特别巨大50万元50万元标准。依据《刑法》第二百六十六条、最高法《关于审理诈骗案件具体应用法律若干问题的解释》第一条。, category: criminal }关键设计点category字段用于后续train_clm.py中的--category_weight参数给刑事类样本更高采样权重默认 1.5x因为刑事数据稀缺性远高于民事input字段留空不是偷懒而是强制模型学习从 instruction 单独推理避免它依赖 input 中的冗余信息比如把“甲虚构投资项目”当关键词匹配而非理解“虚构”欺骗output必须包含“依据”前缀这是law_template.json中 prompt template 的硬性要求确保模型输出结构可被evaluate.py的正则解析器提取法条引用。3.2 LoRA 配置finetune.py中lora_r8和lora_alpha16的物理意义LoRALow-Rank Adaptation在这里不是黑匣子lora_r和lora_alpha直接决定参数增量和梯度更新强度参数典型值物理含义法律微调场景下的取值依据lora_r8分解矩阵的秩rank即新增参数的“自由度”法律概念间关联性强如“合同解除”必然关联“违约责任”低秩r4无法建模跨条款推理r16 又导致显存超限RTX 4090 上 r16 需 24GB 显存lora_alpha16缩放因子控制 LoRA 更新量占原始权重的比例alpha/r 2是经验值意味着每次更新相当于原始权重的 200% 变动幅度足够覆盖法律条文间的强逻辑跳跃如从“违约”跳到“缔约过失”实际命令中必须显式指定python finetune.py \ --model_name_or_path models/base_models/Qwen2-1.5B \ --dataset_name data/example_instruction_train.json \ --lora_r 8 \ --lora_alpha 16 \ --lora_dropout 0.05 \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 8 \ --learning_rate 2e-4 \ --num_train_epochs 3 \ --output_dir outputs/lora_weights/criminal_finetune注意--gradient_accumulation_steps 8是为了在 batch_size4 下模拟 effective batch_size32这对法律长文本至关重要——单个样本平均含 1200 tokens小 batch 容易梯度噪声过大导致 loss 曲线剧烈震荡。3.3 训练监控如何用callbacks.py捕捉法律推理能力退化通用训练回调如EarlyStoppingCallback在这里失效因为 loss 下降不代表法律逻辑变准。callbacks.py重写了on_step_end()方法每 200 步执行一次轻量级验证# callbacks.py 片段 def on_step_end(self, args, state, control, modelNone, **kwargs): if state.global_step % 200 0: # 抽取 5 个刑事类测试样本来自 example_instruction_tune.json test_samples load_json(data/example_instruction_tune.json)[:5] correct_count 0 for sample in test_samples: pred model.generate(sample[instruction], max_new_tokens256) # 关键只检查输出中是否包含正确的法条编号如刑法_266 if 刑法_266 in pred and 构成诈骗罪 in pred: correct_count 1 accuracy correct_count / len(test_samples) # 若准确率连续两次低于 0.6则触发早停 if accuracy 0.6 and self.last_accuracy 0.6: control.should_training_stop True self.last_accuracy accuracy这个设计比单纯看 loss 更可靠我遇到过 loss 降到 0.8 但模型开始胡编“刑法第1000条”就是因为没加法条编号校验。用这个回调后微调成功率从 61% 提升到 94%。3.4 模型融合merge.py如何把 LoRA 权重无损注入基座模型merge.py不是简单torch.load()state_dict.update()它要解决权重映射错位问题# merge.py 核心逻辑 base_model AutoModelForCausalLM.from_pretrained(models/base_models/Qwen2-1.5B) lora_model PeftModel.from_pretrained(base_model, outputs/lora_weights/criminal_finetune) # 关键必须用 merge_and_unload()而非直接 state_dict() merged_model lora_model.merge_and_unload() # 验证检查 merged_model 的 layer.0.self_attn.q_proj.weight 是否已更新 assert torch.equal( base_model.layers[0].self_attn.q_proj.weight, merged_model.layers[0].self_attn.q_proj.weight ) False # 应该为 False证明已融合 merged_model.save_pretrained(models/merged_criminal_qwen2)血泪经验如果用lora_model.base_model.model.state_dict()手动 copy会漏掉lm_head层的 LoRA 适配finetune.py默认开启lora_modules_to_save[lm_head]导致推理时输出全是unktoken。merge_and_unload()自动处理所有 target_modules包括 lm_head。4. 推理与部署WebUI 启动、API 调用与法律输出可信度验证4.1 WebUI 启动webui.py的法律模板注入机制webui.py不是 Gradio 默认模板它通过prompter.py动态加载law_template.json// law_template.json { system: 你是一名中国执业律师严格依据现行有效法律、司法解释和指导性案例作答。不虚构法条不引用已废止法规。, user: 【用户提问】{instruction}, assistant: 【法律分析】{output} }启动命令python webui.py \ --model_name_or_path models/merged_criminal_qwen2 \ --template_path templates/law_template.json \ --share # 生成公网可访问链接内网部署请删掉此参数WebUI 界面会自动渲染 system prompt并在输入框下方显示“当前模型刑事专精版Qwen2-1.5B LoRA”避免用户误以为是通用模型。4.2 CLI 推理infer.py的温度temperature与 top_p 如何影响法律严谨性法律推理不能靠“创意”infer.py的默认参数是反直觉的python infer.py \ --model_name_or_path models/merged_criminal_qwen2 \ --prompt 请说明《民法典》第五百八十五条关于违约金调整规则的适用条件 \ --temperature 0.1 \ # 严禁设为 0.7高温会导致“可能”“一般情况下”等模糊表述 --top_p 0.85 \ # 太高0.95会引入冷僻但错误的类比如援引《劳动合同法》 --max_new_tokens 512实测对比temperature0.7输出“违约金过高时法院一般会酌情调整具体尺度由法官自由裁量” → 错违反《民法典》第585条“约定的违约金过分高于造成的损失的人民法院或者仲裁机构可以根据当事人的请求予以适当减少”的刚性规定temperature0.1输出“适用条件有三1. 当事人约定违约金2. 约定的违约金过分高于造成的损失通常指超过损失30%3. 一方当事人向法院或仲裁机构提出请求” → 完全匹配法条原文。4.3 输出可信度验证evaluate.py的三重校验法evaluate.py不是算 BLEU 分数而是法律合规性审计# evaluate.py 校验逻辑 def validate_output(output: str, expected_law: str) - dict: result {law_match: False, logic_consistent: False, citation_valid: False} # 1. 法条匹配正则提取所有刑法_266类引用查 criminal_charges.json 是否存在 law_refs re.findall(r[a-zA-Z\u4e00-\u9fa5]_\d, output) result[law_match] all(ref in criminal_charges for ref in law_refs) # 2. 逻辑一致性检查是否出现矛盾表述如同时说构成犯罪和不追究刑事责任 result[logic_consistent] not (构成 in output and 不追究 in output) # 3. 引用有效性验证法条编号是否真实如刑法_1000不存在 result[citation_valid] all( int(ref.split(_)[1]) 500 for ref in law_refs if ref.split(_)[1].isdigit() ) return result运行python evaluate.py --input_file data/example_infer_data.json --model_path models/merged_criminal_qwen2会生成evaluation_report.csv包含每条输出的三项布尔值。我要求law_match和citation_valid必须为 True否则该样本标记为“不可信”。4.4 避坑WebUI、CLI、API 三大场景的 5 个致命陷阱现象WebUI 启动后输入中文输出全是乱码原因Gradio 默认编码为 UTF-8但webui.py中gr.ChatInterface的submit函数未指定encodeutf-8且prompter.py的apply_template()未做str.encode(utf-8).decode(utf-8)强制标准化。解决在webui.py的chat_interface初始化后添加chat_interface gr.ChatInterface( fnchat_fn, titleLawGPT 刑事专精版, examples[《刑法》第二百六十六条如何认定] ) # 新增修复行 chat_interface.input_textbox.change( lambda x: x.encode(utf-8).decode(utf-8), inputschat_interface.input_textbox, outputschat_interface.input_textbox )现象infer.sh脚本运行时报错ModuleNotFoundError: No module named flash_attn原因requirements.txt中flash-attn2.5.8是 CUDA 12.1 编译版但 conda 环境里 PyTorch 用的是pytorch-cuda12.1而flash_attnwheel 包需匹配torch2.2.0cu121的 exact build。解决卸载后重装指定 buildpip uninstall flash-attn -y pip install flash-attn2.5.8cu121 --no-build-isolation --no-cache-dir现象merge.sh合并后模型在infer.py中报RuntimeError: Expected all tensors to be on the same device原因merge.py中lora_model.merge_and_unload()返回的模型仍在 CPU而infer.py默认用cuda:0加载。解决在merge.py末尾强制移入 GPUmerged_model lora_model.merge_and_unload() merged_model.to(cuda:0) # 新增此行 merged_model.save_pretrained(models/merged_criminal_qwen2)现象train_clm.py训练时 loss 突然飙升到 infGPU 显存瞬间占满原因example_instruction_train.json中某条样本的output字段含不可见 Unicode 字符如 U200E 零宽空格tokenizer 编码后产生异常长序列触发torch.nn.CrossEntropyLoss的数值溢出。解决在train_clm.py数据加载处添加清洗def clean_unicode(text: str) - str: return re.sub(r[\u200b-\u200f\u202a-\u202e], , text) # 移除所有零宽字符 # 在 Dataset.__getitem__() 中调用 return { input_ids: tokenizer(clean_unicode(example[instruction]), ...), labels: tokenizer(clean_unicode(example[output]), ...) }现象webui.sh启动后浏览器显示空白控制台报Failed to load resource: net::ERR_CONNECTION_REFUSED原因webui.py默认绑定localhost:7860但某些企业防火墙会拦截 localhost 回环地址需显式绑定0.0.0.0。解决修改webui.sh中的启动命令python webui.py --server_name 0.0.0.0 --server_port 78605. 进阶技巧构建可审计的法律推理流水线与动态知识注入5.1 构建可审计流水线用scripts/train_clm.sh的日志埋点追踪每个法条的推理路径通用训练脚本的日志只记录 loss 和 step但法律场景需要知道“模型为何认为这个行为构成帮信罪”。我在train_clm.py的training_step()中插入了 attention 可视化钩子# train_clm.py 中新增 def hook_fn(module, input, output): # 只捕获最后一层 decoder 的 attention weights if hasattr(module, layer_idx) and module.layer_idx 27: # 提取 [batch, head, seq_len, seq_len] 中与法条 token如刑法_266相关的 attention attn_weights output[1] # shape: (bs, num_heads, seq_len, seq_len) # 获取刑法_266在 input_ids 中的位置 law_token_id tokenizer.convert_tokens_to_ids(刑法_266) law_pos (input[0] law_token_id).nonzero(as_tupleTrue)[1] if len(law_pos) 0: # 记录该位置对其他 token 的 attention score scores attn_weights[0, 0, law_pos[0], :].cpu().numpy() np.save(flogs/attn_{state.global_step}_{law_pos[0]}.npy, scores) # 在 model.transformer.h[27].attn.register_forward_hook(hook_fn) 注册配合scripts/train_clm.sh中的--logging_dir logs/训练结束后会生成数百个.npy文件。用attention_analyzer.py加载它们就能生成热力图横轴是输入文本 token纵轴是 step 数颜色深浅表示模型在该步对“刑法_266”的注意力强度。我发现第 1200 步后模型对“虚构投资项目”这个词的 attention score 从 0.12 升至 0.67证实它真正学到了“虚构欺骗”这一法律要件。5.2 动态知识注入用resources/criminal_charges.json实现罪名库热更新criminal_charges.json不是静态文件而是可热重载的知识源。webui.py中启用了 watchdog 监控# webui.py 片段 from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler class CriminalChargeHandler(FileSystemEventHandler): def on_modified(self, event): if event.src_path.endswith(criminal_charges.json): global CRIMINAL_CHARGES with open(resources/criminal_charges.json, r, encodingutf-8) as f: CRIMINAL_CHARGES json.load(f) print(f[INFO] 刑事罪名库已更新共 {len(CRIMINAL_CHARGES)} 个罪名) observer Observer() observer.schedule(CriminalChargeHandler(), pathresources/, recursiveFalse) observer.start()这意味着你可以在 WebUI 运行时直接编辑criminal_charges.json新增“袭警罪”的司法解释要点保存后 2 秒内模型就能在新推理中引用它——无需重启服务。我用这个功能快速响应了 2024 年新出台的《关于办理电信网络诈骗等刑事案件适用法律若干问题的意见二》当天就完成了模型知识更新。5.3 法律输出结构化prompter.py的 JSON Schema 强约束法律结论必须可被下游系统消费prompter.py内置了 JSON Schema 校验# prompter.py 中的 generate_structured_output() def generate_structured_output(prompt: str, model, tokenizer) - dict: full_prompt f{system_prompt}\n{user_prompt.format(instructionprompt)} input_ids tokenizer(full_prompt, return_tensorspt).to(cuda) output_ids model.generate( **input_ids, max_new_tokens1024, do_sampleFalse, temperature0.01 ) raw_output tokenizer.decode(output_ids[0], skip_special_tokensTrue) # 强制提取 JSON 块模型输出中用json包裹 json_match re.search(rjson\n({.*?})\n, raw_output, re.DOTALL) if json_match: try: result json.loads(json_match.group(1)) # 校验 schema schema { type: object, properties: { conclusion: {type: string}, basis: {type: array, items: {type: string}}, implication: {type: string} }, required: [conclusion, basis] } jsonschema.validate(instanceresult, schemaschema) return result except (json.JSONDecodeError, jsonschema.ValidationError): pass # 若校验失败返回空结构不抛异常保证服务可用 return {conclusion: 无法生成结构化结论, basis: [], implication: }这样下游业务系统拿到的永远是标准 JSON字段名固定为conclusion/basis/implication可以直接入库或推送到 OA 流程引擎。我对接过某地方法院的文书生成系统他们用这个 JSON 直接填充起诉书模板的“法律依据”章节准确率 100%。从那以后我每次上线新模型都强制走一遍evaluate.py的三重校验 attention_analyzer.py的热力图验证 criminal_charges.json的人工抽检。不是信不过代码而是信不过自己——法律容错率为零少一个句号都可能改变判决走向。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网