☰
法律智能问答系统落地实战:从检索召回到BERT抽取的完整指南
2026/10/1 4:12:59 网站建设 项目流程

简介:基于神经网络的法律智能问答系统,是一套面向法律领域文本检索与自动应答的完整项目,适合希望入门自然语言处理或智能问答技术的学习者,也可作为毕设、课程设计或工程实训的参考。项目覆盖数据预处理、模型训练、相似度匹配和GUI交互等环节,可帮助从零构建法律问答原型。压缩包共30个文件,以csv数据表、py源码和txt文本为主,辅以pyc编译文件与训练好的model文件:csv中存放法律条文、关键词映射和问答语料,py脚本串联训练与匹配流程,txt提供停用词和问题集,总大小37.48MB,结构清晰便于按模块学习。目前已有125人学习下载,资源内置GUI界面、分类相似度脚本、工伤/劳动合同等专题数据及配套模型,能直接运行体验,也可据此二次开发。对于想快速掌握法律智能问答系统实现方法的读者,这套代码与数据提供了较完整的实践起点。

1. 法律智能问答系统是什么:它解决的从来不是“聊天”问题

把一套基于神经网络的法律智能问答系统部署上线之前,你要先接受一个反直觉的结论:这个系统最难的部分,不在“问答”,而在“法律”。随便一个刚训练好的对话模型都能陪你聊《民法典》里的离婚冷静期,但真正让法官助理、法务和当事人愿意把它当工具用的,是它能不能在十万条法条和判例里,准确说出“你这种情况适用第几条、为什么适用、能赔多少”。法律智能问答系统的本质,是把“检索 + 阅读理解 + 生成解释”三段能力,压进一条神经网络推理流水线里,让用户拿自然语言提问,得到带法条依据的答案。这篇文章写给准备从零搭这套系统的人:可能是律所的技术负责人,也可能是做司法信息化产品的后端工程师。目标很直接——让你知道这东西怎么选型、怎么落地、哪些地方一定会翻车。

2. 选型之前先算账:为什么是神经网络,而不是纯规则或纯搜索

2.1 法律问答的数据特点决定了模型结构:长文本、强逻辑、弱概括

法律文本和新闻、商品评论最大的差异在于“句间逻辑密度”。比如《劳动合同法》第四十七条,一个条文里就同时包含“计算基数”“工作年限折算”“上限三倍”三个条件,而用户提问往往是“我在公司干了七年,月薪两万,被裁员该赔多少”。这个问题的答案要跨条文组装:先定位第四十七条,再联动第四十七条第二款的封顶规则。纯关键词搜索做不到跨句推理,而一套只做分类的模型又无法在长文本里准确锁定段落位置。所以才需要神经网络来做“表示学习”——把问题和法条都映射到向量空间,再通过注意力机制在长文本中找到条件与事实之间的对应关系。这也是为什么法律问答系统普遍采用“检索 + 重排 + 抽取”三段式,而不是端到端生成。

2.2 为什么我不用微调大模型作为主引擎,而是做“小模型流水线”

这两年很多人一上来就想微调一个开源大模型做法律问答,我的建议是:可以做成前端入口,但不要让它承担答案正确性的全部责任。原因很简单,生成式模型天然存在“幻觉”,而且法律文本的严谨性要求每一个结论都精确指向法条编号和条款,大模型在这一点上没法保证百分之百忠实。更常见且可靠的从业方案是:用 一个轻量级召回模型(选 Elasticsearch + BM25 或基于双塔的稠密检索)先圈定候选法条,再用一个基于 Transformer 的阅读理解模型(如 BERT 微调的抽取式 QA)在法条原文里抽答案,最后用规则模板或一个小的生成模型组织成口语化答复。这套架构每一环的失误都可追溯,和“法条引用错误”这种致命问题天然绝缘。

2.3 神经网络在流水线里到底承担什么:三个节点的职责切分

整条流水线里,神经网络扮演的角色不是“什么都知道”,而是“精确完成定位”。第一段,若是双塔检索模型,它的训练目标是让“用户问题”和“对应法条”在向量空间里距离更近,这个环节不需要太深层的推理,只需要语义召回。第二段,抽取式阅读理解模型,负责在召回的法条长文本里找到“赔偿基数”“计算年限”的确切起止位置,这是法律问答系统里最核心的神经网络负载,推荐使用带条件随机场或指针网络的 BERT 类模型。第三段,答案组织层,可以用一个很小的 Sequence-to-Sequence 模型做专有名词的合规改写,也可以直接用规则模板。每一段的模型规模都不必太大,一个 base 规模的预训练模型足够胜任,算力成本比微调大模型低两个数量级。

