从零构建医药知识图谱问答系统:数据爬取、Neo4j存储与智能问答实战
2026/9/3 7:25:35 网站建设 项目流程

大家好,我是专注于技术实战分享的博主。临近毕业季,不少计算机、数据科学相关专业的同学都在为毕设选题和实现发愁。如果你对知识图谱、自然语言处理或智能问答系统感兴趣,那么一个结合了数据采集、知识融合、图数据库存储与智能问答的“医药知识图谱问答系统”将是一个极具挑战性和展示度的绝佳选题。本文将带你从零开始,完整实现一个医药领域的知识图谱问答系统,涵盖从网络数据爬取、知识抽取与融合、Neo4j图数据库存储,到最终构建一个可交互的智能问答应用的全流程。无论你是希望完成一个高质量的毕业设计,还是想深入学习知识图谱的工程化实践,这篇文章都将提供一套可直接复现的代码和清晰的实现思路。

1. 项目背景与核心概念解析

在开始动手之前,我们首先要理解这个项目到底要做什么,以及其中涉及的核心技术概念。

1.1 什么是知识图谱与智能问答?

知识图谱本质上是一种用图结构来建模和存储知识的技术。它将现实世界中的实体(如“阿司匹林”、“高血压”)作为“节点”,将实体之间的关系(如“治疗”、“属于”)作为“边”,从而形成一个庞大的语义网络。这种结构非常符合人类的认知方式,也便于计算机进行复杂的关联查询和推理。

智能问答系统则是基于这个语义网络,允许用户用自然语言(如“阿司匹林能治疗什么病?”)进行提问,系统通过理解问题意图,在图数据库中查询并推理,最终返回结构化的答案。这比传统的基于关键词的搜索引擎更加精准和智能。

1.2 为什么选择医药领域?

医药领域知识结构清晰、关系复杂、专业性强,非常适合用知识图谱来建模。药物、疾病、症状、基因、副作用等实体之间存在着大量的、多层次的关系。构建一个医药知识图谱,不仅能作为毕业设计的优秀案例,其技术思路也可以迁移到金融、法律、教育等其他垂直领域。

1.3 技术栈全景图

本项目将涉及一个完整的技术链路,我们可以将其分为四个核心阶段:

  1. 数据采集层:使用 Python 爬虫从公开的医药网站或数据库(模拟数据源)采集原始文本数据。
  2. 知识处理层:利用自然语言处理技术,从文本中抽取实体和关系,并进行知识融合(对齐相同实体)。
  3. 知识存储层:将清洗和融合后的结构化知识存入 Neo4j 图数据库。
  4. 应用层:构建一个基于 Spring Boot 或 Flask 的 Web 应用,提供图谱可视化界面和智能问答接口。

接下来,我们将按照这个流程,一步步实现。

2. 环境准备与版本说明

工欲善其事,必先利其器。以下是完成本项目所需的主要软件环境。请注意,版本号是撰写本文时的常见选择,实际操作时请以官方最新稳定版为准,核心逻辑通常兼容。

2.1 开发环境与工具

  • 操作系统:Windows 10/11, macOS 或 Linux (如 Ubuntu 20.04+) 均可。本文命令以 Linux/macOS 为例,Windows 用户可在 Git Bash 或 WSL 下运行。
  • Python 环境:Python 3.8 或以上版本。推荐使用 Anaconda 或 Miniconda 创建独立的虚拟环境。
  • Java 环境:JDK 8 或 11 (用于运行 Neo4j 和 Spring Boot)。
  • 数据库:Neo4j 社区版 4.4 或 5.x。
  • IDE/编辑器:PyCharm, VS Code, IntelliJ IDEA 等任选。
  • 项目管理:Maven 3.6+ (用于 Spring Boot 项目), pip (用于 Python 包管理)。

2.2 核心依赖库清单

我们将创建两个主要项目:一个 Python 项目用于数据处理,一个 Java (Spring Boot) 项目用于构建 Web 应用和问答服务。

Python 项目依赖 (requirements.txt)

# 网络请求与解析 requests>=2.28.0 beautifulsoup4>=4.11.0 lxml>=4.9.0 # 数据处理 pandas>=1.5.0 numpy>=1.23.0 # 自然语言处理(用于简单的实体抽取,毕业设计可用规则或简单模型) jieba>=0.42.1 # 中文分词 pyltp>=3.4.0 # 或使用 HanLP, 用于命名实体识别(NER)和关系抽取(需自行下载模型文件) # 注:对于深度学习的NER,可使用 transformers 库,但复杂度较高。 # Neo4j 驱动 neo4j>=5.5.0 # Web框架(可选,用于提供简单API) flask>=2.2.0

