
简介大语言模型在垂直领域的落地往往绕不开数据安全与私有化部署的现实约束。以法律行业为例合同文本和咨询记录受保密协议限制无法上传公有云这就需要在本地GPU环境完成模型微调与推理。参数高效微调技术LoRA通过冻结底座模型、只训练少量低秩矩阵显著降低显存门槛而RAG检索增强生成则能有效抑制法律咨询中的幻觉问题让模型基于本地法规库作答。从PyTorch环境搭建、Transformers加载开源底座到指令样本构造、LoRA训练再到合同条款级审查与RAG知识库检索这一整套方案完整覆盖了法律AI落地的技术链路。本文基于PyTorch与Transformers生态结合CUDA部署实践为合同审查、法律咨询等高频场景提供了一条数据不出内网的可行路径适合需要私有化大模型能力的团队参考。1. 本地法律大模型不是图新鲜是合同数据根本出不了门律所和企业法务普遍卡在同一个矛盾上手里压着几万份历史合同和咨询记录领导一拍板要上“AI 审合同”但你没法把合同原文传到任何一家公有云 API——保密协议第一条就写死了。于是基于 PyTorch 和 Transformers 的本地法律大模型就成了最现实的一条出路用开源底座、私有化部署、数据不出内网先把合同审查和法律咨询这两个高频场景跑通。这篇文章给一套能落地的方案和代码从 CUDA 环境搭起用 LoRA 把通用底座微调成懂法务的模型再用 RAG 解决咨询场景的幻觉问题最后落到内网服务。适合手里有一张 GPU 卡、想把法务场景私有化的团队新手照着跑得通熟手可以直接跳去调参数。2. 先立底座PyTorch 与 Transformers 的版本匹配、CUDA 坑和最小加载链路2.1 为什么选 PyTorch Transformers 而不是别的组合本地法律大模型不是从零训练而是在开源底座上做微调和推理所以框架选型只有一个要求跟上开源模型的脚步。PyTorch 是当前开源大模型的事实标准Hugging Face 的 Transformers 库所有重量级模型首发都先给 PyTorch 版本DeepSpeed、bitsandbytes、PEFT 这些微调工具链也都围绕 PyTorch 转。用 Transformers 做底座还有一个好处它把加载、分词、生成、保存全部统一成一套 Auto API换底座模型时不需要重写业务代码后面微调和部署会省掉大量精力。有人会问为什么不直接用 TensorFlow 或者 JAX不是不能用而是社区生态跟不上。LoRA、QLoRA、vLLM 这些工具对 PyTorch 的支持最完整你能搜到的“大模型微调实战”帖子九成跑在 PyTorch 上。真遇到一个只有英伟达卡和 CUDA 的环境PyTorch 的排查经验也最多血泪教训都替你踩过一遍了。2.2 PyTorch 环境搭建CUDA 版本匹配的一次到位做法环境搭建是本地部署的第一个翻车点。很多人第一步就错在 PyTorch 的 CUDA 版本和驱动不匹配装完之后import torch看着正常一跑torch.cuda.is_available()返回 False心态直接崩掉。先记住一个原则PyTorch 的 CUDA 版本只要小于等于驱动支持的 CUDA 版本就能跑不是非要一模一样。查看驱动支持的最高 CUDA 版本用nvidia-smi看右上角的 “CUDA Version”。比如驱动显示 CUDA 12.2那你用 PyTorch 官方给的 cu118、cu121、cu122 的 wheel 都能跑。下面是完整的创建环境步骤# 1. 创建独立环境Python 用 3.10不要用 3.7/3.8 跑新模型 conda create -n legal-llm python3.10 conda activate legal-llm # 2. 安装 PyTorch 2.x注意指定 CUDA 版本这里以 cu121 为例 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 # 3. 安装 Transformers 全家桶 pip install transformers4.45.2 datasets peft accelerate bitsandbytes安装完成后先别急着往下走跑一段可见性验证你的 GPU 要真能被 PyTorch 看到后面所有微调才有意义。import torch print(PyTorch 版本:, torch.__version__) # 例如 2.4.0 print(CUDA 可用:, torch.cuda.is_available()) # 必须是 True print(GPU 数量:, torch.cuda.device_count()) # 至少 1 print(GPU 名称:, torch.cuda.get_device_name(0)) # 例如 NVIDIA A100-SXM4 print(当前显存占用:, torch.cuda.mem_get_info())如果torch.cuda.is_available()返回 False优先检查三件事是不是装的 CPU 版pip list | grep torch看看有没有cpu后缀、驱动版本是不是太老、系统 CUDA 环境变量是不是把路径指错了。最怕的是机器上装过多个 CUDALD_LIBRARY_PATH指到了旧版本上。解决方法是把环境变量清掉让 PyTorch 自己找系统驱动。2.3 加载底座模型最小链路与显存规划底座选多大取决于显存而不是理想。7B 模型加载到 GPU 做推理16GB 显存很紧张24GB 是甜点48GB 以上可以舒服地做 LoRA 微调。如果手里只有 12GB就走 4bit 量化加载后面第 6 章会讲。先看最小加载链路from transformers import AutoTokenizer, AutoModelForCausalLM import torch # 本地路径提前用 huggingface-cli 下载好底座权重 model_path /data/models/legal-base-7b tokenizer AutoTokenizer.from_pretrained( model_path, trust_remote_codeTrue, # 部分开源模型需要 padding_sideright ) model AutoModelForCausalLM.from_pretrained( model_path, device_mapauto, # 自动把层分配到 GPU torch_dtypetorch.float16, # 半精度加载显存减半 trust_remote_codeTrue ) # 跑一次前向确认没有报错 input_text 请简要说明合同中的违约金条款需要注意什么 inputs tokenizer(input_text, return_tensorspt).to(cuda) output model.generate(**inputs, max_new_tokens128) print(tokenizer.decode(output[0], skip_special_tokensTrue))device_mapauto是 Transformers 在 accelerate 支持下自动做显存规划的方式GPU 放得下就全放 GPU放不下会把部分层放到 CPU跑是能跑但会慢。torch_dtypetorch.float16让 7B 模型显存占用从 28GB 左右降到 14GB 左右。加载链路跑通之后第一个里程碑达成——你已经在本地拥有一个能对话的通用大模型了下一步是让它懂法务。3. 把模型变成法务合同数据清洗、指令样本构造与 LoRA 微调实战3.1 合同文本清洗数据里全是隐藏的地雷直接从业务部门拿到的合同文本九成都是脏的有 PDF 转出来的全角标点、有扫描件 OCR 留下的乱码、有页眉页脚的页码混在正文里。如果不做清洗直接送去微调模型会学到“每段开头是页眉”这种错误规律审查结果会变得非常不稳定。清洗这一步我一般按“全角转半角—去页码—归一化编号”三步走import re def clean_contract(text: str) - str: # 1. 全角转半角合同里最常见的是全角逗号、括号和数字 text text.translate(str.maketrans( 。【】, ,.:;()[]0123456789 )) # 2. 去掉单独成行的页码和页眉页脚 text re.sub(r\n\s*\d{1,4}\s*\n, \n, text) # 3. 把“第一条”、“第1条”统一成“第1条”便于后续条款切分 text re.sub(r第([一二三四五六七八九十百零])条, lambda m: f第{cn2num(m.group(1))}条, text) # 4. 压缩多余空行统一换行符为 \n text text.replace(\r\n, \n) text re.sub(r\n{3,}, \n\n, text) return text.strip()这里的cn2num是把中文数字转阿拉伯数字的小函数网上有现成实现核心是把“第一条”变“第1条”。为什么必须做这一步因为后面风险条款定位要按“第X条第Y款”输出如果训练数据里一会儿“第一条”一会儿“第1条”模型输出格式就会分裂。清洗完要做抽样检查不能只看总量。常见做法是清洗后随机抽 50 条肉眼确认没有乱码、没有残留 OCR 噪声。这一步的瑕疵会在微调阶段被模型放大——数据脏模型比人学得更脏。3.2 构造指令样本条款风险对与三段式问答微调数据的质量直接决定模型能不能从“通用助手”变成“法务助手”。合同审查场景我用的数据格式是“指令 合同条款输入 结构化输出”{ instruction: 你是合同审查助手。阅读合同条款指出风险按风险级别、风险位置、风险描述、修改建议四部分输出。, input: 第8条 违约责任任何一方违约应向守约方支付违约金违约金金额为合同总额的0.5%。, output: 风险级别中风险\n风险位置第8条\n风险描述该条款未约定违约损失的具体计算方式争议发生时责任范围不明确。\n修改建议建议补充损失计算方式并复核违约金比例是否与商业预期一致。 }注意这里故意不让模型去做具体法律判断而是输出结构化的提示框架——风险级别、位置、描述、建议四个字段位置必须精确到条款号。真实项目中这些标注数据由法务整理一条合同条款可以变成一正一反两个样本一个有风险、一个无风险让模型学会区分。法律咨询场景的数据则是另一种格式问题、分析依据、结论三段式其中“依据”一栏必须写明引用了哪份法规或内部制度原文。这样训练出的模型习惯性先给依据再下结论为后面的 RAG 检索对齐做铺垫。3.3 LoRA 微调参数选型与完整训练脚本全参微调一个 7B 模型最小也要 60GB 显存几人团队根本吃不消。LoRA 的做法是冻结原模型只在 attention 层旁边加一小撮可训练的低秩矩阵把训练参数压缩到原来的 1% 不到。7B 模型 LoRA 微调配合 8bit 量化24GB 显存就能跑起来。这是我的标准配置from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments from peft import LoraConfig, get_peft_model, TaskType from datasets import load_dataset model_path /data/models/legal-base-7b model AutoModelForCausalLM.from_pretrained( model_path, device_mapauto, load_in_8bitTrue, # 8bit 加载省显存 trust_remote_codeTrue ) tokenizer AutoTokenizer.from_pretrained(model_path) # LoRA 配置只改 attention 四个投影矩阵 lora_config LoraConfig( task_typeTaskType.CAUSAL_LM, r8, # 秩小数据集用 8大数据集可以上 16 lora_alpha32, # 缩放系数alpha/r 一般是 2~8 lora_dropout0.05, # 丢弃率防过拟合 target_modules[q_proj, k_proj, v_proj, o_proj] ) model get_peft_model(model, lora_config) model.print_trainable_parameters() # 7B 模型的训练参数应该只有 8~20M 级别 training_args TrainingArguments( output_dir./lora-legal-out, num_train_epochs3, # 法律数据量小3 个 epoch 足够 per_device_train_batch_size2, gradient_accumulation_steps8, # 等效 batch size 2*8 16 learning_rate2e-4, # LoRA 学习率比全参大常见 1e-4~5e-4 fp16True, save_strategyepoch, logging_steps20, ) trainer Trainer( modelmodel, argstraining_args, train_datasetdataset, ) trainer.train()参数小数据集千条级中数据集万条级r816lora_alpha3232lora_dropout0.050.1learning_rate2e-41e-4num_train_epochs32lora_alpha32配合r8等价缩放系数 4是最稳妥的起步组合。训练完只保存 LoRA 增量权重model.save_pretrained(./lora-legal-out)出来也就几十 MB原模型一点没动这相当于微调的“后悔药”机制——觉得增量权重不行删掉重训或换一组参数底座永远干净。4. 合同审查功能落地条款切分、风险识别和结构化报告怎么串起来4.1 审查链路总览与文本抽取微调完的模型放进真实流程第一步是处理用户丢进来的合同文件。真实合同是 PDF、Word 混合体不能直接送进模型。我的链路是上传文件 → 抽取文本 → 清洗分条 → 逐条送模型 → 汇总报告。PDF 用pdfplumber抽取Word 用python-docx这两种库在中文场景下表现比老牌库稳表格布局丢失问题也少一些。import pdfplumber from docx import Document def extract_text(path: str) - str: if path.endswith(.pdf): with pdfplumber.open(path) as pdf: return \n.join(page.extract_text() or for page in pdf.pages) elif path.endswith(.docx): doc Document(path) return \n.join(p.text for p in doc.paragraphs if p.text.strip()) else: raise ValueError(仅支持 PDF 或 DOCX 文件)抽取完成先做长度检查一份合同通常在 3000 到 15000 字之间如果抽出来只有几百字八成是扫描件ocr 那套流程需要在前面另起一路。文本抽取的质量决定了后面切分和识别的上限这里卡住了后面全都白做。4.2 条款级风险识别低温度 prompt 与结构化输出合同审查用的是微调后的模型但 prompt 仍然要设计得足够“硬”——要求模型输出 JSON审查结果才能被下游系统解析入库。这里有一个关键参数temperature0.1。审查场景要的是确定性和可复现性不是创造力和多样性。同一份合同每次审查结果必须一致这一点对客户信任极其重要。prompt_template 你是合同审查助手。分析以下合同条款只输出 JSON不要输出额外文字。 格式要求{risk_level: 高|中|低, clause_no: 条款号, risk_desc: 风险描述, suggestion: 修改建议} 合同条款 {clause_text} def review_clause(clause_text: str) - dict: response model.generate( tokenizer(prompt_template.format(clause_textclause_text), return_tensorspt).to(cuda), max_new_tokens256, temperature0.1, # 审查场景用低温度保证结果稳定 do_sampleFalse # 关闭采样输出更确定 ) raw tokenizer.decode(response[0], skip_special_tokensTrue) # 取 JSON 片段防止模型输出多余解释 import json, re match re.search(r\{.*\}, raw, re.S) return json.loads(match.group(0))do_sampleFalse是降低幻觉的第二个抓手。通俗说模型不再“抽签选词”而是每一步都挑概率最高的词代价是输出略显机械但换来了审查结果的稳定性。真到业务验收时客户问“为什么昨天审查说高风险今天说低风险”你能拍着胸脯说温度参数是固定 0.1。4.3 审查报告落地风险分级与修改建议逐条过完模型后还要汇总一份人能直接看的报告。我一般把风险分成三档高风险是付款节点不明、无限连带责任中风险是条款存在歧义低风险是格式不规范。汇总结果用 Pandas 存成结构化表格按风险级别排序输出import pandas as pd def build_report(results: list[dict]) - pd.DataFrame: df pd.DataFrame(results) df[risk_level] df[risk_level].astype(category) # 按风险级别排序高 中 低 df[risk_order] df[risk_level].map({高: 0, 中: 1, 低: 2}) df df.sort_values(risk_order, ignore_indexTrue) return df[[risk_level, clause_no, risk_desc, suggestion]] report build_report(results) report.to_excel(./审查报告.xlsx, indexFalse)导出成 Excel 是最稳妥的做法法务同事可以在上面直接批注。不要试图做成网页展示——真实环境的法务用 Excel 用了十年改变习惯的难度远大于做功能。到这里合同审查的完整闭环已经通了文件进、报告出中间模型只负责条款级判断。5. 法律咨询的 RAG 增强与高频踩坑排查幻觉、上下文截断和显存溢出5.1 为什么咨询场景必须加 RAG微调后的模型能做合同审查因为审查是“从条款里找问题”信息都写在条文里。但法律咨询场景完全不同——“公司单方面解约需要赔偿吗”这类开放问题模型如果全靠记忆回答结果就是一本正经地编法条编得还特别像真的。这是大模型的通病微调解决不了因为微调只是强化了记忆并没有给模型一个“查资料”的通道。RAG检索增强生成的思路是先从一个本地知识库里检索出和问题相关的法规原文再把原文拼进 prompt 里让模型基于给定材料作答。这样模型的任务从“回忆法条”变成“转述法条”幻觉率大幅下降。本地知识库的素材来源有公开法规库、司法解释、公司内部合规手册、历史咨询记录里的标准答复。5.2 本地知识库向量化、检索与 prompt 组装知识库的核心是“切块—向量化—检索”三步。法规原文很长整篇塞进提示词会让模型顾头不顾尾。我的做法是按段落切块每块 512 字符重叠 32 字符这样相邻段落不会因为切分丢掉上下文。向量模型用 BGE 系列或 sentence-transformers 都行国内法律文本用 BGE 的兼容性更好。from sentence_transformers import SentenceTransformer from sklearn.metrics.pairwise import cosine_similarity import numpy as np encoder SentenceTransformer(/data/models/bge-base-zh) # 本地向量模型 # 知识库文本提前切块这里是示意 chunks [ 劳动合同法第X条规定……, 民法典对违约责任的规定……, 公司法关于股东责任的规定……, ] chunk_vectors encoder.encode(chunks, normalize_embeddingsTrue) def retrieve(query: str, top_k: int 5): q_vec encoder.encode([query], normalize_embeddingsTrue) # 直接用向量点积排序等价于余弦相似度 scores np.dot(chunk_vectors, q_vec.T).squeeze() idx scores.argsort()[-top_k:][::-1] return [chunks[i] for i in idx] # 检索结果拼进 prompt约束模型只依据给定材料回答 context \n----\n.join(retrieve(公司单方面解约的赔偿标准)) prompt f根据以下法律条文回答用户问题。只能引用给定条文不得自行编造。 如果给定条文不能完全支持问题答案明确说明“检索材料未能覆盖此问题”。 材料 {context} 问题公司单方面解约需要赔偿吗 检索结果拼进 prompt 的方式有几个细节一是要强调“只能引用给定条文”这句话是约束幻觉的关键咒语二是要给模型留一条退路——“检索材料未能覆盖此问题”允许它拒绝回答。有了拒绝选项模型反而更可信。5.3 五个高频踩坑现象、原因、解决踩坑一torch.cuda.is_available()返回 False但 nvidia-smi 明明有 GPU。现象是 PyTorch 检测不到 CUDA。原因是 pip 装了 CPU 版的 torchwheel 带cpu后缀或者系统 CUDA 驱动版本低于 PyTorch 要求。解决先pip list | grep torch看有没有cpu有就重装--index-url https://download.pytorch.org/whl/cu121没有就检查nvidia-smi驱动版本驱动低于 525 的先把显卡驱动升级。踩坑二加载模型时抛 “xxx is already used by a transformers config, pick another name” 之类的报错。现象是第二次加载同一个本地模型路径时transformers 报配置名冲突。原因是本地缓存目录或输出目录里残留着上一次保存的 config 同名文件新配置注册时名字撞车。解决清理该模型目录下的config.json和*.json临时文件或者换一个全新的输出目录名不要跟旧目录复用一个名字。踩坑三微调刚开始就 OOM启动即崩。现象是训练脚本跑起来几秒钟显存报错。原因是只顾着模型显存忽略了优化器状态和激活值也吃显存。解决加上load_in_8bitTrue同时开gradient_checkpointingTrue用时间换显存把per_device_train_batch_size降到 1用gradient_accumulation_steps补回 batch size。踩坑四模型引用了一个根本不存在的新规。现象是咨询回答里出现具体年份和文号去查发现子虚乌有。原因是模型在自由发挥微调数据和原模型参数里都没有这个信息。解决咨询场景强制走 RAGprompt 里写明“只能引用给定材料”并且把温度降到 0.2 以下。审查场景因为是对条款做判断出现编造的概率低一些但输出 JSON 解析失败时也要有重试机制。踩坑五直接把 8000 字合同原文一股脑送进模型结果关键条款被截断。现象是审查报告里缺了最后几章的内容。原因是上下文窗口不够时Transformers 会从尾部截断而合同的违约责任、管辖条款往往就在后半部分。解决先按“第X条”正则把合同切成条款级别对每条单独推理而不是整篇硬塞。这也让风险定位能精确到条款号一举两得。6. 部署落地与验证从能跑到敢给业务用6.1 量化加载与本地服务化微调和验证阶段的代码直接服务业务不行Trainer 带着训练逻辑启动慢、内存开销大。部署时我一般用两种方式。第一种是 bitsandbytes 4bit 量化加载能跑但吞吐有瓶颈第二种是 vLLM它兼容 OpenAI 的 API 格式启动一个本地服务后业务系统直接用 HTTP 调chat/completions接口改造量最小。显存够 48GB 的直接 vLLM16~24GB 的先用 4bit 量化顶着。6.2 用已知风险的合同做回归验证部署前最重要的一课拿 20 份“已知风险点”的合同做回归验证。让法务同事标注每份合同有哪些高风险条款然后跑一遍你的审查链路算检出率和误报率。检出率低于 80% 不要上线宁可先让法务在系统辅助下干活也别让模型直接出正式法律意见。RAG 检索质量也要单独测抽 100 个真实咨询问题人工确认检索回的前 5 条材料里有多少条真正相关低于 70% 就回去调切块大小和向量模型的选型。这套机制是整个系统最值钱的部分没有它模型看起来再聪明你也不敢真用。我把“先小规模跑通再追求规模”当成做这类本地法律大模型的铁律。第一次做别想着几万份合同一起训练先用 1000 条标注数据把链路跑通再逐步加量。我最大的教训是跳过质量评估直接扩充数据结果训练了两天指标反而变差。教训就一句话法律场景里评估方案必须和数据方案、微调方案同时设计。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。