在尝试将大模型应用于法律领域时,你是否遇到过这样的困境:模型对专业法律术语理解不深,回答流于表面,甚至“一本正经地胡说八道”?或者,想针对特定法律任务微调模型,却苦于数据量小、算力有限,不知从何下手?

本文将以“刑法大模型”为实战目标,手把手带你打通从零到一的全链路。我们将融合 Qwen(通义千问) 作为基座模型,利用 RAG(检索增强生成) 注入精准的法律知识库,并通过 LoRA(低秩适应) 进行轻量化微调,最终实现 罪名识别、刑期预测、司法解释生成 三大核心功能。整个过程不仅包含清晰的代码和配置,更会以“学长带debug”的视角,剖析每一步可能遇到的坑及其解决方案,确保你能成功复现并应用于自己的领域。

无论你是想入门大模型应用开发的学生,还是希望将AI能力落地到垂直领域的开发者,这篇涵盖环境搭建、数据处理、模型训练与部署的完整教程,都将为你提供一套可直接复用的工程化方案。

1. 项目背景与核心技术栈解析

在深入代码之前,我们有必要厘清这个项目要解决的核心问题,以及为什么选择Qwen、RAG、LoRA这三项技术。

1.1 为什么需要“刑法大模型”?

通用大模型(如ChatGPT、通义千问)在开放域对话上表现卓越,但在法律、医疗等高度专业化、强逻辑、重事实的垂直领域,往往存在以下短板:

  1. 知识幻觉 :可能编造不存在的法条或司法解释。
  2. 精度不足 :对相似罪名(如“盗窃罪”与“侵占罪”)的区分能力弱。
  3. 缺乏推理 :刑期预测需要综合考量情节、数额、悔罪表现等多重因素,通用模型难以进行量化推理。
  4. 知识更新滞后 :模型训练数据有截止日期,无法即时获取最新的法律修订和判例。

因此,我们需要一个专精于刑法领域的“专家模型”。

1.2 技术选型:Qwen + RAG + LoRA

我们采用“基座模型 + 知识增强 + 轻量微调”的三层架构,在效果、成本与可行性间取得平衡。

  • 基座模型:Qwen(通义千问)

    • 为什么选它? Qwen系列模型由阿里云开源,具有优秀的中文理解与生成能力,对中文法律文本友好。其开源协议宽松,支持商用,且提供了从1.8B到72B的不同规模版本,便于根据算力选择。本文将以 Qwen2.5-7B-Instruct 版本为例,它在指令跟随和推理上表现更佳。
    • 作用 :提供强大的语言理解和生成基础能力。
  • 知识增强:RAG(检索增强生成)

    • 是什么? RAG通过外挂一个知识库(如法律条文、判例文书)来弥补大模型内部知识的不足或过时。其工作流程为:用户提问 -> 从知识库中检索相关文档 -> 将文档作为上下文与大模型问题一并输入 -> 模型生成基于可靠知识的答案。
    • 为什么需要? 确保模型回答的法律依据(如刑法第几条)绝对准确、可追溯,杜绝知识幻觉。对于“根据XX法,盗窃罪如何量刑?”这类问题,RAG能直接检索出法条原文作为生成依据。
    • 技术组件 :通常包含文档加载器、文本分割器、向量数据库(如ChromaDB, FAISS)、嵌入模型(如BGE)和检索链。
  • 能力微调:LoRA(低秩适应)

    • 是什么? LoRA是一种高效的微调技术。它不在整个庞大的模型参数上做调整,而是通过注入额外的、秩很低的矩阵来模拟全参数微调的效果。这些新增的参数远小于原模型,训练速度快,显存占用低。
    • 为什么需要? 为了让模型学会“刑法领域”特有的任务格式和推理模式。例如,教会模型按照“案情描述 -> 罪名分析 -> 法条引用 -> 刑期预测”的结构化方式输出。仅靠RAG提供知识,模型可能不会以我们想要的格式回答。LoRA就是用来调整模型“行为模式”的。
    • 优势 :轻量、高效、可插拔。训练好的LoRA权重文件很小(通常几十到几百MB),可以轻松加载到基座模型上,实现能力增强。

