0%

大模型应用系列(十九) 大模型RAG项目实战:法律条文助手--方案与数据

介绍RAG的一般实现流程,如何收集数据,处理数据,以及RAG的常见优化方向。

一. 方案

1.1 项目背景与需求设计
1.1.1 核心需求

场景: 法律条文智能问答系统,需满足:

  • 定期更新最新法律条文
  • 支持条款精准引用,如”《劳动法》第36条”
  • 处理复杂查询,如”劳动纠纷中的多条款关联分析”
1.1.2 技术选型: RAG vs 微调
对比维度 RAG方案 微调方案
数据更新频率 支持动态更新知识库 需重新标注并训练模型
内容准确性 直接引用原文,避免生成失真 依赖标注质量,易产生偏差
知识覆盖范围 适合大规模知识体系 需海量标注数据
可解释性 支持条款溯源,符合法律严谨性 黑盒模型,解释性差

重点: RAG在动态更新和可解释性上的优势

1.2 核心实现流程
1.2.1 流程图

image-20250725235004301

1.2.2 关键模块
  1. RAG检索层
    • 使用微调后的通用大模型(如劳动法领域适配模型),在实际中,一般先用通用大模型,如果发现效果不行,再微调。
    • 知识库构建: 结构化法律条文(json格式)
  2. 数据更新模块
    • 定时爬取政府官网最新法规(自动化获取)
    • 自动化解析条款(正则匹配)

重点: RAG与领域微调的结合策略。

1.3 数据收集与整理

法律条文一般比较规范化,律师在回答法律问题时,一般会说,”根据《xx法律》第xx条…”,所以我们希望知识库中法律条文每条就是一个node,可以通过网络爬虫和数据自动化处理来实现,这里爬取的是 中华人民共和国劳动法_中国人大网 ,爬虫脚本需要根据数据网页结构进行调整。主要分为两步: 数据爬取(bs4)和文档切分(正则匹配),代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import json
import re
import requests
from bs4 import BeautifulSoup

def fetch_and_parse(url):
# 请求网页
response = requests.get(url)
# 设置网页编码格式
response.encoding = 'utf-8'
# 解析网页内容
soup = BeautifulSoup(response.text, 'html.parser')
# 提取正文内容
content = soup.find_all('p')
# 初始化存储数据
data = []
# 提取文本并格式化
for para in content:
text = para.get_text(strip=True)
if text: # 只处理非空文本
# 根据需求格式化内容
data.append(text)
# 将data列表转换为字符串
data_str = '\n'.join(data)
return data_str

def extract_law_articles(data_str):
# 正则表达式,匹配每个条款号及其内容
pattern = re.compile(r'第([一二三四五六七八九十零百]+)条.*?(?=\n第|$)', re.DOTALL)
# 初始化字典来存储条款号和内容
lawarticles = {}
# 搜索所有匹配项
for match in pattern.finditer(data_str):
articlenumber = match.group(1)
articlecontent = match.group(0).replace('第' + articlenumber + '条', '').strip()
lawarticles[f"中华人民共和国劳动法 第{articlenumber}条"] = articlecontent
# 转换字典为JSON字符串
jsonstr = json.dumps(lawarticles, ensure_ascii=False, indent=4)
return [jsonstr]

if __name__ == '__main__':
# 请求页面
url = "http://www.npc.gov.cn/npc/c2/c30834/201905/t20190521_296651.html"
data_str = fetch_and_parse(url)
json_str = extract_law_articles(data_str)
with open('data.json', 'w', encoding='utf-8') as file:
json.dump(json_str, file, ensure_ascii=False, indent=4)

经过整理,最终数据格式如下:

1
2
3
4
5
6
7
[{
"中华人民共和国劳动法 第一百零四条": "国家工作人员和社会保险基金经办机构的工作人员挪用社会保险基金,构成犯罪的,依法追究刑事责任。",
"中华人民共和国劳动法 第一百零五条": "违反本法规定侵害劳动者合法权益,其他法律、行政法规已规定处罚的,依照该法律、行政法规的规定处罚。",
"中华人民共和国劳动法 第一百零六条": "省、自治区、直辖市人民政府根据本法和本地区的实际情况,规定劳动合同制度的实施步骤,报国务院备案。",
"中华人民共和国劳动法 第一百零七条": "本法自1995年1月1日起施行。",
...
}]

