☰
中医药知识图谱问答系统:Django与Neo4j实战指南
2026/10/1 2:34:51 网站建设 项目流程

简介:这份资源是面向高校计算机相关专业学生的毕业设计完整项目包,主题为基于Django与Neo4j的中医药知识图谱与智能问答平台。项目难度适中,适合用作毕业设计、期末大作业或课程设计,评审得分达到98分,内容经助教老师审定,源码已在本地编译验证可运行,下载后可直接部署调试。压缩包共约2000个文件,整体39.04MB,以1059个Python源码文件为核心,辅以739个编译缓存文件、66个JavaScript与34个CSS前端资源,另有HTML模板、JSON配置、SQLite数据库及说明文档等,覆盖后端逻辑、图谱构建、问答交互与前端展示各环节。目前已有138人学习关注。读者可获得一套结构完整、可运行的中医药知识图谱问答系统源码,并配套文档说明,便于理解Django与Neo4j的整合方式、知识图谱数据组织与智能问答实现思路,为毕业设计选题、答辩准备与二次开发提供可靠参考。

1. 中医药知识图谱问答系统:从 Django 到 Neo4j 的完整落地路径

中医药领域的知识有个特点:实体类型多、关系复杂、术语还带大量别名。光靠 MySQL 存几张表,做多跳推理查询时 SQL 会写得让人怀疑人生。这个毕业设计资源包的核心思路是用 Neo4j 存图结构、Django 做 Web 层、Python 写数据清洗和问答逻辑,把「中药材—方剂—症状—功效」这些实体和关系组织成一张可查询的知识网络。它适合正在做知识图谱方向毕设的同学,也适合想快速搭一个图谱问答 Demo 的后端开发者。资源包里包含源码和文档说明,拿到手能直接跑起来看效果,再按自己的数据替换。下面我从环境搭建、数据建模、问答实现到避坑,把这条链路拆开讲清楚。

2. 环境搭建与项目结构:Django 和 Neo4j 怎么接上

2.1 为什么选 Django + Neo4j 而不是纯 MySQL

中医药知识图谱的查询模式决定了存储选型。如果只是查「当归有哪些功效」,一张关联表就够了。但实际问答场景里经常出现「含有当归且用于血虚证的方剂有哪些」这类需要两跳甚至三跳的查询。用 SQL 写就是多次 JOIN,表一多性能断崖式下跌,而且每加一种关系就要改表结构。

Neo4j 的图模型天然适合这种场景:节点表示实体,边表示关系,查询用 Cypher 语句描述路径模式,加新关系类型不需要改 schema。Django 在这里的角色是 Web 框架,负责用户请求路由、模板渲染和调用 Python 层的图谱查询逻辑。两者通过 Python 的 neo4j 官方驱动连接,不经过 Django ORM。

常见做法是 Django 只管业务逻辑和页面,Neo4j 只管图数据,中间用一个graph_service.py做封装。这样职责清晰,调试时也容易定位问题出在哪一层。

2.2 Neo4j 安装与 Django 项目初始化

Neo4j 社区版就够用,毕设场景不需要企业版。安装方式根据系统不同有差异,Windows 下下载安装包解压后运行bin\neo4j.bat console,macOS 用 Homebrew 装更方便,Linux 下解压后配置conf/neo4j.conf再启动。

# macOS 下通过 Homebrew 安装 Neo4j 社区版 brew install neo4j # 启动服务 neo4j start # 首次启动后需要设置初始密码,默认用户名 neo4j # 浏览器打开 http://localhost:7474 进入 Neo4j Browser 修改密码

启动后在 Neo4j Browser 里执行一条测试语句确认连通:

// 创建一个测试节点并查询 CREATE (n:TestNode {name: '测试'}) RETURN n

Django 侧的项目初始化:

