news 2026/9/16 5:30:54

基于Python和Neo4j的知识图谱问答系统实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Python和Neo4j的知识图谱问答系统实战解析

简介:面向毕业设计和大作业场景的基于知识图谱的问答系统实现,以Python为主要开发语言,围绕知识图谱构建、意图识别、查询生成等核心环节提供完整代码框架。项目包含实体关系建模、问题意图分类、自然语言到Cypher语句转换等功能模块,适合希望快速掌握知识图谱问答落地方法的开发者参考学习。压缩包共17个文件,其中10个Python脚本承担图谱构建、问答检索、意图识别等主体逻辑,另有配置文件、测试脚本和说明文档,整体大小约12.58MB,结构精简易于阅读和二次开发。当前已有254人学习使用,可作为课程设计、毕业设计或工程实践的起点。从中可以了解从原始数据到图谱构建、再到问答交互的完整链路,并基于自带测试数据验证效果,降低从零搭建系统的门槛。

1. 知识图谱问答系统为什么值得拿来当毕业设计

问一个真实问题:“高血压患者吃哪种药?”传统搜索引擎返回一堆网页,而知识图谱问答系统会先识别“高血压”是疾病实体,再匹配“吃哪种药”是治疗方案意图,然后把问句翻译成MATCH (d:Disease)-[r:TREATS]->(dr:Drug)这样的图查询,最后返回“硝苯地平、氯沙坦”这类结构化答案。这套基于 Python 和 Neo4j 的 Knowledge Graph-based Question-Answering System,正好把知识图谱构建、实体识别、意图识别、Cypher 查询串成一条完整的工程链路。对正在做知识图谱 python 毕业设计或大作业的人来说,源码结构清晰,能直接跑通,也适合在此基础上换数据集、加接口、接大模型,是一个能讲清楚原理又能演示的实战项目。

2. 理解项目骨架:从实体、关系到Cypher建模

拿到压缩包解压后,第一件事不是急着跑代码,而是先把目录结构读懂。这个项目把“建模”和“问答”拆成了两个阶段:先由build_graph.py把结构化医疗数据写进 Neo4j,再由问答链路去读。搞清楚这条边界,后续替换数据和排查错误都会省很多时间。

2.1 读懂目录与模块边界

展开后的核心结构大概是这样的:

SJT-code/ ├── build_graph.py # 构建知识图谱,把数据写入 Neo4j ├── graph_qa.py # 问答主流程,执行 Cypher 查询 ├── intention_recognize.py # 意图识别 ├── intention_to_cypher.py # 意图转 Cypher 查询语句 ├── search_answer.py # 生成自然语言答案 ├── utils.py # 实体提取、别名归一化等通用函数 ├── const.py # 实体类型、关系类型常量 ├── config.py # Neo4j 连接配置 ├── test.py # 命令行测试入口 ├── data/ │ ├── medical/ # 医疗数据文件 │ └── test/ # 测试问句 └── requirements.txt

从依赖关系看,config.pyconst.py是地基,build_graph.py只负责写库,intention_recognize.pyintention_to_cypher.pygraph_qa.pysearch_answer.py组成一条问答流水线。这种分层的最大好处是:构建和查询使用的实体名、关系名都来自const.py,不会出现构建时写的是中药、查询时写的是药物这种不一致。

graph_qa.py在整个链路中只做一件事:接收 Cypher 语句和参数,执行查询,返回记录。它不管问句怎么理解、答案怎么组织。这样设计后,如果你只想测试图谱数据是否建好,可以直接在graph_qa.py里调用run_query,而不需要经过意图识别。

2.2 数据建模:医疗实体和关系如何落地 Neo4j

知识图谱的核心是实体和关系。在医疗问答场景里,实体类型通常包括疾病、症状、药物、检查项目、科室等,关系则描述实体之间的语义关联。项目里一般会在const.py中把这些类型定义成字符串常量:

