法律文本结构化:从法律.rar到可检索知识库的工程实践
简介本资源是一个面向法律AI开发者与NLP初学者的智能法律问答系统完整工程包聚焦于构建中文法律领域问答应用解决法律知识检索、语义匹配与轻量级服务部署等实际问题。压缩包共25个文件包含4个核心Python脚本数据分析.py、app启动.py、main.py、句子相似匹配.py、1个40000条法律问答CSV数据集、1个Visio流程图.vsdx、1个预训练中文BERT模型chinese-bert-wwm-ext含bin/json/txt三件套、以及templates和static前端资源含HTML/JS/CSS/IMG等整体体积达371.77MB结构清晰前后端分离开箱即用。已有139人学习下载读者可直接复现法律问答服务从数据清洗与EDA分析到基于BERT的句子相似度匹配再到Flask/Django风格Web应用启动与界面渲染完整覆盖数据—模型—应用全链路开发实践。1. “法律.rar”不是文件名是工程起点它背后藏着法律文本结构化、可检索、能推理的第一道门你双击打开一个叫“法律.rar”的压缩包里面是几百个 Word、PDF 和 TXT 文件——《刑法》《民法典》《行政处罚法》《最高人民法院指导性案例汇编》……它们安静地躺着但没人能直接问“工伤认定中‘工作时间’的司法解释有哪些”或者“对比《数据安全法》和《个人信息保护法》对‘匿名化’的定义差异”。这不是资料缺失而是法律知识尚未完成数字化基建的关键卡点原始文档 ≠ 可计算资源。所谓“法律.rar”本质是法律领域最普遍、最沉默、也最亟待破局的非结构化文本资产池。它服务的不是法学院学生查法条而是律所知识管理系统自动标引案由、法院智能辅助生成裁判说理段落、企业合规平台实时比对合同条款与最新司法解释。本文不讲宏观意义只聚焦一线工程师每天面对的真实动作如何把一个未经清洗、格式混杂、语义嵌套的“法律.rar”变成可加载进向量数据库、支持语义检索、支撑 RAG 流程、经得起生产环境压测的法律知识基座。你会看到从解压、识别、切分到向量化落地的完整链路每一步都带参数依据、失败日志截图级复现路径以及我亲手踩过的三个“以为跑通了结果上线后召回率暴跌 40%”的坑。2. 解压与识别别跳过这步90% 的后续失败源于 PDF 文本提取失真法律类 PDF 是公认的“文本提取地狱”扫描件OCR 质量差、图文混排表格/脚注错位、多栏版式段落顺序颠倒、页眉页脚干扰把“第 12 条”误提成正文。直接unzip legal.rar后用pdfplumber或PyMuPDF粗暴提取等于给后续所有模型喂噪声。必须分三类处理2.1 判定 PDF 类型用pdfminer.high_level 规则引擎做预筛from pdfminer.high_level import extract_text import re def classify_pdf(pdf_path): try: # 尝试无 OCR 提取纯文本 PDF text extract_text(pdf_path, maxpages1) if len(text.strip()) 50 and 第 in text[:200]: # 含法条编号特征 return text_pdf except Exception: pass # 检查是否为扫描件基于 PDF 元数据 图像密度 from PyPDF2 import PdfReader reader PdfReader(pdf_path) if len(reader.pages) 0 and /Image in reader.pages[0].attrs.get(/Resources, {}): return scanned_pdf # 默认 fallback按文件名关键词启发式判断 name pdf_path.lower() if any(kw in name for kw in [guidance, interpretation, notice]): return official_doc return unknown # 示例调用 pdf_type classify_pdf(data/law/刑法修正案十一.pdf) print(f类型判定: {pdf_type}) # 输出: text_pdf / scanned_pdf / official_doc逻辑说明extract_text在纯文本 PDF 上极快且准确若失败或提取内容过少则触发扫描件检测逻辑。这里不用tesseract全量 OCR太慢而是先用 PyPDF2 检查/Image资源是否存在——这是扫描件最可靠的元数据标志。参数说明maxpages1避免全篇解析耗时len(text.strip()) 50过滤空白页第 in text[:200]是中文法律文本强特征法条起始标识。2.2 分类处理三套 pipeline 并行启动拒绝“一刀切”PDF 类型处理工具关键参数输出要求text_pdfpdfplumbervertical_strategylineshorizontal_strategytext保留表格结构脚注分离为独立块scanned_pdfPaddleOCRCPU 模式use_angle_clsFalse,det_db_box_thresh0.3输出 JSON 格式带坐标用于后续版面还原official_docunstructured 自定义规则strategyhi_res,skip_inferencesTrue强制启用标题层级识别如“一、”“一”“1.”# text_pdf 处理示例pdfplumber 保结构切分 import pdfplumber def parse_text_pdf(pdf_path): with pdfplumber.open(pdf_path) as pdf: full_text for page in pdf.pages: # 提取正文排除页眉页脚 bbox (72, 72, page.width - 72, page.height - 144) # 1英寸边距 cropped page.within_bbox(bbox) text cropped.extract_text( x_tolerance2, # 宽松横向合并 y_tolerance3, # 严格纵向分段 layoutTrue # 保留物理布局信息 ) if text: full_text text \n\n return full_text # scanned_pdf 处理示例PaddleOCR 输出结构化 JSON from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsFalse, langch, det_db_box_thresh0.3) def ocr_scanned_pdf(pdf_path): # PaddleOCR 不直接支持 PDF需先转为图像 from pdf2image import convert_from_path images convert_from_path(pdf_path, dpi200, fmtpng, thread_count2) results [] for img in images[:3]: # 仅前3页做样本验证避免全量耗时 result ocr.ocr(np.array(img), clsFalse) results.extend(result) return results # 返回 [ [[x1,y1,x2,y2], (文本, score)], ... ]为什么用 PaddleOCR 而非 Tesseract法律文本含大量小字号、加粗标题、括号嵌套如“一当事人……”Tesseract 的默认模型在中文复杂版式下字符粘连率超 35%而 PaddleOCR 的ch_PP-OCRv4模型在司法文书测试集上 CER字符错误率低至 2.1%。det_db_box_thresh0.3是关键降低检测框置信阈值避免漏检小字号法条编号。2.3 统一归一化法律文本特有的清洗规则必须硬编码提取后的文本仍含大量干扰页码“第 12 页” → 删除但保留“第十二条”页眉“《中华人民共和国刑法》2020 修正” → 仅保留首行标题脚注标记“¹” → 替换为[1]并提取脚注内容追加到文末法条编号混乱“第一条”“第一条”“第一条 ” → 统一为“第一条”import re def normalize_law_text(raw_text): # 删除页码独立成行且含“第.*页” text re.sub(r^第\d页$, , raw_text, flagsre.MULTILINE) # 清理页眉连续两行以上相同短文本且含书名号 lines text.split(\n) cleaned_lines [] for i, line in enumerate(lines): # 跳过空行和单字符行 if not line.strip() or len(line.strip()) 5: continue # 若当前行含书名号且与上下文重复视为页眉 if 《 in line and 》 in line and i 0 and i len(lines)-1: if line.strip() lines[i-1].strip() or line.strip() lines[i1].strip(): continue cleaned_lines.append(line) text \n.join(cleaned_lines) # 统一法条编号格式 text re.sub(r^(\s*第[零一二三四五六七八九十百千\d]条)[\s ]*, r**\1**, text, flagsre.MULTILINE) # 脚注标记标准化 text re.sub(r([①-⑩]|[0-9])\., r[\1], text) # “①.” → “[①]” return text.strip() # 示例 sample 第 12 页\n《民法典》\n第一条为了保护民事主体的合法权益……\n①. 本条所称…… print(normalize_law_text(sample)) # 输出 # **第一条** 为了保护民事主体的合法权益…… # [①] 本条所称……参数说明re.MULTILINE确保^\$匹配每行首尾\s*匹配任意空白包括全角空格 [\s ]*覆盖中文冒号、空格、全角空格三种常见分隔符。这个正则经过 217 份真实法律文本验证法条编号识别准确率达 99.2%。3. 结构化解析法律文本不是散文它的骨架必须被显式建模法律文档有强层级结构法律 → 章 → 节 → 条 → 款 → 项 → 目。若用通用文本切分器如RecursiveCharacterTextSplitter按固定 token 切会把“第三章 第二节 第二十二条”整个切碎导致检索时无法定位到“第二节”的上下文。必须构建法律专用切分器LawChunker其核心是识别标题模式并保持父子关系。3.1 标题层级识别用正则 有限状态机FSM捕获嵌套结构法律标题存在明确模式章第三章第一章 总则节第二节第二章 第一节条第二十二条第十二条 合同的订立款第一款一1.1项一1.1注意款和项常共用符号需结合上下文import re from typing import List, Dict, Optional class LawTitleParser: def __init__(self): # 预编译正则提升性能 self.patterns { chapter: r^(\s*第[零一二三四五六七八九十百千\d]章)[\s ]*([^\n]*), section: r^(\s*第[零一二三四五六七八九十百千\d]节)[\s ]*([^\n]*), article: r^(\s*第[零一二三四五六七八九十百千\d]条)[\s ]*([^\n]*), paragraph: r^(\s*第[零一二三四五六七八九十百千\d]款)[\s ]*([^\n]*), item: r^(\s*[一二三四五六七八九十\d]|[\d][\.、]|[\(][\d][\)])[\s ]*([^\n]*) } def parse_line(self, line: str) - Optional[Dict]: line line.strip() if not line: return None for level, pattern in self.patterns.items(): match re.match(pattern, line) if match: title match.group(1).strip() content match.group(2).strip() if len(match.groups()) 1 else return { level: level, title: title, content: content, raw: line } return None # 示例解析 parser LawTitleParser() test_line 一明知是伪造的货币而持有、使用数额较大的处三年以下有期徒刑…… result parser.parse_line(test_line) print(result) # 输出{level: item, title: 一, content: 明知是伪造的货币而持有、使用数额较大的处三年以下有期徒刑……, raw: 一明知是伪造的货币而持有、使用数额较大的处三年以下有期徒刑……}为什么不用 LLM 做标题识别实测表明即使使用 Qwen2-7B对“第十二条 合同的订立”这类短文本的分类准确率仅 86%且推理延迟达 320ms/条。而正则 FSM 在 CPU 上平均 0.8ms/条准确率 99.7%基于全国人大官网 128 份法律文本人工标注验证。法律文本的标题模式高度稳定是规则引擎的黄金场景。3.2 构建树状结构用栈维护层级生成可序列化的 LawNode识别出标题后需构建带父子关系的节点树每个节点包含id: 全局唯一标识如law_2020_civil_code_chapter3_section2_article22level:chapter/section/article/paragraph/itemtext: 标题内容如**第二十二条** 合同的订立……parent_id: 父节点 IDchildren: 子节点列表class LawNode: def __init__(self, node_id: str, level: str, title: str, content: str, parent_id: str None): self.id node_id self.level level self.title title self.content content self.parent_id parent_id self.children [] def build_law_tree(lines: List[str]) - List[LawNode]: parser LawTitleParser() stack [] # 存储 (node, depth) 元组 root_nodes [] for i, line in enumerate(lines): parsed parser.parse_line(line) if not parsed: continue # 计算当前深度章0节1条2款3项4 depth_map {chapter: 0, section: 1, article: 2, paragraph: 3, item: 4} current_depth depth_map.get(parsed[level], 0) # 弹出比当前深度深或等深的节点找父节点 while stack and stack[-1][1] current_depth: stack.pop() # 创建当前节点 node_id flaw_{get_law_id()}_{parsed[level]}_{i} node LawNode( node_idnode_id, levelparsed[level], titleparsed[title], contentparsed[content], parent_idstack[-1][0].id if stack else None ) # 添加到父节点 children 或 root if stack: stack[-1][0].children.append(node) else: root_nodes.append(node) stack.append((node, current_depth)) return root_nodes # 辅助函数生成 law_id实际项目中从文件名提取 def get_law_id(): return civil_code_2020 # 示例关键设计点stack维护的是(node, depth)元组而非仅 node。因为同一深度可能有多个兄弟节点如多个“第十二条”必须通过深度比较精准回溯父级。实测表明该算法在 1200 行法律文本上构建树耗时 17ms内存占用 2MB。3.3 生成 Chunk以“条”为最小语义单元保留上下文锚点RAG 场景中用户问题如“工伤认定标准是什么”需召回《工伤保险条例》第二十条但仅返回该条内容会导致上下文缺失如第二十条是否属于“认定程序”章节。因此 Chunk 必须主干以article条为单位上下文附加其父级section和chapter标题锚点在 Chunk 开头添加结构化路径便于调试def generate_chunks_from_tree(root_nodes: List[LawNode], max_chunk_size: int 512) - List[Dict]: chunks [] def traverse(node: LawNode, path: str ): if node.level article: # 构建带路径的 chunk context # 向上追溯到 chapter 和 section parent find_parent(node, section) if parent: context f【{parent.title}】 parent find_parent(node, chapter) if parent: context f【{parent.title}】 full_text f{path}{node.title} {node.content}.strip() if context: full_text f{context}\n{full_text} # 按 token 切分此处简化为字符数实际用 tiktoken if len(full_text) max_chunk_size: # 按句号/分号切分避免截断句子 sentences re.split(r[。], full_text) current_chunk for sent in sentences: if len(current_chunk sent) max_chunk_size: current_chunk sent 。 else: if current_chunk: chunks.append({ id: f{node.id}_chunk_{len(chunks)}, text: current_chunk.strip(), metadata: {law_id: get_law_id(), path: path} }) current_chunk sent 。 if current_chunk: chunks.append({ id: f{node.id}_chunk_{len(chunks)}, text: current_chunk.strip(), metadata: {law_id: get_law_id(), path: path} }) else: chunks.append({ id: f{node.id}_full, text: full_text, metadata: {law_id: get_law_id(), path: path} }) else: # 非 article 节点递归遍历子节点 for child in node.children: new_path f{path}{node.title} → if path else f{node.title} → traverse(child, new_path) for root in root_nodes: traverse(root) return chunks def find_parent(node: LawNode, target_level: str) - Optional[LawNode]: # 简单向上查找实际项目中应缓存 parent 引用 if not node.parent_id: return None # 此处省略具体查找逻辑实际需维护 parent 映射表 return None为什么以“条”为最小单元法律检索的原子粒度是“条”法官援引法条、律师分析要件、企业合规检查义务均以条为单位。实测显示将“条”作为 chunk 主干相比固定长度切分在 MTEB 法律语义检索基准上 Recall5 提升 31.7%。max_chunk_size512是平衡精度与成本的经验值——超过 512 字符的条文如《民法典》第一千一百七十九条需二次切分但必须保证每段以完整句子结尾re.split(r[。], ...)避免语义断裂。4. 向量化与存储法律语义不能靠通用模型必须领域微调直接用text-embedding-ada-002或bge-base-zh对法律文本编码召回效果惨淡查询“用人单位违法解除劳动合同”召回结果首位是《劳动法》第九十八条处罚条款而非《劳动合同法》第四十八条救济条款原因通用模型未学习“违法解除→赔偿金→继续履行→协商解除”的法律因果链也未理解“用人单位”在劳动法语境下特指“具有用工主体资格的组织”。4.1 领域适配用 LawBERT 微调而非直接用通用 EmbeddingLawBERT 是专为中文法律文本预训练的 RoBERTa 模型哈工大 最高法联合发布其词表包含“法条”“要件”“构成”“归责”等法律专属词且在 1.2 亿字法律语料上继续预训练。我们在此基础上做Sentence-BERT 微调目标让语义相似的法律表述距离更近。from sentence_transformers import SentenceTransformer, losses, models from torch.utils.data import DataLoader from datasets import Dataset # 构建训练数据正样本同一条文的不同表述、负样本不同法律领域的条文 train_examples [ # 正样本同一法条的官方表述 vs 司法解释表述 (《劳动合同法》第四十八条用人单位违反本法规定解除或者终止劳动合同……, 用人单位违法解除劳动合同劳动者要求继续履行的用人单位应当继续履行), # 负样本劳动法 vs 刑法条文 (《劳动合同法》第四十八条……, 《刑法》第二百三十四条故意伤害他人身体的处三年以下有期徒刑……) ] # 构建 Dataset dataset Dataset.from_dict({ sentences1: [ex[0] for ex in train_examples], sentences2: [ex[1] for ex in train_examples], labels: [1.0 if i % 2 0 else 0.0 for i in range(len(train_examples))] }) # 加载 LawBERT 作为基础模型 word_embedding_model models.Transformer(hfl/chinese-lawsbert) pooling_model models.Pooling(word_embedding_model.get_word_embedding_dimension()) model SentenceTransformer(modules[word_embedding_model, pooling_model]) # 定义损失函数MultipleNegativesRankingLoss 更适合法律检索 train_dataloader DataLoader(dataset, shuffleTrue, batch_size16) train_loss losses.MultipleNegativesRankingLoss(model) # 微调仅需 1 个 epoch法律领域收敛快 model.fit( train_objectives[(train_dataloader, train_loss)], epochs1, warmup_steps100, output_path./law-sbert-finetuned )为什么选 MultipleNegativesRankingLoss法律检索本质是排序任务给定查询从海量条文中找出最相关 Top-K。该损失函数强制模型将正样本对拉近同时将查询与所有 batch 内其他句子推远比 CosineSimilarityLoss 更契合 RAG 场景。实测在自建法律 QA 测试集上微调后模型 MRR10 提升 22.3%。4.2 向量存储ChromaDB 本地部署 元数据过滤拒绝“全库扫描”法律知识库需支持两类查询语义检索query工伤认定需要哪些材料→ 召回《工伤保险条例》第十八条元数据过滤filter{law_id: work_injury_regulation, level: article}→ 限定范围ChromaDB 支持混合查询且无需 GPU适合本地部署import chromadb from chromadb.utils import embedding_functions # 初始化 ChromaDB持久化到本地 client chromadb.PersistentClient(path./chroma_db) # 创建集合指定 embedding 函数 sentence_transformer_ef embedding_functions.SentenceTransformerEmbeddingFunction( model_name./law-sbert-finetuned ) collection client.create_collection( namelaw_chunks, embedding_functionsentence_transformer_ef, metadata{hnsw:space: cosine} # 余弦相似度 ) # 批量插入 chunks来自 3.3 节 chunks generate_chunks_from_tree(root_nodes) documents [chunk[text] for chunk in chunks] metadatas [chunk[metadata] for chunk in chunks] ids [chunk[id] for chunk in chunks] collection.add( documentsdocuments, metadatasmetadatas, idsids ) # 混合查询示例 results collection.query( query_texts[工伤认定需要哪些材料], n_results3, where{law_id: work_injury_regulation} # 元数据过滤 ) print(results[documents][0]) # 输出匹配的 chunk 文本关键配置hnsw:spacecosine指定 HNSW 索引使用余弦距离法律语义更适合角度而非欧氏距离n_results3是 RAG 的黄金值——太少易漏太多增加 LLM 噪声。实测表明在 5 万条法律 chunk 上ChromaDB 查询延迟稳定在 120ms 内Intel i7-11800H。4.3 验证效果用真实法律 QA 对比拒绝“准确率幻觉”不能只看 embedding 距离必须用真实问题验证查询问题期望召回条文LawBERT 微调前LawBERT 微调后提升“用人单位未及时足额支付劳动报酬劳动者可以解除合同吗”《劳动合同法》第三十八条第一款第 7 位第 1 位↑6“网络侵权责任如何认定”《民法典》第一千一百九十四条第 12 位第 2 位↑10“醉酒驾驶机动车是否一律入刑”《刑法》第一百三十三条之一第 5 位第 1 位↑4# 构建 QA 测试集人工标注 100 个真实问题 qa_test_set [ { question: 用人单位未及时足额支付劳动报酬劳动者可以解除合同吗, expected_article: 劳动合同法_38_1 }, # ... 其他 99 条 ] def evaluate_retrieval(model_path: str, test_set: List[Dict]): # 加载微调后模型 st_model SentenceTransformer(model_path) # 对每个问题编码 questions [q[question] for q in test_set] question_embeddings st_model.encode(questions, batch_size32) # 批量查询 ChromaDB results collection.query( query_embeddingsquestion_embeddings.tolist(), n_results10 ) # 计算 Recall5 hits 0 for i, q in enumerate(test_set): retrieved_ids [doc_id.split(_)[0] for doc_id in results[ids][i]] if q[expected_article] in retrieved_ids[:5]: hits 1 return hits / len(test_set) recall_at_5 evaluate_retrieval(./law-sbert-finetuned, qa_test_set) print(fRecall5: {recall_at_5:.3f}) # 输出: 0.872为什么用 Recall5 而非 AccuracyRAG 中LLM 会综合 Top-5 结果生成答案只要正确条文在前 5 即可。Accuracy 要求 Top-1 精准过于苛刻且不符合实际流程。0.872 的 Recall5 意味着 87% 的问题能在前 5 个结果中找到答案已满足生产要求。5. 避坑指南法律文本处理的 4 个血泪经验第 3 条让我重跑了一周法律文本处理不是技术炫技而是和细节死磕。以下是我在线上环境踩过的坑每一条都附带日志证据和修复代码5.1 坑PDF 提取时页眉页脚混入正文导致“第十二条”被切到下一页开头现象pdfplumber提取的文本中某页末尾出现“第十二条 ……”但实际该条文在下一页开头导致切分后内容错乱原因pdfplumber的extract_text()默认按视觉流排序当页脚文字与下页页眉文字坐标重叠时会错误合并解决禁用自动排序改用extract_words()获取坐标按 Y 坐标分页后手动拼接# 错误写法默认排序 text page.extract_text() # 正确写法坐标控制 words page.extract_words(x_tolerance1, y_tolerance1) # 按 y 坐标分组每 20px 为一页内区域 lines {} for w in words: y_key int(w[top] // 20) if y_key not in lines: lines[y_key] [] lines[y_key].append(w) # 按 y_key 升序拼接 text \n.join([ .join([w[text] for w in lines[k]]) for k in sorted(lines.keys())])5.2 坑OCR 识别“〇”为“0”导致“第二〇二三条”变成“第二023条”法条编号失效现象PaddleOCR将中文数字“〇”识别为阿拉伯数字“0”使“第二〇二三条”变为“第二023条”正则无法匹配原因PaddleOCR 的中文模型未针对法律文本中的“〇”做专项优化解决后处理阶段强制映射且在正则中兼容两种写法# OCR 后处理 text text.replace(0, 〇) # 粗暴但有效 # 正则升级支持“〇”和“0” article_pattern r^(\s*第[零一二三四五六七八九十百千\d〇]条)[\s ]*([^\n]*) # 注意[零〇] 在正则中表示“零”或“〇”5.3 坑LawChunker 将“一”同时识别为paragraph和item导致树结构错乱现象解析《刑法》时“一”被归为paragraph但同一文档中“一”又出现在“款”下作为“项”造成层级混乱原因item模式[一二三四]与paragraph模式第[一二三四]款冲突FSM 未考虑上下文优先级解决引入上下文感知——若前一行是article或paragraph则当前一为item否则为paragraphdef parse_line_with_context(self, line: str, prev_line: str ) - Optional[Dict]: # 先尝试 item 模式 item_match re.match(self.patterns[item], line) if item_match and (款 in prev_line or 条 in prev_line): return {level: item, ...} # 再尝试 paragraph 模式 para_match re.match(self.patterns[paragraph], line) if para_match: return {level: paragraph, ...} # 兜底其他模式 ...5.4 坑ChromaDB 元数据过滤失效where{law_id: xxx}返回所有结果现象执行collection.query(where{...})时过滤条件被忽略返回全库结果原因ChromaDB 3.x 版本中where参数需配合where_document使用且字符串值必须加引号解决升级 ChromaDB 至 4.0并确保where值为字符串非变量# 错误变量未转字符串 collection.query(where{law_id: law_id}) # 正确硬编码字符串或用 f-string collection.query(where{law_id: work_injury_regulation}) # 或 collection.query(where{law_id: f{law_id}})提示所有坑均来自真实线上事故。第 3 条曾导致整套法律问答系统上线后召回率暴跌 40%排查耗时 72 小时——根源在于未在 FSM 中建模“上下文依赖”而法律文本的层级恰恰高度依赖上下文。记住法律不是普通文本它的每一个符号、空格、标点都在传递语义。6. 进阶技巧用“法条依赖图”替代简单 RAG让法律推理真正发生RAG 的瓶颈在于它只是把相关法条“摆”给 LLM而法律适用需要推理链条。例如回答“员工离职后竞业限制补偿金怎么算”不能只召回《劳动合同法》第二十三条还必须关联《劳动合同法》第二十四条竞业限制范围《最高人民法院关于审理劳动争议案件适用法律问题的解释一》第三十六条补偿金标准地方性法规如《深圳经济特区企业技术秘密保护条例》第三十一条这就需要超越“文本相似度”构建法条依赖图Law Dependency Graph。6.1 构建依赖关系三类边全部可自动化抽取边类型抽取方式示例工具引用边正则匹配“参见”“依据”“根据”后的法条编号“参见《民法典》第五百八十四条” → 边article本文还有配套的精品资源点击获取