Spring Boot 项目依赖 (pom.xml部分)

<dependencies> <!-- Web 支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Neo4j 驱动 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-neo4j</artifactId> </dependency> <!-- 模板引擎 (用于简单前端) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- 单元测试 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>

3. 第一阶段:医药数据爬虫采集实战

数据是知识图谱的基石。由于直接使用真实医药数据库(如DrugBank)可能涉及许可问题,我们以一个模拟的、结构清晰的公开医药信息网站为例,讲解爬虫的完整流程。请务必遵守目标网站的robots.txt协议,控制爬取频率,切勿对目标服务器造成压力。

3.1 目标分析与页面结构解析

假设我们的目标是爬取一个模拟的“常见药品说明书”网站,页面列出了药品名称,点击进入详情页后有“适应症”、“不良反应”、“相互作用”等字段。

首先,我们使用浏览器开发者工具(F12)分析页面结构,找到数据所在的HTML标签。

3.2 使用 Requests 和 BeautifulSoup 编写爬虫

我们将编写一个爬虫,先抓取药品列表页,再遍历进入每个药品详情页抓取结构化信息。

# file: spider/medicine_spider.py import requests from bs4 import BeautifulSoup import pandas as pd import time import random class MedicineSpider: def __init__(self): self.base_url = "http://example-medicine-site.com" # 请替换为实际目标或使用本地Mock self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } self.session = requests.Session() self.data_list = [] def parse_list_page(self, page_num): """解析药品列表页,获取药品详情页链接""" list_url = f"{self.base_url}/list?page={page_num}" try: resp = self.session.get(list_url, headers=self.headers, timeout=10) resp.raise_for_status() # 检查请求是否成功 soup = BeautifulSoup(resp.text, 'lxml') # 假设详情页链接在 class='medicine-item' 的 <a> 标签里 medicine_items = soup.find_all('a', class_='medicine-item') detail_urls = [item['href'] for item in medicine_items] return detail_urls except requests.RequestException as e: print(f"请求列表页 {list_url} 失败: {e}") return [] def parse_detail_page(self, detail_url): """解析药品详情页,提取结构化信息""" full_url = self.base_url + detail_url if detail_url.startswith('/') else detail_url try: resp = self.session.get(full_url, headers=self.headers, timeout=10) resp.raise_for_status() soup = BeautifulSoup(resp.text, 'lxml') # 根据实际网页结构定位元素,这里为示例 medicine_info = { 'name': self._extract_text(soup, 'h1.medicine-name'), 'generic_name': self._extract_text(soup, 'div.generic-name'), 'indications': self._extract_text(soup, 'section#indications'), 'side_effects': self._extract_text(soup, 'section#side-effects'), 'interactions': self._extract_text(soup, 'section#interactions'), 'source_url': full_url } # 简单的数据清洗:去除多余空白 for key, value in medicine_info.items(): if isinstance(value, str): medicine_info[key] = ' '.join(value.split()) return medicine_info except requests.RequestException as e: print(f"请求详情页 {full_url} 失败: {e}") return None def _extract_text(self, soup, selector): """辅助方法:根据CSS选择器提取文本,若找不到则返回空字符串""" element = soup.select_one(selector) return element.get_text(strip=True) if element else '' def run(self, start_page=1, end_page=3): """主运行方法""" print("开始爬取医药数据...") for page in range(start_page, end_page + 1): print(f"正在爬取第 {page} 页...") detail_urls = self.parse_list_page(page) for idx, url in enumerate(detail_urls): print(f" 处理药品 {idx + 1}/{len(detail_urls)}: {url}") info = self.parse_detail_page(url) if info: self.data_list.append(info) # 礼貌爬取,添加随机延迟 time.sleep(random.uniform(1, 3)) time.sleep(2) # 每页之间稍长延迟 # 保存数据到CSV if self.data_list: df = pd.DataFrame(self.data_list) df.to_csv('./data/medicine_data_raw.csv', index=False, encoding='utf-8-sig') print(f"数据爬取完成,共 {len(self.data_list)} 条,已保存至 medicine_data_raw.csv") else: print("未爬取到任何数据。") if __name__ == '__main__': spider = MedicineSpider() spider.run(start_page=1, end_page=2) # 演示只爬2页