# const.py DISEASE = "Disease" SYMPTOM = "Symptom" DRUG = "Drug" CHECK_ITEM = "CheckItem" REL_HAS_SYMPTOM = "HAS_SYMPTOM" REL_TREATS = "TREATS" REL_CHECKS = "CHECK_ITEM"

对应的数据建模关系可以整理成下面这张表:

实体类型关系实体类型示例
DiseaseHAS_SYMPTOMSymptom高血压 -> 头晕
DrugTREATSDisease硝苯地平 -> 高血压
DiseaseNEED_CHECKCheckItem糖尿病 -> 糖耐量试验
SymptomBELONGS_TODisease心悸 -> 心律失常

这里有一个容易被新手忽略的点:Cypher 查询里的关系是有方向的。TREATS通常建模为(Drug)-[TREATS]->(Disease),如果你反向写写成(Disease)-[TREATS]->(Drug),查询结果会为空。所以我一般会把关系方向也写在const.py的注释里,或者在build_graph.py中统一用有向的MERGE语句,避免建模和查询方向不一致。

2.3 build_graph.py 的构建流程与去重策略

build_graph.py的目标是把data/medical下的 CSV 或 JSON 转成图数据。常见做法是读取每一行,对每个实体先MERGE再创建关系。MERGECREATE的区别是前者会先检查图中是否已有相同节点,如果有就返回现成节点,没有才创建。对需要反复运行构建脚本的场景,MERGE能避免出现大量重复实体。

# build_graph.py 核心逻辑 from config import driver from const import DISEASE, SYMPTOM, REL_HAS_SYMPTOM def build_from_csv(csv_path): def create_graph(tx, rows): for row in rows: tx.run( "MERGE (d:Disease {name: $disease_name}) " "MERGE (s:Symptom {name: $symptom_name}) " "MERGE (d)-[:HAS_SYMPTOM]->(s)", disease_name=row["disease"], symptom_name=row["symptom"] ) with open(csv_path, encoding="utf-8") as f: rows = csv.DictReader(f) with driver.session() as session: session.execute_write(create_graph, rows) if __name__ == "__main__": build_from_csv("data/medical/symptom.csv")

注意三个参数:disease_namesymptom_name是 Cypher 查询的参数,使用参数化查询而不是把值直接拼进语句,既能避免特殊字符导致的转义问题,也能防止 Cypher 注入;session.execute_write是 Neo4j Python Driver 里带事务重试的写法,比如网络抖动时驱动会自动重试,比session.run更稳。构建完成后,建议为实体加上唯一性约束,例如CREATE CONSTRAINT FOR (d:Disease) REQUIRE d.name IS UNIQUE,这样后续MERGE才会严格按 name 去重。

3. 意图识别与问句转Cypher:规则路径也能跑得稳

问答系统最核心的转折点是把自然语言问句变成机器能执行的查询。这个项目没有一上来就接大模型,而是先用规则和词典把意图识别、实体抽取、Cypher 生成三个步骤拆开。对毕业设计来说,这种可解释性强、不依赖外部 API 的方案反而更容易讲清楚。

3.1 意图识别:规则优先还是训练模型

intention_recognize.py负责判断用户想问什么。意图可以分成“求症状”“求药物”“求检查项目”等。用规则做意图识别,本质上就是维护一组关键词映射:

# intention_recognize.py INTENT_KEYWORDS = { "symptom": ["症状", "表现", "有哪些反应"], "drug": ["吃什么药", "药品", "用药", "治疗"], "check": ["检查项目", "做什么检查", "确诊"], } def recognize(question: str) -> str: for intent, keywords in INTENT_KEYWORDS.items(): for kw in keywords: if kw in question: return intent return "default"

这段代码的关键在于关键词顺序:更具体的短语要放在前面,比如“吃什么药”必须先于“治疗”匹配,否则“高血压吃什么药治疗”会被错误归类为症状意图。规则方法的局限也很明显,用户换个说法就可能漏匹配,但对于受限领域的知识图谱问答系统,覆盖最常见的几种提问方式已经足够。