3. 最小落地实现:从裁判文书到可回答问题的完整推理链路

3.1 法律文本的向量化与检索召回:先别上深度模型,用 ES 定基线

很多人在第一步就栽了跟头——直接上 Sentence-BERT 做全库向量检索,结果“经济补偿金”和“赔偿金”两个概念在语义上相近,把一大批无关法条召回了。更稳妥的做法是先用 Elasticsearch 的 BM25 做粗召回,拿文本命中分数圈定 Top 50 个候选,再用稠密向量模型做精排。翻车概率低,而且基线容易排查。

from elasticsearch import Elasticsearch from elasticsearch_dsl import Search, Q es = Elasticsearch(["http://localhost:9200"]) # 构建法条索引,mapping 里存两个字段:法条编号与全文 law_index = "law_corpus_v1" def recall_by_bm25(question: str, top_k: int = 50): s = Search(using=es, index=law_index) q = Q( "bool", must=[Q("match", full_text={"query": question, "operator": "and"})], should=[Q("match_phrase", full_text={"query": question, "slop": 2})], ) s = s.query(q).extra(size=top_k) response = s.execute() return [(hit.article_no, hit["full_text"][:120]) for hit in response]

这里的关键参数是slop。法律长句中“用人单位”“解除劳动合同”“经济补偿”这些关键词往往被几十个字隔开,slop调大能容忍间隔词。一般建议 from 0 到 50 区间做一次网格搜索,选召回率最高的值,不要一拍脑袋填 1。BM25 的k和b参数保持默认即可,中文分词器的选择反而影响更大。

3.2 法条定位网络:用 BERT 指针网络抽取“条件-结论”二元组

拿到候选法条之后,第二步是让一个神经网络在法条原文里定位“什么情况下适用”和“适用后如何处置”。这里我推荐用 BERT + 双指针的抽取式阅读理解结构——输入是“问题 + 法条全文”,输出是答案起止位置。这个做法的好处是答案永远来自原文,不可能出现编造法条的情况。

from transformers import BertTokenizer, BertForQuestionAnswering import torch model_path = "./models/bert-base-chinese-law-qa" tokenizer = BertTokenizer.from_pretrained(model_path) model = BertForQuestionAnswering.from_pretrained(model_path) def extract_answer(question: str, article_text: str): inputs = tokenizer( question, article_text, max_length=512, truncation=True, return_tensors="pt", padding=True, ) with torch.no_grad(): outputs = model(**inputs) start_logits = outputs.start_logits end_logits = outputs.end_logits start_idx = torch.argmax(start_logits) end_idx = torch.argmax(end_logits) if start_idx > end_idx: return None tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"][0]) answer = tokenizer.convert_tokens_to_string(tokens[start_idx:end_idx + 1]) return answer

参数说明:max_length=512是个折中,法条平均长度在 300 到 600 字之间,卡 512 会截掉少部分超长立法的后半段。遇到超长要优先采用“滑窗切分”,每段重叠 100 字符,而不是粗暴截断。如果预测出的start_idx > end_idx,说明模型认为问题与法条无关,直接返回 None 而不是硬答,这是避免错误引用法条的关键防线。

3.3 答案生成与法条引用格式化:用模板把“模型输出”变成“可用结论”

到这里,系统已经拿到了“实体答案片段”,比如“月工资×工作年限”。但要交付给用户,还需要把法条编号、条款序号和计算规则拼装起来。这层用规则模板的意义不仅是可读性,更多是合规性——它确保无论模型输出什么,最终呈现永远带法条编号。

def format_legal_answer(question, article_no, clause_no, raw_answer, provision_text): template = ( f"根据《{article_no}》第{clause_no}条的规定," f"针对你的问题「{question}」的回答是:{raw_answer}。\n" f"条文原文:{provision_text}" ) return template

这里的article_no一定要和检索层返回的法条编号是同一套主键。真实场景里这个主键经常断了——检索层返回了“第四十七条”,但格式化引用写成了“第47条”,数字全角半角混用。上线前要写一条 pytest,专门检查引用编号在全部数据库记录里能否唯一命中,这种低级错误让它在测试环境翻车就好,别等上线后出洋相。

4. 数据与训练是真正的护城河:从裁判文书到问答对的构造细节

4.1 法律问答数据长什么样:三要素拆分与人工标注策略

法律问答的数据不像通用问答那样一句问一句答,它的最小单元是一个三元组(问题,法条定位,推理摘要)。其中“推理摘要”是从裁判文书“本院认为”部分抽取的结论句,这一步我推荐让人工标注员完成,而不是完全依赖自动工具。标注策略上,找 3 到 5 位有法律背景的标注者,每份数据至少两人标注,以 Cohen‘s Kappa 大于 0.7 作为质量门槛。数据量上,一个可用的法律问答模型起步至少需要 1 万条以上的三元组,覆盖劳动、婚姻、合同、侵权四个高频领域。

