资讯详情 用Python和Neo4j构建保险知识图谱与简易问答系统
📅 2026/10/12 12:14:52
简介面向计算机相关专业学生、毕设开发者和保险行业知识图谱入门者的完整项目资源基于开源保险产品数据完成保险知识图谱构建并配套简易问答系统可直接运行与二次扩展。资源共20个文件包含6个Python源码、5个XML配置、3个编译缓存pyc、1个产品数据表xls、1个文档docx及README说明等压缩包约1.84MB结构清晰便于快速定位图谱构建、问题分类、问题解析与查询等核心模块。目前已有102人学习浏览适合课程设计、毕业设计及知识图谱方向初期立项演示。内容提供完整可运行源码、数据处理表格、项目文档及开发环境配置记录可帮助理解保险产品实体关系建模、基于词典与规则的问题分类解析流程以及WebSocket服务端与前端页面的交互实现节省从零搭建环境与调试问题的时间。1. 为什么保险知识图谱值得自己动手建而不是直接租现成API做保险产品对比时把几十款产品的条款拉进Excel表格销售问一句「这个产品的等待期和免责条款分别是什么」还是要翻半天。本质上这是图结构问题产品、保险公司、保障责任、免责条款之间全是多对多关系表格天生表达不了这种语义。标题里这套方案用开源保险产品数据构建知识图谱再加一个简易问答系统Python源码、产品文档和原始数据一次性配齐。它解决的不是「AI有多聪明」而是「数据到三元组、三元组到图库、图库到问答」这条链路怎么低成本跑通。适合三类人想做金融领域知识图谱练手的NLP工程师需要内部产品问答工具的保险科技团队以及论文或课程设计需要图谱加问答实物的学生。下文按我自己的落地路径展开先选数据源并设计本体再抽取三元组入库Neo4j接着搭一个能用的简易问答最后把一路上踩过的坑全列出来。2. 选数据源与建本体保险知识图谱的地基决定上层问答的天花板2.1 三类开源保险产品数据源字段、格式与获取成本对比开源不等于「拿来就能用」。保险产品数据散落在不同地方我梳理下来主要有三类来源数据源格式字段完整度更新频率获取成本金融监管总局备案产品目录HTML/PDF表格产品名公司名责任细节少按月低需写解析器保险公司官网公开条款页HTML产品责任免责条款齐全格式统一随产品迭代更新中需逐家抓取保险资讯聚合站导出数据CSV/JSON字段全但口径不一致不定低清洗工作量大我的常见做法是「备案目录做产品清单官网条款页做责任和免责抽取」两套数据用产品名做join。备案目录能保证产品覆盖度条款页能保证责任描述的可用性。聚合站数据虽然省事但同一款产品在不同站点的分类、缴费方式、保障期限字段经常对不上清洗成本反而高于自己抓。格式问题要提前想清楚。备案目录里产品名是唯一的条款页里产品名可能带「互联网专属」这类后缀join时要做归一化否则后面图谱里全是重复节点。这个坑在第5章会展开。2.2 本体建模保险产品实体、属性与关系的最小Schema先建本体再写爬虫和解析脚本这个顺序不能反。中途改Schema意味着清洗脚本、抽取脚本、导入脚本全部重写我第一次做时就是因为先抓数据后建本体返工了整整一轮。保险领域的最小本体我建议先只建四类实体实体关键属性说明Insurerid, name保险公司Productid, name, category, premium, wait_period, coverage_period保险产品Coverageid, name, description保障责任Exclusionid, name, description免责条款实体间的关系不需要一开始就设计得很全四类就够了关系方向语义PROVIDED_BYProduct → Insurer产品的承保公司COVERSProduct → Coverage产品覆盖的保障责任EXCLUDESProduct → Exclusion产品免责的情形HAS_CLAUSEProduct → Clause产品对应的条款原文为什么要单独建Clause实体因为问答系统后期要支持「条款怎么说」这类追问把条款原文挂在产品节点下后续做文本检索和增量更新都方便。只建Product和Coverage两类节点虽然能跑通demo但问答边界会很窄加一个Clause节点成本极低收益明显。2.3 规则抽取脚本把条款文本转成三元组条款文本是半结构化数据章节标题固定保险责任、责任免除、保险金额等用正则加关键词规则就够用没必要一上来上NER。实体类型有限规则可比可解释几百条款文本跑下来也稳定。import re from pathlib import Path def split_sections(clause_text): 按第X条把条款文本切成有序章节返回 [(标题, 正文), ...] items re.split(r(第[一二三四五六七八九十百]条), clause_text) # items[0] 是第一个标题前的杂文本通常为空丢弃 result [] for i in range(1, len(items) - 1, 2): title items[i].strip() body items[i 1].strip() result.append((title, body)) return result def extract_triples(product_id, sections, term_dict): 从章节里按关键词表抽取三元组 triples [] for title, body in sections: if 保险责任 in title or 保障内容 in title: for term, keywords in term_dict.items(): if any(kw in body for kw in keywords): triples.append((product_id, covers, term)) if 责任免除 in title or 免责 in title: for term, keywords in term_dict.items(): if any(kw in body for kw in keywords): triples.append((product_id, excludes, term)) return triples # 术语表保障责任名 - 触发关键词 term_dict { 住院医疗: [住院医疗, 住院费用], 门急诊医疗: [门急诊, 门诊医疗], 重大疾病: [重大疾病, 重疾, 恶性肿瘤], 意外身故: [意外身故, 意外伤害身故], }这段代码的逻辑核心是「章节命中加关键词命中」双重过滤。先按章标题切分只处理「保险责任」和「责任免除」两个章节再在正文里查术语表。双重过滤的好处是误报率低条款里的「不承担意外身故责任」这类含否定词的句子因为章节落在责任免除里不会错配到covers关系上。参数上要注意两点。第一term_dict的关键词要包含常见同义变体「重大疾病」和「重疾」必须同时存在否则不同公司的条款表述会漏抽。第二正文匹配用的是in子串判断而不是正则精确匹配这样能容忍「住院医疗费用保险金」这类长名词——但代价是「住院医疗」会同时命中「住院医疗保险金」和「住院医疗津贴」如果业务上要区分两类责任就需要把term_dict拆得更细。抽取结果落盘为CSV三列加一列来源标识product_id,predicate,object_id,source P001,covers,住院医疗,条款_第2章 P001,covers,重大疾病,条款_第2章 P001,excludes,酒后驾驶,条款_第3章source字段是后悔药。图谱数据出问题时能顺着source一路查到原始条款文本不用在黑匣子里瞎猜。这个习惯建议从一开始就保留。3. 图谱入库Neo4j与Cypher验证让三元组变成可查询的知识3.1 三元组文件设计为什么用CSV分四个文件而不是一个大JSON三元组CSV抽取完成后下一步是导入Neo4j。我建议按节点和关系拆成独立文件而不是塞进一个大JSON一次性导入文件必填字段用途product.csvid, name, category, wait_period产品节点insurer.csvid, name公司节点coverage.csvid, name, description保障责任节点rel_covers.csvproduct_id, coverage_id产品-责任关系拆文件的好处有两个。第一增量更新方便——新产品上线时只追加product.csv和对应的rel文件不用重导全部数据。第二故障定位快——关系数对不上时直接查对应rel文件的行数就能确认是不是抽取环节漏了。一个大JSON文件在数据量不大的时候貌似省事但一旦产品超过几百个JSON解析内存占用高且某一行格式错误会拖垮整个导入。CSV加LOAD CSV失败时能定位到具体行这是实战里最实在的优势。3.2 批量导入Neo4jCypher脚本与参数说明我用的是Neo4j 5社区版。CSV文件放到Neo4j的import目录下然后依次执行节点导入和关系导入。// 导入产品节点 LOAD CSV WITH HEADERS FROM file:///product.csv AS row MERGE (p:Product {id: row.id}) SET p.name row.name, p.category row.category, p.wait_period toInteger(row.wait_period); // 导入关系产品-保障责任 LOAD CSV WITH HEADERS FROM file:///rel_covers.csv AS row MATCH (p:Product {id: row.product_id}) MATCH (c:Coverage {name: row.coverage_id}) MERGE (p)-[:COVERS]-(c);这里有几个参数层面的关键点。MERGE而不是CREATE是为了幂等——脚本重复执行时不会产生重复节点和重复关系这是防止增量更新翻车的底线。wait_period用toInteger做类型转换因为CSV里所有字段读进来都是字符串不转换的话后续比较等待期长短会得到错误结果。关系导入前必须先建完所有相关节点否则MATCH匹配不到导入静默跳过但不会报错最后图谱里关系数会莫名其妙地缺失。3.3 三条必查Cypher节点量、孤立点、关系密度导入完成后不要急着写问答先跑三条查询确认图谱没烂// 1. 各类节点数量 MATCH (n) RETURN labels(n) AS type, count(*) AS cnt ORDER BY cnt DESC; // 2. 没有任何关系的孤立产品 MATCH (p:Product) WHERE NOT (p)--() RETURN p.id, p.name LIMIT 20; // 3. 各关系类型的数量分布 MATCH ()-[r]-() RETURN type(r) AS rel, count(*) AS cnt ORDER BY cnt DESC;第一条确认节点量级符合预期产品数和数据源里的清单对得上。第二条是重点孤立产品占比超过10%基本可以断定关系抽取环节出了问题——要么术语表覆盖不够要么条款切分逻辑漏了章节。第三条检查关系密度正常情况下covers关系的数量应该是products节点数的三到五倍每款产品平均三到五项保障责任如果covers明显偏少回去检查抽取脚本的章节命中。这三种查询在后续每次增量导入后都要跑一遍当作健康检查比人工抽查数据可靠得多。4. 简易问答系统三件套意图识别、模板映射与答案生成4.1 先做问题分类保险问句的六种典型意图问答系统的第一步不是实体识别而是意图分类。保险问句的类型非常有限我归纳下来六种就覆盖了绝大多数场景意图示例问句对应查询目标产品类型查询有哪些重疾险按category查产品列表公司产品查询平安有哪些产品在售按公司查产品列表保障责任查询这款保险保什么查产品的covers关系免责查询什么情况不赔查产品的excludes关系属性查询等待期是多长查产品属性字段产品对比这两款有什么区别查两款产品的差异先定意图再抽实体比直接用命名实体识别更稳。原因很实际模板规则可解释、可维护换一个领域改规则就行而NER模型在保险这种专业术语多的场景下没有标注数据很难训好且出错后不知道错在哪。问答上线后的每一次误答都能从模板规则里直接定位。4.2 从问句到Cypher规则模板匹配的Python实现意图规则表加实体词典是保险问答最务实的最小实现。import re def parse_intent(question): 按优先级匹配意图 intent_rules [ (compare_query, [区别, 对比, 哪个好, 怎么选]), (exclusion_query, [不赔, 免责, 除外, 不保]), (coverage_query, [保什么, 保障哪些, 保障内容, 有什么用]), (attr_query, [等待期, 缴费期, 保多久, 保额]), (company_query, [哪些产品, 有什么产品, 在售]), ] for intent, keywords in intent_rules: for kw in keywords: if kw in question: return intent return unknown def extract_entity(question, entity_dict): 按名称长度降序做词典最长匹配 for entity in sorted(entity_dict, keylen, reverseTrue): if entity in question: return entity return None def to_cypher(intent, entity): if intent coverage_query: return fMATCH (p:Product {{name: {entity}}})-[:COVERS]-(c:Coverage) RETURN c.name if intent exclusion_query: return fMATCH (p:Product {{name: {entity}}})-[:EXCLUDES]-(e:Exclusion) RETURN e.name if intent attr_query: return fMATCH (p:Product {{name: {entity}}}) RETURN p.wait_period return Noneparse_intent里两个细节值得说。第一compare_query放在最前面因为「区别」「对比」这类词不会出现在其他意图的合法问句里优先匹配能减少误判。「不赔」和「保什么」并存时如果理赔词在前面这款保险不赔什么会被正确分到exclusion_query而不是coverage_query。第二extract_entity用长度降序匹配避免「百万医疗险」被「医疗险」先命中——虽然in判断里两个词都命中了但先匹配长词能拿到更精确的实体。生成的Cypher用了f-string拼实体名这在demo里能跑但存在注入风险。做成正式服务时建议改成参数化查询实体的值走参数绑定而不是拼进查询串。4.3 问答评估与兜底策略准确率、覆盖率与「不知道」问答系统做完第一版后要立刻建一个评估集不要凭感觉说「效果还行」。我的做法是人工构造80到100条问句覆盖六个意图每条标注期望答案。然后逐个跑一遍统计准确率。准确率计算答对的问句数除以总问句数。这里「答对」的定义要提前约定——属性查询的等待期答对了字段就算对产品对比只要列出的差异项和期望集合有重叠就算对。定义不清晰评估就没有意义。兜底策略同样重要。意图识别结果unknown时不要硬答。Graph数据库答错比不答更伤信任凡是匹配不到意图或实体的问句统一返回「我还没有找到相关信息」把问句记到日志里每周看一次哪些问题没覆盖到再补规则和术语表。这个循环是问答系统准确率从六成拉到九成的主要路径。5. 保险知识图谱问答常见问题与避坑五个血泪踩坑记录5.1 数据层备案名与销售名不一致、条款章节名不统一现象图谱里出现两个看似一模一样的产品节点一个叫「XX医疗险」另一个叫「XX医疗险互联网专属」问答系统查产品时只能返回其中一个用户问另一款产品时答非所问。原因备案目录里的产品名和官网销售页的产品名本身就不一致抽取阶段没做归一化导致同一款产品被建成了两个节点。解决在实体对齐阶段维护一张别名表把「XX医疗险互联网专属」「XX医疗险互联网版」「XX医疗险2024款」都归一到标准名。Product节点的id必须是唯一标识不管数据来自哪个源merge前先查别名表做映射而不是直接用原始名称建节点。现象条款解析后某款产品的covers关系数量明显偏少抽查原文发现「保险责任」章节没被命中。原因部分附加险的条款章节叫「保障内容」或「保障责任范围」不叫「保险责任」split_sections的标题匹配漏了。解决章节名匹配改成候选列表保险责任|保障内容|保障责任一并匹配。这个坑属于典型的「规则过拟合」——写正则时只看了一两家公司的条款就定稿换数据源就翻车。建议一开始就把主要保险公司的条款样式各看十份再定规则。5.2 图谱层关系方向混乱与重复导入现象问答系统查到的保障责任反了比如回答「这款产品保障意外身故」但实际图谱关系是从产品指向意外身故查询时写反了方向导致结果空或结果错。原因定义本体时关系方向没定清楚COVERS到底是Product指向Coverage还是Coverage指向Product不同开发人员理解不同写成反向。解决换一次方向需要重导全部关系数据。在项目第一天就把方向写进产品文档并在导入脚本里统一用(p)-[:COVERS]-(c)的写法。顺带在3.3的验证查询里检查每个Product节点的出边数量如果出现「coverage节点有几十条出边」这种异常基本就是方向搞反了。现象增量导入跑了三遍后covers关系数量翻了三倍问答结果也开始出现重复项。原因导入脚本用了CREATE而不是MERGE重复执行同一批CSV时每次都新建关系。解决所有导入统一用MERGE并给节点和关系建立唯一约束CREATE CONSTRAINT product_id_unique IF NOT EXISTS FOR (p:Product) REQUIRE p.id IS UNIQUE;有唯一约束兜底即使误用CREATE也会直接报错而不是静默产生脏数据。5.3 问答层模板过拟合与Python依赖版本现象用户问「这款保险保障哪些内容」能答上来换成「这款保险都管什么病」就返回「不知道」明明图谱里有对应数据。原因意图规则表里只写了「保障哪些」和「保障内容」没覆盖口语化变体。解决每个意图至少配三到五个同义关键词并在评估集里加入高频变体。这里我自己的经验是不要自己闷头想同义词直接拿用户真实问句日志去补规则一周补一次比拍脑袋高效得多。现象项目换了一台机器重装环境后问答服务报错py2neo连不上Neo4j 5。原因py2neo依赖的Bolt协议版本和Neo4j 5不兼容这个不是代码问题也不是配置问题是驱动和数据库版本不匹配的经典翻车现场。解决直接换官方neo4j-driver这是兼容性最稳的选择。如果公司环境有约束必须用py2neo那要锁Neo4j 4.x版本并用Python 3.8以下的环境。依赖版本这件事没有后悔药开工第一天就在requirements.txt里锁死数据库版本、驱动版本和Python版本能省一周的排查时间。6. 从简易问答走向增量更新的保险知识服务验证路径与进阶方向6.1 增量更新与版本管理图谱不是建完就结束的。保险产品迭代很快新产品上架、老产品停售都需要更新。我的做法是每次更新都走完整流程抓取新条款 → 运行抽取脚本 → 生成增量CSV → 跑3.3的健康检查 → 执行MERGE导入。数据文件放进Git管理每次更新打一个tag出问题时能回滚到上一个版本。不要直接在线上库里手动改节点改完你会失去对图谱状态的所有掌控。6.2 回归测试集问答系统的后悔药问答系统每改一次意图规则或术语表都要跑一遍回归测试防止修了A问题弄坏B功能。用一段极简脚本就能做TEST_SET [ (这款保险保什么, coverage_query), (哪些情况不赔, exclusion_query), (等待期多长, attr_query), (这两款产品有什么区别, compare_query), ] def run_regression(parse_intent, test_setTEST_SET): passed 0 for question, expected in test_set: if parse_intent(question) expected: passed 1 else: print(ffailed: {question}) print(f{passed}/{len(test_set)} passed)回归测试集是问答系统的后悔药。改任何规则前跑一遍改完再跑一遍比上线后被用户投诉发现问题效率高得多。我建议评估集做到200条以上覆盖六个意图的所有已知变体。6.3 从模板问答到检索增强生成模板问答的边界很清楚它能回答六个意图的标准问句但客户问「我爸妈六十多岁该买什么保险」这种策略性问题规则系统无能为力。这时自然的演进路径不是重写系统而是在图谱上做检索增强。图谱检索出相关产品、责任、条款段落拼成上下文再交给大模型组织成答案。图谱负责精准召回模型负责自然表达两者的分工比让模型直接去理解图谱更可控。这套方案从零到一最值得投入的部分不是问答准确率调多高而是把「产品数据 → 本体对齐 → 图谱存储 → 查询召回」的链路彻底打通并文档化。我最初那版图谱产品节点不足五百个问答准确率只有六成出头但正是靠这个底子后续加增量更新和检索增强半年内准确率接近九成。数据、源码和文档握在自己手里每走一步都能复现、能回滚这比任何单点的技术突破都重要。希望帮到你。本文还有配套的精品资源点击获取