3.3 数据清洗与初步处理

爬取到的原始数据通常包含噪声。我们需要进行清洗,为下一步的知识抽取做准备。

# file: data_cleaner.py import pandas as pd import re def clean_medicine_data(input_path, output_path): df = pd.read_csv(input_path, encoding='utf-8-sig') print(f"原始数据形状: {df.shape}") # 1. 去除完全空白的行 df.dropna(how='all', inplace=True) # 2. 填充部分空值 df.fillna('', inplace=True) # 3. 去除重复项(根据药品名称和通用名) df.drop_duplicates(subset=['name', 'generic_name'], keep='first', inplace=True) # 4. 文本清洗:去除特殊字符、多余空格等 text_columns = ['indications', 'side_effects', 'interactions'] for col in text_columns: df[col] = df[col].apply(lambda x: re.sub(r'\s+', ' ', str(x)).strip()) # 5. 标准化:例如,将适应症中的疾病名称统一(这里简化处理,实际可能需要词典) # df['indications'] = df['indications'].apply(standardize_disease_name) print(f"清洗后数据形状: {df.shape}") df.to_csv(output_path, index=False, encoding='utf-8-sig') print(f"清洗后的数据已保存至: {output_path}") return df # 执行清洗 if __name__ == '__main__': clean_df = clean_medicine_data('./data/medicine_data_raw.csv', './data/medicine_data_cleaned.csv')

4. 第二阶段:知识抽取、融合与 Neo4j 存储

有了清洗后的文本数据,我们需要从中抽取出结构化的“实体-关系-实体”三元组。

4.1 基于规则的知识抽取

对于毕业设计,使用基于规则或词典的方法是一个可行的起点。我们定义一些规则来从“适应症”等字段中抽取关系。

# file: knowledge_extractor.py import pandas as pd import jieba import jieba.posseg as pseg from collections import defaultdict # 加载自定义词典(提高实体识别准确率) jieba.load_userdict('./dict/disease_dict.txt') # 疾病词典 jieba.load_userdict('./dict/drug_dict.txt') # 药物词典 class RuleBasedExtractor: def __init__(self): self.disease_keywords = ['病', '症', '炎', '癌', '瘤', '感染'] # 疾病特征词 self.relation_patterns = { '治疗': ['用于治疗', '适用于', '主治', '治疗'], '引起': ['引起', '导致', '可能产生'], '相互作用': ['与.*合用', '不宜与.*同时使用'] } def extract_triplets_from_text(self, text, drug_name): """从一段文本中抽取与特定药物相关的三元组""" triplets = [] if not text: return triplets # 使用jieba进行分词和词性标注 words = pseg.cut(text) # 这里简化处理:寻找包含疾病关键词的词语作为疾病实体 # 更复杂的做法可以使用预训练的NER模型(如HanLP, LTP) potential_diseases = [] for word, flag in words: if any(keyword in word for keyword in self.disease_keywords) and len(word) > 1: potential_diseases.append(word) # 根据关系模式匹配 for relation, patterns in self.relation_patterns.items(): for pattern in patterns: # 简化匹配逻辑,实际应用可能需要更复杂的正则表达式 if pattern in text: for disease in set(potential_diseases): # 去重 triplets.append((drug_name, relation, disease)) return triplets def extract_from_dataframe(self, df): """从整个DataFrame中抽取三元组""" all_triplets = [] for _, row in df.iterrows(): drug_name = row['name'] # 从适应症中抽取“治疗”关系 indications_triplets = self.extract_triplets_from_text(row['indications'], drug_name) # 从副作用中抽取“引起”关系 side_effect_triplets = self.extract_triplets_from_text(row['side_effects'], drug_name) # 从相互作用中抽取“相互作用”关系 interaction_text = row['interactions'] # 假设相互作用文本中包含其他药物名称,这里需要另一个抽取器 # 为简化,我们只做演示 # interaction_triplets = extract_interaction(row['interactions'], drug_name) all_triplets.extend(indications_triplets) all_triplets.extend(side_effect_triplets) # all_triplets.extend(interaction_triplets) # 转换为DataFrame triplet_df = pd.DataFrame(all_triplets, columns=['head', 'relation', 'tail']) # 简单的知识融合:合并相同的实体(例如,“高血压”和“高血压病”) triplet_df['head'] = triplet_df['head'].apply(self._normalize_entity) triplet_df['tail'] = triplet_df['tail'].apply(self._normalize_entity) # 去除重复的三元组 triplet_df.drop_duplicates(inplace=True) return triplet_df def _normalize_entity(self, entity): """简单的实体归一化(示例)""" # 这里可以接入更复杂的标准化服务或词典 normalization_map = {'高血压病': '高血压', '高血脂症': '高血脂'} return normalization_map.get(entity, entity) if __name__ == '__main__': df = pd.read_csv('./data/medicine_data_cleaned.csv', encoding='utf-8-sig') extractor = RuleBasedExtractor() triplet_df = extractor.extract_from_dataframe(df) print(f"共抽取到 {len(triplet_df)} 个三元组") print(triplet_df.head()) triplet_df.to_csv('./data/knowledge_triplets.csv', index=False, encoding='utf-8-sig')

