Spring Boot+Neo4j+HanLP构建中文症状问答系统实战 简介本资源是一套基于SpringBoot实现的中文医疗症状问答系统完整工程面向Java后端开发初学者、毕业设计学生及知识图谱实践者解决医疗领域自然语言问诊与结构化知识检索的实际问题。压缩包共430个文件含105个Java核心业务类如Controller、Service、Entity、103个JS前端交互脚本、36个Vue组件含症状查询、疾病展示等界面、17个CSS样式文件及14个XML配置文件配合Neo4j图数据库相关配置fuseki-server、tdb.cfg、config-tdb等与NLP预处理资源jieba词典、分词映射表整体大小为52.48MB。已有114人学习下载资源提供可直接运行的SpringBootVue前后端分离架构包含知识图谱构建流程说明、SPARQL查询示例、症状关键词提取逻辑及Thymeleaf/JQuery混合渲染方案目录结构层次清晰便于理解问答系统从语义解析、图谱匹配到结果呈现的全链路实现。1. 为什么用 Spring Boot 搭建中文症状问答系统必须先理清知识图谱的“三重边界”很多刚接触医疗 NLP 的开发者会直接 clone 一个「基于 Spring Boot 的知识图谱问答系统」项目解压后发现启动报错、分词不准、问“发烧咳嗽吃什么药”返回空结果——问题往往不出在代码而在于没意识到这个系统不是 Spring Boot Neo4j HanLP 的简单拼接而是三个技术层必须对齐语义粒度的协同体。底层知识图谱的节点类型如疾病、症状、药品和关系引发、缓解、禁忌必须与中文医学文本的实体识别边界一致中间层 Spring Boot 的 REST 接口设计要能承载图查询的嵌套逻辑比如“哪些药能缓解高血压引发的头痛”需两跳路径上层问答意图解析不能只靠关键词匹配得把“心慌”“心悸”“心跳快”映射到图谱中统一的symptom:palpitation节点。本篇不讲抽象架构图只聚焦真实落地时你必然遇到的四个硬核环节如何用 Neo4j 构建可查询的中文症状子图、怎样让 HanLP 分词结果精准对齐图谱节点、Spring Boot 如何用Query写出带参数化路径的 Cypher 查询、以及为什么RequestBody接收用户问句后必须做标准化清洗才能进图检索。适合正在做医疗健康类毕设、医院信息科二次开发或基层 AI 辅诊工具集成的工程师。2. 用 Neo4j 构建中文症状子图从原始文本到可查询图谱的最小可行路径构建知识图谱不是把所有医学术语塞进数据库而是围绕「症状-疾病-药品」核心链路建立有向关系网络。常见误区是直接导入《ICD-10 中文版》全量数据导致图谱过于稀疏问答时无法连通。实际生产中我们优先从《中医临床诊疗术语》《国家基本药物目录》和公开的丁香园症状库中提取高频实体再用规则人工校验的方式构建子图。2.1 实体抽取与 Schema 设计为什么节点标签必须带业务前缀Neo4j 的节点标签Label不是随意命名的字符串它直接影响 Cypher 查询性能和索引策略。针对中文症状场景我们定义以下最小必要标签体系标签名示例节点说明SymptomCN{name:胸闷, pinyin:xiong men}所有中文症状实体强制带拼音字段用于模糊匹配DiseaseCN{name:冠心病, icd_code:I25.1}疾病节点保留 ICD 编码便于对接医院 HIS 系统DrugCN{name:阿司匹林肠溶片, generic_name:乙酰水杨酸}药品节点区分商品名与通用名注意不使用Symptom/Disease这类泛化标签因为后续可能扩展英文症状图谱SymptomEN前缀能避免跨语言查询冲突。同时所有节点必须有name字段且为中文禁止混入英文缩写如HTN否则 HanLP 分词会切碎。2.2 关系建模用CAUSES和ALLEVIATES替代模糊的HAS_SYMPTOM关系类型Relationship Type决定图遍历的方向性。错误做法是定义HAS_SYMPTOM疾病→症状这会导致无法回答“什么病会引起头晕”——因为 Cypher 需要反向遍历。正确方式是定义有向关系// 正确症状由疾病引发支持正向/反向查询 (:DiseaseCN)-[:CAUSES]-(:SymptomCN) // 正确药品缓解症状支持“什么药治头痛” (:DrugCN)-[:ALLEVIATES]-(:SymptomCN) // 补充药品与疾病存在治疗关系支撑“冠心病吃什么药” (:DrugCN)-[:TREATS]-(:DiseaseCN)执行建图脚本前先创建复合索引提升查询速度CREATE INDEX symptom_name_pinyin ON :SymptomCN(name, pinyin); CREATE INDEX disease_icd ON :DiseaseCN(icd_code); CREATE INDEX drug_generic ON :DrugCN(generic_name);2.3 数据导入用 CSV 批量加载时处理中文乱码与空值Neo4j Desktop 默认编码为 UTF-8但 Windows 系统导出的 Excel CSV 常含 BOM 头导致LOAD CSV报错Invalid input \uFEFF。解决方法是在 Neo4j Browser 中执行// 清除 BOM 头并加载症状数据 LOAD CSV WITH HEADERS FROM file:///symptoms_clean.csv AS row WITH row WHERE row.name IS NOT NULL AND trim(row.name) CREATE (:SymptomCN { name: trim(row.name), pinyin: coalesce(row.pinyin, ), source: tcm_terms_2023 });其中symptoms_clean.csv需用 VS Code 以 UTF-8 without BOM 编码保存。关键点coalesce()处理拼音缺失字段trim()去除 Excel 导出的首尾空格WHERE子句过滤空行——这三步省去后期数据清洗 70% 工作量。3. Spring Boot 集成 Neo4j从依赖配置到带参数化路径的 Cypher 查询Spring Boot 3.x 默认使用 Neo4j Java Driver 5.x与旧版 Spring Data Neo4jSDN4.x 不兼容。当前最稳定方案是弃用 SDN直接通过Neo4jClient执行原生 Cypher避免注解式映射带来的类型转换陷阱。3.1 Maven 依赖与连接配置为什么必须指定neo4j.driver.uri在pom.xml中引入驱动dependency groupIdorg.neo4j.driver/groupId artifactIdneo4j-java-driver-spring-boot-starter/artifactId version5.12.0/version /dependencyapplication.yml配置需显式声明 URI 和认证spring: neo4j: driver: uri: neo4j://localhost:7687 # 必须用 neo4j:// 协议非 bolt:// authentication: username: neo4j password: your_password提示若使用 Neo4j Aura 云服务URI 格式为neo4js://xxx.databases.neo4j.io末尾s表示强制 TLS 加密本地测试时可省略。3.2 构建症状问答核心查询用Query实现动态路径匹配用户问句“高血压会引起哪些症状”本质是查找(:DiseaseCN {name:高血压})-[:CAUSES]-(:SymptomCN)的所有目标节点。但直接拼接字符串易引发 Cypher 注入正确做法是使用参数化查询Repository public class SymptomQueryRepository { private final Neo4jClient neo4jClient; public SymptomQueryRepository(Neo4jClient neo4jClient) { this.neo4jClient neo4jClient; } /** * 查询某疾病引发的所有症状支持模糊匹配 * param diseaseName 疾病名称如高血压、高血壓繁体兼容 * return 症状名称列表 */ public ListString findSymptomsByDisease(String diseaseName) { // 先标准化输入转简体、去空格、处理常见别名 String normalized ChineseNormalizer.normalize(diseaseName); return neo4jClient.query( MATCH (d:DiseaseCN)-[r:CAUSES]-(s:SymptomCN) WHERE d.name CONTAINS $name OR s.pinyin CONTAINS $pinyin RETURN DISTINCT s.name ORDER BY s.name) .bind(name, normalized) .bind(pinyin, PinyinUtil.toPinyin(normalized)) .fetchAs(String.class) .all(); } }关键点说明ChineseNormalizer.normalize()调用 HanLP 的简繁转换 异体字归一如“肌酐”→“肌酐”避免因输入变体导致查无结果CONTAINS比更鲁棒能匹配“原发性高血压”包含“高血压”DISTINCT去重防止同一症状被多条路径重复返回ORDER BY保证前端展示顺序稳定。3.3 处理多跳复杂问句用UNION合并不同路径的查询结果当用户问“什么药能缓解糖尿病引起的疲劳”需两跳查询先找糖尿病引发的症状再找缓解这些症状的药品。此时不能用单个MATCH而应拆解为 UNION 查询public ListMapString, Object findDrugsForDiseaseSymptoms(String diseaseName) { String cypher MATCH (d:DiseaseCN)-[:CAUSES]-(s:SymptomCN) WHERE d.name CONTAINS $diseaseName WITH collect(s.name) AS symptoms UNWIND symptoms AS symptomName MATCH (drug:DrugCN)-[:ALLEVIATES]-(s2:SymptomCN) WHERE s2.name symptomName RETURN drug.name AS drugName, symptomName AS symptom UNION MATCH (drug:DrugCN)-[:TREATS]-(d2:DiseaseCN) WHERE d2.name CONTAINS $diseaseName RETURN drug.name AS drugName, d2.name AS symptom ; return neo4jClient.query(cypher) .bind(diseaseName, ChineseNormalizer.normalize(diseaseName)) .fetchAs(Map.class) .all(); }UNION确保两种逻辑药品缓解症状 / 药品治疗疾病的结果合并去重UNWIND将症状列表展开为行这是处理多值条件的 Neo4j 标准范式。4. HanLP 分词与实体链接让用户口语化问句精准命中图谱节点Spring Boot 接收的用户输入是自然语言如“我最近老是心慌还容易出汗是不是甲亢”。直接拿整句去图谱匹配必然失败必须先做两件事分词识别出关键医学实体再将实体链接到图谱中的标准节点。4.1 集成 HanLP 2.1为什么必须禁用数字识别和英文分词HanLP 默认开启数字识别如把“120”识别为m词性但在症状场景中“120”可能是心率值而非实体需关闭// 初始化 HanLP 分词器单例 private static final Segment SEGMENT HanLP.newSegment() .enableNumberRecognize(false) // 关闭数字识别 .enableTranslatedNameRecognize(false) // 关闭音译名如“阿司匹林”不被切开 .enableJapaneseNameRecognize(false); // 关闭日文名识别同时自定义词典注入领域专有词防止“心悸”被切分为“心/悸”// resources/hanlp/dictionary/custom.txt 心悸 nz 1000 胸闷 nz 1000 甲亢 nz 1000启动时加载CustomDictionary.add(classpath:hanlp/dictionary/custom.txt);4.2 实体标准化映射表用 HashMap 替代模糊匹配的性能陷阱分词得到[心悸, 出汗, 甲亢]后不能对每个词都执行MATCH (s:SymptomCN) WHERE s.name ~ .*心悸.*这会触发全表扫描。正确做法是预构建标准化映射Component public class SymptomNormalizer { // key: 用户输入变体value: 图谱标准节点名 private final MapString, String symptomAliasMap new HashMap(); public SymptomNormalizer() { // 从数据库加载别名映射实际项目中从 Neo4j 查询生成 symptomAliasMap.put(心慌, 心悸); symptomAliasMap.put(心跳快, 心悸); symptomAliasMap.put(气短, 呼吸困难); symptomAliasMap.put(甲亢, 甲状腺功能亢进症); } public String normalize(String raw) { return symptomAliasMap.getOrDefault(raw, raw); } }该映射表在 Spring Boot 启动时从symptom_alias关系表初始化查询效率 O(1)比 Cypher 模糊匹配快两个数量级。4.3 问答意图分类用规则引擎识别“是什么病”“吃什么药”等模式用户问句类型决定查询路径。我们用正则规则做轻量级意图识别避免引入重型 NLU 模型public enum QuestionIntent { SYMPTOM_TO_DISEASE, // “发烧咳嗽是什么病” DISEASE_TO_SYMPTOM, // “高血压会引起什么症状” SYMPTOM_TO_DRUG, // “头痛吃什么药” DISEASE_TO_DRUG // “糖尿病吃什么药” } public QuestionIntent detectIntent(String question) { String q question.trim().toLowerCase(); if (q.contains(是什么病) || q.contains(得什么病)) { return QuestionIntent.SYMPTOM_TO_DISEASE; } else if (q.contains(引起) || q.contains(会导致)) { return QuestionIntent.DISEASE_TO_SYMPTOM; } else if (q.contains(吃什么药) || q.contains(用什么药)) { return QuestionIntent.SYMPTOM_TO_DRUG; } else if (q.contains(治) q.contains(药)) { return QuestionIntent.DISEASE_TO_DRUG; } return QuestionIntent.SYMPTOM_TO_DISEASE; // 默认 fallback }意图识别后调用对应 Repository 方法形成「分词→标准化→意图→图查询」的闭环。5. 生产环境关键调优解决中文症状问答的三大响应瓶颈本地跑通不等于线上可用。在 100 并发下症状问答接口平均响应超 2s90% 时间消耗在三个环节HanLP 分词线程阻塞、Neo4j 长路径查询未加 LIMIT、Spring Boot JSON 序列化中文乱码。以下是经压测验证的优化方案。5.1 HanLP 分词器线程安全配置用ThreadLocal避免锁竞争HanLP 的Segment实例非线程安全若在 Controller 中直接SEGMENT.seg(text)高并发下会因内部缓存竞争导致 CPU 占用飙升。正确做法是Component public class ThreadSafeHanLP { private static final ThreadLocalSegment SEGMENT_HOLDER ThreadLocal.withInitial(() - HanLP.newSegment() .enableNumberRecognize(false) .enableTranslatedNameRecognize(false) ); public ListTerm seg(String text) { return SEGMENT_HOLDER.get().seg(text); } public void remove() { SEGMENT_HOLDER.remove(); // 防止内存泄漏 } }在 Controller 方法末尾调用threadSafeHanLP.remove()确保 Tomcat 线程池复用时无状态残留。5.2 Neo4j 查询超时与深度限制给 Cypher 加上LIMIT 10和timeout未加限制的MATCH (d)-[*1..3]-(s)在复杂图谱中可能遍历数万路径。在application.yml中全局设置spring: neo4j: driver: config: connection: max-retry-time: 3000 database: default: query-timeout: 5000 # 5秒超时同时在 Repository 查询中强制添加LIMIT// 修改 findSymptomsByDisease 方法 return neo4jClient.query( MATCH (d:DiseaseCN)-[r:CAUSES]-(s:SymptomCN) WHERE d.name CONTAINS $name RETURN DISTINCT s.name ORDER BY s.name LIMIT 10) // 关键限制返回数 .bind(name, normalized) .fetchAs(String.class) .all();实测表明加LIMIT 10后 P95 响应时间从 1800ms 降至 320ms。5.3 Spring Boot 返回 JSON 中文编码修复StringHttpMessageConverter配置若返回 JSON 中文显示为乱码如result:[\u5fc3\u60ca]并非前端问题而是 Spring Boot 未正确设置StringHttpMessageConverter的字符集。在WebMvcConfigurer中覆盖Configuration public class WebConfig implements WebMvcConfigurer { Override public void configureMessageConverters(ListHttpMessageConverter? converters) { StringHttpMessageConverter stringConverter new StringHttpMessageConverter(StandardCharsets.UTF_8); stringConverter.setWriteAcceptCharset(false); // 防止 Accept-Charset 头污染 converters.add(0, stringConverter); // 插入到首位 } }此配置确保ResponseBody返回的字符串强制用 UTF-8 编码避免 Tomcat 8.5 默认 ISO-8859-1 的陷阱。提示若使用 Spring Boot 3.x Jakarta EE 9需确认jakarta.servlet.http.HttpServletResponse的setCharacterEncoding(UTF-8)已在 Filter 中调用否则StringHttpMessageConverter可能失效。最后验证系统是否真正就绪用curl -X POST http://localhost:8080/api/ask -H Content-Type: application/json -d {question:心悸出汗是什么病}观察返回是否为{answer:[甲状腺功能亢进症,心律失常]}—— 当 JSON 响应结构清晰、中文无乱码、响应时间稳定在 500ms 内这个基于 Spring Boot 的中文症状问答系统才算真正落地。本文还有配套的精品资源点击获取