news 2026/10/3 8:50:59

知识图谱电影问答系统:从CSV数据清洗到Neo4j Cypher查询的Python实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
知识图谱电影问答系统:从CSV数据清洗到Neo4j Cypher查询的Python实战

简介:一份面向计算机专业毕业设计的高分完整项目,基于Python构建知识图谱电影问答系统,覆盖数据清洗、实体构建、图谱存储与问答交互的完整开发链路。项目经导师指导并获九十九分评审,代码完整、可直接运行,即使刚入门的新手也能参照说明逐步启动,适用于毕业设计、课程设计及期末大作业等场景。资源包为zip压缩格式,大小约六点三MB,共一百一十六个文件,包括十三份Python源码、八个HTML页面、七份CSS样式和多个JavaScript脚本,还包含JSON、XML配置文件及电影、人物、类型等CSV关系数据,为知识图谱搭建提供了完整数据支撑。前端页面与后端逻辑分层清晰,并配有Bootstrap等样式组件,方便二次修改与学习。该资源已有一百六十一人学习下载,借助源码内说明文档、数据组织方式和工程配置,读者可较快理解从零实现一个知识图谱问答系统的方法,减少踩坑,提高毕设完成质量。

1. 知识图谱电影问答系统:一份能讲清楚“数据到问答”的 Python 毕设

如果你见过毕业答辩现场,评委最常问的不是“代码能不能跑”,而是“你这个项目到底解决了什么问题”。这套基于知识图谱的电影问答系统源码,值得拿下的点就在这:它把整个问题拆成了“数据建模—图谱构建—自然语言问答”三段,每一段都能单独讲、单独改。我第一次拆这份资源时,以为难点在回答模板怎么写,结果发现最难的是电影名消歧和 Cypher 查询组装——同一个人名、同一部电影,在 CSV 里和用户问句里的写法可能完全不一样。它适合正在做 Python 毕业设计、知识图谱课程设计,或者想完整走一遍数据到问答全链路的人。前端用 Bootstrap 那套样式,后端逻辑集中在数据清洗和问答模块,不是一句句写死答案的玩具。

2. 先拆文件再谈原理:CSV 数据模型与系统三层架构

2.1 只看文件名就能还原数据模型:movie、person_to_movie、movie_to_genre

拿到源码包,我先把目录翻了一遍,记住一件事:真正决定问答效果的不是那些 CSS 文件,而是三个 CSV——movie.csv、person_to_movie.csv、movie_to_genre.csv。前端那几个样式文件(bootstrap.css、style000.css、font-awesome.css、owl.carousel.css、poposlides.css、circles.css、style.css)只是把问答页面、结果列表这些展示层做得像点样子,属于静态资源,图谱导入和问答逻辑都不会碰它们。

在常见建模方式里,movie.csv 是电影主体,记录电影 ID、电影名、评分、上映年份等信息;person_to_movie.csv 是人物与电影的关联表,至少包含人物标识、电影标识和角色类型,有些版本里会直接把“导演”“演员”这类角色写成一个字段;movie_to_genre.csv 负责把电影和喜剧、动作、科幻这些类型挂上钩。这三张表翻译成知识图谱就是两类节点加两类关系:Movie、Person、Genre 是节点,person_to_movie 映射成“出演/导演”关系,movie_to_genre 映射成“属于”关系。

文件内容在知识图谱里的角色
movie.csv电影主体信息Movie 节点
person_to_movie.csv人物与电影的关联Person 节点 + 出演/导演关系
movie_to_genre.csv电影与类型的关联Movie 节点 + 属于关系
*.css页面样式静态资源,不参与问答逻辑

从命名规则也能看出一件事:这套资源不是单文件脚本,而是一个小工程。movie.csv 与 movie_to_genre.csv 通过 movie_id 连接,person_to_movie.csv 同时连接 Person 和 Movie 两类节点。关系是有方向的:人演电影,电影属于类型。在 Neo4j 里方向就是箭头,Cypher 查询句法严重依赖方向,导入时方向反了,问答结果就全是反的。这张表列的是典型列结构,不同版本列名会有些出入,比如 person_to_movie.csv 里可能没有独立的人名列,只有 person_id 和电影 ID,那就要在清洗阶段把“人物节点”单独拆出来。这也给你一个判断标准:如果导入后关系数量对不上,优先检查关联表里的 ID 是不是字符串一致性出了问题,而不是急着改问答代码。

