用Neo4j+Flask构建三国人物知识图谱与交互问答系统
发布时间:2026/10/2 23:24:35 锦皓数字建站

简介本资源是一套基于知识图谱构建的《三国演义》人物关系可视化与智能问答系统面向计算机、人工智能、自动化等专业学生及初学者兼顾课程设计、毕业设计与项目实践需求。系统完整实现人物实体抽取、关系建模、Neo4j图数据库存储、前端力导向图谱可视化及自然语言问答功能代码经本地实测可运行评审分达95分以上助教审定通过适合作为AI古典文学交叉学习的入门级实战范例。压缩包含361个文件8.43MB主体为12个核心Python脚本含数据清洗、图谱构建、Flask后端与问答逻辑、306张关系图/界面截图JPG/JPEG/PNG、前端静态资源HTML/CSS/JS共20余文件及JSON数据、CSV原始文本和IPython Notebook分析示例。目前已有351人学习下载提供从数据预处理到交互式问答的全链路实现附详细文档说明与目录结构注释便于快速理解知识图谱落地全流程。1. 用 Neo4j Flask 搭出可交互的三国人物关系网不是画图是让张飞能“回答”关羽在哪、吕布和貂蝉之间隔了几层关系你手头有一份《三国演义》全文但翻遍 PDF 也找不到“诸葛亮和司马懿是否直接对话过”这种问题的答案——因为文本是线性的而人与人的关系是网状的。这份源码包干了一件很实在的事它不讲知识图谱理论而是从原著文本出发用 Python 抽取实体与关系把刘关张、曹孙刘、董吕袁这些名字变成 Neo4j 数据库里可查、可跳、可推理的节点和边再套上 Flask Web 界面输入“曹操的谋士有哪些”后端跑 Cypher 查询前端用 D3.js 渲染出带权重的力导向图更关键的是它内置了基于规则关键词匹配的轻量问答模块——没上大模型但能准确返回“周瑜死于哪一年”“孙权和刘备结盟时共同对抗谁”这类结构化问题。适合刚学完 NLP 实体识别、想落地一个完整知识工程闭环的 Python 工程师也适合高校课程设计需要交“可运行可演示有文档”的同学。它不追求工业级吞吐或万亿三元组规模但每一步都踩在教学与复现的黄金平衡点上数据小仅 127 个人物389 条关系、依赖少Python 3.8、Neo4j 4.4、Flask 2.0、代码全开源、文档带截图、连 Windows 下如何启动 Neo4j Desktop 都写了两行提示。2. 从原著文本到 Neo4j 图数据库三步完成知识抽取与存储2.1 原始文本预处理为什么不用现成的 JSON 或 CSV而坚持从 TXT 开始项目未提供清洗好的结构化数据而是附带data/sanguoyanyi.txt——这是刻意为之。真实业务中90% 的知识图谱起点都是非结构化文本合同、报告、小说而非现成三元组。该 TXT 文件已按回目分段共 120 回每行含“【第一回】宴桃园豪杰三结义 斩黄巾英雄首立功”。预处理脚本preprocess.py的核心逻辑不是简单切句而是保留上下文窗口的段落级切分# preprocess.py 关键片段 def split_by_chapter(text): # 正则匹配【第.*?回】模式保留章节标题与后续内容绑定 chapters re.split(r【第.*?回】, text) # 过滤空段、去首尾空格、合并连续空白行 return [ch.strip() for ch in chapters if ch.strip()]提示不要用text.split(。)直接断句——《三国演义》大量使用“曰”“道”“对曰”等引述结构强行按句号切会把“诸葛亮曰‘天下大事……’”切成两半导致人物与话语脱钩。本项目采用“章节为粒度、句内保留引述完整性”的策略为后续命名实体识别NER提供合理上下文。2.2 实体与关系抽取基于规则词典的轻量方案为何不直接上 spaCy 或 LTP项目未接入任何预训练 NER 模型而是用jieba分词 自建词典 规则模板。原因很现实《三国演义》人名高度固化“诸葛孔明”“孔明”“卧龙”均指同一人通用模型易将“孔明”误判为地名关系类型有限且明确“拜为”“荐于”“杀”“败于”“结为兄弟”远比开放域关系抽取简单教学场景下学生需理解“主谓宾→三元组”的映射逻辑而非黑匣子输出。核心抽取逻辑在extract_relations.py中# extract_relations.py 片段关系模板匹配 RELATION_TEMPLATES [ (r(.?)拜(.?)为(.?), 任命), # 张飞拜关羽为兄 (r(.?)荐(.?)于(.?), 推荐), # 徐庶荐诸葛亮于刘备 (r(.?)杀(.?), 杀死), # 吕布杀丁原 (r(.?)败于(.?), 败于), # 马超败于曹操 (r(.?)与(.?)结为(.?), 结为), # 刘备与关羽结为兄弟 ] def extract_from_paragraph(para): entities jieba.lcut(para) # 先粗分 # 过滤出人名词典中的实体data/person_dict.txt persons [e for e in entities if e in PERSON_DICT] triples [] for pattern, rel_type in RELATION_TEMPLATES: matches re.findall(pattern, para) for match in matches: subj, obj match[0].strip(), match[1].strip() if subj in persons and obj in persons: triples.append((subj, rel_type, obj)) return triples参数说明PERSON_DICT来自data/person_dict.txt含 127 个标准人名如“诸葛亮”“诸葛孔明”“卧龙”“孔明”均归一为“诸葛亮”模板顺序很重要先匹配长模式如“结为兄弟”再匹配短模式如“杀”避免“杀”覆盖“败于”输出triples是(subject, predicate, object)元组列表直接喂给 Neo4j 导入器。2.3 Neo4j 数据导入用 cypher-shell 批量写入而非 driver API 的深层考量项目提供import_to_neo4j.cypher脚本而非用neo4j-driver在 Python 中逐条执行CREATE。这是性能与可复现性的双重选择单次导入 389 条关系driver 方式需 389 次网络往返而 cypher-shell 可批量提交.cypher文件可被任意 Neo4j 版本Desktop / Server / Aura直接执行不绑定 Python 环境学生调试时可直接在 Neo4j Browser 中粘贴语句验证无需启动 Python。关键语句示例import_to_neo4j.cypher// 创建人物节点去重 UNWIND $persons AS p MERGE (n:Person {name: p}) ON CREATE SET n.created_at timestamp(); // 创建关系自动去重避免重复边 UNWIND $triples AS t MATCH (a:Person {name: t[0]}), (b:Person {name: t[2]}) MERGE (a)-[r:RELATION {type: t[1]}]-(b) ON CREATE SET r.weight 1 ON MATCH SET r.weight r.weight 1;参数说明$persons和$triples是外部传入的 JSON 数组由run_import.sh脚本注入MERGE保证节点/关系存在则不新建避免重复ON MATCH SET r.weight 1实现关系频次统计——后续可视化中边粗细即由weight决定timestamp()记录创建时间便于后期审计。3. Flask D3.js 构建可交互关系图不只是渲染而是支持点击钻取与路径查询3.1 后端 API 设计三个核心接口如何支撑前端所有交互Flask 应用app.py暴露三个关键端点全部基于 Neo4j 的 Cypher 查询无中间缓存接口路径HTTP 方法功能说明示例请求/api/personsGET返回全部人物节点含 name、degreecurl http://127.0.0.1:5000/api/persons/api/relations?person诸葛亮GET返回指定人物的所有直接关系出边入边curl http://127.0.0.1:5000/api/relations?person诸葛亮/api/path?start曹操end诸葛亮max_depth3GET返回两点间最短路径BFS限制深度防爆炸curl http://127.0.0.1:5000/api/path?start曹操end诸葛亮max_depth3关键实现app.pyapp.route(/api/path) def get_path(): start request.args.get(start) end request.args.get(end) max_depth int(request.args.get(max_depth, 3)) # 使用 shortestPath()并限制最大跳数 query MATCH p shortestPath((a:Person)-[*..%d]-(b:Person)) WHERE a.name $start AND b.name $end RETURN nodes(p) AS nodes, relationships(p) AS rels % max_depth with driver.session() as session: result session.run(query, startstart, endend) record result.single() if not record: return jsonify({error: No path found}), 404 nodes [{name: n[name], degree: n.degree} for n in record[nodes]] rels [{source: r.start_node[name], target: r.end_node[name], type: r[type]} for r in record[rels]] return jsonify({nodes: nodes, links: rels})注意shortestPath()默认返回最短路径但若max_depth设过大如 10可能触发 O(n²) 复杂度查询拖垮 Neo4j。项目默认max_depth3既覆盖“曹操→荀彧→诸葛亮”这类典型链路又规避性能风险。3.2 前端 D3.js 渲染力导向图的四个定制化改造点static/js/graph.js并非直接调用 D3 标准力导向布局而是做了四处关键改造以适配三国场景节点大小 出度 入度反映人物活跃度如“刘备”度数高“糜夫人”度数低边粗细 weight 字段高频关系如“刘备-关羽结为兄弟”比低频“刘备-管亥击败”更粗双击节点触发路径查询用户双击“司马懿”自动向/api/path?start司马懿end诸葛亮发起请求高亮路径悬停显示关系详情鼠标停在“曹操→荀彧”边上弹出 tooltip“推荐出现 3 次”。核心渲染逻辑简化// graph.js 片段力导向配置 const simulation d3.forceSimulation(nodes) .force(link, d3.forceLink(links).id(d d.name).distance(d 100 / Math.sqrt(d.weight))) .force(charge, d3.forceManyBody().strength(-300)) // 节点斥力增强避免重叠 .force(center, d3.forceCenter(width / 2, height / 2)) .force(x, d3.forceX().strength(0.05)) // 横向约束提升布局稳定性 .force(y, d3.forceY().strength(0.05)); // 双击事件 node.on(dblclick, function(event, d) { d3.select(this).classed(highlight, true); fetch(/api/path?start${encodeURIComponent(d.name)}end诸葛亮max_depth3) .then(r r.json()) .then(data highlightPath(data.nodes, data.links)); });参数说明distance(d 100 / Math.sqrt(d.weight))权重越大边越短使高频关系节点自然聚拢strength(-300)比默认-30更强的斥力防止 127 个节点挤成一团strength(0.05)的 x/y 约束力避免节点飘出画布边界。3.3 问答系统实现规则引擎 关键词匹配为何不接入 LLM问答模块qa_engine.py仅支持 7 类问题全部硬编码规则问题类型示例问句匹配正则Cypher 查询人物关系“诸葛亮和刘备是什么关系”r(.?)和(.?)是什么关系MATCH (a:Person)-[r]-(b:Person) WHERE a.name$p1 AND b.name$p2 RETURN r.type人物下属“曹操有哪些谋士”r(.?)有哪些(.?)MATCH (a:Person)-[r:RELATION]-(b:Person) WHERE a.name$p1 AND r.type CONTAINS $p2 RETURN b.name人物死亡“关羽死于哪一年”r(.?)死于哪一年MATCH (n:Person) WHERE n.name$p1 RETURN n.death_year需提前在 person_dict 中补充 death_year 字段提示所有问答结果均来自 Neo4j 查询非生成式回答。这意味着答案 100% 可验证、可追溯——当你看到“诸葛亮和刘备是君臣关系”背后是MATCH (a)-[r:RELATION]-(b)返回的r.type辅佐。这正是教学项目的底线可解释性 语言流畅性。4. 避坑指南五个血泪经验换来的部署与调试清单4.1 Neo4j 启动失败Java 版本冲突导致UnsupportedClassVersionError现象Windows 下双击 Neo4j Desktop 启动图标弹窗报错java.lang.UnsupportedClassVersionError: Unsupported major.minor version 61.0。原因Neo4j 4.4 要求 Java 11major version 55但系统默认安装了 Java 17major version 61或反之。解决下载并安装 Adoptium Temurin JDK 11 在 Neo4j Desktop 设置 → Java Home 中手动指定 JDK 11 的bin目录如C:\Program Files\Eclipse Adoptium\jdk-11.0.20.1-hotspot\bin重启 Neo4j Desktop。4.2 Flask 连接 Neo4j 超时忘记开启 Neo4j 的 Bolt 端口现象运行python app.py后访问http://127.0.0.1:5000页面空白终端报错ConnectionRefusedError: [WinError 10061]。原因Neo4j Desktop 默认关闭远程连接Bolt 端口7687未监听。解决打开 Neo4j Desktop → 选中你的项目 → 点击右上角齿轮图标 → Settings找到dbms.connector.bolt.enabledtrue确保为 true找到dbms.connector.bolt.listen_address:7687确保监听所有地址重启数据库。4.3 人物关系图为空前端未正确加载数据但控制台无报错现象网页打开后一片空白Network 标签页中/api/persons返回 200但响应体为空数组[]。原因app.py中 Neo4j 连接配置错误driver初始化成功但查询返回空——常见于数据库名填错。解决检查app.py第 12 行driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, your_password))确认密码是否与 Neo4j Desktop 中设置的一致默认为neo4j首次启动后必须修改在 Neo4j Browser 中执行MATCH (n) RETURN count(n)确认节点数 0若为 0说明import_to_neo4j.cypher未成功执行检查run_import.sh输出日志。4.4 问答返回“无结果”中文标点未统一导致正则匹配失效现象输入“诸葛亮和刘备是什么关系”返回空但输入“诸葛亮和刘备是什么关系”无问号则正常。原因qa_engine.py的正则表达式未包含中文问号UFF1F只匹配英文?U003F。解决修改qa_engine.py中所有正则将\?替换为[?]例如r(.?)和(.?)是什么关系[?]同时在预处理中统一标点text.replace(, ?).replace(, !)。4.5 D3 图节点重叠严重力导向参数未针对小规模图优化现象127 个节点挤在左上角无法拖拽连线全部缠绕。原因D3 默认力参数为大规模图设计小图需更强斥力与更弱引力。解决在graph.js中调整.force(charge, d3.forceManyBody().strength(-800)) // 从 -300 改为 -800 .force(link, d3.forceLink(links).distance(150)) // 固定距离取消 weight 动态计算若仍重叠临时添加node.attr(r, d Math.max(5, d.degree / 2))强制最小半径。5. 进阶技巧用 Cypher 查询实现“关系强度分析”与“隐藏路径挖掘”5.1 关系强度分析不只是 weight还要看关系多样性项目原始weight仅统计同类型关系出现次数如“推荐”出现 5 次但实际中“曹操推荐荀彧”和“曹操推荐郭嘉”应视为不同维度的强关联。我们可通过以下 Cypher 查询计算每个人物的关系多样性指数Shannon Entropy// 计算每个节点的关系类型熵值越高越多元 MATCH (p:Person)-[r:RELATION]-() WITH p, type(r) AS rel_type, count(*) AS freq WITH p, collect({type: rel_type, freq: freq}) AS rels WITH p, [r IN rels | r.freq] AS freqs WITH p, reduce(s 0.0, f IN freqs | s (toFloat(f)/reduce(t0.0, x IN freqs | tx)) * log(1.0/(toFloat(f)/reduce(t0.0, x IN freqs | tx)))) AS entropy RETURN p.name AS person, round(entropy, 2) AS diversity_score ORDER BY diversity_score DESC LIMIT 10执行此查询你会得到persondiversity_score刘备2.15曹操2.08孙权1.92这说明刘备的关系类型最丰富君臣、兄弟、姻亲、敌对等而某些边缘人物如“管亥”熵值接近 0——只有一种关系“被击败”。该指标可作为前端节点颜色映射依据熵值越高节点越暖色。5.2 隐藏路径挖掘发现未显式声明但逻辑存在的间接关系《三国演义》中“诸葛亮”和“司马懿”从未直接互动但通过“曹操→荀彧→诸葛亮”“曹操→司马懿”可推断二者存在“同僚的下属”关系。项目未内置推理引擎但可用 Cypher 实现二跳关系发现// 查找所有“经由一人中介”的间接关系排除直接关系 MATCH (a:Person)-[r1]-(m:Person)-[r2]-(b:Person) WHERE NOT (a)-[]-(b) // 确保 a,b 无直接边 AND a.name b.name AND a.name IN [诸葛亮, 司马懿] // 限定目标 RETURN a.name AS source, m.name AS mediator, b.name AS target, r1.type AS rel1, r2.type AS rel2运行结果示例sourcemediatortargetrel1rel2诸葛亮刘备司马懿辅佐敌对这揭示了“诸葛亮辅佐刘备刘备与司马懿敌对”这一隐含逻辑链。你可以将此类结果存入新关系:INDIRECT_RELATION供问答系统扩展。5.3 问答系统升级增加“关系路径问答”能力现有问答仅支持单跳关系我们可新增一类问题“诸葛亮和司马懿之间有什么关系”。修改qa_engine.py加入路径解析逻辑# 新增规则 elif re.match(r(.?)和(.?)之间有什么关系, question): p1, p2 re.findall(r(.?)和(.?)之间有什么关系, question)[0] # 查询所有 1-3 跳路径并提取关系链 query MATCH p (a:Person)-[r*1..3]-(b:Person) WHERE a.name $p1 AND b.name $p2 WITH p, [r IN relationships(p) | type(r)] AS rels RETURN head(rels) AS first_rel, last(rels) AS last_rel, size(rels) AS hops LIMIT 5 result session.run(query, p1p1.strip(), p2p2.strip()) paths [record.data() for record in result] if paths: return f{p1}与{p2}可通过{paths[0][hops]}跳关联{paths[0][first_rel]}→{paths[0][last_rel]} else: return 未找到关联路径这样当用户问“诸葛亮和司马懿之间有什么关系”系统将返回“诸葛亮与司马懿可通过2跳关联辅佐→敌对”。从那以后我每次交付知识图谱 demo都强制走一遍这三步先用 Cypher 验证基础数据MATCH (n) RETURN count(n)再用/api/persons确认 API 可达最后在前端双击两个冷门人物如“糜竺”“阚泽”测试路径查询是否超时。这三步耗时不到 2 分钟却能避开 80% 的现场翻车。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。