4.2 Neo4j 安装与启动

在将三元组存入图数据库前,我们需要安装并启动 Neo4j。

  1. 下载与安装:访问 Neo4j 官网下载中心,选择社区版,根据你的操作系统下载安装包或使用 Docker。
    • 桌面版:适合初学者,提供图形化界面。
    • 命令行版:更轻量。
    • Dockerdocker run -d --name neo4j -p 7474:7474 -p 7687:7687 -v neo4j_data:/data -v neo4j_logs:/logs -v neo4j_import:/var/lib/neo4j/import --env NEO4J_AUTH=neo4j/your_password neo4j:latest
  2. 启动与访问:启动服务后,在浏览器中打开http://localhost:7474,使用默认用户名neo4j和你在安装时设置的密码登录。
  3. 创建数据库:在 Neo4j Browser 中,你可以直接执行 Cypher 查询语言来操作图数据。

4.3 使用 Python 连接并导入数据到 Neo4j

我们将使用neo4j官方驱动,将上一步生成的三元组 CSV 文件导入到 Neo4j 中。

# file: neo4j_importer.py from neo4j import GraphDatabase import pandas as pd class Neo4jImporter: def __init__(self, uri, user, password): self.driver = GraphDatabase.driver(uri, auth=(user, password)) def close(self): self.driver.close() def create_constraints(self): """创建约束和索引以提高查询性能并保证数据唯一性""" with self.driver.session() as session: # 为药品实体创建唯一性约束 session.run("CREATE CONSTRAINT IF NOT EXISTS FOR (d:Drug) REQUIRE d.name IS UNIQUE") # 为疾病实体创建唯一性约束 session.run("CREATE CONSTRAINT IF NOT EXISTS FOR (s:Disease) REQUIRE s.name IS UNIQUE") print("约束创建完成。") def import_triplets_from_csv(self, csv_path): """从CSV文件导入三元组到Neo4j""" df = pd.read_csv(csv_path, encoding='utf-8-sig') with self.driver.session() as session: tx = session.begin_transaction() try: for _, row in df.iterrows(): head, relation, tail = row['head'], row['relation'], row['tail'] # 根据关系类型创建不同的边 if relation == '治疗': cypher = """ MERGE (d:Drug {name: $head}) MERGE (s:Disease {name: $tail}) MERGE (d)-[:TREATS]->(s) """ elif relation == '引起': cypher = """ MERGE (d:Drug {name: $head}) MERGE (s:Disease {name: $tail}) MERGE (d)-[:CAUSES]->(s) """ elif relation == '相互作用': # 假设相互作用是药物-药物关系 cypher = """ MERGE (d1:Drug {name: $head}) MERGE (d2:Drug {name: $tail}) MERGE (d1)-[:INTERACTS_WITH]->(d2) """ else: # 通用关系创建 cypher = """ MERGE (e1:Entity {name: $head}) MERGE (e2:Entity {name: $tail}) MERGE (e1)-[:RELATION {type: $relation}]->(e2) """ tx.run(cypher, head=head, tail=tail, relation=relation) tx.commit() print(f"成功导入 {len(df)} 条三元组到 Neo4j。") except Exception as e: tx.rollback() print(f"导入失败: {e}") raise if __name__ == '__main__': # 修改为你的 Neo4j 连接信息 URI = "bolt://localhost:7687" USER = "neo4j" PASSWORD = "your_password_here" # 务必修改! importer = Neo4jImporter(URI, USER, PASSWORD) try: importer.create_constraints() importer.import_triplets_from_csv('./data/knowledge_triplets.csv') finally: importer.close()