三者关系总结 Qwen 是大脑, RAG 是随时可查阅的精准法律图书馆, LoRA 是让大脑更擅长处理法律考题的特训课程。三者结合,才能打造出既知识准确又擅长法律推理的专用模型。

2. 环境准备与项目搭建

工欲善其事,必先利其器。我们先搭建一个清晰、可复现的开发环境。

2.1 硬件与软件要求

  • 操作系统 :Linux (Ubuntu 20.04/22.04) 或 Windows (WSL2)。本文以 Ubuntu 22.04 为例。
  • GPU :强烈推荐使用 NVIDIA GPU 进行微调和推理。训练LoRA,RTX 3090 (24GB) 或 RTX 4090 (24GB) 是较好的起点。仅推理可使用显存更小的GPU或CPU(速度较慢)。
  • Python :3.9 或 3.10。
  • CUDA :>= 11.8(与PyTorch版本匹配)。

2.2 创建虚拟环境与安装依赖

首先,创建一个独立的Python环境以避免包冲突。

# 创建并激活虚拟环境
conda create -n law_llm python=3.10 -y
conda activate law_llm

# 或使用 venv
# python -m venv law_llm
# source law_llm/bin/activate  # Linux
# law_llm\Scripts\activate  # Windows

接下来,安装核心依赖。我们将使用 transformers , peft (用于LoRA), accelerate , langchain chromadb

# 安装PyTorch (请根据你的CUDA版本访问 https://pytorch.org/ 获取最新命令)
# 例如,对于 CUDA 11.8
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

# 安装 transformers, datasets, peft, accelerate (用于模型加载和训练)
pip install transformers datasets peft accelerate

# 安装 langchain 及相关组件 (用于构建RAG)
pip install langchain langchain-community langchain-chroma

# 安装向量数据库和嵌入模型
pip install chromadb sentence-transformers

# 安装其他工具库
pip install jupyter pandas tqdm

2.3 项目目录结构

一个清晰的项目结构有助于管理代码、数据和模型。

law_llm_project/
├── data/                    # 存放原始和预处理后的数据
│   ├── raw/                # 原始法律文本(刑法条文、司法解释等)
│   ├── processed/          # 处理后的数据(用于RAG和微调)
│   └── dataset/            # 微调任务数据集
├── knowledge_base/         # RAG知识库存储(ChromaDB持久化路径)
├── scripts/                # 执行脚本
│   ├── build_kb.py         # 构建知识库脚本
│   ├── train_lora.py       # LoRA微调脚本
│   └── inference.py        # 推理演示脚本
├── models/                 # 存放下载的基座模型和训练好的LoRA权重
│   ├── Qwen2.5-7B-Instruct/
│   └── lora_law/
├── config/                 # 配置文件
│   └── lora_config.json
├── requirements.txt        # 项目依赖
└── README.md

你可以使用以下命令快速创建这个结构:

mkdir -p law_llm_project/{data/{raw,processed,dataset},knowledge_base,scripts,models,config}
cd law_llm_project

3. 构建法律知识库(RAG核心)

RAG的效果高度依赖于知识库的质量。我们的知识库需要包含《中华人民共和国刑法》条文、重要的司法解释(如关于盗窃、诈骗等罪的数额认定标准)以及典型的指导案例摘要。

3.1 数据收集与预处理

步骤1:准备原始文本 将刑法全文(可从权威法律网站获取)保存为 data/raw/criminal_law.txt 。每一条文最好单独一行或用一个分隔符隔开。 示例 criminal_law.txt 内容格式:

