基于Django与Neo4j的中医药知识图谱问答平台构建实战 简介这份资源是面向高校计算机相关专业学生的毕业设计与课程设计参考项目主题为基于Django框架与Neo4j图数据库的中医药知识图谱与智能问答平台。项目难度适中源码经过本地编译验证可运行并附有文档说明适合用于毕业设计、期末大作业或课程设计等场景帮助读者理解知识图谱构建、图数据库查询与智能问答交互的完整实现思路。资源包共约2000个文件以Python源码1059个为核心辅以编译缓存、JavaScript与CSS前端资源、HTML模板、配置文件及依赖安装包等整体约39.04MB目录结构完整便于按模块阅读与二次开发。目前已有138人学习关注。通过研读源码与配套文档读者可掌握Django与Neo4j的集成方式、中医药实体关系建模、问答逻辑处理等关键环节并参考其工程组织方式完成自己的项目搭建与功能扩展。1. 中医药知识图谱问答平台从数据到答案的完整链路中医药领域的知识有个特点概念之间关系密集、术语别名多、古籍与现代教材表述不一致。比如「消渴」和「糖尿病」在中医语境下高度相关但直接做字符串匹配根本对不上。这就是为什么用 Django Neo4j 做中医药知识图谱与智能问答平台比用传统关系型数据库更合适——图数据库天然擅长处理「实体-关系-实体」这种多跳查询而中医药知识恰恰是一张巨大的关系网。这个方案适合谁如果你是计算机相关专业的毕业生正在找一个既有技术深度又能体现业务价值的毕设题目或者你是刚接触知识图谱的开发者想用一个完整项目把 Neo4j、Django、NLP 串起来——这套组合是经过验证的。它解决的核心问题是把散落在教材、药典、方剂文献里的中医药知识结构化存进图数据库再通过一个问答接口让用户用自然语言提问系统自动解析意图、查询图谱、返回答案。整条链路分四层数据采集与清洗、知识图谱构建本体设计 实体关系抽取 Neo4j 入库、Django 后端服务API 用户管理 查询路由、前端问答交互。下面按落地顺序拆开讲。2. 知识图谱构建本体设计、数据抽取与 Neo4j 入库2.1 中医药本体怎么设计才不翻车本体设计是知识图谱的地基。中医药领域的核心实体类型一般包括中药材、方剂、症状、疾病、证候、功效、归经。关系类型包括方剂「包含」药材、药材「具有」功效、药材「归」某经、方剂「主治」疾病、疾病「表现为」症状、证候「对应」方剂。我一般会先用一张表把实体和关系定清楚再动手写代码。表结构大概长这样实体类型属性字段示例中药材名称、别名、性味、归经、功效黄芪、绵黄芪、甘温、归肺脾经方剂名称、出处、组成、主治补中益气汤、《脾胃论》、黄芪人参白术…疾病名称、别名、症状列表消渴、糖尿病、多饮多尿症状名称、所属系统口渴、内分泌系统本体不要一开始就追求大而全。血泪经验是先覆盖 35 个核心实体类型和 58 种关系跑通全链路再逐步扩展。很多同学一上来设计几十种关系结果数据标注跟不上图谱稀疏得查不出东西。2.2 从半结构化文本抽取实体关系中医药数据来源通常是教材、药典 PDF、网页百科。完全靠人工标注不现实常见做法是「规则 词典 少量模型」的组合。先建一个药材词典和方剂词典用词典匹配做实体识别再用正则和依存句法做关系抽取。import re from collections import defaultdict # 构建药材别名词典 HERB_DICT { 黄芪: [黄芪, 绵黄芪, 北芪], 人参: [人参, 红参, 生晒参], 白术: [白术, 于术, 冬术], } # 反向索引别名 - 标准名 alias_to_standard {} for standard, aliases in HERB_DICT.items(): for alias in aliases: alias_to_standard[alias] standard def extract_herbs(text): 从文本中抽取药材实体返回标准名列表 found set() for alias, standard in alias_to_standard.items(): if alias in text: found.add(standard) return list(found) def extract_formula_relation(text): 抽取方剂-药材的包含关系基于组成关键词定位 results [] # 匹配 XX汤由A、B、C组成 或 XX汤A、B、C pattern r([\u4e00-\u9fa5]汤)[由:]\s*([\u4e00-\u9fa5、,]) for match in re.finditer(pattern, text): formula match.group(1) herb_text match.group(2) herbs extract_herbs(herb_text) if herbs: results.append({formula: formula, herbs: herbs}) return results # 测试 sample 补中益气汤由黄芪、人参、白术、当归组成主治脾胃气虚。 print(extract_formula_relation(sample)) # 输出: [{formula: 补中益气汤, herbs: [黄芪, 人参, 白术]}]这段代码的逻辑是先用别名词典做实体归一化再用正则定位「方剂-组成」的句式模板。参数方面HERB_DICT需要根据你的数据源持续扩充建议至少覆盖 300500 味常用药材。正则里的[\u4e00-\u9fa5]汤只匹配以「汤」结尾的方剂名实际项目中还要加上「丸」「散」「饮」等后缀。抽取完成后数据以三元组形式暂存格式统一为(头实体, 关系, 尾实体, 属性字典)方便后续批量入库。2.3 Neo4j 建库与批量导入的实操命令Neo4j 社区版足够支撑毕设规模的数据量几万节点、几十万关系。安装配置这里不展开重点讲导入。有两种方式一是用LOAD CSV适合大批量二是用 Python 驱动逐条写入适合需要做数据清洗的场景。我一般先用 Python 驱动做一轮清洗和去重再导出 CSV 用LOAD CSV批量导入速度差很多。from neo4j import GraphDatabase driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, your_password)) def create_herb_node(tx, name, properties): tx.run( MERGE (h:Herb {name: $name}) SET h.nature $nature, h.flavor $flavor, h.meridian $meridian, namename, natureproperties.get(性味, ), flavorproperties.get(味, ), meridianproperties.get(归经, ) ) def create_formula_contains_herb(tx, formula_name, herb_name, dosage): tx.run( MERGE (f:Formula {name: $fname}) MERGE (h:Herb {name: $hname}) MERGE (f)-[r:CONTAINS {dosage: $dosage}]-(h), fnameformula_name, hnameherb_name, dosagedosage ) with driver.session() as session: session.execute_write(create_herb_node, 黄芪, {性味: 甘温, 归经: 肺脾经}) session.execute_write(create_formula_contains_herb, 补中益气汤, 黄芪, 15g)关键参数说明MERGE而不是CREATE保证重复执行不会产生重复节点。execute_write会自动处理事务重试。如果数据量超过 1 万条逐条写入会很慢建议攒批后用UNWIND批量执行UNWIND $batch AS row MERGE (h:Herb {name: row.name}) SET h.nature row.nature, h.meridian row.meridian在 Python 侧把数据攒成 5001000 条一批调用一次session.run()传入batch参数导入速度能提升一个数量级。注意Neo4j 社区版默认内存配置偏保守导入大量数据前记得在neo4j.conf里调大dbms.memory.heap.max_size和dbms.memory.pagecache.size否则导入到一半可能卡死。3. Django 后端API 设计、查询路由与用户管理3.1 用 Django REST Framework 暴露图谱查询接口Django 在这里的角色是「中间层」接收前端提问调用 NLP 模块解析意图转成 Cypher 查询从 Neo4j 拿结果再格式化返回。不建议让前端直接连 Neo4j一是安全二是业务逻辑不好复用。先建一个 Django apppython manage.py startapp kg_api然后在settings.py里注册并配置 Neo4j 连接参数# settings.py NEO4J_URI bolt://localhost:7687 NEO4J_USER neo4j NEO4J_PASSWORD your_password INSTALLED_APPS [ # ... rest_framework, kg_api, ]写一个 Neo4j 连接工具类避免每次请求都重建驱动# kg_api/neo4j_client.py from neo4j import GraphDatabase from django.conf import settings class Neo4jClient: _driver None classmethod def get_driver(cls): if cls._driver is None: cls._driver GraphDatabase.driver( settings.NEO4J_URI, auth(settings.NEO4J_USER, settings.NEO4J_PASSWORD) ) return cls._driver classmethod def query(cls, cypher, paramsNone): with cls.get_driver().session() as session: result session.run(cypher, params or {}) return [record.data() for record in result]这个单例模式很关键。如果每个请求都GraphDatabase.driver()连接池会爆表现为请求越来越慢直到超时。参数方面max_connection_lifetime默认 3600 秒毕设场景不用改。3.2 问答意图解析与 Cypher 查询路由智能问答的核心不是「智能」而是「路由」。用户问「黄芪有什么功效」系统需要识别出实体黄芪意图查功效。然后映射到 Cypher# kg_api/qa_engine.py import re from .neo4j_client import Neo4jClient # 意图模板正则 - Cypher 模板 INTENT_PATTERNS [ { pattern: r(.?)(?:有什么|有哪些|的)?功效, cypher: MATCH (h:Herb {name: $entity})-[:HAS_EFFICACY]-(e) RETURN e.name AS answer, type: herb_efficacy }, { pattern: r(.?)(?:由|包含|组成), cypher: MATCH (f:Formula {name: $entity})-[:CONTAINS]-(h) RETURN h.name AS answer, type: formula_composition }, { pattern: r(.?)(?:主治|治疗|用于), cypher: MATCH (f:Formula {name: $entity})-[:TREATS]-(d) RETURN d.name AS answer, type: formula_treats }, ] def parse_and_query(question): for intent in INTENT_PATTERNS: match re.search(intent[pattern], question) if match: entity match.group(1).strip() results Neo4jClient.query(intent[cypher], {entity: entity}) if results: answers [r[answer] for r in results] return {entity: entity, type: intent[type], answers: answers} else: return {entity: entity, type: intent[type], answers: [], msg: 图谱中未找到相关记录} return {msg: 暂不支持该问题类型}这段代码的逻辑是按优先级遍历意图模板正则匹配成功后提取实体名代入预定义的 Cypher 模板查询。参数方面$entity是参数化查询不要用字符串拼接否则会有 Cypher 注入风险。实际项目中意图模板需要覆盖 1020 种常见问法并且要处理「别名」问题——用户可能问「北芪」而不是「黄芪」需要在查询前做一次别名归一化。3.3 用户模块与查询历史记录毕设项目通常需要用户注册登录和查询历史。Django 自带的 auth 够用查询历史建一张表# kg_api/models.py from django.db import models from django.contrib.auth.models import User class QueryHistory(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) question models.TextField() answer models.TextField() intent_type models.CharField(max_length50, blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-created_at]每次问答接口调用后异步写一条记录。注意on_deletemodels.CASCADE用户注销时历史一并清除。如果要做「热门问题」统计可以加一个hit_count字段用F()表达式原子递增。4. 避坑与排查那些让项目卡住的真实问题4.1 Neo4j 导入中文数据后查询为空现象CSV 导入后Neo4j Browser 里MATCH (h:Herb) RETURN h.name LIMIT 10能看到数据但 Django 接口查同一个名字返回空。原因编码不一致。CSV 文件可能是 GBK 编码Neo4j 按 UTF-8 读取中文变成乱码存进去了。或者 Django 请求里的实体名带了首尾空格。解决导入前统一转 UTF-8Python 侧对实体名做.strip()处理。排查时在 Cypher 里用RETURN h.name, size(h.name)看字符长度是否异常。4.2 Django 连接 Neo4j 报「Too many open files」现象本地测试正常压测或连续请求几十次后报连接错误。原因每次请求都创建了新 driver没有复用连接数耗尽。解决用单例模式管理 driver如 3.1 节的Neo4jClient并在 DjangoAppConfig.ready()里初始化。如果用了多线程确保 driver 是线程安全的——Neo4j Python driver 本身线程安全但 session 不是每个请求要新建 session。4.3 意图识别正则匹配到错误实体现象问「补中益气汤由什么组成」正则(.?)(?:由|包含|组成)匹配到的实体是「补中益气汤由什么」而不是「补中益气汤」。原因非贪婪匹配.?遇到「由」就停了但如果问题里有多个「由」或句式变体会截错。解决把正则改成^(.?)(?:由|包含|组成)加行首锚定或者在匹配前先做一次实体词典扫描优先用词典里的实体名去匹配问题。后者更稳推荐。4.4 Neo4j 社区版内存不足导致查询超时现象图谱数据量到几万节点后多跳查询比如「查某个证候对应的方剂里包含哪些药材」响应超过 10 秒甚至超时。原因社区版默认堆内存 512MBpage cache 也小多跳查询需要遍历大量关系。解决在neo4j.conf里调大dbms.memory.heap.max_size2G和dbms.memory.pagecache.size1G根据机器实际内存调整。另外给常用查询的实体名建索引CREATE INDEX FOR (h:Herb) ON (h.name)。4.5 前端跨域请求被拦截现象Django 接口用 Postman 能通前端页面调用报 CORS 错误。原因Django 默认没有配置跨域头。解决安装django-cors-headers在INSTALLED_APPS和MIDDLEWARE里注册设置CORS_ALLOWED_ORIGINS为前端地址。开发阶段可以设CORS_ALLOW_ALL_ORIGINS True上线前改回来。5. 问答效果验证与图谱扩展的实用技巧5.1 用测试集量化问答准确率不要凭感觉说「效果还行」。建一个 50100 条的问题-答案对作为测试集覆盖每种意图类型跑一遍算准确率# tests/test_qa_accuracy.py from kg_api.qa_engine import parse_and_query TEST_CASES [ {q: 黄芪有什么功效, expected_type: herb_efficacy, expected_contains: 补气}, {q: 补中益气汤由什么组成, expected_type: formula_composition, expected_contains: 黄芪}, {q: 四物汤主治什么, expected_type: formula_treats, expected_contains: 血虚}, ] def run_accuracy_test(): correct 0 for case in TEST_CASES: result parse_and_query(case[q]) if result.get(type) case[expected_type]: answers .join(result.get(answers, [])) if case[expected_contains] in answers: correct 1 print(f准确率: {correct}/{len(TEST_CASES)} {correct/len(TEST_CASES)*100:.1f}%) run_accuracy_test()这个测试跑完你会清楚知道哪类意图识别率低。常见情况是「主治」和「功效」混淆因为问法相似。解决办法是在意图模板里加优先级或者引入一个轻量分类模型比如用 jieba 分词 TF-IDF SVM做意图分类正则只做兜底。5.2 图谱扩展从单跳查询到多跳推理初期图谱只支持单跳查询药材→功效但中医药问答的价值在多跳。比如「哪些方剂含有能治疗消渴的药材」需要三跳方剂→包含→药材→具有→功效→对应→疾病。MATCH (f:Formula)-[:CONTAINS]-(h:Herb)-[:HAS_EFFICACY]-(e:Efficacy)-[:HAS_EFFICACY]-(h2:Herb)-[:CONTAINS]-(f2:Formula)-[:TREATS]-(d:Disease {name: 消渴}) RETURN DISTINCT f.name AS formula_name, h.name AS herb_name LIMIT 20这类查询在数据量上来后容易慢建议对高频路径做预计算把结果存成物化视图Neo4j 里可以用额外的关系类型存预计算结果查询时直接读。5.3 一个让我少走弯路的习惯我做完这个项目最大的教训是先跑通最小闭环再堆功能。一开始我花了两周设计本体、标注数据结果发现 Neo4j 导入有问题、Django 接口调不通整个链路是断的。后来改成先用 20 味药材、5 个方剂跑通「导入→查询→返回」全流程再逐步加数据、加意图、加用户模块效率高很多。如果你正在做这个方向建议第一周就定一个目标让「黄芪有什么功效」这个问题从 Django 接口返回正确答案。跑通之后剩下的都是体力活。希望帮到你。本文还有配套的精品资源点击获取