这里可以和基于 DeepSeek 的问答系统做个对比。大模型能理解更开放的表达,但需要额外部署、控制成本,而且可能生成图谱里没有的答案。更务实的做法是把规则作为主链路,把大模型作为兜底或意图纠正的辅助模块,这个我们在第 5 章再展开。

意图触发词示例对应查询方向
symptom症状、表现(Disease)-[:HAS_SYMPTOM]->(Symptom)
drug吃什么药、用药(Drug)-[:TREATS]->(Disease)
check检查、确诊(Disease)-[:NEED_CHECK]->(CheckItem)

3.2 实体抽取与别名归一化

光知道意图还不够,还要知道问的是哪个疾病。utils.py里通常会提供一个从问句中抽取标准实体名的函数。常见做法是先维护一个“别名 -> 标准名”的映射,再对问句做最长匹配:

# utils.py ENTITY_ALIASES = { "高血压": ["高血压", "高血压病", "hypertension", "血压高"], "糖尿病": ["糖尿病", "diabetes"], } def extract_entity(question: str, aliases: dict = ENTITY_ALIASES) -> str | None: for standard_name, alias_list in aliases.items(): for alias in sorted(alias_list, key=len, reverse=True): if alias in question: return standard_name return None

sorted(alias_list, key=len, reverse=True)这一步是为了让“高血压病”优先于“高血压”被匹配。如果先匹配短词,“高血压病”会被截成“高血压”,虽然也能查出结果,但不够精确。返回的是标准名而不是命中别名,因为图谱里的节点存储在name属性上,查询时只能用标准名去匹配实体节点。项目里还可以扩展别名表,把“血压高”“hypertension”统一映射到“高血压”,这也是知识表达中的一个常见环节。

3.3 问句到 Cypher 的模板映射

意图和实体都确定后,intention_to_cypher.py负责把它们组合成可执行的 Cypher。这里最直接的实现是模板字符串配合参数化传值:

# intention_to_cypher.py TEMPLATES = { "symptom": ( "MATCH (d:Disease {{name: $entity}})-[:HAS_SYMPTOM]->(s:Symptom) " "RETURN s.name AS name LIMIT $limit" ), "drug": ( "MATCH (d:Disease {{name: $entity}})<-[:TREATS]-(dr:Drug) " "RETURN dr.name AS name LIMIT $limit" ), } def to_cypher(intent: str, entity: str, limit: int = 5): if intent not in TEMPLATES: raise ValueError(f"unsupported intent: {intent}") cypher = TEMPLATES[intent].format(entity=entity) return cypher, {"limit": limit}

注意模板里{{}}是为了在 Python string 的format中保留字面的大括号,最终生成的是MATCH (d:Disease {name: $entity})。实体名通过$entity参数传入,而不是直接格式化进语句,这是为了防止实体名带引号或特殊字符破坏查询结构。limit参数用来限制返回条数,避免“高血压”这类实体关联几十个症状时刷屏。实际调用时,graph_qa.py接收这个 Cypher 和参数,执行后返回记录列表。

4. 图查询与答案生成:graph_qa.py + search_answer.py 的配合

问答流水线的最后一段是查询和答案包装。这一阶段最容易出的问题是:图谱明明有数据,却查询不到;查询到了,又不会组织成人类能读的句子。graph_qa.pysearch_answer.py分别解决这两件事。

4.1 graph_qa.py:执行Cypher并处理空结果

graph_qa.py的核心是执行 Cypher 并返回结构化记录。代码可以简明地写成这样:

# graph_qa.py from config import driver def run_query(cypher: str, params: dict = None): if params is None: params = {} with driver.session() as session: result = session.run(cypher, **params) return [record.data() for record in result] def answer_question(question: str): from intention_recognize import recognize from intention_to_cypher import to_cypher from utils import extract_entity intent = recognize(question) entity = extract_entity(question) if entity is None: return "没有识别到疾病实体,请换一种描述再试。" cypher, params = to_cypher(intent, entity) records = run_query(cypher, params) return records