# 创建虚拟环境 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装依赖 pip install django neo4j jieba # 创建 Django 项目 django-admin startproject tcm_kg cd tcm_kg # 创建应用 python manage.py startapp qa

依赖里neo4j是官方 Python 驱动,jieba用于中文分词,后面问答模块会用到。Django 版本建议 3.2 以上,Python 3.8 以上,太老的版本在异步和模板语法上会有兼容问题。

2.3 Django 连接 Neo4j 的配置方式

Django 的settings.py里加 Neo4j 连接参数:

# settings.py NEO4J_CONFIG = { 'uri': 'bolt://localhost:7687', 'user': 'neo4j', 'password': '你的密码', }

然后在qa应用下建一个graph_service.py:

# qa/graph_service.py from neo4j import GraphDatabase from django.conf import settings class GraphService: def __init__(self): config = settings.NEO4J_CONFIG self.driver = GraphDatabase.driver( config['uri'], auth=(config['user'], config['password']) ) def close(self): self.driver.close() def run_query(self, cypher, params=None): """执行 Cypher 查询并返回结果列表""" with self.driver.session() as session: result = session.run(cypher, params or {}) return [record.data() for record in result] # 单例,避免每次请求都创建连接 graph_service = GraphService()

这里用单例模式持有 driver,因为 Neo4j 的 driver 本身是线程安全的连接池,不需要每次请求都新建。run_query统一封装了 session 的创建和结果转换,返回的是字典列表,方便 Django 视图层直接序列化成 JSON。

注意:Neo4j 的 bolt 协议默认端口是 7687,HTTP 端口是 7474。Django 连接用的是 bolt,不是浏览器那个地址。

3. 中医药知识图谱的数据建模与导入

3.1 实体类型与关系设计

中医药知识图谱的核心实体类型包括:中药材、方剂、症状、功效、证型、脏腑。关系类型包括:方剂-包含->中药材、中药材-具有功效->功效、方剂-主治->症状、症状-属于->证型等。

这个资源包里的数据模型大致是这样的结构:

实体类型标签名关键属性
中药材Herbname, alias, nature, flavor
方剂Formulaname, source, usage
症状Symptomname, description
功效Effectname
证型Syndromename

关系设计上,Formula-[:CONTAINS]->Herb是最核心的一条边,问答系统里大量查询都围绕它展开。Herb-[:HAS_EFFECT]->Effect和Formula-[:TREATS]->Symptom是另外两条高频路径。

建模时有个容易忽略的点:中药材的别名。比如「当归」又叫「干归」,「人参」又叫「棒槌」。如果不在图谱里处理别名,用户搜「干归」就查不到任何结果。常见做法是给 Herb 节点加一个alias数组属性,查询时用WHERE '干归' IN h.alias OR h.name = '干归'来匹配。

3.2 用 Python 脚本批量导入 CSV 数据

假设你手头有整理好的 CSV 文件,格式如下:

# herbs.csv name,alias,nature,flavor 当归,"干归,秦归",温,"甘,辛" 人参,"棒槌,神草",平,"甘,微苦" # formulas.csv name,source,usage 四物汤,太平惠民和剂局方,水煎服 补中益气汤,脾胃论,水煎服 # formula_herb.csv formula_name,herb_name 四物汤,当归 四物汤,川芎 补中益气汤,人参

导入脚本:

# scripts/import_data.py import csv from neo4j import GraphDatabase driver = GraphDatabase.driver('bolt://localhost:7687', auth=('neo4j', '你的密码')) def import_herbs(tx, csv_path): with open(csv_path, 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: tx.run( """MERGE (h:Herb {name: $name}) SET h.alias = split($alias, ','), h.nature = $nature, h.flavor = $flavor""", name=row['name'], alias=row['alias'], nature=row['nature'], flavor=row['flavor'] ) def import_formula_relations(tx, csv_path): with open(csv_path, 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: tx.run( """MATCH (f:Formula {name: $formula}) MATCH (h:Herb {name: $herb}) MERGE (f)-[:CONTAINS]->(h)""", formula=row['formula_name'], herb=row['herb_name'] ) with driver.session() as session: session.execute_write(import_herbs, 'data/herbs.csv') session.execute_write(import_formula_relations, 'data/formula_herb.csv') driver.close() print("导入完成")