2.2 三层架构:“数据清洗—图谱查询—问答组装”的选型理由

我习惯把这类项目拆成三层来读。第一层是数据层,CSV 进来以后先做清洗,编码统一、去重、空值处理,得到一批干净的节点和关系;第二层是图谱层,实体和关系落到 Neo4j,用 Cypher 完成“查某人的作品”“查某片的导演”这类查询;第三层是问答层,接收用户问句,解析出电影名或人名、判断意图、拼 Cypher、把结果拼回一句人话返回给前端。

为什么会选 Neo4j 而不是 MySQL?关键差别在“多跳关系”。问“周星驰主演过的喜剧片里评分最高的三部”,MySQL 得写三四层 join,图数据库用模式匹配一句话就出来了。对毕设来说,图模型在答辩时也更容易讲清楚“多对多关系怎么建模”。但整套流程真正耗时的地方是数据清洗和实体匹配,不是 Neo4j 本身。所以我拿到这份源码后的操作顺序是:先看你手上的 CSV 能不能对上 ID,再看 Neo4j 连接能不能通,最后才动问答逻辑。

问答请求链路通常是这样的:前端提交问句,Flask 路由收下,交给一个问答服务类,类里先做意图分类,再抽取实体,然后调知识图谱查询,最后把结果格式化返回。由于数据量不大,这种单体结构完全够用,也比前后端分离更容易在答辩现场演示。还有一点值得说:这套资源把数据放在 CSV 而不是数据库脚本里,最大的好处是离线可读、方便 Python 处理,也方便你替换成自己的数据集。只要保持列名不变,问答逻辑几乎不用动。你完全可以换成漫威电影、动画番剧或者图书数据,CSV 文件本身就是你的“数据接口”。这个收益在中期答辩时非常明显,导师问“数据哪来的”,你直接给 CSV 和清理脚本就能说清楚。

2.3 那些 CSS 文件到底做什么用:先分清静态资源和业务代码

目录里有 bootstrap.css、style000.css、font-awesome.css、owl.carousel.css、poposlides.css、circles.css、style.css 一堆样式,第一次见这股阵仗的初学者,容易误以为这是下载来的整站模板。实际上这些文件只负责页面表现:bootstrap 是最基础的栅格和组件样式,font-awesome 提供图标,owl.carousel 和 poposlides 是做轮播和滚动展示的插件样式,circles 多半用于圆形图表,style.css 是定制样式。它们的存在说明资源包带了一个相对完整的前端页面,不需要你另找模板。

处理方式是不用逐行读这些 CSS。把入口 HTML 模板打开,找到引入 CSS 的标签,就知道哪个文件作用于哪个模块。真要替换成自己的界面,只保留 style.css 和 bootstrap.css 也够用。如果你只想跑通问答逻辑,这些静态文件可以直接忽略,它不会影响 Neo4j 导入和问答结果。拿到资源先分清“业务代码”和“静态资源”,能省下不少无效阅读时间。

3. 把 CSV 变成图谱:数据清洗、实体对齐与 Cypher 导入

3.1 用 pandas 清洗三份 CSV:编码、空值、去重与列名规范

清洗这一步决定后面百分之八十的成败。我拿到 CSV 第一件事是判断编码。用 Excel 改过或 Windows 环境下另存过的 CSV,经常带有 BOM 头,pandas 用默认 utf-8 读会把第一列列名读成带“\ufeff”的脏字符串;另外 CSV 里某些字段因为含逗号,会被解析成多列。所以读取时我一般固定用 utf-8-sig 和 engine="python"。