运行此脚本后,你的医药知识图谱就初步构建完成了。可以在 Neo4j Browser 中执行MATCH (n) RETURN n LIMIT 25来可视化图谱。

5. 第三阶段:构建智能问答系统后端(Spring Boot)

知识图谱建好后,我们需要构建一个应用来使用它。这里我们使用 Spring Boot 搭建后端,提供问答 API。

5.1 项目初始化与配置

使用 Spring Initializr 创建一个新项目,选择依赖:Web, Neo4j, Thymeleaf。

application.yml 配置

spring: neo4j: uri: bolt://localhost:7687 authentication: username: neo4j password: your_password_here thymeleaf: cache: false # 开发时关闭缓存 server: port: 8080

5.2 定义 Neo4j 实体与关系

// file: src/main/java/com/example/kgqa/entity/Drug.java package com.example.kgqa.entity; import org.springframework.data.neo4j.core.schema.*; import java.util.HashSet; import java.util.Set; @Node("Drug") public class Drug { @Id @GeneratedValue private Long id; @Property("name") private String name; @Relationship(type = "TREATS", direction = Relationship.Direction.OUTGOING) private Set<Disease> treatedDiseases = new HashSet<>(); @Relationship(type = "CAUSES", direction = Relationship.Direction.OUTGOING) private Set<Disease> causedSideEffects = new HashSet<>(); @Relationship(type = "INTERACTS_WITH", direction = Relationship.Direction.OUTGOING) private Set<Drug> interactingDrugs = new HashSet<>(); // 构造器、Getter和Setter省略... }
// file: src/main/java/com/example/kgqa/entity/Disease.java package com.example.kgqa.entity; import org.springframework.data.neo4j.core.schema.*; @Node("Disease") public class Disease { @Id @GeneratedValue private Long id; @Property("name") private String name; // 构造器、Getter和Setter省略... }

5.3 创建 Repository 层进行图查询

// file: src/main/java/com/example/kgqa/repository/DrugRepository.java package com.example.kgqa.repository; import com.example.kgqa.entity.Drug; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.data.repository.query.Param; import java.util.List; public interface DrugRepository extends Neo4jRepository<Drug, Long> { Drug findByName(String name); // 查询治疗某种疾病的所有药物 @Query("MATCH (d:Drug)-[:TREATS]->(dis:Disease {name: $diseaseName}) RETURN d") List<Drug> findDrugsTreatingDisease(@Param("diseaseName") String diseaseName); // 查询某种药物的所有副作用(导致的疾病) @Query("MATCH (d:Drug {name: $drugName})-[:CAUSES]->(dis:Disease) RETURN dis") List<Disease> findSideEffectsOfDrug(@Param("drugName") String drugName); // 查询与某种药物有相互作用的其他药物 @Query("MATCH (d1:Drug {name: $drugName})-[:INTERACTS_WITH]-(d2:Drug) RETURN d2") List<Drug> findInteractingDrugs(@Param("drugName") String drugName); }

5.4 实现简单的问答服务层

我们实现一个基于规则的问题理解模块,将自然语言问题解析为预设的 Cypher 查询模板。

