
简介面向中医药信息化、毕业设计与Python全栈开发学习者的可运行项目基于Django搭建后端服务结合Neo4j图数据库完成中医药知识图谱的构建与存储并实现基于图谱的智能问答功能。资源附带文档说明适合作为毕业设计、期末大作业或课程设计参考也可用于快速了解知识图谱与传统Web框架的整合思路。压缩包内共2000个文件以py源码、pyc编译文件为主辅以js、css前端资源、html页面、配置文件及sqlite3数据库文件打包大小39.02MB结构包含完整工程代码与部署运行所需依赖。资源已获得较高评审分源码本地编译可运行读者可直接调试修改节省环境搭建与排错时间。目前已有156人学习下载适合具备一定Django和Python基础、希望从零搭建中医药知识图谱问答系统的开发者参考。1. 拆解 DjangoNeo4j 中医药知识图谱问答平台这份源码到底能跑通什么这几年知识图谱和智能问答一直是计算机毕业设计的热门选题但很多同学卡在同一个地方用 Django 写后端容易用 Neo4j 建图也不难难的是把两者串成一套能演示、能答上问题的系统。这套基于 Django Neo4j 的中医药知识图谱与智能问答平台源码恰好把这条链路完整铺好了——从 CSV 数据清洗、本体建模、图谱导入到 Django REST API 封装、问句解析和答案回填再到文档说明每一步都有对应代码。它适合两类人一类是拿它做毕业设计、需要快速跑通原型的学生另一类是刚接触图数据库、想看看中医这种强关联领域怎么做问答的 Python 开发者。这篇笔记我会按“图谱怎么建 → Django 怎么接 → 问答怎么做 → 有哪些坑”的顺序把能复用的代码和参数一个个拆开。2. 本体设计与图谱构建关系建模比选数据库更重要2.1 中医药领域本体六类节点与六种关系的约定拿到这份资源时我第一件事不是看 Django 代码而是看数据目录里的 CSV 和 Cypher 脚本。知识图谱项目里本体设计决定后面所有查询好不好写这个道理在这套代码里体现得很清楚。这套资源采用的是比较经典的中医药知识组织结构我把它拆成六类节点中药Herb、方剂Formula、疾病Disease、症状Symptom、归经Meridian、功效Effect。关系则有六种关系起始节点指向节点业务含义包含方剂中药某方剂由哪些药材组成主治方剂疾病某方剂用于治疗什么病治疗中药疾病某味药对某病症有效表现疾病症状某疾病有哪些临床表现归经中药归经某味药归属哪条经络忌用中药疾病/人群某味药在哪种情况下不宜使用这个设计好在哪它把“方剂-中药-疾病”这条核心链拆成了可直接遍历的边。比如用户问“治疗感冒的方剂里有哪些药”对应查询路径是疾病-(主治)-方剂-(包含)-中药天然是图遍历不需要像关系型数据库那样做两次 JOIN。我比较欣赏的是忌用这个关系被单独建模了这是中医药场景里很关键但很多入门项目容易漏掉的部分——它让后续撑着问“什么药不能吃”这类否定性问题时有所依托。资源里每个节点标签下都预留了若干属性字段比如中药节点带name、pinyin、category方剂节点带name、source、function。属性不宜贪多够支撑问答模板就行这是这套代码的一个务实之处。2.2 CSV 清洗与 LOAD CSV 批量导入全量入库的推荐姿势数据导入是知识图谱项目最磨人、也最容易被低估的一步。这套资源采用的是 CSV Cypher 脚本的方式而不是直接在 Python 里逐条写节点。我拆了几遍代码把资源里最核心的导入逻辑整理成一个标准流程。首先准备 CSV 文件放在 Neo4j 的import目录下。以中药节点为例CSV 字段大概长这样name,pinyin,category,property,meridian 人参,renshen,补虚药,甘微苦温,脾肺 黄芪,huangqi,补虚药,甘微温,脾肺 当归,danggui,补血药,甘辛温,肝心脾这里meridian字段是多个值用逗号分隔的字符串导入时要拆成独立节点并用归经关系连接。对应 Cypher 导入语句LOAD CSV WITH HEADERS FROM file:///herb.csv AS row MERGE (h:中药 {name: trim(row.name)}) ON CREATE SET h.pinyin row.pinyin, h.category row.category, h.property row.property WITH h, row UNWIND split(row.meridian, 、) AS meridianName MERGE (m:归经 {name: trim(meridianName)}) MERGE (h)-[:归经]-(m)这段代码的逻辑不复杂LOAD CSV WITH HEADERS把 CSV 按带表头方式读入MERGE保证中药节点存在——不存在则创建存在则跳过创建只做属性补充。最后用UNWIND把meridian字段按顿号拆开每个归经名称独立匹配或创建节点并建立归经关系。这里有两个参数值得注意。第一trim()必须加CSV 里每行末尾的不可见字符会让同名节点重复创建第二split()的分隔符要和 CSV 里实际使用的符号一致这份资源的中药数据用顿号、方剂里的药物组成用逗号两个文件我都碰到过全角半角混用的问题后面避坑章节会细说。导入完节点后还要导入关系数据。方剂和中药的组成关系可以单独用一个formula_herb.csv每行两列formula_name和herb_name。导入语句如下LOAD CSV WITH HEADERS FROM file:///formula_herb.csv AS row MATCH (f:方剂 {name: trim(row.formula_name)}) MATCH (h:中药 {name: trim(row.herb_name)}) MERGE (f)-[:包含]-(h)这段是纯关系导入所以用MATCH而不是MERGE去定位两端节点。如果定位不到节点环会直接失败说明 CSV 里有脏数据——这时候不要急着改代码先回头查源头数据。关于“用LOAD CSV还是用 Python 驱动写入”我的建议是首次全量导入用本段的方式速度快且能复用后续增量更新用驱动逐条写入。这套资源里也写了 Python 增量更新的脚本但主干还是 CSV。2.3 图谱自检导入后必须跑的三条校验语句导入完成不代表数据正确。我自己有过惨痛教训也是在这份资源里踩过的——当时导入了几百个中药节点但后来发现其中有十几个节点因为MERGE时属性不一致被重复创建了。所以我现在每次导入完都固定跑三条校验语句。第一条检查各类型节点总数是否符合预期MATCH (n) WITH labels(n) AS label, count(*) AS cnt RETURN label, cnt第二条检查没有任何关系的孤立节点MATCH (n) WHERE NOT (n)--() RETURN labels(n)[0] AS label, n.name AS name第三条检查关键关系链是否完整比如“方剂-中药”是否存在指向不存在节点的悬空关系MATCH (f:方剂)-[r:包含]-(h:中药) WHERE h.name IS NULL RETURN f.name AS formula, r这三条语句跑完基本能确认图谱底子搭没搭歪。前两条特别适合在做问答之前跑一遍因为问答的查全率直接受孤立节点和重复节点影响——用户问“人参的功效”如果图谱里同时存在“人参”和“人参 ”两个节点答案就必然少一半。建议把这套自检脚本固化成一个check_graph.py文件每次改完数据就跑一次。3. Django 工程与 Neo4j 集成从驱动封装到 REST API 的完整写法3.1 项目结构规划与依赖选型看完图谱部分接下来拆 Django 侧。这份资源采用的是典型的 Django 多 app 结构但我更关注的是它怎么处理 Django 和 Neo4j 的边界。我个人尤其认同它是把 Neo4j 相关逻辑单独抽成了一个 service 层而不是散落在各个 views 文件里。推荐按下面这种布局组织工程tcm_kg/ ├── manage.py ├── config/ │ ├── settings.py │ └── urls.py ├── apps/ │ ├── graph/ │ │ ├── views.py # 图谱查询接口 │ │ ├── services.py # Cypher 查询封装 │ │ └── urls.py │ ├── qa/ │ │ ├── views.py # 问答接口 │ │ ├── intent.py # 意图识别与槽位抽取 │ │ └── generator.py # 从问句生成 Cypher │ └── accounts/ │ ├── views.py # 用户与 RBAC 权限 │ └── models.py ├── utils/ │ └── neo4j_client.py # 驱动封装、连接池 └── data/ ├── herb.csv ├── formula.csv └── disease.csv依赖方面这份资源的核心依赖如下我建议直接照着装Django4.2 djangorestframework3.14 neo4j-driver5.14 jieba0.42.1 python-dotenv1.0关于集成的关键选型值得单独说明的是这里使用neo4j-driver官方驱动而不是py2neo。原因是py2neo的维护状态这两年一直不太稳定而官方驱动对 Neo4j 4.x/5.x 的版本兼容性更好事务和连接池管理也更透明。如果读者拿这套代码做毕设答辩时能说清楚“为什么选官方驱动”本身就是一个加分项。3.2 Neo4j 驱动封装连接池与会话管理Django 是请求/响应模型每隔请求都会调用查询接口。如果每次查询都重新建立 Neo4j 连接开销会非常明显所以需要把驱动实例全局复用。这套资源里提供了一个neo4j_client.py核心逻辑我整理成如下代码from neo4j import GraphDatabase class Neo4jClient: def __init__(self, uri, user, password, databaseneo4j): self.driver GraphDatabase.driver( uri, auth(user, password), max_connection_pool_size50, connection_timeout10, ) self.database database def close(self): self.driver.close() def query(self, cypher, parametersNone): 执行只读查询并返回记录列表 with self.driver.session(databaseself.database) as session: result session.run(cypher, parameters or {}) return [record.data() for record in result] def execute_write(self, cypher, parametersNone): 执行写入操作使用显式事务 with self.driver.session(databaseself.database) as session: with session.begin_transaction() as tx: tx.run(cypher, parameters or {}) tx.commit() # 全局单例模块导入时只初始化一次 _neo4j_client None def get_neo4j_client(): global _neo4j_client if _neo4j_client is None: _neo4j_client Neo4jClient( urios.getenv(NEO4J_URI, bolt://localhost:7687), useros.getenv(NEO4J_USER, neo4j), passwordos.getenv(NEO4J_PASSWORD), ) return _neo4j_client这段代码有几个参数值得讲透。max_connection_pool_size50是连接池上限Django 默认开发服务器线程数为几十这个值比线程数略大即可connection_timeout10是建立连接的超时秒数默认值 30 秒太长线上表现为某个接口卡住很久才报错调到 10 秒能更早暴露故障。query()方法内部用with self.driver.session()来管理会话生命周期——关键是这一点很多人容易漏掉会话不关闭会造成连接泄漏最终报Failed to obtain a connection from pool。写入操作为什么单独拆一个execute_write因为 Neo4j 的自动提交事务只适用于数据量小的场景涉及批量写入比如增量导入知识时你必须用begin_transaction()拿到事务对象然后通过tx.commit()显式提交。这套封装把“读”和“写”区分开后面在业务代码里就不容易把两种事务模式搞混。3.3 图谱查询服务把 Cypher 封装成 Django 可用的接口有了客户端封装下一步是把 Cypher 查询封装成服务方法。这里以“从某味中药出发查询它治疗哪些疾病、所属方剂、禁忌人群”为例这是一条非常典型的一节点多路径查询也是知识图谱问答最核心的查询场景。先写 Cypher 查询MATCH (h:中药 {name: $herbName}) OPTIONAL MATCH (h)-[:治疗]-(d:疾病) OPTIONAL MATCH (h)-[:包含]-(f:方剂) OPTIONAL MATCH (h)-[:忌用]-(forbidden) RETURN h.name AS 中药, collect(DISTINCT d.name) AS 主治疾病, collect(DISTINCT f.name) AS 所属方剂, collect(DISTINCT forbidden.name) AS 禁忌这段查询用的是OPTIONAL MATCH作用是即使某个路径为空其他路径照样返回结果不会因为“这味药没有关联的方剂”而整行掉空。collect(DISTINCT ...)把多条路径聚合到数组里同时去掉重复项。再把它封装成 Django 服务层的 Python 方法from utils.neo4j_client import get_neo4j_client def query_herb_fast(herb_name: str) - dict: cypher MATCH (h:中药 {name: $herbName}) OPTIONAL MATCH (h)-[:治疗]-(d:疾病) OPTIONAL MATCH (h)-[:包含]-(f:方剂) OPTIONAL MATCH (h)-[:忌用]-(forbidden) RETURN h.name AS 中药, collect(DISTINCT d.name) AS 主治疾病, collect(DISTINCT f.name) AS 所属方剂, collect(DISTINCT forbidden.name) AS 禁忌 records get_neo4j_client().query( cypher, {herbName: herb_name} ) if not records: return None record records[0] return { herb_name: record[中药], diseases: [d for d in record[主治疾病] if d], formulas: [f for f in record[所属方剂] if f], contraindications: [c for c in record[禁忌] if c], }接着写一个继承 DRFAPIView的视图接口from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from apps.graph.services import query_herb_fast class HerbDetailView(APIView): def get(self, request, herb_name: str): data query_herb_fast(herb_name) if data is None: return Response( {error: f未找到中药: {herb_name}}, statusstatus.HTTP_404_NOT_FOUND, ) return Response(data)接口返回的 JSON 结构会是{ herb_name: 人参, diseases: [气虚证, 肺虚喘咳], formulas: [四君子汤, 参苓白术散], contraindications: [实热证] }这套封装模式的核心价值在于views 层完全不懂 Cypher只调用 Python 方法service 层只负责查询逻辑不感知 HTTP。后续如果要把 Neo4j 换成其他图数据库只需要替换 service 层实现。接在/api/herb/herb_name/后面前端就能直接拿到 JSON 数据做展示这也是资源里写好的接口路径约定。4. 智能问答实现从问句到 Cypher 的完整链路拆解4.1 问句意图识别模板匹配加词性标注的轻量方案图谱搭好了Django 接口也通了接下来最核心的是智能问答模块。这套资源没有引入复杂的深度学习模型而是用简单的模板匹配 关键词分类方案——在我看是合理的因为中医药问答的句式相对固定没必要为 2000 条问句训一个 Bert。代码核心在apps/qa/intent.py里核心思路我先展开。预设四大类意图查功效、查方剂组成、查疾病治疗、查禁忌。每类意图对应一组触发词实现如下import re import jieba INTENT_RULES [ {name: herb_effect, keywords: [功效, 作用, 主治, 治什么], target_tag: 中药}, {name: formula_composition, keywords: [组成, 成分, 药材, 有哪些药], target_tag: 方剂}, {name: disease_treatment, keywords: [治, 怎么治, 吃什么药, 方子], target_tag: 疾病}, {name: herb_forbidden, keywords: [忌, 不能吃, 禁忌, 慎用], target_tag: 中药}, ] def match_intent(question: str) - str: for rule in INTENT_RULES: for kw in rule[keywords]: if kw in question: return rule[name] return unknown这里匹配顺序是有讲究的。herb_forbidden的触发词包含“忌”和“不能吃”而disease_treatment里也有“治”这个词所以必须把更具体的“禁忌”规则放在后面否则“什么药忌用于孕妇”会被错误识别成“疾病治疗”。我在读这套代码时看到作者把herb_forbidden放在列表最后一位这个细节能避免很多答非所问的案例。同时还要把问句里的实体抽出来这里用 jieba 分词并加自定义词典import jieba from pathlib import Path # 加载中药名词自定义词典避免“人参”被切成“人”“参” custom_dict Path(__file__).parent.parent.parent / data / herb_name.txt jieba.load_userdict(str(custom_dict)) def extract_entities(question: str) - list: words list(jieba.cut(question)) # 这里可以通过候选词典过滤出实际存在于图谱中的实体名 candidates [] for word in words: if len(word) 2: candidates.append(word) return candidatesload_userdict加载的herb_name.txt每行一个中药名格式为“人参 3 n”。需要说明的是如果词典里没有某个疾病名称比如“风热感冒”jieba 可能切成“风热”和“感冒”。所以资源里额外准备了一份disease_name.txt同样写入load_userdict效果立竿见影。4.2 槽位抽取与查询生成从自然语言到 Cypher 的映射意图分类只能识别“查什么”具体查哪个病、哪个药还要做槽位抽取。以“感冒吃什么药”为例这句话的意图是disease_treatment槽位是“疾病感冒”。生成 Cypher 的逻辑在apps/qa/generator.py核心代码如下def generate_cypher(intent: str, entities: dict) - str | None: if intent disease_treatment: disease entities.get(disease) if not disease: return None return f MATCH (d:疾病 {{name: {disease}}}) OPTIONAL MATCH (d)-[:主治]-(f:方剂) OPTIONAL MATCH (f)-[:包含]-(h:中药) RETURN d.name AS disease, collect(DISTINCT f.name) AS formulas, collect(DISTINCT h.name) AS herbs if intent herb_effect: herb entities.get(herb) if not herb: return None return f MATCH (h:中药 {{name: {herb}}}) OPTIONAL MATCH (h)-[:治疗]-(d:疾病) RETURN h.name AS herb, collect(DISTINCT d.name) AS treated_diseases return None这段代码能跑但有一个明显问题直接用 f-string 拼接用户输入存在 Cypher 注入风险这个在避坑章节里我会展开。这里先给出修正后的参数化写法def generate_cypher_safe(intent: str, entities: dict) - tuple: if intent disease_treatment: disease entities.get(disease) if not disease: return None, {} cypher MATCH (d:疾病 {name: $disease}) OPTIONAL MATCH (d)-[:主治]-(f:方剂) OPTIONAL MATCH (f)-[:包含]-(h:中药) RETURN d.name AS disease, collect(DISTINCT f.name) AS formulas, collect(DISTINCT h.name) AS herbs return cypher, {disease: disease} return None, {}这里的关键变化是 Cypher 字符串和参数分离。Neo4j 驱动拿到的是$disease占位符由驱动在服务端做参数绑定用户输入怎么都影响不到查询结构。4.3 答案组织与兜底策略没有答案时的降级方案问答系统最怕的其实是“问了个图谱里不存在的问题”。这套资源里有一段兜底逻辑值得单独拿出来讲它的做法是把问题中的实体放到图谱里做模糊前缀匹配如果找到相似名称节点就返回“您是不是想问xxx”而不是硬给一个空答案。def make_reply(intent: str, records: list) - dict: if not records: return { answer: 图谱中暂无相关信息请换个说法或检查实体名称。, status: not_found, } if intent disease_treatment: record records[0] formulas [f for f in record.get(formulas, []) if f] herbs [h for h in record.get(herbs, []) if h] return { answer: f治疗{record[disease]}的常用方剂有{、.join(formulas)} f主要药材包含{、.join(herbs)}。, status: ok, } return {answer: json.dumps(records, ensure_asciiFalse), status: ok}对空结果资源还做了日志埋点。每次问答请求都会把这句问话写入logs/qa_miss.log方便后续人工审查和图谱迭代——这是很实用的设计等于把问答系统的持续优化路径留下来了。5. 避坑指南Django 与 Neo4j 集成时的常见问题与排查5.1 坑一Cypher 注入——f-string 拼接问句带来的安全风险现象用户输入“感冒 OR 11 --”后接口返回了整个图谱里的所有疾病数据。原因generate_cypher里用 f-string 拼接用户实体名构造出来的 Cypher 被恶意改写比如{name: 感冒}变成了{name: 感冒 OR 11 --}注释符把后面的查询条件全吃掉了。解决所有用户输入统一走参数化查询。Neo4j 驱动的session.run()支持第二参数传递字典把脏数据放在字典值里Cypher 模板里用$name占位。我已经在答疑时给过修正版凡是涉及前端输入拼 Cypher 的路径全部改成参数化写法没得商量。顺带提一个搜索词里常问的点Django 里做“查询-删除对象”操作如果是通过 ORM 删除 MySQL 数据正常Model.objects.filter(...).delete()就行但如果对象关系指向 Neo4j 里的节点必须先查出来删关系再删节点否则会报约束冲突。这一点和下面的坑二相关。5.2 坑二删除节点前没删关系——报错和半删除状态现象用 Python 驱动执行session.run(MATCH (h:中药 {name:人参}) DELETE h)时报错提示Cannot delete node because it still has relationships。原因Neo4j 默认不允许删除带关系的节点必须先删关系再删节点。这也是新手最常遇到的错误之一对应搜索里“django执行查询-删除对象”的高频问题。很多人在 Django 接口里删一条数据只删了业务表记录Neo4j 里相应节点还挂着关系两边就出现了数据不一致。解决把删除操作改成两段式。先删关系再删节点MATCH (h:中药 {name: 人参}) DETACH DELETE hDETACH DELETE会先移除该节点所有关系再删除节点比手写两步更安全。但有个新问题如果希望通过删除某个中药节点同时保留它跟方剂节点之间的历史记录DETACH DELETE会把这些记录也一并清掉。所以更稳妥的写法是先把关系存到临时数据或直接同步更新 Django 侧的业务表再执行删除保证两边一致。5.3 坑三驱动连接池耗尽——“Failed to obtain a connection from pool”现象系统运行一段时间后Django 接口开始随机报错提示Failed to obtain a connection from pool重启后暂时恢复但过一阵又复发。原因代码里某些查询路径没有用with self.driver.session()每次session.run()后忘了调用session.close()。Neo4j 驱动连接池默认大小 50每泄漏一个会话就少一个可用连接很快就池满。解决全部通过with语句管理会话确保会话被释放。并把连接池大小调成可观测值比如 30并加日志记录当前池使用率def query(self, cypher, parametersNone): with self.driver.session() as session: result session.run(cypher, parameters or {}) records [record.data() for record in result] return records顺带留意如果 QPS 很高可在GraphDatabase.driver()里加max_transaction_retry_time5参数设置事务重试时间降低网络抖动带来的瞬时失败率。5.4 坑四中文名称的字节对齐问题和标签索引缺失导致全表扫描现象导入时用MERGE (h:中药 {name: 人参})后续查询用MATCH (h:中药 {name: trim(row.name)})却匹配不到结果。排查半天发现有的节点name属性里带全角空格或首尾换行符。原因CSV 文件清洗不彻底或者引入了 BOM 头。, 、 ,常见于 Windows 环境下编辑的 CSVtrim()对全角空格不可见字符无效。解决入库前统一做一次数据清洗在 Python 里用re.sub(r[\u3000\u00A0\s], , text)把全角空格、普通空格、换行符一起干掉。然后针对高频查询的name字段建立索引这也是知识图谱查询性能的关键防坑点CREATE INDEX herb_name_index IF NOT EXISTS FOR (h:中药) ON (h.name)这一步做完用户搜“人参”从原来的全表扫描变成索引命中速度提升一个量级。如果还搜不到再用CONTAINS做模糊匹配但要避免CONTAINS不带索引前缀滥用它适合小图不适合大数据场景。5.5 坑五Django ORM 与 Neo4j 的事务边界不一致现象一次性增量导入 100 个中药节点代码先写 Django MySQL 业务表再写 Neo4j 图谱结果 MySQL 写完了、执行 Neo4j 写入时报错回滚逻辑却没生效MySQL 和 Neo4j 两边数据不一致。原因Djangotransaction.atomic()只管理 Django 所连接的数据库事务管不到 Neo4j。两边数据操作不是一个事务单元任何一边失败都会留下脏数据。解决不强求分布式事务而是在业务层实先消息补偿机制。具体做法是先写 Neo4j再写 MySQLNeo4j 成功而 MySQL 失败时用一条回滚 Cypher 删除刚才写入的节点同时在 MySQL 里记录一条“待补偿日志”。对于毕设项目简单做法就是写一个补偿函数def compensate(neo4j_nodes: list): cypher MATCH (h:中药) WHERE h.name IN $names DETACH DELETE h get_neo4j_client().execute_write(cypher, {names: neo4j_nodes})这五个坑是我觉得这份资源里最有价值的部分几乎每个都是从代码里实际遇到的问题。避坑的思路本身比解决方案更值得收藏。6. 上线前必做图谱索引优化、慢查询定位与部署检查问答系统部署到生产环境之前有几步优化是必须提前做的顺序如下第一建立高频查询索引。知识图谱的前端查询基本是中药.name、疾病.name、方剂.name精确匹配这三个字段都要建单属性索引。话不多说跑一遍下面的脚本CREATE INDEX IF NOT EXISTS FOR (h:中药) ON (h.name) CREATE INDEX IF NOT EXISTS FOR (d:疾病) ON (d.name) CREATE INDEX IF NOT EXISTS FOR (f:方剂) ON (f.name)第二用EXPLAIN和PROFILE定位慢查询。Django 侧可以设一个监控中间件记录每条 Neo4j 查询耗时超过 300ms 的执行计划单独拉出来看PROFILE MATCH (h:中药 {name: 人参}) OPTIONAL MATCH (h)-[:治疗]-(d:疾病) RETURN d.name LIMIT 10PROFILE会显示每个算子的行数和内存占用如果看到NodeByLabelScan而不是NodeIndexSeek说明索引没生效。第三问答日志反哺图谱。把前面说的logs/qa_miss.log每周统计一次凡是高频出现的未命中实体就批量补进 CSV 再导入。这一条算是把问答系统从“演示工具”变成“可持续演进系统”的关键步骤。我当时在这个项目上做了一轮迭代把日志里出现 20 次以上的缺失疾病名和药材名导回去次月问答命中率从 62% 提到了 84%。第四部署参数检查。Neo4j 配置文件neo4j.conf里重点看三个值dbms.memory.heap.initial_size建议 1Gdbms.memory.pagecache.size建议按数据量设置至少 512Mdbms.default_listen_address0.0.0.0确保 Docker 外部客户端可以访问。从那以后我每次部署知识图谱问答系统都强制走一遍“建索引 → 跑 PROFILE → 查日志 → 调内存”这个流程一次没落过。希望帮到你。本文还有配套的精品资源点击获取