第二百六十四条 【盗窃罪】盗窃公私财物,数额较大的,或者多次盗窃、入户盗窃、携带凶器盗窃、扒窃的,处三年以下有期徒刑、拘役或者管制,并处或者单处罚金;数额巨大或者有其他严重情节的,处三年以上十年以下有期徒刑,并处罚金;数额特别巨大或者有其他特别严重情节的,处十年以上有期徒刑或者无期徒刑,并处罚金或者没收财产。
第二百六十六条 【诈骗罪】诈骗公私财物,数额较大的,处三年以下有期徒刑、拘役或者管制,并处或者单处罚金;数额巨大或者有其他严重情节的,处三年以上十年以下有期徒刑,并处罚金;数额特别巨大或者有其他特别严重情节的,处十年以上有期徒刑或者无期徒刑,并处罚金或者没收财产。
...

步骤2:编写知识库构建脚本 创建 scripts/build_kb.py

# scripts/build_kb.py
import os
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.docstore.document import Document

def build_knowledge_base():
    # 1. 加载法律文本
    raw_path = "./data/raw/criminal_law.txt"
    if not os.path.exists(raw_path):
        print(f"错误:未找到原始法律文件 {raw_path}")
        return
    
    loader = TextLoader(raw_path, encoding='utf-8')
    documents = loader.load()
    
    # 2. 分割文本
    # 法律条文本身有结构,适合按章节或条文分割,这里使用递归字符分割作为备选
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=500,  # 每个片段约500字符
        chunk_overlap=50, # 片段间重叠50字符,保持上下文
        separators=["\n\n", "\n", "。", ";", ",", " ", ""] # 中文分隔符优先
    )
    splits = text_splitter.split_documents(documents)
    print(f"原始文档分割为 {len(splits)} 个片段。")
    
    # 3. 选择嵌入模型 (使用轻量且效果好的中文模型)
    # 也可以使用 OpenAI API,但本地部署推荐开源模型
    model_name = "BAAI/bge-small-zh-v1.5" # 中文嵌入模型,效果不错且速度快
    embeddings = HuggingFaceEmbeddings(
        model_name=model_name,
        model_kwargs={'device': 'cuda'}, # 使用GPU加速
        encode_kwargs={'normalize_embeddings': True} # 归一化,提升检索效果
    )
    
    # 4. 创建并持久化向量数据库
    persist_directory = "./knowledge_base/chroma_law"
    vectordb = Chroma.from_documents(
        documents=splits,
        embedding=embeddings,
        persist_directory=persist_directory
    )
    vectordb.persist() # 持久化到磁盘
    print(f"知识库已构建并保存至 {persist_directory}")
    
    # 5. 简单测试检索
    query = "盗窃罪如何量刑?"
    docs = vectordb.similarity_search(query, k=2)
    print(f"\n测试检索问题:'{query}'")
    for i, doc in enumerate(docs):
        print(f"\n--- 相关片段 {i+1} ---")
        print(doc.page_content[:200] + "...") # 打印前200字符

if __name__ == "__main__":
    build_knowledge_base()

步骤3:运行脚本构建知识库

cd law_llm_project
python scripts/build_kb.py

如果一切顺利,你会看到知识库构建完成,并输出与“盗窃罪”相关的法条片段。

3.2 Debug 要点:知识库构建常见问题

  • 问题1: No module named 'langchain_community'

    • 原因 :LangChain 版本更新,部分模块路径发生变化。
    • 解决 :确保安装的是较新版本 pip install langchain-community 。如果还报错,可以尝试 from langchain.embeddings import HuggingFaceEmbeddings (旧路径),但建议使用新路径以保持兼容性。
  • 问题2:嵌入模型下载慢或失败

    • 原因 :从Hugging Face下载模型可能受网络影响。
    • 解决
      1. 使用国内镜像源: export HF_ENDPOINT=https://hf-mirror.com (Linux) 或设置环境变量。
      2. 或者,先手动下载模型到本地,然后修改 model_name 为本地路径。
      # 假设模型下载到了 ./models/bge-small-zh
      model_name = "./models/bge-small-zh"
      
  • 问题3:检索结果不相关

    • 原因 :文本分割策略不当或嵌入模型不适合中文法律文本。
    • 解决
      1. 调整分割器 :尝试 CharacterTextSplitter 或按特定分隔符(如“第xxx条”)分割。
      2. 尝试其他嵌入模型 :如 moka-ai/m3e-base 在中文语义搜索上表现也很好。
      3. 检查原始文本质量 :确保文本清晰,没有乱码或多余格式。

