简介:面向中文法律领域的大模型应用与自然语言处理研究者,ChatLaw 中文法律大模型素材包以模型配置、演示与评估为主线,浓缩了项目从运行到验证的完整素材。压缩包内共 35 个文件,涵盖 JSON/JSONL 配置与数据集、Python/Shell 运行脚本、Markdown 说明文档、LICENSE 协议,以及大量 JPG/PNG 框架图、界面截图和演示图片,整体仅 7.78MB,便于快速下载并按目录梳理模型结构。文档与脚本覆盖模型部署、启动运行、法律概念问答、法律咨询等典型场景,同时提供 ELO 评估数据与多阶段演示数据,可帮助读者理解中文法律大模型的数据组织、评估方式与落地流程。已有 270 人浏览学习,适合具备一定自然语言处理基础、希望快速上手中文法律大模型应用与二次开发的开发者参考使用。
1. 一套能直接跑起来的中文法律大模型工程:ChatLaw 资源包拆解
从文件清单能看到什么?demo 数据、web.py、run.sh、MERGE.md、ELO_val、truthfulqa.jpg……这不是一个扔给你权重的压缩包,而是把数据、训练、部署、评估串成一条线的完整工程。ChatLaw 的目标不是让你去和 GPT 比闲谈,而是解决法律咨询、法条检索、文书生成这类对事实性要求极高的场景。通用大模型在术语上会一本正经地胡说八道,领域微调是当前最落地的做法。如果你在做 AI 大模型应用开发,或者正在给企业做知识库问答,这套资源可以直接作为参考基线:先跑通 webui,再替换自己的法律数据,最后用 ELO 和 TruthfulQA 验证效果。按本地部署大模型的通常路径,从数据、部署、微调到评估逐层拆解,整个过程不依赖闭源 API。
2. 模型侧设计:法律领域微调的数据形态与评估依据
2.1 基座选择与领域适配的逻辑
ChatLaw 没有从零预训练,而是在中文能力较强的开源底座上做领域适配,这类底座常见的是 Ziya-LLaMA、BELLE 等。为什么这么选?法律语料虽然量大,但高质量、带专家标注的问答数据很少,从头训练一个法律模型,成本高且难收敛。站在已有底座上做增量预训练和指令微调,能用更少的数据拿到更稳的效果。
法律领域与通用闲聊有一个本质区别:术语必须保持单一语义。普通对话里“不可抗力”可以口语化成“不能预料的情况”,但法律回复里必须严格对应民法中的定义和适用条件。另一个痛点是法条有版本时效,训练数据一旦混入已废止条文,模型就会以极高的置信度引用错误法条。所以领域适配的重点不是让模型会说话,而是让它在限定概念空间里做选择。
从资源包的文件结构能看出设计意图:demo_data_法律概念.jsonl负责基础概念注入,demo_data_法律咨询.jsonl负责用户场景化问答,而demo_data_stage2.json则承担更复杂的指令顺应与多轮对话。MERGE.md的存在说明权重很可能是以 LoRA 或类似低秩适配形式发布,使用前需要合并回基座模型。对做 AI 大模型应用落地的人来说,这套分层数据组织方式比单一大杂烩数据更容易复现和迁移。
2.2 三类 demo 数据的结构与用途
解压后先别急着跑模型,把数据文件打开看几行,信息量比 README 大得多。三个数据文件对应三个不同的训练目标:
| 数据文件 | 代表样本类型 | 训练阶段 | 典型字段 |
|---|---|---|---|
demo_data_法律概念.jsonl | 名词解释、制度说明 | 领域概念注入 | instruction / output |
demo_data_法律咨询.jsonl | 用户场景化提问 | 指令微调 | instruction / input / output |
demo_data_stage2.json | 多轮对话、复杂指令 | 指令顺应强化 | messages / system / tools |
以法律咨询为例,数据行大致长这样:
{"instruction": "我在公司工作半年,未签订劳动合同,现在被辞退,能主张什么赔偿?", "input": "", "output": "可以主张未签订劳动合同的二倍工资差额以及违法解除劳动合同的赔偿金。具体标准建议咨询当地劳动争议仲裁委员会,并保存工资流水与解除通知作为证据。"}每个字段各有用途:instruction是用户侧问题,input用来放附加背景或参考材料,output是标准回复。input不是必填项,空字符串表示只有单轮问答。stage2的文件结构会更复杂,常见的是类似 OpenAI 的 messages 数组,支持 system 角色约束回答风格,也支持多轮历史拼接。标注质量直接决定模型上限,同一事实换几种问法各写几条,比写一百条同质问题有用得多。
2.3 事实性评估与偏好排序:TruthfulQA 和 ELO 为什么同时出现
资源包里同时出现truthfulqa.jpg和ELO_val目录,这不是巧合。TruthfulQA 考察的是模型生成内容是否与事实一致,对法律场景尤其关键:回复可以不够流畅,但不能编造法条和案号。ELO_val 则对应偏好排序,让两个版本的模型对同一组问题作答,由人工或裁判模型打分,再更新 Elo 积分,最后产出类似win_rate.png的胜率对比图。
我的判断是:中文法律大模型不能只看 PPL 或 ROUGE,这两项指标都抓不住“引用了错误法条但表述流畅”的问题。因此考虑用 TruthfulQA 做硬性过滤,用 ELO 做版本对比的软性排序。要注意 ELO 的裁判提示词会对结果产生很大干扰,固定裁判模板、乱序匿名对比是最基本的操作。如果以后自己搭评估集,建议覆盖劳动纠纷、民间借贷、婚姻家事等高频案由,否则 Elo 涨上去也可能只是在少数题上过拟合。
3. 本地部署和 Web 界面:跑通 run.sh,再改 web.py
3.1 先看机器和依赖
部署一个 13B 级别的中文法律大模型,显存是绕不开的约束。ChatLaw 这类模型用 fp16 推理大概需要 26GB 显存,做 4bit 量化后可以降到 12GB 左右。我的建议是:16GB 显存适合量化推理,24GB 以上再考虑微调,不然 OOM 会反复打断调试节奏。
资源包里没有看到 requirements.txt,不过这不妨碍我们补齐依赖。我的习惯是按这个最小集安装:
pip install transformers==4.30.2 peft==0.4.0 accelerate==0.21.0 gradio==3.41.2 einops sentencepiece版本锁定是有原因的:transformers 太新时,AutoModelForCausalLM对老格式权重的兼容性会变差,经常出现权重加载一半直接报 key 不匹配。gradio 3.x 和 4.x 的 API 差异也较大,queue()、launch()参数都不太一样。先按这个组合跑通,再逐版本上探更稳妥。
3.2 run.sh 里到底写了什么
正常发布的工程包里,run.sh 一般负责导出环境变量并启动 Web 服务。一个典型的启动脚本如下:
#!/bin/bash export CUDA_VISIBLE_DEVICES=0 export MODEL_PATH="./models/chatlaw-13b" export PORT=7860 python web.py \ --model_path $MODEL_PATH \ --port $PORT \ --max_new_tokens 1024 \ --temperature 0.3脚本很短,但参数都值得过一遍。CUDA_VISIBLE_DEVICES指定使用哪块 GPU,多卡机器上写错编号会导致模型跑到空闲但显存小的卡上。MODEL_PATH指向合并后的完整模型目录,不是 LoRA 适配器目录,很多新手在这里翻车。temperature调到 0.3 是为了让法律回答偏向保守,避免随机性过大导致法条引用出错。max_new_tokens设 1024 是因为法律回复需要同时包含结论、依据和解释,512 通常会截断后半部分。
实际跑的时候大概率会遇到两个问题。第一是MODEL_PATH写成相对路径,而 run.sh 的执行位置不在工程根目录,导致 tokenizer 加载失败。第二是量化代码写法过时,比如在from_pretrained里传load_in_8bit=True但 transformers 版本不支持。我的排查顺序是先跑一条最小加载命令,确认权重文件本身没问题,再回头查脚本参数。
3.3 web.py 的对话循环逻辑
web.py 本质上是把模型包装成一个 Gradio 应用,核心逻辑可以拆成三段:加载模型、构造对话模板、流式返回生成结果。
import gradio as gr from transformers import AutoModelForCausalLM, AutoTokenizer model_dir = "./models/chatlaw-13b" tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained(model_dir, torch_dtype=torch.float16, device_map="auto") def chat(message, history): # history 是 [(用户, 助手), ...] 列表,拼接成模型能看懂的对话模板 prompt = build_chat_prompt(message, history) inputs = tokenizer(prompt, return_tensors="pt").to(model.device) outputs = model.generate( **inputs, max_new_tokens=1024, temperature=0.3, top_p=0.85, do_sample=True, repetition_penalty=1.05 ) answer = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True) return answergenerate 参数里最值得调的是temperature和repetition_penalty。法律场景我一般把temperature放在 0.2 到 0.4 之间,repetition_penalty放在 1.03 到 1.05 之间,否则模型会在解释法条时反复绕圈。top_p保持 0.85 附近即可,不需要和 ChatGPT 那样激进。拼接多轮对话时要注意角色模板,LLaMA 系和 ChatGLM 系的 human/assistant 标记不同,ChatLaw 这类模型要遵循基座自带的模板,不能只把最近的几条消息拼起来,否则多轮之后模型会分不清谁在说话。
4. 数据改造实战:把杂乱的咨询记录变成可微调的语料
4.1 清洗与转结构
实际项目里拿到的数据大概率不是干净的 instruction/input/output,而是客服对话、裁判文书或留言片段。我一般会先写一个脚本做三件事:去重、去敏感信息、转指令格式。
import json def convert_case_to_sample(case: dict) -> dict: raw_q = case["question"].strip() raw_a = case["answer"].strip() if len(raw_q) < 10 or len(raw_a) < 20: return None # 过短样本直接丢弃,避免教坏模型 return { "instruction": raw_q, "input": case.get("context", ""), "output": raw_a } with open("legal_cases.jsonl", "r", encoding="utf-8") as fin, \ open("legal_cases_sft.jsonl", "w", encoding="utf-8") as fout: for line in fin: sample = convert_case_to_sample(json.loads(line)) if sample: fout.write(json.dumps(sample, ensure_ascii=False) + "\n")这个脚本把自然语言问答映射到训练模板需要的字段,同时过滤掉过短样本。注意output字段如果包含法条,尽量保持原文引用,不要口语化转述,否则模型会学成“意思对但法条编号错”。input字段适合放案件背景、当事人关系、争议焦点等结构化信息,训练后模型会更习惯用input约束输出范围。
4.2 对话长度截断与滑动处理
13B 模型的训练上下文一般是 2048 或 4096,但法律纠纷描述经常超过这个长度,尤其是带判决书摘要的样本。直接从头截断会丢掉结尾的判决依据,从尾截断会丢掉事实背景。我常用的办法是保头保尾,中间做标记:
PREFIX_LEN = 300 SUFFIX_LEN = 700 def trim_long_text(text: str) -> str: chars = list(text) if len(chars) <= PREFIX_LEN + SUFFIX_LEN: return text return "".join(chars[:PREFIX_LEN] + [" ...[中间省略]... "] + chars[-SUFFIX_LEN:])注意字符截断和 token 截断有差异,中文场景下按字符截断通常偏差不大,但如果数据里混了大量英文法条名称,建议改用 tokenizer 计算长度。多轮对话样本则要先拼成完整文本再截断,不能逐轮单独截断,否则会丢失跨轮指代信息。
4.3 微调时的高频坑位
这个环节我踩过的坑大概能列一屏,挑四个最常见的说。
- 基座模板不统一:训练时用 Ziya 的模板,推理时换成别的模板,效果立刻下降。模板必须和基座模型严格对应。
- 法条被模型意译:这其实是数据问题,标注员为了方便把法条改写成白话,模型就学会了凭感觉生成,而不是引用原文。
- padding 侧不一致:训练时用左 padding,生成时用右 padding,batch 推理会出现严重乱码。统一用
padding_side="left"可以解决绝大多数问题。 - 训练 loss 很低但生成依然乱来:优先检查验证集是否混入训练数据,重复样本太多时模型会背答案而不是学泛化。
提示:当验证集 loss 和训练集 loss 差得很小,但生成结果仍然出现明显错乱时,先检查 tokenizer 的 pad_token 是否配置,而不是急着调学习率。
5. 评估验证与权重合并:让模型真正可用前的最后两步
5.1 ELO 评估的落地操作
ELO_val目录里应该是评估脚本和中间结果。如果你要复现同等对比,一个实用的方案是准备 30 到 50 条法律咨询组成的固定测试集,然后让旧模型和新模型分别生成,再做两两对比。Elo 积分更新函数很简洁:
def elo_update(ra, rb, score): ea = 1 / (1 + 10 ** ((rb - ra) / 400)) eb = 1 - ea k = 32 return ra + k * (score - ea), rb + k * (score - eb)score取 1 表示 A 胜,0 表示 A 负,0.5 表示平局。每队样本跑 50 轮后看胜率图,超过 55% 的新权重才值得替换线上模型。如果只有一两道题的胜率差异,基本可以判定是随机波动。
5.2 用 MERGE.md 把 LoRA 权重合回基座
ChatLaw 这类开源模型经常会以 LoRA 适配器形式发布,目的是减小分发体积。MERGE.md讲的就是合回基座的操作。常见做法是用 PEFT 自带脚本:
python merge_peft_adapter.py \ --base_model ./models/ziya-llama-13b \ --adapter_model ./output_chatlaw_lora \ --output_model ./models/chatlaw-13b合并逻辑是把低秩增量加回原始参数,另存为一个完整模型。合并后必须做一致性验证:加载合并前的 LoRA 模型和合并后的完整模型,输入同一句测试文本,确认生成结果完全一致。如果对不上,优先检查adapter_config.json里的base_model_name_or_path是否与真实基座路径匹配,这个字段错了会导致权重错位。
5.3 发布前的一组冒烟测试
正式替换模型前,我会跑一组只有 10 条的冒烟测试,覆盖民法、刑法、劳动法各三道题,再加一道完全无关的闲聊,看模型会不会被带偏。验证时不要通过 Gradio 前端,直接用本地推理接口最小化验证,避免前端缓存干扰结果:
python - <<'PY' from transformers import AutoModelForCausalLM, AutoTokenizer model = AutoModelForCausalLM.from_pretrained("./models/chatlaw-13b", device_map="auto") tok = AutoTokenizer.from_pretrained("./models/chatlaw-13b") prompt = "失业金可以领多久?" inputs = tok(prompt, return_tensors="pt").to(model.device) out = model.generate(**inputs, max_new_tokens=64, temperature=0.3, do_sample=False)[0] print(tok.decode(out[inputs.input_ids.shape[1]:], skip_special_tokens=True)) PY如果闲聊问题被一本正经地回复法律条文,先不要重新训练,把 generation 的temperature降到 0.2,或者在 system prompt 里明确写上“不在知识范围内时礼貌说明无法回答”。这个操作比重新微调便宜得多,往往能救回一版模型。
本文还有配套的精品资源,点击获取