import pandas as pd movie = pd.read_csv("movie.csv", encoding="utf-8-sig", engine="python") person_to_movie = pd.read_csv("person_to_movie.csv", encoding="utf-8-sig", engine="python") movie_to_genre = pd.read_csv("movie_to_genre.csv", encoding="utf-8-sig", engine="python") # 后续 Cypher 里要用一致的主键,先把列名固定成英文 movie.columns = ["movie_id", "title", "rating", "release_date"] person_to_movie.columns = ["person_id", "person_name", "movie_id", "role"] movie_to_genre.columns = ["movie_id", "genre_name"] # 主键必须不为空且不重复 movie = movie.dropna(subset=["movie_id"]).drop_duplicates(subset=["movie_id"]) person_to_movie = person_to_movie.dropna(subset=["person_id", "movie_id"]) movie_to_genre = movie_to_genre.dropna(subset=["movie_id", "genre_name"]) # 关键一步:主键统一转成字符串并去掉空格,防止 Cypher 里数字和字符串不匹配 movie["movie_id"] = movie["movie_id"].astype(str).str.strip() person_to_movie["movie_id"] = person_to_movie["movie_id"].astype(str).str.strip() person_to_movie["person_id"] = person_to_movie["person_id"].astype(str).str.strip() movie_to_genre["movie_id"] = movie_to_genre["movie_id"].astype(str).str.strip()

几个参数值得说清楚。encoding="utf-8-sig" 会在读取时自动吃掉开头的 BOM,多花的时间几乎为零,但能避免很多“列名对不上”的玄学问题;engine="python" 是让 pandas 用 Python 解析器而不是 C 解析器,碰到字段里有逗号、引号错位时更稳,缺点是慢,但这份数据量根本感觉不出来。dropna 和 drop_duplicates 是实体唯一性的第一道防线,因为 LOAD CSV 导入时如果主键重复,MERGE 会静默覆盖或产生意外属性。astype(str) 也很有必要,LOAD CSV 读进来的字段全部是字符串,而 Excel 里的 ID 容易被存成数字,两边不一致就容易匹配失败。

清洗完以后,如果你手上的 person_to_movie.csv 只有 person_id 和 movie_id,没有单独的人物表,需要额外拆一张 Person 节点表:

person = person_to_movie[["person_id", "person_name"]].drop_duplicates() person.to_csv("person.csv", index=False, encoding="utf-8-sig")

person.csv 是给 Neo4j 导入“人物节点”用的中间文件。实际项目里还可能遇到同一个人名对应多个 person_id 的情况,这就属于实体对齐任务,放到后面小节讲。

提示:清洗脚本和导入脚本建议分开保存。清洗只产出 CSV,导入负责写 Neo4j,这样换数据集时不用改导入逻辑。

3.2 Neo4j 建约束和索引:先定义实体唯一性

在导入数据前,我强烈建议先在 Neo4j 里建好唯一约束。它有三个作用:防止同一个电影、人物、类型出现重复节点;加速后续查询;告诉评审你的建模有“唯一性设计”。以下 Cypher 在 Neo4j 4.x 和 5.x 里都可以直接执行:

CREATE CONSTRAINT movie_unique IF NOT EXISTS FOR (m:Movie) REQUIRE m.movie_id IS UNIQUE; CREATE CONSTRAINT person_unique IF NOT EXISTS FOR (p:Person) REQUIRE p.person_id IS UNIQUE; CREATE CONSTRAINT genre_unique IF NOT EXISTS FOR (g:Genre) REQUIRE g.genre_id IS UNIQUE;

注意这里给 Genre 节点加了 genre_id 属性,它可以直接用 genre_name 作为 ID。约束一旦创建,后面再往库里写入相同主键时,CREATE 会直接报错,所以数据导入脚本里才统一用 MERGE。如果你用的是老版本 Neo4j 3.x,要写成 FOR (m:Movie) ON (m.movie_id) ASSERT m.movie_id IS UNIQUE,很多教程没区分版本,照抄会翻车。

3.3 批量导入节点与关系:LOAD CSV 与逐条写入的选择

节点导入用 LOAD CSV 最省事。先把清洗好的 movie.csv 和 person.csv 放进 Neo4j 的 import 目录。在社区版默认配置里,LOAD CSV 只能读 file:/// 开头的文件,不能随便读磁盘任意路径,这是 Neo4j 出于安全考虑的限制。

