资讯动态

基于知识图谱与RAG的法务智能问答系统实践

发布时间:2026/8/31 22:53:57 来源:尧图企业网站定制
简介本资源是一个面向法律科技领域开发者的法务智能知识图谱实践项目聚焦NLP与知识图谱技术在法律问答场景的落地应用适用于具备Python、图数据库及自然语言处理基础的中级以上学习者。项目构建了覆盖20万条法务问答与法律资讯的知识库完成案由预测、问题类型分类、自动问答服务及图谱驱动的知识查询四大核心功能可支撑法律咨询系统原型开发与智能客服模块集成。压缩包为ZIP格式大小33.88MB包含源代码、知识图谱构建脚本、模型训练与推理模块、数据预处理工具及结构化法律语料如案由库、对话知识库等文件组织清晰便于分模块调试与二次开发。目前已有674人学习下载提供完整可运行代码与工程化目录结构涵盖从数据采集、图谱构建到服务部署的全链路实现是深入理解法律垂直领域知识图谱构建与应用的优质实践样本。 做这个项目之前我手上最头疼的一件事就是法务部门堆了几千份合同、法规、历史裁判文书但真到了问答环节法务同事还是习惯性打开浏览器翻半天。大模型固然能聊但法律场景下它给的信息一旦出错后果不是“重来一次”能解决的。所以当我决定做一个面向法务场景的智能问答系统时核心目标从一开始就不是“让模型更会聊天”而是让“每一个回答都能对应到具体的法条、案例或来源”。这个源就是知识图谱和经过治理的法律数据。这个项目从设计到落地我把它命名为“基于法务智能知识图谱含码源的20W法务问答与法律资讯问答功能”。整套系统包含了一个可复用的码源工程一个20万条规模的法务问答数据管线一整套法律资讯采集与问答链路以及一个将知识图谱和向量检索深度融合的混合问答引擎。本文会把这套系统从数据、图谱、检索到问答生成的过程完整拆开讲适合正在做法律知识库、RAG应用、图谱问答的开发者参考也适合刚接触法务智能化项目的产品和技术负责人快速建立技术认知。1. 为什么法务问答不能只靠大模型项目出发点与架构决策1.1 大模型直接回答法律问题会踩哪些致命坑先说结论直接让大模型做法律问答表面上“什么都懂”实际上在严谨性、时效性和可追溯性三个维度都不达标。幻觉问题LLM生成的自然语言很流畅但法律条文讲究的是精确用词。一个法条的“应当”和“可以”效力完全不一样。大模型在缺乏上下文时很容易把“应当”说成“可以”这种差别在合同条款和裁判逻辑里是致命的。时效性问题法律知识更新频率极高。新法规出台、旧法规修订、司法解释发布都会影响一个法律问题的答案。而大模型的训练数据有截止日期用户问“2024年新公司法的注册资本规定”它极有可能还在按旧法回答。溯源性缺失法务和律师天然需要“依据”。回答若没有给出具体的法律条文编号、生效时间、发布机关或者案例出处法务人员根本不敢采信也无法用于工作流。也就是说单纯靠“堆参数”解决不了法务场景的问题。答案需要先被检索出来再被组织成语言而这正好是RAG检索增强生成和知识图谱能做的事情。1.2 知识图谱和向量数据库在系统里各扮演什么角色我在系统里同时用了知识图谱和向量数据库不是赶时髦而是因为它们解决的东西完全不同。技术组件解决的问题典型场景知识图谱Neo4j实体之间的关系、多跳推理、规则约束“劳动合同法第二十条”和“工资支付暂行规定”之间是什么关系某个案件涉及哪些法条向量数据库Milvus语义相似度检索、非结构化文本召回用户用口语问“被公司辞退怎么要赔偿”需要召回相关法条片段和法律资讯BM25/关键词索引精确匹配、专有名词检索用户问“《公司法》第四十七条”直接用词法匹配法条编号实际运行时的流程是用户提问后系统并行发起三类查询——知识图谱查询把问题解析成实体和关系寻找图谱内的关联路径、向量相似度检索把自然语言映射到法律文本片段、BM25关键字检索精确捕捉法条编号、术语。三路结果经过融合排序再交给大模型去组织答案。这套架构的本质是知识图谱保证“关系上的精准”向量检索保证“语义上的泛化”关键字保证“专有名词不错过”。三者互为兜底。1.3 整体架构的四个核心层整个项目按功能拆成四层每一层都是独立的可以单独替换或扩展数据治理层原始法律文本、20W问答数据、法律资讯的采集、清洗、格式化统一成JSON与CSV格式落盘再分别进入图谱和向量库。知识图谱层基于Neo4j负责存储法条、法规、案例、术语、机构的实体以及它们之间的关系包括引用、适用、替代、上位法、下位法等。检索融合层GraphQA图查询、VectorQA向量召回、KeywordQA关键词匹配三个子检索器并行工作输出经过归一化排序的候选片段。问答生成层用LLM对候选片段进行摘要、重组、生成答案并强制要求答案中附带引用来源无法引用的内容宁可不答。整个系统从启动到问答端到端的链路是请求进来 → 查询理解 → 多路召回 → 重排合并 → LLM生成 → 返回带来源的内容。对了这套系统还内置了“拒答”机制——如果三路检索的置信度全部低于阈值大模型必须明确回答“当前知识库范围内未找到可靠依据”而不是硬编一个答案。2. 20W法务问答数据的获取、清洗与评测集搭建2.1 数据来源与整体构成标题里写的20W指的是经过完整清洗后可用于模型微调、检索评测和问答测试的法务问答对数量。这批数据主要来自以下几个渠道公开法律咨询平台用户在平台上的真实法律咨询问题和律师回答这类数据有天然的问答结构但噪声很大需要重点清洗。裁判文书库公开的裁判文书中将“原告诉称”“被告辩称”“法院认为”等段落改造成问答对这部分是很好的事实性问答数据。法条和司法解释把“某法条的核心内容是什么”“某法律术语怎么定义”这类静态知识做成FAQ这部分准确率最高。法律资讯内容再加工把资讯中的结构化信息新的司法解释、典型案例通报转化为问题与答案。这20W条数据并不是一个均匀分布的大杂烩我在清洗后按照法律领域做了分类统计分布大致如下领域分类问答对数量占比劳动与社会保障4200021%合同纠纷3800019%婚姻家事2600013%公司法/企业治理2200011%知识产权180009%刑事法律基础140007%房产与建设工程100005%行政法律80004%其他2200011%2.2 清洗流程中的关键决策清洗这一步决定了后面所有环节的效果。我的经验是如果你只洗掉显而易见的HTML标签和空行那后面知识图谱的实体抽取和向量检索的召回质量都会非常受限。实际清洗流程我做了四步第一步是内容规整。法律咨询平台的原始数据里带有大量“您好根据您的描述律师分析如下……”这种寒暄模板对话前后缀不统一。我写了一套规则脚本把“律师回复”部分截取到“如有疑问可继续追问”之前截断除了正文之外的冗余信息。另外把全角字符统一转半角把“ㄑ”“”等变体括号统一为标准中文括号。第二步是无效问答筛选。法律咨询平台上很多问题是“你好在吗”“怎么联系律师”这种无效内容还有的回答是“建议你找个律师详谈”这种空话。我训练了一个轻量分类器结合规则把“回答长度少于60个字”“回答中没有出现任何法条关键词或法律术语”的问答对直接剔除大概滤掉了200多万条原始内容中的大部分噪声。第三步是术语统一。这一步直接影响后续图谱构建的一致性。比如“劳动者”和“员工”在同一篇问答中可能混用但在知识图谱中它们应属于同一个概念。我建了一个法律同义词表将常见同义表述映射到标准概念例如劳动者/员工/职工/打工者 → work_employee公司/企业/用人单位/雇主 → work_employer甲方/乙方/合同方 → contract_party第四步是超长问答的分段。有些平台的律师回答长达几千字不适合直接作为问答对存入向量库。我按段落切分每一段再结合问题和上下文生成独立的子问答对并根据段落位置标注“法律分析”“结论建议”“风险提示”等类型标签。这种分段方式让我在检索时能够更精准地命中答案的位置。2.3 训练集、验证集、评测集的划分方法20W条问答对不能一股脑丢进模型或向量库需要严格的划分。我采用的是8:1:1的划分策略80%约16W条进入基础训练与向量知识库。这16W条是RAG系统的核心召回池也是任何微调训练的基座。10%约2W条作为验证集。用于调优检索器的超参数比如向量检索的top_k、BM25的权重、重排阈值等。10%约2W条作为评测集。这个评测集专门用来检验问答效果不允许在调参过程中“偷看”。同时我把评测集按照问题类型做了细颗粒度标注包含事实型问题“试用期最长可以约定多久”——这类必须给出准确法条依据。关系型问题“合同纠纷可以适用哪些法律”——这类需要知识图谱的多跳查询。场景型问题“公司把我调岗降薪我不同意可以解除合同并要求赔偿吗”——这类需要综合多个法条和案例推理。评测集必须“锁死”任何模型版本迭代都在这套集上跑分否则你会被随机性误导分不清是模型优化了还是评测集变了。2.4 数据增强技巧让问答数据真正可用法律问答数据最缺的不是“量”而是“多样性”。一个问法“试用期被辞退怎么赔偿”在真实场景里可能被表述成十几种不同句式。如果向量库里的原文句式单一召回率会很难看。我做了两个增强动作一是句式改写。用大模型把同一个法律问题从多个角度改写比如“试用期被辞退怎么赔偿”改写为“还在试用期公司说我不合格直接让我走这合法吗”、“试用期辞退需要给经济补偿金吗”、“公司试用期内解除劳动合同的条件是什么”。一个原始问答对可以扩展出5到10条语义相近但句式不同的数据。二是正反例构造。对同一个法律事实构造“合法做法”和“违法做法”两组问答比如合法的解除劳动合同流程和违法的非法辞退情形。这样让向量库在语义空间中把两类情况区分开检索时不会把“可以辞退”和“违法辞退”的文本混在一起。数据增强做得越细后面检索的负担就越小。我的实测是增强后的检索召回率Recall10从61%提升到了79%提升非常明显。3. 法务知识图谱构建本体设计、实体抽取与规则注入3.1 本体设计法务领域到底需要哪些实体和关系知识图谱不是把文本塞进图数据库就行它需要一套“本体”Ontology来定义“这个世界里有什么类型的东西、它们之间有什么关系”。法务领域的本体设计我基于实际问答需求沉淀了5类核心实体和6类核心关系。实体类型Statute法条单条法律条文属性包括法律名称、条号、款号、生效日期、时效状态。Regulation法规整部法律或行政法规属性包括名称、发布机关、效力层级、实施日期。Case案例裁判文书或指导案例属性包括案号、法院、裁判年份、裁判要旨。Term法律术语如“不可抗力”“竞业限制”“经济补偿金”属性包括定义、来源。Organization机构/主体包括法院、仲裁委、行政机关、企业、个人等法律行为主体。关系类型REFERENCE引用如Statute A引用Statute B。BELONGS_TO归属如Statute属于某部Regulation。APPLIES_TO适用如Case适用Statute。SUPERSEDES替代如新法替代旧法。RELATED_TO相关通用的语义关联关系。SUBJECT_TO约束如Organization受Regulation约束。这个设计是从下往上反推出来的。一开始我试图把本体设计得很完备加了十几个实体类型和二十多种关系结果查询复杂、抽取困难效果反而差。后来调整为最小可用本体在实际使用中不够再补反而流畅很多。3.2 实体和关系的抽取流水线实体抽取是知识图谱构建中最耗时、也最容易出错的部分。20W问答数据加上法律资讯、法条文本全量文本量超过2亿字不可能人工标注只能靠自动化抽取加人工抽检。我的抽取流水线分三步第一步法条和法规结构化解析。法条和法规本身有严格的格式我写了专门的解析脚本把“第X条”结构抽取出来。这一步准确率可以做到99%以上因为法律文本的格式相对固定。第二步LLM批量抽取。对于案例、问答对和资讯中非结构化的实体和关系我给大模型设计了一套抽取指令模板每次输入一段文本要求输出JSON格式的实体列表和关系列表。核心prompt类似于你是一个法律文本信息抽取器。请从给定文本中抽取法律实体和关系。 实体类型包括Statute法条、Regulation法规、Case案例、Term法律术语、Organization机构主体。 关系类型包括REFERENCE引用、BELONGS_TO归属、APPLIES_TO适用、SUPERSEDES替代、RELATED_TO相关、SUBJECT_TO约束。 要求 1. 实体必须能从原文中找到依据禁止编造。 2. 关系必须基于原文语义判断无法确定时返回空列表。 3. 输出格式为严格JSON不要输出任何额外文字。为了让抽取结果稳定我用了温度参数设为0、配合JSON Schema校验的方式一次抽取10万条文本人工抽样1000条检查实体识别的准确率能稳定在86%左右。剩下14%的误差大多集中在“同案不同表述”和“简称指代”这两种情况属于当前技术条件下比较难啃的部分。第三步规则修正与去重。LLM抽取出来的实体名经常不统一比如“劳动法”和“《劳动法》”会抽成两个实体。我建立了实体归一化规则去掉书名号、统一简称与全称的映射、法人名称按统一社会信用代码去重。同时对相同实体名的不同记录做合并保留属性最全的那一条。3.3 Neo4j中的图谱存储与批量导入图谱存储我选了Neo4j社区版原因是它查询灵活、Cypher语法简单而且对知识图谱问答场景支持得比较成熟。在数据规模层面本项目最终约有60万实体节点和120万条关系社区版完全扛得住。批量导入方式我强烈推荐用neo4j-admin或cypher-shell的LOAD CSV命令不要用逐条CREATE语句否则几百万条数据可以导入到天荒地老。实际导入中我用的是LOAD CSV加PERIODIC COMMIT分批次导入实体和关系。示例命令USING PERIODIC COMMIT 5000 LOAD CSV WITH HEADERS FROM file:///statutes.csv AS row CREATE (s:Statute { id: row.id, name: row.name, law_name: row.law_name, article_no: row.article_no, content: row.content, issue_date: row.issue_date, effective_date: row.effective_date, status: row.status });每个实体创建时都加上唯一ID和各类属性后续建立关系时直接用ID匹配这样比用名称匹配快几个数量级。实体的重复导入问题一定要提前处理。我在实体节点上建了唯一约束CREATE CONSTRAINT statute_id_unique IF NOT EXISTS FOR (s:Statute) REQUIRE s.id IS UNIQUE; CREATE CONSTRAINT regulation_id_unique IF NOT EXISTS FOR (r:Regulation) REQUIRE r.id IS UNIQUE;这样即使CSV中存在重复行第二次导入会被数据库直接拒绝不会产生脏数据。3.4 规则注入让图谱“懂”法律逻辑图谱不只是“存数据”还要把法律逻辑结构化地存进去。我在构建图谱时特意做了两个规则注入一是时效性标注。每个法条都有发布和失效日期关系上有“SUPERSEDES”这样的替代关系。比如2024年施行的新公司法全面替代了旧公司法我在图谱中建立新法条文对旧法条文的SUPERSEDES关系。这样问答系统面对“旧法还有效吗”这类问题时可以直接通过图查询得到答案而不是靠大模型猜。二是法条之间的引用关系解析。法律体系是一个互相引用的网络。比如《劳动合同法》第八十七条引用了《劳动合同法》第四十八条和《劳动合同法实施条例》第二十五条。我把这些引用关系在解析阶段就抽取出来存成REFERENCE关系。这样用户问“经济补偿金怎么算”系统不仅能找到劳动合同法第四十七条的原文还能通过引用关系顺藤摸瓜找到实施条例里关于“工资”定义的具体条款。做完这一步知识图谱就不再是一个静态的数据库而是一个能支持多跳推理的法律知识网络。这是系统与普通“法条搜索引擎”拉开差距的核心所在。4. 法律资讯问答的实现链路从采集到向量化的全流程4.1 为什么要在图谱之外再加一套资讯问答很多人会问有知识图谱和20W问答库了为什么还要单独做一个“法律资讯问答”功能原因是法律知识的时效性问题。知识图谱里的法条和判例是相对稳定的静态知识但法律资讯是高频更新的动态信息。比如“某地法院发布了一个劳动争议典型案例”“最高法发布了新的司法解释”这类资讯如果只放在图谱里一是抽取成实体关系会丢失大量细节二是资讯更新频率高每次都做实体对齐成本太高。所以我单独做了一条资讯问答链路资讯原文进入向量数据库用户提问时通过向量检索召回相关资讯片段再由LLM组织答案。这条链路和知识图谱问答是互补的——图谱回答“法条怎么规定”资讯回答“最近有什么新动态、案例怎么判”。4.2 资讯采集与预处理资讯采集是一个需要长期维护的工程。我用Python写了采集模块主要抓取以下几类来源法律法规数据库的更新公告高院和各地法院发布的指导案例、典型案例主流法律媒体的专业解读文章政府官网的政策法规栏采集框架上我用requests加BeautifulSoup配合一套防反爬策略随机User-Agent、请求间隔控制、出错自动重试。针对部分有反爬措施的站点加了代理池和Cookie池。但这里要特别说明采集必须遵守目标网站的robots协议和法律法规只采集公开信息的摘要和链接导向控制请求频率并且以学习研究为目的使用商业使用前一定要确认授权和合规问题。资讯预处理阶段我做三件事标题和正文清洗去掉广告、页脚、无关链接把正文提取出来。发布时间解析把各种格式的日期统一成ISO标准格式用作者和发布时间作为资讯的唯一标识。分类打标用规则加LLM给每条资讯打上领域标签包括“劳动法”“公司法”“婚姻家事”“知识产权”“刑事”等。这个标签在检索排序时可以用来做过滤条件。4.3 资讯去重SimHash的关键参数资讯源经常出现同一篇内容被多个平台转载的情况直接入库会污染向量库。我用了SimHash算法做近重复检测。SimHash的核心是把文本分词后按词权重累加得到一个64位指纹两篇内容的指纹汉明距离小于一定阈值就判定为近似重复。我实测下来的参数配置是分词粒度中文按jieba分词对法律专有名词加入自定义词典指纹位数64位汉明距离阈值3小于等于3判定为重复比较范围启用索引分块只比较嫌疑区块内的指纹这里有一个实际坑如果把阈值设为0只能去掉完全相同的文章很多“转载后改了个标题正文一字不改”的内容漏网。但若把阈值设得太大比如10一些正常讨论同一热点但观点不同的文章会被误杀。3到4是我测试后比较平衡的值。4.4 向量化与分段策略资讯文本进入向量库之前必须先做分段。分段策略直接影响召回效果。法律资讯的篇幅通常较长直接整篇向量化检索时噪声会很大。我用的分段策略是按段落和语义双重切分。先按段落拆再把过短的段落合并保证每个文本块长度在300到500字之间。设置重叠窗口。上一个分块的末尾约50字会和下一个分块的开头重叠避免分割线正好切断关键法律表述。块元数据保留。每个块向量入库时都携带标题、来源、发布时间、原始链接、领域标签等元数据。这样检索命中后可以直接追溯到来源也可以按发布时间排序。向量化模型我用的是BGE-large-zh-v1.5在中文语义检索上表现不错还在法律文本上做了少量领域自适应微调。向量维数是1024维单条文本向量化耗时约80毫秒整个资讯库20万条文本全量向量化大概需要几个小时可以离线跑批。向量存储我选了Milvus 2.x用Collection来组织数据索引类型选择IVF_FLATnprobe调为16查询性能在百万级向量规模下能达到毫秒级响应。这个参数组合在我实际测试中精度和速度的平衡最好。5. 混合检索与答案生成图谱和RAG怎么协同工作5.1 查询理解先搞清楚用户想问什么问答系统的第一步不是急着检索而是先做查询理解。用户的问题五花八门同一个意思有几十种说法。我设计了一个轻量级的查询理解模块它的输出是一个结构化的查询意图包含问题类型事实型、关系型、场景型、资讯型实体提及问题中出现的法条、法规、术语、机构等领域标签劳动法、公司法、合同、婚姻家事等约束条件时间、地区、效力层级等信息比如用户问“2024年新公司法里股东出资期限最长是多久”查询理解模块要能识别出实体是“股东出资期限”、法规是“新公司法”、时间是“2024年”然后把这些结构化信息分别传给图谱查询和向量检索。这一步做得好坏直接决定后面检索的命中率。我建议不要全靠大模型做理解规则加模型混合的方式更稳定——专有名词和法条编号用正则与词典精确抽取长尾表达用大模型做意图分类。5.2 三路检索的具体实现查询理解完成后系统同时发起三路检索第一路图谱查询。把查询理解中识别出的实体和关系组合成Cypher查询。比如用户问“试用期被辞退有什么赔偿”系统识别出关键词“试用期”和“赔偿”在图谱中定位到劳动法相关实体然后沿着主题关系和引用关系寻找相关法条返回一组候选法条和它们的关联路径。示例CypherMATCH path (s:Statute)-[:REFERENCE|BELONGS_TO|RELATED_TO*1..3]-(related) WHERE s.name CONTAINS 试用期 OR s.content CONTAINS 试用期 RETURN s.name AS source, related.name AS target, type(path) AS relType LIMIT 50;这种多跳查询是相对较重的操作需要注意索引和LIMIT控制。第二路向量检索。把用户原始问题喂给向量化模型得到问题向量然后在Milvus里检索最相似的文本块。返回top_k个候选片段同时返回它们的元数据。我通常把top_k设为20再在重排阶段精筛。第三路BM25关键词检索。对问题做分词后用BM25算法在法条库和资讯库里做全文检索。BM25特别擅长精确匹配“劳动合同法第二十一条”这样的专有名词纯向量检索在这类精确指代上经常抓瞎。5.3 融合排序把三路结果合并成一份高质量候选三路检索各自返回的结果存在重叠和冲突。融合排序阶段我给每一路结果打分再用加权方式合并。打分因子包括检索相关性分数向量相似度、BM25权重实体匹配分数问题中的实体在候选片段中的出现覆盖度来源权威度法条原文 官方案例 法律媒体解读 普通咨询问答时效性资讯类结果发布时间越近分数越高法条类则看生效状态融合公式大致是final_score 0.4 * vector_score 0.3 * bm25_score 0.2 * entity_coverage 0.1 * source_authority权重的初始值来自经验后续通过验证集做网格搜索微调。实测下来这种加权融合方式比任何单一检索方式的准确率都高验证集上的推荐命中率从单一向量检索的62%提升到了84%。5.4 答案生成如何让LLM“引用有据”检索完成后系统把候选片段组装成Prompt交给LLM生成最终答案。这一环的关键是设计Prompt强迫模型做到三点只依据给定材料回答、答案必须标注来源、无法回答时明说。我实际使用的生成Prompt模板如下你是一名法律问答助手。请依据下面提供的法律条文、案例或资讯片段回答用户问题。 要求 1. 只能使用给出的材料进行回答禁止使用自己记忆中的法律知识 2. 回答的每一个关键结论后面必须标注来源如【来源劳动合同法第X条】【来源资讯链接】 3. 如果材料不足以回答问题直接回答“当前知识库内没有足够依据回答该问题”不要编造 4. 回答要用清晰的中文分点列出结论和依据。 用户问题{question} 参考材料 {context}生成之后还有一个后处理过滤器。我会检查答案文本中是否引用了至少一个来源如果没有自动改为拒答话术。这一步把“胡编乱造”的概率压到了很低。5.5 拒答机制的工程实现法律场景宁可不答不能错答。我定义了一套拒答触发的条件和处理策略三路检索全部未返回任何候选直接返回“未在知识库中检索到相关依据”。加权融合后最高分低于设定的阈值这个阈值在验证集上统计得到直接拒答。生成的答案无法通过来源校验改为拒答。这套机制看起来保守但在法务场景里非常受用。法务同事说得好“它告诉我不知道我可以自己查它告诉我一个错答案我可能直接写进合同里那是要出事的。”这个认知是整个系统的安全底线。6. 码源核心模块拆解关键代码与工程化细节6.1 项目整体目录结构“含码源”是标题里的关键词之一也是很多读者最关心的部分。源码头寸是整个问答系统的工程母体目录结构如下legal-kg-qa/ ├── data/ │ ├── raw/ # 原始采集数据 │ ├── processed/ # 清洗后的结构化数据 │ ├── qa_pairs.json # 20W问答对最终文件 │ └── news_articles/ # 法律资讯处理结果 ├── src/ │ ├── ingestion/ │ │ ├── clean.py # 数据清洗 │ │ ├── augment.py # 数据增强 │ │ └── vectorize.py # 文本向量化与入库 │ ├── graph/ │ │ ├── extract_entities.py # 实体关系抽取 │ │ ├── build_graph.py # 图谱构建与导入 │ │ └── query_graph.py # 图谱查询接口 │ ├── retrieval/ │ │ ├── vector_retriever.py # 向量检索 │ │ ├── keyword_retriever.py # BM25检索 │ │ ├── fusion.py # 融合排序 │ │ └── query_understanding.py # 查询理解 │ ├── generator/ │ │ ├── prompt_builder.py # Prompt组装 │ │ ├── llm_client.py # LLM调用与流式输出 │ │ └── answer_filter.py # 答案后处理与拒答 │ └── api/ │ ├── main.py # FastAPI服务入口 │ └── schemas.py # 请求响应的数据结构 ├── config/ │ ├── settings.yaml # 全局配置 │ └── prompts/ # Prompt模板 └── scripts/ ├── train_retriever.py # 检索器微调 └── eval_qa.py # 评测脚本这个结构遵循一个原则数据采集、知识图谱构建、检索服务、答案生成彼此解耦。你想单独替换某一块不用动其他模块。比如不想用Neo4j了只需要改src/graph/query_graph.py的落库方式上层检索融合逻辑完全不用动。6.2 核心代码段一实体与关系抽取实体抽取是整个系统的第一环也是最容易出现“只见树木不见森林”的环节。以下是我基于LLM做批量抽取的简化版本import json from openai import OpenAI client OpenAI(base_urlYOUR_LLM_ENDPOINT, api_keyYOUR_KEY) EXTRACT_PROMPT 你是一个法律文本信息抽取器。请从给定文本中抽取法律实体和关系。 实体类型Statute法条、Regulation法规、Case案例、Term法律术语、Organization机构主体 关系类型REFERENCE引用、BELONGS_TO归属、APPLIES_TO适用、SUPERSEDES替代、RELATED_TO相关、SUBJECT_TO约束 要求 1. 实体必须能从原文中找到依据禁止编造。 2. 关系必须基于原文语义判断无法确定时返回空列表。 3. 输出格式为严格JSON不要输出任何额外文字。 文本 {text} def extract_entities_and_relations(text): prompt EXTRACT_PROMPT.format(texttext[:2000]) resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: prompt}], temperature0, response_format{type: json_object}, ) data json.loads(resp.choices[0].message.content) return data.get(entities, []), data.get(relations, [])这里有两个细节我必须强调第一个是temperature必须设为0。如果允许随机性同一个段落抽取两次结果不一致下游的图谱合并和去重就要爆炸。第二个是response_format强制JSON输出。如果你用的是支持JSON mode的模型务必打开如果不支持就在Prompt里反复强调“只输出JSON”再利用正则把输出中的JSON片段截取出来否则解析环节会频繁翻车。6.3 核心代码段二图谱查询接口图谱查询接口设计上要隐藏Cypher细节对外提供的是“给实体名返回关联法条列表”这种高级语义接口from neo4j import GraphDatabase from typing import List, Dict class GraphQuery: def __init__(self, uri, user, pwd): self.driver GraphDatabase.driver(uri, auth(user, pwd)) def query_related_statutes(self, entity_name: str, max_depth: int 2) - List[Dict]: cypher MATCH path (start)-[:REFERENCE|RELATED_TO|BELONGS_TO*1..{max_depth}]-(s:Statute) WHERE start.name CONTAINS $entity_name OR start.content CONTAINS $entity_name RETURN s.name AS statute_name, s.law_name AS law_name, s.article_no AS article_no, s.content AS content, s.status AS status, length(path) AS path_len ORDER BY path_len LIMIT 30 .format(max_depthmax_depth) with self.driver.session() as session: result session.run(cypher, entity_nameentity_name) return [record.data() for record in result] def close(self): self.driver.close()这段代码的价值在于用动态可变深度查询把“关系路径长度”作为排序因子之一。用户问的实体如果和图谱中的核心法条直接相连路径长度就短排序靠前隔了好几跳的弱关联会被排在后面召回内容的主次关系清晰。6.4 核心代码段三混合检索融合融合排序是实现高准确率召回的关键枢纽。以下是我实现中比较核心的融合逻辑import numpy as np def fused_ranking(vector_hits, bm25_hits, entity_coverage, weightsNone): if weights is None: weights {vector: 0.4, bm25: 0.3, entity: 0.2, source: 0.1} score_dict {} def add_score(doc_id, score, alpha): score_dict[doc_id] score_dict.get(doc_id, 0.0) alpha * score max_vec max([h[score] for h in vector_hits], default1.0) for h in vector_hits: add_score(h[doc_id], h[score] / max_vec, weights[vector]) max_bm25 max([h[score] for h in bm25_hits], default1.0) for h in bm25_hits: add_score(h[doc_id], h[score] / max_bm25, weights[bm25]) for doc_id, cov in entity_coverage.items(): add_score(doc_id, cov, weights[entity]) ranked sorted(score_dict.items(), keylambda x: x[1], reverseTrue) return ranked这个融合函数里的关键点是标准化。三路检索返回的分数量纲完全不同向量的余弦相似度在0到1之间BM25可以到十几甚至几十如果不做归一化直接加权BM25会完全主导结果。所以我对每一路都先除以该路的最大值把分数压到0到1之间再加权。6.5 核心代码段四答案生成与后处理最后一段核心代码是答案生成与后处理这部分负责把检索到的内容变成最终交付给用户的答案同时实现“引用有据”的约束class AnswerGenerator: def __init__(self, llm_client, source_threshold0.5): self.llm_client llm_client self.source_threshold source_threshold def generate(self, question: str, context_items: list) - dict: if not context_items: return {answer: 当前知识库内没有足够依据回答该问题。, citations: []} merged_score self._compute_fused_score(context_items) if merged_score self.source_threshold: return {answer: 当前知识库内没有足够依据回答该问题。, citations: []} context_text \n\n.join( f[来源{i1}] {item[content]}\n元信息: {item.get(source, )} for i, item in enumerate(context_items[:20]) ) prompt self.prompt_builder.build(question, context_text) raw_answer self.llm_client.chat(prompt) filtered_answer, citations self._validate_citations(raw_answer, context_items) if not citations: return {answer: 当前知识库内没有足够依据回答该问题。, citations: []} return {answer: filtered_answer, citations: citations}动态阈值的想法是后来加上的不同类型的用户问题对于检索分数的要求不同比如“法律规定是什么”这类事实题要求高置信度“你怎么理解”这类分析题则允许偏低置信度。我按问题类型分别设定阈值而不是一个全局阈值走天下。6.6 性能与部署注意事项整个服务我用FastAPI封装部署时最需要注意的有三点第一Neo4j和Milvus连接池。每次请求都新建连接会直接拖垮性能必须用连接池复用。Neo4j的Driver本身内置了连接池机制Milvus的Python SDK也支持连接复用需要确保在全局初始化。第二LLM调用延迟。一个端到端问答请求大模型生成答案的时间可能要3到8秒。如果面向生产环境提供服务建议把LLM调用改为流式输出前端一边生成一边展示体验会好很多。第三缓存策略。针对高频问题我把“问题哈希 答案”存进Redis命中缓存直接返回不再走检索和生成链路。实测常见问题比如“试用期辞退赔偿标准”的缓存命中率能到15%左右对整体响应时间优化非常明显。7. 实测效果、典型问答与踩坑记录7.1 评测集上的整体表现我用2W条评测集跑了一轮完整的端到端测试核心指标如下指标数值说明检索召回率Recall1084.2%正确答案出现在前10个候选中的比例最终答案准确率76.8%答案与标准答案语义一致且引用正确的比例引用来源正确率89.3%答案中引用的法条编号和来源正确的比例拒答率8.1%因无可靠依据而拒绝回答的比例平均响应时间2.3秒不含LLM输出流式显示从请求到首内容返回的耗时和纯向量检索基线Retrieval-only RAG相比融合了知识图谱和BM25之后的最终答案准确率提高了近14个百分点。提升最关键的点是知识图谱贡献的“关系路径”信息让模型能在更精确的法条规定上组织答案。7.2 三类典型问答的用户实际反馈我找了几位法务同事对这个系统做了试用下面摘录三类比较有代表性的问答案例第一类精确法条问答。用户问“试用期最长约定多久”系统答“根据《劳动合同法》第十九条试用期最长为六个月来源劳动合同法第十九条。具体来说劳动合同期限三个月以上不满一年的试用期不得超过一个月劳动合同期限一年以上不满三年的试用期不得超过二个月三年以上固定期限或无固定期限的劳动合同试用期不得超过六个月。”法务同事评价“这个答案可以直接引用结构也清楚。”第二类场景推理问答。用户问“公司把我从技术岗调到销售岗工资降了40%我不同意公司说这不属于调岗属于企业自主用工权我该怎么办”系统答“根据《劳动合同法》第三十五条变更劳动合同应当协商一致用人单位单方面变更需承担不利后果。您的调岗行为实质上是变更了劳动合同内容岗位和工资若未经协商一致可能构成未按约定提供劳动条件您可以依据《劳动合同法》第三十八条解除劳动合同并主张经济补偿金。”法务同事评价“它能抓住‘工资降了40%’这个关键事实而不是泛泛而谈调岗规则。”第三类资讯型问答。用户问“最近最高法对网络消费纠纷有没有新的司法解释”系统答“根据2024年发布的网络消费纠纷相关司法解释对于网络购物合同中的‘七天无理由退货’适用范围和新业态消费中的责任认定提出了新的规定详细内容可参考附带的资讯原文和链接。”资讯源访问需要合规授权法务同事评价“法规库外的动态知识终于不用自己手动搜了。”7.3 踩坑记录Neo4j查询性能为何从秒级退化到分钟级整个项目里最让我印象深刻的一个坑是知识图谱查询性能从秒级退化到分钟级。事情起因是图谱规模扩大后我希望一次查询能找出某个实体三跳内的所有关联法条。一开始我只写了简单的CypherMATCH path (a)-[*1..3]-(b) WHERE a.name 劳动合同解除 RETURN b数据量小的时候没什么问题但图谱增长到60万节点、120万关系后带全路径展开的查询经常要扫描上百万个中间节点响应时间从几百毫秒直接飙升到几分钟。排查后发现两个问题。第一个是缺少合适的索引。Neo4j默认对属性建过基本的索引但我的查询按name过滤而name并没有建唯一约束或索引。建索引后这一层查询耗时从4秒降到300毫秒。CREATE INDEX statute_name_idx IF NOT EXISTS FOR (s:Statute) ON (s.name); CREATE INDEX statute_content_idx IF NOT EXISTS FOR (s:Statute) ON (s.content);第二个是路径查询的爆炸式增长。多跳图谱查询如果不加条件限制路径数量会呈指数级增长。我通过两种方式做了约束一是限制每层关系的类型只走REFERENCE和BELONGS_TO这些关键关系二是给中间节点加类型约束避免把无关实体的路径都展开。优化后的查询性能重新回到秒级响应。这个坑给我最大的教训是知识图谱的查询写法和关系型数据库完全不一样它天然偏向“局部网络遍历”你必须清楚地知道你要遍历的是什么子图并且从一开始就用类型和索引把遍历范围收缩住。7.4 踩坑记录向量化模型和法律文本的水土不服第二个值得记录的坑是向量化召回的法律专业性不够。最初我用通用中文embedding模型做向量化结果发现“试用期解除劳动合同”和“转正后解除劳动合同”这两个在法律上差异极大的情形在向量空间里的距离却非常近。因为通用模型只学到了“解除劳动合同”这个语义面的相似不理解“试用期”和“转正后”在法律关系上是两种完全不同的前提。解决方法是做领域自适应微调。我从20W问答对中抽出3万条用对比学习的方式微调embedding模型同一个法律问题的不同表述作为正例不同法律问题的表述作为负例。微调之后法律文本的检索召回率提升了10个点以上。这类问题在通用领域RAG中不明显但在垂直领域RAG中几乎是必经之路。做法律、医疗、金融这类专业领域通用embedding模型的表现会明显不够用强烈建议做一次领域微调不要偷懒。7.5 踩坑记录资讯数据的时效冲突第三个坑发生在法律资讯入库后。我发现“过时资讯”有时候会和新法条信息打架。比如2023年的某条资讯说“旧公司法规定了注册资本认缴制”但2024年新公司法实施后规则已经变了。系统可能同时召回这条旧资讯和新的法条两者在同一个答案中出现就产生了矛盾。解决方式是在资讯入库时增加了一个“时效状态”字段同时在Prompt中明确告诉模型“如果资讯发布时间早于对应的法条替代时间优先采用法条信息资讯仅作为背景参考。”这一步让资讯问答的准确性提高了不少。8. 后续演进方向与个人实践体会8.1 从单轮问答走向多轮对话目前的问答系统是单轮架构用户每次提问都独立处理。但法务场景中用户常有连环追问比如“试用期被辞退有赔偿吗”“那如果公司没有提前通知呢”“赔偿基数怎么算”。这几轮问题之间是有依赖关系的。后续我计划引入对话状态管理每一轮提问都带上对话历史并把前三轮实体提取出来作为本轮追问的上下文避免用户反复描述背景。8.2 法规时效的自动跟踪知识图谱中的法条时效状态目前是构建时通过规则注入的后续需要自动化。我的想法是定期采集法规变更公告通过文本比对识别到“条款变更”“废止”“新增”等操作自动在图谱中标记SUPERSEDES关系并同步更新对应法条节点的status属性。这样系统才能长期保持准确而不是上线后几个月就逐渐过时。8.3 对做同类项目的建议最后分享几点我的个人实践体会。首先垂直领域的知识图谱项目成败关键往往不在模型而在数据治理。我前期在清洗、对齐、去重上花的时间远远超过模型调优但带来的收益也是最大的。其次检索增强系统一定要设计拒答机制这是垂直领域与通用聊天最大的区别。最后不要追求一步到位先跑通最小闭环再逐步增加实体类型、关系和资讯源。这个项目一开始我也试图把本体设计得非常完备、把所有资讯都抽进图谱后来发现量太大、准度也不够反而改用“图谱 向量 资讯”三轨并行的方案效果立竿见影。这套系统上线后法务同事使用最多的功能其实是“引用来源”那一条——每一个结论旁边都有对应的法条编号和出处链接。对我而言这就是知识图谱和RAG结合的价值不是让AI替代人判断而是让AI把“找依据”这个最耗时的工作做到又快又准。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价