4. 准备LoRA微调数据集

为了让模型学会执行“罪名识别”、“刑期预测”等特定任务,我们需要准备指令微调数据集。数据格式通常为 instruction-input-output

4.1 数据集格式设计

我们创建一个合成数据集。在实际项目中,应使用更高质量的标注数据。

创建 data/dataset/train.jsonl (JSON Lines格式),每条数据如下:

{
  "instruction": "请根据以下案情描述,分析可能构成的罪名,并预测可能的刑期范围。请以'罪名:','法条依据:','刑期预测:'的格式回答。",
  "input": "张三于2023年5月夜间,潜入某小区多户居民家中,窃取手机、笔记本电脑等财物,总计价值人民币5万元。归案后,张三如实供述了自己的罪行,并积极退赔了部分赃款。",
  "output": "罪名:盗窃罪。\n法条依据:《中华人民共和国刑法》第二百六十四条。盗窃公私财物,数额巨大的,处三年以上十年以下有期徒刑,并处罚金。本案盗窃数额为5万元,根据相关司法解释,属于‘数额巨大’范畴。\n刑期预测:考虑到张三有入户盗窃情节(法定加重情节),但具有如实供述、部分退赔等酌定从轻情节,综合预测刑期可能在四年至六年有期徒刑之间,并处罚金。"
}
{
  "instruction": "请生成关于‘抢劫罪’中‘携带凶器抢夺’认定的司法解释摘要。",
  "input": "",
  "output": "根据《最高人民法院关于审理抢劫刑事案件适用法律若干问题的解释》第六条规定:携带凶器抢夺,是指行为人随身携带枪支、爆炸物、管制刀具等国家禁止个人携带的器械进行抢夺,或者为了实施犯罪而携带其他器械进行抢夺的行为。行为人将随身携带凶器有意加以显示、能为被害人察觉到的,直接适用刑法第二百六十三条的规定定罪处罚(即认定为抢劫罪)。"
}

你需要准备数百到数千条这样的高质量数据,涵盖多种罪名、情节和任务类型。

4.2 数据集加载脚本

创建 scripts/prepare_data.py 来加载和格式化数据,供训练使用。

# scripts/prepare_data.py
import json
from datasets import Dataset

def load_and_format_data(file_path):
    instructions = []
    inputs = []
    outputs = []
    
    with open(file_path, 'r', encoding='utf-8') as f:
        for line in f:
            data = json.loads(line.strip())
            instructions.append(data['instruction'])
            inputs.append(data['input'])
            outputs.append(data['output'])
    
    # 构建模型输入格式:将instruction和input组合成prompt
    prompts = []
    for instr, inp in zip(instructions, inputs):
        if inp:
            prompt = f"{instr}\n\n{inp}"
        else:
            prompt = instr
        prompts.append(prompt)
    
    # 创建 Hugging Face Dataset 对象
    dataset_dict = {
        'prompt': prompts,
        'completion': outputs
    }
    dataset = Dataset.from_dict(dataset_dict)
    return dataset

if __name__ == "__main__":
    train_dataset = load_and_format_data('./data/dataset/train.jsonl')
    print(f"训练集样本数:{len(train_dataset)}")
    print(train_dataset[0])

5. LoRA微调Qwen模型

这是项目的核心环节。我们将使用 peft transformers 库来微调 Qwen2.5-7B-Instruct 模型。

5.1 下载基座模型

建议使用 huggingface-cli snapshot_download 下载模型。确保你有足够的磁盘空间(约15GB)。

# 方法1:使用 huggingface-cli (需先登录 huggingface-cli login)
pip install huggingface-hub
huggingface-cli download Qwen/Qwen2.5-7B-Instruct --local-dir ./models/Qwen2.5-7B-Instruct

