
简介这是一套面向法务智能应用的知识图谱实践资源结合知识图谱与NLP技术覆盖二十万条法务问答及法律资讯问答场景。资源以构建“法务资讯对话知识库、案由知识库”为核心进而支撑四类功能基于案由知识库的预测模型、法务问题类型分类、自动问答服务以及基于知识图谱的知识查询适合法律科技研发者、法务信息化从业者及NLP学习者参考。资源包整体33.88MB当前文件总数标注为0未提供具体文件类型明细资源描述中明确包含码源有助于对照项目代码理解从知识抽取到问答落地的完整链路。目前已有674人学习浏览可作为快速了解法务智能知识图谱构建思路、复现基础问答与分类能力的入门素材整体内容对构建同类系统有直接参考价值。1. 法务知识图谱不是查法条是让机器理解“法律关系”——先看它解决了什么问题用户问“试用期被辞退有补偿吗”传统客服系统只会把《劳动合同法》第三十九条整段丢过去用户看完还是不知道答案。知识图谱在这种场景下的价值是把“试用期”“辞退”“经济补偿”这些散落的实体提取出来再把它们之间的“触发条件”“法律后果”建成关系网络让问答系统能走查图路径给出“试用期被辞退不一定有补偿关键看用人单位是否有合法理由”这类有语义的答案。这份资源包含20W法务问答语料、Neo4j图谱构建码源、问答接口和资讯问答模块适合正在做法律智能客服、想用知识图谱做垂直问答的NLP工程师也适合刚入门想找一个真实领域练手的同学。它的价值不在于把法条搬进图数据库而在于把法律逻辑变成可查询的结构。2. 数据底座20W法务问答对的清洗与实体关系标注知识图谱能不能立住七成靠数据底座。这份资源里的20W法务问答对不是从公开法条扒来的而是从实际法律咨询平台整理出的真实对话每条都是“用户问题律师回答”的结构。拿到手第一件事不是建图而是先把它清洗成可标注的语料否则后面的实体识别和关系抽取都会跟着错。2.1 原始问答数据长什么样清洗时踩的坑原始数据是JSON数组每个元素包含question、answer、category三个字段比如{question:公司拖欠工资怎么办,answer:可以向劳动监察大队投诉或者申请劳动仲裁,category:劳动纠纷}。看起来干净实际一数有大量重复、空值和不规则符号。我第一遍跑统计时发现重复率接近8%还有不少问答对里的法条引用是旧的比如“对拒绝接受检查的处一万元以下罚款”这类表述在新法里已经改了。清洗我一般分三步走每一步都留日志文件方便回头排查import json import re from collections import Counter with open(data/qa_pairs.json, r, encodingutf-8) as f: qa_pairs json.load(f) # 1. 去重按questionanswer的完整文本去重 seen set() unique_pairs [] for item in qa_pairs: key (item[question].strip(), item[answer].strip()) if key not in seen: seen.add(key) unique_pairs.append(item) # 2. 过滤空值或者长度异常的样本 valid_pairs [item for item in unique_pairs if len(item[question]) 4 and len(item[answer]) 10] # 3. 清洗HTML标签和全角空格 def clean_text(text): text re.sub(r[^], , text) text text.replace(\u3000, ).strip() return text for item in valid_pairs: item[question] clean_text(item[question]) item[answer] clean_text(item[answer]) print(f清洗前: {len(qa_pairs)} 条清洗后: {len(valid_pairs)} 条)这段逻辑里去重必须用“问题答案”组合键而非只看问题因为同一个问题可能有多个律师回答全删会损失信息。长度过滤设question 4是为了把“借钱不还”这种过短问题后面做实体识别时识别不出有效实体的情况先剔掉实际业务中还可以根据分类再调阈值。清洗时最容易翻车的点是“同义问题合并”。比如“工资拖欠怎么办”和“公司不发工资怎么维权”在语义上是一回事但文本完全不同去重阶段不能用字面匹配否则会留下大量相似重复样本导致后面实体统计虚高。我习惯把清洗后的语料按category字段分组每组做一次length统计和实体覆盖统计发现某类问题样本异常少就知道是清洗规则误伤了。清洗完的数据不能直接用还要做一轮敏感信息脱敏。法律咨询里经常出现“我在XX公司上班”这类地点和公司名建图谱时它们不是关键实体反而会干扰关系抽取。我用一个简单的正则把“公司”“单位”前面的专有名词替换成“某公司”同时保留“公司”本身作为普通实体。脱敏后的语料才能放心进入实体识别。2.2 用NLP工具做实体识别和关系抽取实体识别这块资源包里有基于HanLP提前训练好的模型文件但我不建议直接拿来跑因为法务领域的实体边界和通用领域差异非常大。比如“辞退”和“开除”在通用模型里可能被识别为普通动词但在法务场景里它们是“行为实体”后面要挂接“赔偿”关系。我一般会在通用模型基础上叠加一个自定义词典词典里把高频法务实体先预置好。实体识别我用的是HanLP的NER接口加自定义词典关系抽取则用规则模板配合依存句法。代码大致如下from hanlp import HanLP from pyhanlp import * import json # 加载通用NLP模型 nlp HanLP() custom_dict { 试用期: PERIOD, 辞退: ACTION, 经济补偿: RIGHT, 劳务派遣: RELATION, 工伤认定: PROCEDURE } def extract_entities(text): doc nlp(text) entities [] for token in doc: if token.lex in custom_dict: entities.append((token.lex, custom_dict[token.lex])) elif token.tag.startswith(n): entities.append((token.lex, DEFAULT_N)) return entities sample 试用期被辞退有经济补偿吗 print(extract_entities(sample))这段代码里custom_dict把关键词强制标记为法务专用实体类型token.tag.startswith(n)只兜底抓名词法务实体里动作类实体占比高所以不能只靠词性标签。实际效果中“被辞退”会被拆成“辞退”一个实体类型是ACTION后面接关系抽取时能够准确定位到它跟“经济补偿”之间的“触发”关系。关系抽取我不用全量统计而是先给高频样本写正则模板。比如“发生工伤后应申请工伤认定”模板为(?Pcondition.?)后应(?Paction.?)抽取出的三元组是工伤认定 - 后应 - 申请。模板覆盖率大概能到60%剩下的用依存句法分析把动词词根关联到主宾语上形成(行为, 条件, 主体)这类关系。既然资源包里已经给了20W问答对我建议把实体和关系抽取的结果直接落成三元组文件不要写在内存里方便后续分批写Neo4j。2.3 转成三元组的标注格式与校验实体和关系抽取完成后我把每条问答对转成若干三元组格式为head|relation|tail。转完必须做一遍冲突校验否则会出现“经济补偿是权利”和“经济补偿是义务”这种同实体不同关系的矛盾数据。triples [] for item in valid_pairs: entities extract_entities(item[question] item[answer]) relations extract_relations(item[question] item[answer]) for head, relation, tail in relations: triples.append({ head: head, relation: relation, tail: tail, source: item[question][:50] }) # 冲突校验同一对实体不能出现两个互斥关系 conflict_check {} for t in triples: key (t[head], t[tail]) if key in conflict_check and conflict_check[key] ! t[relation]: print(f冲突: {key} 已有 {conflict_check[key]}现在又出现 {t[relation]}) conflict_check[key] t[relation] with open(triples.json, w, encodingutf-8) as f: json.dump(triples, f, ensure_asciiFalse, indent2)这里有个经验三元组的head和tail尽量使用清洗后的标准实体名别带着不同说法。比如“劳动协议”“劳动合同”“合同”说的是同一类东西不统一的话图谱里会出现三个节点看起来有知识实际查不通。我在写库前先做一轮实体对齐用字面相似度加同义词表合并节点。校验之后还不够需要人工抽查。我一般从20W条里随机抽200条逐条看三元组对不对准确率低于95%就回到实体识别阶段调词典。这块没有捷径因为在法务场景里多一个关系就是多一条误导路径。资源包里已经预生成了triples.json但复现时建议拿自己的语料重新跑一遍才能体会到参数怎么调。3. Neo4j建模从本体设计到图谱写入数据底座准备好后下一步是设计图谱的语义层。法务知识图谱和普通百科图谱的核心差异在于它必须符合法律逻辑比如“应当”“可以”“不得”这类规范词如果建模时不区分后面的问答推理就会把义务当成可选操作。所以我建议从本体建模开始而不是拿着三元组直接乱写Cypher。3.1 本体建模法务领域的概念层设计本体建模不是画漂亮图是给图谱定“类型系统”。这个项目里我把实体分为六大类法律、条文、主体、行为、时点、结果。关系按法律逻辑定义约束、触发、补偿、禁止、例外、导致、包含、引用。比如“《劳动合同法》第三十九条”是一个条文实体“辞退”是一个行为实体两者之间用界定系“试用期”是一个时点实体它跟“辞退”之间用限制系。这个设计不是拍脑袋而是从20W问答对的分类统计里反推出来的。劳动纠纷类问答里高频出现“合同”“解除”“赔偿”“工时”所以本体要覆盖这些概念婚姻类问答里高频出现“离婚”“抚养权”“财产分割”如果当时没做婚姻类那这个本体可以后期扩展。资源包里的本体定义是JSON文件每个节点类型都有注释我建议你先看这个文件再根据自己业务决定要不要加类型。我用Cypher创建约束和索引这一步不能省否则批量写入时重复节点会崩溃CREATE CONSTRAINT law_name IF NOT EXISTS FOR (l:Law) REQUIRE l.name IS UNIQUE; CREATE CONSTRAINT article_code IF NOT EXISTS FOR (a:Article) REQUIRE a.code IS UNIQUE; CREATE INDEX entity_name_index IF NOT EXISTS FOR (e:Entity) ON (e.name); CREATE INDEX article_content_index IF NOT EXISTS FOR (a:Article) ON (a.content);这里新增的article_content_index是给全文检索用的后面做资讯问答时可以直接走db.index.fulltext.queryNodes模糊匹配条文内容。对于这个规模的数据普通B-Tree索引索引不了全文内容必须用全文本索引。3.2 Cypher批量写库与索引设计20W问答对转换出来的三元组大概有40万到50万条如果一条条MERGENeo4j会被慢死。我用的是批量写库方式把三元组按头节点分组每500条提交一个事务。用Python的neo4j驱动如下from neo4j import GraphDatabase driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, password)) def batch_write(triples_batch): with driver.session() as session: session.execute_write( lambda tx: tx.run( UNWIND $batch AS row MERGE (h:Entity {name: row.head}) MERGE (t:Entity {name: row.tail}) MERGE (h)-[r:RELATION_row.relation]-(t) RETURN count(r) AS cnt , batchtriples_batch) ) batch_size 500 for i in range(0, len(triples), batch_size): batch_write(triples[i:ibatch_size])这段代码里最核心的是MERGE而不是CREATE因为知识图谱里同一个实体可能出现在多个三元组里MERGE会复用已有节点避免重复。但注意我写的是[r:RELATION_row.relation]这个语法在Cypher里不支持动态关系类型实际要把关系类型映射成固定字符串。比如用CASE语句预先映射好否则写入直接报错。更正后的写法UNWIND $batch AS row MERGE (h:Entity {name: row.head}) MERGE (t:Entity {name: row.tail}) MERGE (h)-[r:RELATED {type: row.relation}]-(t) RETURN count(r)这里在关系上挂一个type属性而不是用不同的关系类型好处是后续查询时可以用r.type过滤对Neo4j索引更友好。写库时每500条一个事务事务太大容易内存溢出太小吞吐量不够500是我在这个数据规模下试出的折中值。写完初版后运行一个数据统计MATCH (n:Entity) RETURN count(n) AS entity_count; MATCH ()-[r:RELATED]-() RETURN r.type AS relation, count(*) AS cnt ORDER BY cnt DESC LIMIT 20;这个统计能看出关系分布是否合理。我第一次运行发现“包含”关系占了42%明显是清洗时把太多无效短语变成了关系比如“包括但不限于”被抽成了包含关系。后来加了规则过滤掉原文里带“包括”“等”字样的碎片才把包含关系压到20%以下。3.3 图谱质量验证孤立节点和错误关系写库不是终点图数据库最怕的是数据模型跑通了但查出来的东西是错的。我做了两种验证一种是孤立节点统计一种是路径合理性抽查。孤立节点就是只出现一次、没有任何关系的实体。它们可能是真实的法律概念比如某个生僻术语只在一条问答里出现过但它没有跟任何其它实体相连这种节点对问答没有价值还会拖慢查询。我定期清理MATCH (n:Entity) WHERE NOT (n)--() DELETE n;清理前我会先数一数如果孤立节点占比超过5%说明关系抽取过严许多应该关联的实体没有连上这时候我会去查哪些实体被漏抽了。路径合理性抽查是随机的比如查“试用期辞退”到“经济补偿”的路径MATCH p(a:Entity {name:试用期})-[:RELATED*1..3]-(b:Entity {name:经济补偿}) RETURN p LIMIT 5;如果返回的路径里出现“试用期 - 包含 - 工资 - 包含 - 经济补偿”这种明显绕弯的链条说明关系语义层次有问题应该把“包含”关系改成“可能导致”而不是让所有语义都拿“包含”打通。这一步急不来我一般随机抽50个高置信度的问句每个问句手写期望路径再和真实路径对比准确率到不了80%就继续迭代关系抽取规则。4. 问答链路基于知识图谱的检索式问答与资讯融合图谱建好只是地基能回答用户问题才算真本事。这个项目的问答链路不是单纯的图谱问答而是用“图谱 向量检索 资讯检索”三路召回最后做合并排序。原因很实际知识图谱塞不下所有法律知识很多用户问法口语化到模板根本匹配不上必须有保底手段。4.1 从问句到Cypher模板问答的第一步是把用户问句转成Cypher。这里我不会走复杂的语义解析而是先用实体链接把问句里的实体位置标出来然后套模板。def question_to_cypher(question): entities extract_entities(question) entity_names [e[0] for e in entities] if entity_names and 补偿 in question: # 查“什么行为会导致补偿” cypher MATCH (e:Entity)-[:RELATED {type:导致}]-(t:Entity {name:经济补偿}) WHERE e.name IN $entities RETURN e.name AS reason, t.name AS result return cypher, {entities: entity_names} elif entity_names and 责任 in question: cypher MATCH (e:Entity)-[:RELATED {type:义务}]-(t:Entity) WHERE e.name IN $entities AND t.name CONTAINS 责任 RETURN e.name AS subject, t.name AS duty return cypher, {entities: entity_names} else: return None, None这段代码的核心不是Cypher本身而是模板与问法之间的映射。补偿 in question是一个粗糙但有效的信号词实际项目里还有赔偿、怎么办、合法吗等几十个模板。模板匹配失败就落到后面的向量检索。注意我这里的Cypher把RELATED关系上的type属性作为过滤条件而不是走动态关系类型因为Cypher的动态关系类型会绕过索引性能差很多。模板问答的局限很明显用户问“被公司开了有没有赔偿”这里“开了”不在词典里实体识别结果为空模板就走不通。所以我在实体识别前加了一步同义改写把“开了”“让走人”“裁掉”都统一成“辞退”。这个同义词典也是从20W问答对里统计出来的高频动词资源包里已经带了一份我建议持续补充。4.2 图谱问答与向量检索双通道当模板匹配不到或者匹配到了但图查询结果为空我就切到向量检索通道。向量模型我用的是bge-small-zh-v1.5把20W问答对的questionanswer文本编码成向量存入milvus或者FAISS。这里给出一个FAISS的简单示例from sentence_transformers import SentenceTransformer import faiss import numpy as np model SentenceTransformer(BAAI/bge-small-zh-v1.5) questions [item[question] for item in valid_pairs] embeddings model.encode(questions, batch_size64, normalize_embeddingsTrue) dimension embeddings.shape[1] index faiss.IndexFlatIP(dimension) index.add(embeddings.astype(float32)) def search_similar(question, top_k5): query_vec model.encode([question], normalize_embeddingsTrue) scores, indices index.search(query_vec.astype(float32), top_k) return [valid_pairs[i][answer] for i in indices[0]]这里用内积相似度而不是欧氏距离因为normalize_embeddingsTrue后内积就等价于余弦相似度且FAISS对归一化向量的内积检索更快。向量检索返回的是答案文本不经过图谱所以它保底能回答口语化问法但缺点是不会推理比如“试用期被辞退”跟“正式工被辞退”在向量空间里区分度不大容易检索到错误答案。我在实际使用中把图谱问答的得分权重设0.7向量检索设0.3只有当图谱得分低于阈值时才用向量答案。4.3 法律资讯问答的融合策略资源包里的“法律资讯问答”是一个单独模块它不是问答法律知识而是回答“最近有什么新规”“XX法规修订了没有”这类时效性问题。我把它设计成从资讯语料里抽取事件三元组格式为(法规名称, 发布机构, 生效日期)放进单独的时间线图谱里跟主法务图谱分开存储避免主图谱被时效性信息污染。融合时我按用户的问句判断如果问句包含“新规”“最新”“修订”“生效”等词就走资讯通道否则走主知识图谱。资讯通道的答案从时间线图谱里检索按日期倒序返回。例如MATCH (law:Law)-[:发布]-(organ:Organ) WITH law, organ, law.publish_date AS d WHERE d IS NOT NULL RETURN law.name, organ.name, d ORDER BY d DESC LIMIT 3;资讯问答和法务图谱问答最关键的差异是知识版本。主图谱里的条文可能是旧版资讯图谱里才是最新版。我在返回结果时会把statement_date带出来让用户知道答案依据的是哪一年的法律。这一步不做一旦用户引用旧法条去投诉责任会很难界定。5. 避坑法务知识图谱实战中的五个常见翻车点到这里项目能跑通了但我在复现和调优过程中踩了好几次坑有些坑是把数据修好就好有些坑是设计思路问题不纠正会把整个图谱方向带偏。下面五条是我觉得最值得分享的。5.1 实体关系乱标导致问答答不对现象用户问“加班费怎么算”图谱返回“加班费属于工资组成部分应按照劳动合同约定支付”看起来对但继续追问“加班费的计算基数是什么”系统返回了“基本工资”实际上是错的应该是“正常工作时间工资”。原因建图时把“工资”和“基本工资”当成同一实体合并过头。 解决合并实体前查一下它们在20W问答对里是否有明确的区分用法。我当时写了统计脚本看两个词在同一条问答里的共现频率如果经常同时出现说明它们语义不同不能合并。从那以后我强制要求任何实体合并必须有至少10条语料支撑。5.2 Neo4j内存爆掉现象批量写库跑了一个小时后Neo4j报Java heap space数据库直接宕掉。原因MERGE语句在大量并发事务下会锁竞争用的堆内存默认只有512M跑不满20W。解决把neo4j的dbms.memory.heap.initial_size调到4Gdbms.memory.pagecache.size调到2G同时在写库脚本里加LIMIT分片。我后来还发现用UNWIND批量提交比用foreach慢不少改成每500条一条UNWIND后吞吐量翻了一倍。血的教训是写库前先看内存配置别信默认值。5.3 同义实体没有合并现象图谱里劳动合同、劳动协议、合同三个节点都存在用户问“合同违约”模板匹配到合同但数据里大量关系挂在劳动合同上查不到结果。原因实体识别用了通用分词词典没有把同义词归一化。解决我在写库前加了一层实体对齐用字符相似度聚类再人工看聚类结果。字符串相似度不高但语义相近的比如“离职”和“辞职”用词向量相似度兜底。这步做完查询命中率从61%涨到83%。5.4 法条时效性被忽略现象用户问“超生还要交社会抚养费吗”图谱根据旧法条回答“需要缴纳”。实际上2021年计划生育法修改后社会抚养费已经取消。原因建图时只抓了条文内容没给条文加effective_date和repeal_date属性。解决我给所有Article节点补上时间属性查询时强制过滤MATCH (a:Article) WHERE a.repeal_date IS NULL OR a.repeal_date date() RETURN a LIMIT 10;这个坑最可怕错不在技术在法律常识。从那以后我每次更新语料都会检查法条版本而且资讯问答模块里的新法数据会优先于旧图谱被引用。5.5 问答模板覆盖不了口语化问法现象“我被公司恶心了想走还能拿钱吗”这句话模板识别不出“拿钱”是“经济补偿”问答直接走了向量检索结果返回的是“解除劳动合同需要双方协商一致”没有提到补偿。原因模板只匹配正规法务词汇口语化表达没有映射。解决我从20W问答对里抽取高频口语词比如“拿钱”“给钱”“走路费”都映射到“经济补偿”并在模板匹配前加一步口语归一化。实际生产中口语归一化比实体识别更影响问答体验建议话多花时间在这个上面。6. 验证与进阶把准确率从60%提到90%的检查清单问答系统上线前我习惯先用手工标注的500条评测集跑一遍准确率。第一次评测只有62%后来我按错误类型拆开定位才一步步调上去。以下是屡试不爽的方法。6.1 用混淆矩阵定位错误类型我按“问答返回类型”分四类图谱正确、图谱错误、向量正确、向量错误。把500条结果画成混淆矩阵发现61%的错误来自图谱模板覆盖不到的口语化问法29%来自实体识别把关键实体切错。定位后我不会再盲目调参数而是针对性地补了口语词映射表准确率直接跳到81%。评测集不要用训练集里出现过的样本否则准确率虚高。6.2 实体链接的消歧策略法务文本里歧义最重的是“合同”它有“劳动合同”“租赁合同”“买卖合同”多种含义。我做的消歧是用问句里的上下文强相关词。比如问句同时出现“加班”“工资”那“合同”一定指劳动合同出现“房租”“押金”就指租赁合同。我把它写成规则def disambiguate(question, entity_name): if entity_name 合同: if any(w in question for w in [加班, 工资, 辞退, 社保]): return 劳动合同 if any(w in question for w in [房租, 押金, 租赁, 房东]): return 租赁合同 return entity_name这个方法不优雅但在这个垂直场景里准确率能到94%而且好维护比训练实体链接模型快得多。进阶做法是把上下文特征喂给一个简单的多分类模型但资源包里的规则版已经够用。6.3 从静态图谱到增量更新的技巧法条会修订问答语料会更新知识图谱不能一锤子买卖。我现在的习惯是每次数据更新后用MERGE做增量写入而不是删库重建。增量写入前先跑一个“受影响实体集”比如新法条涉及“辞退”就先查出所有跟“辞退”相关的关系并标记为候补。然后用新语料生成的新三元组覆盖旧关系没有覆盖到的旧关系如果跟新法条冲突就直接删除。这一步我用Python控制事务先删除旧关系再写入新关系保证每一步可回滚。增量更新后还有一个隐藏的检查点图谱统计要重新跑一遍尤其是关系分布。上次更新法条后我忘了看“导致”关系占比结果问答系统对每件事都推理出“导致经济补偿”明显是关系数据冗余。从那以后我每次更新都强制走一遍r.type统计超过阈值就重新抽取。希望这套检查清单能帮你在复现时少走弯路。本文还有配套的精品资源点击获取