这里的细节是record.data(),它会把每条 Cypher 返回记录转换成 Python 字典,便于后续处理。session.run(cypher, **params)中的**params{"limit": 5}展开成limit=5传进去,也就是我们在上一章模板里看到的$limit。如果查询结果为空,records是空列表,而不是None,因此调用方可以直接用if not records判断。

实际使用中要注意 Neo4j 驱动的版本兼容性。旧版本用session.run,新版推荐session.execute_readsession.execute_write,从 Neo4j 4.4 开始事务函数是更稳的写法。如果连接失败或查询超时,多半是config.py里的URI、用户名密码写错,或者索引缺失导致查询全图扫描。

4.2 search_answer.py:从记录到自然语言答案

查询返回的是字典列表,比如[{"name": "头晕"}, {"name": "乏力"}]。如果直接把这些数据扔给前端,用户没法用。search_answer.py通常负责把记录拼成一句话:

# search_answer.py from graph_qa import answer_question def generate_answer(question: str) -> str: intent = recognize(question) entity = extract_entity(question) records = answer_question(question) if not records: return f"抱歉,图谱中暂时没有「{entity}」的{intent}信息。" names = [r["name"] for r in records] if intent == "symptom": return f"{entity}的常见症状包括:{'、'.join(names)}。" if intent == "drug": return f"治疗{entity}的常用药物有:{'、'.join(names)}。" return str(records)

这段代码的意义在于把“查询结果”和“展示结果”解耦。即使后续改成 Web 接口,前端也只关心answer字段,不需要理解图谱结构。注意names = [r["name"] for r in records]里用了r["name"],因为在to_cypher的模板里,我们RETURN s.name AS name,这里AS name必须和代码里的 key 保持一致,否则报 KeyError。

4.3 常见查询问题与排查清单

按照我的经验,第一次跑通问答系统时,大部分时间都花在下面这几个问题上。

错误现象可能原因修复方式
Failed to connect to Neo4jconfig.py中 URI 写成了http://连接驱动写成bolt://127.0.0.1:7687
Neo4jError: The client is unauthorized用户名密码错误修正config.py中的认证信息
Label 'Disease' not found还没运行build_graph.py先执行python build_graph.py
查询返回空列表,但数据存在关系方向写反对比const.py中关系方向定义
中文变成乱码CSV 编码问题读取时指定encoding="utf-8"

验证整个链路是否通顺,可以执行:

python build_graph.py && python test.py

test.py一般会内置几条测试问句,比如“高血压有什么症状?”“高血压吃什么药?”,然后把generate_answer的结果打印出来。如果输出符合预期,说明图谱构建、意图识别、Cypher 生成、答案包装这条链路没有问题。

5. 毕业设计和大作业里怎么二开:换数据、加接口、接大模型

很多人拿到这套源码后,只会跑test.py,然后发现和自己研究的数据集对不上。这一章直接讲怎么改造成自己的项目。

5.1 换成自己的医疗数据集

最消耗时间的是把数据整理成图谱能接受的格式。我一般会先在data/medical下建立三个 CSV:disease.csvsymptom.csvrelation.csv,其中relation.csv至少包含entity1, relation, entity2三列。然后在const.py中注册新的实体类型和关系类型。如果实体类型变了,build_graph.py中也要同步修改MERGE语句里的标签名。这个过程可以先用 10 条数据手工验证,跑通后再批量导入,避免一次导入几千条后找不到报错源头。

5.2 用 Flask 包装成 Web 接口

毕业设计如果需要演示网页,最轻量的办法是用 Flask 包一层 HTTP 接口:

# app.py from flask import Flask, request, jsonify from search_answer import generate_answer app = Flask(__name__) @app.route("/qa", methods=["POST"]) def qa(): data = request.get_json(force=True) question = data.get("question", "") if not question: return jsonify({"error": "question is required"}), 400 answer = generate_answer(question) return jsonify({"answer": answer}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)

