法律.rar解压指南:构建可检索、可版本管理的法律知识库
简介这份资源面向法律科技方向的学习者与开发者提供一套基于Python的智能法律问答应用完整工程可用于搭建法律咨询机器人、练习中文语义匹配与Web服务部署。压缩包共25个文件约371.77MB以png图片、py脚本、html模板、js脚本为主另含vsdx流程图、csv数据集、css样式、bin模型权重、json配置等覆盖数据、模型、后端与前端各环节。核心内容包括40000条法律问答数据集以及数据分析、句子相似匹配、应用启动与主程序等脚本并集成chinese-bert-wwm-ext预训练模型配合templates与static目录完成界面与交互资源流程图文档则梳理了整体业务流程与系统架构。目前已有139人学习下载适合希望从数据预处理、语义检索到Web应用落地完整走通一遍的中级Python开发者参考借鉴。1. 法律.rar一个被误读的压缩包和它背后真正的技术活第一次看到「法律.rar」这个标题我以为是哪个律所把合同模板打包了。点进去才发现它指向的是一类很具体的技术需求把散落在各处的法律法规、司法解释、地方性法规、裁判文书做成一个可检索、可版本管理、可离线部署的本地知识库。说白了就是把「法律」这个非结构化文本集合压成一个能用的数据资产。这件事的痛点非常真实。做企业合规、合同审查、法务问答系统的团队几乎都卡在同一个地方公开的法律文本格式混乱PDF、Word、HTML 混在一起更新频繁条款层级深直接丢给大模型就是一本糊涂账。你需要的是把「法律.rar」解压成结构化的条文树再挂上检索和引用溯源。这篇文章讲的就是这条链路怎么走通适合法务技术、RAG 工程师、以及想自己搭法律问答的独立开发者。2. 法律文本的结构化拆解从乱码 PDF 到条文树2.1 为什么不能直接把法律 PDF 丢进向量库我见过太多团队的第一步就是错的把整部《民法典》PDF 用 PyPDF2 抽成一大段文本按 500 字切块直接 embedding。结果就是检索出来的片段经常从「第一百五十三条」中间断开引用的时候根本对不上条款号。法律文本的核心价值在于「条-款-项」的层级关系一旦切碎引用溯源就是空谈。正确的做法是先做结构识别再做语义切分。法律文本有非常强的格式规律编、章、节、条、款、项每一级都有固定的编号模式。中文法律里「第X条」是基本单元「第X款」通常以换行或缩进区分「第X项」用一二标记。你要做的是写一个解析器把这些层级还原成一棵树而不是一锅粥。常见做法是用正则先定位「第[一二三四五六七八九十百千]条」把每条切成独立块再在块内识别款项。这个思路对 90% 的中央法规有效地方性法规和部门规章格式差异大需要额外规则。2.2 用 Python 写一个条文级解析器下面这段代码是我在多个项目里迭代过的版本核心逻辑是先按「条」切分再在条内按「款」和「项」细分最后输出带层级路径的 JSON。import re import json # 中文数字转阿拉伯数字用于排序和引用 CN_NUM {一:1,二:2,三:3,四:4,五:5,六:6,七:7,八:8,九:9,十:10, 百:100,千:1000} def cn_to_int(cn): 把一百五十三转成153处理到千位 if not cn: return 0 result, unit 0, 1 cn cn.replace(零, ) for i in range(len(cn)-1, -1, -1): c cn[i] if c in CN_NUM: n CN_NUM[c] if n 10: unit n if result 0: result n else: result n * unit return result def parse_article(text): 解析单部法律文本返回条文列表 # 匹配第X条保留条号中文和正文 pattern re.compile(r第([一二三四五六七八九十百千])条\s*) matches list(pattern.finditer(text)) articles [] for i, m in enumerate(matches): start m.end() end matches[i1].start() if i1 len(matches) else len(text) body text[start:end].strip() art_no cn_to_int(m.group(1)) # 在条内按款切分通常以换行后非一开头为标志 paragraphs re.split(r\n(?[^\s]), body) articles.append({ article: art_no, article_cn: m.group(1), paragraphs: [p.strip() for p in paragraphs if p.strip()], raw: body }) return articles # 使用示例 with open(minfadian.txt, r, encodingutf-8) as f: raw f.read() result parse_article(raw) print(json.dumps(result[:2], ensure_asciiFalse, indent2))逻辑说明cn_to_int处理中文数字注意「十」在开头时代表 10 而不是 1×10这个函数用从右往左的累加方式规避了大部分边界。parse_article先用finditer拿到所有条号位置再用相邻位置差切出每条正文。条内按换行加非括号开头切款这是经验规则对多数法律有效。参数说明如果你的文本里「第X条」后面有全角空格或制表符正则里的\s*要改成[\s\u3000]*。地方性法规常用「第X条」但款项用「一」直接跟不分行这时候需要把re.split的规则改成按「[一二三四五六七八九十]」切。2.3 层级路径的构建与存储解析出条文后还要补上「编-章-节」的路径。我的做法是在解析前先扫一遍全文用「第X编」「第X章」「第X节」的正则记录它们的位置区间然后给每条打上所属的编章节标签。这样最终每条记录长这样{ law: 中华人民共和国民法典, path: [第一编 总则, 第一章 基本规定], article: 153, text: 违反法律、行政法规的强制性规定的民事法律行为无效..., effective_date: 2021-01-01 }存储上条文级数据用 SQLite 就够字段包括 law_name、path、article_no、content、version。向量检索另建一个 collection每个条文一条向量metadata 里带上 law_name 和 article_no这样检索命中后能直接回表拿原文引用溯源就闭环了。提示不要把「款」单独做向量粒度太细会导致召回碎片化。以「条」为最小检索单元款和项作为条内上下文一起返回效果最稳。3. 法律知识库的检索层向量、关键词还是混合3.1 纯向量检索在法律场景的三个翻车点法律文本的检索有个特殊性用户经常用精确的术语比如「不可抗力」「表见代理」「善意取得」。这些词在向量空间里未必和同义表述靠得近反而可能因为上下文差异被稀释。我实测过纯向量检索在「找具体法条」这个任务上召回率经常不如 BM25。第二个翻车点是数字和编号。「第一百五十三条」这种查询向量模型基本抓瞎它会把「一百五十三」当成普通数字和「一百五十四」的向量距离可能比和「一百五十二」还近。第三个是版本问题同一部法律有修订前后多个版本纯向量无法区分「民法典」和「合同法」里相似但已废止的条文。所以我的结论很直接法律检索必须做混合检索向量负责语义泛化BM25 负责术语和编号精确匹配两者用 RRFReciprocal Rank Fusion融合。3.2 用 rank_bm25 和向量库搭混合检索下面是一个最小可跑的混合检索实现向量部分用 sentence-transformersBM25 用 rank_bm25融合用 RRF。from rank_bm25 import BM25Okapi from sentence_transformers import SentenceTransformer import numpy as np # 假设 articles 是上一步解析出的条文列表 corpus [a[raw] for a in articles] tokenized [list(a[raw]) for a in articles] # 中文按字切简单但有效 bm25 BM25Okapi(tokenized) model SentenceTransformer(shibing624/text2vec-base-chinese) embeddings model.encode(corpus, normalize_embeddingsTrue) def hybrid_search(query, top_k5, k_rrf60): # BM25 检索 bm25_scores bm25.get_scores(list(query)) bm25_rank np.argsort(bm25_scores)[::-1][:top_k*2] # 向量检索 q_emb model.encode(query, normalize_embeddingsTrue) vec_scores embeddings q_emb vec_rank np.argsort(vec_scores)[::-1][:top_k*2] # RRF 融合 rrf {} for rank, idx in enumerate(bm25_rank): rrf[idx] rrf.get(idx, 0) 1/(k_rrf rank 1) for rank, idx in enumerate(vec_rank): rrf[idx] rrf.get(idx, 0) 1/(k_rrf rank 1) sorted_idx sorted(rrf, keyrrf.get, reverseTrue)[:top_k] return [(articles[i][article], articles[i][raw][:80]) for i in sorted_idx] print(hybrid_search(违反强制性规定的民事法律行为效力))逻辑说明BM25 对中文按字切分是最省事的做法不需要分词器对法律术语的精确匹配反而更好。向量用 text2vec-base-chinese这个模型对中文语义的覆盖够用且体积小。RRF 的 k 值取 60 是原论文推荐值实际调参时可以在 40 到 80 之间试。参数说明top_k*2是每路召回数量融合后取 top_k。如果你的条文库超过 10 万条BM25 的get_scores会变慢建议用 Elasticsearch 的 BM25 实现替代。向量检索的normalize_embeddingsTrue必须开否则内积不等于余弦相似度。3.3 引用溯源让每条回答都能点回原文检索命中后返回给用户或大模型的不能只是文本片段必须带上「法律名称 条号 版本」。我的做法是在 metadata 里存law_name、article_no、version检索结果按law_name分组同一条法律下的条文按条号排序。这样大模型在生成回答时可以要求它引用格式为「《民法典》第153条」用户点击就能跳转到原文。这一步的坑在于不同法律的条号会重复比如《民法典》和《合同法》都有「第X条」。所以 metadata 里的law_name必须作为主键的一部分不能只靠条号去重。4. 法律.rar 的版本管理与增量更新别让旧法条污染知识库4.1 法律更新为什么是知识库的头号杀手法律不是静态文本。一部法律修订后旧版本条文如果还留在向量库里检索时就会同时召回新旧两条大模型可能引用已废止的条款。我见过一个合同审查系统因为没做版本隔离把《合同法》的条文和《民法典》混在一起生成的审查意见直接引用了失效条款法务当场就炸了。解决思路有两个方向一是物理隔离每个版本单独建 collection检索时按生效日期过滤二是逻辑隔离在 metadata 里加effective_date和expired_date检索时加时间过滤条件。我倾向后者因为维护成本低且支持「查历史版本」这种需求。4.2 用 effective_date 做时间切片检索在条文入库时给每条记录打上effective_date和expired_date未废止则为 null。检索时如果用户没有指定时间默认只召回effective_date today且expired_date is null or expired_date today的条文。from datetime import date def filter_by_date(articles, query_dateNone): 按生效日期过滤条文query_date 为 None 时取今天 if query_date is None: query_date date.today().isoformat() valid [] for a in articles: eff a.get(effective_date) exp a.get(expired_date) if eff and eff query_date: continue if exp and exp query_date: continue valid.append(a) return valid逻辑说明effective_date是字符串格式YYYY-MM-DD直接字符串比较即可不需要转 datetime。expired_date为 null 表示尚未废止。这个过滤要在检索前做而不是检索后否则会浪费召回名额。参数说明如果你的数据里日期格式不统一先用dateutil.parser归一化。对于「修订」而非「废止」的情况旧版本要设expired_date为新版本生效日的前一天新版本设effective_date为生效日。4.3 增量更新的幂等入库法律更新时不能简单删了重灌因为向量库的 ID 会变引用会断。我的做法是用law_name article_no version作为唯一键更新时先查是否存在存在则更新内容和日期不存在则插入。这样引用 ID 保持稳定。def upsert_article(conn, article): 幂等写入唯一键为 law_name article_no version cur conn.cursor() cur.execute( INSERT INTO articles (law_name, article_no, version, content, effective_date, expired_date) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(law_name, article_no, version) DO UPDATE SET contentexcluded.content, effective_dateexcluded.effective_date, expired_dateexcluded.expired_date , (article[law_name], article[article_no], article[version], article[content], article[effective_date], article.get(expired_date))) conn.commit()逻辑说明SQLite 的ON CONFLICT语法要求唯一索引建表时加UNIQUE(law_name, article_no, version)。向量库那边用同样的唯一键做 metadata 过滤更新时先按唯一键删旧向量再插新向量保证不重复。参数说明version字段建议用生效日期或修订批次号不要用「v1」「v2」这种无意义编号否则跨法律对比时会混乱。5. 避坑与排查法律知识库落地时最容易翻车的 5 个地方5.1 现象检索结果里混入了「征求意见稿」原因很多法律数据库会把征求意见稿、草案也收录这些文本没有法律效力但格式和正式法律几乎一样。如果你的解析器不区分就会一起入库。解决在数据源层面加status字段只入库statuseffective的文本。征求意见稿单独存一个 collection检索时默认不召回。如果用户明确要查草案再切换数据源。5.2 现象条文切分后款项内容被截断原因有些法律的「款」不是以换行开头而是以全角空格或制表符开头正则\n(?[^\s])匹配不到导致整条被当成一款。解决把切分规则改成re.split(r\n\s*(?[^]), body)允许换行后跟空白再跟非括号字符。更稳的做法是先统计文本里「款」的实际分隔符再写针对性规则。5.3 现象向量检索对「第X条」查询完全失效原因向量模型把「第一百五十三条」编码成语义向量和条文内容的向量距离没有区分度。BM25 按字切分时「一百五十三」和「一百五十四」只有一个字不同得分接近。解决对条号查询做特殊处理先用正则从 query 里提取条号如果命中直接按article_no精确过滤再在过滤结果里做语义排序。这一步能极大提升「查具体法条」的准确率。5.4 现象大模型引用法条时编造条号原因检索返回的片段里没有明确的条号标记大模型只能靠上下文猜猜错就编。解决在检索结果的文本前强制拼接「《法律名称》第X条」前缀让大模型看到的就是带条号的完整引用。同时在 prompt 里要求「只引用检索结果中出现的条号不得自行推断」。5.5 现象增量更新后旧向量没删干净原因向量库的删除操作是异步的或者按 metadata 过滤删除时条件写错导致旧版本条文仍然能被召回。解决更新流程改成「先删后插」删除时用law_name article_no version精确匹配删完等索引刷新再插入。如果向量库支持事务把删除和插入放在同一个事务里。每次更新后跑一次验证查询确认旧版本条号查不到。6. 进阶技巧用条文引用网络做重排序法律条文之间不是孤立的存在大量引用关系比如「依照本法第X条」「适用第X章的规定」。这些引用关系可以构建成一张有向图用来做检索结果的重排序。我的做法是在解析阶段用正则提取每条条文里对其他条文的引用存成(source_article, target_article)的边。检索时如果命中条文 A且 A 引用了 B那么 B 的排序权重上调。import re def extract_refs(text, law_name): 提取条文中的引用关系返回目标条号列表 refs [] # 匹配第X条、第X章、本法第X条 for m in re.finditer(r第([一二三四五六七八九十百千])条, text): refs.append(cn_to_int(m.group(1))) return refs # 构建引用图 ref_graph {} for a in articles: refs extract_refs(a[raw], a[law_name]) ref_graph[a[article]] refs def rerank_by_ref(hits, ref_graph, boost0.3): 对检索结果按引用关系加权 scores {h[article]: h[score] for h in hits} for h in hits: for ref in ref_graph.get(h[article], []): if ref in scores: scores[ref] boost return sorted(scores.items(), keylambda x: x[1], reverseTrue)逻辑说明extract_refs只提取「第X条」形式的引用章、节级别的引用粒度太粗不适合做重排序。boost取 0.3 是经验值太高会让引用关系主导排序太低没效果。这个重排序要在混合检索之后做作为最后一步微调。参数说明如果你的法律文本里引用格式多样比如「本法第X条」「《XX法》第X条」正则要扩展成r(?:本法|《[^》]》)?第([一二三四五六七八九十百千])条并区分本法和外部法。外部法的引用需要跨法律匹配复杂度高建议先只做本法内引用。这套方案我在两个项目里跑过条文解析准确率在 95% 以上混合检索的 top-5 命中率比纯向量高 20 个百分点左右。最大的教训是别急着上大模型先把条文结构和版本管理做扎实否则后面全是补丁。法律这个领域数据的准确性比模型的聪明程度重要得多。希望帮到你。本文还有配套的精品资源点击获取