// file: src/main/java/com/example/kgqa/service/QuestionService.java package com.example.kgqa.service; import com.example.kgqa.entity.Disease; import com.example.kgqa.entity.Drug; import com.example.kgqa.repository.DrugRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.*; import java.util.regex.Pattern; @Service public class QuestionService { @Autowired private DrugRepository drugRepository; /** * 解析问题并返回答案 * @param question 自然语言问题 * @return 答案字符串 */ public String answerQuestion(String question) { question = question.trim().toLowerCase(); // 规则1:治疗某病的药物有哪些? Pattern treatPattern = Pattern.compile(".*治疗.*?(.+?)的药.*|.*什么药.*治疗.*?(.+?)[??]?"); // 规则2:某药有什么副作用? Pattern sideEffectPattern = Pattern.compile(".*?(.+?)的副作用.*|.*什么副作用.*?(.+?)[??]?"); // 规则3:某药和什么药相互作用? Pattern interactPattern = Pattern.compile(".*?(.+?)和什么药.*相互作用.*|.*相互作用.*?(.+?)[??]?"); // 尝试匹配规则 java.util.regex.Matcher matcher; matcher = treatPattern.matcher(question); if (matcher.find()) { String diseaseName = matcher.group(1) != null ? matcher.group(1) : matcher.group(2); return answerTreatQuestion(diseaseName); } matcher = sideEffectPattern.matcher(question); if (matcher.find()) { String drugName = matcher.group(1) != null ? matcher.group(1) : matcher.group(2); return answerSideEffectQuestion(drugName); } matcher = interactPattern.matcher(question); if (matcher.find()) { String drugName = matcher.group(1) != null ? matcher.group(1) : matcher.group(2); return answerInteractQuestion(drugName); } return "抱歉,我暂时无法理解您的问题。请尝试询问关于药物治疗、副作用或相互作用的信息。"; } private String answerTreatQuestion(String diseaseName) { List<Drug> drugs = drugRepository.findDrugsTreatingDisease(diseaseName); if (drugs.isEmpty()) { return String.format("知识图谱中暂无治疗【%s】的药物信息。", diseaseName); } StringBuilder sb = new StringBuilder(); sb.append("治疗【").append(diseaseName).append("】的药物有:"); drugs.forEach(drug -> sb.append(drug.getName()).append(";")); return sb.toString(); } private String answerSideEffectQuestion(String drugName) { Drug drug = drugRepository.findByName(drugName); if (drug == null) { return String.format("知识图谱中暂无药物【%s】的信息。", drugName); } List<Disease> sideEffects = drugRepository.findSideEffectsOfDrug(drugName); if (sideEffects.isEmpty()) { return String.format("药物【%s】在知识图谱中记录的副作用信息为空。", drugName); } StringBuilder sb = new StringBuilder(); sb.append("药物【").append(drugName).append("】可能引起的副作用包括:"); sideEffects.forEach(disease -> sb.append(disease.getName()).append(";")); return sb.toString(); } private String answerInteractQuestion(String drugName) { Drug drug = drugRepository.findByName(drugName); if (drug == null) { return String.format("知识图谱中暂无药物【%s】的信息。", drugName); } List<Drug> interactingDrugs = drugRepository.findInteractingDrugs(drugName); if (interactingDrugs.isEmpty()) { return String.format("药物【%s】在知识图谱中暂无相互作用药物记录。", drugName); } StringBuilder sb = new StringBuilder(); sb.append("药物【").append(drugName).append("】已知与以下药物存在相互作用:"); interactingDrugs.forEach(d -> sb.append(d.getName()).append(";")); return sb.toString(); } }

5.5 创建控制器和简单前端

// file: src/main/java/com/example/kgqa/controller/QAController.java package com.example.kgqa.controller; import com.example.kgqa.service.QuestionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; @Controller public class QAController { @Autowired private QuestionService questionService; @GetMapping("/") public String index(Model model) { model.addAttribute("answer", ""); model.addAttribute("question", ""); return "index"; } @PostMapping("/ask") public String askQuestion(@RequestParam String question, Model model) { String answer = questionService.answerQuestion(question); model.addAttribute("answer", answer); model.addAttribute("question", question); return "index"; } }
<!-- file: src/main/resources/templates/index.html --> <!DOCTYPE html> <html lang="zh-CN" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>医药知识图谱问答系统</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body class="container mt-5"> <h1 class="mb-4">💊 医药知识图谱智能问答系统</h1> <p class="lead">基于Neo4j图数据库构建,尝试询问药品与疾病的关系。</p> <form th:action="@{/ask}" method="post" class="mb-4"> <div class="input-group"> <input type="text" class="form-control" name="question" th:value="${question}" placeholder="例如:治疗高血压的药物有哪些? 或 阿司匹林有什么副作用?" required> <button class="btn btn-primary" type="submit">提问</button> </div> </form> <div th:if="${question}" class="alert alert-secondary"> <strong>你的问题:</strong> <span th:text="${question}"></span> </div> <div th:if="${answer}" class="alert alert-info"> <strong>系统回答:</strong> <span th:text="${answer}"></span> </div> <hr> <p class="text-muted">示例问题:</p> <ul> <li>治疗感冒的药有哪些?</li> <li>青霉素有什么副作用?</li> <li>布洛芬和什么药相互作用?</li> </ul> </body> </html>

启动 Spring Boot 应用 (KgqaApplication),访问http://localhost:8080即可与你的医药知识图谱问答系统交互。