这样就可以将每个键值对划分为一个node,这样LLM看到一个node就是一个条款。

重点: 正则表达式设计与结构化存储。

1.4 Lora微调优化
1.4.1 微调场景

RAG的效果取决于两个方面:1.给的文档素材是否正确(数据收集与整理);2.LLM本身能力(微调)。

知识库检索之前还有一步是大模型理解问题,所以大模型能否正确理解问题会极大影响最终效果,对于法律条文助手,大模型的理解能力已经很不错,可能不需要微调,但对于一些小众领域,可能需要提升大模型的理解能力,这时就要通过微调实现。

1.4.2 微调步骤
  1. 准备少量高质量问答数据, 格式如下:

    1
    2
    {"question": "劳动合同解除的法定条件是什么?",
    "answer": "《劳动合同法》第36条规定..."}
  2. Lora微调通用模型,得到领域适配模型。

重点: 小样本微调和RAG协同优化

1.5 加载数据

使用 llama_index加载数据集,将文档嵌入并生成Embedding代码及解释如下:

运行过程中会报错缺少某些包, 直接到官网搜索该包名称会提供下载方法:LlamaIndex - LlamaIndex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# -*- coding: utf-8 -*-
import json
import time
from pathlib import Path
from typing import List, Dict

import chromadb
from llama_index.core import VectorStoreIndex, StorageContext, Settings
from llama_index.core.schema import TextNode
from llama_index.llms.huggingface import HuggingFaceLLM
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.vector_stores.chroma import ChromaVectorStore


# ================== 配置区 ==================
# 需要修xu
class Config:
# 模型路径
EMBED_MODEL_PATH = r"/home/cw/llms/embedding_model/sungw111/text2vec-base-chinese-sentence"
LLM_MODEL_PATH = r"/home/cw/llms/Qwen/Qwen1___5-1___8B-Chat"
# 存放json文件的文件夹路径
# 向量数据库路径
DATA_DIR = "/home/cw/projects/demo_22/data"
VECTOR_DB_DIR = "/home/cw/projects/demo_22/chroma_db"
PERSIST_DIR = "/home/cw/projects/demo_22/storage"

COLLECTION_NAME = "chinese_labor_laws"
TOP_K = 3

# ================== 初始化模型 ==================
def init_models():
"""初始化模型并验证"""
# Embedding模型
embed_model = HuggingFaceEmbedding(
model_name=Config.EMBED_MODEL_PATH,
# encode_kwargs = {
# 'normalize_embeddings': True,
# 'device': 'cuda' if hasattr(Settings, 'device') else 'cpu'
# }
)



Settings.embed_model = embed_model


# 验证模型
test_embedding = embed_model.get_text_embedding("测试文本")
print(f"Embedding维度验证:{len(test_embedding)}")

return embed_model

# ================== 数据处理 ==================
# 处理data_dir下的所有json文件, 每个json文件都是一个字典列表,类似
# [{
# "中华人民共和国劳动法 第一百零六条": "省、自治区、直辖市人民政府根据本法和本地区的实际情况,规定劳动合同制度的实施步骤,报国务院备案。",
# "中华人民共和国劳动法 第一百零七条": "本法自1995年1月1日起施行。"
# }]
def load_and_validate_json_files(data_dir: str) -> List[Dict]:
"""加载并验证JSON法律文件"""
json_files = list(Path(data_dir).glob("*.json"))
assert json_files, f"未找到JSON文件于 {data_dir}"

all_data = []
for json_file in json_files:
with open(json_file, 'r', encoding='utf-8') as f:
try:
data = json.load(f)
# 验证数据结构
if not isinstance(data, list):
raise ValueError(f"文件 {json_file.name} 根元素应为列表")
for item in data:
if not isinstance(item, dict):
raise ValueError(f"文件 {json_file.name} 包含非字典元素")
for k, v in item.items():
if not isinstance(v, str):
raise ValueError(f"文件 {json_file.name} 中键 '{k}' 的值不是字符串")
all_data.extend({
"content": item, # item是文件中的字典列表
"metadata": {"source": json_file.name} # 把文件名称作为元数据, 所以这里文件名取为中华人民共和国劳动法(法律名称)
} for item in data)
except Exception as e:
raise RuntimeError(f"加载文件 {json_file} 失败: {str(e)}")

