
简介面向毕业设计和大作业场景的基于知识图谱的问答系统实现以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.py和const.py是地基build_graph.py只负责写库intention_recognize.py、intention_to_cypher.py、graph_qa.py、search_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再创建关系。MERGE和CREATE的区别是前者会先检查图中是否已有相同节点如果有就返回现成节点没有才创建。对需要反复运行构建脚本的场景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_namerow[disease], symptom_namerow[symptom] ) with open(csv_path, encodingutf-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_name和symptom_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, keylen, reverseTrue): if alias in question: return standard_name return Nonesorted(alias_list, keylen, reverseTrue)这一步是为了让“高血压病”优先于“高血压”被匹配。如果先匹配短词“高血压病”会被截成“高血压”虽然也能查出结果但不够精确。返回的是标准名而不是命中别名因为图谱里的节点存储在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(funsupported intent: {intent}) cypher TEMPLATES[intent].format(entityentity) 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.py和search_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}展开成limit5传进去也就是我们在上一章模板里看到的$limit。如果查询结果为空records是空列表而不是None因此调用方可以直接用if not records判断。实际使用中要注意 Neo4j 驱动的版本兼容性。旧版本用session.run新版推荐session.execute_read或session.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:7687Neo4jError: The client is unauthorized用户名密码错误修正config.py中的认证信息Label Disease not found还没运行build_graph.py先执行python build_graph.py查询返回空列表但数据存在关系方向写反对比const.py中关系方向定义中文变成乱码CSV 编码问题读取时指定encodingutf-8验证整个链路是否通顺可以执行python build_graph.py python test.pytest.py一般会内置几条测试问句比如“高血压有什么症状”“高血压吃什么药”然后把generate_answer的结果打印出来。如果输出符合预期说明图谱构建、意图识别、Cypher 生成、答案包装这条链路没有问题。5. 毕业设计和大作业里怎么二开换数据、加接口、接大模型很多人拿到这套源码后只会跑test.py然后发现和自己研究的数据集对不上。这一章直接讲怎么改造成自己的项目。5.1 换成自己的医疗数据集最消耗时间的是把数据整理成图谱能接受的格式。我一般会先在data/medical下建立三个 CSVdisease.csv、symptom.csv、relation.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(forceTrue) 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(host0.0.0.0, port5000, debugTrue)这里generate_answer是我们上一章封装好的函数前端只需要 POST 一个 JSON后端返回另一个 JSON。参数forceTrue允许接收不带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里的模板这是最省时间的验证技巧。本文还有配套的精品资源点击获取