news 2026/8/31 22:53:52

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于知识图谱与RAG的法务智能问答系统实践

简介:本资源是一个面向法律科技领域开发者的法务智能知识图谱实践项目,聚焦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条语义相近但句式不同的数据。

二是正反例构造。对同一个法律事实,构造“合法做法”和“违法做法”两组问答,比如合法的解除劳动合同流程和违法的非法辞退情形。这样让向量库在语义空间中把两类情况区分开,检索时不会把“可以辞退”和“违法辞退”的文本混在一起。

数据增强做得越细,后面检索的负担就越小。我的实测是增强后的检索召回率(Recall@10)从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_FLAT,nprobe调为16,查询性能在百万级向量规模下能达到毫秒级响应。这个参数组合在我实际测试中精度和速度的平衡最好。

5. 混合检索与答案生成:图谱和RAG怎么协同工作

5.1 查询理解:先搞清楚用户想问什么

问答系统的第一步,不是急着检索,而是先做查询理解。用户的问题五花八门,同一个意思有几十种说法。我设计了一个轻量级的查询理解模块,它的输出是一个结构化的查询意图,包含:

  • 问题类型:事实型、关系型、场景型、资讯型
  • 实体提及:问题中出现的法条、法规、术语、机构等
  • 领域标签:劳动法、公司法、合同、婚姻家事等
  • 约束条件:时间、地区、效力层级等信息

比如用户问“2024年新公司法里股东出资期限最长是多久”,查询理解模块要能识别出实体是“股东出资期限”、法规是“新公司法”、时间是“2024年”,然后把这些结构化信息分别传给图谱查询和向量检索。

这一步做得好坏,直接决定后面检索的命中率。我建议不要全靠大模型做理解,规则加模型混合的方式更稳定——专有名词和法条编号用正则与词典精确抽取,长尾表达用大模型做意图分类。

5.2 三路检索的具体实现

查询理解完成后,系统同时发起三路检索:

第一路:图谱查询。把查询理解中识别出的实体和关系组合成Cypher查询。比如用户问“试用期被辞退有什么赔偿”,系统识别出关键词“试用期”和“赔偿”,在图谱中定位到劳动法相关实体,然后沿着主题关系和引用关系寻找相关法条,返回一组候选法条和它们的关联路径。

示例Cypher:

MATCH 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_url="YOUR_LLM_ENDPOINT", api_key="YOUR_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(text=text[:2000]) resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], temperature=0, 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_depth=max_depth) with self.driver.session() as session: result = session.run(cypher, entity_name=entity_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, weights=None): 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], default=1.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], default=1.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(), key=lambda x: x[1], reverse=True) return ranked

这个融合函数里的关键点是标准化。三路检索返回的分数量纲完全不同:向量的余弦相似度在0到1之间,BM25可以到十几甚至几十,如果不做归一化直接加权,BM25会完全主导结果。所以我对每一路都先除以该路的最大值,把分数压到0到1之间再加权。

6.5 核心代码段四:答案生成与后处理

最后一段核心代码是答案生成与后处理,这部分负责把检索到的内容变成最终交付给用户的答案,同时实现“引用有据”的约束:

