简介:一套面向高校毕业设计与课程作业的智能问答系统项目,基于Neo4j图数据库构建知识图谱,融合自然语言处理、实体识别、语义匹配与答案检索,解决从问题解析到知识查询的核心链路。压缩包共74个文件,大小仅1.37MB,以Java源码、txt说明与csv数据为主,辅以xml/properties配置、模块工程文件、图片及Markdown/readme文档,能够覆盖系统搭建、数据导入和查询实现等主要环节。已有223人学习。对准备毕设或课设的读者,可通过源码把握系统分层与Neo4j查询方式,借助说明文档理解实体关系建模、问句解析、答案召回与排序机制;该工程也可作为二次开发框架,通过替换知识数据扩展到其他问答场景。完整的工程目录与配置文件便于快速导入环境,适合用于选题参考、代码讲解和答辩准备。
1. 基于Neo4j的智能问答系统:这到底是一份能跑的毕设,还是一个半成品
毕设和课程作业里,最容易被答辩老师追问的一句话就是“你系统的智能体现在哪”。这套基于Neo4j的智能问答系统,是一个能直接跑通的人工智能方向项目:后端用 Java 搭配 Spring Boot,数据层用图数据库 Neo4j,问答链路覆盖了中文问题解析、知识图谱查询和答案组装三块硬内容。我拆完这份资源后最直观的感受是,它不是把几个第三方接口拼在一起的演示壳子,而是从数据建模到查询生成都自己控制的闭环系统。适合正在选毕设题目的本科生,也适合想快速搭一个图谱问答 Demo 的开发者。本文从工程文件入手,把项目结构、NLP 解析、查询生成和常踩的坑逐一拆开讲。
2. 工程结构拆解与图谱建模:五个模块、三类节点和一组关系
2.1 压缩包里到底有什么:从 pom.xml 反推工程骨架
拿到压缩包第一件事不是跑mvn spring-boot:run,而是先看目录结构。解压后能看到pom.xml、src、test、main以及Spring-Neo4j-Movie.iml、Answer.iml这类 IDE 模块文件,还有readme和spark命名相关的文件。从这个布局能推断出它是一个标准 Maven 多模块工程,核心模块一个是图谱数据模块,另一个是问答处理模块。
先打开pom.xml,依赖清单基本决定了整个项目的技术边界。这里贴一段我当时整理的关键依赖结构:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-neo4j</artifactId> </dependency> <dependency> <groupId>org.neo4j</groupId> <artifactId>neo4j-java-driver</artifactId> </dependency> <dependency> <groupId>com.hankcs</groupId> <artifactId>hanlp</artifactId> <version>由pom锁定版本为准</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>逻辑说明:这份依赖清单里最关键的是spring-data-neo4j和neo4j-java-driver这一对,前者提供 Spring Data 风格的 Repository 访问方式,后者是底层驱动。HanLP 负责中文分词和词典匹配,让系统能处理中文问答而不是只能查英文。Lombok 只是减少样板代码,去掉也能跑。
参数说明:spring-data-neo4j和neo4j-java-driver的版本必须兼容,这一点在避坑章节会单独展开。spring-boot-starter-web是必须的,因为问答系统最终要通过 HTTP 接口对外提供服务,答辩演示时用 Postman 或浏览器就能调用。
2.2 为什么选 Neo4j 而不是 MySQL:关系查询的深度优势
做知识图谱问答,核心操作是按关系跳转。比如“某演员合作过哪些导演”,在 MySQL 里要 join 三四张表,在图数据库里只需要沿着关系遍历两跳。这种场景下 Neo4j 的查询逻辑和问题意图几乎一一对应,写出来的 Cypher 本身就是一种可解释的语义表达。
对比一下两种存储方案:
| 对比项 | MySQL | Neo4j |
|---|---|---|
| 关系查询 | 多表 JOIN,层级越深 SQL 越复杂 | 模式匹配,一条 MATCH 走完 |
| Schema 变化 | 加关系类型需要改表结构 | 随时新增关系类型 |
| 查询可读性 | 业务含义隐藏在 JOIN 条件里 | Cypher 基本等同于语义描述 |
| 适合场景 | 强事务、固定结构业务 | 关系密集型、结构多变的图谱 |
选 Neo4j 的另一个实际原因是演示效果好。答辩时在 Neo4j Browser 里执行一条MATCH (p:Person)-[:ACTED_IN]->(m:Movie),可视化界面直接画出图谱节点和连线,比对着数据表讲“关系”直观太多。这份资源的知识图谱就是围绕电影领域构建的,节点和关系非常适合做可视化演示。
2.3 数据模型长什么样:节点、关系与字段设计
电影领域图谱一般包含三类节点:电影节点、演员/导演人物节点、类型标签节点。关系主要是“出演”、“导演”、“属于某类型”这几类。打开src/main/resources下的导入脚本,能看到类似下面的 Cypher 建模语句:
CREATE CONSTRAINT movie_title IF NOT EXISTS FOR (m:Movie) REQUIRE m.title IS UNIQUE; CREATE CONSTRAINT person_name IF NOT EXISTS FOR (p:Person) REQUIRE p.name IS UNIQUE; CREATE (m:Movie {title: '某电影', released: 2024, rating: 8.5}) CREATE (p:Person {name: '某演员', born: 1980}) CREATE (p)-[:ACTED_IN {role: '主角'}]->(m);逻辑说明:第一条语句给电影 title 创建唯一约束,第二条给人物 name 创建唯一约束。唯一约束在问答系统里是实体识别的基础——用户输入的名字只有能精确匹配到图谱节点,后续查询才有意义。第三条和第四条创建示例节点和关系,role属性存的是演员在电影里的角色名,这个属性在回答“某演员在电影里演谁”这类问题时会被查出来。
参数说明:released存上映年份,rating存评分,属性设计要贴合问答维度。项目自带的导入脚本通常还会用LOAD CSV批量导入数据。spark命名的文件在这里大概率是一个批处理导入脚本的代号,和 Spark 大数据框架没什么关系,只做单机演示的话直接用 Neo4j Browser 执行导入脚本就行。
3. 中文问题解析:意图识别、实体抽取与 Cypher 模板的映射关系
3.1 问答流程先立起来:从问题输入到答案输出的四个阶段
一个中文问句从进来到返回答案,中间要经历四个阶段:先做意图识别,判断用户在问什么类型的问题;再做实体抽取,找到问题里的电影名或人名;然后依据意图把抽取结果填充进对应的 Cypher 模板;最后执行查询并把图数据组装成自然语言句子。
这个项目没有训练深度学习模型,而是用规则模板加词典的方式实现。对毕设场景来说这反而是优点:逻辑透明,答辩时能讲清楚每一步在做什么,而且不依赖 GPU 训练环境。分词和实体识别用的是 HanLP 的词典分词能力,再配合图谱节点名做二次对齐。
3.2 意图识别:规则模板比训练模型更可控
意图识别是整个问答系统的入口,意图分错了后面全错。这份资源把常见问题归成几类:查演员演过哪些电影、查电影的主演阵容、查电影的导演、查合作演员、查上映时间和评分。每一类意图对应一组触发词和一种查询模板。
这里给出一个简化版的意图识别代码片段,核心思路是关键词命中:
public QaIntent classifyIntent(String question) { if (question.contains("演过") && question.contains("哪些")) { return QaIntent.ACTOR_FILMS; } if (question.contains("主演") || question.contains("演员")) { return QaIntent.FILM_ACTORS; } if (question.contains("导演")) { return QaIntent.FILM_DIRECTOR; } if (question.contains("合作")) { return QaIntent.COOPERATION; } return QaIntent.UNKNOWN; }逻辑说明:classifyIntent接收用户原始问题,通过关键词命中判断意图类型。比如“某演员演过哪些电影”会命中第一个条件返回ACTOR_FILMS,“某电影的导演是谁”命中FILM_DIRECTOR。这种实现方式不依赖训练数据,新增一种问题类型只需要加一个contains判断和对应的模板,扩展成本很低。
参数说明:关键词顺序有讲究,要把更具体的判断放前面。比如“合作”和“主演”可能同时出现在一句话里,实际项目里通常会先判断更具体的意图。还有一个细节:不同意图后面要接不同的 Cypher 模板,所以QaIntent枚举值会直接作为模板索引使用。
3.3 实体识别与 Cypher 生成:用字典匹配和对齐图谱节点
意图定了之后,要从问题里抽出实体名。常见做法是先分词,再用图谱中已有的节点名做最大匹配对齐。这里有一个容易忽略的点:分词结果不一定准,所以很多实现会把“分词后抽取到的候选词”和“图谱中的节点属性值”做交集匹配,保证抽出来的实体在图谱里真实存在。
public String extractEntity(String question, List<String> nodeNames) { List<String> terms = HanLP.segment(question) .stream() .map(term -> term.word) .collect(Collectors.toList()); for (String name : nodeNames) { if (question.contains(name)) { return name; } for (String term : terms) { if (name.contains(term) && term.length() >= 2) { return name; } } } return null; }逻辑说明:第一步先分词,第二步遍历图谱节点名做包含匹配。第二个循环里的name.contains(term)处理的是“图谱节点名比分词词元长”的情况,比如用户说“某演员”,分词可能切出“某”和“演员”,而图谱节点名是完整的人名,这时用包含关系对齐。term.length() >= 2是为了过滤掉单字噪声,避免一个“影”字就命中节点。
参数说明:nodeNames列表需要在系统启动时从 Neo4j 里批量加载一次,缓存到内存里,不用每次查询都访问数据库。如果图谱数据量不大,几百个节点全量加载完全没压力。
实体抽出来之后,根据意图查模板表生成 Cypher:
| 意图 | 抽取实体 | 生成 Cypher 模板 | | -------------- | -------- | --------------------------------------------------------------------- | | ACTOR_FILMS | person | MATCH (p:Person {name:$person})-[:ACTED_IN]->(m:Movie) RETURN m.title | | FILM_ACTORS | movie | MATCH (m:Movie {title:$movie})<-[:ACTED_IN]-(p:Person) RETURN p.name | | FILM_DIRECTOR | movie | MATCH (m:Movie {title:$movie})<-[:DIRECTED]-(p:Person) RETURN p.name | | COOPERATION | person | MATCH (p1:Person {name:$person})-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person) RETURN p2.name, m.title |参数填充不能用字符串拼接,要用参数化查询,直接把$person、$movie占位符对应的值传给 Neo4j 驱动。这样做既能防止输入里的特殊字符破坏查询语句,也让 Cypher 执行计划可以被缓存复用,性能更好。
4. 问答核心服务实现:把“问题”变成“带参数的 Cypher 查询”
4.1 用 Spring Boot 把链路串成 REST 接口
前面几块逻辑最终要串成一个对外可调用的服务。工程里典型的做法是分三层:Controller 接收 HTTP 请求,Service 编排问答流程,Repository 负责 Neo4j 数据访问。这里给出一个问答接口的典型实现:
@RestController @RequestMapping("/api/qa") public class QaController { private final QaService qaService; public QaController(QaService qaService) { this.qaService = qaService; } @PostMapping("/ask") public QaResponse ask(@RequestBody QaRequest request) { return qaService.answer(request.getQuestion()); } }@Service public class QaService { private final Neo4jTemplate neo4jTemplate; public QaResponse answer(String question) { QaIntent intent = classifyIntent(question); if (intent == QaIntent.UNKNOWN) { return QaResponse.fail("暂时只能回答电影、演员、导演相关的问题"); } String entity = extractEntity(question, cachedNodeNames); if (entity == null) { return QaResponse.fail("没有在知识图谱中找到相关实体"); } String cypher = buildCypher(intent, entity); List<Map<String, Object>> result = neo4jTemplate.query(cypher, entity); return assembleAnswer(intent, result); } }逻辑说明:answer方法把第 3 章的意图识别、实体抽取、模板生成按顺序编排起来。每一步都有兜底返回:意图不识别、实体没找到、查询结果为空,分别返回不同的提示语。这在答辩演示时很关键,老师随便问一句领域外的问题,系统也能给出合理回复而不是抛异常。
参数说明:Neo4jTemplate是 Spring Data Neo4j 提供的操作入口,query方法接收 Cypher 语句和参数。这里的entity会作为参数传入,对应模板里的$person或$movie占位符,避免拼接注入问题。
4.2 查询执行、结果排序与自然语言组装
Neo4j 查询返回的是节点和关系数据,直接展示给用户肯定不行,要组装成自然语言句子。比如查询“某演员演过哪些电影”,返回的是一批电影 title 字段,问答服务要把它们拼成“该演员出演过的电影包括:电影A、电影B、电影C”。
组装逻辑里有一个工程细节:结果排序。如果用RETURN m.title不加排序,Neo4j 返回顺序不稳定,同一条数据每次查出来顺序可能不一样。常见处理是加一个稳定的排序字段,比如按上映年份倒序排,让答案每次都一致。
private String buildFilmsAnswer(List<Map<String, Object>> result) { if (result.isEmpty()) { return "没有查到该演员出演的电影记录"; } List<String> titles = result.stream() .map(row -> String.valueOf(row.get("m.title"))) .filter(title -> !"null".equals(title)) .collect(Collectors.toList()); return String.join("、", titles); }逻辑说明:从查询结果里取出m.title字段拼成中文枚举句式。这里对null值做了过滤,避免空属性污染答案。图数据库中并不是每个节点都完整填充了所有属性,这一步属于数据清洗,在问答系统里不能省。
参数说明:字段名m.title的写法要和 Cypher 里的别名保持一致,取不到值时要能判断出是字段不存在还是值为空。实际调试中常见的问题是 Cypher 里写了RETURN m,这里却用m.title取值,类型对不上。
4.3 配置文件与运行参数:连接、超时和日志
把服务跑起来之前,先确认application.yml里的 Neo4j 连接配置。这块配错,启动时不会立刻报错,但第一个查询接口会直接超时或抛出连接异常:
spring: neo4j: uri: bolt://localhost:7687 authentication: username: neo4j password: your-password data: neo4j: repositories: enabled: true logging: level: org.neo4j.driver: INFO com.example.qa: DEBUG逻辑说明:uri是 Neo4j 的 Bolt 协议地址,默认端口 7687。authentication下的用户名密码要和本机 Neo4j 数据库一致,Neo4j 安装时设置的初始密码是后续连接的基础。日志配置特意把com.example.qa调成 DEBUG,这样每个问答请求的完整处理日志会打到控制台,排查问题事半功倍。
参数说明:如果 Neo4j 跑在 Docker 容器里,uri要写成映射出来的宿主机端口。repositories.enabled决定是否启用 Spring Data Repository 代理,项目如果直接使用Neo4jTemplate,这里保持默认即可。日志里注意只开自己包路径的 DEBUG,全开驱动级 DEBUG 会让控制台被刷爆。
5. 避坑:五个最常见的翻车现场与排查方法
5.1 问题:Spring 启动就报 Bean 创建失败
现象:项目启动不到三秒直接抛BeanCreationException,错误信息里能看到NoClassDefFoundError或NoSuchMethodError。
原因:spring-data-neo4j和neo4j-java-driver版本不匹配。大部分课程作业项目在网络上流传后,依赖版本被反复改过,很容易出现跨大版本组合。
解决:先看pom.xml里 Spring Boot 父依赖的版本号,再查对应版本的spring-data-neo4j兼容矩阵。这里的原则是不要手动指定一个很高版本的驱动去配低版本的 Spring Data,以 Maven 仓库里官方发布说明为准。
5.2 问题:中文问题全被识别成“未知名实体”
现象:问“某演员演过什么电影”,启动日志显示实体为 null,或者抽出来的候选词完全不对。
原因:HanLP 默认词典里没有图谱中的影视人名,分词时把人名切成了单字,后续和图谱节点名匹配时全部失败。
解决:第一,把图谱中全部 Person 和 Movie 名称导出成自定义词典,放到 HanLP 的词典目录。第二,在实体抽取时先做一次“全字符串包含”匹配,再依赖分词结果。这样只要用户问题里原样出现了图谱节点名,就能优先命中。
5.3 问题:同名实体张冠李戴
现象:问“某电影的导演是谁”,系统返回了一个同名电影的结果;或者图谱里有一个叫同一个名字的人名和电影名,查询返回了错误方向的数据。
原因:实体名匹配没有结合意图限制标签类型,只做了全图谱字符串匹配。人名和电影名发生碰撞后,模板里写的(p:Person)和(m:Movie)标签没有起到限定作用。
解决:实体抽取时把图谱节点名按标签分组缓存,ACTOR_FILMS意图只匹配 Person 列表,FILM_ACTORS意图只匹配 Movie 列表。抽取方法增加一个标签参数,匹配速度更快,歧义也更少。
5.4 问题:查询慢到像是系统卡死
现象:数据量只有几百条时查询飞快,但导入几千条数据后,一个问题要等好几秒。
原因:图谱没有建索引。虽然 Neo4j 在数据量小时看不出问题,但是问答系统里的实体匹配和关系遍历一旦全库扫描,性能会断崖式下降。之前提到的唯一约束其实已经创建了索引,但只对部分属性有效。
解决:把问答涉及的属性全部补上索引,尤其是Person.name和Movie.title。另外在执行 Cypher 时加LIMIT限制返回条数,避免明星演员关联出几十部电影导致结果集过大。
5.5 问题:同样的问题每次回答排序都不一样
现象:连着问三次“某演员演过哪些电影”,答案内容一样,但电影出现的顺序每次不同。
原因:Cypher 没有ORDER BY,Neo4j 底层存储节点和关系的物理顺序决定了返回顺序,这不是稳定排序。
解决:在所有查询模板的RETURN子句前统一加ORDER BY,比如按released DESC排序。这件事要在模板层解决,不能靠调用方去排序,否则每个接口都要重复处理。
6. 进阶:用评测集验收问答质量,再把图谱换到新领域
6.1 建一个 30 条问题的评测集,把黑匣子打开
问答系统的“智能程度”不能靠感觉,建议在答辩前准备一个评测问题集,每个问题标注期望答案的类型和实体范围。把问题批量喂到接口,统计两个指标:实体命中率,也就是系统能不能正确抽取出实体;意图准确率,也就是系统能不能选对查询模板。这个动作能把黑匣子打开,任何一次改动都能量化影响。
对于每一条评测失败的问题,把QaService的日志打开就能看到是意图错了、实体错了还是 Cypher 错了,Debug 日志里会逐行打印分类结果和最终执行的查询语句。从这个角度说,一个好用的问答系统一定是一个日志友好的系统,而不是每次只返回一个最终答案。
6.2 从电影图谱扩展到其他领域
这套系统的架构不绑定电影领域,只要换数据文件和模板表就能支撑另一个知识图谱问答场景。比如把Movie、Person节点换成学术论文、作者,把ACTED_IN换成AUTHOR_OF,意图模板表同步调整,问答链路完全复用。扩展时要同步改掉实体缓存加载逻辑,让节点名列表来源于新图谱的节点标签。
回顾整个拆解过程,这套基于 Neo4j 的智能问答系统最大的学习价值不在于某一个算法,而在于完整闭环:图数据库建模、中文 NLP 解析、模板化查询生成、Spring 服务封装,每一环都能讲清楚原理。每次我在别的项目里看到“智能问答”三个字,都会先看它是真查询还是假搜索,这已经是拆项目留下的职业习惯了。建议你按第 5 章的排查顺序过一遍启动流程,把这套工程跑通之后再去改查询模板,希望帮到你。
本文还有配套的精品资源,点击获取