简介:这是一套面向高校计算机及相关专业学生(如人工智能、自动化、物联网等)的毕业设计级知识图谱实践项目,聚焦电影领域问答场景,解决自然语言问句到结构化知识查询的映射问题,适合课程设计、毕设选题与知识图谱入门进阶。资源包共45个文件,涵盖7个核心Python脚本(含question_classification.py、server.py、data2neo4j.py等)、5个CSV数据文件(movie.csv、person_to_movie.csv等构建图谱基础)、5张PNG流程图与界面示意图、8个文本类资源(含vocabulary.txt、label.txt及中文分词词典),辅以设计文档(.doc)、项目说明(.md)和前端静态资源(HTML/CSS/JS),整体仅1.19MB,轻量易部署。已有72人学习下载,资源经严格测试可直接运行,提供完整Neo4j图谱构建、问句分类、模板匹配与后端服务全流程代码,附详细设计报告与模块化目录结构,便于理解知识图谱问答系统的技术闭环与工程实现路径。
1. 为什么一个电影问答系统,非得用知识图谱不可?
你有没有试过在某个电影App里问“周星驰和吴孟达合作过哪些喜剧片”,结果搜出来一堆带“喜剧”标签但根本没他们俩的片子?或者输入“豆瓣评分8分以上、2010年后上映、王家卫导演的爱情片”,界面直接卡死——不是前端慢,是后端压根没能力把这三重条件串成一条逻辑链。这就是传统关键词检索的硬伤:它认字,不认关系。而这个基于 Neo4j 的电影问答系统,就是冲着“关系”来的:它把演员、导演、类型、年份、评分这些实体,连成一张可推理、可追溯、可追问的网。不是匹配词,而是理解“张艺谋→执导→《红高粱》→属于→‘剧情’类型→同时属于→‘国产’地区”这条链。整套方案用 Python 做前后端胶水,Neo4j 存实体与关系,数据集覆盖 3000+ 电影、5000+ 人物、20+ 类型,完整走通了从原始 CSV 到图数据库导入、问题分类、槽位抽取、Cypher 生成、答案返回的全链路。它不是玩具 Demo,而是能支撑毕设答辩、课设验收、甚至小型教学演示的闭环系统——尤其适合人工智能、自然语言处理方向的学生,把“知识图谱”四个字,从 PPT 里的概念,变成答辩时能现场敲命令、改问题、看图谱实时响应的实物。
2. 总体架构设计:四层结构与五个核心模块的职责划分
这套系统不是“Python 调个 API 就完事”的轻量级项目,它必须清晰分层,否则后期改一个槽位识别逻辑,整个服务就崩。我把它拆成四层:数据层(CSV 原始数据 + Neo4j 图库)、处理层(问题解析与 Cypher 构建)、服务层(Flask Web 接口)、表现层(HTML+JS 前端)。五模块则对应具体功能单元:数据预处理模块、问题分类模块、实体-关系抽取模块、Cypher 查询生成模块、答案格式化模块。每一模块都独立可测,接口明确。比如问题分类模块只负责输出{"type": "movie_by_director", "slots": {"director": "张艺谋"}},绝不碰数据库;Cypher 生成模块只接收这个 JSON,输出MATCH (d:Person)-[:DIRECTED]->(m:Movie) WHERE d.name = '张艺谋' RETURN m.title,绝不做 NLP。这种强契约设计,让 A 同学调参分类模型、B 同学优化 Cypher 模板、C 同学改前端样式,互不干扰。下面展开关键模块的设计逻辑与实现细节。
2.1 数据预处理:从七张 CSV 到 Neo4j 图谱的映射规则
原始数据共 7 个 CSV 文件:movie.csv(含 title, year, rating 等)、genre.csv(genre_id, genre_name)、person.csv(person_id, name, role)、person_to_movie.csv(person_id, movie_id, relation_type)、movie_to_genre.csv(movie_id, genre_id)、data2neo4j.py是核心转换脚本。关键不在“怎么读 CSV”,而在“怎么定义节点与关系”。我们约定:
- 所有
movie.csv行 → 创建:Movie节点,属性title,year,rating,id - 所有
person.csv行 → 创建:Person节点,属性name,role,id;注意role字段值为"actor"/"director"/"writer",这是后续查询过滤的关键 person_to_movie.csv→ 根据relation_type创建不同关系:[:ACTED_IN],[:DIRECTED],[:WROTE]movie_to_genre.csv→ 创建[:HAS_GENRE]关系,连接:Movie与:Genre节点
data2neo4j.py的核心逻辑如下(已去除非关键日志):
# data2neo4j.py from py2neo import Graph, Node, Relationship import pandas as pd graph = Graph("bolt://localhost:7687", auth=("neo4j", "your_password")) # 1. 导入 Movie 节点 movies = pd.read_csv("data/movie.csv") for _, row in movies.iterrows(): movie_node = Node("Movie", title=row['title'], year=int(row['year']) if pd.notna(row['year']) else None, rating=float(row['rating']) if pd.notna(row['rating']) else None, id=row['id']) graph.create(movie_node) # 2. 导入 Person 节点(注意 role 字段决定后续关系语义) persons = pd.read_csv("data/person.csv") for _, row in persons.iterrows(): person_node = Node("Person", name=row['name'], role=row['role'], # critical: "director" vs "actor" id=row['id']) graph.create(person_node) # 3. 导入关系:person_to_movie.csv 中 relation_type 决定边类型 p2m = pd.read_csv("data/person_to_movie.csv") for _, row in p2m.iterrows(): person = graph.nodes.match("Person", id=row['person_id']).first() movie = graph.nodes.match("Movie", id=row['movie_id']).first() if person and movie: if row['relation_type'] == 'director': rel = Relationship(person, "DIRECTED", movie) elif row['relation_type'] == 'actor': rel = Relationship(person, "ACTED_IN", movie) elif row['relation_type'] == 'writer': rel = Relationship(person, "WROTE", movie) graph.create(rel)提示:
py2neo版本需与 Neo4j 服务端兼容(本项目适配 Neo4j 4.4.x,故用py2neo==4.4.0)。若用neo4j-driver,需改写为 session.run() 形式,但逻辑不变——重点是relation_type字段必须严格映射到 Cypher 关系名,否则后续查询永远为空。
2.2 问题分类模块:基于规则+轻量模型的双保险机制
纯靠规则写死所有问法?不现实。纯靠 BERT 微调?毕设时间不够,且小样本下泛化差。本系统采用“规则兜底 + 模板匹配 + 轻量分类器”三级策略。question_classification.py主流程如下:
- 预处理:去停用词(用
vocabulary.txt+userdict3.txt自定义词典增强电影专有名词识别)、统一标点、转小写 - 模板匹配:对高频问法硬编码正则,如
r"(.*)主演(.*)" → "movie_by_actor",r"(.*)导演(.*)" → "movie_by_director" - 特征工程:提取 n-gram(1~3)、是否含年份数字、是否含“评分”“豆瓣”等关键词、问题长度归一化
- SVM 分类:用
sklearn.svm.SVC训练,类别共 8 种:movie_by_director,movie_by_actor,movie_by_genre,movie_by_year,person_by_movie,genre_by_movie,rating_by_movie,movie_list
训练数据来自question_classification.txt,每行格式:问题文本\t标签,共 1200+ 条人工标注样本。关键代码片段:
# question_classification.py from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.svm import SVC import jieba # 加载自定义词典(解决“周星驰”被切为“周/星驰”的问题) jieba.load_userdict("userdict3.txt") def preprocess(q): # 去标点、转小写、用 jieba 分词 q = re.sub(r"[^\w\s]", "", q).lower() words = jieba.lcut(q) return " ".join([w for w in words if w not in stopwords]) # 特征向量化(TF-IDF) vectorizer = TfidfVectorizer(max_features=5000, ngram_range=(1,3)) X_train_vec = vectorizer.fit_transform(X_train_processed) # 训练 SVM(RBF核,C=1.0,gamma='scale') clf = SVC(kernel='rbf', C=1.0, gamma='scale', probability=True) clf.fit(X_train_vec, y_train) # 预测函数:返回最可能标签 + 置信度 def classify_question(q): q_proc = preprocess(q) q_vec = vectorizer.transform([q_proc]) pred = clf.predict(q_vec)[0] prob = clf.predict_proba(q_vec)[0].max() return {"type": pred, "confidence": float(prob)}注意:
vocabulary.txt是通用停用词表,userdict3.txt是电影领域专有词(如“豆瓣”“IMDb”“票房”“烂番茄”),二者缺一不可。若跳过jieba.load_userdict(),模型会把“阿凡达”切为“阿/凡/达”,导致特征失效。
2.3 实体-关系抽取模块:基于依存句法与规则的槽位填充
分类只是知道“用户想查导演”,但“导演是谁”还得抽出来。process_question.py不依赖复杂依存分析库(如 LTP 或 Stanza),而是用更鲁棒的规则+位置策略。核心思想:电影名、人名、类型名、年份,在问题中通常紧邻关键词。例如:“张艺谋导演的电影有哪些?”——“张艺谋”在“导演”前 1 位;“2010 年以后的科幻片”——“2010”在“年”前,“科幻”在“片”前。
抽取逻辑分三步:
- 关键词定位:扫描问题,记录“导演”“主演”“类型”“年份”“评分”等 trigger 词位置
- 窗口扫描:以 trigger 位置为中心,向前向后各扫 3 个词,用
person.csv和genre.csv的 name 列做字符串匹配(支持模糊匹配,编辑距离 ≤1) - 优先级裁决:若同一问题匹配出多个候选(如“周星驰和吴孟达主演”),按出现顺序取第一个;若无匹配,则回退到问题中所有名词(用 jieba 提取),再查图谱验证是否存在该节点
关键函数extract_slots()示例:
# process_question.py import difflib def extract_slots(question, question_type, person_names, genre_names): slots = {} # 步骤1:找trigger位置 triggers = {"director": ["导演", "执导"], "actor": ["主演", "扮演", "出演"], "genre": ["类型", "片", "风格"], "year": ["年", "年代"], "rating": ["评分", "分数"]} for slot_key, trigger_list in triggers.items(): for trig in trigger_list: pos = question.find(trig) if pos != -1: # 步骤2:窗口扫描(前3后3字) start = max(0, pos - 3) end = min(len(question), pos + 3 + len(trig)) window = question[start:end] # 步骤3:匹配 person_names 或 genre_names(模糊匹配) candidates = [] if slot_key in ["director", "actor"]: candidates = [name for name in person_names if difflib.SequenceMatcher(None, name, window).ratio() > 0.6] elif slot_key == "genre": candidates = [name for name in genre_names if difflib.SequenceMatcher(None, name, window).ratio() > 0.7] if candidates: slots[slot_key] = candidates[0] # 取最相似的一个 break # 找到即停,避免重复赋值 # 回退:若未抽到 director/actor,尝试提取问题中所有名词并查图谱 if question_type in ["movie_by_director", "movie_by_actor"] and "director" not in slots and "actor" not in slots: words = jieba.lcut(question) for w in words: if len(w) > 2 and w in person_names: # 简单长度+存在性过滤 slots["director" if question_type == "movie_by_director" else "actor"] = w break return slots提示:
person_names和genre_names是启动时从 CSV 预加载的列表,避免每次查询都 IO。模糊匹配阈值.ratio() > 0.6是血泪经验——设太高(0.8)会漏掉“徐克”写成“许克”的错别字;设太低(0.4)会把“功夫”误认为“功夫熊猫”的“功夫”。
3. 核心实现:Cypher 查询生成与答案渲染的确定性逻辑
分类和抽取只是铺垫,真正让系统“活起来”的,是把自然语言问题,稳稳当当地翻译成 Neo4j 能执行的 Cypher 语句,并把返回的 JSON 结果,变成用户看得懂的答案。这一环容不得半点“大概”“可能”,必须每种问题类型,对应唯一、可验证、可调试的 Cypher 模板。question_template.py就是这份确定性的载体——它不是动态拼接字符串,而是为每类问题预置结构化模板,再用安全参数填充。
3.1 Cypher 模板引擎:八种问题类型的精准映射
question_template.py定义了一个TemplateManager类,其核心是get_cypher()方法。它接收分类结果{"type": "movie_by_director", "slots": {...}},返回完全合法的 Cypher 字符串。所有模板均经过 Neo4j Browser 实测,确保语法正确、性能可控(全部加LIMIT 10防爆)。以下是三个典型模板:
movie_by_director:查某导演的所有电影MATCH (p:Person)-[:DIRECTED]->(m:Movie) WHERE p.name = $director AND p.role = 'director' RETURN m.title AS title, m.year AS year, m.rating AS rating LIMIT 10movie_by_genre:查某类型的所有电影(注意:Genre 节点需先创建)MATCH (g:Genre)<-[:HAS_GENRE]-(m:Movie) WHERE g.name = $genre RETURN m.title AS title, m.year AS year, m.rating AS rating LIMIT 10person_by_movie:查某电影的所有主创人员(导演、主演、编剧)MATCH (m:Movie)<-[]-(p:Person) WHERE m.title = $movie RETURN p.name AS name, p.role AS role LIMIT 10
get_cypher()的实现逻辑是字典映射 + 参数安全注入:
# question_template.py class TemplateManager: TEMPLATES = { "movie_by_director": ( "MATCH (p:Person)-[:DIRECTED]->(m:Movie) " "WHERE p.name = $director AND p.role = 'director' " "RETURN m.title AS title, m.year AS year, m.rating AS rating " "LIMIT 10" ), "movie_by_actor": ( "MATCH (p:Person)-[:ACTED_IN]->(m:Movie) " "WHERE p.name = $actor AND p.role = 'actor' " "RETURN m.title AS title, m.year AS year, m.rating AS rating " "LIMIT 10" ), # ... 其他5种模板 } def get_cypher(self, question_type, slots): if question_type not in self.TEMPLATES: raise ValueError(f"Unknown question type: {question_type}") # 安全参数注入:只允许 slots 中的 key 出现在模板占位符中 # $director, $actor, $genre 等必须与 slots 键名一致 cypher = self.TEMPLATES[question_type] # 检查 slots 是否包含所需参数(防 KeyError) required_params = [k[1:] for k in re.findall(r'\$([a-z_]+)', cypher)] for param in required_params: if param not in slots: raise ValueError(f"Missing slot: {param} for type {question_type}") return cypher注意:这里用
$director而非字符串拼接'"' + director + '"',是 Neo4j 官方推荐的安全方式,彻底杜绝 Cypher 注入风险。server.py中执行时,必须用session.run(cypher, **slots)传参,而非.format()。
3.2 答案格式化:从 Cypher 结果到 HTML 表格的标准化管道
Neo4j 返回的是Record对象列表,每个Record是字段名到值的映射。server.py中的format_answer()函数,负责把这堆数据,变成前端index.html里<table>能直接渲染的 JSON。它不做任何业务逻辑判断,只做三件事:字段名标准化、空值处理、类型转换。
# server.py def format_answer(records, question_type): """ 将 Neo4j Record 列表格式化为前端可用的 dict list records: [Record, Record, ...], each has keys like 'title', 'year', 'rating' returns: [{"title": "xxx", "year": 1994, "rating": 9.7}, ...] """ result = [] for record in records: item = {} # 标准化字段:所有模板都返回 title/year/rating/name/role for field in ["title", "year", "rating", "name", "role"]: val = record.get(field, None) if val is not None: # 类型转换:year 强制 int,rating 强制 float,其他保持 str if field == "year" and isinstance(val, (int, float)): item[field] = int(val) elif field == "rating" and isinstance(val, (int, float)): item[field] = round(float(val), 1) else: item[field] = str(val) result.append(item) # 若结果为空,返回提示 if not result: return [{"message": "未找到相关信息,请检查人名、电影名是否准确"}] return result前端index.html的 JS 部分,只做纯粹的数据绑定:
<!-- static/js/main.js --> function renderResult(data) { const table = document.getElementById('result-table'); table.innerHTML = ''; // 清空 if (data.length === 0 || data[0].message) { table.innerHTML = `<tr><td colspan="3">${data[0].message}</td></tr>`; return; } // 动态生成表头(根据 data 第一项的 keys) const headers = Object.keys(data[0]); const headerRow = document.createElement('tr'); headers.forEach(h => { const th = document.createElement('th'); th.textContent = h === 'title' ? '电影名' : h === 'year' ? '年份' : h === 'rating' ? '评分' : h === 'name' ? '姓名' : '角色'; headerRow.appendChild(th); }); table.appendChild(headerRow); // 填充数据行 data.forEach(row => { const tr = document.createElement('tr'); headers.forEach(h => { const td = document.createElement('td'); td.textContent = row[h] || '-'; tr.appendChild(td); }); table.appendChild(tr); }); }提示:前端不解析
question_type,只认data结构。这意味着后端改一种问题的返回字段(如movie_by_genre新增genre_count),前端无需动一行代码——只要format_answer()输出的item字典包含新字段,表格就会自动多一列。这种松耦合,是系统长期可维护的关键。
3.3 Web 服务层:Flask 接口的健壮性设计与错误隔离
server.py是整个系统的门面,但它绝不能成为单点故障。它的设计原则是:请求进来,快速分流;出错时,不崩服务,只返错误码。整个 Flask App 只暴露一个 POST 接口/ask,所有逻辑封装在handle_question()函数内,该函数被@app.errorhandler(Exception)全局捕获。
# server.py from flask import Flask, request, jsonify from question_classification import classify_question from process_question import extract_slots from question_template import TemplateManager from py2neo import Graph app = Flask(__name__) graph = Graph("bolt://localhost:7687", auth=("neo4j", "your_password")) template_mgr = TemplateManager() @app.route('/ask', methods=['POST']) def handle_question(): try: data = request.get_json() if not data or 'question' not in data: return jsonify({"error": "缺少 question 字段"}), 400 question_text = data['question'].strip() if not question_text: return jsonify({"error": "问题不能为空"}), 400 # Step 1: 分类 cls_result = classify_question(question_text) if cls_result["confidence"] < 0.5: return jsonify({"error": "问题意图不明确,请换一种问法"}), 400 # Step 2: 抽取槽位 slots = extract_slots(question_text, cls_result["type"], person_names, genre_names) # person_names/genre_names 需全局加载 # Step 3: 生成 Cypher cypher = template_mgr.get_cypher(cls_result["type"], slots) # Step 4: 执行查询(带超时) with graph.session() as session: result = session.run(cypher, **slots).data() # .data() 返回 list of dict # Step 5: 格式化答案 formatted = format_answer(result, cls_result["type"]) return jsonify({ "success": True, "answer": formatted, "question_type": cls_result["type"], "slots": slots }) except ValueError as e: return jsonify({"error": str(e)}), 400 except Exception as e: # 记录详细错误日志(生产环境应写入文件) app.logger.error(f"Server error: {e}", exc_info=True) return jsonify({"error": "服务器内部错误,请稍后重试"}), 500 if __name__ == '__main__': # 预加载 person_names, genre_names 到内存 person_names = load_person_names() # 从 person.csv 读 genre_names = load_genre_names() # 从 genre.csv 读 app.run(host='0.0.0.0', port=5000, debug=False) # 生产环境务必关 debug注意:
debug=False是硬性要求。开启 debug 模式会暴露服务端路径、变量名,且 Flask 自带的调试器在生产环境极不安全。所有错误信息(app.logger.error)必须记录到文件,而非返回给前端——这是答辩时老师常问的“安全性如何保障”的标准答案。
4. 避坑指南:部署与调试中最容易翻车的五个致命点
这套系统看着模块清晰,但实际部署时,90% 的失败都集中在以下五个点。它们不是“可能出错”,而是“几乎必踩”,且错误现象极其隐蔽,比如前端显示空白、后端静默退出、Neo4j 查询永远返回空。我把每个坑都拆成“现象→原因→解决”,全是真实血泪经验。
4.1 现象:data2neo4j.py运行报ConnectionRefusedError,或 Neo4j Browser 显示“not connected”
原因:Neo4j 服务根本没启动,或配置了错误的监听地址。默认 Neo4j 4.4.x 只监听localhost:7687,若你在远程服务器跑,而server.py里写的是bolt://127.0.0.1:7687,本地开发没问题,但部署到云服务器时,127.0.0.1指向服务器自身,而 Neo4j 可能没开远程访问。
解决:
- 确认 Neo4j 已启动:
sudo systemctl status neo4j(Linux)或打开 Neo4j Desktop - 检查
neo4j.conf中dbms.connectors.default_listen_address是否为0.0.0.0(允许所有 IP) - 检查
dbms.connector.bolt.listen_address是否为0.0.0.0:7687 - 若用 Docker,启动命令必须加
-p 7687:7687并设置NEO4J_dbms_connectors_default__listen__address=0.0.0.0 server.py和data2neo4j.py中的连接字符串,统一用bolt://localhost:7687(开发)或bolt://你的服务器IP:7687(生产),绝不用 127.0.0.1
4.2 现象:问题分类准确率极低,classify_question()总返回movie_list,置信度 0.99
原因:jieba.load_userdict("userdict3.txt")失败,导致“周星驰”被切成“周/星驰”,TF-IDF 特征向量里根本没有“周星驰”这个词,所有训练样本的特征都错位。userdict3.txt文件路径错误、编码为 GBK(而代码用 UTF-8 读)、或文件为空,都会触发此问题。
解决:
- 在
question_classification.py开头加诊断代码:try: jieba.load_userdict("userdict3.txt") print("✅ 成功加载 userdict3.txt") print("测试分词:", jieba.lcut("周星驰导演的电影")) # 应输出 ['周星驰', '导演', '的', '电影'] except Exception as e: print("❌ 加载 userdict 失败:", e) - 确保
userdict3.txt与question_classification.py同目录,且用 UTF-8 编码(用 VS Code 或 Notepad++ 查看编码) - 文件内容必须是纯文本,每行一个词,如:
周星驰 张艺谋 豆瓣 IMDb 烂番茄
4.3 现象:process_question.py抽不到实体,slots总是空字典,但问题明明含“张艺谋”
原因:person_names列表加载失败,或difflib.SequenceMatcher的阈值设得过高。person_names是从person.csv读的,但如果 CSV 里name列有空格、换行、或编码错误(如 Excel 保存为 CSV 时用了逗号分隔但名字里含逗号),pandas.read_csv()会解析错行,导致person_names为空或错乱。
解决:
- 在
server.py启动时,打印len(person_names)和前 5 个名字:person_names = [row['name'].strip() for _, row in pd.read_csv("data/person.csv").iterrows() if pd.notna(row['name'])] print(f"✅ 加载 person_names: {len(person_names)} 个,示例:{person_names[:5]}") - 若长度为 0,检查
person.csv:用head -n 5 data/person.csv看前 5 行,确认分隔符是,且无异常字符;用file -i data/person.csv确认编码是utf-8 - 若
person_names正常但还是抽不到,临时降低模糊匹配阈值:ratio() > 0.5,观察是否命中,再逐步调回 0.6
4.4 现象:Cypher 查询在 Neo4j Browser 里能跑,但在server.py中执行返回空列表
原因:参数注入方式错误。session.run(cypher, director="张艺谋")是对的,但若写成session.run(cypher.format(director="张艺谋")),则director值会被当字符串字面量,而 Neo4j 期望的是带引号的字符串,如'张艺谋'。.format()不会自动加引号,导致 Cypher 语法错误,但session.run()默认不抛异常,只返回空。
解决:
- 绝对禁止在 Cypher 字符串中用
.format()或%拼接参数 - 必须用
$param占位符 +**slots解包传参 - 调试时,在
server.py中加日志:app.logger.info(f"Executing Cypher: {cypher} with params {slots}") result = session.run(cypher, **slots).data() app.logger.info(f"Cypher returned {len(result)} records") - 复制日志中的 Cypher 和参数,粘贴到 Neo4j Browser 的
:params命令中手动测试::params {"director": "张艺谋"} MATCH (p:Person)-[:DIRECTED]->(m:Movie) WHERE p.name = $director RETURN m.title
4.5 现象:前端index.html打开后,输入问题无响应,浏览器控制台报Failed to load resource: the server responded with a status of 400
原因:Flask 服务没起来,或前端 JS 的fetch地址写错了。client.py是命令行测试脚本,index.html的 JS 里fetch('/ask')默认发到当前域名的/ask,如果 Flask 运行在http://localhost:5000,而你双击index.html用file://协议打开,浏览器会因跨域拒绝请求。
解决:
- 必须用 Flask 启动服务,再用浏览器访问
http://localhost:5000,而不是双击index.html - 检查
index.html中 JS 的 fetch 地址:fetch('/ask')是对的(相对路径),但若 Flask 运行在http://192.168.1.100:5000,则前端页面也必须通过http://192.168.1.100:5000/index.html访问 - 若需外部访问,
app.run()必须加host='0.0.0.0',且防火墙放行 5000 端口 - 控制台报 400 时,点开 Network 标签页,看
/ask请求的 Response,里面会有 Flask 返回的具体错误信息(如“缺少 question 字段”),这才是真正的线索
5. 答辩与验收:从代码演示到文档呈现的六步通关法
毕设答辩不是技术发布会,而是“证明你真的做过、真的懂、真的能跑通”。老师最关心三件事:需求是否真实、设计是否合理、结果是否可验证。这套电影问答系统,恰恰能把这三点,变成你答辩时的“可视化证据链”。我带过三届学生用类似方案答辩,总结出一套六步通关法,每一步都对应一个可展示、可截图、可复现的动作,让你从“讲PPT”变成“现场操作”。
5.1 第一步:用 Neo4j Browser 展示图谱的“真实感”
别一上来就跑 Python。打开 Neo4j Browser(http://localhost:7687),执行三条命令,截图放在答辩 PPT 第一页:
// 1. 查总节点数(证明数据已导入) MATCH (n) RETURN count(n) AS total_nodes // 2. 查导演节点数(证明角色字段有效) MATCH (p:Person) WHERE p.role = 'director' RETURN count(p) AS director_count // 3. 查一条真实关系链(证明图结构成立) MATCH (d:Person)-[r:DIRECTED]->(m:Movie)<-[r2:ACTED_IN]-(a:Person) WHERE d.name = '张艺谋' AND a.name = '巩俐' RETURN d.name AS director, type(r) AS relation1, m.title AS movie, type(r2) AS relation2, a.name AS actor这三行输出,就是你“数据层”的铁证。老师看到
total_nodes是 8000+,director_count是 500+,第三条还真跑出《红高粱》,立刻相信你不是拿 demo 网站截图糊弄。记住:图数据库的“眼见为实”,比 Python 控制台打印print("success")有力十倍。
5.2 第二步:用client.py命令行演示“输入-输出”的确定性
client.py是本项目的“最小可运行证明”。它不依赖前端,纯 Python 脚本,直接调用server.py的逻辑。答辩时,打开终端,依次执行:
# 启动服务(后台运行,不阻塞) nohup python server.py > server.log 2>&1 & # 测试问题(复制粘贴,不手打) python client.py "张艺谋导演的电影有哪些?" python client.py "周星驰和吴孟达合作过哪些电影?" python client.py "20 <p> <a href="https://download.csdn.net/download/RuanJian_GC/90264531" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>