
简介基于故障诊断知识图谱的问答系统项目是面向知识图谱、人工智能、自动化等方向的在校学生与研发人员的完整毕设/课设参考主要解决故障诊断场景下知识如何结构化、图谱如何查询以及问答如何自动匹配等问题。整套资料共76个文件、约1.57MB除源码外还包含配套文档与数据以Python后端代码、前端页面、CSV数据集、字体和图片资源为主其中CSV保存实体关系数据前端脚本与样式支撑Web交互代码量紧凑结构清晰便于快速理解和运行。项目已经过完整测试并获导师认可答辩评审分达到95分内容包含知识图谱建模、基于余弦相似度的问句匹配、模板页面和静态资源模块能够帮助读者理解从数据层、算法层到展示层的系统实现路径。目前已有93人学习下载特别适合直接用于毕业设计、课程设计也可以在此基础上替换业务数据、调整匹配逻辑扩展为其他领域的智能问答应用。1. 故障诊断问答系统不是所有答案都该搜百度车间里最值钱的不是设备是老师傅脑子里的那套“一听声音就知道哪坏了”的经验。这套经验一旦人走了就断了。基于故障诊断知识图谱的问答系统解决的正是经验结构化问题把历史故障记录、维修手册、专家判断拆成“现象—原因—维修方案”的图结构用户用日常话术提问系统自动把问句映射成图查询返回可操作的结论。这个项目的核心难点不在 Cypher 语法而在“用户问的是‘轴承温度偏高咋办’图谱里存的是‘温度过高—润滑不足—更换润滑脂’你怎么让这两句话对上”。本文拆解这个开源项目的完整链路Neo4j 数据建模、余弦相似度问句匹配、Flask 请求响应、以及最终如何调优到能真的给维修师傅用。适合做设备运维、PHM 预测性维护、以及人工智能方向课程设计的人参考新手能照着把流程跑通熟手可以关注我在第五章给出的阈值策略和多轮检索思路。2. 知识图谱建模故障实体关系设计与 Cypher 导入2.1 故障诊断领域的实体、关系与属性设计知识图谱的本质是“用节点和边表示实体及其联系”。在故障诊断场景里我们至少需要四类实体FaultPhenomenon故障现象、FaultCause故障原因、DevicePart设备部件、RepairMethod维修方案。它们之间的关系比通用知识图谱更直白但有明确的业务语义关系类型头节点尾节点业务含义典型例子OCCURS_ONFaultPhenomenonDevicePart现象发生在哪个部件上轴承温度过高 → 发生在 → 轴承CAUSED_BYFaultPhenomenonFaultCause现象由什么原因导致轴承温度过高 → 由…导致 → 润滑不足FIXED_BYFaultCauseRepairMethod该原因用什么方法修复润滑不足 → 用…修复 → 更换润滑脂RELATES_TOFaultCauseFaultCause原因之间的关联润滑不足 → 关联 → 密封失效属性方面每个实体建议至少带一个name字段作为规范化名称另外可以加description、frequency发生频次、cost维修成本等辅助排序字段。项目里喂给图谱的数据来源一般是 Excel 或 CSV列名直接对应属性名后续导入时不需要做复杂映射。2.2 用 Neo4j Cypher 建节点和关系建图前先确认 Neo4j 版本。常见做法是用 Neo4j 5.x 的默认数据库连接地址是bolt://localhost:7687用户名密码默认neo4j/neo4j首次登录会要求改。下面这段 Cypher 可以直接在 Browser 里执行创建一批故障知识种子数据CREATE CONSTRAINT fault_phenomenon_name IF NOT EXISTS FOR (p:FaultPhenomenon) REQUIRE p.name IS UNIQUE; CREATE (p1:FaultPhenomenon {name:轴承温度过高, description:轴承座表面温度超过70°C}) CREATE (p2:FaultCause {name:润滑不足, frequency:23}) CREATE (p3:DevicePart {name:驱动端轴承}) CREATE (p4:RepairMethod {name:更换润滑脂, cost:150}) CREATE (p1)-[:OCCURS_ON]-(p3) CREATE (p1)-[:CAUSED_BY]-(p2) CREATE (p2)-[:FIXED_BY]-(p4);这段代码先为FaultPhenomenon的name字段建立唯一约束避免重复导入产生冗余节点。然后创建四个节点和三条关系。frequency属性会在后续问答排序中派上用场——当多个原因都有可能时优先返回频次高的那个。实际项目中数据量大时不会手写 Cypher而是用 Python 脚本批量执行。常见做法是先用 pandas 读取 CSV再通过 py2neo 的Graph.run()逐行写入。2.3 从结构化表格批量导入图谱先看项目里可能存在的data目录结构一般会包含phenomenon.csv、cause.csv、repair.csv和一张relations.csv。批量导入脚本的简化版如下import pandas as pd from py2neo import Graph, Node, Relationship graph Graph(bolt://localhost:7687, auth(neo4j, password)) df pd.read_csv(data/fault_data.csv) for _, row in df.iterrows(): phenom Node(FaultPhenomenon, namerow[phenomenon], descriptionrow[desc]) cause Node(FaultCause, namerow[cause], frequencyint(row[freq])) part Node(DevicePart, namerow[part]) method Node(RepairMethod, namerow[method], costfloat(row[cost])) graph.merge(phenom, FaultPhenomenon, name) graph.merge(cause, FaultCause, name) graph.merge(part, DevicePart, name) graph.merge(method, RepairMethod, name) graph.merge(Relationship(phenom, OCCURS_ON, part)) graph.merge(Relationship(phenom, CAUSED_BY, cause)) graph.merge(Relationship(cause, FIXED_BY, method))代码逻辑说明graph.merge()是幂等操作如果已存在同name的节点不会新建而是更新属性这比用create安全得多。Relationship()创建关系merge关系时同样会检查是否已存在避免重复边。参数说明auth元组按(用户名, 密码)传merge的第一个节点是目标第二个参数是节点标签第三个参数是唯一属性。如果 CSV 里没有frequency字段就把int(row[freq])换成0或直接删掉这行代码图数据库不要求所有节点属性一致。3. 问句解析余弦相似度与候选路径生成3.1 为什么不用编辑距离和正则用户问“轴承过热怎么办”如果用正则匹配“温度过高”永远匹配不上。编辑距离只能处理错别字无法处理同义表达“过热”和“温度过高”之间编辑距离很大但语义很近。这里采用更工程化的方案把问句分词后和图谱里所有实体名称做 Text 相似度计算取分数最高的实体作为“查询锚点”。选定余弦相似度的原因有两个。第一向量化之后的余弦相似度对文本长度不敏感问句里附带“我想问一下”“麻烦看看”这类噪声词时干扰较小第二工程上用 TF-IDF 向量不需要额外训练词向量模型离线资源少、部署轻。缺点是 TF-IDF 对同义词识别能力弱这个问题可以在实体名称标准化时解决比如把“过热”统一成“温度过高”。3.2 基于 TF-IDF 和余弦相似度的候选实体匹配项目里的cosin.py核心就是做这件事。先看一个可运行的实现import jieba from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity from py2neo import Graph graph Graph(bolt://localhost:7687, auth(neo4j, password)) def fetch_all_entity_names(): query MATCH (n) WHERE n: FaultPhenomenon OR n: FaultCause OR n: DevicePart OR n: RepairMethod RETURN labels(n)[0] AS type, n.name AS name results graph.run(query).data() return [(r[type], r[name]) for r in results] def match_entity(question, top_k3): entities fetch_all_entity_names() entity_names [e[1] for e in entities] question_words .join(jieba.cut(question)) corpus [question_words] entity_names vectorizer TfidfVectorizer(token_patternr(?u)\b\w\b) tfidf_matrix vectorizer.fit_transform(corpus) sim_scores cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:]).flatten() candidates [] for idx in sim_scores.argsort()[-top_k:][::-1]: candidates.append({ entity: entity_names[idx], type: entities[idx][0], score: round(float(sim_scores[idx]), 4) }) return candidates if __name__ __main__: print(match_entity(轴承温度过高什么原因))逻辑说明先把问题用 jieba 分词并用空格连起来再和图谱里所有实体名称放在一起组成语料库。随后使用TfidfVectorizer把整个语料转成 TF-IDF 矩阵第一行是问题后面行是实体名。cosine_similarity计算第一行与后面每一行的相似度最后按分数从高到低取前top_k个实体作为候选。参数说明token_pattern要匹配中文字符直接用默认的正则对中文不友好这里通过 jieba 预分词后再让 vectorizer 按词之间空格切分所以 token_pattern 用普通单词模式即可。top_k3表示至少保留三个候选实体防止第一个匹配错误导致整条查询失败。3.3 从实体匹配到查询路径生成匹配到实体后不能直接把实体名拼进 Cypher因为用户问的是“是什么原因”还是“怎么修”意图不同要走的路径也不同。因此项目里常见做法是在QA.py中维护一个意图词典INTENT_CYPHER { cause: MATCH (p:FaultPhenomenon {name: $entity})-[r:CAUSED_BY]-(c:FaultCause) RETURN c.name AS cause, c.frequency AS freq ORDER BY c.frequency DESC , repair: MATCH (p:FaultPhenomenon {name: $entity})-[:CAUSED_BY]-(c:FaultCause)-[:FIXED_BY]-(m:RepairMethod) RETURN m.name AS method, m.cost AS cost ORDER BY m.cost ASC , part: MATCH (p:FaultPhenomenon {name: $entity})-[:OCCURS_ON]-(d:DevicePart) RETURN d.name AS part }判定意图的方式可以简单粗暴问句包含“原因”“为什么”“咋回事”就走cause分支包含“怎么修”“方法”“咋办”走repair分支包含“哪个部件”“在哪”走part分支。如果没有命中任何关键词默认返回cause路径。这里的关键是用参数化查询$entity而不是字符串拼接。既防止 Cypher 注入又避免特殊字符破坏语法。查询结果按照frequency排序体现故障知识图谱中“按频次归因”的思想。4. Flask 问答链路从 HTTP 请求到 Cypher 查询再到答案渲染4.1 后端架构与路由设计系统采用 Flask 提供 Web 服务项目里的templates存放 HTML 页面static存放 CSS/JS 文件。整体请求链路是用户在浏览器输入问题 → 前端通过 AJAX POST 到后端/ask接口 → 后端调用问句解析和意图识别模块 → 得到实体和意图后执行 Cypher → 将结果渲染为 JSON 返回前端。简化版的后端核心代码如下from flask import Flask, request, jsonify, render_template from cosin import match_entity from QA import INTENT_CYPHER from py2neo import Graph import jieba app Flask(__name__) graph Graph(bolt://localhost:7687, auth(neo4j, password)) def detect_intent(question): if any(word in question for word in [怎么修, 方法, 咋办, 修复]): return repair if any(word in question for word in [哪个部件, 在哪, 位置]): return part return cause app.route(/, methods[GET]) def index(): return render_template(index.html) app.route(/ask, methods[POST]) def ask(): data request.get_json() question data.get(question, ) if not question or not question.strip(): return jsonify({code: 400, msg: 问题不能为空}), 400 candidates match_entity(question) if not candidates or candidates[0][score] 0.5: return jsonify({code: 404, msg: 无法匹配到相关故障实体}), 404 entity candidates[0][entity] entity_type candidates[0][type] intent detect_intent(question) cypher INTENT_CYPHER[intent] records graph.run(cypher, entityentity).data() if not records: return jsonify({code: 200, msg: 图谱中暂无该故障的详细知识}), 200 return jsonify({code: 200, entity: entity, intent: intent, answer: records}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)逻辑说明detect_intent用关键词快速判断用户意图虽然简单但对工业场景足够稳定。match_entity里如果最高得分低于 0.5说明用户的问题和图谱实体语义差距过大此时不要硬查而是直接返回提示。graph.run()第二个参数entity会自动绑定到 Cypher 中的$entity上。参数说明score 0.5的阈值来自反复测试。如果设成 0.3很多无关问题会被错误映射设成 0.7用户稍微换种说法就匹配不上。0.5 是个平衡点。host0.0.0.0允许局域网访问方便后续推到车间 PAD 上测试。4.2 前端怎样把答案组织成易读卡片后端返回 JSON 后前端不能只是把JSON.stringify结果直接怼到页面上否则用户看到的是带大括号和引号的原生对象。更好的做法是在前端把结果分类渲染成卡片。项目里templates/index.html中常见的处理方式如下div idanswer-box styledisplay: none; h3 identity-name/h3 div idresult-list/div /div script async function askQuestion() { const question document.getElementById(question).value; const resp await fetch(/ask, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({question: question}) }); const data await resp.json(); if (data.code ! 200) { document.getElementById(result-list).innerText data.msg; return; } document.getElementById(entity-name).innerText 识别实体 data.entity 意图 data.intent ; const html data.answer.map(item { if (item.cause) return p可能原因${item.cause}频次${item.freq}/p; if (item.method) return p维修方法${item.method}成本${item.cost}元/p; return p相关部件${item.part}/p; }).join(); document.getElementById(result-list).innerHTML html; document.getElementById(answer-box).style.display block; } /script说明展示时把识别到的实体和意图显式标出来既让用户确认系统没有理解错也方便后端调试。如果用户发现实体识别错了可以换一种说法再问这实际上是变相的用户反馈机制。4.3 测试脚本与回归验证项目里还有test.py它承担自动化测试问答准确率的工作。常见做法是准备一份问答测试集每一行是一组“问题—期望实体—期望意图—期望答案关键词”然后循环调用问答案链路统计命中率import json from QA import detect_intent from cosin import match_entity test_cases [ {q: 轴承温度过高什么原因, entity: 轴承温度过高, intent: cause}, {q: 轴承过热怎么修, entity: 轴承温度过高, intent: repair}, ] match_count 0 for case in test_cases: entity_candidates match_entity(case[q]) entity_match entity_candidates and entity_candidates[0][entity] case[entity] intent_match detect_intent(case[q]) case[intent] if entity_match and intent_match: match_count 1 print(fPASS: {case[q]}) else: print(fFAIL: {case[q]}, expected{case[entity]}, got{entity_candidates}) print(fAccuracy: {match_count / len(test_cases):.2%})这段测试不是为了追求 100% 准确率而是为了每次修改cosin.py或意图词典时能快速知道有没有破坏已有功能。5. 进阶阈值调优、多轮检索与可视化扩展5.1 相似度阈值是动态的不是拍脑袋定的前面把阈值固定成 0.5但实际部署时不同实体名称的文本长度、图数据库里实体的拥挤程度都会影响分数。更稳妥的做法是把阈值做成可配置项同时针对不同类型实体单独设置阈值。例如故障现象名称往往较长与问句的相似度天然低于短实体名这时可以放宽阈值而维修方案名称和问句的相似度通常不高直接通过CAUSED_BY关系二次过滤而不是依赖实体匹配分数。实操上可以把测试集按实体类型分组分别统计分数分布用 P90 作为阈值基准。如果某个类型 P90 以下仍然有大量正确命中就降到 P80。这个调参过程和模型调参一样需要看具体数据。5.2 多轮追问先从图谱找原因再用对话引导细化单轮问答的局限在于用户的问题经常不完整。比如用户说“电机抖动”系统返回了 5 个可能原因。此时用户可能想继续问“那怎么查”。在现有架构下可以在 Flask 的 session 中记录上次命中的实体和候选原因下一轮如果用户问题里没有新的实体名词则默认沿用上一轮实体from flask import session app.secret_key your-secret-key app.route(/ask, methods[POST]) def ask_with_session(): question request.get_json().get(question, ) candidates match_entity(question) if candidates and candidates[0][score] 0.5: session[entity] candidates[0][entity] else: # 当前问题没有命中实体尝试沿用上一轮 if entity in session: session_entity session[entity] # 重新计算低分匹配的实体但保留上一轮的实体 intent detect_intent(question) cypher INTENT_CYPHER[intent] records graph.run(cypher, entitysession_entity).data() session[entity] session_entity return jsonify({code: 200, entity: session_entity, intent: intent, answer: records}) else: return jsonify({code: 404, msg: 没找到对应故障知识}), 404这里的关键是让多轮追问的逻辑落在「低分匹配时回退到 session」这一分支上。注意要给 Flask 设置secret_key否则 session 无法使用。这种方案比引入 Rasa 之类对话框架轻量得多适合故障诊断这种领域封闭的场景。5.3 答案可视化用 ECharts 把层级关系画出来文本卡片只适合展示单层答案如果用户提问“列出这个故障所有可能的原因和对应维修方法”用表格呈现更直观。项目里可以新增一个/graph接口返回某个故障实体周围一跳或两跳的所有节点和关系前端用 ECharts 的力导向图渲染。Cypher 查询MATCH (p:FaultPhenomenon {name: $entity})-[r1]-(n) OPTIONAL MATCH (n)-[r2]-(m) WHERE n:FaultCause OR n:DevicePart OR n:RepairMethod RETURN p, r1, n, r2, m返回的数据中节点去重后映射成{id, name, category}关系映射成{source, target, label}。ECharts 的graph系列可以直接消费这种格式。可视化的价值不只是炫技它能让维修人员一眼看出某个原因关联到哪些维修方案比线性文字列表更适合故障排查时的发散思维。5.4 把知识图谱问答变成检索增强生成的底座另一个进阶方向是把这套问答系统作为检索模块接入大语言模型做生成式回答。当图谱返回了候选原因和维修方案后不直接渲染而是将这些结构化作参照文本让本地部署的大模型重新组织语言。比如问“轴承温度过高怎么办”图谱先查出[润滑不足, 更换润滑脂]大模型再生成一段“建议检查润滑油位若油位正常则需要停机后更换润滑脂”的更自然的答案。这里要注意必须限制大模型的输出不能脱离检索结果否则模型可能胡编维修步骤在工业场景这是安全问题。这种架构比单独用大模型更可靠因为知识图谱保证了事实来源大模型负责润色。项目已有的match_entity和INTENT_CYPHER可以直接复用不需要改图谱结构。5.5 快速部署到局域网的验证方法在车间环境通常没有公网 IP直接python QA.py启动 Flask注意关闭 debug 模式避免代码泄露。用生产级服务方式部署时可以将 Flask 应用挂到 gunicorn 后面pip install gunicorn gunicorn -w 4 -b 0.0.0.0:5000 QA:app启动后在同一局域网内的终端浏览器访问http://服务器IP:5000即可测试。验证时重点关注三件事第一连续发 10 个故障现象问句看实体识别准确率是否稳定第二故意输入一个图谱里不存在的故障描述看系统是否正确地返回“无法匹配”而不是报错第三同时开 10 个浏览器窗口并发提问观察是否有查询超时如果超时就在 Neo4j 配置里增大dbms.connector.bolt.max_connections。本文还有配套的精品资源点击获取