简介这份源码面向具备Python基础、希望入门知识图谱与问答系统的开发者及计算机相关专业学生围绕国内大学信息查询与高考志愿填报辅助场景提供一套可运行的完整实现。项目以Python脚本为核心借助Neo4j图数据库存储大学、专业、地理位置等节点与关系并通过问题分类、自然语言解析与回答模板等模块完成从提问到应答的闭环同时预留了与企业微信等外部系统对接的接口。压缩包共39个文件、约15.04MB包含17个py脚本、4个csv与3个json数据文件、4个txt说明、2个png及1个jpg图片另有pyc编译文件、cfg配置、log日志、license与gitignore等分别承担逻辑实现、数据存储、界面展示、运行监控与版本管理职责。已有434人学习下载。读者可据此理解知识图谱构建流程、问答模块拆分方式与数据组织思路并在此基础上扩展专业、院校等查询维度。1. 从一份大学知识图谱问答源码说起它到底能解决什么高考志愿填报季家长和考生最头疼的不是没数据而是数据散在几十个页面里——学校官网一套格式、省考试院一套格式、第三方平台又是另一套。想查广东省内、985、计算机专业、去年录取位次在 8000 名左右的学校得开五六个标签页手动比对。这份基于 Python 和 Neo4j 的国内大学知识图谱问答系统源码干的就是把这种多跳查询压成一句话的事你问广东有哪些 985 大学的计算机专业系统在图上走一圈直接给答案。它适合三类人一是做课程设计或毕设的学生需要一套能跑通、结构清晰的图谱问答工程二是想入门知识图谱构建的开发者拿它当 Neo4j 建模和 Cypher 查询的实操样本三是做高考志愿类工具的产品或独立开发者想验证图谱问答这条技术路线在垂直领域的可行性。整套源码覆盖了从数据抽取、图数据库建模、实体关系导入到自然语言问句解析的完整链路不是那种只贴几个查询语句的玩具 demo。2. 图谱数据建模大学、专业、分数线怎么落成节点和关系2.1 为什么选 Neo4j 而不是关系型数据库大学信息查询的本质是关系遍历。用 MySQL 存查某大学某专业在某省的录取线要 join 三四张表再往上叠这个专业属于哪个学院、这个学院还有哪些专业、这些专业在别的省分数线多少SQL 会迅速膨胀成嵌套子查询。Neo4j 的图模型把这类问题变成路径查找一跳就是一条边多跳就是路径长度语义直观且查询性能随深度增长衰减更慢。常见做法是把大学、专业、省份、年份、科类文/理/综合都建成独立节点录取记录作为关系属性挂在专业-省份-年份这条边上。这样2023 年广东物理类某专业最低分就是一条带属性的边而不是一行需要过滤的记录。2.2 节点与关系的建模脚本下面这段 Cypher 是建约束和导入节点的核心逻辑我一般会先跑约束再灌数据否则后期去重很痛苦// 为大学、专业、省份建立唯一约束防止重复导入 CREATE CONSTRAINT university_name IF NOT EXISTS FOR (u:University) REQUIRE u.name IS UNIQUE; CREATE CONSTRAINT major_name IF NOT EXISTS FOR (m:Major) REQUIRE m.name IS UNIQUE; CREATE CONSTRAINT province_name IF NOT EXISTS FOR (p:Province) REQUIRE p.name IS UNIQUE; // 创建大学节点附带层次、地区、类型等属性 MERGE (u:University {name: 中山大学}) SET u.level 985, u.city 广州, u.type 综合类; // 创建专业节点 MERGE (m:Major {name: 计算机科学与技术}) SET m.category 工学; // 建立大学开设专业的关系 MATCH (u:University {name: 中山大学}) MATCH (m:Major {name: 计算机科学与技术}) MERGE (u)-[:OFFERS]-(m);逻辑说明MERGE而不是CREATE是因为数据源里同一所大学可能出现在多个批次或年份用MERGE保证幂等重复执行不会产生重复节点。SET后面跟的属性按你手头数据源的字段来定常见的有办学层次、所在城市、院校类型。OFFERS关系表示开设如果数据里有学院维度可以再加一层College节点让University-[:HAS_COLLEGE]-College-[:OFFERS]-Major形成三级结构。参数说明约束名university_name可以自定义但建议保持语义清晰IF NOT EXISTS在 Neo4j 4.x 以上支持3.5 版本语法不同需要写成CREATE CONSTRAINT ON (u:University) ASSERT u.name IS UNIQUE。如果你用的是 Neo4j Desktop 或社区版导入前确认版本语法差异是新手翻车高发区。2.3 录取分数线的关系属性设计分数线不是节点是关系属性。把专业-省份-年份-科类作为关系的复合定位条件分数、位次、批次作为属性// 建立专业在特定省份年份的录取关系属性存分数和位次 MATCH (m:Major {name: 计算机科学与技术}) MATCH (p:Province {name: 广东}) MERGE (m)-[r:ADMISSION {year: 2023, subject: 物理类}]-(p) SET r.min_score 650, r.min_rank 4200, r.batch 本科批;逻辑说明ADMISSION关系上的year和subject是复合键的一部分查询时用WHERE r.year 2023 AND r.subject 物理类过滤。把分数放在关系上而不是节点上是因为同一个专业在不同省份、不同年份的录取线完全不同做成节点会导致节点爆炸且查询路径变长。参数说明min_score和min_rank是最低分和最低位次实际数据里可能还有平均分、最高分按需加属性即可。batch存批次信息新高考省份可能没有传统批次概念可以改成segment或直接省略。3. 问答链路实现从自然语言问句到 Cypher 查询3.1 问句解析的两种路线与选型问答系统的核心是把广东有哪些 985 大学的计算机专业翻译成 Cypher。常见两条路线一是基于规则的模板匹配二是基于意图分类加槽位填充。这份源码走的是规则词典的轻量路线适合数据规模不大、问句模式相对固定的场景。规则路线的逻辑是先分词再匹配实体词典大学名、专业名、省份名、层次标签然后根据问句中的疑问词和关系词判断查询意图。比如出现有哪些哪些通常对应列表查询出现多少分分数线对应数值查询。相比上深度学习模型规则路线不需要标注数据调试直观缺点是问句变体多了要不断补规则。我一般会先统计用户可能问的句式归纳出 10 到 20 个模板覆盖 80% 的查询需求剩下的长尾再逐步加。这套源码的意图识别模块就是这个思路代码结构清晰改起来不费劲。3.2 实体识别与词典构建实体识别靠词典匹配词典从 Neo4j 里反查生成保证图谱里有什么就能识别什么from py2neo import Graph # 连接 Neo4j默认 bolt 协议端口 7687 graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) def build_entity_dict(): 从图谱中抽取所有实体名称构建识别词典 entity_dict {} # 查询所有大学名称 universities graph.run(MATCH (u:University) RETURN u.name AS name).data() for row in universities: entity_dict[row[name]] University # 查询所有专业名称 majors graph.run(MATCH (m:Major) RETURN m.name AS name).data() for row in majors: entity_dict[row[name]] Major # 查询所有省份名称 provinces graph.run(MATCH (p:Province) RETURN p.name AS name).data() for row in provinces: entity_dict[row[name]] Province return entity_dict # 构建一次缓存起来避免每次问句都查库 ENTITY_DICT build_entity_dict()逻辑说明py2neo是 Python 操作 Neo4j 的常用库Graph对象封装了连接和查询。graph.run()执行 Cypher 并返回结果.data()把结果转成字典列表。把实体名称和类型存成字典后续分词后直接查字典判断是不是实体。参数说明连接地址bolt://localhost:7687是 Neo4j 默认的 Bolt 协议端口如果你改了端口或用了远程主机对应替换。auth里的密码是你安装 Neo4j 时设置的忘了密码可以进 Neo4j Browser 重置。ENTITY_DICT在服务启动时构建一次即可数据更新后重新构建不要每次问句都查库否则响应会慢得让你怀疑人生。3.3 意图识别与 Cypher 模板映射意图识别用关键词匹配加优先级判断不同意图对应不同的 Cypher 模板import jieba def parse_question(question): 解析问句返回意图和识别到的实体 words jieba.lcut(question) entities {University: [], Major: [], Province: []} for word in words: if word in ENTITY_DICT: etype ENTITY_DICT[word] entities[etype].append(word) # 意图判断按关键词优先级匹配 if any(kw in question for kw in [分数线, 多少分, 录取分]): intent query_score elif any(kw in question for kw in [有哪些, 哪些, 列出]): intent query_list elif any(kw in question for kw in [怎么样, 介绍, 简介]): intent query_info else: intent unknown return intent, entities逻辑说明jieba.lcut做中文分词然后遍历词列表匹配实体词典。意图判断用关键词优先级先判断分数线类再判断列表类最后判断介绍类。这个顺序有讲究——广东有哪些 985 大学的计算机专业分数线同时命中有哪些和分数线应该优先走分数线查询所以分数线判断放前面。参数说明关键词列表可以按你的实际问句扩展比如位次排名也该归入query_score。unknown意图要有兜底回复不能直接报错。分词粒度用默认的精确模式即可搜索引擎模式会切出更多短词反而增加误匹配。3.4 查询执行与结果组装意图和实体确定后映射到具体 Cypher 并执行def execute_query(intent, entities): 根据意图和实体执行对应的 Cypher 查询 if intent query_list: # 查某省某层次的大学开设了哪些专业 province entities[Province][0] if entities[Province] else None level 985 if 985 in str(entities) else None cypher MATCH (u:University)-[:OFFERS]-(m:Major) WHERE ($province IS NULL OR u.city $province) AND ($level IS NULL OR u.level $level) RETURN u.name AS university, m.name AS major LIMIT 20 result graph.run(cypher, provinceprovince, levellevel).data() return result elif intent query_score: # 查某专业在某省的录取分数线 major entities[Major][0] if entities[Major] else None province entities[Province][0] if entities[Province] else None cypher MATCH (m:Major)-[r:ADMISSION]-(p:Province) WHERE m.name $major AND p.name $province RETURN r.year AS year, r.min_score AS score, r.min_rank AS rank ORDER BY r.year DESC result graph.run(cypher, majormajor, provinceprovince).data() return result return []逻辑说明query_list意图用MATCH匹配大学和专业的关系WHERE里用$province IS NULL OR的写法实现可选过滤——参数为 None 时不过滤该条件。query_score意图匹配录取关系按年份倒序返回。参数化查询用$占位符不要用字符串拼接否则会有注入风险且 Cypher 缓存失效。参数说明LIMIT 20防止结果过多撑爆前端实际可以分页。ORDER BY r.year DESC让最新年份排前面。如果查询结果为空前端要给出未找到相关数据的提示而不是空白页。4. 环境搭建与数据导入Neo4j 安装和批量灌数的实操细节4.1 Neo4j 版本选择与安装路径Neo4j 有 Desktop、社区版、企业版三个常见选择。做课程设计或本地开发Desktop 最省事图形界面管理数据库一键启动。社区版适合部署到服务器但没有热备份等企业特性。版本上4.x 和 5.x 的 Cypher 语法有差异源码如果用的是 4.x 写法装 5.x 可能报错建议先确认源码里的语法风格再定版本。安装步骤以 Desktop 为例下载安装包后一路默认首次启动会让你创建数据库设置密码。数据库启动后Neo4j Browser 在http://localhost:7474Bolt 端口 7687。如果 7474 被占用改配置文件里的dbms.connector.http.listen_address。提示Neo4j 5.x 默认要求密码至少 8 位且首次登录强制改密码。忘了密码可以停库后删data/dbms/auth文件重置但会清空所有用户。4.2 批量导入 CSV 数据的两种方式数据量小的时候用 Cypher 的LOAD CSV就够数据量大或者需要清洗转换用 Python 脚本批量写入更灵活。先看LOAD CSV的写法// 从 CSV 导入大学数据CSV 需放在 Neo4j 的 import 目录下 LOAD CSV WITH HEADERS FROM file:///universities.csv AS row MERGE (u:University {name: row.name}) SET u.level row.level, u.city row.city, u.type row.type;逻辑说明LOAD CSV WITH HEADERS把首行当列名row.name取对应列的值。file:///指向 Neo4j 安装目录下的import文件夹不是任意路径。MERGE保证重复导入不产生重复节点。参数说明CSV 文件编码建议 UTF-8带 BOM 的话首列名会多出不可见字符导致row.name取不到值。列名大小写敏感row.Name和row.name不是一回事。Python 脚本导入适合需要做数据清洗的场景import csv from py2neo import Graph, Node, Relationship graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) def import_universities(csv_path): 从 CSV 读取大学数据并批量写入 Neo4j with open(csv_path, r, encodingutf-8) as f: reader csv.DictReader(f) tx graph.begin() count 0 for row in reader: # 清洗去掉首尾空格空值转 None name row[name].strip() if not name: continue node Node(University, namename, levelrow.get(level, ).strip(), cityrow.get(city, ).strip()) tx.merge(node, University, name) count 1 # 每 500 条提交一次避免事务过大 if count % 500 0: tx.commit() tx graph.begin() tx.commit() print(f导入完成共 {count} 条)逻辑说明graph.begin()开启事务tx.merge(node, University, name)按 name 属性合并节点存在则更新不存在则创建。每 500 条提交一次是平衡内存和速度的经验值太小提交频繁慢太大事务日志膨胀。参数说明encodingutf-8必须显式指定Windows 默认 GBK 会乱码。row.get(level, )用 get 避免列不存在时 KeyError。tx.merge的第二个参数是节点标签第三个是用于判重的属性名。4.3 数据一致性校验导入后必须校验否则问答系统会给出错误答案。校验分两步一是节点数量对得上二是关系没有孤儿节点。// 统计各标签节点数量 MATCH (u:University) RETURN count(u) AS university_count; MATCH (m:Major) RETURN count(m) AS major_count; // 查找没有开设任何专业的大学孤儿节点 MATCH (u:University) WHERE NOT (u)-[:OFFERS]-(:Major) RETURN u.name AS orphan_university; // 查找没有录取数据的专业 MATCH (m:Major) WHERE NOT (m)-[:ADMISSION]-(:Province) RETURN m.name AS major_without_admission;逻辑说明第一条统计数量和源数据对比。第二条用NOT (u)-[:OFFERS]-(:Major)找没有出边的大学这类节点在问答里查某大学有哪些专业会返回空需要排查是数据缺失还是导入遗漏。第三条同理查专业。参数说明count(u)返回节点数AS起别名方便读取。孤儿节点不一定是错误可能确实没有数据但要确认是源数据就没有还是导入丢了。5. 避坑与排查源码跑不起来时先看这几条5.1 连接报错 Unable to connect to localhost:7687现象Python 脚本一运行就抛连接异常Neo4j Browser 却能正常打开。原因Neo4j 服务没启动或者 Bolt 端口被防火墙拦了或者密码错了但报错信息不直观。解决先确认 Neo4j Desktop 里数据库是 Running 状态再用telnet localhost 7687测端口通不通密码错误的话py2neo会抛认证异常检查auth参数里的用户名密码默认用户名是neo4j不是admin。5.2 Cypher 语法报错 Invalid input现象从源码里抄的 Cypher 在 Neo4j Browser 里执行报语法错误。原因Neo4j 版本不匹配。4.x 的CREATE CONSTRAINT ON ... ASSERT在 5.x 里改成了CREATE CONSTRAINT ... FOR ... REQUIRE反过来也一样。解决先CALL dbms.components()查版本号然后按版本改语法。约束、索引、LOAD CSV的写法在不同大版本间都有差异别硬套。5.3 中文实体识别不出来现象问句里明明有中山大学系统却识别不到返回空结果。原因分词把中山大学切成了中山和大学词典里只有完整名称匹配不上。解决把大学名、专业名加入 jieba 的自定义词典jieba.add_word(中山大学)或者用jieba.load_userdict(dict.txt)批量加载。自定义词典每行一个词可以带词频和词性。5.4 查询结果重复现象问广东有哪些 985 大学同一所大学出现多次。原因MATCH匹配到了多条路径。比如一所大学开设了多个专业MATCH (u)-[:OFFERS]-(m)会为每个专业返回一行大学名就重复了。解决用RETURN DISTINCT u.name去重或者用collect(m.name)把专业聚合成列表一行返回一所大学及其所有专业。5.5 导入数据后问答结果为空现象数据明明导进去了问句也解析出了实体但查询返回空。原因属性值不匹配。比如图谱里大学城市存的是广州市问句里识别出的是广东WHERE u.city 广东自然查不到。或者年份、科类等过滤条件对不上。解决先在图谱里MATCH (u:University) RETURN u.city LIMIT 10看看实际存的值长什么样再调整问句解析里的实体归一化逻辑。城市名带不带市、省份简称和全称都要在词典层面统一。6. 进阶技巧把问答准确率从能用推到好用规则路线的问答系统准确率瓶颈往往不在查询本身而在问句理解的边界处理。我在这类项目上踩过的坑是一开始只盯着主流程等用户真用起来发现广东 985 计算机这种省略了有哪些的问句直接掉进 unknown 意图。后来加了一层兜底——当意图识别为 unknown 但实体识别到了 University 或 Major 时默认走列表查询准确率立刻上了一个台阶。另一个提升点是实体归一化。图谱里存计算机科学与技术用户可能说计科计算机CS这些变体要在词典层面映射到标准名。做法是维护一个别名词典分词后先性别名再查标准实体# 别名词典用户说法 - 图谱标准名 ALIAS_DICT { 计科: 计算机科学与技术, 计算机: 计算机科学与技术, CS: 计算机科学与技术, 中大: 中山大学, 华工: 华南理工大学, } def normalize_entities(entities): 把识别到的实体别名替换为标准名 normalized {University: [], Major: [], Province: []} for etype, names in entities.items(): for name in names: standard ALIAS_DICT.get(name, name) if standard not in normalized[etype]: normalized[etype].append(standard) return normalized逻辑说明ALIAS_DICT.get(name, name)有别名用别名没有就用原名。去重避免同一实体多次加入。这个函数放在实体识别之后、查询执行之前调用。参数说明别名词典可以持久化到 JSON 文件或数据库方便运营维护。别名的粒度要控制太短的别名比如单字计容易误匹配建议至少两个字。验证问答系统好不好用我一般会准备一组测试问句覆盖列表查询、分数查询、介绍查询和边界情况空实体、多实体、无匹配每次改完解析逻辑就跑一遍看命中率和准确率有没有退化。这套源码的测试问句集可以直接拿来扩充把你们省的实际院校和专业加进去跑通之后再接前端。从那以后我每次拿到图谱类项目都强制先跑一遍实体覆盖率和问句命中率统计再动手改代码——数据层面的问题代码写得再漂亮也救不回来。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?