简介:面向需要完成Python期末大作业、毕业设计,或希望上手知识图谱实战项目的学习者,这是一份基于Neo4j图数据库的医疗知识图谱智能问答机器人完整源码。项目围绕医疗问答场景展开,覆盖问题解析、实体识别、CQL查询生成、图谱构建与答案返回等关键环节,代码经过严格调试,可直接运行。压缩包共36个文件,约15.34MB,以Python脚本为主体,配置了配套文本说明、静态页面资源和示例图片,目录结构清晰,便于按模块定位和理解。已有296人学习浏览,可帮助读者快速搭建同类系统,也可作为图数据库应用和问答系统的课程设计参考,整体实用性和完成度较高。
1. 医疗知识图谱问答不是大模型玩具:先用 Neo4j 把病历变成可查询的图
一个常见的认知偏差,是以为“智能问答机器人”必须靠大模型才能做。但在医疗这种对准确率、可解释性要求极高的垂直领域,基于 Neo4j 图数据库构建知识图谱,再配合规则和模板生成答案,反而是落地性价比最高的方案。它不依赖大规模算力,查询路径可追溯,每一条答案都能回到图谱里的某条实体关系上去。这个项目标题的完整链路是:用 Python 从半结构化医疗数据中抽取实体和关系,写入 Neo4j 图数据库,再通过问句分类、命名实体识别、Cypher 查询模板映射,实现“输入自然语言问题、输出结构化答案”的问答机器人。适合正在做毕业设计、医疗信息化项目或者想切入知识图谱方向的 Python 开发者。这套方案的价值在于:它把“能跑起来的 Demo”和“能解释的答案”之间的距离缩到了最短。
2. Neo4j 环境与 Python 驱动:安装、启动、连库的三个关键步骤
2.1 为什么医疗问答场景优先选图数据库,而不是 MySQL
医疗数据的核心特征是实体多、关系密。一个高血压患者会关联到症状、药品、科室、禁忌、并发症等多个维度的实体,而这些实体之间又互相牵扯。如果用 MySQL 的关系表来建模,要么建一堆中间关联表,要么靠多个 JOIN 才能查出一条完整路径。查询一旦超过两层 JOIN,SQL 的复杂度和维护成本就开始失控。Neo4j 的属性图模型用节点表示实体、用关系表示联系,查询路径天然就是图的遍历。“高血压患者需要避开什么药”这样的问题,在 Cypher 里可以只用一个 MATCH 语句表达,语义清晰,性能也不随关系深度指数劣化。
做技术选型时还有一个现实原因:Neo4j 社区版免费,对单机部署的项目完全够用。它提供了原生的可视化界面,实体和关系能被直接渲染出来,这对调试数据清洗效果和向上汇报演示都有天然优势。相比之下,其他图数据库要么在可视化上弱一些,要么对 Linux 服务器配置要求更高,前期投入成本更大。
2.2 Neo4j 安装与配置:Desktop 和社区版的取舍
我一般推荐新手直接用 Neo4j Desktop,它会帮你管理 JDK 版本和数据库实例,省去大量环境变量配置时间。但如果你所在的内网环境不便下载安装包,或者你在远程 Linux 服务器上部署,那就要用社区版的neo4j-x.x.x-unix.tar.gz手动解压安装。无论哪种方式,安装后的第一个坎是改初始登录密码。Neo4j 默认初始账号是neo4j,密码也是neo4j,第一次通过浏览器访问http://localhost:7474时,系统会强制要求修改密码。
如果你在安装启动时遇到 neo4j 无法启动或者报 JVM 内存错误,优先检查两件事:一是 JDK 版本是否匹配,Neo4j 4.x 需要 Java 11,5.x 则需要 Java 17;二是 JVM 堆内存参数是否设得太低。默认的堆内存偏保守,但也不建议一上来就调高,先跑通再优化。修改neo4j.conf里的server.memory.heap.initial_size和server.memory.heap.max_size即可生效。有一点必须注意:Neo4j 默认只监听本机回环地址,如果你需要局域网内别的机器访问页面,要在配置文件里修改监听地址为0.0.0.0,改完后必须重启服务才生效。
# Linux 环境手动安装 Neo4j 社区版的关键步骤 tar -zxvf neo4j-community-5.x.x-unix.tar.gz cd neo4j-community-5.x.x/bin ./neo4j console # 首次启动后,打开浏览器访问 http://localhost:7474 # 使用默认账号 neo4j / neo4j 登录,然后按要求修改密码这段命令先解压安装包,再进入 bin 目录用console模式前台启动。console模式的好处是日志直接打印在终端,看到报错马上能定位;确认无误后,再改用./neo4j start后台启动。参数说明:community-5.x.x代表社区版和版本号,不同版本对 Java 的版本要求不同,务必看官方文档里对应版本的 Java 兼容性说明;console是调试参数,正式使用应该换成start。
2.3 Python 驱动连接 Neo4j:最简连库代码与参数说明
Python 操作 Neo4j 的官方驱动是neo4j这个库,直接pip install neo4j即可。驱动的核心概念是Driver对象和Session对象:Driver是连接池,一个应用全局只初始化一次;Session是具体执行 Cypher 的会话,用完了必须关闭。很多初学者翻车的点恰恰在这里——每查一次就创建一个 Driver,运行几次后连接池就被占满,程序卡死。
from neo4j import GraphDatabase class Neo4jClient: def __init__(self, uri, user, password, database="neo4j"): self.driver = GraphDatabase.driver(uri, auth=(user, password)) self.database = database def query(self, cypher, parameters=None): # 使用 session 执行查询,with 块能保证 session 被正确关闭 with self.driver.session(database=self.database) as session: result = session.run(cypher, parameters) return [record.data() for record in result] def close(self): self.driver.close() client = Neo4jClient("bolt://localhost:7687", "neo4j", "你的密码") rows = client.query("MATCH (n) RETURN n LIMIT 5") print(rows) client.close()逻辑说明:构造函数初始化Driver连接池,指定 Bolt 协议地址、账号和密码;query方法用上下文管理器确保Session一定关闭,避免连接泄漏;session.run可以安全传入参数化查询,防止 Cypher 注入。参数说明:bolt://localhost:7687是 Neo4j 的二进制传输协议端口,跟浏览器访问的 7474 HTTP 端口不同,别写混;database参数默认是neo4j,如果你建了多库才能用到;record.data()将 Cypher 返回的记录转成 Python 字典,方便后续处理。
提示:完整跑通第一段代码之前,不建议直接进入图谱构建阶段。先建一条
CREATE (n:Test {name:'hello'}),再用MATCH (n:Test) RETURN n查询,确认驱动和数据库链路是通的,后续踩坑时你才能判断问题到底出在 Python 还是 Neo4j。
3. 医疗知识图谱构建:从清洗数据到 LOAD CSV 导入
3.1 医疗数据清洗:实体统一与术语归一化
医疗数据源最典型的三种形态:公开的医学百科词条、医院信息系统导出的结构化病历、论文或教材里的半结构化文本。无论哪一类,直接导入图谱都会制造垃圾实体。最常见的例子是“高血压”和“高血压病”“原发性高血压”并存,“阿司匹林”和“阿司匹林肠溶片”被当成两个节点。这种实体歧义会让后续问答检索出现严重遗漏——用户问“高血压吃什么药”,图谱里只匹配到“高血压病”,答案自然为空。
我一般会建一个术语映射表,用 Python 做一次归一化。核心思路:先抽取所有实体候选词,按词频排序,对高频词的别名做人工确认,确认结果存成 CSV 的映射关系,再在清洗阶段统一替换。这个步骤很耗时,但它是决定问答准确率的上限。
import pandas as pd # 原始数据示例:来自爬取的医学百科词条 raw_data = pd.read_csv("raw_medical_data.csv", encoding="utf-8-sig") alias_map = pd.read_csv("alias_mapping.csv", encoding="utf-8-sig") alias_dict = dict(zip(alias_map["alias"], alias_map["standard_name"])) def normalize_entity(text): for alias, standard in alias_dict.items(): if alias in text: text = text.replace(alias, standard) return text.strip() raw_data["entity_normalized"] = raw_data["entity"].apply(normalize_entity) raw_data.to_csv("cleaned_medical_data.csv", index=False, encoding="utf-8-sig")逻辑说明:先读入原始数据和别名映射表,将别名映射表转成 Python 字典;normalize_entity函数对每一条实体做遍历替换,将别名统一替换成标准名;最后把清洗结果写回新 CSV。参数说明:encoding="utf-8-sig"是关键,用 UTF-8 保存 CSV 后如果直接入库会出现中文乱码,utf-8-sig带 BOM 头,既能被 Neo4j 正确读取,也能被 Excel 正常打开;alias_mapping.csv的格式至少包含alias和standard_name两列。
3.2 实体—关系模型设计:医疗图谱的节点类型与关系方向
清洗完数据后,先画一张图谱的 Schema 草图再动手。医疗问答最常见的问题类型是:某疾病的症状、某疾病用药、某药品禁忌、某症状去哪个科室。围绕这四类问题,节点类型和关系可以这样设计:节点Disease(疾病)、Symptom(症状)、Drug(药品)、Department(科室)、Food(食物);关系HAS_SYMPTOM(疾病到症状)、DRUG_FOR(药品到疾病)、CONTRADICTION(药品到疾病,表示禁忌)、DEPARTMENT_OF(症状到科室)。
| 关系类型 | 起点节点 | 终点节点 | 语义说明 |
|---|---|---|---|
| HAS_SYMPTOM | Disease | Symptom | 该疾病可能出现的症状 |
| DRUG_FOR | Drug | Disease | 该药品用于治疗某疾病 |
| CONTRADICTION | Drug | Disease | 该药品对该疾病或人群禁用 |
| DEPARTMENT_OF | Symptom | Department | 该症状对应的就诊科室 |
关系的方向不要搞反。在 Cypher 里方向即语义,(d:Disease)-[:HAS_SYMPTOM]->(s:Symptom)和(s:Symptom)-[:HAS_SYMPTOM]->(d:Disease)虽然都能查询,但语义完全相反。我习惯统一按自然语义建模:疾病拥有症状,所以从疾病指向症状;药品用于疾病,所以从药品指向疾病。这样在问答模块写模板时,大脑里的逻辑映射是顺的,不需要每次查询前先想方向。
3.3 LOAD CSV 导入:节点和关系的导入脚本
Neo4j 的LOAD CSV是社区版体量下最实用的导入手段。两百万行以内的数据,用这个方式在分钟级完成,完全不需要上neo4j-admin import大数据导入工具。导入前提是把 CSV 文件拷贝到 Neo4j 的import目录下,或者使用file:///前缀指定路径。如果文件放在自定义目录,需要在neo4j.conf里配置server.directories.import。
// 导入疾病节点 LOAD CSV WITH HEADERS FROM 'file:///disease.csv' AS row MERGE (d:Disease {id: row.disease_id}) SET d.name = row.name, d.description = row.description; // 导入症状节点 LOAD CSV WITH HEADERS FROM 'file:///symptom.csv' AS row MERGE (s:Symptom {id: row.symptom_id}) SET s.name = row.name;逻辑说明:第一段脚本从 import 目录读取disease.csv,按disease_id作为唯一标识执行MERGE,不存在则创建新节点,存在则更新属性;第二段同理导入症状节点。参数说明:WITH HEADERS表示 CSV 首行是列名,后续通过row.列名引用字段;MERGE会先按条件查找,匹配不到才创建,比CREATE更安全,避免重复导入产生冗余节点;节点的id字段是业务主键,不等于 Neo4j 内部生成的节点 ID,后续关系导入时要靠它关联实体。
// 导入疾病-症状关系 LOAD CSV WITH HEADERS FROM 'file:///disease_symptom.csv' AS row MATCH (d:Disease {id: row.disease_id}) MATCH (s:Symptom {id: row.symptom_id}) MERGE (d)-[r:HAS_SYMPTOM]->(s) SET r.weight = toFloat(row.weight);关系导入必须先定位起点和终点节点,再创建关系。注意两个 MATCH 之间不要加WHERE条件,直接用属性匹配效率更高。toFloat(row.weight)把 CSV 里的字符串转成浮点数,这个字段可以用于问答排序——比如多个症状同时命中时,返回权重更高的那个答案。如果导入后发现有些关系没建立,优先检查 CSV 里是否有多余空格、disease_id在节点导入文件里是否存在。出现匹配失败并不报错,只是静默跳过,这也是 LOAD CSV 对数据质量要求高的原因。
3.4 索引与约束:查询变慢前的关键设计
导入完成后,很多人急着写问答,直接开始查 Cypher。但在数据量达到十万节点、几十万关系后,全库扫描的查询速度会肉眼可见地下降。Neo4j 里的MATCH (d:Disease {id: 'xxx'})默认是全库扫节点属性,没有索引支撑时性能就是灾难。
CREATE CONSTRAINT disease_id_unique IF NOT EXISTS FOR (d:Disease) REQUIRE d.id IS UNIQUE; CREATE INDEX disease_name_idx IF NOT EXISTS FOR (d:Disease) ON (d.name);逻辑说明:第一条语句为疾病节点的id字段创建唯一约束,既保证数据不重复,又隐式创建了索引;第二条为疾病节点的name字段创建普通索引,供问答模块按名称匹配时使用。参数说明:REQUIRE d.id IS UNIQUE是 Neo4j 5.x 的语法,4.x 需要用ASSERT d.id IS UNIQUE;索引字段一定要跟查询条件保持一致——如果你查询写的是MERGE (d:Disease {name: row.name}),那应该建在name上而不是id上,否则索引形同虚设。
注意:关系方向、主键字段、索引字段这三件事必须在导入完成后立刻自查。方向错了,问答答案含义颠倒;主键没用业务 id,后续更新数据会产生重复节点;索引缺失,数据涨到几十万条后每次问答都像在做全表 JOIN。自查方法很简单:在 Neo4j Browser 里分别执行
MATCH (d:Disease)-[:HAS_SYMPTOM]->(s:Symptom) RETURN d.name, s.name LIMIT 5和MATCH (d:Disease)<-[:HAS_SYMPTOM]-(s:Symptom) RETURN d.name, s.name LIMIT 5,看哪一条结果符合语义。
4. 智能问答机器人落地:问句分类、实体识别与 Cypher 模板生成
4.1 问答架构:先分类,再识别实体,最后套模板
问答模块不是直接把整句话丢给 Cypher 查询,而是拆成三个子任务串行执行。第一步,判断用户问的是“症状”“用药”“科室”“禁忌”中的哪一类;第二步,在问句里抽取出疾病、药品、症状等实体名;第三步,根据问题类型和实体名拼接出对应的 Cypher 查询语句。这种规则加模板的方案在限定领域内效果稳定,单句响应耗时十几毫秒,后期要引入深度学习模型时也只需要替换意图分类和实体识别两个模块,整体架构不用动。
问句分类看似简单,实际是决定整个系统表现的关键。设计不佳时,用户问“高血压一般有什么感觉”,系统会先匹配到疾病实体“高血压”,却因为分类错乱去查了“高血压的用药”,答非所问。我倾向于用基于关键词模板的快速分类器,而不是一开始就上一堆机器学习分类模型。原因是医疗问句的句式高度集中,用几个核心关键词就能覆盖绝大多数情况。
import re def classify_question(question): question = question.strip() if re.search(r"什么药|用药|吃什么|治疗|药物", question): return "drug" elif re.search(r"症状|表现|感觉|出现|有什么", question): return "symptom" elif re.search(r"科室|挂号|看什么科|哪个科", question): return "department" elif re.search(r"不能吃|禁忌|禁用|避免|不宜", question): return "contradiction" else: return "default"逻辑说明:classify_question接收用户问句,逐个用正则表达式匹配关键词。命中“什么药”“用药”等词时判定为药品查询;命中“症状”“表现”时判定为症状查询;以此类推。参数说明:关键词的匹配顺序有讲究——先匹配更具体的“不能吃”“禁用”,再匹配宽泛的“什么药”,是为了防止“高血压不能吃什么药”这种句子被误归到普通用药查询;default是兜底类型,交给问答模块返回引导语而不是报错。这套规则的缺点也很明显,用户换一种问法就失效,所以要配合运维阶段不断把翻车问句的规则补进去。
4.2 命名实体识别:用 jieba 加自定义词典做医疗实体识别
医疗实体识别方案中,hanlp和LTP的功能更强,但部署依赖更多;jieba加自定义词典的方案在限定领域内识别效果已经够用,而且部署成本极低。第一步是把图谱里所有的标准实体名导出到词典文件,然后在 Python 里使用用户词典完成切词。这也体现了知识图谱构建阶段术语归一化的价值——词典越干净,实体识别越准确。
import jieba # 从 Neo4j 导出所有疾病名称到词典文件 disease_names = client.query("MATCH (d:Disease) RETURN d.name AS name") with open("medical_dict.txt", "w", encoding="utf-8") as f: for item in disease_names: f.write(item["name"] + "\n") jieba.load_userdict("medical_dict.txt") def extract_entities(question): # 加载自定义词典后再切词,医疗术语会被识别为完整词 words = jieba.lcut(question) entities = [] for word in words: if len(word) >= 2: entities.append(word) return entities这段代码先把图谱中的所有疾病名称导出到medical_dict.txt,再加载为用户词典。关键点是jieba.load_userdict必须在切词前执行,且每次进程启动后只加载一次,不要放在循环里重复加载。jieba.lcut返回切分后的词列表,这里做了一个简单的过滤,只保留长度大于等于 2 的词,过滤掉“我”“的”“怎么”这类单字噪音。一个明显的局限是:如果用户问句里的实体和词典不完全一致,比如问“高血压病吃啥药”而图谱里标准名是“高血压”,就会识别失败。解决办法是在实体识别后再做一次别名归一化,把“高血压病”映射回“高血压”,这块在避坑章节里细讲。
4.3 从问题到 Cypher:模板映射与答案组装
拿到意图类型和实体列表之后,就可以拼接 Cypher 查询了。这里的核心原则是:“永远用参数化查询,不要用字符串拼接拼出完整的 Cypher 语句”,后者不但有注入风险,而且遇到实体名里带特殊字符时,查询会直接报错。
def generate_answer(question, entity_list): intent = classify_question(question) if not entity_list: return "请问您想了解哪种疾病?可以告诉我具体的病名。" entity = entity_list[0] # 取第一个实体作为查询主体 if intent == "drug": cypher = """ MATCH (d:Disease {name: $entity})<-[:DRUG_FOR]-(drug:Drug) RETURN drug.name AS drug_name """ params = {"entity": entity} elif intent == "symptom": cypher = """ MATCH (d:Disease {name: $entity})-[:HAS_SYMPTOM]->(s:Symptom) RETURN s.name AS symptom_name """ params = {"entity": entity} elif intent == "contradiction": cypher = """ MATCH (d:Disease {name: $entity})<-[:CONTRADICTION]-(drug:Drug) RETURN drug.name AS drug_name """ params = {"entity": entity} else: return "这个问题我需要再理解一下,您可以换个问法,例如:高血压有什么症状?" rows = client.query(cypher, params) if not rows: return f"抱歉,没有找到与“{entity}”相关的信息,请确认疾病名称是否正确。" if intent == "symptom": symptoms = [row["symptom_name"] for row in rows] return f"{entity}的常见症状有:" + "、".join(symptoms[:5]) else: drugs = [row["drug_name"] for row in rows] return f"{entity}相关的药品有:" + "、".join(drugs[:5])这段代码是问答模块的核心逻辑:先获取意图和实体,再根据意图选择对应的 Cypher 模板,最后执行查询并组装答案。几个容易忽略的细节:$entity是参数占位符,params里的键名必须一致;entity_list[0]直接取第一个实体,如果问句里包含多个实体,这里就只处理主体;答案用[:5]截取前五个,避免一次性返回几十种药品导致答案过长。模板的局限在于它只能生成预设好的回答格式,遇到“高血压的并发症有哪些”这种需要遍历多跳关系的复杂问题,要到下一阶段增加模板类型。
4.4 把问答封装成服务:避免每次调用重新初始化
问答模块在项目里通常是作为 Flask 或 FastAPI 接口对外提供服务。这里最常见的错误是把 Neo4j 驱动的初始化代码放到每个请求处理函数里,每来一个用户请求就新建一个连接,最终导致连接池耗尽、查询超时。正确的做法是启动服务时初始化一次客户端,后续请求全部复用。
from flask import Flask, request, jsonify app = Flask(__name__) client = Neo4jClient("bolt://localhost:7687", "neo4j", "你的密码") @app.route("/qa", methods=["POST"]) def qa(): data = request.get_json() question = data.get("question", "") answer = generate_answer(question, extract_entities(question)) return jsonify({"question": question, "answer": answer}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)这段代码把client声明在模块级别,Flask 应用启动后只初始化一次驱动连接池。POST /qa接口接收 JSON 格式的请求体,提取question字段,调用问答函数后返回 JSON 格式的答案。参数说明:host="0.0.0.0"表示允许局域网内其他机器访问这个问答服务,方便你接入微信机器人、Web 前端或简单的演示页面。部署这一层后,知识图谱和用户之间就有了一个稳定的桥——前端不需要直接接触 Cypher,也避免了前端暴露数据库连接信息的安全风险。
5. 避坑专章:医疗图谱问答常见的五类翻车现场
5.1 中文乱码:CSV 文件编码不一致
现象:LOAD CSV 导入中文数据后,Neo4j 里的节点名称变成乱码或出现问号。原因:CSV 文件不是 UTF-8 编码,或者 Python 写入时用了默认的gbk编码,导致 Neo4j 读取失败。解决:写入和读取的编码保持一致,Python 写入时显式指定encoding="utf-8-sig",同时LOAD CSV语句保证文件本身是 UTF-8。建议导入前用文本编辑器检查文件编码格式,不要靠肉眼判断,用 Python 读取时打印一行验证。
5.2 属性名与参数名不一致导致的静默失败
现象:点击问答接口,系统返回“未找到相关信息”,但 Neo4j Browser 里手工查询,图谱里明明有节点和关系。原因:实体识别的结果和图谱里的标准名不一致。比如图谱里存的是“高血压”,用户问“高血压病”,识别出来的实体是“高血压病”,Cypher 查询MATCH (d:Disease {name: '高血压病'})查不到任何节点。解决:在实体识别模块后增加一个别名归一化函数,把高频别名的映射词典和构建图谱时的alias_mapping.csv保持一致。出现“未找到”时,先在日志里打印出识别出来的实体词,判断是识别问题还是映射问题。
5.3 关系导入时静默跳过导致图谱残缺
现象:关系 CSV 导入完成后统计关系数量,和预期值差了一大截,也没有任何报错。原因:关系 CSV 里的某个id在节点导入时不存在,MATCH匹配不到节点就直接跳过,不报错。解决:导入前用 Python 做一次两个文件的外键校验——把关系 CSV 里的disease_id和疾病节点 CSV 的id做差集,查出的差异行先修正或删除再导入。更稳健的做法是导入后执行计数脚本,分别统计节点数和关系数,和源数据做对比,差距超过一定阈值就回溯数据清洗步骤。
5.4 同一关系重复创建导致路径膨胀
现象:用CREATE导入关系时,同一个疾病和同一个症状之间出现多条相同类型的关系。原因:CREATE是“无条件创建”,数据源里存在重复记录或多次执行导入脚本时,就会创建重复关系。后果是问答结果出现同一个症状重复输出,且图谱可视化界面会变得混乱不堪。解决:所有创建语句都用MERGE,它先按关系模式查找再创建;如果你已经导入重复数据,用下面的去重命令清理,但前提是关系上没有需要保留的独立属性:
MATCH (d:Disease)-[r:HAS_SYMPTOM]->(s:Symptom) WITH d, s, collect(r) AS rels WHERE size(rels) > 1 FOREACH (r IN tail(rels) | DELETE r);逻辑说明:先找出所有疾病到症状的关系,按起终点节点分组,收集每组的所有关系。如果关系数量大于 1,就保留第一个,删除剩余的所有重复关系。这个命令在数据量较大时,可能会比较耗时,建议放在数据库空闲时间段执行。
5.5 多实体问句只取第一个导致答非所问
现象:用户问“高血压和糖尿病分别有什么症状”,系统只回答高血压,或者返回了不相干的内容。原因:实体识别提取到["高血压", "糖尿病"]两个实体,但问答逻辑只取了第一个。解决:如果是正式项目,需要在问句分类阶段识别出“比较型”或“并列型”问题,生成更复杂的 Cypher;如果是演示项目,建议在回答前对实体数量做判断,超过一个时返回提示语:“检测到您询问多个疾病,请一次询问一个疾病更准确。”反过来,如果实体都没识别出来,千万不要直接抛异常,要返回引导语让用户补充病名。这两条兜底联合作业时,大部分测试问题都不会直接返回空白。
6. 评估问答质量并横向扩展:准确率统计与下一阶段的三个方向
搭建完整个问答系统只是第一步,真正决定这个项目能不能交付的是评估和迭代。我习惯准备一份标准测试集,包含至少一百条问题,覆盖症状、用药、科室、禁忌四大类型,每条问题都标注了期望答案。测试时批量调用问答接口,对比实际答案和期望答案的重合率。重合率超过百分之八十,说明图谱和模板基本能撑住主要场景;如果低于这个线,不要急着换模型,先定位是实体识别失败还是 Cypher 模板缺失。记录每一次识别失败的问句原文和日志里打印出的实体词,比对着修规则,修完再跑一遍测试集。这种死循环式的调优虽然枯燥,但答准确率提升是最直观可感知的。
横向扩展有三个方向值得考虑。第一个方向是把问答从单轮升级到多轮:记录用户上一轮提及的疾病实体,当下一轮问句里没有实体时自动继承,比如用户先问“高血压有什么症状”,再问“吃什么药”,系统应该自动把药品查询的主体绑定为“高血压”。第二个方向是在实体识别层引入深度学习模型,比如用 BERT 微调命名实体识别任务,替代 jieba 加词典的方案,识别率会提升,代价是部署体积和推理延时明显上涨。第三个方向是补充图谱知识维度,目前只覆盖症状、药品、科室,可以继续加入检查项目、手术方式、并发症等节点类型,每加一类,问答模板的数量也要同步扩充。
我个人的习惯是在项目收尾前把问答过程的可视化查询路径存成日志。每次请求都记录问题、识别出的意图、命中的实体、生成的 Cypher、返回的答案。这样做的好处是,即使三个月后用户反馈某个答案不对,你也能通过日志还原当时的逻辑链路,不用重新跑一遍代码。遵守这套方法做下来的医疗知识图谱问答项目,不管最后是毕业设计展示还是小规模试运行,都会比停留在“能跑通 Demo”的水平高出不少。希望帮到你。
本文还有配套的精品资源,点击获取