# 方法2:在Python代码中下载(如下一步脚本所示)

5.2 编写LoRA微调脚本

创建 scripts/train_lora.py 。这是一个完整的训练脚本,包含了模型加载、LoRA配置、训练参数设置和训练循环。

# scripts/train_lora.py
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, Trainer
from peft import LoraConfig, get_peft_model, TaskType
from datasets import load_from_disk
import os
from huggingface_hub import snapshot_download

# 1. 模型与分词器加载
model_name_or_path = "Qwen/Qwen2.5-7B-Instruct"
local_model_path = "./models/Qwen2.5-7B-Instruct"

# 如果本地没有模型,则下载
if not os.path.exists(local_model_path):
    print(f"本地未找到模型,正在从Hugging Face下载 {model_name_or_path} ...")
    snapshot_download(repo_id=model_name_or_path, local_dir=local_model_path)
    print("下载完成。")

print("加载分词器...")
tokenizer = AutoTokenizer.from_pretrained(local_model_path, trust_remote_code=True)
# Qwen模型需要设置padding_side,通常为'left'用于生成
tokenizer.padding_side = 'left'
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token  # 设置pad token

print("加载模型...")
model = AutoModelForCausalLM.from_pretrained(
    local_model_path,
    torch_dtype=torch.bfloat16,  # 使用BF16节省显存并保持精度
    device_map="auto",           # 自动分配模型层到可用设备(GPU/CPU)
    trust_remote_code=True
)

# 2. 配置LoRA
lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,  # 因果语言模型任务
    r=8,                           # LoRA秩,影响参数量,通常8,16,32
    lora_alpha=32,                 # 缩放因子
    lora_dropout=0.1,              # Dropout概率,防止过拟合
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], # 在QKV和输出投影层添加LoRA
    bias="none",                   # 是否训练偏置
)

# 将原模型转换为PeftModel,仅训练LoRA参数
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()  # 打印可训练参数量,应该只占原模型很小一部分

# 3. 数据准备
from prepare_data import load_and_format_data  # 导入之前写的数据处理函数

train_dataset = load_and_format_data('./data/dataset/train.jsonl')
# 假设我们划分一小部分作为验证集
split_dataset = train_dataset.train_test_split(test_size=0.1, seed=42)
train_data = split_dataset['train']
eval_data = split_dataset['test']

# 数据预处理函数:将文本转换为模型输入所需的token IDs
def tokenize_function(examples):
    # 将prompt和completion拼接,中间加一个分隔符(如\n\nAssistant:)
    # 对于Qwen Instruct模型,通常格式为:<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n{completion}<|im_end|>
    # 这里我们简化处理,直接拼接。更严谨的做法是使用模型特定的chat template。
    texts = [p + "\n\n" + c for p, c in zip(examples['prompt'], examples['completion'])]
    # 对拼接后的文本进行tokenize
    tokenized = tokenizer(texts, truncation=True, padding="max_length", max_length=512)
    # 将标签设置为与输入相同(用于计算损失),但需要忽略掉prompt部分的损失
    labels = tokenized["input_ids"].copy()
    # 假设我们只想计算completion部分的损失,需要将prompt部分的标签设为-100
    # 这里简化处理,实际应根据模型模板和prompt长度精确计算
    # 这是一个需要根据实际情况调整的关键点!
    prompt_lengths = [len(tokenizer.encode(p, truncation=True, max_length=512)) for p in examples['prompt']]
    for i, label in enumerate(labels):
        prompt_len = prompt_lengths[i]
        label[:prompt_len] = [-100] * prompt_len  # 将prompt部分的标签设为-100(忽略损失)
    tokenized["labels"] = labels
    return tokenized

tokenized_train = train_data.map(tokenize_function, batched=True)
tokenized_eval = eval_data.map(tokenize_function, batched=True)