这里generate_answer是我们上一章封装好的函数,前端只需要 POST 一个 JSON,后端返回另一个 JSON。参数force=True允许接收不带Content-Type: application/json的请求,适合联调时图省事的场景。有了这个接口,再配合简单的 HTML 页面,就够完成一个完整的毕设展示。

5.3 用大模型补全开放问答能力

规则模板的能力边界在于:图谱里没有的关系,系统只能回“没有找到答案”。想要提升体验,可以在search_answer.py返回空列表时,把问题转交给大模型兜底。比如保留知识图谱问答作为确定性答案来源,当records为空时再调用接入了 DeepSeek 这类模型的接口,让模型基于医学常识回答。这样做的好处是既不牺牲已有图查询的准确率,又能覆盖用户更宽泛的表达。配置时注意把大模型返回的时间控制在两秒以内,否则演示效果会打折扣。调整关系模板时,我一般会用一句MATCH p=()-[r]->() RETURN p LIMIT 1先在 Neo4j Browser 里确认关系方向,再改intention_to_cypher.py里的模板,这是最省时间的验证技巧。

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

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

SpringBoot快速搭建与高效开发实战指南

1. SpringBoot项目快速搭建指南SpringBoot作为当下Java领域最流行的开发框架&#xff0c;其"约定优于配置"的理念让开发者能够快速构建生产级应用。我在实际项目中已经用SpringBoot开发过十几个微服务系统&#xff0c;今天就来分享一套经过实战检验的快速启动方案。对…

作者头像 李华
网站建设 2026/9/16 5:30:21

COMSOL仿真手性纳米材料的光学响应与建模技巧

1. 项目概述&#xff1a;等离子体手性纳米材料与COMSOL仿真的交叉研究在纳米光子学领域&#xff0c;等离子体手性纳米材料因其独特的光-物质相互作用特性正引发研究热潮。这类材料通过精心设计的几何结构&#xff08;如螺旋形、G形或扭曲纳米棒阵列&#xff09;&#xff0c;能够…

作者头像 李华
网站建设 2026/9/16 5:29:47

Word 2013与Word 2021处理高清图片文档性能差距全解析

做了快十年的文字工作&#xff0c;我这两年最常被问的问题之一就是&#xff1a;为什么别人发来的Word文档&#xff0c;在我电脑上打开像放幻灯片一样&#xff0c;一卡一卡的&#xff1f;尤其是那种带了一堆高清截图、相机原图、扫描件的文档&#xff0c;几十页下来&#xff0c;…

作者头像 李华
网站建设 2026/9/16 5:29:27

AF700标记α-银环蛇毒素实验操作全指南

1. 项目背景与核心价值AF700-a-Bungarotoxin&#xff08;AF700标记的α-银环蛇毒素&#xff09;是神经生物学研究中的重要工具分子&#xff0c;这种荧光标记的神经毒素能特异性结合乙酰胆碱受体&#xff0c;在突触研究、药物筛选和神经退行性疾病机制探索中具有不可替代的作用。…

作者头像 李华
网站建设 2026/9/16 5:29:25

C++11枚举类:类型安全与工程实践详解

1. 枚举类基础回顾与类型安全革命2008年发布的C11标准引入的enum class&#xff08;枚举类&#xff09;彻底改变了传统枚举的使用方式。作为一名长期使用C进行系统开发的工程师&#xff0c;我深刻体会到enum class带来的类型安全革命。传统C风格enum最大的问题在于其枚举值会隐…

作者头像 李华
网站建设 2026/9/16 5:28:07

Ubuntu安装ROS2完整指南:从环境配置到工业级部署

1. 项目概述&#xff1a;为什么在Ubuntu上安装ROS2是机器人开发绕不开的第一步ROS 2不是单纯的一个软件包&#xff0c;而是一整套面向真实机器人系统的中间件架构——它把传感器驱动、运动控制、路径规划、状态监控这些原本需要从零写起的模块&#xff0c;变成可插拔、可复用、…

作者头像 李华