脚本用MERGE而不是CREATE,这样重复执行不会产生重复节点。execute_write是 neo4j 驱动 5.x 的写法,自动处理事务提交和重试。如果你的驱动版本是 4.x,需要改成session.write_transaction。

导入完成后在 Neo4j Browser 里验证:

// 查看所有实体数量 MATCH (h:Herb) RETURN count(h) AS herb_count MATCH (f:Formula) RETURN count(f) AS formula_count // 查看一条完整路径 MATCH (f:Formula)-[:CONTAINS]->(h:Herb) RETURN f.name, h.name LIMIT 10

3.3 数据清洗中的几个实际问题

原始中医药数据往往来自不同来源,格式不统一。常见问题包括:同一味药在不同文件里写法不同(「炙甘草」和「甘草(炙)」)、方剂名称带括号注释、CSV 编码不是 UTF-8。

处理方式是在导入前加一层清洗脚本,用正则统一格式。比如去掉括号注释、把全角逗号转半角、统一「炙」字的位置。这一步看起来琐碎,但不做的话后面查询会大量漏结果。

另一个实际问题是 Neo4j 社区版的内存配置。默认配置下导入几千条数据没问题,但中医药图谱如果做到几万节点,需要改conf/neo4j.conf里的dbms.memory.heap.max_size和dbms.memory.pagecache.size。一般 4GB 堆内存加 2GB 页缓存能撑住十万级节点。

4. 智能问答模块:从自然语言到 Cypher 查询

4.1 问句解析的基本流程

问答模块要做的事情是:用户输入「当归有什么功效」,系统返回「补血活血、调经止痛」。中间需要把自然语言转成 Cypher 查询。

流程分三步:分词和实体识别、意图判断、模板匹配生成 Cypher。这个资源包里用的是基于规则的方式,没有上深度学习模型,好处是可控、可解释、不需要训练数据。

分词用 jieba,实体识别靠预先构建的词典匹配。意图判断用关键词规则:出现「功效」「作用」就归为查功效,出现「方剂」「汤」就归为查方剂,出现「症状」「治」就归为查主治。

4.2 基于规则的问句到 Cypher 转换

# qa/qa_engine.py import jieba from qa.graph_service import graph_service # 加载自定义词典,确保中医药术语不被切碎 jieba.load_userdict('data/tcm_dict.txt') # 意图关键词映射 INTENT_RULES = { 'effect': ['功效', '作用', '有什么效果'], 'formula': ['方剂', '方', '汤', '包含哪些药'], 'symptom': ['治', '主治', '症状', '用于'], } def detect_intent(question): for intent, keywords in INTENT_RULES.items(): if any(kw in question for kw in keywords): return intent return None def extract_entity(question): """从问句中提取中医药实体名称""" words = jieba.lcut(question) # 从图谱中查询匹配的实体 for word in words: if len(word) < 2: continue result = graph_service.run_query( "MATCH (h:Herb) WHERE h.name = $name OR $name IN h.alias RETURN h.name AS name", {'name': word} ) if result: return result[0]['name'] return None def answer(question): intent = detect_intent(question) entity = extract_entity(question) if not entity: return "未识别到相关的中医药实体,请换个说法试试。" if not intent: return "暂时无法理解这个问题,可以试试问「XX的功效」或「XX方剂包含哪些药」。" if intent == 'effect': cypher = """MATCH (h:Herb {name: $name})-[:HAS_EFFECT]->(e:Effect) RETURN e.name AS effect""" elif intent == 'formula': cypher = """MATCH (f:Formula)-[:CONTAINS]->(h:Herb {name: $name}) RETURN f.name AS formula""" elif intent == 'symptom': cypher = """MATCH (f:Formula)-[:TREATS]->(s:Symptom) WHERE EXISTS { (f)-[:CONTAINS]->(:Herb {name: $name}) } RETURN DISTINCT s.name AS symptom""" else: return "暂不支持该类型的问题。" results = graph_service.run_query(cypher, {'name': entity}) if not results: return f"图谱中暂时没有关于「{entity}」的相关记录。" values = [list(r.values())[0] for r in results] return f"{entity}的{'功效' if intent == 'effect' else '相关方剂' if intent == 'formula' else '主治症状'}:{'、'.join(values)}"

