简介面向希望学习人工智能与自然语言处理技术的学习者这份基于神经网络的法律智能问答系统提供了从数据构建到模型训练再到交互界面的完整工程案例适用于毕业设计、课程作业或项目实训。资源共包含30个文件主要类型为11个CSV数据文件、6个Python源码、5个TXT语料与停用词表、5个编译缓存及2个训练模型压缩包整体约37.48MB结构清晰便于按模块研读与复用。已有126人浏览学习。项目以法律领域问答为场景覆盖劳动合同、工伤保险、员工权益等热点议题代码中实现了相似度匹配与神经网络分类等关键逻辑并附带GUI程序。通过学习可掌握数据清洗、特征处理、模型保存与调用等完整流程也可在此基础上替换语料拓展至其他垂直领域具备较强的实用性与二次开发价值。1. 法律智能问答系统为什么神经网络方案值得认真做用户输入「试用期最后一天被辞退公司要赔钱吗」传统法条检索系统能查出《劳动合同法》相关条目但用户要的是「要赔而且按工作年限算经济补偿」这样一句能直接落地的回答。基于神经网络的法律智能问答系统就是把「查法条」这个动作升级为「读问题、找依据、组织答案」以深度神经网络为核心在问答对上训练让模型学会把用户口语映射到法律语义空间。这个方向适合三类人做法律科技产品的团队、给律所或企业法务搭建知识库的工程师、想把 NLP 落到垂直领域的算法同学。它不一定能在通用榜单拿高分却能在真实咨询场景里省掉大量重复劳动。2. 先定架构再写代码三类神经网络问答方案怎么选做法律问答系统最容易犯的错是拿到语料直接上 Transformer。先花半天想清楚架构能省下后面两周返工。法律问答和开放域闲聊的最大差别在于答案要有据不能顺着概率把法条编出来。架构选型本质上是在「可控性」和「自然度」之间做取舍不同取舍对应完全不同的一套代码。2.1 检索式、生成式、混合式各自吃掉哪类法律问题法律咨询里大约六成是高频 FAQ试用期辞退、离婚财产分割、合同违约这类答案稳定且变化少剩下四成是开放长尾用户描述一段具体案情问怎么办这种问题必须让神经网络理解案情细节再组织回答。纯检索式系统对用户来说像个黑匣子你并不知道它为什么没召回而且用户换个说法就彻底失效。架构典型实现优势劣势适合问题检索式BM25、向量检索匹配 FAQ / 法条库可控、可溯源换说法即召回失败高频标准化咨询生成式Seq2Seq、LSTM、Transformer 直接生成答案表达自然、能组合案情法条幻觉严重、难溯源开放长尾案情混合式先检索法条再抽取或生成既给依据又给推理工程链路长、调参点多生产环境的主方案我一般建议第一版直接做混合式但不是一上来就搭完整链路而是先用生成式模型跑通把错误样本攒一批再决定加不加检索。生成式模型在这个场景里最大的优势是能吸收整段案情描述比如「公司以我业绩不达标辞退我但我入职时签的合同写明了提成比例」这类复合信息检索式会拆得七零八落。如果后续把案由、法条、审判结果建成知识图谱图神经网络可以做答案推理的辅助但工程落地里用得少别被论文带偏。2.2 为什么 CNN、BP 神经网络和前馈神经网络做不了法律问答一维卷积神经网络TextCNN在文本分类上是经典选择但它用固定尺寸卷积核在局部窗口里提取 n-gram 特征不擅长跨长距离组合信息。法律案情描述动辄几百上千字第 40 行的事实要和第 300 行的争议焦点关联卷积核窗口根本够不着。BP 神经网络和前馈神经网络本质上是静态特征映射器不建模序列顺序把「小王借了钱没还」和「钱没还小王借了」当成同一输入这在法律语义里是完全相反的两个命题。RNN 循环神经网络能建模序列但标准 RNN 在长文本上梯度消失严重实践中基线直接用 LSTM 神经网络靠遗忘门和输入门把信息在长序列里留住。还有一层隐藏原因数据规模。法律问答对通常只有几万条甚至几千条大模型动辄上亿参数一上去就过拟合。这个领域有个没写进论文的玄学参数规模越大、训练数据越少幻觉越严重。所以第一个版本别追求模型大先把小模型基线的错误格局看清楚再说。2.3 最小可跑的 LSTM Seq2Seq 基线我不太建议直接照搬网上开源的法律问答模型常见做法是自己基于 Seq2Seq 框架搭一个最小基线数据量小、跑得快、方便定位问题。下面这个结构是经典 encoder-decoderencoder 用 LSTM 读用户问题decoder 用 LSTM 逐词生成答案。import torch import torch.nn as nn class EncoderRNN(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim) self.lstm nn.LSTM(embed_dim, hidden_dim, num_layers2, batch_firstTrue, dropout0.3) def forward(self, x, x_len): emb self.embedding(x) packed torch.nn.utils.rnn.pack_padded_sequence( emb, x_len.cpu(), batch_firstTrue, enforce_sortedFalse) _, (h, c) self.lstm(packed) return h, c class DecoderRNN(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim) self.lstm nn.LSTM(embed_dim, hidden_dim, num_layers2, batch_firstTrue, dropout0.3) self.fc nn.Linear(hidden_dim, vocab_size) def forward(self, y, h, c): emb self.embedding(y) out, (h, c) self.lstm(emb, (h, c)) logits self.fc(out) return logits, h, c这里用两层 LSTM 是因为单层对法律问题里的转折结构「虽然……但是……」建模不足两层能捕捉更高阶语义组合。dropout0.3 防止几千条小语料上直接过拟合。embedding_dim 128、hidden_dim 256 是模型容量和显存之间较保守的选择显存吃紧时先降 hidden_dim不要先降 batch_sizebatch_size 太小会让梯度更新噪声变大训练抖动。训练时用 teacher forcingdecoder 每一步输入用真实答案词而不是上一步预测收敛快但引出后文要说的暴露偏差问题。3. 法律语料是真正的护城河清洗、切分与标注的落地细节跑通模型很简单数据才是无底洞。法律语料有三个来源各有各的坑处理不当会让后面所有训练都白做。3.1 原始语料获取与清洗判例、法条与问答对的合并策略裁判文书是量大但噪音最重的来源带固定格式的页眉页脚、案件字号、审判人员名单如果不清理模型会把「本院认为」之前的表单文本也学进去。法律法规库结构最规整但一条法条经常包含多款多项切分粒度要小心一个条款下的两款完全可能是两个独立回答单元。公开法律问答平台最接近用户真实口语是训练问答对的核心数据但混着律师广告和免责声明需要单独过滤。清洗规则上统一繁体到简体、全角标点转半角、删除 URL 和联系方式、过滤掉长度小于 20 字的无关片段。这些操作不高级但如果跳过词表会被各种符号撑大embedding 学出一堆噪音。import re def clean_legal_text(text: str) - str: text text.replace(, ().replace(, )) text text.replace(, ,).replace(。, .) text re.sub(r[A-Za-z0-9._%-][A-Za-z0-9.-], , text) # 邮箱 text re.sub(r1[3-9]\d{9}, , text) # 手机号脱敏 text re.sub(r\s, , text) return text.strip()正则里第一个是邮箱第二个是大陆手机号公开数据里经常夹带当事人隐私上线前必须统一脱敏这不是研发技巧问题是数据合规底线。另外注意清洗顺序先把标点统一再做正则脱敏最后压缩空白顺序反了会出现「手机号中间被换行截断、正则匹配不上」的尴尬情况。3.2 滑窗切分与答案重定位让长判决书变成训练样本裁判文书平均几千字直接整篇丢给 LSTM 不现实。常见做法是滑窗切分按 256 或 512 token 切成片段与问题配对然后判断答案是否完整落在片段内。答案被切断是最常见的翻车点用带重叠的滑窗能缓解。def slide_windows(tokens, window256, stride128, answer_spanNone): windows [] for start in range(0, len(tokens), stride): end min(start window, len(tokens)) windows.append([start, end, tokens[start:end]]) if end len(tokens): break if answer_span is None: return windows merged [] for start, end, w in windows: if answer_span[0] start and start 0: merged[-1][1] end merged[-1][2] w continue merged.append([start, end, w]) return mergedwindow 控制单样本最大长度stride 控制窗口重叠。stride 太小样本冗余度过高训练慢太大容易切丢答案。我一般设 stride 为 window 的一半这个比例在大多数判决书上都能覆盖到答案。merged 的逻辑是把跨窗口答案补到前一个窗口保证至少有一个样本包含完整答案片段。更稳妥的做法是切完后再校验一次答案 token 序列是否完整出现在窗口里没有就直接丢弃该样本不要硬凑。3.3 数据增强与标注一致性两名标注员的冲突怎么处理法律问答对的人工标注成本高一名熟练标注员一天大约能标 150 到 200 条。两个标注员对同一问题给出的答案不一致时别用投票解决法律场景里少数派意见经常更严谨因为多数派容易顺着常识写而忽略例外条款。我建议两标注员加一个法律背景复核三个人出结论。标注流程里要算两人之间的一致性Cohens kappa低于 0.6 说明标注规范没写清楚先回头改规范再继续标不然数据越多模型越乱。数据增强方面法律场景不能乱做同义词替换「甲方」「乙方」「用人单位」「劳动者」这类术语可以互相替换但「赔偿」和「补偿」绝不能互替法律意义上差很远。安全做法是只对问题做句式改写陈述句改疑问句、加口语语气词答案保持原样避免污染。4. 用 PyTorch 训练法律问答模型数据加载、训练循环与评估模型结构定好、数据清洗完进入训练阶段。这个阶段最容易反复折腾的是数据加载和训练策略而不是网络结构本身。4.1 数据加载器与词表构建jieba 分词后的长度控制中文不像英文天然按空格分词。法律文本里专业术语密集用 jieba 时要配自定义词典把「劳动仲裁」「经济补偿金」「竞业限制」这类词加进去不然它们会被切成零碎字。词表构建要设置最小词频否则低频词占满词表embedding 层大部分参数在重复学同一个 UNK。import jieba from collections import Counter from torch.utils.data import Dataset for term in [经济补偿金, 劳动仲裁, 竞业限制, 抚养权]: jieba.add_word(term) def build_vocab(all_texts, min_freq2): counter Counter() for text in all_texts: counter.update(jieba.cut(text)) vocab {w: i 4 for i, (w, c) in enumerate(counter.items()) if c min_freq} vocab[pad] 0 vocab[unk] 1 vocab[bos] 2 vocab[eos] 3 return vocabc min_freq会把只出现一次的词过滤掉词表从四五万压到一两万训练速度和显存占用都会好看很多。i 4是因为 0 到 3 被固定占位符占用顺序无所谓保证词表遍历和索引一致就行。min_freq 设 2 是经验值设 5 以上会让不少法律术语直接进 UNK模型看不懂「竞业限制」时答出来的东西基本没法用。4.2 训练循环损失函数、学习率调度与早停策略损失函数用 CrossEntropyLoss必须把pad位置 ignore 掉否则模型会把大量梯度花在预测无意义的占位符上。起步学习率 1e-3优化器用 AdamWweight_decay 设 1e-4。学习率调度用 ReduceLROnPlateau验证集损失连续两个 epoch 不降就把学习率减半。早停条件是验证集 loss 五个 epoch 不降就停。import torch from torch.utils.data import DataLoader model Seq2Seq(vocab_sizelen(vocab), embed_dim128, hidden_dim256) optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience2) criterion torch.nn.CrossEntropyLoss(ignore_indexvocab[pad]) for epoch in range(epochs): model.train() total_loss 0 for q_ids, a_ids in loader: decoder_input a_ids[:, :-1] decoder_target a_ids[:, 1:] logits model(q_ids, decoder_input) loss criterion(logits.reshape(-1, len(vocab)), decoder_target.reshape(-1)) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() total_loss loss.item() val_loss evaluate(model, val_loader) scheduler.step(val_loss)这里的梯度裁剪很重要LSTM 在长序列上梯度范数很容易冲到几十。每步打印梯度范数是个好习惯如果经常超过 5说明存在异常长样本或学习率偏大。注意 ReduceLROnPlateau 是在每个 epoch 结束后用验证集 loss 调学习率不是每个 batch 调一次这是新手经常搞错的地方。4.3 评估指标BLEU 和 F1 之外还要看引法条准确率BLEU 是 n-gram 重合度指标适合机器翻译这类对表达顺序有要求的任务但对法律问答有欺骗性。模型回答「根据《劳动法》第 57 条不需要赔偿」标准答案是「根据《劳动合同法》第 39 条无需支付经济补偿」语义接近但 BLEU 只有 0.3分不清好坏。所以评估要同时看三个维度。import re from collections import Counter law_pattern re.compile(r《([^》])》第([0-9])条) def check_citation(pred: str, law_article_map: dict) - bool: matched law_pattern.findall(pred) if not matched: return False for law_name, article_no in matched: if law_name not in law_article_map: return False if article_no not in law_article_map[law_name]: return False return True def token_f1(pred_tokens, gold_tokens): pred_counter, gold_counter Counter(pred_tokens), Counter(gold_tokens) overlap sum((pred_counter gold_counter).values()) if overlap 0: return 0.0 precision, recall overlap / len(pred_tokens), overlap / len(gold_tokens) return 2 * precision * recall / (precision recall)law_article_map 是从法条库构造的{劳动合同法: {39: 条文内容, 47: 条文内容}}结构。check_citation 上线后要挂在生成管线后面一旦引用法条不存在就触发兜底或重生成。法条引用准确率是法律问答独有的指标它比任何模型层面的约束都直接因为幻觉的集中体现就是编法条。token-level F1 衡量关键信息有没有答全人工抽评则看有据、完整、可读三个维度每个版本抽 50 条让律师打分这部分工作量不能省。5. 法律问答系统避坑指南五条踩坑记录这些坑有些是自己踩的有些是看同事翻车学到的每一条都对应一次返工。写在这里希望能帮你绕过去。5.1 训练阶段的三条踩坑记录坑一模型编造法条编号而且编得特别像。现象是验证集里六成生成答案都带法条引用其中三成引用的条款在法条库里根本不存在但句子读起来非常顺畅。原因是生成式模型优化的是「下一个词概率最大」法条编号对模型来说只是高频共现符号它没有核对知识源的能力。解决方法是生成管线后面挂法条引用校验也就是第 4 章那个 check_citation引用不存在的直接触发重生成重生成两次都不行就降级为检索式回答。坑二长答案截断后信息残缺。现象是模型在 max_len 设 64 时生成的答案经常答到一半就停验证集 token-F1 上不去。原因是标注答案普遍 80 到 120 字而我把解码长度拍脑袋设为 64。解决方法是先统计标注答案长度的分位数按 P95 设置 max_len该调的是数据统计分析而不是拍脑袋。P95 是多少就设多少模型参数量不变只是生成步数变长显存会相应涨一点。坑三类别不平衡让模型变成「离婚问答机」。现象是模型对婚姻家庭类问题答得不错劳动合同类一塌糊涂。原因是公开语料里婚姻家庭咨询量大约是劳动合同的三倍。解决方法是按案由做分层采样每个案由在每个 epoch 里抽到的样本数保持接近少数类靠重复采样补齐。注意不是简单复制少数类样本而是在一个 epoch 内让每个案由进入 batch 的比例均衡避免连续几个 batch 全是同一案由导致优化方向被带偏。5.2 上线产品阶段的两条踩坑记录坑四用户口语和法言法语之间映射没做好。「我被公司开了有赔偿吗」和「公司单方解除劳动合同未提前通知是否需支付补偿金」在语义上是同一件事但检索或生成的结果完全不同。原因是训练数据里书面语比例太高模型没学会口语到术语的等价替换。解决方法是做一份「口语-术语」同义词典开了等于解除、赔钱等于经济补偿金或赔偿金在生成前先把用户问题改写一轮。等价替换只发生在问题侧不发生在答案侧防止生成带病文本。坑五没有置信度兜底系统硬着头皮回答不知道的问题。现象是模型对超出语料范围的地方性政策问题会生成一段语气笃定的错误回答免费使用阶段无所谓一旦涉及付费咨询就会被投诉。原因是训练时没有定义「不知道」。解决方法是统计验证集样本的预测概率分布设置一个阈值低于阈值时返回固定话术「根据现有资料无法准确回答建议咨询执业律师。」这个兜底是所有优化手段里投入产出比最高的。6. 进阶检索增强让模型学会引法条以及上线前的验证方法单靠 Seq2Seq 模型解决不了幻觉问题检索增强生成RAG才是法律问答落地里最实用的解法。具体做法是把法条库按「法条编号加正文」切成条目用 BM25 对用户问题做召回取 top3 法条拼进生成上下文让模型在参考信息的基础上组织答案。from rank_bm25 import BM25Okapi law_articles [] # 每条形如 劳动合同法#39: 劳动者有下列情形之一的用人单位可以解除劳动合同…… bm25 BM25Okapi([list(jieba.cut(a)) for a in law_articles]) def retrieve_law(question: str, top_k: int 3): scores bm25.get_scores(list(jieba.cut(question))) top_idx sorted(range(len(scores)), keylambda i: scores[i], reverseTrue)[:top_k] return [law_articles[i] for i in top_idx]BM25 的 k1 和 b 参数保持默认值 1.5 和 0.75 在法条库里一般不用调。真正影响召回质量的是切条粒度一条法条按「编号加正文」切完整单元效果最好单独把「第X条」切开会让 BM25 打分失真。召回结果拼进模型输入的格式一般是[上下文] 用户问题decoder 生成时会参考上下文里的法条表述而不是凭空记忆。上线前验证方法准备 200 条没进过训练集的问答对100 条高频、100 条长尾分别跑纯生成基线和 RAG 增强版本对比法条引用准确率和 token-F1。我自己的项目经验是 RAG 对高频问题提升不大因为基线本来就能答对但长尾问题的法条引用准确率明显上升。这个趋势你可以拿自己语料验证。最后说一个我的教训不要迷信「更大模型」能解决幻觉越大的模型越能流畅编造法条它的流畅本身就是 bug。现在我的习惯是每个版本发布前先跑一遍法条引用校验通过率再谈其他指标。法律问答系统的第一原则不是像人而是有据希望帮到你。本文还有配套的精品资源点击获取