简介:这是一份基于Python实现的医疗知识图谱问答系统完整工程,面向计算机相关专业毕业设计、知识图谱或自然语言处理方向的开发者,也适合希望快速搭建医疗问答原型的工程师参考。系统围绕“医疗问题—图谱查询—答案生成”主线,集成数据采集、实体关系抽取、Neo4j图数据库建模、问句意图分类与答案检索等完整流程,可直接运行学习。资源包共28个文件,以Python源码(8个py)为核心,辅以txt医疗词典与预处理数据、JSON图谱数据、XML工程配置及少量JPG示意图片,整体大小15.85MB,结构清晰,便于按模块对照理解。目前已有625人学习下载。除源码外,包内还提供从网页爬取整理的结构化医疗数据、疾病/症状/药物等实体关系图谱,以及问题解析与答案搜索相关的规则文件,能够帮助读者省去数据搜集与清洗环节,专注理解知识图谱构建和问答逻辑。
1. 医疗知识图谱问答系统:一个 zip 背后的四层技术栈
这个标题读起来像是一个打包好的毕业设计成品:基于 Python 的医疗知识图谱,再套一个知识问答系统。拿到这个压缩包,第一步不是急着解压代码,而是先想清楚它到底想证明什么。背后其实是四层东西在支撑:数据层、图谱层、问答层、服务层。数据层负责把疾病、症状、药品、科室这些医疗信息整理成结构化三元组;图谱层用图数据库把三元组存成网络;问答层把用户的自然语言问题翻译成图查询;服务层则用 Web 接口把答案送到浏览器里。
这套链条适合两类人:一类是毕业设计选了自然语言处理或数据工程方向的学生,另一类是想用最短路径体验知识图谱构建的开发者。很多人误以为问答系统的难点在模型,但实际上大多数翻车都发生在数据没对齐、图没导进去、查询语句跑不通这三层。真正可用的源码包,数据文件一定比代码更值得花时间研究。通用做法是先跑通“读取一个实体、建立一条关系、回答一个问题”的最小闭环,再谈扩展。
2. 从实体识别到三元组:医疗知识图谱是怎么建出来的
知识图谱构建,说白了就是“把非结构化文本变成结构化三元组”。医疗场景里,最常见的原始数据是电子病历、疾病百科、药品说明书、门诊科室记录。标题里单独把“数据”标出来,说明这个 zip 的重点大概率不在前端界面,而在图谱构建这一步。构建路线有两条,一条是自顶向下:先定义好 schema,再把数据往里填;另一条是自底向上:从文本里抽出实体和关系,再归纳成 schema。毕业设计普遍选第一条,因为医疗领域的实体类型相对固定:疾病、症状、药物、科室、检查项目就那么几类,先定 schema 可以少走很多弯路。
2.1 医疗数据的常见来源和“数据”目录里该有什么
你解压之后,不要急着看代码,先把数据文件当作最重要的审查对象。一个能用的知识图谱系统,数据文件至少应该具备以下特征:实体表有主键和名称,关系表有起始节点、终止节点、关系类型。常见的情况是,diseases.csv 里放着 disease_id、name、icd10,drugs.csv 里放着 drug_id、name、aliases,relations.csv 里是 head_id、relation、tail_id,这样的结构让人一看就懂。反过来说,如果看到的是一个巨大的 JSON,每条记录里嵌套了十几层字典,那就要花一个下午做数据展开。
更常见的情况是,数据里有大量冗余和脏值。比如“感冒”和“普通感冒”同时出现在疾病表里,或者“阿司匹林”和“拜阿司匹灵”都出现在药品表里。在医疗场景里,同一实体的别名比你想的多得多。别急着建图,先做实体对齐。这一步做不好,后面查“阿司匹林”和查“拜阿司匹灵”会返回两套不同结果,答辩老师一问就露馅。所以数据目录里真正值钱的是那一个人工维护的别名映射表,它决定了整个图谱的实体会不会散成一地。
2.2 用 Python 做疾病-症状-药品实体对齐:最小 pipeline 代码
我一般会把实体对齐写成一条短 pipeline:先把名称统一去掉首尾空格,再映射别名,最后按规范化后的名称去重。下面这段代码是通用的,你只需要把 csv 路径换成自己的数据文件。
import pandas as pd # 读取原始疾病表和药品表 diseases = pd.read_csv("data/diseases.csv") drugs = pd.read_csv("data/drugs.csv") # 手动维护一组别名映射,医疗场景里这个映射必须人工过一遍 alias_map = {"拜阿司匹灵": "阿司匹林", "扑热息痛": "对乙酰氨基酚"} def normalize_name(name): if isinstance(name, str): name = name.strip() # 去掉首尾空格 name = name.lower() # 统一小写,西药名尤其需要 name = alias_map.get(name, name) # 别名映射不命中就保留原值 return name diseases["name_clean"] = diseases["name"].apply(normalize_name) drugs["name_clean"] = drugs["name"].apply(normalize_name) # 按清洗后的名称去重,保留第一个出现的 id diseases_dedup = diseases.drop_duplicates(subset="name_clean", keep="first") drugs_dedup = drugs.drop_duplicates(subset="name_clean", keep="first") print(f"疾病表:{len(diseases)} -> {len(diseases_dedup)}") print(f"药品表:{len(drugs)} -> {len(drugs_dedup)}")这段代码的核心逻辑就三件事:读取数据、规范化名称、按规范名去重。参数里最关键的是 alias_map,这份映射表看着不起眼,实际上决定了整个图谱的质量。真实医疗数据集里,同一种药有商品名、通用名、化学名,同一种病有俗称和医学名称,你不可能用正则穷举完,所以必须保留一个人工维护的映射文件。drop_duplicates 里的 keep 参数建议保留 first,因为第一条数据通常来自权威来源;如果你知道哪个来源更可信,可以改成按来源排序后再去重。
做完实体对齐,还应该顺手检查关系表里有没有引用不存在的节点。常见做法是用 pandas 的 isin 做一次外键校验,把那些 head_id 或 tail_id 不在实体表里的行打印出来,逐条看。医疗数据里最容易出问题的地方是手工整理的关系表,症状名写错一个字,整条关系就悬空了。你可以花十分钟写个校验脚本,比事后在 neo4j 里发现一堆悬空节点要便宜得多。
2.3 图谱落库:neo4j 的导入命令与 schema 设计
实体对齐完成之后,下一步是把三元组写进图数据库。毕业设计层面,图数据库几乎只有 neo4j 一个选项,原因很直接:Cypher 查询语法容易学,可视化界面能直接截图放进论文,社区版免费,单机处理几万条三元组毫无压力。有些教程会推荐用 networkx 做内存图,但那顶多叫演示,不叫系统;保存、重启、查询都做不到生产形态,答辩老师随便一问就会被问倒。
导入数据的方式有两种。数据量小的时候别用 neo4j-admin import,那是给千万级数据用的离线导入工具,改一次文件就得重新构建整个库。直接用 Cypher 的 LOAD CSV 在线导入,改数据、重启服务、重新跑脚本,迭代速度快得多。
LOAD CSV WITH HEADERS FROM 'file:///diseases.csv' AS row MERGE (d:Disease {id: row.disease_id, name: row.name_clean}); LOAD CSV WITH HEADERS FROM 'file:///drugs.csv' AS row MERGE (d:Drug {id: row.drug_id, name: row.name_clean});这里必须用 MERGE 而不是 CREATE,因为 CSV 文件里同一个节点可能在多行出现,CREATE 会生成重复节点。MERGE 是按指定属性查找,找不到才创建。节点导入之后,再导入关系。需要特别注意的是,实体节点如果是分成 Disease、Drug、Symptom 多个标签,关系导入时就要按关系类型拆文件写死标签,因为 Cypher 里没法用参数动态构造标签。这样做看起来笨,但好处是导入脚本和数据结构一一对应,答辩时讲起来很清晰。
导入完成后不要忘记建索引。我见过太多人导入完直接查,结果一句MATCH (d:Disease {name: '感冒'})等了十几秒。在 neo4j 里给实体的 name 建索引,因为问答服务每次都要按名字做精确查找,没有索引就是全表扫描,数据量一涨立刻现出原形。索引命令很简单:
CREATE INDEX disease_name_idx IF NOT EXISTS FOR (d:Disease) ON (d.name); CREATE INDEX drug_name_idx IF NOT EXISTS FOR (d:Drug) ON (d.name);如果遇到“玄学”问题,比如 LOAD CSV 第一次成功第二次报错,大概率不是语法问题,而是 CSV 文件的编码不对。把所有数据文件统一转成 UTF-8,并且不要带 BOM,Windows 上导出的 Excel 表格尤其容易踩这个坑。具体表现是第一个字段名变成带 BOM 的字符串,Cypher 提示列不存在,排查的时候看报错信息,隐藏字符往往会被原样打出来。
3. 把“问”变成“查”:问答系统的三种实现路线与选型
知识图谱构建完成,只是把数据变成了图,问答系统要做的,是把人的自然语言“翻译”成图查询。这个翻译过程有两种极端:一种是完全靠规则模板,另一种是训练一个序列到序列模型。对毕业设计来说,选型原则不是“哪个更先进”,而是“哪个能稳定跑通并说清楚”。医疗场景和工业知识图谱有一个共同点:错了代价很高,用户问“这个药和那个药能不能一起吃”,答案必须可解释,不能像生成模型那样编理由。所以我建议优先走模板匹配或意图识别路线,生成式问答放到最后再考虑。
3.1 模板匹配:最稳妥的毕业设计基线
模板匹配的思路很朴素:把问句套进几个固定句型,命中哪个模板就走哪个查询。这样做的优点是实现快、可解释、每一条回答都能追溯到规则,缺点是覆盖面有限。但作为毕业设计基线,先让它跑通,再慢慢扩展模板,比一上来就砸深度学习模型靠谱得多。
import re QUESTION_TEMPLATES = [ { "name": "disease_symptom", "pattern": re.compile(r"(.*?)有哪些症状"), "cypher": ( "MATCH (d:Disease {{name: '{entity}'}})-[:HAS_SYMPTOM]->(s:Symptom) " "RETURN collect(s.name) AS answer" ), }, { "name": "drug_indication", "pattern": re.compile(r"(.*?)能治什么病"), "cypher": ( "MATCH (d:Drug {{name: '{entity}'}})-[:TREATS]->(dis:Disease) " "RETURN collect(dis.name) AS answer" ), }, ] def parse_intent(text): for tpl in QUESTION_TEMPLATES: m = tpl["pattern"].match(text) if m: return tpl["name"], m.group(1).strip() return None, None这段代码唯一需要解释的是 pattern 里的捕获组,它把用户问题里最可能的实体名称抠出来。(.*?)是懒惰匹配,问句是“高血压有哪些症状”时,实体就是“高血压”。注意 Cypher 模板里的双花括号{{ }},这是 Python format 的转义写法,否则会被当成占位符替换。真实落地时,你不会直接拼字符串进 Cypher,因为用户输入可能带上引号、括号等特殊字符,注入进去轻则查询报错,重则把图谱搅乱。更好的做法是用 py2neo 的参数化查询,把 entity 作为参数字典传进去,而不是字符串拼接。
模板匹配的隐患在于,用户提问很难完全按你写模板的字面走。比如“高血压的病人会有什么表现”就命中不了“有哪些症状”这个句型。这时候不要急着加模板,先梳理出一份“问题类型清单”,把用户问题按意图分成疾病症状、药物治疗、科室推荐、药物禁忌等几类,每类做三到五个句型,覆盖面就能到七成左右。剩余三成答不上来的,直接返回“这个问题我还在学习中”,比瞎猜一个答案更安全。
3.2 基于意图识别和槽位填充的对话式问答
模板匹配再往前走一步,就是意图识别加槽位填充。这里的意图不再靠正则硬匹配,而是用文本分类模型判断意图类型,再用实体识别模型或词典抽取出实体作为槽位。毕业设计的数据量不大,不需要上 BERT,用 jieba 分词加词频特征,训练一个逻辑回归或随机森林就足够。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline import joblib # train_data 是 (问句, 意图标签) 列表,从已有问答日志里人工标注 train_texts = [...] train_labels = [...] model = make_pipeline( TfidfVectorizer(tokenizer=str.split, max_features=5000), LogisticRegression(max_iter=200, C=1.0) ) model.fit(train_texts, train_labels) joblib.dump(model, "models/intent_model.joblib") # 槽位填充:用分词结果在候选实体词典里做最长匹配 def slot_fill(text, entity_dict): for token in jieba.lcut(text): if token in entity_dict: return entity_dict[token] return None这里的三个参数值得说一下。max_features=5000限制特征维度,医疗问句词汇量不大,5000 个词足够覆盖大部分表达,再大只会引入噪声。max_iter=200是为了确保逻辑回归收敛,特征多了不改这个参数会反复报警告。C=1.0是正则化强度,毕设阶段不用调,理解它是“越小约束越强”就行。模型训练完之后,不要只看整体准确率,把每个意图的精确率和召回率分别打出来,你会发现“药物禁忌”类的样本少,模型几乎学不到东西,这时候老老实实回去补模板,比硬调参数有用。
诚实地说,这套组合已经到了一个毕业设计能承受的复杂度上限。再往上加 BERT 微调、对话管理、知识增强生成,光是数据标注和算力就够你忙两个月,答辩时很容易被问倒。真正值得投入的是前面那一张精心维护的实体别名表,它直接决定了槽位填充能不能命中。如果槽位都不能正确抽出来,意图识别得再准也没用。
3.3 生成式问答为什么不适合这个源码包
现在打开各种技术社区,到处都是大模型问答的 demo,好像不做 LLM 就落伍了。但你把生成式问答塞进医疗知识图谱系统之前,要想清楚一个问题:生成模型给出的答案,你拿什么保证它对?医疗场景里,模型胡编一个不存在的症状组合,用户照着做了,这个责任谁来背。这也是学术界和工业界对医疗问答格外谨慎的原因。
常见的做法是如果你真的想展示对大模型有了解,可以把图谱检索和生成结合起来,做一个检索增强问答:先用实体链接找到图数据库里的候选子图,再把候选三元组拼接成文本,最后丢给大模型做语言组织。这样一来,答案的事实来源仍然是知识图谱,大模型只负责把图里的信息改写成通顺的句子,哪怕它表达得不够好,事实也不会跑偏。但要注意,这个方案在答辩时需要解释清楚子图检索的边界,否则评委可能会追问“你的知识图谱到底起了什么作用”。如果只是想在毕设里加点新鲜感,把这类 RAG 作为系统的可选扩展模块,基线仍然用模板匹配,是最省力的做法。
这一章的选型结论很明确:模板匹配是骨架,意图识别是肌肉,生成式问答是外壳。骨架不稳,外壳再漂亮也没用。
4. 在本地跑通整个系统:环境、参数和可复现步骤
拿到源码包之后,最快建立信心的方法是先把系统跑起来,再去读代码。很多同学卡在第一步,原因不是代码差,而是环境不一致。Python 生态最烦人的地方就是“在我机器上明明是好的”,为了让“在你机器上也能跑”,你必须用虚拟环境锁定依赖版本,并且把启动顺序固定下来。整个流程分三块:建环境、导数据、启动问答服务。
4.1 创建虚拟环境并锁定关键依赖
先把 Python 装好。这里说的“装好”,不是指你有一个能打开 Jupyter 的 Anaconda,而是你能在终端里跑出python --version。Python 官网下载安装包之后,记得勾选 Add Python to PATH,这一步能省掉后面半个小时的排错时间。然后用 venv 创建独立环境,不要直接装进系统环境,不然隔三个月你会发现依赖冲突到怀疑人生。
python -m venv venv source venv/bin/activate # Windows 上执行 venv\Scripts\activate pip install --upgrade pip pip install pandas jieba flask py2neo scikit-learn这份依赖清单是知识图谱问答系统的常见配置,不是源码包固定的要求。pandas 负责读数据,jieba 负责分词和实体识别,flask 负责启动 web 服务,py2neo 负责连接 neo4j,scikit-learn 负责训练意图分类模型。版本锁定上,建议装完之后执行pip freeze > requirements.txt,把这个文件提交到代码库里,别人拿到压缩包就能一口气装回同样环境。Windows 用户如果不习惯命令行,可以用 PyCharm 或 VS Code 的终端,但要保证当前激活的是 venv 而不是全局环境,终端提示符前面会出现(venv)字样。pip 下载慢就换镜像源,命令是pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple。这些细节看起来都是小事,但每一次答辩演示前在别人电脑上重跑一遍,你就会发现都是坑。
4.2 启动 neo4j 并导入医疗三元组
neo4j 社区版下载后解压即用,启动命令是neo4j start,默认控制台在 7474 端口,Bolt 端口是 7687。首次启动会让你修改默认密码,这个密码就是 Python 代码里连接需要的凭据,先记在项目根目录的 .env 文件里,别写死在 .py 里。数据导入脚本上面已经给了,这里只说一个最容易忽视的问题:LOAD CSV 读取的路径是 neo4j 的 import 目录,不是你在系统里写 csv 的地方。为了省事,直接把数据文件丢进 neo4j 安装目录下的 import 文件夹最稳妥。
# 放在 import 目录后,执行导入脚本 python scripts/import_graph.py导入脚本里建议用一个事务包装器,因为几百条数据虽然一次导入很快,但中途报错会导致库里有半截数据,排查起来非常头痛。neo4j 的 py2neo 接口支持graph.begin()开事务,导入成功再 commit,失败就 rollback,这比你反复删库重建省力得多。反复导入时也会残留旧数据,最干净的做法是在导入脚本开头执行MATCH (n) DETACH DELETE n;,把图清空再导入。导入完成后,在浏览器里执行MATCH (n:Disease) RETURN count(n),确认数量和数据文件里的行数一致,再随便查一个实体看属性是否正确。
4.3 用 Flask 包一层 Web 问答接口:最小可运行 demo
问答服务最简方案是 Flask 单文件应用。一个POST /api/qa接口,接收 JSON 格式的 question,解析意图和实体,执行查询,返回 answer。下面是去掉所有业务装饰的最小骨架。
from flask import Flask, request, jsonify from py2neo import Graph app = Flask(__name__) graph = Graph("bolt://localhost:7687", auth=("neo4j", "your_password")) @app.route("/api/qa", methods=["POST"]) def qa(): data = request.get_json() question = data.get("question", "") # 调用上一章的 parse_intent 和 slot_fill intent, entity = parse_intent(question) if not intent or not entity: return jsonify({"answer": "这个问题我还在学习中"}) # 执行对应模板的 Cypher result = graph.run(template_cypher, entity=entity).data() return jsonify({"answer": result}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)这段代码有几个关键点。graph.run()的第二个参数 entity 是参数化查询,别用 f-string 往 Cypher 里拼,前面说过的安全问题不是吓唬人。host="0.0.0.0"表示允许局域网访问,毕设演示时手机和电脑连同一个 Wi-Fi 就能给评委展示;如果只在自己电脑上用,改成127.0.0.1更安全。debug=True开发阶段开着方便看报错,答辩时一定要关掉,否则任何人都能弹出你的调试控制台。
启动命令就一行:flask --app app.py run,然后另开一个终端用 curl 验证接口通不通。
curl -X POST http://127.0.0.1:5000/api/qa \ -H "Content-Type: application/json" \ -d '{"question": "高血压有哪些症状"}'如果返回的不是期望答案,先不要怀疑 Flask 代码。第一步去 neo4j 浏览器里手动执行同一条 Cypher,看能不能查出数据;第二步在 Python 里打印 intent 和 entity,确认实体识别是否正确;第三步再回来看结果结构。按这个顺序排查,五分钟内就能定位到问题是出在数据、查询还是接口层。如果你不想手写前端页面,可以用 Flask 的 render_template_string 把表单和结果拼在一起,输入框绑定 question,提交后调用/api/qa显示 answer。这里的数据绑定很简单,不涉及复杂组件,够毕设演示用。
5. 毕业设计问答系统避坑:5 个最容易翻车的现场
这部分内容是真正的血泪经验。以下每一条都是我在答辩现场见过不止一次的典型案例,按照“现象 → 原因 → 解决”的顺序写给你,每一条都值得在打包代码之前自查一遍。
5.1 数据导入时的三个坑
第一个坑是 CSV 中文乱码或导入后实体变空。现象是 neo4j 浏览器里看到的中文都是乱码,或者本来就是中文的 name 字段变成了 NULL。原因绝大多数是文件编码不是 UTF-8,Windows 的 Excel 默认用 GBK 保存 CSV,neo4j 的 LOAD CSV 不认。解决方法很简单,先用 Python 批量转编码,转出来的文件确认是 UTF-8 并且不带 BOM,再丢进 import 目录。
with open("source.csv", "r", encoding="gbk") as f: content = f.read() with open("target.csv", "w", encoding="utf-8") as f: f.write(content)这段代码本身没什么好讲的,关键是 encoding 字段要按源文件实际情况改。如果你不确定源文件是 GBK 还是 UTF-8,可以用 Python 的chardet先检测一遍。注意写入时用utf-8,不要用utf-8-sig,后者会写入 BOM,neo4j 的 LOAD CSV 会把 BOM 当成字段名的一部分,报“列不存在”的错误。
第二个坑是关系导不进去,报某个 id 找不到。现象是节点导入成功,但执行关系导入脚本时报 MATCH 没有结果。原因往往是关系表里的 head_id 和实体表的主键不是同一套值,比如实体表主键是字符串 “D001”,关系表里却是数字 1,或者字段名带了不可见的空格。解决方法是导入前做一个外键校验脚本,把关系表里所有 head_id 和 tail_id 在实体表里做一遍 lookup,打印出所有不匹配的值,修掉之后再去 neo4j 里导。这个步骤虽然慢,但是能省下后面无数次的反复删库。
第三个坑是重复节点。现象是你 MERGE 了节点,但查询时发现同一种病有两三个节点。原因可能是实体表里 name 没有做对齐,也可能是 MERGE 的键选错了。解决方法是回到 2.2 节的去重 pipeline,并且给源文件固定一个主键。建议在导入脚本里用MERGE (d:Disease {id: row.disease_id})来作为唯一性依据,name 只是属性不作为 merge 键,这样即使原始数据里名称有重复,图结构也不会乱。
5.2 问答效果暴露的两类问题
第四个坑是模板命中率非常低,测试集里十句话八句答不上。现象是系统跑通了,但问得稍微绕一点就直接走“学习中心”兜底。原因是你把模板写得太死,比如只匹配“有哪些症状”却不匹配“什么表现”“会有哪些反应”。解决方法是建一个测试集,至少五十条问句,每条标注期望意图和实体,然后跑一遍你的模板匹配,把所有没命中的问句归一下类,按高频失败场景补模板。我用这个办法把命中率从 40% 拉到 90%,原理不复杂,就是让测试集替你找到模板的盲区。每次改完模板都要重跑一遍测试集,不然你看到一个错误就加一个规则,改完发现另一个意图又坏了,这叫回归问题。
第五个坑是答案查询成功,但结果和用户问的南辕北辙。现象是用户问“高血压能吃什么药”,系统返回的却是高血压这个疾病的症状。原因大概率是实体识别的歧义和关系类型不匹配,比如用户提到“高血压”时,你的实体链接同时匹配到了疾病和症状两个节点,而 Cypher 里对节点标签没有做约束。解决方法是把实体链接结果打印出来人工核对,调整词典的优先级和歧义消解规则,同时在 Cypher 里显式指定路径类型。比如“能吃什么药”对应(:Disease)-[:RECOMMENDED_DRUG]->(:Drug),不要用无类型的关系去查,否则图谱一复杂就分不清哪个边代表什么语义。
还有一个隐藏在测试集里的坑:如果你的测试问句总是和训练模板一致,评估出来的准确率会高得离谱,但一到现场演示就翻车。正确做法是让没看过代码的同学帮你写问句,最好用大白话,这样测出来的指标才是真实水平。
6. 用准确率和召回率验证你的问答系统:一个能写进论文的评估脚本
问答系统不是能回答几个问题就完事了,你需要一个可量化的评估方法撑起论文里的实验部分。最实用的做法是把测试集组织成 JSON 文件,每条记录包含 question、intent、gold_entities,然后写一个脚本自动跑评估。测试集至少准备 100 条,覆盖五个意图类型,每个类型二十条,这样分开算指标时每一种都有足够样本。
import json def evaluate(test_file, answer_func): with open(test_file, "r", encoding="utf-8") as f: cases = json.load(f) correct = 0 for case in cases: pred = answer_func(case["question"]) # 判定正确的方法:比较意图和实体交集,而不是比较整句 gold = set(case["gold_entities"]) pred_entities = set(pred.get("entities", [])) if pred.get("intent") == case["intent"] and gold & pred_entities: correct += 1 return correct / len(cases) def my_answer_func(question): # 这里调用你自己的问答系统,返回 {"intent": ..., "entities": [...]} pass if __name__ == "__main__": acc = evaluate("data/test_questions.json", my_answer_func) print(f"整体准确率: {acc:.2f}")这段脚本的判定逻辑特意用了“意图与实体交集”而不是“字符串完全一致”,因为医疗问句的答案本来就是集合,顺序也不重要。你可以在脚本里按意图分组,分别打印出每个意图的准确率和召回率。如果某个意图准确率明显低于平均,那说明模板和槽位填充在这类问题上有系统性的缺陷,而不是偶发失误。这样一份实验报告,比贴十个演示截图更有说服力。
最后分享一个我的习惯:在所有代码提交前,我一定会在data/下新建一个叫test_questions.json的文件,至少塞进五十条问题,并且每加一个功能就先把这条测试集跑一遍。这个习惯救过我很多次,尤其是改实体对齐代码的时候,往往你以为只是在修数据,一不小心就把原有问答逻辑带崩了。希望帮到你。
本文还有配套的精品资源,点击获取