ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

法律智能问答系统实践:混合式架构与神经网络语义匹配

法律智能问答系统实践:混合式架构与神经网络语义匹配 简介基于神经网络的法律智能问答系统是一份面向初学者的完整工程项目适合用作毕业设计、课程设计或工程实训。项目围绕法律条文问答场景整理了劳动合同、员工权益、工伤事故、辞退解雇等中文文本数据并配套Python源码、交互界面与预训练模型可帮助读者理解从数据清洗、关键词匹配到模型推理的完整流程。压缩包共30个文件其中11个CSV数据表用于训练与检索6个Python脚本覆盖GUI交互、文本预处理、相似度匹配和模型训练5个TXT文本提供问答语料与停用词表2个model文件保存已训练好的模型整体大小37.48MB目录结构清晰便于按模块学习。目前已有126人学习下载。借助这份资源包可快速搭建一个可运行的法律问答Demo同时参考其中律师问答、关键词分类、相似度匹配等代码逻辑迁移到其他垂直领域智能问答项目。1. 法律智能问答系统到底是什么检索词条背后的语义缺口当事人写“我把钱借给朋友他没还”系统必须知道这是民间借贷纠纷而不是诈骗律师问“预抵押登记是否具有物权效力”系统要把物权编和担保制度解释同时翻出来。这是“基于神经网络的法律智能问答系统”要解决的核心问题用神经网络把用户口语表达和结构化法律知识映射到同一个语义空间再通过检索、匹配和生成组织成可读答案。它不是什么新奇实验室项目而是把法条库、裁判文书、对话生成串起来的一套中台能力。适合律师助理、法律科技团队、政企法务和做知识服务的工程师要么用于内部咨询助手要么用于对外智能客服核心产出是“能把当事人说不清楚的话翻译成法律问题并给出可溯源答案”的系统。2. 架构选型与语料为什么法律问答不能只靠关键词检索2.1 三种主流架构检索式、生成式、混合式怎么选做法律问答第一直觉是关键词检索把法条库灌进 Elasticsearch用户输入“借钱不还”ES 分词命中“借款”“不还”返回民间借贷相关条目。这套方案在早几年的法律产品里很常见实际效果是短而规范的问题能答当事人真实提问的召回率掉得厉害。法条语言和日常语言是两套系统法条写“借款人未按照约定返还借款的”当事人只会说“他借我的钱拖了半年”。关键词重叠度低倒排索引的 BM25 分数给得很低甚至完全召回不了。这不是 ES 的缺陷而是语义鸿沟需要另一层能力来填补。行业里普遍把方案收敛成三类检索式问答靠 ES 加向量检索把相关法条和判例捞回来生成式问答直接用大模型读语料生成回答不依赖外部检索混合式问答先检索约束范围再交给生成模型“带稿回答”。我经历过的落地项目里混合式胜出得多原因看这张对比就清楚架构可解释性口语表达适配幻觉风险落地成本检索式高直接给出条文来源低对模糊问题无解低低生成式低黑匣子不可溯源高能用口语解释高容易编造高混合式中高来源在前端可见高靠召回补齐表达中由检索约束中选混合式还有一个现实理由法律场景要求输出可溯源。当事人可以接受模型说得不够流畅但不能接受答案没有依据。混合式把溯源前置到检索环节后面生成的自由度被检索结果约束住幻觉空间就小了很多。2.2 判决书法条清洗一份可复用的预处理流水线无论走哪条路语料都要先处理。法律语料有几个特殊性判决书是半结构化长文本“本院认为”之后才是判决理由前面是当事人、案号、诉讼请求法条按章条编码条款之间有引用依赖裁判文书带地域、法院层级、审级信息这些都会成为后续过滤的字段。最容易踩的坑是把整篇判决书直接喂进模型又慢又乱。我一般会把原始语料统一清洗成这种 JSONL 格式{case_id:2023某市民初188号,case_type:民间借贷纠纷,court_level:基层,valid_law:民法典 第六百七十九条,facts:2019年12月原告通过银行转账向被告出借10万元未签订书面借款合同。,reasoning:本院认为自然人之间的借款合同自贷款人提供借款时成立……,asking:原告诉请被告返还借款本金及利息是否成立}生成这个 JSONL 的逻辑是先用正则按段落拆判决书识别“原告”“被告”“本院认为”等区域标记再从“本院认为”里抽条文引用比如“依照《中华人民共和国民法典》第六百七十九条”会落到 valid_law 字段。清洗阶段的抽取算法不必一开始就上模型正则加规则就能覆盖多数判决书例外情况用少量兜底规则补。清洗的性价比远高于模型内置能力。对应的 Python 清洗脚本通常长这样import json import re from pathlib import Path RAW_DIR data/judgments_raw/ OUT_FILE data/judgments_clean.jsonl REGION_PATTERNS { facts: r(经审理查明|审理查明)(?Pcontent.*?)(本院认为|本院经审查认为|依据), reasoning: r本院认为(?Pcontent.*?)(依照|判决如下|裁判如下), } def extract_case_id(text): m re.search(r(\d{4}).*?民(初|终)字第?(\d)号, text) return f{m.group(1)}-民{m.group(2)}-{m.group(3)} if m else unknown def normalize_whitespace(s): return re.sub(r\s, , s).strip() def raw_doc_to_record(fp): raw_text fp.read_text(encodingutf-8) record {case_id: extract_case_id(raw_text)} for field, pat in REGION_PATTERNS.items(): m re.search(pat, raw_text, re.S) record[field] normalize_whitespace(m.group(content)) if m else return record with open(OUT_FILE, w, encodingutf-8) as fout: for fp in Path(RAW_DIR).glob(*.txt): rec raw_doc_to_record(fp) fout.write(json.dumps(rec, ensure_asciiFalse) \n)逻辑说明脚本按四个语义区域粗切核心是用“经审理查明”“本院认为”这类固定边界做锚点。extract_case_id 匹配“2023某市民初188号”这种常见案号匹配不到写 unknown留到后面人工补。REGION_PATTERNS 里的 content 是命名分组re.S 让点号跨行匹配保证“本院认为”之后到“依照”之前的整段判理被完整捕获。参数说明三个地方需要按自己的语料调。一是 REGION_PATTERNS 的边界词刑事判决书常用“本院认为”前面是“公诉机关指控”民事判决书是“经审理查明”都换成实际表述。二是 extract_case_id 的正则不同法院在案号写法上有差异有些直接写“民初第188号”没有年份建议先跑一遍统计未命中比例超过 5% 就补正则。三是 normalize_whitespace 会把半角全角连续空白都压成单个空格千万别省判决书排版噪声会干扰后面所有分词和向量化步骤。提示清洗后的 JSONL 一定要保留原始文本路径和切分版本号后续调模型时才能回溯是哪一批语料导致效果变化。2.3 混合式问答的最小链路召回 → 精排 → 生成混合式系统把问题拆成三段每段各自可替换可回退。第一段召回用户 query 同时走 BM25 和向量检索分别从法条库和判例库取 top 50。第二段精排用交叉编码器把召回结果逐条与 query 算分取 top 5。第三段生成把 top 5 的条文和判例摘要按模板拼进 prompt让生成模型输出答案并强制它只能在检索片段里摘录细节。这个三段链路成立的关键是每段失败都能降级召回返回空就退回关键词检索结果而不是直接报错精排分数全部低于阈值就反问用户补充案情而不是硬答。链路参数有经验值。BM25 的 k1 和 b 保持默认附近即可法律文本长b 可调到 0.7 左右避免长度惩罚过度向量召回用 768 维中文向量模型够用精排模型输入长度限制在 512 token 内因为它只看“问题—条文片段”匹配度。生成侧 max_new_tokens 控制在 512 以内temperature 在 0.2 到 0.5 之间温度太高模型爱自由发挥。这条链路最大的好处是每一段都可独立上线哪天向量召回效果不好只换向量模型不动精排和生成模块。3. 神经网络语义层用双塔模型解决“法条和当白话对不上”3.1 短文本匹配的难点与双塔/交叉编码器的取舍这种语义鸿沟落到模型层本质是文本匹配问题给定 query 和候选法条判断它们是否指向同一法律意图。神经网络在这一层的主流形态是句子表征模型常见的是双塔Dual Encoder和交叉编码器Cross Encoder。双塔把 query 和候选各自编码成向量再用余弦相似度打分优点是候选可以离线预计算在线只算 query 向量适合召回。交叉编码器把 query 和候选拼成一句话过模型直接输出匹配分精度更高但候选必须在线逐个算只适合对 top 50 做精排。法律问答里两者的分工很固定双塔做召回交叉编码器做精排。只上双塔不做精排top 5 里经常混入语义相近但法律上无关的对只上精排不做召回就得把全量法条逐条过模型在线延迟不可接受。顺带说明为什么法律文本不首选一维卷积或 LSTM 做句子编码。这类模型在短文本分类上仍有价值但法律问答的输入是长句和带指代的案情描述卷积的感受野和 LSTM 的长期依赖表达都不如预训练 Transformer。LSTM 的主场是时间序列预测这里是问答系统直接用预训练语言模型当主干更省事。图神经网络等结构化方法适合在后文的关系抽取阶段用做语义匹配不是它的主场。3.2 用中文向量模型跑通语义召回最小可运行实验先不做复杂训练。我一般在任何法律问答项目里第一周先用现成中文向量模型跑基线看召回质量够不够再决定要不要在垂直语料上微调。最小实验只需要一个脚本from sentence_transformers import SentenceTransformer, util model SentenceTransformer(BAAI/bge-large-zh-v1.5) query 我把钱借给朋友他没有还怎么办 law_chunks [ 民法典第六百七十九条 自然人之间的借款合同自贷款人提供借款时成立。, 刑法第二百六十六条 诈骗公私财物数额较大的处三年以下有期徒刑……, 民法典第六百六十七条 借款合同是借款人向借款人借款到期返还借款并支付利息的合同。, ] q_vec model.encode(query, normalize_embeddingsTrue) chunk_vecs model.encode(law_chunks, normalize_embeddingsTrue) scores util.cos_sim(q_vec, chunk_vecs)[0] for c, s in sorted(zip(law_chunks, scores), keylambda x: x[1], reverseTrue): print(round(s.item(), 4), c)逻辑说明encode 把 query 与三条候选法条分别向量化normalize_embeddings 让余弦相似度等价于内积结果才可比较。输出会看到“借款合同成立”这条分数明显高于诈骗条文这正是要的语义召回效果。注意 bge 系列对短查询有加指令前缀的惯例不加的话相似度会整体偏低新手很容易拿到全面低分然后怀疑模型坏了。参数说明normalize_embeddings 必须开不开的话直接比较原始向量bge 在原始向量空间里不保序。util.cos_sim 返回二维张量取 [0] 再排序。候选条数少时看不出问题实际落地时候选库几十万条双塔的代价在第一次全量向量化千万级文本在一张 A100 上也要数小时但只跑一次之后存向量数据库即可。检索时的主要参数是 top_k常见做法是先取 50交给精排再砍到 5不要在召回阶段就把 top_k 缩到 5否则精排失去意义。3.3 相似度阈值怎么定三类参考值与按案由校准向量相似度不是“大于 0.8 就相关”这种绝对说法。我在多个法律项目里验证过的经验是同一个模型、同一个向量库不同案由下阈值要分开调。民间借贷类表述高度集中0.72 以上基本靠谱知识产权类长句多、术语杂0.68 以下就已经开始出现误召回劳动争议里当事人常把加班费和经济补偿混在一起说相似度高但法律意图可能完全相反。所以阈值必须分桶校准把测试集按案由分组对每组画 Precision-Recall 曲线取 F1 最大的点做该组阈值。只维护一个全局阈值是法律问答最容易翻车的点之一。校准方法是准备 300 条问答对每条标注与候选法条是否相关1/0分别计算每个候选阈值下的准确率召回率再按案由分桶求最优值。法条库规模大时负样本不够是常态此时可以参考“前 50 召回里至少存在一条真正相关”作为召回阈值精排阈值则参考“top 5 中允许出现几条误召回”。这套做法把相似度阈值从玄学变成可度量的迭代依据每换一次模型就重新采一次阈值。4. 生成式回答用 LoRA 微调法律对话模型并约束输出4.1 为什么检索答案不能直接回复给用户召回和精排给出了条文与判例但把“第六百七十九条 自然人之间的借款合同……”直接甩给用户产品上不成立用户看到法条编号还是不知道该怎么办。法律问答系统的最后一步必须把检索结果转译成对话式回答既要解释法条含义也要告诉用户下一步该收集什么证据、起诉还是调解。这段转译天然是生成模型的工作。但生成式模型在法律领域有两个限制一是通用对话大模型没读过足够多的法条原文和判决说理直接提问会得到“我无法提供法律意见”的保守回答或者一本正经编一个不存在的案号二是模型参数越大微调成本越高很多法律团队没有从头训练的条件。常见解法是保留通用大模型的底座在自建法律语料上用 LoRA 做参数高效微调只更新一小部分低秩矩阵就能获得可用的“法官律师”混合说话风格。4.2 用 PEFT 微调最小可运行代码与关键参数微调前要准备对话样本把上一章的 JSONL 记录改写成问答对。例如 prompt 是“用户欠钱不还出借人向法院提供了银行转账记录和聊天记录是否会被认定为民间借贷”answer 是“根据民法典第六百七十九条自然人之间借款合同自贷款人提供借款时成立。银行转账记录能证明款项交付聊天记录中的‘借’字表述可证明借贷合意……”。一条样本就是一个 (prompt, answer) 对数据量不需要上百万几千条高质量对话对就能见效。训练脚本在单卡场景下通常长这样from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training import torch dataset load_dataset(json, data_filesdata/legal_qa_train.jsonl)[train] model_name Qwen/Qwen2.5-7B-Instruct # 预算有限时换成 1.5B 版本 tokenizer AutoTokenizer.from_pretrained(model_name, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained( model_name, torch_dtypetorch.bfloat16, device_mapauto, load_in_4bitTrue, ) model prepare_model_for_kbit_training(model) lora_config LoraConfig( r16, lora_alpha32, lora_dropout0.05, target_modules[q_proj, k_proj, v_proj, o_proj], task_typeCAUSAL_LM, ) model get_peft_model(model, lora_config) training_args TrainingArguments( output_dircheckpoints/legal_lora, per_device_train_batch_size2, gradient_accumulation_steps4, learning_rate2e-4, num_train_epochs3, logging_steps50, save_steps500, bf16True, ) trainer Trainer(modelmodel, argstraining_args, train_datasetdataset, tokenizertokenizer) trainer.train()逻辑说明load_in_4bit 开 4bit 量化单张 24GB 显存就能训练 7B 模型prepare_model_for_kbit_training 确保量化模式下可训练。LoraConfig 的 r16 是低秩矩阵的秩lora_alpha32 是缩放系数两者配合决定微调强度。target_modules 要按模型实际模块名写不同底座不同换模型前先打印 model.named_modules() 确认。训练目标是语言建模 lossdataset 预处理要把 prompt 和 answer 拼成一个文本并截断到 max length一般 2048 足够避免把多轮对话拼成超长样本。参数说明learning_rate2e-4 对多数开源对话模型够用太小低于 5e-5 往往训不动太大高于 5e-4 容易把底座知识压制掉回答风格变生硬。per_device_train_batch_size 在量化后可以开到 2 或 4显存不够就把 gradient_accumulation_steps 从 4 提到 8。num_train_epochs 控制在 2 到 3 轮法律问答数据量小多轮会过拟合典型表现是回答里开始重复训练集的原句。注意target_modules 必须与基座模型真实模块名匹配换底座前先打印模型结构确认否则 LoRA 不会生效但也不报错。4.3 让模型不编造法条引用、免责声明与不确定性表达LoRA 微调解决风格和领域知识但模型依然可能编造。法律场景编造的风险极高引用一条被废止的司法解释会让系统一夜之间失去信任。所以系统设计上要用“格式化输出约束 生成前检索约束”双保险。生成前检索约束在混合链路里做了答案必须先基于检索片段这里再做一层格式化约束让模型只输出固定结构。prompt 模板一般如下def build_answer_prompt(query, law_chunks, judge_reasons): chunks_text \n.join([f[法条{i1}] {c} for i, c in enumerate(law_chunks)]) reasons_text \n.join([f[判例{i1}] {r} for i, r in enumerate(judge_reasons)]) return f你是法律问答助手。请只引用下面给出的法条和判例回答不得引用未提供的来源。 {chunks_text} {reasons_text} 用户问题{query} 输出要求 1. 若法条和判例不足以回答明确说明“现有资料无法完整回答”并列出还需要的信息。 2. 法条引用格式为“民法典第六百七十九条”。 3. 结尾附一句“以上信息仅供参考不构成法律意见”。 回答逻辑说明模板把检索片段按序号列在问题前模型没接触过片段之外的法条生成时的引用范围被卡在检索结果里。输出要求的三条都是可被代码检查的硬约束回答里若出现“[法条”编号以外的数字引用就判定不合规结尾缺免责声明就重生成或退回检索答案。参数说明这个模板配合生成参数使用temperature 建议 0.2 到 0.5top_p 0.8 到 0.9。温度越高输出越多样但对需要稳定引用的法律问答来说多样性是敌人。max_new_tokens 不要给太长512 足够容纳一段问答超过 512 的答案意味着模板设计有问题长度不是质量指标。调试时如果模型无视模板先检查检索结果里是否真的包含对应法条编号很多时候是检索侧把它裁掉了模型根本没看见可引用的条文于是被迫自行发挥。5. 法律问答系统常见问题排查五条从现象到修复的踩坑记录出现问题时我固定的排查顺序是先看检索结果能不能找到正确答案再看生成模型有没有被允许自由发挥最后看数据分布是否在掩盖问题。这三步能区分八成的故障来源。下面五条是反复遇到的现象。5.1 引用已废止法条时效链断裂现象系统回答“根据《合同法》第八条……”而《合同法》已经废止。用户按该条文主张权利被法院驳回。原因法条库导入了历史文本但没有维护条文的效力状态。检索模型不会自动区分现行有效和已废止它在语料里看到“合同法”与“借款”语义相关就把它召回。解决预处理时给法条表加 valid_until 和 status 字段status 取“现行有效/已废止/已修改”检索前用过滤器只保留“现行有效”。涉及“已修改”的条文额外维护一张指向替代条文的映射表。生成侧的引用格式也要携带效力字段供前端展示时标注。这条排查起来不难难的是建立机制法条库要有每日同步和变更日志每次同步都 diff 出新增、废止、修改的条目。5.2 当事人用词不同导致结果漂移现象同一案情用户写“借条”“欠条”“借钱不还”系统给出的答案完全不同甚至把欠条案导向买卖合同纠纷。原因三种表述语义相关但法律性质不同借条对应借贷合同关系欠条可能对应货款、工资或损害赔偿指向完全不同。向量模型很容易把“欠条”和“借条”学得很近无法自动区分这些法言法语里的精细差别。解决在召回前增加一个“争议焦点归一化”模块用扩展词表加少量规则把当事人表述映射成标准法律案由欠条、借条都归一为“债权凭证”但追问它的形成原因再把这层归一结果注入 query重新编码进向量召回。词表要人工审不能全量交给模型生成法律场景的错误映射比不映射危害更大。5.3 生成器编造判例案号现象回答中引用“最高人民法院2019最高法民终1234号”检索列表里根本没有这个案号。原因生成模型在训练阶段见过大量案号范式LoRA 微调后又有输出压力于是在两个真实案号之间插接了一个不存在的组合。案号幻觉是法律问答生成侧最隐蔽的毛病因为案号格式太规整人眼都不一定能第一时间识破。解决用代码做案号白名单校验。回答中包含案号时必须能在本次检索结果的“案例号”字段里找到完全匹配项找不到就把该句标记为可疑执行“删除该句并重生成”的降级逻辑。更保险的做法是让模型只在模板里填“参照上述第 2 个案例”案号由前端从检索结果动态拼装模型完全不产出案号从根上断掉幻觉。5.4 长文本被截断关键事实丢了现象输入刑事判决书原文模型把“二审改判有期徒刑缓期执行”这类关键结论完全忽略回答基于了一审判决认定的事实。原因BERT 类模型输入上限普遍是 512 token长判决书会被硬截断。而判决书的关键结论恰恰在文书后半段——“本院认为”和“判决如下”的位置粗暴截断等于把答案区切没了。解决不要在模型侧做全文截断要在预处理侧做要素抽取式分段先用规则把判决书切分成“指控/查明/认为/判决”四段只把“查明事实”和“本院认为”送入模型若这两段仍超长用滑动窗口 256 步长 128 切块逐块过模型后按位置权重融合。这个方案牺牲整体阅读能力换来关键结论不丢。5.5 评估指标好看但分案由不均衡现象整体准确率 91%上线后发现劳动纠纷答得不错知识产权和破产案件一塌糊涂。原因测试集和训练集一样不均衡劳动纠纷样本占七成平均指标被少数类目拉高掩盖了长尾案由的低质量。解决评估强制按案由分桶报告每一桶的准确率、召回率、驳回率和法条命中率设置最少桶数量门槛如每个案由至少 50 条测试样本少于 50 条的列为灰色地带禁止进入量化统计。这一步不是技术指标问题而是交付物边界问题要让甲方知道系统在标注数据充足的案由上是稳定的在样本不足的案由上处于探索状态而不是假装全面可用。6. 从回测到上线一个用公开裁判文书做评估的小技巧上线前最值得做的验证是端到端回测从公开裁判文书里抽取 200 个真实问答对跑一遍完整链路计算“答案是否正确引用至少一条有效法条”和“答案是否覆盖裁判结论”两个指标。这个回测脚本要写得足够简单能放进 CI 里每次发版自动跑import json with open(data/eval_200.jsonl, encodingutf-8) as f: cases [json.loads(line) for line in f] def evaluate(system_answer, expected_law, expected_conclusion): hit_law expected_law in system_answer hit_conclusion expected_conclusion in system_answer return hit_law, hit_conclusion law_hit, conclusion_hit 0, 0 for case in cases: answer legal_qa_system.answer(case[query]) # 你的完整链路 lh, ch evaluate(answer, case[expected_law], case[expected_conclusion]) law_hit lh conclusion_hit ch print(f法条命中率: {law_hit/len(cases):.2%}) print(f结论命中率: {conclusion_hit/len(cases):.2%})逻辑说明expected_law 设计成“民法典第六百七十九条”这种精确表达expected_conclusion 设计成“借款合同成立”这种判决核心词。两个指标分开统计能看出是检索侧问题还是生成侧问题法条命中率低去查召回和精排结论命中率低去查生成模板和 LoRA 权重。参数说明200 条是下限最好按案由均匀抽取避免再犯上一章的分桶不均衡问题。这个脚本不要贪多加额外指标会让它在 CI 里跑得既慢又不稳定。进阶方向有三个。一是把多轮对话状态加进来让系统在用户第一次没说清时主动追问“有没有转账记录”“对方写了借条还是欠条”而不是一次答完。二是用图神经网络把当事人、法条、担保物、金额实体建成关系图辅助召回和证据链梳理这比在纯文本上堆模型更能体现法律结构。三是做增量更新法条库变更后只重编受影响向量的索引不用全量重跑。我自己最大的教训是上线前过度优化生成模型忽略了检索侧一条已废止法条被召回结果生成得越流利错得越完整。现在每次迭代都先跑回测把法条命中率钉在第一位再谈流畅度。这套习惯救了我好几次希望帮到你。本文还有配套的精品资源点击获取
返回列表