# 4. 设置训练参数
training_args = TrainingArguments(
    output_dir="./models/lora_law",      # 输出目录
    num_train_epochs=3,                  # 训练轮数
    per_device_train_batch_size=2,       # 每个设备的训练批次大小,根据GPU显存调整
    per_device_eval_batch_size=2,
    gradient_accumulation_steps=4,       # 梯度累积步数,模拟更大batch size
    warmup_steps=100,                    # 学习率预热步数
    logging_steps=10,                    # 每多少步打印一次日志
    eval_steps=50,                       # 每多少步评估一次
    save_steps=200,                      # 每多少步保存一次检查点
    evaluation_strategy="steps",
    save_strategy="steps",
    load_best_model_at_end=True,         # 训练结束后加载最佳模型
    metric_for_best_model="eval_loss",   # 根据评估损失选择最佳模型
    greater_is_better=False,
    learning_rate=2e-4,                  # 学习率,LoRA通常可以设大一点
    fp16=True,                           # 使用混合精度训练,节省显存
    # bf16=True,                         # 如果GPU支持BF16,优先用BF16
    gradient_checkpointing=True,         # 梯度检查点,用时间换显存
    optim="paged_adamw_8bit",            # 使用8-bit优化器,进一步节省显存
    report_to="tensorboard",             # 可选:记录到tensorboard
    save_total_limit=3,                  # 最多保存3个检查点
)

# 5. 创建Trainer并开始训练
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_train,
    eval_dataset=tokenized_eval,
    tokenizer=tokenizer,
    # data_collator=..., # 可以使用默认的
)

print("开始训练...")
trainer.train()

# 6. 保存LoRA权重
print("训练完成,保存LoRA适配器...")
model.save_pretrained("./models/lora_law_final")
tokenizer.save_pretrained("./models/lora_law_final")
print("LoRA权重已保存至 ./models/lora_law_final")

5.3 Debug 要点:LoRA训练常见坑

  • 问题1:CUDA Out Of Memory (OOM)

    • 原因 :模型、批次大小、序列长度超出GPU显存。
    • 解决
      1. 减小 per_device_train_batch_size
      2. 启用梯度检查点 ( gradient_checkpointing=True )。
      3. 使用更低的精度 ( fp16=True bf16=True )。
      4. 使用 optim="paged_adamw_8bit" adamw_bnb_8bit 优化器。
      5. 减小 max_length (如从512降到256)。
      6. 使用 device_map="auto" accelerate 自动将部分层卸载到CPU或磁盘(速度会慢)。
  • 问题2:损失不下降或输出乱码

    • 原因
      1. 学习率不合适 :LoRA学习率通常可以设得比全参数微调大(如1e-4到5e-4)。
      2. 数据格式错误 :模型没有正确理解指令格式。 这是最常见的问题!
      3. 标签掩码错误 :在 tokenize_function 中,没有正确地将prompt部分的损失忽略(标签设为-100)。
    • 解决
      1. 调整学习率。
      2. 严格按照基座模型要求的对话模板格式化数据 。对于Qwen Instruct,应使用其 apply_chat_template 方法。修改数据预处理部分:
      from transformers import AutoTokenizer
      tokenizer = AutoTokenizer.from_pretrained(local_model_path, trust_remote_code=True)
      # 使用模型的chat template
      messages = [
          {"role": "user", "content": prompt},
          {"role": "assistant", "content": completion}
      ]
      text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=False)
      tokenized = tokenizer(text, ...)
      
      1. 仔细检查并修正标签掩码逻辑。
  • 问题3: RuntimeError: expected scalar type Float but found Half

    • 原因 :混合精度训练时数据类型不匹配。
    • 解决 :确保模型加载时指定了 torch_dtype (如 torch.float16 torch.bfloat16 ),并与 TrainingArguments 中的 fp16/bf16 设置一致。

6. 整合推理:RAG + LoRA Qwen

训练完成后,我们将加载基础模型、LoRA权重以及RAG知识库,构建一个完整的推理管道。

6.1 推理脚本编写

创建 scripts/inference.py

# scripts/inference.py
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
from peft import PeftModel, PeftConfig
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings

class LawRAGLoRAAssistant:
    def __init__(self, base_model_path, lora_model_path, knowledge_base_path):
        # 1. 加载模型和分词器 (基座 + LoRA)
        print("加载基座模型和分词器...")
        self.tokenizer = AutoTokenizer.from_pretrained(base_model_path, trust_remote_code=True)
        self.tokenizer.padding_side = 'left'
        if self.tokenizer.pad_token is None:
            self.tokenizer.pad_token = self.tokenizer.eos_token
            
        base_model = AutoModelForCausalLM.from_pretrained(
            base_model_path,
            torch_dtype=torch.bfloat16,
            device_map="auto",
            trust_remote_code=True
        )
        print("加载LoRA适配器...")
        self.model = PeftModel.from_pretrained(base_model, lora_model_path)
        self.model.eval()  # 设置为评估模式
        print("模型加载完成。")
        
        # 2. 加载RAG知识库
        print("加载法律知识库...")
        embeddings = HuggingFaceEmbeddings(
            model_name="BAAI/bge-small-zh-v1.5",
            model_kwargs={'device': 'cpu'}, # 嵌入模型可以放在CPU
            encode_kwargs={'normalize_embeddings': True}
        )
        self.vectordb = Chroma(
            persist_directory=knowledge_base_path,
            embedding_function=embeddings
        )
        self.retriever = self.vectordb.as_retriever(search_kwargs={"k": 3}) # 检索最相关的3个片段
        print("知识库加载完成。")
        
    def retrieve_context(self, query):
        """从知识库中检索相关上下文"""
        docs = self.retriever.get_relevant_documents(query)
        context = "\n\n".join([doc.page_content for doc in docs])
        return context
    
    def generate_answer(self, user_query, use_rag=True):
        """生成回答"""
        prompt = user_query
        
        if use_rag:
            # 检索增强
            context = self.retrieve_context(user_query)
            if context:
                # 构建包含上下文的提示词
                enhanced_prompt = f"""请基于以下提供的法律条文信息,回答用户的问题。如果信息不足以完全回答问题,你可以结合自身的法律知识进行补充,但请明确指出哪些信息来自提供的条文。

相关法律条文:
{context}

用户问题:
{user_query}

请给出专业、准确的分析:"""
                prompt = enhanced_prompt
            else:
                print("警告:未检索到相关法律条文。")
        
        # 使用模型的chat template构建输入
        messages = [{"role": "user", "content": prompt}]
        text = self.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
        
        # 将输入转换为tensor
        inputs = self.tokenizer(text, return_tensors="pt", padding=True).to(self.model.device)
        
        # 生成
        with torch.no_grad():
            outputs = self.model.generate(
                **inputs,
                max_new_tokens=512,        # 生成的最大token数
                temperature=0.7,           # 创造性,越低越确定
                top_p=0.9,                 # 核采样参数
                do_sample=True,
                repetition_penalty=1.1,     # 重复惩罚
                pad_token_id=self.tokenizer.pad_token_id,
                eos_token_id=self.tokenizer.eos_token_id,
            )
        
        # 解码输出,并去掉输入部分
        generated_ids = outputs[0][inputs['input_ids'].shape[1]:]
        response = self.tokenizer.decode(generated_ids, skip_special_tokens=True)
        
        return response
    
    def interactive_chat(self):
        """交互式对话"""
        print("\n=== 法律AI助手已启动 (输入 'quit' 退出) ===")
        print("模式:RAG增强 + LoRA微调模型")
        while True:
            try:
                user_input = input("\n您:")
                if user_input.lower() in ['quit', 'exit', 'q']:
                    print("再见!")
                    break
                if not user_input.strip():
                    continue
                    
                answer = self.generate_answer(user_input, use_rag=True)
                print(f"\n助手:{answer}")
            except KeyboardInterrupt:
                print("\n程序被中断。")
                break
            except Exception as e:
                print(f"\n生成时发生错误:{e}")