LOAD CSV WITH HEADERS FROM "file:///movie.csv" AS row MERGE (m:Movie {movie_id: row.movie_id}) SET m.title = row.title, m.rating = toFloat(row.rating), m.release_date = row.release_date; LOAD CSV WITH HEADERS FROM "file:///person.csv" AS row MERGE (p:Person {person_id: row.person_id}) SET p.name = row.person_name;

用 MERGE 而不是 CREATE,是因为这条导入脚本你可能要跑好几次;MERGE 保证按主键幂等执行,重复跑不会生成重复电影。rating 字段这里用 toFloat 转一次,否则 Cypher 里排序时会把评分按字符串排序,“9.0”会排在“10.0”前面,这类隐蔽问题在问答排名场景里特别容易踩。

关系和节点不一样:person_to_movie.csv 里的 role 字段决定了是“出演”还是“导演”,用纯 Cypher 写条件比较绕,我一般选择用 Python 的 neo4j driver 逐条建立关系。数据量只有几百上千条,这种写法的性能完全够,而且逻辑清楚:

from neo4j import GraphDatabase driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "your_password")) def create_relations(person_to_movie_df): with driver.session() as session: for row in person_to_movie_df.itertuples(): if "导演" in str(row.role): rel_type = "DIRECTED" else: rel_type = "ACTED_IN" session.run( f""" MATCH (p:Person {{person_id: $pid}}) MATCH (m:Movie {{movie_id: $mid}}) MERGE (p)-[:{rel_type}]->(m) """, pid=row.person_id, mid=row.movie_id, )

这里 f-string 只拼接关系类型,关系类型是程序里算出来的,不是用户输入,没有注入风险;而节点 ID 必须走参数,避免拼接字符串碰到特殊字符。如果用 py2neo,把 session.run 换成 graph.run,逻辑一致。批量的另一种做法是先写出 relationship CSV 再用 LOAD CSV,但需要保证 role 到关系类型的映射在文件里就已经处理好,适合数据量更大时用。

3.4 导入后自检:孤立节点和重复关系怎么查

导入完成不要急着写问答,先做三分钟自检。先在 Neo4j Browser 里执行三个统计查询,看看节点数和关系数是否符合预期:

MATCH (m:Movie) RETURN count(m) AS movies; MATCH (p:Person) RETURN count(p) AS persons; MATCH (p:Person)-[r]->(m:Movie) RETURN type(r), count(*) AS cnt ORDER BY cnt DESC;

第三个查询如果出现多个关系类型,说明 role 字段没洗干净。还有一种常见情况:movie_id 在 CSV 里有前导空格,或者 person_id 是数字格式,导致关系表里的 ID 匹配不上电影节点,结果就是一堆孤立电影。查孤立节点的语句要记住:

MATCH (m:Movie) WHERE NOT exists((m)<--()) RETURN m.title LIMIT 20;

有孤立节点不一定会报错,但“查某导演的电影”时结果会莫名少几部。所以我的习惯是导入后先把这个查询跑一遍,看到返回空列表才继续做问答层。

4. 让自然语言落到图谱:问句分类与 Cypher 查询组装

4.1 意图分类:规则模板为何是毕业设计的稳妥选择

问答层最核心的模块是“把自然语言变成 Cypher”。现在大模型很火,但毕设项目我一般不推荐直接上大模型做语义解析,原因有四个:需要 GPU、响应慢、结果不可控、评委追问一句“底层怎么实现的”很难讲清楚。规则模板虽然“笨”,但每个分支都是你能解释的逻辑,也足够覆盖电影问答的常见问法。

先做意图分类。常见意图有四类:查某人的作品、查某部电影的导演、查某部电影的类型、查评分排名。用关键词加优先级就能处理:

def parse_intent(question: str) -> str: q = question.strip() # 注意顺序:'谁导' 先命中导演意图,因为它比 '导演' 更具体 if any(w in q for w in ["谁导", "导演"]): return "director_of" if any(w in q for w in ["主演", "演过", "出演", "参演"]): return "acted_in" if any(w in q for w in ["类型", "什么类型", "属于什么"]): return "genre_of" if any(w in q for w in ["评分最高", "评分从高到低", "排名"]): return "top_rated" return "unknown"