print(f"成功加载 {len(all_data)} 个法律文件条目")
return all_data

def create_nodes(raw_data: List[Dict]) -> List[TextNode]:
"""添加ID稳定性保障"""
nodes = []
for entry in raw_data:
law_dict = entry["content"] # 取出所有字典
source_file = entry["metadata"]["source"] # 文件名
for full_title, content in law_dict.items():
# 生成稳定ID(避免重复)
node_id = f"{source_file}::{full_title}" # 中华人民共和国劳动法::第一百零六条

parts = full_title.split(" ", 1)
law_name = parts[0] if len(parts) > 0 else "未知法律"
article = parts[1] if len(parts) > 1 else "未知条款"
# 每个法律条款生成一个节点
node = TextNode(
text=content,
id_=node_id, # 显式设置稳定ID
metadata={
"law_name": law_name,
"article": article,
"full_title": full_title,
"source_file": source_file,
"content_type": "legal_article"
}
)
nodes.append(node)

print(f"生成 {len(nodes)} 个文本节点(ID示例:{nodes[0].id_})")
return nodes

# ================== 向量存储 ==================

def init_vector_store(nodes: List[TextNode]) -> VectorStoreIndex:
chroma_client = chromadb.PersistentClient(path=Config.VECTOR_DB_DIR) # 持久化存储
chroma_collection = chroma_client.get_or_create_collection(
name=Config.COLLECTION_NAME,
metadata={"hnsw:space": "cosine"}
)

# 确保存储上下文正确初始化
storage_context = StorageContext.from_defaults(
vector_store=ChromaVectorStore(chroma_collection=chroma_collection)
)

# 判断是否需要新建索引, 如果本地有数据库,就直接加载数据库,否则重新嵌入
if chroma_collection.count() == 0 and nodes is not None:
print(f"创建新索引({len(nodes)}个节点)...")

# 显式将节点添加到存储上下文
storage_context.docstore.add_documents(nodes)

index = VectorStoreIndex(
nodes,
storage_context=storage_context,
show_progress=True
)
# 双重持久化保障
storage_context.persist(persist_dir=Config.PERSIST_DIR) # 存文档,用于人看,RAG用不到
index.storage_context.persist(persist_dir=Config.PERSIST_DIR) # <-- 新增
else:
print("加载已有索引...")
storage_context = StorageContext.from_defaults(
persist_dir=Config.PERSIST_DIR,
vector_store=ChromaVectorStore(chroma_collection=chroma_collection)
)
index = VectorStoreIndex.from_vector_store(
storage_context.vector_store,
storage_context=storage_context,
embed_model=Settings.embed_model
)

# 安全验证
print("\n存储验证结果:")
doc_count = len(storage_context.docstore.docs)
print(f"DocStore记录数:{doc_count}")

if doc_count > 0:
sample_key = next(iter(storage_context.docstore.docs.keys()))
print(f"示例节点ID:{sample_key}")
else:
print("警告:文档存储为空,请检查节点添加逻辑!")


return index

# ================== 主程序 ==================
def main():
embed_model = init_models()

# 仅当需要更新数据时执行
if not Path(Config.VECTOR_DB_DIR).exists():
print("\n初始化数据...")
raw_data = load_and_validate_json_files(Config.DATA_DIR)
nodes = create_nodes(raw_data)
else:
nodes = None # 已有数据时不加载

print("\n初始化向量存储...")
start_time = time.time()
index = init_vector_store(nodes)
print(f"索引加载耗时:{time.time()-start_time:.2f}s")

if __name__ == "__main__":
main()

这里只建了数据库,还没有查询,所以没有加载大模型。

结果如下: 说明向量数据库创建成功

image-20250723231041824

如果您读文章后有收获,可以打赏我喝咖啡哦~