这段代码的核心逻辑是:先判断用户问的是什么类型的问题,再提取问题中的实体名称,最后根据意图选择对应的 Cypher 模板。extract_entity里遍历分词结果去图谱里匹配,匹配到就返回标准名称,这样即使用户输入的是别名也能正确查询。

symptom意图的 Cypher 用了一个子查询EXISTS { ... },先找到包含目标药材的方剂,再查这些方剂主治的症状。这是两跳查询,用 SQL 写需要嵌套子查询,Cypher 里表达更直观。

4.3 Django 视图与前端交互

视图层把问答引擎包成一个 POST 接口:

# qa/views.py import json from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from qa.qa_engine import answer @csrf_exempt def ask(request): if request.method != 'POST': return JsonResponse({'error': '仅支持 POST'}, status=405) data = json.loads(request.body) question = data.get('question', '').strip() if not question: return JsonResponse({'error': '问题不能为空'}, status=400) reply = answer(question) return JsonResponse({'question': question, 'answer': reply})

前端页面用一个简单的表单加 fetch 请求就能跑通。资源包里应该带了模板文件,放到templates/目录下,在settings.py里配置好TEMPLATES的DIRS路径即可。

提示:Django 的 CSRF 中间件对 POST 请求有校验,开发阶段可以用@csrf_exempt跳过,上线前记得换成正常的 CSRF token 方式。

5. 避坑与常见问题排查

5.1 Neo4j 连接报错「Unable to connect」

现象:Django 启动后调用图谱查询接口,报neo4j.exceptions.ServiceUnavailable。

原因:Neo4j 服务没启动,或者 bolt 端口被防火墙拦了。另一个常见原因是 Neo4j 4.x 之后默认只监听 localhost,如果 Django 跑在容器里就连不上。

解决:先确认neo4j status显示 running,再用telnet localhost 7687测试端口通不通。容器场景下改neo4j.conf里的dbms.default_listen_address=0.0.0.0,然后重启服务。

5.2 Cypher 查询返回空结果但数据明明存在

现象:在 Neo4j Browser 里能查到的数据,Django 里查出来是空列表。

原因:最常见的是参数传递时类型不匹配。比如节点属性存的是字符串,传参传了整数。另一个原因是中文编码问题,CSV 导入时用了 GBK 编码,存进去的数据带乱码,查询时用 UTF-8 匹配不上。

解决:在 Neo4j Browser 里用完全相同的 Cypher 和参数跑一遍,确认数据本身没问题。然后检查 Python 侧传参的类型,用type()打印确认。编码问题在导入脚本里统一指定encoding='utf-8'。

5.3 jieba 分词把中医药术语切碎

现象:用户输入「四物汤」,jieba 切成「四物」「汤」,导致实体识别失败。

原因:jieba 默认词典不含中医药专业术语,按通用语料切分。

解决:建一个自定义词典文件,每行一个术语,格式为「词语 词频 词性」,词频给高一点确保不被切开。然后在代码里用jieba.load_userdict()加载。资源包里如果有tcm_dict.txt就直接用,没有的话从图谱的实体名称导出生成一份。

5.4 Django 模板里静态文件加载失败

