
简介系统整合电影知识图谱与微信小程序构建了一个可交互的智能问答应用源码、说明文档与配置资源均包含在内。项目面向计算机相关专业学生、教师及初级开发者尤其适合作为毕业设计、课程设计、作业或初期项目演示的起点代码在提交前已通过运行验证功能可用可放心用于学习与二次开发。压缩包共59个文件整体仅411KB核心为22个Java后端文件与6个JS逻辑文件配合3个WXML和7个WXSS构成小程序前端页面另有6个JSON配置、properties参数、Maven脚本、png图片及说明文档完整覆盖后端服务、前端交互与部署配置。目前已有48人学习下载。压缩包内不仅包含可运行代码还附带项目文档、授权码文本与目录结构说明有助于理清知识图谱查询、问答匹配和小程序交互的完整链路基础较好的读者可在该代码上扩展新功能直接用于课设、毕设或项目立项演示。1. 电影知识图谱问答系统一个把图谱建库、NLP 和微信小程序串起来的完整项目在微信小程序里输入「周星驰演过哪些电影」传统做法是分词后做全文检索返回一堆含关键词的网页而基于知识图谱的问答系统会先识别出实体「周星驰」和关系「出演」再通过图数据库的关联查询直接给出影片列表和上映年份。这套以 Neo4j 为核心存储、Spring Boot 做接口、微信小程序做交互的源码把知识图谱构建、自然语言问句解析、前端页面展示三条链路完整打通不是只讲概念的 PPT而是一个能跑通、能答辩、也能二次开发的真实工程。它特别适合人工智能、软件工程、自动化等专业的毕设和课设前端开发者和后端工程师也能从中快速拆出一套可复用的问答系统骨架。2. 电影知识图谱构建Neo4j 数据建模与本体设计2.1 从形式语义学到知识图谱本体论如何指导数据建模知识图谱的底层逻辑来自本体论早期用形式语义学定义概念、属性和关系再到后来以三元组实体-关系-实体描述世界。电影问答场景里的本体并不复杂演员参演电影、导演执导电影、电影属于某种类型、演员之间存在合作关系。先把这些概念抽象成图谱模式再决定数据怎么落库比对着一张表硬拼查询要清晰得多。这套项目在 Neo4j 中建立的核心本体就围绕四种主角节点展开电影、演员、导演、类型外加一组描述关系的边。本体设计决定了问句能回答到什么深度。如果只建了「电影-演员」关系那么「某两部电影有哪些共同演员」这种跨实体问题就查不出来。我在拆这套源码时发现它的数据模型保留了比较丰富的边比如演员参演电影、导演执导电影、电影从属类型、演员间合作这为后面扩展多跳查询留了空间。图谱建模时不要把属性都堆在节点上像「演员出生日期」「电影评分」这类描述性信息应该作为节点属性保存而不是单独建节点否则查询语句会变得异常冗长。2.2 节点、关系与属性映射规则项目源码中的数据处理模块把结构化电影数据转换成 Neo4j 的节点和关系。下表是这套资源里最核心的图谱映射约定也是二次开发时新增实体类型的参照。节点标签主要属性入度关系出度关系Movietitle, year, rating, introACTED_IN, DIRECTED, GENRE_OF无Actorname, birth, nationality无ACTED_INDirectorname, birth无DIRECTEDGenrename无GENRE_OFActor同上COOPERATE_WITHCOOPERATE_WITH关系类型统一使用大写命名属性比如 ACTED_IN 上可以带 role角色名和 year参演年份。这种设计让 Cypher 查询非常好写比如查「演员演过哪些电影」就是MATCH (a:Actor)-[:ACTED_IN]-(m:Movie)不需要关心属性过滤条件。需要特别注意的是Neo4j 中关系是单向的但查询时可以不指定方向这一点在问答生成环节会减少很多模板数量。2.3 用 Cypher 写入数据与验证查询结果数据导入阶段最常见的做法是用 Cypher 的 MERGE 语句它兼具「存在即匹配不存在则创建」的能力避免重复插入。下面是从电影 CSV 导入节点和关系的核心语句。LOAD CSV WITH HEADERS FROM file:///movies.csv AS row MERGE (m:Movie {title: row.title}) ON CREATE SET m.year toInteger(row.year), m.rating toFloat(row.rating); LOAD CSV WITH HEADERS FROM file:///cast.csv AS row MATCH (m:Movie {title: row.title}) MERGE (a:Actor {name: row.actor_name}) MERGE (a)-[:ACTED_IN {role: row.role}]-(m);第一段先把每部电影创建为 Movie 节点title 作为唯一键year 和 rating 在节点首次建立时写入。第二段处理演职人员关系先匹配已有电影再创建或复用演员节点最后用 MERGE 建立一条从演员指向电影的 ACTED_IN 关系。这里用 MERGE 而不是 CREATE 的目的很明确——如果同一个演员在多部电影中出现Actor 节点不会被重复创建。写入完成后验证数据是否就绪可以执行以下查询统计图里节点和关系的数量或者直接查看某部电影的所有邻居。MATCH (n) RETURN labels(n)[0] AS label, count(*) AS total; MATCH (m:Movie {title: 大话西游})-[r]-(n) RETURN type(r) AS rel, n.name, n.title;第一行按节点标签分组统计数量能快速看出各类实体是否都导入成功。第二行返回指定电影关联的所有关系和邻居节点这是测试图谱完整性的快捷方式。如果某个演员查不到优先检查 CSV 里的名字是否有多余空格或编码问题这类脏数据比 Cypher 语法错误更隐蔽。2.4 图谱可视化与「只显示 25 个标签」的问题处理在 Neo4j Browser 里初次打开图谱页面常常只显示少量节点这正是热搜里常提到的「知识图谱只显示 25 个标签」问题。Browser 默认对查询结果做了LIMIT 25限制防止浏览器渲染过多节点导致卡顿。解决办法不是改全局配置而是在查询语句末尾显式控制返回规模。MATCH p (:Movie)-[r]-(:Actor) RETURN p LIMIT 200;这段查询把返回路径限制到 200 条Browser 右侧的显示设置里还可以继续调连接数和节点数。注意LIMIT 改的是返回行数而不是图里实际数据量数据仍然全部在 Neo4j 中只是可视化的投影数量变了。如果希望前端页面也默认展示更多节点需要在小程序端相关页面里设置分页或者滚动加载而不是指望图数据库返回全部数据。3. 智能问答引擎从自然语言到 Cypher 查询的完整路径3.1 问句分类意图识别与实体抽取双通道问答系统的核心不是查数据库而是把用户的自然语言变成数据库能执行的查询。这套项目的做法是「模板匹配 词典归一」组合先把问题归类到几个固定意图再从问句中切出实体。意图分类的维度决定了问答系统的上限源码里的问答模块主要覆盖以下五类问题意图标识问题模板对应 Cypher 模式ACTOR_MOVIES某演员演过哪些电影(a:Actor)-[:ACTED_IN]-(m:Movie)MOVIE_ACTORS某电影有哪些演员(m:Movie)-[:ACTED_IN]-(a:Actor)MOVIE_DIRECTOR某电影的导演是谁(m:Movie)-[:DIRECTED]-(d:Director)DIRECTOR_MOVIES某导演拍过哪些电影(d:Director)-[:DIRECTED]-(m:Movie)MOVIE_RATING某电影的评分有多高(m:Movie {title:...}) 返回 rating识别流程分两步先按正则或关键词匹配意图再抽取实体。意图匹配我一般建议用几个强特征词比如出现「导演」优先归类到导演相关「演过」「参演」归类到演员。实体抽取则依赖项目 data 目录下的同义词词典把「星爷」「周星星」都映射到「周星驰」这个标准名避免图谱里查不到数据。3.2 Spring Boot 接口设计与模板匹配实现后端只暴露一个问答接口请求体里带用户输入的问题响应体返回答案文本和可选的电影列表。核心代码结构如下RestController RequestMapping(/api/qa) public class QuestionController { private final QAService qaService; public QuestionController(QAService qaService) { this.qaService qaService; } PostMapping(/ask) public Result ask(RequestBody QuestionDTO dto) { String question dto.getQuestion(); if (question null || question.trim().isEmpty()) { return Result.error(问题不能为空); } return Result.success(qaService.answer(question)); } }QuestionDTO 接收小程序的 JSON 请求只含一个question字段。Result是统一返回结构附带 code、message 和 data方便前端根据状态码提示错误。Controller 层不做任何业务逻辑只负责参数校验和结果包装这种写法在小程序对接时非常友好因为前端只需要解析固定结构。真正的问答逻辑在 QAService 中先匹配实体再匹配意图最后拼装 Cypher 执行。下面这条核心方法可以看到整体思路。Service public class QAService { private final Neo4jClient neo4jClient; private final EntityDict entityDict; public AnswerResult answer(String question) { String entity entityDict.extractEntity(question); if (entity null) { return AnswerResult.withMessage(暂时没听懂换个说法试试比如周星驰演过哪些电影); } Intent intent IntentMatcher.match(question); if (intent null) { return AnswerResult.withMessage(我还不支持这类问题); } String cypher CypherBuilder.build(intent, entity); ListMapString, Object rows neo4jClient.query(cypher); return AnswerResult.fromRows(intent, entity, rows); } }答案先做实体抽取没有实体就直接返回兜底提示而不是去查数据库这样能节省一次无效查询。意图匹配失败也走兜底避免返回空白。CypherBuilder根据意图枚举把实体拼接进 Cypher需要注意实体值必须转义或者参数化防止引号破坏语法。常见做法是用 Neo4j Java Driver 的Map参数方式而不是字符串拼接。public String build(Intent intent, String entity) { return switch (intent) { case ACTOR_MOVIES - MATCH (a:Actor {name: $name})-[:ACTED_IN]-(m:Movie) RETURN m.title, m.year; case MOVIE_ACTORS - MATCH (m:Movie {title: $name})-[:ACTED_IN]-(a:Actor) RETURN a.name, a.birth; // 其他意图省略 }; }$name 是参数占位符执行时绑定实体值既能防止 Cypher 注入又避免中文名字里的特殊符号干扰语法。参数化查询是 Neo4j 官方推荐的写法性能上还能复用查询计划。3.3 同义词词典与实体归一化实体归一化是中文问答里最容易被低估的环节。用户不会老老实实输入「周星驰」可能说「星爷」「周星星」「周星驰导演」这些表述必须映射到图谱里存储的标准名称。项目把词典放在 resources 下的entity_dict.txt每行一个实体映射星爷 周星驰 周星星 周星驰 周星驰导演 周星驰 大话西游之大圣娶亲 大话西游之大圣娶亲 功夫电影 功夫加载字典后实体抽取会做最长匹配优先匹配更长的别名避免「周星驰导演」被拆成「周星驰」和「导演」两个词。我实际调试时遇到过一个问题用户问「星爷和吴孟达合作过哪些电影」如果词典不够全「吴孟达」无法识别整个问题就会走到兜底所以词典需要根据历史问题持续补全。3.4 多跳问题与兜底策略基础五类问题都只涉及一跳关系但实际用户提问更抽象例如「周星驰和吴孟达合作过哪些电影」需要两跳演员a参演某电影演员b也参演同一部电影。这类问题在源码中通过预置模板覆盖本质是关系路径查询。MATCH (a:Actor {name: $name1})-[:ACTED_IN]-(m:Movie)-[:ACTED_IN]-(b:Actor {name: $name2}) RETURN DISTINCT m.title;这里对电影节点做了聚合去重因为两个演员可能合作多部电影DISTINCT 能把重复标题消掉。兜底策略也很重要任何未匹配意图都返回一句引导话术并附带几个示例问题让用户知道系统能回答什么。小程序端拿到这个提示后原样展示用户体验不至于中断。4. 微信小程序端页面导航、问答交互与接口对接4.1 原生小程序还是 uniapp从项目结构看实现方式拆开源码可以看到app.json、pages、app.wxss、colorui这些目录这是典型的原生微信小程序结构而不是 uniapp 或 Taro 编译产物。原生小程序的一个明显特征是每个页面目录下同时存在.js/.json/.wxml/.wxss四个文件且全局配置直接放在根目录的app.json。相比之下uniapp 项目会用pages.json统一配置页面路由源码里并没有这个文件说明作者选择原生开发。原生方案的好处是调试方便微信开发者工具里直接运行无需额外编译链路缺点是没法跨端复用到支付宝小程序或 H5。如果你打算把项目扩展成多端应用再迁移到 uniapp 也不迟而只做毕设展示的话原生方案足够。4.2 app.json 全局配置与导航栏高度适配小程序的页面结构由app.json统一管理问答页面、榜单页面、我的页面都要在这里注册。下面是精简后的配置。{ pages: [ pages/index/index, pages/qa/qa, pages/detail/detail ], window: { navigationBarTitleText: 电影知识图谱问答, navigationBarBackgroundColor: #2c2c2c, navigationBarTextStyle: white }, usingComponents: { cu-custom: /colorui/components/cu-custom } }pages数组第一项是启动页也就是进入小程序后第一个加载的页面。如果不想让用户每次打开都停留在问答首页可以调整这一项的顺序。navigationBarBackgroundColor控制顶部导航栏背景色navigationBarTextStyle只能设置 black 或 white。关于经常被提到的「微信小程序顶部导航栏高度」它由系统状态栏高度和导航栏自身高度组成不同机型不一样。源码用 ColorUI 的cu-custom组件自适应原理是读取状态栏高度后动态设置占位块。const systemInfo wx.getSystemInfoSync(); this.setData({ statusBarHeight: systemInfo.statusBarHeight, navBarHeight: 44 });statusBarHeight对应 iPhone 等设备的刘海高度navBarHeight是胶囊按钮区域的标准高度。自定义导航栏时页面顶部要预留这两段高度否则内容会被刘海遮挡。4.3 问答页面 WXML 与数据请求问答页面是系统的主交互入口输入框、发送按钮、消息列表从上到下排列。下面是最核心的 WXML 片段。view classqa-container scroll-view scroll-y classmessage-list scroll-into-view{{scrollTo}} block wx:for{{messages}} wx:keyid view classmsg-item {{item.from user ? right : left}} text{{item.content}}/text /view /block /scroll-view view classinput-bar input bindconfirmsendQuestion bindinputhandleInput value{{currentQuestion}} / button bindtapsendQuestion发送/button /view /viewscroll-into-view用于消息变多时自动滚动到底部绑定的scrollTo是最后一条消息的 id。输入框绑定两个事件bindinput实时保存当前输入内容bindconfirm在键盘点搜索或回车时触发发送逻辑。发送后把用户问题追加到messages同时清空输入框再调用后端接口获取回答。sendQuestion() { const question this.data.currentQuestion.trim(); if (!question) return; this.setData({ messages: [...this.data.messages, { id: Date.now(), from: user, content: question }], currentQuestion: }); wx.request({ url: http://localhost:8080/api/qa/ask, method: POST, data: { question }, header: { content-type: application/json }, success: (res) { const answer res.data.data; this.setData({ messages: [...this.data.messages, { id: Date.now(), from: ai, content: answer }] }); }, fail: () { wx.showToast({ title: 网络异常, icon: none }); } }); }wx.request是小程序向服务器发起请求的唯一入口url 在开发阶段可以直接写本地 IP 地址但真机调试时必须改成局域网地址或线上域名。需要注意小程序的success回调只代表请求发出并收到响应不代表业务成功后端返回的 code 字段仍需要判断上面的代码把整个res.data都拿出来用是简化写法。正式项目里可以封装一个request工具统一处理状态码和错误提示。4.4 修改加载页面与常见展示坑资源包里提到「修改刚进入的加载页面」实际指的是pages/qa/qa页面在数据未返回前的占位状态。常见做法是在页面 data 中增加一个loading字段请求前显示「正在思考」的动画请求完成后切换到消息列表。这个状态位不需要预渲染多个页面只通过wx:if控制即可。搜索结果页还会涉及电影列表渲染。如果返回的图片来自外部 CDN小程序需要在小程序管理后台配置 downloadFile 合法域名否则图片无法加载。另一个高频问题是页面只用view和text展示富文本遇到带换行和缩进的内容时排版错乱建议用rich-text组件处理后端返回的格式化文本。还有微信小程序内嵌 H5 时返回箭头丢失的问题与这里无关但如果后续接入 WebView要记住wx.navigateBack和 H5 内部路由是两套体系。5. 打包部署与知识图谱问答能力的扩展技巧5.1 用 Maven 完成后端清理构建项目根目录有mvnw、mvnw.cmd和pom.xml说明这是一个标准 Maven 工程。mvnw是 Maven Wrapper可以在没有安装本地 Maven 的环境下自动下载指定版本避免团队协作时环境不一致。命令行执行以下命令即可完成清理和打包。./mvnw clean package -DskipTestsclean删除 target 目录下的旧产物package把项目打成可执行 jar-DskipTests跳过测试用例加快构建速度。打包完成后在 target 目录找到*.jar通过java -jar启动。启动前务必确认 Neo4j 已打开并且application.yml中的数据库连接地址、用户名、密码与本地环境一致。最容易踩的坑是 Neo4j 默认密码为neo4j/neo4j首次启动会强制修改而项目配置文件里仍写旧密码导致握手失败。5.2 扩展新实体与问题模板想把这个知识图谱问答系统扩展成图书问答或音乐问答不需要改前端页面结构只需三步在 Neo4j 里新增节点和关系类型比如Book、Author在项目的数据导入模块中添加对应 CSV 解析逻辑在IntentMatcher中新增意图枚举并在CypherBuilder里补充对应的 Cypher 模板。每新增一个意图建议先在 Neo4j Browser 手动执行一遍 Cypher确认语法无误后再写进 Java 代码能省掉大量联调时间。5.3 快速验证问答正确率验证系统是否可靠最简单的方式是整理一份测试问题清单覆盖每个意图至少十个不同问法然后在页面逐个输入并记录回答是否正确。如果发现某个问题未命中先看日志里打印的意图和实体再查词典是否需要补充同义词。把维护好的测试集放在 resources 目录的test_questions.txt后续每次代码改动后批量跑一遍可以防止回归问题。建议在application.yml里开启 SQL 日志打印观察每个问题实际生成的 Cypher 语句。答错时先人工判断 Cypher 是否正确再判断结果渲染是否出错这条链路能快速定位问题出在 NLP 层还是数据层也是答辩时展示系统排错思路的一个亮点。本文还有配套的精品资源点击获取