ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

民航知识图谱问答系统:从词典匹配到Neo4j查询实现

民航知识图谱问答系统:从词典匹配到Neo4j查询实现 简介面向民航业知识图谱与自动问答系统学习者的一份完整工程包适用于有一定 Python 基础、希望掌握知识图谱构建及 NLP 问答落地流程的开发者也可作为课程设计或毕业设计的参考。项目围绕民航数据覆盖数据抓取与预处理、实体关系抽取、图谱存储及 SPARQL 查询问答等关键环节同时包含 Web 展示界面、交互脚本和配置说明。包内共 76 个文件以 28 个 Python 源码、13 个 HTML 页面、12 个 txt 说明与数据文件为主另含 ipynb 示例、PNG 图示以及 json、md 等辅助资源整体仅 4.16MB结构清晰便于按模块学习。目前已有205人学习下载。借助实体识别、图数据库构建和语义匹配模块以及配套的测试用例和样例问题读者可以快速跑通从原始数据到自动问答的完整链路并结合 README 与配置文件进行二次改造或场景迁移。1. 民航知识图谱问答系统先跑起来再看代码在知识图谱课程设计、NLP 大作业这类场景里“基于民航业知识图谱的自动问答系统”是一个比通用聊天机器人更容易展示成果的选题它把机场、航线、航班、航空公司这些实体以及“从北京到上海有哪些航班”这样的业务问题落到一张结构化图谱上。这篇博客要拆解的是一个开源的 Python 实现源码里没有调大模型接口而是用“词典匹配 模板规则 Neo4j 查询”来把整个问答链路跑通。它的工程代码只有十几个 Python 文件做完能让你一次性看明白前端查询、意图解析、图谱存储三块是怎么接起来的。适合手里有 Python 基础、想快速理解知识图谱问答系统全貌的读者。2. 源码结构与运行前置从文件清单到 Neo4j 连接2.1 项目文件清单与分层设计先说结论这个项目不是一个重型微服务而是把问答拆成“构建图谱—词表加载—问题解析—答案检索—展示”五段。源码在QA-CivilAviationKG-master目录下核心文件不过十余个。我按提问者的视角把这些文件分成四层分层文件职责数据层data/存放实体表、关系表、词典等原始数据图谱构建层build_cakg.py解析数据并写入 Neo4j完成实体合并与关系创建问答解析层const.py,chatbot.py,question_classifier.py,question_parser.py加载词典、分类问题、抽取槽位、生成查询条件检索与展示层answer_search.py,run_web.py,run_cmd.py,web/,demo/执行 Cypher 查询组织答案提供命令行与 Web 两种入口辅助文件test/,doc/,README.md,LICENSE,requirements.txt单元测试、说明文档、依赖列表与授权信息这种分层的直接好处是你可以只替换build_cakg.py的抽数逻辑把民航数据换成医疗数据问答层完全复用也可以单独替换question_classifier.py把词典匹配换成 BERT 二分类检索层不用动。对于课程设计每一层都能单独验收。需要特别留意const.py。它不像名字看起来那样只放常量实际还承担了“全局配置”的角色Neo4j 地址、账号密码、实体名与标签映射、关系映射以及问答模板都集中在这里。我把这类文件类比为整个项目的“接线板”改图谱模型时先改它而不是满项目找字符串。2.2 环境安装与 Neo4j 连接配置在跑通之前先把环境初始化好。依赖写在requirements.txt里通常包括py2neo、neo4j、flask、jieba、pyahocorasick这类库。安装命令cd QA-CivilAviationKG-master python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt这里建议用虚拟环境而不是直接pip install到全局避免和本机其他项目的包版本撞车。如果你刚开始装 Python记得在安装向导里勾选“Add python.exe to PATH”否则后面执行pip会提示命令找不到。然后打开const.py把 Neo4j 连接信息改成你本机的配置# -*- coding: utf-8 -*- NEO4J_HOST bolt://localhost:7687 NEO4J_USER neo4j NEO4J_PASSWORD 123456 # 实体与 Neo4j Label 的映射 entity_type_to_label { city: City, airport: Airport, airline: Airline, flight: Flight, airplane: Airplane, }这段配置说明三点第一bolt://localhost:7687是 Neo4j 的 Bolt 协议入口需要确认 Neo4j 桌面版或社区版已经启动不要只开浏览器面板不看服务状态第二entity_type_to_label决定了后边build_cakg.py建节点时用哪个标签第三如果你的 Neo4j 是 5.x 初次启动会强制修改默认密码请把改后的密码填到NEO4J_PASSWORD否则连接时会报Unauthorized。2.3 一行命令启动问答服务并自测依赖装好、Neo4j 启动后可以先用命令行入口自测python run_cmd.py --question 北京到上海有哪些航班 python run_web.py --port 5000run_cmd.py通常会读取argparse参数再把问题交给chatbot.py的ChatbotGraph处理。run_web.py则是启动一个 Flask 服务浏览器访问http://localhost:5000就能打开web/下的演示页面。我用这个顺序跑是因为命令行模式下出错的堆栈更直接能分辨是词典没加载还是 Neo4j 连接失败比直接开网页调试更高效。如果命令行模式下拿到空答案大部分时候不是代码 bug而是图谱里根本没写入对应该问题的数据。下一步先检查build_cakg.py是否把data/目录里的数据真正导进去了。3. 民航知识图谱构建实体建模与 CQL 批量写入3.1 多源数据的实体划分与关系定义民航领域的自然语言问题高频实体集中在“城市—机场—航空公司—航班—飞机”这几类。data/目录的常见结构是几张 CSV 或 JSON 表一张存机场基础信息三字码、名称、所在城市、坐标一张存航空公司一张存航班计划航班号、起降城市、执飞机型、班期。这个项目把它们统一映射成下面这张关系模型头实体关系尾实体业务含义FlightSTART_FROMAirport航班的起飞机场FlightARRIVE_ATAirport航班的到达机场FlightOPERATE_BYAirline航班由哪家航司执飞AirportLOCATED_INCity机场所在城市CityHAS_AIRPORTAirport城市拥有的机场反向冗余我建议在写代码前先在纸上画出这张模型图因为后边所有问答模板都是在为这些关系做查询。比如“北京到上海有哪些航班”实际要查询的是Flight - START_FROM - 北京机场且Flight - ARRIVE_AT - 上海机场。如果建模时把“北京到上海”直接存成一个字段问答系统就退化成 SQL like 查询了知识图谱的价值也就没了。3.2 build_cakg.py 的数据清洗与节点创建对应实体表里的行用py2neo的Node和Relationship写入即可。build_cakg.py的核心逻辑通常长这样from py2neo import Graph, Node, Relationship graph Graph(bolt://localhost:7687, auth(neo4j, 123456)) def load_csv(path): # 读取 data 目录下的 csv返回 list[dict] pass def build(): graph.delete_all() airport_cache {} city_cache {} for row in load_csv(data/airport.csv): code row[code] node Node(Airport, codecode, namerow[name], cityrow[city]) airport_cache[code] node graph.merge(node, Airport, code) for row in load_csv(data/flight.csv): no row[flight_no] fnode Node(Flight, nono) graph.merge(fnode, Flight, no) dep_code row[dep_code] arr_code row[arr_code] graph.create(Relationship(fnode, START_FROM, airport_cache[dep_code])) graph.create(Relationship(fnode, ARRIVE_AT, airport_cache[arr_code])) if __name__ __main__: build()代码里有几个关键点graph.delete_all()在每次构建前清空整个库避免多次运行造成重复数据graph.merge(node, Airport, code)会先按code属性查找节点存在则返回已有节点不存在才创建比直接graph.create更安全但Relationship没有做 merge重复执行会在两个相同节点间生成多条关系所以我一般会在关系创建前先做一次MATCH去重或者直接用graph.run写一段MERGECypher 来兜底。许多课程设计的data/文件并不是标准 CSV而是一行一个三元组比如北京 位于 首都机场 上海 位于 上海虹桥机场对于这种数据更合适的做法是写一个通用三元组读取函数把“头实体、关系、尾实体”分别映射到Node和Relationship再批量提交。使用 Python 的pandas.read_csv能省掉不少编码和分隔符问题但要注意to_dict()后的列顺序以及NaN值的清洗。3.3 写入后的验证与常见数据坑建完图之后的验证我一般不在 Python 里 print 节点数量而是直接打开 Neo4j Browser 跑一段 CypherMATCH (f:Flight)-[:START_FROM]-(a:Airport) RETURN f.no, a.code LIMIT 10;这段查询的目的是确认Flight到Airport的路径能走通同时也检查方向。很多人建模时把START_FROM方向建反问答系统查“北京到上海”时怎么也查不出数据。遇到这种情况跑到浏览器里把路径反向查一下立刻能定位。数据层常见的坑有三个CSV 文件带着 UTF-8 BOM首列列名会变成\ufeffcity导致后续KeyError城市名有“北京市”和“北京”两种写法构建图谱前必须做别名表机场三字码是唯一键不能把城市名当唯一键用因为“北京”对应首都机场和大兴机场两个机场稍一错就会让问答系统的“北京到上海”结果重复或缺少。如果这部分只做课程设计掌握到“图里有 500 个机场、3000 条航班用 Cypher 能看到节点”就足够了。再往下走就是问答层的事了。4. 问句处理与意图识别AC 自动机、槽位抽取与模板补全4.1 基于词典的实体识别与意图分类前处理问答系统的第一步是把用户输入的“北京到上海有哪些航班”转成结构化查询。由于限定在民航领域实体词典可以通过知识图谱反查出来。question_classifier.py的常见做法是把城市、机场、航司、航班号整理成四个字典然后用 AC 自动机在问句里做多模式匹配匹配到的实体类型直接作为分类特征。# question_classifier.py 结构示意 import ahocorasick def build_automaton(): from const import city_dict, airport_dict, airline_dict a ahocorasick.Automaton() for word in city_dict: a.add_word(word, (CITY, word)) for word in airport_dict: a.add_word(word, (AIRPORT, word)) for word in airline_dict: a.add_word(word, (AIRLINE, word)) a.make_automaton() return a def classify(question): automaton build_automaton() entities [] for end_index, (label, word) in automaton.iter(question): entities.append((label, word)) return entities代码说明Automaton是 AC 自动机库把所有关键词一次性加入后执行make_automaton之后对同一句话只扫描一遍就能找出所有命中的词。这样比对词典做几十次if word in question要快得多在课程演示时也不会产生卡顿感。entities里的每一条都带类型和原词比如(CITY, 北京)、(CITY, 上海)。这里有个容易忽视的细节航班号“CA1234”和城市词“上海”同时出现时分类的结果受词典构建顺序影响。所以我一般会约定优先匹配航班号、机场三字码这类信息量大的词再匹配城市名避免把“北京”从“北京南苑机场”里拆出来。AC 自动机默认最长匹配但不同引擎的行为不一致建议匹配后做一次最长结果筛选。4.2 从槽位填充到中间查询表示question_classifier.py管“分类”question_parser.py管“解析”。解析层拿到带标签的实体后要按照预置模板补齐缺失槽位。例如定义几个意图模板意图触发表达必要槽位示例问题flight_query哪些航班、有航班、航班吗、怎么去dep_city,arr_city北京到上海有哪些航班flight_time_query几点、时间、出发时间、起飞flight_noCA1234 几点起飞airport_query机场在哪、靠近哪里city北京有哪些机场question_parser.py的任务很简单先判断问句命中了上表中的哪类触发表达再检查必要槽位是否已经从实体中提取到了。缺“到达城市”就在结果里标missing_slot不要直接返回空答案。有了中间表示后面生成查询逻辑就统一了。中间表示可以是 Python dict 或 dataclass# 最终生成的解析结果 parsed { intent: flight_query, dep_city: 北京, arr_city: 上海, date: None }这里把“北京到上海有哪些航班”归一化成intentflight_querydep_city北京arr_city上海后续无论问句是“北京飞到上海”“想从北京去上海”都能落到同一个模板。这也是模板问答省事的根本答案不是靠模型猜的而是先填槽再到图谱里查。4.3 为什么限定域场景不上 BERT你可能会有疑问现在都在说大模型、Fine-tune这里还用 AC 自动机和正则模板是不是太老了我的判断是在民航知识图谱这种限定域里几万个实体、十几种问题模式模板方案有三个明显优势不需要标注几千条训练数据每条答案可以反推是哪条规则匹配的方便答辩解释在普通 CPU 上运行时延低于 20ms体验远好于本地跑一个小 Bert。但模板方案的硬边界是“措辞不能太自由”。比如用户问“从北京到上海有航班吗”和“北京前往上海”要扩充到同一个意图需要人工在keywords里补表达。为了减少这种补充可以在const.py里维护一个“说法映射表”把“飞往”“前往”“到”“去”都映射成同一动作词。这不是模型能力不足而是工程上的成本选择——课程设计的数据量根本喂不饱深度学习模型硬上 BERT 反而会在小样本上过拟合。5. 答案检索与 Web 演示Cypher 生成、Flask 接入与旁路词典热更新5.1 将中间表示翻译成可执行的 Cypheranswer_search.py接收 parsed dict然后根据不同意图拼接出查询。对flight_query的查询片段类似def build_flight_query(parsed): dep parsed[dep_city] arr parsed[arr_city] cypher ( fMATCH (dep_airport:Airport {{city: {dep}}}) f-[:START_FROM]-(f:Flight)-[:ARRIVE_AT]- f(arr_airport:Airport {{city: {arr}}}) RETURN f.no, f.dep_time, f.arr_time, dep_airport.code, arr_airport.code LIMIT 20 ) return cypher这里两个{}的匹配条件利用的是飞机场节点上的city属性而不是另行关联城市节点后再查一遍因为我们已经把 city 冗余到 Airport 节点上了。如果建模时没有冗余就需要多一个MATCH关系跳转。这就是为什么建图时要提前规划查询路径。直接拼接字符串有注入风险但只用于本地演示正式产品里应使用neo4j驱动带的参数化查询。匹配到结果后chatbot.py会把记录拼成一句自然语言“CA1234北京首都机场 08:00 起飞上海虹桥机场 10:25 到达每日一班。”到这一句问答链路已经闭环。5.2 用 Flask 把问答封装成 HTTP 接口run_web.py提供的是一个很薄的 Web 层核心逻辑还是调用同一个ChatbotGraph。代码可以简化成from flask import Flask, request, jsonify from chatbot import ChatbotGraph app Flask(__name__) handler ChatbotGraph() app.route(/qa, methods[POST]) def qa(): data request.get_json() question data.get(question, ) answer handler.answer(question) return jsonify({question: question, answer: answer}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)这个接口设计成 POST 而不是 GET是为了避免中文问题在 URL 里被浏览器转义后出现解码问题handler.answer内部串起 classifier、parser、answer_search 三步对前端来说只暴露一个question字段。前端web/目录里就是一个简单的静态页面通过 ajax 把输入框内容发到这个接口并渲染结果。演示时如果 Neo4j 的图谱节点类型很多浏览器右上角可能会看到“知识图谱只显示 25 个标签”的提示。这不是程序问题是 Neo4j Browser 对可视化节点数量的默认限制。可以调整为按f.no聚合显示或者只返回先导结果避免把整张图铺在页面上。5.3 上线前必调的两个细节旁路词典热更新与日志追踪最后说一个很实用的小技巧在const.py维护实体词典时很多人会遇到“图谱里有新机场但问答系统还是答不出来”的情况因为词典是启动时加载到内存的。我为这个项目加了一个旁路逻辑在answer_search.py查询无结果时自动从 Neo4j 重新拉一遍全部实体和旧词典做 diff把新增实体追加到question_classifier的 AC 自动机里。def sync_dict_from_graph(): from const import entity_dict graph_entities fetch_entity_names_from_neo4j() new_words set(graph_entities) - set(entity_dict) if new_words: for word in new_words: automaton.add_word(word, (ENTITY, word)) automaton.make_automaton()这样不重启进程也能让新词典生效演示中途手工向图谱插一条航线刷新页面就能查到。第二点是打开 Flask 的请求日志把每次用户问句、解析中间结果和最终 Cypher 追加到一个qa_log.jsonl文件里出坏答案时直接看日志比在 Flask 里打断点快得多。如果你要把系统迁移到医疗问诊把const.py里的标签映射换成 ICD-10 诊断编码和检查项目再在data/里放同样格式的 CSV问答链路基本就能复用。本文还有配套的精品资源点击获取
返回列表