if __name__ == "__main__":
    # 路径配置
    BASE_MODEL_PATH = "./models/Qwen2.5-7B-Instruct"
    LORA_MODEL_PATH = "./models/lora_law_final"
    KB_PATH = "./knowledge_base/chroma_law"
    
    # 初始化助手
    assistant = LawRAGLoRAAssistant(BASE_MODEL_PATH, LORA_MODEL_PATH, KB_PATH)
    
    # 运行交互式对话
    assistant.interactive_chat()

6.2 运行与测试

运行推理脚本,开始与你的“刑法大模型”对话吧!

cd law_llm_project
python scripts/inference.py

你可以尝试提问:

  • “张三盗窃了价值3万元的财物,会判多久?”
  • “抢劫罪和抢夺罪有什么区别?”
  • “请解释一下什么是‘故意伤害罪致人重伤’的认定标准。”

观察模型的回答是否:

  1. 引用了正确的法条(来自RAG知识库)。
  2. 按照我们微调的结构化格式(罪名、法条依据、刑期预测)进行回答。
  3. 推理过程符合法律逻辑。

7. 常见问题与优化方向

7.1 项目运行问题排查清单

问题现象 可能原因 解决思路
导入LangChain模块失败 版本不兼容或未安装 langchain-community pip install langchain-community langchain-chroma
ChromaDB 无法持久化或读取 路径权限问题或版本冲突 检查路径是否存在且可写;尝试 pip install chromadb==0.4.22 (一个稳定版本)
模型下载极慢或失败 网络连接Hugging Face不畅 使用镜像源: export HF_ENDPOINT=https://hf-mirror.com
训练时GPU显存不足 批次大小、序列长度或模型太大 减小 per_device_train_batch_size max_length ;启用 gradient_checkpointing fp16 ;使用 optim=“paged_adamw_8bit”
模型生成无关内容或格式错误 1. 数据格式未按模型模板处理
2. LoRA训练不充分或过拟合
1. 使用 tokenizer.apply_chat_template 严格格式化数据
2. 检查训练损失曲线,增加数据量或调整超参(如学习率、dropout)
RAG检索结果不准确 1. 文本分割策略差
2. 嵌入模型不匹配
3. 检索top_k值不合适
1. 按法律条文自然结构(如“第X条”)分割
2. 尝试 moka-ai/m3e-base 等中文模型
3. 调整 search_kwargs={“k”: 3} 中的k值

7.2 效果优化与进阶方向

  1. 知识库优化

    • 多源数据 :加入司法解释、权威判例、学术观点,构建更立体的知识体系。
    • 混合检索 :结合关键词检索(BM25)和向量检索,提升召回率。
    • 重排序 :使用更精细的模型对检索结果进行重排序,提升top1精度。
  2. 微调策略优化

    • 更多任务数据 :收集更多高质量的(案情,判决)配对数据。
    • 数据格式 :尝试不同的指令模板,找到最适合Qwen模型的格式。
    • LoRA参数 :调整 r (秩)、 alpha target_modules ,甚至尝试 DoRA 等更先进的微调方法。
    • 全参数微调 :如果数据量和算力充足,可以对整个模型进行全参数微调,效果通常更好。
  3. 推理优化

    • 模型量化 :使用GPTQ、AWQ等技术将模型量化到4bit或8bit,大幅降低推理显存和加速。
    • 推理框架 :使用 vLLM TGI (Text Generation Inference) 等高性能推理框架部署,支持动态批处理,提高吞吐量。
    • 前端集成 :使用 Gradio Streamlit 快速构建Web界面,方便演示和测试。
  4. 工程化部署

    • API服务化 :使用 FastAPI 将模型封装成RESTful API。
    • 配置管理 :将模型路径、超参数等写入配置文件(如YAML)。
    • 日志与监控 :添加详细的日志记录和性能监控。

通过以上步骤,你不仅完成了一个融合RAG与LoRA的“刑法大模型”原型,更掌握了一套在垂直领域构建专业大模型应用的通用方法论。这套方法可以迁移到医疗、金融、教育等任何需要专业知识与复杂推理的领域。

更多推荐