6. 常见问题与排查思路

在实现上述流程时,你可能会遇到一些典型问题。

问题现象可能原因排查思路与解决方案
爬虫无法获取数据或被封IP1. 网站有反爬机制(如验证码、频率限制)。
2. 请求头User-Agent被识别。
3. 页面结构已更新。
1. 添加更完整的请求头(如Referer,Accept-Language)。
2. 使用代理IP池(需谨慎合法使用)。
3. 显著降低请求频率,添加随机延迟。
4. 检查并更新HTML解析规则。
Neo4j 连接失败1. Neo4j 服务未启动。
2. 连接地址、端口、用户名或密码错误。
3. 防火墙阻止了 Bolt 端口(7687)。
1. 运行neo4j status检查服务状态。
2. 确认application.yml或 Python 脚本中的连接信息正确。
3. 尝试在浏览器访问http://localhost:7474确认服务可用。
4. 检查防火墙设置,确保端口开放。
知识抽取准确率低1. 规则过于简单,无法覆盖复杂句式。
2. 分词或实体识别不准确。
3. 领域词典不全。
1. 考虑使用预训练的中文NER模型(如 HanLP, LTP)替代规则。
2. 扩充自定义词典(疾病、药物名)。
3. 引入依存句法分析来更准确地识别关系。
4. 对于毕业设计,可以手动标注少量数据,结合规则提升效果。
问答系统无法理解问题1. 问题解析规则有限。
2. 用户问题与规则模式不匹配。
3. 问题中的实体未在知识图谱中。
1. 增加更多问题匹配模式(正则表达式)。
2. 引入意图识别和实体识别模块,可以使用 Rasa 或飞桨PaddleNLP 等框架。
3. 在回答前,先检查实体是否存在,给出友好提示。
Spring Boot 无法注入 Neo4j Repository1. 主应用类未添加@EnableNeo4jRepositories
2. 包扫描路径不正确。
1. 在主应用类上添加@EnableNeo4jRepositories注解。
2. 确保@SpringBootApplication能扫描到 Repository 所在的包。

7. 项目优化与扩展建议

完成基础版本后,你可以从以下方向进行深化和扩展,这会让你的毕设更具亮点。

7.1 数据与知识层面

  • 多源数据融合:不仅爬取一个网站,可以整合多个公开的医药数据集(如中医药方、药品说明书库),进行实体对齐和冲突解决。
  • 引入更先进的抽取模型:使用 BERT、ERNIE 等预训练模型进行命名实体识别和关系联合抽取,大幅提升自动化程度和准确率。
  • 构建属性图谱:不仅存储关系,还为实体添加属性(如药品的化学式、生产厂商、疾病的高发人群等)。

7.2 系统架构层面

  • 前后端分离:将 Spring Boot 仅作为后端 API 服务,使用 Vue.js 或 React 构建更美观、交互性更强的独立前端。
  • 引入缓存:对于热门查询,使用 Redis 缓存 Cypher 查询结果,提升系统响应速度。
  • 容器化部署:使用 Docker Compose 将 Neo4j、Spring Boot 后端、前端应用打包,实现一键部署。

7.3 问答能力层面

  • 语义解析升级:抛弃简单的规则匹配,采用语义解析技术,将自然语言问题直接转换为规范的 Cypher 查询。
  • 支持多跳查询:实现诸如“治疗A病的药物,会引起哪些副作用?”这类涉及多步关系的复杂问答。
  • 答案生成与摘要:不仅返回实体列表,还能生成连贯的文本摘要作为答案。

7.4 工程化与展示

  • 增加图谱可视化:集成 ECharts 或 G6 等图表库,在Web页面上动态展示查询涉及的子图。
  • 实现用户登录与问答历史:增加用户系统,记录用户的提问历史。
  • 编写完整的项目文档:包括需求分析、系统设计、核心算法、部署手册等,这是毕设答辩的重要材料。

通过这个从数据采集到智能问答的全流程实战,你不仅完成了一个完整的毕业设计项目,更深入理解了知识图谱技术的核心环节。这个项目具有很强的可扩展性,你可以根据自己的兴趣和时间,选择上述任何一个方向进行深化。动手开始搭建吧,每一步的实践都会让你对知识图谱有更深刻的认识。如果在实现过程中遇到具体问题,欢迎在评论区交流探讨。

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

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

立即咨询