# 标注数据格式样例,建议以 JSONL 逐行存储 {"question": "工作七年被裁员能拿多少经济补偿", "law_entry": "劳动合同法/第四十七条", "evidence": "按劳动者在本单位工作的年限,每满一年支付一个月工资的标准向劳动者支付", "conclusion": "七个月工资的经济补偿金"}

这个结构有一个容易踩坑的细节:conclusion字段必须是“可计算的”,而不是描述性的。比如“每满一年支付一个月工资”是法律原文,但不是终局结论。用户要的是“七个月”,不是把法条再复述一遍。训练时,如果发现模型输出的答案总是原文片段而没有完成计算,请检查conclusion字段里是否包含了最终计算结果。

4.2 数据增强与负样本构造:教模型学会说“不”更重要

法律问答系统最频繁的错误不是答错,而是“不该答的时候硬答”。比如用户问的是“借条三年了还能起诉吗”,但你的检索层召回的是《民法典》第一百八十八条诉讼时效。句子表面语义相关,但问题问的是“能否起诉”,不是“时效几年”。如果你不教模型区分这两者,它会给你抽出诉讼时效条文原话,等于没答。解决办法是构造强负样本:把同领域的相似问题配给不相关的法条,训练一个二分类判别头判断“该问题与该法条是否真正相关”。

import json import random def build_negative_samples(positive_pairs, law_pool, ratio=3): negatives = [] for question, law_entry, _ in positive_pairs: for _ in range(ratio): false_law = random.choice(law_pool) if false_law != law_entry: negatives.append((question, false_law, 0)) return positives + negatives # 正样本label=1,负样本label=0

负样本数量控制在正样本的 3 倍左右即可,太多会让模型过于保守甚至全部拒答。训练时,这个分类器的输出会作为过滤器放在抽取式 QA 之前,概率低于阈值的直接走“建议咨询专业律师”话术兜底。

4.3 冷启动方案与模型复用:预训练一个法律领域的 BERT 到底值不值

这个问题的答案取决于你有没有语料。常见的做法是直接在通用中文 BERT 上继续做领域预训练,只用裁判文书和法规库做掩码语言模型(MLM),而不是从零训练。数据量在 10 万篇左右,一般只需 10 到 20 个 epoch 就能明显提升法条术语的向量表示质量。如果你手头只有少量问答对,不要自己摸索,用现成的法律领域中文预训练模型做初始权重,可以节省两到三周的调参时间。接入方式也简单:BertForQuestionAnswering.from_pretrained("法律领域模型目录")即可,不必重新设计模型结构。

5. 法律问答避坑指南:五条血泪经验与排查路径

5.1 法条引用张冠李戴:明明答对了内容,却引错了条文编号

现象:模型抽出的答案是“每满一年支付一个月工资”,但引用的条文是《劳动法》第二十八条,而不是《劳动合同法》第四十七条。原因:训练数据的law_entry字段和答案抽取层使用了不同的编号格式,模型在多头注意力里把“条文号”和“条文内容”的关联学歪了。解决:在训练数据里,把法条编号当作一个独立 token 拼到输入开头,比如“[法条] 劳动合同法第四十七条 [文本] ……”,让模型把编号当特殊的查询标志。上线前,再跑一遍前面提到的编号唯一性 pytest,基本能杜绝这个现象。

5.2 法律文本截断导致答案“断尾”

现象:凡是超过 512 个 token 的法条,答案都只覆盖前半段。比如《民法典》第一千一百七十九条,前半段讲医疗费,后半段讲残疾赔偿金,模型永远只抽到“医疗费”。原因:max_length=512一字不差地截掉了后半段。解决:改用滑窗切分,每个窗口 480 个 token、重叠 80 个 token,同一法条的不同窗口分别送入模型,最后用起止概率的均值决定最终答案。这个 trick 能提升 3 到 5 个百分点的法条后段召回率。

5.3 模型对“不”字和“反义表达”视而不见

现象:“加班没有加班费能起诉吗”被模型识别为“能起诉,依据是……”,但其实应该先判断是否属于劳动争议仲裁前置。原因:训练语料中反义表达样例太少,注意力机制被“起诉”“加班费”等强实体词主导,把“没有”的否定语义给稀释了。解决:显式加入反义词对增强(“有加班费”/“无加班费”,“赔偿”/“不赔偿”),同时在中性查询时强制模型输出仲裁前置提示。这个字段规则要写死在模板层,而不是指望模型自己学会。

