简介:这份资源面向希望入门智能法律咨询开发的Python学习者与算法实践者,围绕法律问答场景提供了一套可运行的完整项目素材。压缩包共25个文件,以py脚本、png图片、html页面、js与css前端资源为主,另含csv数据集、vsdx流程图及BERT模型权重文件,整体约371.77MB。核心是一份4万条法律问答数据集,配合数据分析脚本完成清洗与探索,句子相似匹配脚本实现问题与答案的语义检索,main.py与app启动脚本搭建Web服务,templates与static目录提供问答界面与交互样式,流程图则梳理了系统整体架构。读者可据此理解从数据处理、模型加载到前端展示的完整链路,并借助预训练中文BERT模型体会语义匹配在法律问答中的落地方式。目前已有140人学习下载,适合作为课程设计或小型法律咨询原型的参考起点。
1. 法律.rar:一个压缩包名字背后的检索工程问题
第一次看到「法律.rar」这个标题,我以为是某个法规汇编的下载包。真正拆开看,它指向的是一类很实际的需求:把散落在各处的法律文本、裁判文书、合同模板、监管问答,做成一个可检索、可复用、可增量更新的本地知识库。压缩包只是外壳,里面装的是非结构化文本和一堆命名混乱的目录。
这类需求在法务、合规、合同审核、政策研究场景里非常普遍。从业者真正想要的不是「一个包」,而是「输入一句自然语言,能定位到具体条款和出处」。所以这篇笔记不讲怎么解压,而是讲怎么把一堆法律文本变成能查、能引用、能维护的检索系统。适合两类人:手里已经有一批法律文档、想自己搭检索的工程师;以及被「找条款找半天」折磨、想搞清楚技术边界的法务从业者。
2. 法律文本检索的选型:为什么不能直接上向量库
2.1 法律语料的三个特殊结构
法律文本和普通网页、论文不一样,直接套通用 RAG 方案大概率翻车。第一个结构是层级强:编、章、节、条、款、项,一条法规的效力往往取决于它挂在哪一章下面。第二个结构是引用密集:「依照本法第 X 条」「参照第 Y 条规定处理」,一条文本脱离引用上下文就失去意义。第三个结构是版本敏感:同一部法规有修订前后多个版本,检索时必须能区分「现行有效」和「已废止」。
这三点决定了纯向量检索不够用。向量擅长语义相似,但对「第 23 条」这种精确编号、对「2021 年修订版」这种版本约束,召回很不稳定。我一般会做混合检索:关键词/编号精确匹配打底,向量做语义补充,再用元数据过滤版本和效力状态。
2.2 分块策略:按「条」切,不按字数切
通用做法是按 512 token 滑窗切块,法律文本这么切会把一条法规拦腰截断,检索出来半句话,引用就废了。更稳的做法是以「条」为最小单元,条内如果太长再按「款」切,并且把上级章节标题作为元数据挂上去。
import re def split_by_article(text: str): # 以「第X条」为切分锚点,保留条号作为元数据 pattern = re.compile(r'(第[一二三四五六七八九十百零〇\d]+条)') parts = pattern.split(text) chunks = [] # parts[0] 是首条之前的章节标题等前言,丢弃或单独存 for i in range(1, len(parts), 2): article_no = parts[i] body = parts[i + 1].strip() if not body: continue chunks.append({ "article": article_no, "content": article_no + body, "char_len": len(body) }) return chunks这段代码的关键在pattern.split的用法:带捕获组的 split 会把分隔符本身也保留在结果里,所以奇数位是条号、偶数位是条文。参数上要注意两点:正则里的中文数字范围要覆盖「百」「零」「〇」,否则遇到「第一百零三条」会漏切;char_len留着后面做超长条文的二次切分判断,一般单条超过 800 字就该考虑按款再拆。
2.3 元数据设计:版本和效力是检索的生命线
分块之后必须给每个块打元数据,否则后面过滤无从谈起。我一般至少保留这几个字段:法规名称、发布机关(用代称或编码,不写具体机构)、版本年份、效力状态(现行有效/已修订/已废止)、章节路径、条号。效力状态这个字段最容易被忽略,但它是排错时第一个要看的东西——检索结果不对,十有八九是废止版本混进来了。
| 字段 | 类型 | 是否必填 | 用途 |
|---|---|---|---|
| law_name | string | 是 | 按法规聚合、过滤 |
| version_year | int | 是 | 区分修订版本 |
| status | enum | 是 | 过滤失效文本 |
| chapter_path | string | 否 | 展示层级、辅助排序 |
| article_no | string | 是 | 精确匹配、引用定位 |
建库时把status建成可过滤字段,检索默认只查「现行有效」,需要查历史版本时显式放开。这个默认值能挡掉大量脏召回。
3. 把法律.rar 跑通:从解压到可检索的最小链路
3.1 解压与编码清洗:先解决乱码再谈检索
压缩包里的文本常见两种坑:GBK 编码的老文件,以及从网页复制带进来的全角空格和软换行。不先清洗,后面分词和匹配全是噪声。
# 批量探测编码并转 UTF-8,先备份原目录 mkdir -p cleaned for f in raw/*.txt; do enc=$(file -b --mime-encoding "$f") if [ "$enc" = "unknown-8bit" ] || [ "$enc" = "iso-8859-1" ]; then iconv -f GBK -t UTF-8 "$f" -o "cleaned/$(basename "$f")" 2>/dev/null \ || cp "$f" "cleaned/$(basename "$f")" else cp "$f" "cleaned/$(basename "$f")" fi donefile --mime-encoding对中文文本经常判成 unknown,所以这里做了兜底:判不准就按 GBK 试转,失败再原样拷贝。转完还要做一次字符归一化,把全角空格、不间断空格、软连字符统一掉,否则「第 一 条」和「第一条」在检索时对不上。这一步用 Python 的unicodedata.normalize('NFKC', text)加正则替换就能覆盖大部分情况。
3.2 建索引:关键词与向量双路
清洗完的块要进两套索引。关键词侧用倒排索引,条号、法规名这类精确字段走它;向量侧用嵌入模型,负责「意思相近但用词不同」的召回。两路结果用倒数排名融合(RRF)合并,比简单加权稳。
def rrf_merge(keyword_hits, vector_hits, k=60): scores = {} for rank, doc_id in enumerate(keyword_hits): scores[doc_id] = scores.get(doc_id, 0) + 1.0 / (k + rank + 1) for rank, doc_id in enumerate(vector_hits): scores[doc_id] = scores.get(doc_id, 0) + 1.0 / (k + rank + 1) return sorted(scores.items(), key=lambda x: -x[1])k=60是 RRF 的常用平滑常数,作用是压低头部排名的绝对优势,让两路结果更均衡。如果关键词路明显更准(比如用户输入里带条号),可以把关键词路的权重单独乘一个系数,但别超过 2,否则向量路基本失效。融合之后再过一遍元数据过滤,把status != 现行有效的块剔掉。
3.3 引用回链:让每条结果都能落到原文
检索结果只给一段文字是不够的,法务场景必须能点回原文位置。做法是在块元数据里存源文件路径和字符偏移,前端展示时按偏移高亮。偏移在清洗阶段就要算好,因为清洗会改变字符位置,所以偏移要基于清洗后的文本记录,别拿原始文件的偏移去对清洗后的内容,这是血泪经验,对不上就是整段错位。
4. 法律.rar 检索的避坑清单:五个真实翻车点
4.1 现象:搜「违约责任」召回一堆已废止的旧法
原因:建库时没区分版本,所有文本平铺进索引,向量对「违约责任」这种通用词一视同仁。解决:给每个块打version_year和status,检索默认过滤现行有效;历史版本单独建集合,需要时显式切换。上线前拿几部有修订史的法规做回归测试,确认旧版不会漏进默认结果。
4.2 现象:条号能搜到,但点进去是半句话
原因:按字数切块把一条法规切断了,条号挂在第一块,后半句在第二块且没带条号。解决:改按「条」切,条内超长再按「款」切,每块都继承条号;切分后做一次校验,统计「以条号开头」的块占比,低于 95% 就说明切分逻辑有问题。
4.3 现象:同一部法规在库里出现两份,内容还不一样
原因:增量更新时直接追加,没做去重,新旧版本共存。解决:以「法规名 + 版本年份 + 条号」做唯一键,入库用 upsert;更新前先按法规名查一遍已有版本,确认是新增还是替换。这个唯一键设计要在建库第一天就定好,后期补很痛苦。
4.4 现象:向量检索对「第 23 条」这种查询几乎无效
原因:嵌入模型对纯编号不敏感,语义空间里「第 23 条」和「第 24 条」距离很近。解决:查询里检测到条号模式就走关键词精确匹配优先,向量只做补充;或者把条号单独抽成字段做结构化查询,别指望向量扛精确匹配。
4.5 现象:清洗后字符偏移全乱,高亮错位
原因:清洗阶段做了全角转半角、去空格,字符数变了,但偏移还是按原始文本算的。解决:偏移一律基于清洗后文本重新计算,原始文件只做备份不做定位;如果必须回溯原文,存「清洗后偏移 + 清洗规则版本号」,用同一套规则重放定位。
5. 进阶:用查询改写把「口语问题」翻译成「法律检索式」
5.1 查询改写的两个方向
用户不会按法条措辞提问,会说「公司拖欠工资怎么办」,而库里是「用人单位未按时足额支付劳动报酬的」。直接拿口语去检索,向量能救一部分,但关键词路基本废。我一般做两层改写:第一层抽实体和意图(主体、行为、诉求),第二层映射到法规术语(「拖欠工资」→「未足额支付劳动报酬」)。改写不追求一次到位,生成 2 到 3 个候选查询并行检索再融合,比单查询稳。
def build_queries(user_input: str, term_map: dict): queries = [user_input] for colloquial, legal in term_map.items(): if colloquial in user_input: queries.append(user_input.replace(colloquial, legal)) return list(dict.fromkeys(queries)) # 去重保序term_map是人工维护的口语到术语映射表,规模不用大,覆盖高频场景即可,几百条就能明显改善召回。注意dict.fromkeys去重会保留首次出现顺序,别用 set,否则查询顺序随机,融合结果不稳定。
5.2 验证改写是否有效:看三个指标
改写不是越多越好,加太多候选查询会引入噪声。我一般盯三个数:Top5 命中率(前五条里有没有正确条款)、首条准确率、平均召回条数。改写前后各跑一遍固定测试集,Top5 命中率提升低于 5 个百分点就说明改写没抓到点子上,回去检查映射表而不是继续加查询。
| 指标 | 改写前 | 改写后 | 判断标准 |
|---|---|---|---|
| Top5 命中率 | 基线 | 提升 ≥5pp | 低于则映射表有问题 |
| 首条准确率 | 基线 | 不下降 | 下降说明噪声引入 |
| 平均召回条数 | 基线 | 增幅 ≤30% | 暴涨说明查询发散 |
5.3 一个具体技巧:把章节路径当弱信号排序
同样命中「劳动报酬」的块,挂在「劳动合同」章下的通常比挂在「附则」下的更相关。排序时给章节路径加一个很小的加权,比如命中块所属章节标题与查询有词重叠就加 0.05 分。这个权重不能大,大了会压过语义相关性,0.05 到 0.1 之间试,用测试集定。我自己踩过的坑是早期给了 0.3,结果所有「总则」章的内容被顶到前面,因为总则里什么词都有,反而把具体条款挤下去了。
这套链路跑下来,一个中等规模的法律文本库(几千条法规、几十万块)在单机上就能撑住日常检索。真正花时间的不是模型选型,是清洗、切分、元数据和回归测试这四件脏活。我现在的习惯是每接一批新语料,先抽 20 条做切分和召回的人工核对,确认没问题再全量入库,省得后面返工。希望帮到你。
本文还有配套的精品资源,点击获取