简介:LexiLaw中文法律大模型微调资源包,基于ChatGLM-6B架构在法律数据集上微调而成,面向法律从业者、法学生及普通用户,提供法律咨询、条款解读、案例解析与法规解读等智能问答支持。资源共60个文件、压缩包1.48MB,以Python脚本为主(28个py),涵盖模型微调(finetune_lora.py、finetune_ptuning.py)、推理(inference_lora.py)及ChatGLM模型定义、配置等代码,并含Markdown说明、启动脚本、JSON配置与指令数据示例,其中启动脚本覆盖多种微调训练方式,便于快速上手训练与部署。已有339人学习下载。借助此资源可深入理解中文法律大模型的微调流程、数据组织与推理实现,同时作者还分享了在大模型基础上微调的经验与最佳实践,适合人工智能研究者、法律科技开发者和NLP入门者据此复现实验、改进模型或拓展到其他垂直领域。
1. 中文法律大模型 LexiLaw 到底解决什么问题:先回答效力层级与法条时效这两个真需求
做中文法律大模型这事,最早踩的坑不是算力,是“你以为它懂法”。把一份劳动合同纠纷扔给通用大模型,它能给出三段式分析,但不会提醒你《劳动合同法》第十九条试用期条款的强制属性;你问它民间借贷利息上限,它答得出数字,却说不清这个结论在 2020 年修法后已经换了依据。LexiLaw 这类中文法律大模型要解决的,就是把预训练大语言模型的通用语言能力,压到中国法律体系的事实认定、规范索引和效力层级上。这篇笔记写给三类人:想把通用底座往法律领域调参的算法工程师、要给律所搭知识中台的研发,以及评估法律 AI 产品值不值得投入的技术负责人。
2. 预训练还是微调:为什么中文法律大模型要站在通用底座上继续训练
从零预训练一个法律大模型,听起来很“正统”,但算一笔账就明白不划算:中文预训练语言模型已经把语法、常识、推理能力都练好了,法律领域缺的只是“专业分布”。真正可靠的从业方案是站在通用底座上做继续预训练加指令微调。这一章把基座选型、继续预训练的参数设计和脚本写法一次讲透。
2.1 基座模型怎么选:中文词表、上下文长度和开源许可
选底座时我看四个东西:中文 token 效率、上下文窗口、显存占用、开源许可。中文 token 效率直接决定训练和推理成本——同样是“本院认为”,词表里带常用法律词汇的模型可能按一个词元切,词表偏英文的模型可能切成三四个碎片,长文本场景下成本差距能到 30% 以上。
目前常见的中文底座选择集中在 Qwen 系列、Baichuan2、ChatGLM 系列、Yi 系列,以及 Llama 系加中文词表扩展的变体。我的习惯是列一张对比表再决定:
| 选型维度 | 检查重点 | 踩坑提醒 |
|---|---|---|
| 中文词表 | 法律术语切分后 token 数是否有明显膨胀 | 用“合同解除权”做一次 tokenizer 实测 |
| 上下文窗口 | 原始长度最好 ≥ 8K,后续可外推 | 4K 窗口处理判决书全文会频繁截断 |
| 显存占用 | 7B 全量微调至少需要 4×80G | 先用 1 条样本跑通前向再开训 |
| 开源许可 | 是否有商用限制、是否需要开源衍生权重 | 商用前让法务过一遍模型卡 |
这里插一句为什么不做从零预训练:通用中文语料加法律语料的总量级在万亿 token 级别,训练成本是千万级起步,而继续预训练只需把法律语料按 1:9 到 2:8 的比例混入通用数据,用 1e-5 量级的学习率练几十亿 token,就能把法律实体、文书风格、法条表述的分布拉进模型。对绝大多数团队,这是性价比最高的路径。
2.2 继续预训练阶段:数据配比、学习率与序列长度
继续预训练的目标不是“让模型背法条”,而是让模型在生成时更偏好法律表达。数据配比是这里最容易翻车的地方。早期我试过 100% 纯法律语料继续预训练,结果模型开始“法言法语”到连日常指令都听不懂,回答任何问题都像在写判决书。后来调整为通用数据与法律数据 8:2 或 7:3,通用能力才稳住。法律数据内部也有结构:裁判文书占比最高(50% 左右),其次法规、法考题目、法律咨询问答。
学习率要压得比普通预训练低一个量级。全量预训练常见学习率在 1e-4 到 3e-4,继续预训练我一般取 1e-5 到 2e-5,配合 cosine 衰减。批次大小方面,7B 模型在 8 卡 A100 上设 global batch size 512 到 1024 都算合理,关键看 loss 曲线是否平滑下降。序列长度不要迁就显存而设成 512,法律条款和判决书的论证段落动辄上千字,建议至少 2048,有条件直接上 4096。序列太短的后果是模型在长文本上训练不充分,后续做指令微调时一遇到长输入就乱.
2.3 用 DeepSpeed 跑继续预训练:启动脚本与参数说明
下面是一个我用 DeepSpeed ZeRO-2 跑 7B 继续预训练的启动脚本骨架。它不绑定特定框架,换成 Megatron 或 PaddlePaddle 也可以,关键是参数语义要对齐。
deepspeed --num_gpus=8 train_continue_pretrain.py \ --model_name_or_path Qwen/Qwen-7B \ --train_data_path ./data/law_corpus_jsonl \ --bf16 True \ --output_dir ./outputs/law_base_v1 \ --num_train_epochs 2 \ --per_device_train_batch_size 8 \ --gradient_accumulation_steps 8 \ --learning_rate 1.5e-5 \ --lr_scheduler_type cosine \ --warmup_ratio 0.03 \ --max_length 4096 \ --save_steps 2000 \ --logging_steps 10 \ --deepspeed ./ds_config.json逐个说参数:bf16 True在 A100/H100 上能省显存且数值稳定,如果只有 V100 就得换fp16。per_device_train_batch_size8 加上gradient_accumulation_steps8,等效总 batch 是 8 卡乘 8 再乘 8,等于 512 条样本。learning_rate取 1.5e-5,比普通预训练低是因为底座已经收敛过,学习率太大会把通用能力冲掉。warmup_ratio 0.03的意思是前 3% 的 step 线性升温,避免开局 loss 震荡。max_length 4096是这条命令里最影响显存的参数,显存不够时优先把它降到 2048,而不是动 batch size。
这里特别提醒:继续预训练阶段不要用 LoRA。LoRA 适合指令微调,因为它改变的是行为风格;继续预训练需要更新底层知识分布,低秩适配会限制模型吸收新语料的能力。全量微调慢,但这一步慢得值。
3. 法律语料工程:把裁判文书与法规库变成可训练数据的完整流水线
法律大模型效果的上限不在模型结构,在数据。这一章写语料从哪里来、怎么清洗、怎么去重、怎么配比。每一步都有具体脚本和参数,照着改就能跑。
3.1 语料来源与合规边界
法律语料按优先级排,第一梯队是国家法律法规数据库和裁判文书公开网。法规库结构化程度高,直接能转成条文对;裁判文书则要处理大量格式噪声。第二梯队是法考真题、法学教材、法律咨询社区问答。第三梯队才是法律百科和新闻,噪声高,只能做补充。
合规是硬门槛。裁判文书即使公开,也包含当事人姓名、身份证号、住址、银行账号。我处理文书的固定流程是:先做实体脱敏,再进清洗管线。脱敏规则上,姓名用随机替换姓氏加“某”,身份证号用正则匹配 18 位数字后整体打码,手机号保留前三位和后两位,其余星号。这一步不做,数据不能出内网,更不能拿去训练。
3.2 从 PDF 判决书到干净文本:清洗脚本与正则规则
import pdfplumber import re def extract_pdf_text(pdf_path): full_text = [] with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: text = page.extract_text() if text: full_text.append(text) return "\n".join(full_text) def clean_judgment(text): # 去掉页眉页脚:多为法院名称与页码 text = re.sub(r'^\s*第\s*\d+\s*页.*$', '', text, flags=re.MULTILINE) text = re.sub(r'^\s*※{3,}.*$', '', text, flags=re.MULTILINE) # 去掉案号行之外的空行与多余空格 text = re.sub(r'[ \t]+', ' ', text) text = re.sub(r'\n{3,}', '\n\n', text) # 按审判程序切分主文与说理部分 text = text.replace('本院认为', '\n【本院认为】\n') text = text.replace('判决如下', '\n【判决如下】\n') return text def desensitize(text): # 身份证号:18 位,支持 X 结尾 text = re.sub(r'\b[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx]\b', '【身份证号已隐藏】', text) # 手机号 text = re.sub(r'\b1[3-9]\d{9}\b', '【手机号已隐藏】', text) return text逻辑说明:pdfplumber负责把 PDF 逐页抽成文本,这一步跑不快,10 万份文书建议用多进程并行。clean_judgment里的三个正则分别处理页眉页脚、多余空白和段落切分。把“本院认为”和“判决如下”标记成特殊段落头,是为了后续构造指令数据时能精准切出“裁判说理”和“裁判结果”两个片段。desensitize里的身份证正则看着长,其实只做一件事:匹配 18 位身份证号并整体替换。注意顺序不能反,先脱敏再清洗,否则原始文本里的脱敏标记会被清洗规则误删。
清洗后的文本长度分布一定要看。我踩过的坑是:一次性把所有文书拼成长文本,导致单条样本超过 10 万 token,训练时只能硬截断,法官的说理逻辑被切成两半。正确做法是按“一案一文”切分,一份判决书作为一条独立样本,超长的再按“本院认为”和“判决如下”切成上下两段。
3.3 去重与配比:防止模型被某一类文书带偏
法律语料的重复率比想象中高。同一个案由的判决书,模板部分几乎一样;同一部法规的解读在不同网站被反复转载。不去重,模型会过度拟合高频模板,生成时不断复读“依照《中华人民共和国民法典》第五百七十七条”,却说不清违约责任的构成要件。
去重我用两层。第一层是 MinHash 做全文近似去重,阈值设 0.85,也就是相似度 85% 以上的文本对只保留一条。第二层是对“本院认为”段落单独做 SimHash 去重,因为这部分是模型最需要学的推理内容,重复了影响最大。两层的脚本都不复杂,核心参数就是哈希函数数量和阈值,前者影响召回,后者影响精度,不必调太细,先跑一遍看重复率再决定要不要收紧。
配比上,我当前的默认配比是:裁判文书 50%、法律法规 20%、法考题目与解析 15%、法律咨询问答 10%、法学教材 5%。这个比例的核心逻辑是:裁判文书占比最高,因为它包含了法律事实、证据认定、条文引用的完整体现,模型从中学到的是“怎么把规范用到事实上”;法规是骨架,但纯背条例无法形成推理能力;法考题目和咨询问答负责教会模型“提问-回答”的格式。教材占比最低,因为它和通用语料的知识重叠度高,加太多收益有限。
4. 指令微调与对齐:让模型学会论证而不是复读法条
继续预训练做完,模型只是“法律语料概率分布模拟器”。要让它像一个法律助手那样回答问题,必须做指令微调。这一章讲指令数据怎么构造、微调超参怎么定、以及法律场景下特有的对齐问题。
4.1 从裁判文书构建议问对:把“本院认为”变成训练数据
import re import json def build_qa_from_judgment(clean_text): # 提取裁判说理部分 reasoning_match = re.search(r'【本院认为】\s*(.*?)(?:\s*【判决如下】|$)', clean_text, re.S) decision_match = re.search(r'【判决如下】\s*(.*?)(?:\s*$)', clean_text, re.S) if not reasoning_match: return None reasoning = reasoning_match.group(1) decision = decision_match.group(1) if decision_match else "" # 按案由生成问题模板 cause_match = re.search(r'((?:民间借贷|劳动合同|买卖合同|离婚|侵权)纠纷)', clean_text) cause = cause_match.group(1) if cause_match else "民事纠纷" qa_pairs = [ { "instruction": f"请基于以下案情,分析{cause}中双方主要的争议焦点。", "output": reasoning, }, { "instruction": f"案件判决结果是什么?", "output": decision, }, ] return qa_pairs with open("judgments_clean.jsonl", "r") as f: with open("law_instructions.jsonl", "w") as out: for line in f: doc = json.loads(line) pairs = build_qa_from_judgment(doc["text"]) if pairs: for p in pairs: out.write(json.dumps(p, ensure_ascii=False) + "\n")这个脚本的逻辑是:从清洗后的判决书里,定位“本院认为”和“判决如下”两个关键段落,再把它们映射成“分析争议焦点”和“陈述判决结果”两类指令。问题模板虽然机械,但胜在稳定、不易跑偏。这里有一条主动加的噪声:在问题里保留案由,是为了让模型学会按案由类型调整回答结构,是民间借贷就谈利息与还款期,是劳动合同就谈解除与赔偿金。
指令数据的质量比数量重要。我见过有人为了凑数据量,把“本院认为”整段复制到输出里,结果模型学会的是把案情复述一遍再给结论,而不是抽象出裁判规则。正确做法是至少让一个懂法律的人对 100 条指令输出做改写,把“法院认为”的口吻改成“法律分析”的口吻,去掉具体案号和人名,让结论更通用。这 100 条改写后的数据放进训练集,能明显改变模型的回答风格。
4.2 微调超参与 loss 观察:全量微调还是 LoRA
model: Qwen/Qwen-7B method: lora lora: r: 32 lora_alpha: 64 lora_dropout: 0.05 target_modules: ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"] train: batch_size: 4 gradient_accumulation_steps: 16 learning_rate: 2e-4 num_epochs: 3 max_length: 2048 warmup_ratio: 0.03 scheduler: cosine这份配置是 LoRA 微调的典型参数。r表示低秩矩阵的秩,32 在 7B 模型上是经验值,太小欠拟合,太大显存压力高且容易过拟合。learning_rate用 2e-4,比全量微调的 1e-5 高一个量级,这是 LoRA 的特性——它只更新少量参数,需要更大的步长才能收敛到有效区域。target_modules把注意力 QKV 和 MLP 投影都覆盖了,因为法律论证需要模型在两个层面同时适配:注意层决定它关注哪些案情要素,MLP 层决定它怎么组织论证逻辑。
观察 loss 曲线时,不要只盯训练 loss。常见情况是训练 loss 降到 0.6,但验证 loss 在某个点开始反弹,说明过拟合了。法律指令数据量通常在几万到几十万条,3 个 epoch 已经偏多,我一般先训 2 个 epoch 存一次中间权重,再对比验证集效果决定要不要继续。另外要盯生成结果的“重复率”——如果模型输出里反复出现“根据《中华人民共和国民法典》”这样的套话,说明过拟合到模板上了,应该立刻停训。
4.3 对齐:法律模型里的“人工智能偏见”怎么处理
法律场景的对齐,和通用助手的“安全无害”不太一样。我把它拆成三个具体要求:立场中立、不教唆规避法律、不编造法条来源。
立场中立是最难实现的。裁判文书天然带立场——法官做出了判决,模型学到的就是“判决书式结论”。当你问“这笔借款该不该还”时,模型可能直接给出判决式回答,而不是列出双方主张与法律依据。处理办法是在指令数据里故意加入对立视角的问答对,比如同一案情分别问“债权人如何主张”和“债务人如何抗辩”,让模型学会站在不同位置分析。这个做法很像对抗训练,数据量不用多,几千条就能见效。
不教唆规避法律这条,需要在对齐阶段加入拒答模板。训练数据里要配一批“如何在借条上不写借款金额”这类问题的安全回答,明确告诉模型这类问题不提供操作建议,只做法律风险提示。我在实践中会单独做一次 DPO(直接偏好优化),偏好数据就是从这些边界问答里构造的。效果比单纯在 prompt 里加安全提示词稳定得多。
5. 法律大模型训练避坑:五个翻车点与可落地的排查手段
这一章是从多次训练和部署里攒出来的踩坑记录,每条都是“现象、原因、解决”三步。遇到同类问题,直接对照排查。
5.1 现象:模型把“应当”和“可以”搞混,回答没有效力层级
微调后测试,模型在回答“合同未约定履行期限,债权人能否随时要求履行”时,回答成“债权人应当随时要求履行”。问题很严重:“应当”是强制性规范,“可以”是授权性规范,法律效力完全不同。
原因在于训练数据没有强调规范词与效力层级的对应关系。模型在预训练时见过大量“应当”和“可以”的混用语境,微调数据量没大到能纠正这种混淆。
解决方法是两条腿走路。第一,在指令数据里人为构建“规范词辨析”问答对,比如“约定无效与合同无效的区别”“应当与可以的法律后果差异”。第二,在后处理层对输出做规范词检查,发现“应当”出现在非强制语境时,用规则标记并提示生成模块重新生成。规则检查不完美,但能拦下大部分低级错误。
5.2 现象:微调后通用能力崩了,回答什么都像在写判决书
指令微调做完,把模型拉回通用对话场景测试,发现它连“帮我写封请假邮件”都回答成“依据《劳动法》第四十条……”。这是领域模型最常见的翻车方式。
原因是指令数据里法律问答占比过高,模型把“法律论证”当成了唯一任务模式。深层原因是底层预训练分布被微调覆盖,丧失了多样性。
解决办法是在微调数据里混入 15% 到 20% 的通用指令数据,覆盖翻译、摘要、写作、日常对话等任务。同时把法律指令数据的输出样式做多样化处理,不要全部使用“分析-依据-结论”三段式。另外检查一下学习率是否过高,LoRA 训练中学习率超过 3e-4 会对原始权重造成破坏,降到 2e-4 以下通常能缓解。
5.3 现象:引用法条时编造“第 XX 条”,乍看合理实则虚构
模型回答“根据《民法典》第五百八十八条,违约金过高可以请求适当减少”,但这条规定实际并不存在——第五百八十八条确实存在,但内容和违约金无关。这是典型的幻觉。
原因有两个:一是模型对法条的记忆不精确,它记得“违约金”和“民法典”经常共现,就编织了一条不存在的法条编号;二是训练数据中法条原文与解读文本混杂,模型分不清哪句是原文、哪句是解读。
解决思路分两层。训练层面,在指令数据中把法条引用格式统一成“《民法典》第 XXX 条”,并单独构造一批法条编号修正问答来强化记忆。推理层面,上线时务必外接法条库检索,也就是下一章要讲的 RAG。只靠模型记忆无法根治幻觉,检索增强才是可信兜底。
5.4 现象:新旧法冲突答错,民间借贷利率按旧标准算
测试问“民间借贷年利率 36% 合法吗”,模型按 2015 年司法解释回答“合法,但超出部分自愿履行”。2020 年修法后,上限已调整为 LPR 的四倍。
原因是语料时间维度混乱:训练数据里包含大量已被废止的旧法条和旧判决,模型没有法条生效时间的概念。
解决办法是给语料打时间戳。在清洗阶段,给每条法规和判决书标注“生效日期”“废止日期”或“裁判日期”。在训练时把时间信息拼进文本前缀,比如“【裁判日期:2021年】”。推理时,在 prompt 里注入当前日期,并配合法条版本库做二次校验。市场上有做法律版本管理的数据库,选型时优先考虑那些能区分“现行有效”和“已废止”的数据源。
5.5 现象:显存 OOM 频繁,训练一到 4000 长度就爆
7B 模型用 LoRA 微调,max_length设成 4096 后,8 张 80G 的卡照样 OOM。检查日志发现是激活值爆炸。
原因是长序列下注意力矩阵和 MLP 激活值按序列长度平方增长,4096 长度下显存占用远超 2048 的两倍。
解决组合是:开启 Flash Attention,激活值显存能降一半以上;开启梯度检查点,用少量计算换大量显存;max_length如果业务确实需要 8192,就用序列打包把多条短样本拼成一条长样本,而不是直接把空白 padding 到 8192,否则无效 token 会浪费大量显存。还有一个被忽略的点:logging_steps设得太小会导致频繁记录 loss 而触发同步等待,造成显存不释放,建议设成 50 到 100。
6. 进阶用法:给 LexiLaw 加检索增强,把幻觉压到可商用水平
单靠模型参数记住法条,无论训多少轮都有天花板。我现在的固定做法是:微调模型在前,检索增强在外,把“记忆”和“查找”拆开。
接入方式不复杂。先把现行有效的法规库切条、做 embedding 存入向量库,推理时用用户问题检索 Top-K 条文,拼进 prompt。Embedding 模型选中文场景表现稳定的 bge-large-zh,检索 Top-K 设 5,K 太小漏依据,K 太大会把不相关条文也塞进上下文。
from sentence_transformers import SentenceTransformer import chromadb embedder = SentenceTransformer("BAAI/bge-large-zh") client = chromadb.PersistentClient(path="./law_db") collection = client.get_or_create_collection( name="law_articles", metadata={"hnsw:space": "cosine"} ) def retrieve_articles(question, top_k=5): q_emb = embedder.encode(question, normalize_embeddings=True) results = collection.query(query_embeddings=[q_emb], n_results=top_k) return [doc["document"] for doc in results["documents"][0]]这段代码的重点在后两行:query里传的q_emb必须和写入时的 embedding 用同一个模型生成,且都做 L2 归一化,否则余弦相似度计算会失真。检索结果拼接到 prompt 时,要明确告诉模型“只依据检索到的条文回答”,并在输出中标注引用了哪一条,方便人工核对。这一步能把法条编号错误率从 15% 以上压到 3% 以下。
最后分享一个习惯:每次微调完,我先跑 50 条人工标注的法务压测题,按“结论正确率”“法条引用准确率”“是否包含明确免责声明”三个维度打分,分数不过 80 就不上生产。哪怕模型结构没变、数据只改了几百条,也不跳过这一轮。这套笨办法救了我好几次。希望帮到你。
本文还有配套的精品资源,点击获取