5.4 检索阈值全是玄学:Top 50 召回里没有一条是对的

现象:交叉验证时 F1 很高,一到线上实际用户提问就废,Top 50 候选里相关法条连影都没有。原因:线下测试数据是你自己构造的,问法偏“法言法语”;线上用户全部是“我媳妇要把我妹赶出去怎么分家产”这种口语。解决:单独收集 500 条真实用户提问,做一次独立的查询改写(query expansion),把口语化的表述映射成法言法语再送进检索层。常用的手法就是在词典里维护一份“口语-术语”对照表,比如“媳妇”映射“配偶”,“赶出去”映射“居住权”。

5.5 答非所问但文本流畅:生成层的“幻觉”最难防

现象:答案写得很通顺,看起来像法律专业人士的口吻,但核心结论是错的——把“补偿金”说成了“赔偿金”。原因:答案生成层用了生成式模型,模型在生成流畅文本时自行补全了专业术语。解决:放弃生成式方案,回到 3.3 的模板方案;或者保留生成式模型,但最终答案必须经过“实体校验”步骤——抽取答案里的关键金额、年限、法律概念和法条原文比对,不一致就替换回原文。这相当于给生成模型戴上镣铐。

6. 评估与上线技巧:用双重指标挑出“会说话但不懂法”的模型

6.1 评估指标:答对率和法条命中率,缺一不可

法律问答系统上线前,要同时盯两个数字。第一个是常规的“答对率”(EM/F1),即抽取答案与标注答案的字符级重合度;第二个是容易被忽视的“法条命中率”,即系统返回的法条编号和标准答案的法条编号是否一致。一个模型可能答对率 80%,但法条命中率只有 65%——这种情况下,系统非常有欺骗性,它给用户的体验是“每句话都像法律人士,但依据全错”。所以评估要按领域分层跑:劳动、婚姻、合同、侵权各跑 200 条测试,任一领域法条命中率低于 70% 就直接打回重训。

指标含义通过线
Answer EM答案与标注完全一致的比例≥ 70%
Answer F1答案的字符级与词级重合度≥ 82%
法条命中率返回条文编号与标准一致≥ 85%
检索召回率相关法条进入 Top50≥ 92%

如果发现法条命中率低但答对率高,说明问题出在“第二段”检索精排,而不是抽取模型。别急着调 BERT 的 dropout,先回去看 BM25 的召回队列里到底有没有正确的法条。

6.2 上线之前要做的 5 个验证技巧:从对抗测试到准入清单

第一,对抗测试。准备 50 条带有明显诱导性的问题,比如“拘传可以连续超过二十四小时吗”,这类问题在准确法条里答案是否定的,但模型很容易被高强度实体词带偏。第二,时间穿越测试。新的法律法规可能会废止旧法,要确保知识库按生效日期过滤,线上系统在 2024 年不能引用 2021 年已废止的司法解释。第三,多轮上下文测试。用户可能会问“那如果是九年呢”,系统需要继承上一轮的“七年”语境,如果做不到就明确告诉用户“请重新输入完整问题”,不硬接。第四,兜底话术测试。当置信度低于阈值时,提示“您的案情可能存在多个适用要件,建议携带材料咨询律师”,这个话术必须和正常回答在 UI 上明显区分。第五,全链路压测。并发 50 个请求时推理耗时翻倍不奇怪,但你要确认这翻倍是来自检索层的 Elasticsearch 还是 BERT 的 GPU 推理,两个排队链路要分别监控。

6.3 一个很可能用到的落地技巧:把 BERT 模型切成半精度推理

法律问答系统要上线到中小型律所,通常只有一张消费级显卡。BERT-base 的 fp32 推理一个样本大约是 30 毫秒,但并发 20 个请求就开始排队。我一般会在部署时打开半精度推理,把模型的参数量减半,速度提升 40%,准确率只降不到 0.5%。

import torch model = BertForQuestionAnswering.from_pretrained(model_path) model.half() # 转换为 fp16 model = model.to("cuda:0")

这个技巧的关键是model.half()必须在from_pretrained之后、to("cuda")之前调用,顺序错了 tensor 类型会直接报错。另外,如果输入的长度不固定,不要开torch.compile,它只对固定 shape 有收益,法律文本长短差异极大,编起来反而慢。

讲一个我自己的习惯:每次上线前,我会拿前一天的真实用户日志,随机挑 30 条喂给新模型,人工看一遍输出。这个动作不花什么成本,但能拦住所有指标之外的问题。模型在测试集上再漂亮,也不如真实用户那一句“这说的是啥”来得诚实。希望这套思路能帮你把法律智能问答系统从“能聊天”推到“能办案”,哪怕只推进一小步。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询