这个函数的边界条件是关键词顺序。比如“谁导演的”同时包含“导演”和“谁导”,必须先匹配更具体的“谁导”;“评分最高的科幻片”同时有“评分最高”和“类型”两个信号,如果意图判断先命中 genre_of,就会丢掉排名逻辑。所以我的方法是先抽实体,再判断有没有排序词,最后再分主意图。

4.2 电影名/人名抽取:基于实体词典的最大匹配

问句里实体名是最不确定的部分。用户可能说“周星驰演的电影”,也可能直接说“星爷的电影”,而库里存的是全名。通用分词工具如 jieba,会把“大话西游之大圣娶亲”切成“大话西游 / 之 / 大圣娶亲”,只匹配“大话西游”会查出两条不同电影。所以更稳的做法是先把自己库里所有电影名、人名做成词典,按长度倒序做包含匹配。

def load_entities(graph): entities = { "movie": [r["title"] for r in graph.run("MATCH (m:Movie) RETURN m.title AS title")], "person": [r["name"] for r in graph.run("MATCH (p:Person) RETURN p.name AS name")], } return entities def extract_entity(question: str, entities): q = question.replace(" ", "").replace("\u3000", "") for name in sorted(entities["movie"], key=len, reverse=True): if name in q: return "movie", name for name in sorted(entities["person"], key=len, reverse=True): if name in q: return "person", name return None, None

排序按长度倒序是这里的关键。如果不排序而按字典顺序,“功夫”会先于“功夫熊猫”被命中,用户明明问的是《功夫熊猫》,最后查出来的却是《功夫》。“先长后短”就是最大匹配的核心思路。还有一个处理是去掉全角空格和普通空格,因为用户在浏览器里输入的文本经常混入不可见字符,如果不处理,contains 匹配会失败。

抽实体放在意图分类之前还是之后,取决于问法。我实践下来的顺序是:先抽实体,再判断意图。因为“评分最高”这类词不依赖实体,而“周星驰演过哪些喜剧”需要先知道实体是人名,才能把“评分最高”和“出演”组合起来。顺序换一下,代码会多出很多 if 分支。

4.3 把意图映射成 Cypher 并拼接答案

实体和意图都出来后,剩下就是模板映射。每个意图对应一段 Cypher 模板,用参数化查询把实体名传进去,避免拼接字符串带来的引号问题。下面是一个简化版:

def build_cypher(intent: str, entity_type: str, entity_name: str): if intent == "acted_in" and entity_type == "person": return """ MATCH (p:Person {name: $name})-[:ACTED_IN]->(m:Movie) RETURN m.title AS title, m.rating AS rating ORDER BY m.rating DESC """ if intent == "director_of" and entity_type == "movie": return """ MATCH (m:Movie {title: $name})<-[:DIRECTED]-(p:Person) RETURN p.name AS name """ if intent == "genre_of" and entity_type == "movie": return """ MATCH (m:Movie {title: $name})-[:BELONGS_TO]->(g:Genre) RETURN g.name AS genre """ return None

然后用统一入口执行:

def run_qa(question: str): intent = parse_intent(question) entity_type, entity_name = extract_entity(question, entities) if not entity_name: return ["我还没学会回答这个问题,试着换个说法"] cypher = build_cypher(intent, entity_type, entity_name) if cypher is None: return ["这个问题缺少可执行的查询模板"] result = graph.run(cypher, name=entity_name).data() return format_answer(result, intent)

format_answer 按意图拼答案,比如 acted_in 就把每个 title 用顿号连接,查不到时返回“库中没有找到相关作品”。这一步看起来简单,但很重要:很多源码在查不到结果时直接返回空页面,用户体验很不好,也容易被评委挑出边界场景。加上一个空结果兜底,答辩时就能多说一句“我处理了查无此人、查无此片的情况”。

5. 避坑与排查:从环境初始化到中文匹配的高频故障

5.1 环境初始化:Python 版本、依赖版本与 Neo4j 认证

