资讯动态

知识图谱电影问答系统实战:Neo4j+Python搭建智能问答机器人

发布时间:2026/10/18 21:38:26 来源:尧图企业网站定制
简介一份面向计算机专业毕业设计的高分项目资源基于Python和Neo4j构建知识图谱电影问答系统。项目源自作者大四期间完成的毕业设计经导师指导并认可评审得分为99分代码完整、可直接运行既适合正在准备毕设的学生也适合需要项目实战练习的学习者还可作为课程设计或期末大作业。资源共59个文件涵盖Python后端、爬虫脚本、小程序前端、JSON数据、SVG流程图、PNG运行截图以及Markdown说明文档等压缩包大小6.28MB目录结构清晰便于定位。已有77人学习下载。随包提供系统源码、配套文档、运行效果截图与工作流程示意图覆盖从数据采集、知识图谱构建到问答交互的完整链路。借助该项目可深入理解Neo4j图数据库在知识图谱问答中的应用方法也可直接作为毕业设计答辩的功能演示与项目参考。1. 毕业设计选知识图谱电影问答为什么这个组合容易出高分同样是电影信息查询类毕设用关系型数据库做出来的答辩时最多演示几条 SQL换成「知识图谱 Neo4j 问答系统」这个组合演示逻辑就从「查表」变成了「问答」背后是数据建模、图存储、自然语言处理和系统集成一整条链路。这个标题的核心是知识图谱电影问答系统Python 负责逻辑和接口Neo4j 负责存储实体与关系最终交付一个输入「周星驰演过哪些喜剧片」就能直接给结构化答案的网页系统。它适合三类人正在选毕设题目的学生想把普通电影管理网站改出技术亮点的学生以及想零基础入门知识图谱落地的开发者。不过先泼盆冷水这套系统的难点不在 Neo4j在问答链路的数据清洗和模板设计——后面几章我会把每个环节的代码、参数和翻车点一次讲清。2. 图谱构建先过数据关电影本体、清洗与 Neo4j 批量导入很多同学拿到源码第一件事就是启动 Neo4j 导入数据结果运行起来发现「导演是谁」「合作过哪些演员」这类问题答不了原因不是代码有 bug而是本体设计阶段把关系建少了。我在模拟项目X里见过一份数据集只有 movie 和 person 两类节点导演和演员全部用同一个 person 节点表示关系里也没有区分「执导」和「主演」——最后调了两天模板发现问题根本出在数据模型。所以这一章从本体设计开始讲再给数据清洗和导入的具体代码。2.1 先画本体电影、演员、导演三类实体和四种关系本体设计就是回答三个问题有哪些实体类型、每个实体有哪些属性、实体之间有哪些关系。电影问答系统里最常见的实体是电影、演员、导演再往上加可以扩充类型编剧、制片公司和属性评分、片长、制片国家、上映年份但是毕设规模不建议一开始就超过五类实体。实体类型关键属性关系出度Movie片名、年份、评分、片长、简介主演、导演、类型、发行Actor姓名、出生日期、国籍主演Director姓名、出生日期、国籍导演属性要不要做索引取决于问答场景。比如「2018 年上映的科幻片有哪些」如果没有给上映年份建索引这个查询就是在全图谱里扫一遍数据量小时无所谓跑到一万部电影以上就得建索引否则每次问答从秒级响应变成几秒。实体之间的关系在 Cypher 里是有方向的建关系时统一按「电影指向人」还是「人指向电影」要一致模板写起来才不会晕。2.2 数据清洗公开数据集里最容易出问题的三处本体定好后最繁琐的是数据准备。公开影视数据集往往来自爬虫或社区整理格式五花八门常见做法是用 pandas 先洗一遍再入库。最容易出问题的三处演员名和导演名不统一同一人名有的带空格、有的带繁体写法同一部电影有多个条目尤其翻拍和同名片需要用年份做区分评分字段里混着「7.8」「暂无评分」这种字符串入库时要么转 float 要么置空。清洗代码通常长这样import pandas as pd df pd.read_csv(movies.csv, encodingutf-8-sig) # 去重片名年份 重复的只保留第一条 df df.drop_duplicates(subset[name, year]) # 评分统一格式无法转换的置为 None df[rating] pd.to_numeric(df[rating], errorscoerce) # 演员字段切割兼容逗号和中文顿号 df[actors] df[actors].str.split(r[,、]) df.head()to_numeric 的 errorscoerce 参数是最常用的处理手法遇到「暂无评分」这类值会转成 NaN而不是报错中断。split 时用正则同时切逗号和顿号能避开「张某某,李某某」和「张某某、李某某」两种格式混在一个文件里的问题。切完记得检查一下每个 list 的长度分布有的电影演员列几十个人有的只有一个人后面导入关系时不影响但会影响前端展示。2.3 批量导入 Neo4jUNWIND 批量写入的正确姿势清洗完怎么入图常见有两种一种是用 Neo4j 自带的 LOAD CSV 语法在浏览器里执行 Cypher另一种是用 py2neo 在 Python 里连接 Neo4j 写。毕设项目用 py2neo 更多原因是问答逻辑本身就在 Python 里后续维护只写一套连接代码。批量写入不要用循环一条条 create那种写法数据量稍大就会非常慢正确姿势是用 UNWIND 批量创建。下面这段代码可以当作通用模板from py2neo import Graph, Node, Relationship graph Graph(bolt://localhost:7687, auth(neo4j, password)) batch_size 500 nodes [] # movies_df 是清洗后的 DataFrame for _, row in movies_df.iterrows(): node Node(Movie, namerow[name], yearrow[year], ratingrow[rating]) nodes.append(node) if len(nodes) batch_size: graph.create(*nodes) # 批量提交 nodes.clear() if nodes: graph.create(*nodes)Graph 的三个参数分别对应连接协议、地址、认证信息。batch_size 一般取 200 到 500太大会让单次事务内存压力上升太小又体现不出批量优势。graph.create(*nodes) 的重点在于一次性提交一个列表而不是在循环里每次写 Neo4j这是最常见的性能优化点。写入过程中如果出现重复节点报错说明数据里仍有重复键回头检查去重逻辑别直接在导入代码里 try-except 吞掉否则会留下大量僵尸节点。关系导入要等节点全部写完后进行原因很简单创建关系时必须引用到两端节点节点还没入库就建关系会直接失败。关系写入同样用批量思路先给 Movie 和 Actor 的 name 建唯一约束再用 match 找节点引用CREATE CONSTRAINT movie_name IF NOT EXISTS FOR (m:Movie) REQUIRE m.name IS UNIQUE; CREATE CONSTRAINT actor_name IF NOT EXISTS FOR (a:Actor) REQUIRE a.name IS UNIQUE;# 假设 relation_df 是 (actor_name, movie_name) 的映射表 tx graph.begin() for _, row in relation_df.iterrows(): a graph.nodes.match(Actor, namerow[actor]).first() m graph.nodes.match(Movie, namerow[movie]).first() if a and m: tx.create(Relationship(a, 主演, m)) tx.commit()有了唯一约束match 走索引关系导入速度能快一个数量级还能顺带防止重复节点。这段代码里最花时间的是 match 查找数据量过万以后每次 match 全库扫会非常慢所以索引必须在导入前建好。到这里图谱侧就绪可以开始写问答逻辑。3. 问答核心逻辑意图分类、实体抽取与 Cypher 模板生成问答系统的难点从来不在「查数据库」而在「把一句中文问句变成一条 Cypher」。问「周星驰演过的电影有哪些」和「周星驰的出生日期」前者要把实体定位到电影后者要取属性两者生成的查询路径完全不同。大部分毕设项目的做法是模板匹配先判断用户问的是什么类型的问题再用规则抽取问句里的实体最后套用写好的查询模板。这三步走完系统基本就能覆盖日常问答。3.1 意图分类五类问题覆盖大多数「电影问答」场景意图分类就是先判定「用户在问什么」。常见的分类方案是五类演员作品类、电影信息类、导演信息类、合作关系类、属性类。每一类对应一组触发关键词比如「演过、主演、出演」触发演员作品「导演、执导、谁拍的」触发导演信息。实现上用关键词规则就够毕设阶段不需要上意图识别模型因为电影问答的句式非常固定规则写好了准确率能到九成。分类函数可以这样组织INTENT_RULES { actor_movies: [演过, 出演, 主演, 作品, 电影], movie_info: [评分, 简介, 片长, 上映, 信息], director_movies: [执导, 导演的, 谁拍的], cooperation: [合作, 同台], attr_query: [出生, 生日, 国籍, 年龄, 哪里人], } def classify_intent(question): # 按规则表逐个匹配返回第一个命中的意图 for intent, keywords in INTENT_RULES.items(): for kw in keywords: if kw in question: return intent return unknown这里有一个容易被忽略的细节意图分类要在实体抽取之前做。因为「周星驰演过的电影」和「周星驰的出生日期」抽出来的实体都是周星驰但反问句意图不同后续查询路径完全不同。所以分类的目的不是检索而是决定走哪套模板。规则表的顺序也有讲究越具体的意图放前面比如「谁拍的」和「导演的」都涉及导演但「导演的」可能指导演的电影作品顺序反了会误判。上面代码里我把更具体的「导演的」放在前面就是这个原因。3.2 实体抽取给 jieba 加载自定义词典别让电影名被切碎意图定完下一步是从问句里找出具体的电影名、演员名。常见做法是用 jieba 分词但是直接分词会把「大话西游之大圣娶亲」这种长片名切成好几段所以必须加载自定义词典。import jieba jieba.load_userdict(movie_dict.txt) # movie_dict.txt 每行格式词语 词频 词性 # 大话西游之大圣娶亲 10 nz # 周星驰 10 nr def extract_entity(question): segs [w for w in jieba.cut(question) if w.strip()] # 从切分结果中挑出在词典里出现过的词作为候选实体 return [w for w in segs if w in ENTITY_DICT]词典文件格式是「词语 词频 词性」词频越大越容易成词。这里有个小坑词性标 nr人名还是 nz作品名会间接影响模板匹配结果因为后面会拿词性做类型判断。实体抽取完不要直接拿第一个候选当答案同名情况在电影领域特别多后面的避坑部分会专门讲消歧。3.3 模板匹配从标准化问句到 Cypher 查询的映射函数抽取到意图和实体后最后一步是生成 Cypher。做法是写一个映射函数输入意图和实体输出一条带参数的查询def build_cypher(intent, entity, entity_type): if intent actor_movies: # 查询某演员演过的电影返回片名和评分 return (MATCH (a:Actor {name: $name})-[:主演]-(m:Movie) RETURN m.name AS title, m.rating AS rating ORDER BY m.year DESC), {name: entity} elif intent movie_info: # 查询某一部电影的信息 return (MATCH (m:Movie {name: $name}) RETURN m.name, m.year, m.rating, m.duration), {name: entity} elif intent cooperation: # 查询两位演员是否合作过 return (MATCH (a1:Actor {name: $name1})-[:主演]-(m:Movie) -[:主演]-(a2:Actor {name: $name2}) RETURN m.name AS movie), {name1: entity[0], name2: entity[1]} return None, Nonebuild_cypher 的三个参数里entity 可能是单个词也可能是多个候选entity_type 是从分词词性带过来的类型标记。Cypher 查询参数用 $name 而不是字符串拼接一是防止注入二是 Neo4j 会缓存执行计划性能更稳定。返回的记录列表在组装答案时拼成一句自然语言。如果 build_cypher 返回 None通常说明意图和实体对不上比如问「某演员的导演是谁」意图是导演信息但实体类型是演员要么改模板要么提示用户换个问法。模板匹配有个常见翻车点实体的词性类型和意图不一致时不要硬匹配。比如查电影《风之谷》的导演意图是导演信息但抽取出的实体是电影名模板里需要把「风之谷」当 Movie 处理。所以我一般会在 build_cypher 里加一个校验分支entity_type 与模板期望不一致时尝试用「先定位电影节点再沿导演关系跳转」的兜底查询而不是直接返回空答案。模板的维护顺序也有讲究。我在毕设阶段习惯把所有模板集中在一个列表里按「具体到通用」排序而不是散落的 if-else。具体模板比如带年份范围的查询通用模板比如只查片名前面命中了就不再往后走。这样后续加新问题类型时只需要往列表里插一个模板不用改动主流程。4. Python 后端与前端对接让问答从命令行走进页面问答核心逻辑跑通后剩下的是把它包成一个系统。毕设项目的常见架构是 Flask 做后端一个极简 HTML 页面做前端用 fetch 调接口。Neo4j 的查询不能在浏览器里直接暴露给客户端所有 Cypher 执行必须放在服务端前端只发送问句文本、接收 JSON 答案。4.1 Flask 后端一条 /qa 接口串起整条问答管线Flask 项目里只需要一个路由就能撑起整个问答功能from flask import Flask, request, jsonify from qa_pipeline import answer_question app Flask(__name__) app.route(/qa, methods[POST]) def qa(): data request.get_json() question data.get(question, ).strip() if not question: return jsonify({code: 400, msg: 问题不能为空}) result answer_question(question) return jsonify({code: 200, data: result}) if __name__ __main__: app.run(host0.0.0.0, port8000, debugFalse)answer_question 是前面几章的封装先分类意图再抽取实体然后 build_cypher 拿到查询和参数最后执行查询并组装答案。把这条链路收进一个函数而不是散在路由里后续加日志、加缓存都方便。host 设为 0.0.0.0 是为了方便局域网里演示毕设答辩时评委可能用另一台电脑访问。debugFalse 是运行习惯开发阶段开 True 能直接在页面看到报错堆栈。封装函数里最好加上异常捕获防止 Neo4j 查询报错时把堆栈直接返回给前端def answer_question(question): try: intent classify_intent(question) entities extract_entity(question) cypher, params build_cypher(intent, entities) if not cypher: return {answer: 暂时没理解这个问题换一种问法试试} records graph.run(cypher, **params).data() return {answer: assemble_answer(intent, records)} except Exception as e: # 记录日志到文件前端显示兜底文案 logging.exception(QA failed) return {answer: 查询出错请稍后再试}graph.run 返回的是一个游标调用 .data() 才会真正把记录加载成 Python 字典列表。这里有个性能细节返回条数不确定时最好在 Cypher 里加 LIMIT 限制比如演员作品最多返回 20 部否则遇到作品特别多的演员JSON 会很大前端渲染也会卡。4.2 前端页面一个输入框、一个对话列表的极简实现前端不需要复杂框架一个输入框加一个消息列表足够演示。核心代码就两段一段渲染对话气泡一段发请求。div idchat-list/div input idq-input placeholder输入问题例如周星驰演过哪些喜剧片 / script const list document.getElementById(chat-list); const input document.getElementById(q-input); async function ask(question) { // 把用户问题和系统答案按顺序追加到对话列表 const res await fetch(/qa, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({question}) }); const json await res.json(); const item document.createElement(div); item.textContent json.data.answer; list.appendChild(item); } input.addEventListener(keydown, (e) { if (e.key Enter input.value.trim()) { const q input.value.trim(); const item document.createElement(div); item.textContent Q: q; list.appendChild(item); input.value ; ask(q); } }); /scriptfetch 的 Content-Type 必须写成 application/jsonFlask 的 request.get_json() 才能正确解析写成别的类型或漏掉这个头会拿不到 body。调试时打开浏览器开发者工具的网络面板如果 /qa 返回 415 或 400先看请求头是不是少了 Content-Type。消息列表的样式可以用 CSS 简单区分左右气泡这块不影响功能放在最后做。4.3 项目结构先搞清楚文档和代码怎么对应着看拿到这个项目后先别急着运行花 10 分钟把目录结构和文档对应起来能少走很多弯路。毕设项目通常包含两个核心文档一个是环境搭建说明另一个是设计文档。环境文档会讲 Python 版本、Neo4j 版本、第三方库清单设计文档一般按「需求分析 → 本体设计 → 系统实现 → 测试」的顺序组织直接对应代码里的数据导入、问答管线、后端接口这几个模块。我的阅读顺序是先看环境文档确认版本组合再打开数据导入脚本看字段名是否和 CSV 表头一致然后打开问答管线的入口文件确认三个核心函数的位置最后才启动服务。前端页面放在最后看因为它只依赖 /qa 接口的返回格式接口的数据结构一旦稳定页面代码基本不用动。按这个顺序排查有个好处出问题时你能快速判断是数据层、逻辑层还是接口层的问题而不是在整份源码里乱翻。5. 避坑运行毕设项目时最常翻车的 4 个地方这部分内容来自我帮 A 同学排查问答系统时攒下的经验。每个坑都按「现象 → 原因 → 解决」来写可以直接对照检查。5.1 py2neo 和 Neo4j 版本不匹配导致连不上库现象运行导入脚本报 ConnectionError 或 AuthError提示无法连接 bolt://localhost:7687但浏览器能打开 Neo4j 的网页界面。原因py2neo 的版本和 Neo4j 服务端版本不匹配。py2neo 对服务端的协议支持有明显版本兼容差异新版服务端配旧驱动握手直接失败。另一个常见诱因是 Neo4j 修改了默认密码后脚本里的 auth 参数还是初始密码。解决先在浏览器里用 Neo4j 自带的查询界面执行 RETURN 1 确认服务端正常然后核对 py2neo 版本与服务端大版本是否匹配。如果项目文档明确写了依赖版本优先按文档组合装别用最新的。改完密码记得重启 Neo4j 服务只改密码不重启是新手最容易忽略的一步。5.2 中文数据导入乱码编码问题比你想的常见现象CSV 导入后Neo4j 页面里中文全是乱码或者 Python 读取 CSV 时抛 UnicodeDecodeError。原因CSV 文件的实际编码是 GBK 或带 BOM 的 UTF-8而读取代码写的是普通 UTF-8。Windows 上用 Excel 编辑过的 CSV 尤其容易变成 GBK直接 pd.read_csv 默认读 UTF-8 就翻车。解决读取时指定 encodingutf-8-sig 或 gbk先读一个小文件测试确认中文正常后再处理全量数据。还有一种隐蔽情况文件整体是 UTF-8 但个别字段混了脏字符这种情况用 errorsignore 丢掉少数脏字符比中断任务好接受。检查是否乱码最直接的办法是导入后执行一条 Cypher 查一条记录看浏览器渲染结果不要只看 Python 控制台里的打印。5.3 同名实体问答答错图谱里有「两个周星驰」怎么办现象问「周星驰演过哪些电影」系统返回的是一串完全不对的电影名。原因知识图谱里有重名的实体可能是两个同名演员也可能是演员和电影同名。抽取实体时只按名称匹配没有做类型或年份约束匹配到了错误节点。解决在实体抽取后增加消歧步骤如果候选实体存在多个同名的先按意图类型过滤。比如意图是 actor_movies优先匹配 Actor 类型而不是 Movie 类型如果同名 Actor 还有多个就结合问句里出现的年份词比如「九十年代的周星驰」缩小范围。在 Cypher 里也可以加属性约束比如 MATCH (a:Actor {name: $name, birth_year: $year})但前提是数据里真的维护了这些属性。5.4 模板没匹配上问题答不出时先打印生成的 Cypher现象图谱里有数据Python 也没报错但前端永远返回「暂时没理解这个问题」。原因意图分类返回了 unknown或者 build_cypher 的某个分支没覆盖到当前问法。这类问题隐蔽在它不是崩溃性错误而是静默返回空答案新手最容易在页面和接口之间来回找原因。解决在 answer_question 里把 intent、entities、cypher 三个变量打到日志或者临时加 print 输出。然后拿一条失败问句走一遍链路问句切分对不对词典里有没有这个词意图命中没命中最后拿打印出来的 Cypher 去 Neo4j 浏览器手动执行看有没有数据返回。按这个顺序排查绝大多数「答不出」能在十分钟内定位到具体环节。6. 验证与进阶让问答系统从 70 分走到 90 分系统跑通不等于能拿高分。答辩评委最常问的问题是「你怎么证明系统是有效的」。空口说「我测了没问题」没有说服力得有一份结构化的测试记录。6.1 构造 30 条测试集按意图分类统计准确率我建议准备一份固定测试集每个意图至少 5 条问句共 30 条覆盖正常问法、颠倒语序、带年份修饰、带别名等变体。跑完统计两个指标意图分类准确率和最终答案准确率。前者判断规则表有没有误分类后者判断 Cypher 模板和数据有没有配合失误。这两组数字写进毕设文档的测试章节比十页截图都有说服力。6.2 答案排序评分、热度、年份的加权规则模板查询经常返回多条记录比如「周星驰演过的电影」有几十部返回顺序需要设计。最简单的做法是让 Cypher 按年份或评分排序但这只是单因子。我在模拟项目X里用的是加权评分按「年份 × 0.4 评分 × 0.4 热度 × 0.2」热度可以用数据库里的评论数或收藏数代替没有就统一给默认值。权重不要写死在 Cypher 里在 Python 里组装答案时算方便答辩时现场调参数演示效果。6.3 换个更抗打的思路从小模板到大模型如果时间有余可以在模板匹配之外加一层词向量召回把问句向量化后和已有问答模板做相似度匹配相似度低于阈值再走规则表。这样做的好处是即使规则里没覆盖新问法只要语义相近就能落到正确模板。不过这一步是加分项不是必选项先把规则表的准确率做到九成再考虑向量化顺序反了会把项目拖进调参泥潭。做完这些这个毕设项目的技术含量不止在「实现了问答」更在「能说清楚每条问句从输入到答案经历了哪几步、每步失败怎么排查」。我现在的习惯是保留一张问题排查表把上面 4 个坑按现象列成清单每次改代码后逐条回归一遍30 条测试集全过了才算完成一次迭代。这套「先定测试集、再改代码、最后回归」的顺序帮我省掉了大量改完 A 坏 B 的时间希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