简介:面向家电行业智能问答场景,该完整项目将SpringBoot与Neo4j图数据库深度整合,演示从数据建模、Cypher查询到Spring Data Neo4j持久化、服务层与控制器实现的端到端开发流程,适合具备Java基础、希望入门知识图谱问答系统的开发者。压缩包共633个文件,大小约36.99MB,以JS、CSS、HTML等前端界面资源,Java源码、class编译文件,以及XML、properties配置和Cypher脚本为主,同时包含HanLP分词所需的词典、bin模型与txt数据,便于直接搭建运行。目前已有6483人学习下载。借助该资源可获得完整的家电知识图谱问答项目源码、数据库初始化脚本、前端展示页面及中文处理组件,理解实体识别、意图匹配与答案生成的方法,并参照pom.xml与工程目录快速复用整合配置,是构建智能客服或知识图谱应用的一份可实操参考。
1. 家电问答系统为什么绕不开知识图谱:先建图,再问答
家电售后工单里,大约六成问题高度重复:“这台冰箱不制冷怎么排查”“空调报了E4是什么故障”“洗衣机甩干异响严重”。传统处理方式要么人工翻维修手册,要么在文档库里做关键词检索,速度和准确性都靠运气。而基于知识图谱的问答系统(SpringBoot+Neo4j)先不动接口,而是把型号、故障现象、故障原因、维修方案、售后政策建模成一张带方向的网络,用户用自然语言提问,系统沿着“现象→原因→方案”的关系边找到答案。这个项目适合正在做智能客服、售后辅助诊断、家电语音问答的开发者。它解决的问题不是“把文档搬进数据库”,而是“把查文档变成走图谱边”。
2. 知识图谱建模:先把家电故障域抽象成“实体-关系-属性”
2.1 为什么问答必须先建模,而不是先写接口
我第一次做这个项目时也想着“先搭SpringBoot,再慢慢补数据”,结果问句一到就傻眼:“冰箱不制冷”到底是要查现象、查原因,还是直接给维修方案?三类需求对应的图谱遍历深度完全不同。后来才明白,问答质量九成由本体设计决定,代码只是在消费这张图。
关系型数据库擅长多表连接和聚合统计,但在“走边”这件事上效率很低。一个典型的家电故障问题要经历四跳:型号→故障现象→故障原因→维修方案。用SQL做需要join四到五张表,而且原因是多个、方案有优先级、同一个原因可能出现在不同型号上,表结构会越写越纠结。Neo4j里这个过程就是顺着关系遍历,节点和边的类型本身就表达业务语义。
建模时我会守住三条原则:实体是能够独立存在的名词,比如型号、现象、原因、方案;关系是实体之间的动词,比如“某型号出现某现象”“某现象由某原因导致”;属性是修饰实体的描述词,比如故障码、严重级别、维修工时。最容易犯的错误是把“原因”写成“现象”节点上的字符串属性——当时省事,后面做“同一原因对应多型号”时就彻底卡住。
2.2 实体、关系与属性设计:吃透家电故障域
这个项目的核心本体我整理成两张表。实体表如下:
| 标签 | 业务含义 | 关键属性 | 示例 |
|---|---|---|---|
| ApplianceModel | 家电型号 | code, brand, series, energyLevel | 某品牌BCD-520 |
| FaultPhenomenon | 故障现象 | code, name, severity, keywords | 不制冷、甩干异响 |
| FaultCause | 故障原因 | code, name, parts, checkPoint | 压缩机故障、门封条老化 |
| RepairSolution | 维修方案 | code, title, steps, cost, duration | 更换压缩机、清洁排水泵 |
| AfterSalePolicy | 售后政策 | policyId, content, warrantyYears | 整机保修一年 |
关系表如下:
| 起点 | 关系 | 终点 | 说明 |
|---|---|---|---|
| ApplianceModel | HAS_SYMPTOM | FaultPhenomenon | 该型号已知哪些故障 |
| FaultPhenomenon | CAUSED_BY | FaultCause | 现象背后的原因 |
| FaultCause | FIXED_BY | RepairSolution | 原因对应的维修方案 |
| AfterSalePolicy | COVERS | ApplianceModel | 售后政策覆盖哪些型号 |
设计时我把故障码放在现象节点上作为普通属性,而不是单独建一类节点。因为绝大多数提问是“这个故障码是什么意思”,只需要一次匹配就能找到现象,不需要多跳。keywords属性用数组存同义说法,比如“不制冷”同时挂“不降温”“冷藏室热”“压缩机不停机”,这些都是真实工单里出现过的问法。
2.3 Cypher建库语句与唯一约束
确定完模型后,用Cypher创建第一批节点和关系:
// 创建故障现象节点 CREATE (p:FaultPhenomenon { code: 'P001', name: '冰箱不制冷', severity: 'high', keywords: ['不制冷', '不降温', '冷藏室温度高', '压缩机不停机'] }); // 给该现象挂原因 CREATE (c:FaultCause { code: 'C001', name: '压缩机启动电容损坏', parts: '启动电容', checkPoint: '万用表测电容容量,低于标称值80%判定损坏' }); // 连接现象与原因 MATCH (p:FaultPhenomenon {code: 'P001'}) MATCH (c:FaultCause {code: 'C001'}) CREATE (p)-[:CAUSED_BY]->(c);这段代码分三段执行。前两个CREATE负责建节点,第三个MATCH+CREATE负责建关系。属性里的checkPoint我建议写成维修师傅能照着操作的排查步骤,而不是一个结论词,这样后面的问答结果才有“可执行性”。severity用low/mid/high三档,后续答案排序会用到。
节点建好后,必须给唯一字段建约束:
CREATE CONSTRAINT model_code_unique IF NOT EXISTS FOR (m:ApplianceModel) REQUIRE m.code IS UNIQUE; CREATE CONSTRAINT phenomenon_code_unique IF NOT EXISTS FOR (p:FaultPhenomenon) REQUIRE p.code IS UNIQUE;注意:如果建了唯一约束,后面再用MERGE导入数据时会自动去重;如果没有约束,LOAD CSV重复跑一次就会出现双份节点,问答结果跟着翻倍。这也是为什么我在导入数据之前先把约束建好。
2.4 生产数据导入:LOAD CSV 与批量提交
手写CREATE只适合建验证用的几十条数据。真实项目里故障数据往往来自售后工单Excel,我会先转成CSV放到Neo4j的import目录,再执行LOAD CSV:
提示:LOAD CSV的file://路径是相对Neo4j安装目录的import文件夹,不是项目里随便放个CSV就能读。
LOAD CSV WITH HEADERS FROM 'file:///faults.csv' AS row MERGE (p:FaultPhenomenon {code: row.phenomenon_code}) ON CREATE SET p.name = row.phenomenon_name, p.severity = row.severity, p.keywords = split(row.keywords, '|');逻辑说明:MERGE先按code匹配,已存在则跳过,不存在才创建;ON CREATE SET只在新建节点时写入属性,避免覆盖已有数据。参数说明:split(row.keywords, '|')把CSV里用竖线分隔的关键词串转成字符串数组;phenomenon_code是CSV表头,命名要和文件里完全一致。几十万条数据导入时,建议在LOAD CSV前面加USING PERIODIC COMMIT 500,让Neo4j每500条自动提交一次,避免单个大事务把内存打满。
导入完不要急着写接口,先用一条查询做连通性检查:
MATCH (m:ApplianceModel {code: 'M-BCD520'})-[:HAS_SYMPTOM]->(p:FaultPhenomenon) RETURN m.code AS model, p.name AS symptom, p.severity AS severity;如果返回空,优先检查两点:型号code是否大小写完全一致;关系方向是否创建反了。这个阶段查不出来,后面所有问答都会“安静地失败”,所以我把这步当作数据落地的强制验收项。
3. SpringBoot整合Neo4j:从驱动配置到查询封装
3.1 依赖选择与连接池配置
Spring Boot整合Neo4j,最省事的路径是引入官方Spring Data Neo4j(SDN)starter。项目用Spring Boot 2.7.x时对应SDN 6.x,换到Spring Boot 3.x则对应SDN 7.x,版本最好成套升级,不要混用。pom里加:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-neo4j</artifactId> </dependency>这个starter会自动装配连接驱动和事务管理,不需要手写驱动依赖。连接配置写在application.yml:
spring: neo4j: uri: bolt://localhost:7687 authentication: username: neo4j password: ${NEO4J_PASSWORD} pool: max-connection-pool-size: 50 connection-acquisition-timeout: 20s参数说明:uri默认走bolt协议,生产环境建议把密码放到环境变量NEO4J_PASSWORD,不要明文提交到仓库。max-connection-pool-size控制连接池上限,普通问答项目20到50之间都行;压测时如果出现Pool temporarily exhausted,优先加这个值,同时注意Neo4j服务端的dbms.connector.bolt.max_connections也要同步调大。
注意:Neo4j 5.x与4.x的语法略有差异,Cypher建约束部分尽量写
IF NOT EXISTS兼容写法,避免升级时批量报错。
3.2 实体映射:@Node与@Relationship的边界
SDN支持把节点映射成Java对象。比如故障现象节点:
@Node("FaultPhenomenon") public class FaultPhenomenonEntity { @Id @GeneratedValue private Long id; @Property("code") private String code; @Property("name") private String name; @Property("severity") private String severity; @Property("keywords") private List<String> keywords; @Relationship(type = "CAUSED_BY", direction = Direction.OUTGOING) private List<FaultCauseEntity> causes; // getter / setter 省略 }@Node("FaultPhenomenon")里的名称必须和Cypher里建的标签完全一致,大小写不同会被当作另一个标签。@Property可省略,默认会把Java属性名转成Neo4j属性名,但我建议显式写出来,避免Java风格命名和Cypher属性命名不一致时来回猜。@Relationship映射关系,Direction.OUTGOING表示从当前节点指向目标节点。
这里要提醒一句:别把JPA的ORM思维照搬过来。Neo4j实体对象适合读模型,不适合做复杂聚合。当你发现一个查询要跨四层关系且只取个别字段时,直接写Cypher返回Map更痛快,没必要让实体类背上所有关联关系。
3.3 Repository层:Cypher查询的“命名道场”
在Repository接口里定义查询方法,把Cypher写在注解里是最直观的做法:
@Repository public interface FaultGraphRepository extends Neo4jRepository<ApplianceModelEntity, Long> { @Query(""" MATCH (m:ApplianceModel {code: $modelCode})-[:HAS_SYMPTOM]->(p:FaultPhenomenon) RETURN p.code AS code, p.name AS name, p.severity AS severity, p.keywords AS keywords """) List<Map<String, Object>> findSymptomsByModel(@Param("modelCode") String modelCode); @Query(""" MATCH path = (m:ApplianceModel {code: $modelCode})-[:HAS_SYMPTOM]->(p:FaultPhenomenon)-[:CAUSED_BY]->(c:FaultCause)-[:FIXED_BY]->(s:RepairSolution) RETURN p.name AS symptom, c.name AS cause, s.title AS solution, s.steps AS steps, length(path) AS depth ORDER BY p.severity, c.name """) List<Map<String, Object>> findRepairPath(@Param("modelCode") String modelCode); }第一个方法用于单跳查询,适合回答“这个型号有哪些常见故障”。第二个方法一次走四跳拿到完整维修路径,length(path)返回路径中的关系数量,用来评估答案链路长度。注意所有查询参数都用$modelCode这种参数化写法,绝对不要把用户输入直接拼进Cypher字符串,这是避坑章节要展开讲的安全线。第二个查询里我用ORDER BY p.severity, c.name做了初步排序,但遇到同一原因对应多个方案时,还需要在Service里再做一轮打分。
3.4 Service层:图谱数据转DTO,别裸奔实体
Controller如果直接返回Neo4j实体,很容易踩序列化递归爆栈的坑,特别是实体之间配了双向关系时。我的做法是在Service层组装成面向问答的DTO:
public AnswerDTO buildAnswer(String modelCode) { List<Map<String, Object>> rows = faultGraphRepository.findRepairPath(modelCode); if (rows == null || rows.isEmpty()) { return AnswerDTO.notFound("没有找到该型号的维修记录,请核对型号编码"); } List<RepairStepDTO> steps = rows.stream().map(row -> { RepairStepDTO dto = new RepairStepDTO(); dto.setSymptom((String) row.get("symptom")); dto.setCause((String) row.get("cause")); dto.setSolution((String) row.get("solution")); dto.setDetail((String) row.get("steps")); return dto; }).collect(Collectors.toList()); return AnswerDTO.success(steps); }这段代码做了三件事:查图谱、判空兜底、转DTO。特别说明AnswerDTO.notFound不是返回异常,而是带着“未找到完全匹配结果”的提示状态返回200,这样前端可以正常渲染“暂无答案”而不是被异常打断。RepairStepDTO里的detail字段对应Cypher里维修方案节点的steps属性,它是一段可读的排查步骤文本,问答系统最终返回的就是这个。
到这里SpringBoot侧的读取链路已经打通,剩下最核心的部分是:用户那句千奇百怪的自然语言,怎么变成上面这些精心设计的Cypher查询。
4. 问答引擎:把自然语言问句翻译成可执行的Cypher
4.1 问题分类:四种问法对应四类查询
家电问答表面上问法千变万化,归纳下来主要就四类:
| 问题类型 | 问法特征 | 示例 | 目标查询模式 |
|---|---|---|---|
| 现象查询 | 描述故障现象,无明确故障码 | 冰箱不制冷、洗衣机甩干异响 | 型号→现象 |
| 故障码查询 | 问句中包含字母数字组合,如E4、F6 | 某品牌空调显示E4是什么意思 | 故障码→现象→原因 |
| 维修方案查询 | 含“怎么办、怎么修、解决” | 压缩机坏了怎么办 | 原因→维修方案 |
| 售后政策查询 | 含“保修、几年、免费” | 这台冰箱保修几年 | 型号→政策 |
分类的作用不是写一个膨胀的分类器,而是决定Cypher模板的起点和终点。故障码查询的起点是FaultPhenomenon.code,维修方案查询的终点是RepairSolution,起点终点定了,中间走什么边是固定的。分类错误的代价是走错模板,轻则答非所问,重则查不到结果。
4.2 问句解析:正则抽型号、抽故障码、识别意图
家电型号和故障码都有很强的格式特征,用正则比训练模型更可控。我通常维护两套模式:
public class QuestionParser { // 型号:某品牌BCD-520 或 KFR-35GW 这种字母数字混合 private static final Pattern MODEL_PATTERN = Pattern.compile("([A-Za-z]{2,6}[- ]?\\d{2,4}[A-Za-z]?)"); // 故障码:E4、F6、P0 这种字母加1到3位数字 private static final Pattern FAULT_CODE_PATTERN = Pattern.compile("\\b([A-Za-z]{1,3}\\d{1,3})\\b"); // 意图关键词:用集合做快速命中 private static final Set<String> SOLUTION_WORDS = Set.of("怎么办", "怎么修", "怎么解决", "怎么处理", "维修方案"); private static final Set<String> POLICY_WORDS = Set.of("保修", "质保", "几年", "免费", "售后政策"); public QuestionIntent parse(String rawQuestion) { Matcher modelMatcher = MODEL_PATTERN.matcher(rawQuestion); String modelCode = modelMatcher.find() ? modelMatcher.group(1).replace(" ", "") : null; Matcher codeMatcher = FAULT_CODE_PATTERN.matcher(rawQuestion); String faultCode = codeMatcher.find() ? codeMatcher.group(1).toUpperCase() : null; if (faultCode != null) { return QuestionIntent.forFaultCode(faultCode); } if (containsAny(rawQuestion, SOLUTION_WORDS)) { return QuestionIntent.forSolution(modelCode); } if (containsAny(rawQuestion, POLICY_WORDS)) { return QuestionIntent.forPolicy(modelCode); } return QuestionIntent.forSymptom(modelCode); } }这段代码就是把问句拆成三个槽位:modelCode、faultCode、意图分类。有几个参数细节值得说:MODEL_PATTERN把字母部分控制在2到6位,数字部分2到4位,避免把“E4”这种故障码误识别成型号;解析后再用replace(" ", "")把“BCD 520”处理成“BCD520”,减少型号写法不统一带来的漏查。FAULT_CODE_PATTERN加了\\b边界,防止匹配到长单词内部的组合。意图判断顺序固定先故障码、再维修、再政策、最后落到现象,因为故障码的置信度最高,现象只能作为兜底。
另外有一个处理细节容易被忽略:用户输入经常是全角字符,比如“BCD520”,正则默认只匹配半角英文数字。我在Parser的构造函数里会先调normalize()方法,把全角英数转半角,再喂给正则。还有问句尾部常见“?”和“!”之类的干扰符,统一在解析前去掉。这个归一化步骤放在最前面,能减少大概三成无效查询。
4.3 Cypher组装:槽位到查询模板的映射
解析完得到意图对象后,需要把它映射成具体Cypher。我的做法是让QuestionIntent持有查询类型和参数,Service里用switch分派到不同模板:
public AnswerDTO answer(QuestionIntent intent) { String modelCode = intent.getModelCode(); String faultCode = intent.getFaultCode(); return switch (intent.getType()) { case FAULT_CODE -> faultCodeQuery(faultCode); case SOLUTION -> solutionQuery(modelCode); case POLICY -> policyQuery(modelCode); default -> symptomQuery(modelCode); }; } private AnswerDTO faultCodeQuery(String faultCode) { List<Map<String, Object>> rows = repository.findByFaultCode(faultCode.toUpperCase()); return AnswerDTO.fromRows(rows); }对应的Repository方法:
@Query(""" MATCH (p:FaultPhenomenon {code: $faultCode})-[:CAUSED_BY]->(c:FaultCause)-[:FIXED_BY]->(s:RepairSolution) RETURN p.name AS symptom, c.name AS cause, s.title AS solution, s.steps AS steps, s.cost AS cost """) List<Map<String, Object>> findByFaultCode(@Param("faultCode") String faultCode);这里有一个设计重点:故障码查询不经过型号节点,因为故障码本身就是现象节点的属性,从故障码出发直接向后走两条边就能拿到原因和方案。s.cost是维修方案节点上的成本属性,用户问“修一下多少钱”时会用到。如果查出来为空,不要直接返回“无答案”,而是降级成全文检索,把keywords里包含故障码的现象节点捞出来,让用户看到相近结果:
@Query(""" MATCH (p:FaultPhenomenon) WHERE any(kw IN p.keywords WHERE kw CONTAINS $keyword) RETURN p.code AS code, p.name AS name LIMIT 5 """) List<Map<String, Object>> fuzzySearchByKeyword(@Param("keyword") String keyword);any(kw IN p.keywords WHERE kw CONTAINS $keyword)是Cypher列表谓词,遍历keywords数组,只要有一个包含用户关键词就命中。LIMIT 5控制兜底结果数量,避免把整个现象表扫出来。这里用的是CONTAINS子串匹配,数据量小够用,数据量大时我一般换成全文索引。兜底结果会拼成“以下是与‘xxx’相关的故障,请确认具体现象”这样的引导文案返回,而不是冷冰冰的“无答案”。
5. 整合Neo4j避坑指南:五个高频问题与排查套路
每次有人拿这个项目跑不通来找我,翻来覆去基本都是下面五类问题。我按“现象→原因→解决”的顺序写出来,希望你能少走我当初的路。
5.1 中文关键词查不到数据
现象:现象节点keywords里明明存了“不制冷”,用WHERE p.name CONTAINS "不制冷"查不到任何结果,或者全文索引查中文完全失效。
原因:Neo4j默认的全文索引analyzer按空格分词,中文文本被当成一个完整的token,“不制冷”和“不制 冷”这种被拆得七零八落。另外很多人不知道CONTAINS并不会走全文索引,它本质是全表扫描,数据量大时又慢又不出结果。
解决:小数据量直接用CONTAINS加参数就够了,别硬上全文索引;数据量上来后给keywords字段配ngram或smartcn分析器。我一般先用CALL db.index.fulltext.queryNodes('fault_index', '不制冷')验证分词效果,再决定是否继续调analyzer。还有一个土办法:问句解析阶段就把“不制冷”归一成标准词,用别名映射解决,后面会专门讲。
5.2 关系方向写反,查询结果恒为空
现象:明明用浏览器能看到两个节点和一条线,但MATCH (m)-[:HAS_SYMPTOM]->(p)查出来是空。
原因:Neo4j的关系是有方向的,创建时写的是<-还是->决定了查询方向是否匹配。很多人在导入阶段CREATE关系时不注意箭头朝向,查询时又习惯用直觉方向写,于是两边对不上。
解决:建完图谱先跑MATCH ()-[r]->() RETURN DISTINCT type(r) LIMIT 20,把全部关系类型和方向摊开看一遍。统一约定“左到右 = 主语到宾语”,比如“型号HAS_SYMPTOM现象”就是型号在左、现象在右。查询时如果吃不准,先用MATCH ()-[r]-()无方向查询确认关系存在,再补方向。
5.3 Jackson序列化递归爆栈
现象:Controller直接返回实体对象,控制台报StackOverflowError,或者JSON输出里嵌套了十几层同样的节点。
原因:实体类里配置了双向@Relationship,比如FaultPhenomenonEntity持有FaultCauseEntity列表,而FaultCauseEntity又持有FaultPhenomenonEntity列表,Jackson序列化时两边互相引用,无限递归。
解决:三个办法任选。第一,实体只保留单向关系,反向关系需要用时单独查询;第二,在实体字段上加@JsonIgnore,告诉Jackson这个字段不序列化;第三,也是最推荐的,Service层组装成DTO再返回,让Controller永远不直接接触实体类。这个习惯能顺便解决懒加载导致的LazyInitializationException。
5.4 数据批量导入慢,甚至内存溢出
现象:LOAD CSV导入几万行数据,跑到一半卡住或报OutOfMemoryError,之前导入的批次也全部回滚。
原因:默认情况下LOAD CSV把整个文件当成一个大事务处理,内存里要缓存全部变更,文件一大就爆。
解决:加USING PERIODIC COMMIT 500,让导入按500行一个事务分批提交,不要攒到最后。另外导入前先建好唯一约束,MERGE才能发挥作用,否则重复行会生成重复节点。我一般还会在导入脚本里加上CREATE INDEX IF NOT EXISTS FOR (p:FaultPhenomenon) ON (p.code)这类索引,让MERGE的匹配查找从全表扫描变成索引命中,速度提升非常明显。
5.5 Cypher注入:用户输入拼接出大事
现象:用户输入E4' OR '1'='1或者; MATCH (n) DETACH DELETE n之类的内容,系统返回了不该看到的数据,严重时图谱被清空。
原因:问句里的故障码、型号被当成字符串直接拼进Cypher,$param参数化完全没启用。这跟SQL注入是一个道理,而且Neo4j有些写操作同样能被拼接执行。
解决:所有查询一律走@Param参数化,禁止字符串拼接。模板里固定的Cypher框架放在代码中,只把用户输入作为参数传进去。对用户输入做白名单校验也好使:故障码强制大写、型号走正则校验、非法的直接返回“请输入正确的型号或故障码”,不要在数据库层指望过滤。
6. 让问答结果更可靠:答案打分、别名扩展与可视化验证
6.1 答案可信度打分
当同一个故障现象对应多个原因、多个方案时,直接按图谱存储顺序返回会很随意。我最后会在查询里加一个打分公式:
MATCH (m:ApplianceModel {code: $modelCode})-[:HAS_SYMPTOM]->(p:FaultPhenomenon) MATCH path = (p)-[:CAUSED_BY]->(c:FaultCause)-[:FIXED_BY]->(s:RepairSolution) RETURN p.name AS symptom, c.name AS cause, s.title AS solution, (s.frequency * 0.6) + (1.0 / length(path) * 0.3) + (CASE p.severity WHEN 'high' THEN 0.1 ELSE 0 END) AS score ORDER BY score DESCfrequency是维修方案节点上的历史维修次数,越高说明越常见;length(path)是路径长度,越短说明诊断链路越直接;severity字段对高危故障做加权。权重参数我会保存在配置项里,上线后根据真实工单反馈微调,比如某类故障维修次数统计不准时,就把frequency的权重从0.6降到0.4。
6.2 别名扩展:别把同义词塞进正则
家电问答最磨人的是用户说话不统一,“不制冷”“不冷”“制冷差”“冰不凉”其实指向同一个现象。一个常用做法是给现象节点加keywords数组,在Cypher里用UNWIND p.keywords AS kw展开匹配,这样加新说法只需要在Neo4j里更新节点属性,不用改代码。我此前顺手把一批同义词写死在正则里,后来维护到第三周就改成别名表了,新增问法要触发重新发布,这教训够我记很久。
6.3 用可视化验证整条故障路径
每次改完本体或导入新数据,我建议先停下来做一次完整路径核对。最简单的方式是直接在开发环境跑Neo4j Browser,执行下面的查询:
MATCH path = (m:ApplianceModel {code: 'M-BCD520'}) -[:HAS_SYMPTOM]->(p:FaultPhenomenon) -[:CAUSED_BY]->(c:FaultCause) -[:FIXED_BY]->(s:RepairSolution) RETURN path把返回的路径图和维修手册逐条比对,重点看有没有短路径被长路径“淹没”。下载下来的工程项目包里已经带好实体类、Repository和问答解析器,你只需要把型号、故障数据换成自己的,再按线上工单调一遍权重即可。比如某次用户投诉“冰箱不制冷为什么只报压缩机”,我查了图谱才发现门封条老化的路径确实存在,但因为路径更长、frequency更低,默认排序里被排到后面,用户看不到。从那以后我每次改完本体,都强制在浏览器的可视面板里走一遍故障路径再提交代码,确认方案顺序和维修手册一致,低频原因也要能被查到。这个习惯帮我挡掉好几次隐蔽的数据问题,希望帮到你。
本文还有配套的精品资源,点击获取