这份源码的依赖集中在 py2neo 或 neo4j-driver、Flask、pandas 这几项。很多跑不通的问题都不是业务逻辑问题,而是版本组合不对。我本地使用过的组合是 Python 3.8 + Neo4j 4.4 + py2neo 2021.2.3,先把流程跑通,再考虑迁移到更高版本。如果你用的是 Python 3.10 以上和 py2neo,连接时容易出现加密协议相关报错,这时候换成官方 neo4j-driver 会更省心。neo4j-driver 的写法是 GraphDatabase.driver,而 py2neo 是 Graph("bolt://localhost:7687", auth=...),区别只在驱动层,业务代码基本不用动。

认证问题几乎每届毕设都会遇到:Neo4j 初次启动默认账号密码是 neo4j/neo4j,首次登录会强制让你改新密码。如果导入脚本里写死旧密码,要么连接报认证失败,要么第一次能连、重启后就连不上。我的习惯是把连接参数统一放到一个 config.py 里,密码、URI、数据库名都从环境变量读取,避免答辩换机器时手忙脚乱。

5.2 运行期高频故障:乱码、中文匹配、LOAD CSV 权限、同名实体

下面这五条,是我拆这类型项目时遇到频率最高的坑,每条都按现象、原因、解决来写:

坑 1:pandas 读出来的第一列列名带着“\ufeff”。现象:print(movie.columns) 第一个列名是 “\ufeffmovie_id”,按 movie_id 取列时报 KeyError。原因:CSV 被 Windows 下 Excel 保存过,带 UTF-8 BOM。解决:读取时加 encoding="utf-8-sig",一次性吞掉 BOM,不要在清洗脚本里 str.replace 反复操作。

坑 2:LOAD CSV 一直提示找不到文件。现象:执行 LOAD CSV WITH HEADERS FROM "file:///movie.csv" 报 Couldn't load the external resource。原因:Neo4j 默认只允许从安装目录下的 import 目录读取 file:/// 开头的文件,你把 CSV 放在了 D 盘任意目录。解决:把三个 CSV 复制到 $NEO4J_HOME/import 目录,重启 Neo4j 再执行;如果实在不想挪文件,也可以改 dbms.security.allow_csv_import_from_file_urls=true,但生产环境不建议这么放开。

坑 3:问“功夫”查不到,库里明明有《功夫》。现象:问答返回空列表,用 Cypher 手动查又能查到。原因:用户输入“功夫”没带年份,库里存的是“功夫(2004)”,或者电影名里混了全角空格。解决:导入时给电影加一个 alias 属性,把“功夫(2004)”的标题剥离年份后存成别名;抽取实体时优先查 title,再查 alias。这个内容我会在 6.2 里给出具体做法。

坑 4:评分排序结果不对。现象:查“评分最高的电影”返回的是“9.0、8.7、10.0”,10.0 排到了最后。原因:rating 字段以字符串形式进入 Neo4j,字符串排序按字符逐位比较。解决:LOAD CSV 时用 toFloat(row.rating) 转类型;如果已经在库里了,用 SET m.rating = toFloat(m.rating) 批量修正。

坑 5:同名演员把问答结果搞混。现象:问“张国立演过哪些电影”,返回里混进了另一位演员的作品。原因:person 节点只按名字唯一约束,没有区分出生日期或人物 ID。解决:确认 person_id 在源数据里是否全局唯一;如果唯一,把关系建立全部改为按 person_id 匹配,不要按 name 匹配。若源数据只有名字,就需要额外维护一个别名表做消歧。页面问答时可以额外列出“同名人物”给用户选择,这也是一个很实用的小功能。

6. 把它改造成“你的”毕设:答案校验与两个低成本技巧

6.1 搭一个最小的答案校验脚本

问答系统的通病是你只记得测试过成功的问法,忘记验证失败的问法。我拿到这类源码后会先写一个十几行的冒烟测试集,把每个意图的中文问法、预期答案都放进去,跑一遍统计准确率。