class AnswerGenerator: def __init__(self, llm_client, source_threshold=0.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"[来源{i+1}] {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条评测集跑了一轮完整的端到端测试,核心指标如下:

指标数值说明
检索召回率(Recall@10)84.2%正确答案出现在前10个候选中的比例
最终答案准确率76.8%答案与标准答案语义一致且引用正确的比例
引用来源正确率89.3%答案中引用的法条编号和来源正确的比例
拒答率8.1%因无可靠依据而拒绝回答的比例
平均响应时间2.3秒(不含LLM输出流式显示)从请求到首内容返回的耗时

和纯向量检索基线(Retrieval-only RAG)相比,融合了知识图谱和BM25之后的最终答案准确率提高了近14个百分点。提升最关键的点是知识图谱贡献的“关系路径”信息,让模型能在更精确的法条规定上组织答案。

7.2 三类典型问答的用户实际反馈

我找了几位法务同事对这个系统做了试用,下面摘录三类比较有代表性的问答案例:

第一类:精确法条问答。

用户问:“试用期最长约定多久?”

系统答:“根据《劳动合同法》第十九条,试用期最长为六个月(来源:劳动合同法第十九条)。具体来说:劳动合同期限三个月以上不满一年的,试用期不得超过一个月;劳动合同期限一年以上不满三年的,试用期不得超过二个月;三年以上固定期限或无固定期限的劳动合同,试用期不得超过六个月。”

法务同事评价:“这个答案可以直接引用,结构也清楚。”

第二类:场景推理问答。

用户问:“公司把我从技术岗调到销售岗,工资降了40%,我不同意,公司说这不属于调岗,属于企业自主用工权,我该怎么办?”

系统答:“根据《劳动合同法》第三十五条,变更劳动合同应当协商一致,用人单位单方面变更需承担不利后果。您的调岗行为实质上是变更了劳动合同内容(岗位和工资),若未经协商一致,可能构成未按约定提供劳动条件,您可以依据《劳动合同法》第三十八条解除劳动合同并主张经济补偿金。”

法务同事评价:“它能抓住‘工资降了40%’这个关键事实,而不是泛泛而谈调岗规则。”

第三类:资讯型问答。

用户问:“最近最高法对网络消费纠纷有没有新的司法解释?”

系统答:“根据2024年发布的网络消费纠纷相关司法解释,对于网络购物合同中的‘七天无理由退货’适用范围和新业态消费中的责任认定提出了新的规定,详细内容可参考附带的资讯原文和链接。”(资讯源访问需要合规授权)

法务同事评价:“法规库外的动态知识终于不用自己手动搜了。”

7.3 踩坑记录:Neo4j查询性能为何从秒级退化到分钟级

整个项目里最让我印象深刻的一个坑,是知识图谱查询性能从秒级退化到分钟级。

事情起因是图谱规模扩大后,我希望一次查询能找出某个实体三跳内的所有关联法条。一开始我只写了简单的Cypher:

MATCH 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把“找依据”这个最耗时的工作做到又快又准。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 22:53:41

如何控制YouTube视频主题

https://riat-.blog.csdn.net/article/details/164224166 国家机密&#xff0c;&#xff0c;&#xff0c;无可奉告

作者头像 李华
网站建设 2026/8/31 22:53:29

STM32G431外部时钟50MHz上限详解:PLL配置直达170MHz实战

做 STM32G431 项目的人&#xff0c;基本都会被同一个问题卡过&#xff1a;这颗芯片的外部时钟频率&#xff0c;最高到底能喂多少&#xff1f;我最早是给一块数字电源控制板做时钟方案时撞上这个问题的。当时想用一颗有源外部时钟直接进 HSE&#xff0c;把板上的无源晶振位置省掉…

作者头像 李华
网站建设 2026/8/31 22:50:44

STM32C092 FDCAN重映射踩坑与配置详解:从GPIO到时钟源

1. 为什么我盯上了 STM32C092 的 FDCAN 重映射先交代一下背景。前阵子手头一个项目&#xff0c;主控选了 STM32C092FCP6&#xff0c;看重的是它价格低、封装小、还带了双 FDCAN&#xff0c;做车载低速网关或者工业互联节点都够用。可问题恰恰出在这颗芯片的 FDCAN 引脚重映射上…

作者头像 李华
网站建设 2026/8/31 22:50:03

LLM接入工业Modbus设备:寄存器映射与Function Calling的架构实践

如果你最近在折腾 LLM Agent 接入工业设备&#xff0c;大概率会有同样的感受&#xff1a;让大模型去直接解码 Modbus 寄存器&#xff0c;就像让一个文科生去逐字节翻译二进制协议报文。不是大模型不聪明&#xff0c;而是这件事从一开始就不该交给它。Modbus 寄存器本质上是 16 …

作者头像 李华
网站建设 2026/8/31 22:49:57

26年专科毕业论文AI哪个好用?5款实测对比

专科毕业论文写作周期短、实践性强&#xff0c;选题、文献整理、初稿撰写几个环节堆在一起&#xff0c;时间往往不够用。市面上的AI写作工具数量不少&#xff0c;参数宣传和实际体验之间常有落差。本文选取5款主流工具&#xff0c;围绕专科毕业论文场景做一轮横向实测&#xff…

作者头像 李华
网站建设 2026/8/31 22:48:21

开源大模型如何“可信可用”?从模型卡到评估框架

前几天有位做 AI 应用的朋友问我&#xff1a;“GitHub 上那个新开源的大模型权重&#xff0c;是不是直接拉下来部署就能用了&#xff1f;”我反问他&#xff1a;“你找到它的模型卡了吗&#xff1f;”电话那头沉默了两秒。这个沉默很有意思&#xff0c;因为它指向了一个正在被很…

作者头像 李华