现象:页面样式丢失,控制台报 404。

原因:settings.py里STATIC_URL配了但STATICFILES_DIRS没配,或者模板里{% load static %}没写。

解决:确认INSTALLED_APPS里有django.contrib.staticfiles,STATICFILES_DIRS指向项目的 static 目录,模板开头加{% load static %},引用时用{% static 'css/style.css' %}这种写法。

5.5 Neo4j 导入大量数据时内存溢出

现象:导入脚本跑到一半报OutOfMemoryError,Neo4j 服务崩溃。

原因:默认堆内存太小,大批量写入时事务日志和索引占满内存。

解决:改conf/neo4j.conf里的dbms.memory.heap.max_size=4G和dbms.memory.pagecache.size=2G。导入脚本里每 1000 条提交一次事务,不要把所有数据放在一个事务里。用session.execute_write分批调用。

6. 进阶技巧:用 APOC 插件做路径查询和问答增强

基础版的问答只能处理一跳查询,比如「当归的功效」。但用户经常会问「当归和川芎能一起治什么」,这需要先找到两味药的共同方剂,再查方剂主治的症状,是三跳查询。手写 Cypher 不是不行,但路径模式一复杂就容易写错。APOC 插件提供了路径查询和图算法的封装,能省不少事。

安装 APOC 就是把对应的 jar 包放到 Neo4j 的plugins目录下,然后在neo4j.conf里加一行dbms.security.procedures.unrestricted=apoc.*,重启服务。版本要和 Neo4j 版本对应,4.4 的 Neo4j 就下 4.4 的 APOC。

装好之后,查两味药的共同方剂和主治可以这样写:

// 查找两味药材的共同方剂及其主治症状 MATCH (h1:Herb {name: '当归'})-[:CONTAINS]-(f:Formula)-[:CONTAINS]-(h2:Herb {name: '川芎'}) MATCH (f)-[:TREATS]->(s:Symptom) RETURN f.name AS 方剂, collect(DISTINCT s.name) AS 主治症状

这个查询的模式是:当归和川芎同时出现在某个方剂中,然后查这个方剂主治什么。collect(DISTINCT s.name)把同一方剂的多个症状聚合成列表返回。

APOC 的apoc.path.expandConfig更适合做不确定跳数的路径查询:

// 从当归出发,查找 2 到 3 跳内的所有关联实体 MATCH (start:Herb {name: '当归'}) CALL apoc.path.expandConfig(start, { minLevel: 2, maxLevel: 3, relationshipFilter: 'CONTAINS|HAS_EFFECT|TREATS' }) YIELD path RETURN path LIMIT 20

relationshipFilter用竖线分隔多种关系类型,minLevel和maxLevel控制跳数范围。这个在探索性查询里很好用,比如用户问「当归相关的所有信息」,你可以用这个把两跳内的实体都拉出来展示。

问答引擎里集成 APOC 的方式是加一个「关联查询」意图。当用户问「XX和XX有什么关系」时,走 APOC 路径查询,把返回的路径转成自然语言描述。路径转文本的逻辑大概是:遍历路径上的节点和关系,按「A 通过 关系 连接到 B」的模板拼接。

还有一个实用技巧是用 Neo4j 的全文索引做模糊匹配。中医药名称经常有错别字或者简写,精确匹配容易漏。建索引的语句:

CREATE FULLTEXT INDEX herbNameIndex FOR (h:Herb) ON EACH [h.name, h.alias]

查询时用CALL db.index.fulltext.queryNodes('herbNameIndex', '当归')就能做模糊匹配,对用户输入容错更好。

我自己的习惯是每次改完 Cypher 查询,先在 Neo4j Browser 里用EXPLAIN看执行计划,确认没有全表扫描再放到代码里。中医药图谱节点不多的时候感觉不出来,数据量上去之后一个没走索引的查询能把响应时间从毫秒级拉到秒级。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询