test_set = [ ("周星驰主演过哪些电影", ["功夫", "大话西游"]), ("《功夫》的导演是谁", ["周星驰"]), ("《大话西游》是什么类型", ["喜剧"]), ("评分最高的科幻片", ["星际穿越"]), ("张国立的作品", []), # 预期失败,看兜底逻辑 ] correct = 0 for question, expected in test_set: answer = run_qa(question) hit = len(set(answer) & set(expected)) > 0 correct += int(hit) print(f"{question}: {answer} -> 期望 {expected} -> {'OK' if hit else 'MISS'}") print(f"准确率: {correct / len(test_set):.2%}")

这里用集合交集判断命中,而不是完全相等,是因为答案顺序、标点不影响语义,能容忍“大话西游”和“大话西游之月光宝盒”这类差异。每次修改问答模板或清洗逻辑后,跑一遍这个脚本,能很快发现哪一类问法被改坏了。这个方法也是答辩时可以说出口的“验证策略”。

6.2 两个低成本改造:jieba 候选词和前端实体提示

第一个改造是给实体抽取加一层模糊兜底。用户输入“星爷”或“周星星”这类别称,词典匹配会落空。常见做法是引入 jieba 切词后做候选召回,但一定要把库里的电影名优先注册进词典:

import jieba for name in entities["movie"] + entities["person"]: jieba.suggest_freq(name, True)

suggest_freq 的作用是提高整词成词概率,避免“大话西游之大圣娶亲”被拆碎。配合全量实体名做包含匹配,比纯正则更抗造。这不会让你的准确率一夜暴涨,但能把问句里“别名”“简称”的体验拉上去。

第二个改造是前端输入候选列表。前端模板里给搜索框加一个数据源,后端暴露一个 /api/entity/search 接口,根据用户已输入的两个字,返回前十个相似的电影名或人名。用户基本不会打错全名,问答命中率自然就上去了。这个接口实现起来只有十几行,但答辩演示时比纯空框好看得多。改造完以后,我每次拿到新的 CSV 数据都会强制走一遍同样的流程:清洗、建约束、导入、跑测试集。那次就是因为我偷懒跳过数据核对,直接把新电影导进 Neo4j,结果上映年份列错位,评分排名整整乱了一晚上。从那以后,数据进库前的自检脚本就成了固定动作,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 8:50:36

豆包工作如何完成从需求分析到成果交付的长任务

豆包工作是豆包品牌下面向个人、团队和企业的智能体工作平台&#xff0c;核心价值是帮助用户完成文档、表格、PPT、网页和系统搭建等复杂任务&#xff0c;把想法转化为可交付的工作成果。不同于只能单次问答的普通AI&#xff0c;豆包工作可以承接跨多环节的长任务&#xff0c;从…

作者头像 李华
网站建设 2026/10/3 8:50:27

【生产力】Jev:让 AI 接手那些每天重复的小判断

导读&#xff1a;一封邮件该交给谁&#xff1f;一份资料值得读吗&#xff1f;一个请求能否自动处理&#xff1f;每天拖慢工作的&#xff0c;常常是成百上千次这样的小判断。Jev 尝试把它们变成软件可以直接使用的结果&#xff1a;选择类别、给出评分、估计某个条件是否成立。它…

作者头像 李华
网站建设 2026/10/3 8:49:33

玻璃钢冷却塔厂家实力参考:山东华科环境科技用户力荐

玻璃钢冷却塔怎么选才不踩坑?这家潍坊厂家把实力摆在了明面上工业循环水降温设备看似普通&#xff0c;实则直接关系到产线能否连续运转、电费账单是否失控。一台选错的冷却塔&#xff0c;带来的可能是填料频繁堵塞、风机常年高耗电、壳体三五年就老化渗漏。本文以议论文的视角…

作者头像 李华
网站建设 2026/10/3 8:48:12

计算机网络学习避坑指南:教材搭配、以太网实训与408备考要点

上次那篇《计算机网络学习总结》发出去之后&#xff0c;后台陆续收到不少私信&#xff0c;问题高度集中&#xff1a;教材到底选自顶向下还是谢希仁、头歌实训平台上的以太网题怎么老做不对、期末复习和408考研能不能用同一套打法、还有背不完的“网络八股”到底怎么记。说实话&…

作者头像 李华
网站建设 2026/10/3 8:47:10

DeepSeek LeetCode 147. 对链表进行插入排序 Python3实现

# Definition for singly-linked list. # class ListNode: # def __init__(self, val0, nextNone): # self.val val # self.next nextclass Solution:def insertionSortList(self, head: ListNode) -> ListNode:dummy ListNode(0) # 哑结点&#x…

作者头像 李华