资讯动态

Django+Neo4j中医药知识图谱问答系统开发实践

发布时间:2026/10/27 23:43:24 来源:尧图企业网站定制
简介这套基于 Django 与 Neo4j 构建的中医药知识图谱及智能问答平台是一份面向计算机类专业毕业设计、课程设计与期末大作业的完整工程源码。项目围绕中医药知识体系将图谱建模、数据存储与问答交互整合在统一框架中难度适中源码已经本地编译验证可运行评审达到 98 分并经过助教审定适合学生直接参考或二次开发。压缩包共收录 2000 个文件总体量约 39MB。其中以 1059 个 Python 源码文件与 739 个编译缓存文件为主同时包含 66 个 JavaScript、34 个 CSS 及 HTML 模板等前端资源另有依赖配置文件、虚拟环境脚本与 SQLite 数据库文件结构与目录组织比较完整便于按模块进行阅读和调试。目前已有 138 人学习下载说明其在同类毕业设计资源中具备一定的参考价值。下载后可直接获得可运行项目、文档说明和完整代码框架省去从零搭建环境与整理数据的时间适合用于快速验证知识图谱问答系统的整体实现思路。1. 中医药知识图谱问答平台这个毕业设计不是“爬虫网页展示”的换壳项目每年毕业设计季大量Python方向的题目都逃不开“Django 爬虫 表格展示”的套路而“Django Neo4j 中医药知识图谱与智能问答平台”这个题目的反直觉之处在于它真正难的不是Django而是怎么把散落在文献里的中药、方剂、性味归经整理成一张能回答问题的图。这个项目做出来不是一个查数据库的网页而是一个能用中文问句直接拿结果的问答服务——用户问“黄芪有什么功效”系统回功效问“桂枝汤包含哪些药”系统回组成。它适合想系统接触知识图谱的Python方向学生也适合想低成本验证图谱落地的从业者。下面按建模、导入、查询、问答、排错的顺序把这套方案完整拆一遍。2. 从零搭出Neo4j图谱中药材、方剂、性味归经的实体关系建模与批量导入2.1 实体和关系怎么设计六个核心标签与五类关系的取舍知识图谱构建的第一步不是写代码而是定schema。这个题目里的数据源通常来自《中华本草》《中药学》教材或网络百科整理后大致能归成六类实体中药、方剂、功效、性味、归经、疾病。多一张表就会多一层维护成本初版不要追求“全”要把能支撑问答的几条路径先走出来。我的建议是只保留六个节点标签Herb、Formula、Effect、Property、Meridian、Disease。关系五类方剂含中药、中药有功效、中药有性味、中药归经、方剂治疾病。这样的设计很直白每个关系都能被一条Cypher查询直接命中不绕弯。实体标签关键属性示例数据主要关系Herbname, pinyin, alias黄芪、桂枝HAS_EFFECT / HAS_PROPERTY / GOES_TO_MERIDIANFormulaname, source, function桂枝汤、麻黄汤CONTAINS指向HerbEffectname补气、解表、发汗被Herb的HAS_EFFECT指向Propertyname辛、甘、温、寒被Herb的HAS_PROPERTY指向Meridianname肺经、脾经被Herb的GOES_TO_MERIDIAN指向Diseasename咳嗽、风寒感冒被Formula的TREATS指向你可能会问性味归经做成属性不行吗行但“哪些药是辛味的”这种查询会变成对属性做字符串匹配没法用图遍历。做成节点后一个问题就变成一次邻居查询语义也更贴近知识图谱的用法。属性保留name、pinyin、alias就够了不要一次性塞入几十个字段后面改数据会很痛苦。schema定的好导入脚本和问答模块都省力。2.2 用py2neo把CSV批量灌进Neo4j参数化MERGE与事务边界数据建模完成后下一步是把CSV灌进Neo4j。常见做法是写一个独立的Python脚本不挂在Django里因为导入是一次性操作不需要走Web请求。我用py2neo连图数据库用Cypher的MERGE做节点去重这样重复执行脚本不会产生重复节点。from py2neo import Graph import csv graph Graph(bolt://localhost:7687, auth(neo4j, 改成你的密码)) def import_herb_effects(csv_path): merge_query MERGE (h:Herb {name: $herb}) WITH h UNWIND $effects AS eff_name MERGE (e:Effect {name: eff_name}) MERGE (h)-[:HAS_EFFECT]-(e) with open(csv_path, encodingutf-8-sig) as f: reader csv.DictReader(f) for row in reader: effects [x.strip() for x in row[effects].split(|) if x.strip()] if not effects: continue graph.run(merge_query, herbrow[herb_name], effectseffects)这段代码的关键点有三处。第一csv_path用utf-8-sig打开能兼容Excel导出的带BOM头文件否则第一列列名会多个不可见字符。第二effects字段用竖线分隔因为有些功效名里带逗号CSV本身的逗号分隔会冲突。第三UNWIND $effects把Python列表展开成Cypher里的多行配合两个MERGE完成节点去重和关系去重。如果你有大量数据要导入比如超过一万条建议每500行做一次事务提交而不是逐条graph.run。逐条跑会把Neo4j的事务日志写得很大后期查询会明显变慢。常见做法是循环里记一个count每满500次重新开启事务上面代码为了可读性只展示了逐条MERGE生产环境跑数据时一定要加批处理。还有一种导入方式是Neo4j自带的LOAD CSV把CSV放进import目录后直接在Cypher里写LOAD CSV FROM file:///herb.csv AS row MERGE ...。优点是快缺点是从浏览器后台导入看到中文乱码时排查链路更长。我更推荐用py2neo读CSV因为报错信息会直接打到Python控制台方便在数据清洗阶段快速定位是哪一行出了问题。2.3 这版数据模型的边界在哪别一开始就想做全科图谱很多人在第一个版本就想把中药、方剂、经络、针灸、医案全部放进一张图最后往往死在数据清洗上。真实情况是网上能直接用的结构化中医药数据集很少大部分是pdf或扫描版教材整理出几百条干净的方剂和中药关系已经够毕业设计演示了。所以初版把范围锁在“常用100味药 50个方剂 对应的功效/性味/归经/主治疾病”这个量级跑通全链路后再考虑扩数据。这版模型的另一个边界是它不支持“同义实体”。比如“仙灵脾”和“淫羊藿”是同一种药如果CSV里出现两个名字图谱里会生成两个独立节点问答时查“淫羊藿”查不到“仙灵脾”的数据。解决方法是建一个alias属性导入时把别名放在alias字段里查询时用WHERE h.name $name OR $name IN h.alias。初版不必把别名做得很细但至少要给Herb和Formula留这个字段。3. Django后端封装图谱查询三个核心接口与py2neo连接Neo4j的写法3.1 创建Django工程与qa应用迁移命令和图谱无关拿到题目第一件事是建工程。这里不需要复杂架构一个qa应用就够了。执行下面两条命令django-admin startproject tcm_platform cd tcm_platform python manage.py startapp qa然后打开tcm_platform/settings.py在INSTALLED_APPS里加入qaINSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, qa, ]这里有个新手必踩的坑以为Neo4j是Django的数据库可以写进DATABASES然后靠python manage.py migrate建图。Neo4j是属性图数据库Django官方ORM只支持关系型数据库migrate命令根本管不到图数据库。正确做法是migrate只负责Django自己的用户表、会话表图谱数据全部通过py2neo脚本处理。你甚至可以把Neo4j的连接信息单独放到qa/neo4j_client.py里避免和Django配置混在一起。这一步还有一个细节settings.py里要配置ALLOWED_HOSTS本地调试时加上[127.0.0.1, localhost]否则前端页面访问API时容易报DisallowedHost。搜“django创建app”能看到很多教程只讲命令不讲settings配置实际被这个报错卡住的人不在少数。3.2 用py2neo连接Neo4jbolt协议、认证参数与查询函数封装连接Neo4j我一般单独写一个模块不在每个视图里重复Graph(),因为连接是有开销的重复实例化会白白浪费时间。下面是qa/neo4j_client.py的写法import os from py2neo import Graph graph Graph( bolt://localhost:7687, auth(os.getenv(NEO4J_USER, neo4j), os.getenv(NEO4J_PASSWORD, neo4j)), nameneo4j )先说连接串。bolt://localhost:7687是Neo4j的二进制协议端口另一个常见端口是7474那是HTTP端口浏览器访问Neo4j管理界面用的。py2neo可以用http://localhost:7474连接但速度比bolt慢而且遇到大查询时更容易超时所以我会直接用bolt。auth参数接收用户名和密码的元组不要写进连接串里否则日志里容易泄露密码。nameneo4j指向数据库名。Neo4j从4.0开始支持多数据库默认数据库叫neo4j。如果用的是Neo4j Desktop创建了其他数据库这里改成对应的名字就行。查询封装放在services.py里把Cypher语句集中管理# qa/services.py from .neo4j_client import graph def query_herb_effects(herb_name: str): query MATCH (h:Herb {name: $name})-[:HAS_EFFECT]-(e:Effect) RETURN e.name AS name ORDER BY e.name return graph.run(query, nameherb_name).data() def query_herb_property(herb_name: str): query MATCH (h:Herb {name: $name})-[:HAS_PROPERTY]-(p:Property) RETURN p.name AS name return graph.run(query, nameherb_name).data() def query_formula_herbs(formula_name: str): query MATCH (f:Formula {name: $name})-[:CONTAINS]-(h:Herb) RETURN h.name AS name return graph.run(query, nameformula_name).data()这里用$name参数化查询而不是把herb_name直接拼接进Cypher字符串。这么做的原因很实际中药名里可能带括号、引号、特殊符号直接拼串容易把Cypher语法搞崩还会留下注入风险。参数化之后py2neo会帮你处理转义查询也更快因为Neo4j能复用执行计划。graph.run(...).data()会把结果转换成Python字典列表比如返回[{name: 补气}, {name: 固表}]后面在视图中取字段就很方便。3.3 视图与路由统一JsonResponse返回结构和异常兜底视图层不要直接写Cypher只做参数校验和响应格式化。下面是qa/views.py的写法from django.http import JsonResponse from django.views.decorators.http import require_GET from . import services require_GET def herb_effect(request): herb request.GET.get(herb, ).strip() if not herb: return JsonResponse({code: 400, msg: herb 参数不能为空}, status400) try: rows services.query_herb_effects(herb) return JsonResponse({ code: 0, data: { herb: herb, effects: [r[name] for r in rows], } }) except Exception as exc: return JsonResponse({code: 500, msg: str(exc)}, status500) require_GET def formula_herbs(request): formula request.GET.get(formula, ).strip() if not formula: return JsonResponse({code: 400, msg: formula 参数不能为空}, status400) rows services.query_formula_herbs(formula) return JsonResponse({ code: 0, data: { formula: formula, herbs: [r[name] for r in rows], } })require_GET是Django自带的装饰器只允许GET请求避免POST调用误入。每个接口都先做空参数校验返回固定的code和msg结构这样前端拿到响应不需要猜字段。如果查询结果为空rows是个空列表接口正常返回code: 0和空数组不需要额外报错这符合“查不到数据不等于接口异常”的原则。路由配置很简单在tcm_platform/urls.py里加上两个pathfrom django.contrib import admin from django.urls import path from qa.views import herb_effect, formula_herbs urlpatterns [ path(admin/, admin.site.urls), path(api/herb/effect, herb_effect), path(api/formula/herbs, formula_herbs), ]这里有个痛点如果Neo4j没启动services.query_herb_effects会抛连接异常被try/except捕获后返回500。但500状态码会让前端认为服务崩溃更好的做法是区分“数据库不可用”和“数据为空”。我一般会在except分支里判断异常类型如果是py2neo.database.ClientError返回503而不是500。初版用统一的{code: 500}也能跑但口试时如果能说出两者的区别会加分不少。4. 智能问答模块用规则模板把问句映射成Cypher查询的三层结构4.1 问句分类用正则由中文问法里抽意图和实体智能问答是这套系统的门面但别一上来就想用大模型。对于实体数量有限的中医药领域规则引擎加词典匹配是最稳、最容易解释的方案。我先把用户问句按意图分成四类查功效、查组成、查治疗、查替代。每一类对应一个正则模板从问句里抽出目标实体名。import re INTENT_PATTERNS { effect: re.compile(r(?Pherb\S?)(?:的)?(?:功效|作用|主治)), ingredient: re.compile(r(?Pformula\S?)(?:的)?(?:组成|成分|包含哪些|含哪些)), treatment: re.compile(r(?:治疗|治|缓解)(?Pdisease\S?)(?:的)?(?:中药|药|方剂)), substitute: re.compile(r(?Pherb\S?)替代|代替), } def classify_question(question: str): for intent, pattern in INTENT_PATTERNS.items(): match pattern.search(question.strip()) if match: entity match.groupdict().get(herb) or match.groupdict().get(formula) or match.groupdict().get(disease) if entity: return intent, entity return fallback, None这个正则写法看起来简单实际跑的时候有几个坑要防。第一\S?是懒惰匹配遇到“黄芪有什么功效”会正确抽出“黄芪”但如果问句是“这个黄芪和党参有什么区别”会匹配到“这个黄芪”需要后续做实体词典截断。第二中文问句里“的”字可省可不省所以我在功效和组成两个模板里加了个可选分组(?:的)?。第三“替代”意图放在最后因为问句里往往同时含药名和“替代”二字先匹配它容易误伤其他意图。对比一下如果你用jieba分词能处理更复杂的问法但分词结果需要和实体表做匹配复杂度上去了一个量级。毕业设计用正则加词典足够答辩时把边界说清楚就行。4.2 匹配到了怎么答意图到Cypher的映射与自然语言拼接分类完成后每个意图都对应一个查询函数。这些函数可以复用第3章services里的方法再拼接成自然语言答案。下面是问答逻辑的核心部分def answer_effect(herb_name: str): rows services.query_herb_effects(herb_name) if not rows: return f没有查到「{herb_name}」的功效记录你是不是想问别的药 effects 、.join(r[name] for r in rows) return f「{herb_name}」的主要功效是{effects}。 def answer_substitute(herb_name: str): query MATCH (h:Herb {name: $name})-[:HAS_EFFECT]-(e:Effect)-[:HAS_EFFECT]-(alt:Herb) WHERE alt.name $name RETURN alt.name AS name, count(e) AS score ORDER BY score DESC LIMIT 5 rows graph.run(query, nameherb_name).data() if not rows: return f没有找到「{herb_name}」的替代药。 candidates 、.join(r[name] for r in rows) return f「{herb_name}」可以尝试用{candidates}。替代药这个查询是知识图谱的经典玩法找两个中药之间共享功效的数量数量越多说明功效越接近。count(e)统计的是共同功效关系数倒序取前5。这会给出一个可解释的结果——不是因为“数据库里有这条记录”而是因为“这两味药在功效维度上重叠度最高”。这里要注意一点替代药的查询结果里没有排除功效完全相反的药物。比如“黄连”和“黄芪”共享“清热解毒”不对黄芪没有这个功效。实际数据里你不加限制也没关系初版先看效果如果后续发现替代结果离谱再考虑加入性味互斥条件。answer_ingredient和answer_treatment的逻辑类似区别是查询语句不同。我习惯把自然语言拼接放在一个qa_router函数里统一路由def get_answer(question: str) - str: intent, entity classify_question(question) if intent effect: return answer_effect(entity) if intent ingredient: return answer_ingredient(entity) if intent treatment: return answer_treatment(entity) if intent substitute: return answer_substitute(entity) return 我暂时只能回答中药功效、方剂组成、疾病用药和中药替代这类问题。可以试着问我黄芪有什么功效4.3 答不上来的时候实体词典对齐与兜底话术规则问答的死穴是“用户换了个说法”。比如“给我推荐点治咳嗽的药”里有“治”、“咳嗽”treatment模板能匹配到但“咳嗽吃什么药好得快”就匹配不上。我的做法是准备一个实体词典先把问句里的已知实体名抽出来再反查它属于哪个标签。ENTITY_DICT { 黄芪: Herb, 桂枝: Herb, 桂枝汤: Formula, 风寒感冒: Disease, } def extract_known_entity(question: str): for name, label in ENTITY_DICT.items(): if name in question: return name, label return None, None这个词典不用太大覆盖问答模块里出现的实体即可。抽到实体后如果正则没抽出有效实体就根据词典结果走对应标签的默认查询。比如“咳嗽吃什么药好得快”抽到“咳嗽”对应Disease就走answer_treatment(咳嗽)。兜底话术也别太死板。用户问“这个方子能治啥”实际想查的是Formula的治疾病关系但正则不容易抽准。这时候我一般的处理是先返回一个示例引导比如“你可以这样问桂枝汤能治什么病”引导用户用系统已支持的句式。别让用户觉得这是个黑匣子让他知道边界在哪体验反而更好。5. 避坑与排查Neo4j连接、中文编码、数据膨胀这五个粘人问题5.1 py2neo版本变化导致连接API报错现象按旧教程写Graph(http://localhost:7474, passwordxxx)在较新版本里直接抛TypeError或ValueError。原因py2neo从4.0开始默认使用bolt协议用户名密码必须通过auth参数传递HTTP连接串的写法已经废弃。版本迭代把不少老代码留在坑里。解决统一用Graph(bolt://localhost:7687, auth(neo4j, 密码))。如果你用的是很新的py2neo个别方法还可能改名比如graph.find_one被移除换成graph.run(MATCH ... RETURN ... LIMIT 1).first()。我的习惯是先在Python控制台跑一句graph.run(RETURN 1).data()验证连接再写业务代码。5.2 中文查询时Cypher语法错误现象用f-string拼接中药名遇到“甘草炙”“三七粉”这类带括号或特殊符号的名称查询直接报语法错误浏览器页面显示500。原因括号和引号在Cypher里有特殊含义直接拼字符串等于把用户输入送进了解析器一条带特殊符号的数据就能炸掉整个查询。解决所有查询一律使用参数化graph.run(query, nameherb_name)不要用graph.run(fMATCH (h:Herb {{name: {herb_name}}})...)。参数化之后特殊符号都会被安全处理这也是我前面services层反复强调参数化的原因。这不算进阶技巧而是必备习惯。5.3 图谱数据量稍大后查询明显变慢现象导入几千条数据后首页问答接口响应从几十毫秒涨到几秒有的查询直接超时。原因Neo4j默认堆内存设置偏保守而且初版导入时没有给高频查询字段建索引每次都是全库扫描标签节点。解决先在neo4j.conf里调大堆内存常见配置如下dbms.memory.heap.initial_size512m dbms.memory.heap.max_size1g dbms.memory.pagecache.size512m然后在Neo4j浏览器里执行建索引语句CREATE INDEX herb_name IF NOT EXISTS FOR (h:Herb) ON (h.name); CREATE INDEX formula_name IF NOT EXISTS FOR (f:Formula) ON (f.name); CREATE INDEX disease_name IF NOT EXISTS FOR (d:Disease) ON (d.name);加了索引后MATCH (h:Herb {name: $name})这种等值查询会走索引而不是全扫。另外所有查询都加上LIMIT 100能防止意外的大结果集拖垮带宽。搜“neo4j安装与配置”时能看到大量内存调优文章但真正影响毕业设计演示的就是heap size和索引这两项。5.4 中文数据导入后前端显示乱码或首列名丢失现象用Excel另存的CSV直接导入Neo4j里看到的中文全部变成乱码或者第一列列名变成了锘縗ame之类的怪字符。原因Excel在中文系统里默认用GBK编码保存CSVPython的open()如果不指定编码读取中文环境下的文件很可能按GBK处理而utf-8-sig这个编码会额外吃掉BOM头能同时解决编码错乱和首列字段前带不可见字符两个问题。解决导入脚本里统一写open(csv_path, encodingutf-8-sig)。如果是自己用脚本生成的数据写入时也用utf-8-sig。这里要养成一个习惯CSV文件不是给人看的是给程序读的编码越显式越好。另外数据清洗阶段先把中文空格和全角字符替换掉否则会在图谱里生成一堆“黄芪”和“黄芪 ”这种看起来一样、实际不同的节点。5.5 Django的migrate命令跑完页面依然没有图谱数据现象跟着教程执行了python manage.py migrate启动服务后调接口返回空数据以为迁移没生效。原因Django的migrate只处理INSTALLED_APPS里注册的应用模型和Neo4j没有任何关系。图谱数据必须靠前面第2章的导入脚本灌入migrate不会帮你创建任何图节点和关系。解决把数据导入脚本单独放一个文件比如scripts/import_data.py不要写进migrations目录。运行顺序是先启动Neo4j再跑python scripts/import_data.py最后启动Django。如果改了实体模型重新导入时先执行MATCH (n) DETACH DELETE n清空图谱再灌新数据避免新旧数据混在一起出现重复节点。清空操作不可逆执行前一定确认数据已备份。6. 从演示到答辩用一组自测用例证明你的问答平台不是黑匣子做完整套系统后最后一步是验证。最怕的是写完了导入脚本也能启动页面但现场演示时问一句“黄芪有什么功效”返回空结果。我现在的习惯是写一个自测脚本把每种意图的典型问法放进一个列表逐个跑一遍检查答案里是否包含预期实体。case_list [ (黄芪有什么功效, [补气, 固表]), (桂枝汤包含哪些药, [桂枝, 甘草]), (治疗风寒感冒的中药, [桂枝, 麻黄]), (生姜可以用什么替代, [桂枝]), ] def run_tests(): for question, expected in case_list: answer get_answer(question) hit all(any(keyword in answer) for keyword in expected) status PASS if hit else FAIL print(f{status}: {question} - {answer})这个脚本的价值不只是给自己看更是答辩时的辅助材料。当评委问“你怎么证明你的系统可用”你可以直接展示这个自测列表说明每条case都覆盖了对应意图比口头解释“我测过没问题”有力得多。这里我把“替代”意图的预期结果写成一个不保证命中的示例实际数据不同结果会不一样初版更稳妥的做法是只保留前三个case替代药单独人工检查。还有一个答辩加分的小技巧在Django的DEBUGTrue模式下把一个真实问答请求的SQL和Cypher打印出来展示给评委看。比如问“桂枝汤包含哪些药”你可以在控制台看到Django打印的GET请求参数以及py2neo发出的Cypher查询和返回结果。这能说明你是真的在用图数据库做查询而不是在列表里写死数据。为了让这个展示更自然你可以在services.py里加一个print(graph.run(query, nameformula_name).data())的调试输出答辩完再注释掉。最后说一个我自己第一次做这类项目的教训我把大部分时间花在了前端页面的样式上觉得图表好看就赢了结果答辩被评委追问“图谱里一共有多少实体、关系覆盖哪些类型、问答准确率怎么算”时答不上来。现在我做知识图谱相关的东西第一件事是把自测和统计做扎实页面只是展示层底层的实体数量、关系路径、查询响应时间这些可量化的指标才是项目的底气。有条件的话再加一个简单的统计接口返回“当前图谱包含多少个Herb、多少个Formula、多少条关系”答辩时一张表就能说清整个项目规模。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