
1. 项目概述当知识不再是碎片而是一张可行走的地图你有没有试过用当前最火的RAG工具查一个看似简单、实则暗藏玄机的问题比如“为什么2023年Q4客户投诉率突然上升它和上季度上线的新订单系统有什么关系”——传统RAG大概率会给你返回三段话一段讲投诉数据统计一段讲新系统功能列表一段讲客服培训纪要。它把这三段“相似文本”喂给大模型模型再凭语感拼凑出一个听起来合理、但逻辑链断裂的回答。这不是模型不聪明而是它被喂了“断章取义”的原料。我去年在给一家保险科技公司做知识中枢升级时就卡在这个点上他们有2000份产品条款、监管问答、内部SOP和历史客诉工单全是PDF和Word。用标准RAG做客服辅助回答“这款重疾险是否覆盖早期甲状腺癌”准确率92%但一问“如果客户同时持有A款重疾险和B款医疗险在确诊后如何触发双重赔付”准确率直接掉到58%且错误答案无法追溯根源。问题不在LLM而在知识组织方式本身——我们一直把知识当成一堆待检索的“沙砾”却忘了人类大脑从来不是靠沙砾堆砌理解世界的。知识天然长成一张网节点是实体人、系统、事件、规则边是关系依赖、变更、归属、因果、时间先后。Graph RAG不是给RAG加了个“图”字噱头它是把知识建模的底层范式从“文本相似度匹配”切换到了“关系路径导航”。它不问“哪段文字最像这个问题”而是问“哪些实体被这个问题激活它们之间存在怎样的可验证路径”。这就像把一张模糊的卫星图换成了一张带地铁线路、公交换乘、步行导航的高精地图——你不再需要靠猜去连点成线路已经画好了。本文接下来要拆解的就是这张“知识高精地图”怎么画、怎么走、踩过哪些坑以及为什么在企业级复杂场景里它正从“可选项”变成“必选项”。2. 内容整体设计与思路拆解从文本索引到关系引擎的范式迁移2.1 为什么必须放弃“纯向量检索”这条捷径很多人第一次接触Graph RAG第一反应是“又要搞图数据库又要写实体识别规则太重了不如多调几个embedding模型试试。”这个想法非常真实也恰恰暴露了传统RAG思维的惯性陷阱。我们必须先厘清一个根本差异传统RAG优化的是“检索效率”Graph RAG解决的是“推理可靠性”。举个具体例子。假设你有一份《GDPR合规检查清单》PDF其中包含这样一句话“第7条要求数据控制者在发生个人数据泄露后72小时内向监管机构报告除非该泄露不太可能对自然人的权利和自由造成风险。”传统RAG会把这句话切分成chunk生成向量当你问“GDPR对数据泄露的报告时限要求是什么”它能精准召回这个chunk答案正确。但如果你问“如果某次泄露影响了500名用户是否触发72小时报告义务”传统RAG就懵了——它召回的chunk里根本没有“500名用户”这个数字也没有“影响程度评估”的判断逻辑。它只能靠LLM从其他无关chunk里“脑补”关联结果要么胡说要么回避。而Graph RAG的处理流程完全不同它在预处理阶段会将这句话解析为结构化三元组(GDPR_第7条, has_requirement, 72_hour_reporting)、(GDPR_第7条, has_exception_condition, low_risk_to_individuals)、(low_risk_to_individuals, is_assessed_by, impact_scale_and_scope)。当问题出现时系统不是找“最像”的文本而是启动图查询MATCH (r:Regulation {name:GDPR_第7条})-[:has_exception_condition]-(e:Condition)-[:is_assessed_by]-(a:Assessment) RETURN a.name。答案直接指向“impact_scale_and_scope”这个评估维度LLM只需基于这个明确的上下文生成解释而非无中生有。这个差异不是技术选型的优劣而是问题域的本质决定的当你的业务问题天然涉及多跳、多条件、多实体绑定时向量空间里的“近邻”永远无法替代图谱中的“路径”。我见过太多团队在Poc阶段用简单QA测试觉得传统RAG够用直到上线后遇到真实业务问题才意识到那92%的准确率只覆盖了20%的典型问题。2.2 Graph RAG的三层架构为什么不能只堆一个Neo4j一个健壮的Graph RAG系统绝非简单地把文档丢进图数据库就完事。它是一个精密的三层流水线每一层都承担不可替代的角色缺一不可。我把这三层称为“感知层、编织层、导航层”。感知层Perception Layer是整个系统的“眼睛和耳朵”负责从原始非结构化文本中精准识别出构成知识图谱的原子要素。这远不止是NER命名实体识别那么简单。以一份IT运维日志为例“服务A在2024-03-15T14:22:05因数据库连接超时错误码5003导致API响应延迟2s”这句话感知层需要识别实体服务A、数据库、API、属性错误码5003、延迟2s、时间戳2024-03-15T14:22:05、关系类型“因...导致”隐含因果“响应延迟”隐含性能影响。我们团队实测发现仅用开源spaCy或BERT-NER模型对这类技术文本的实体识别F1值只有68%。后来我们采用“规则模型”混合策略先用正则精准捕获错误码、时间戳、IP地址等强模式字段再用微调后的领域BERT模型识别服务名、组件名等弱模式实体最后用依存句法分析器如Stanza解析动词关系将“因...导致”映射为causes_failure_of关系。这一层的质量直接决定了后续所有工作的天花板。编织层Weaving Layer是“大脑”负责将感知层提取的离散原子编织成一张逻辑自洽、语义一致的知识网络。这里的核心挑战是消歧与融合。例如文档1提到“Penwood集团”文档2提到“Lord Penwood”文档3提到“Penwood Holdings Ltd.”它们是同一个实体吗传统方法靠字符串相似度误差极大。我们的方案是引入实体共指消解Coreference Resolution和跨文档实体链接Cross-document Entity Linking。具体操作先为每个候选实体生成一个轻量级嵌入向量用Sentence-BERT对上下文窗口编码再计算其与已知权威知识库如Wikidata或企业内部主数据中实体的相似度结合规则如“Lord X”通常指代X家族的现任家主进行投票决策。最终所有指向同一现实对象的提及都被归并到一个唯一的图节点下并打上same_as关系。这个过程不是一次性的而是一个持续迭代的闭环当新文档加入系统会自动检测是否产生新的同义指代触发融合流程。导航层Navigation Layer是“双脚”负责将用户问题转化为图上的精确查询路径并将结果结构化地馈送给LLM。这里的关键在于查询意图理解与图查询生成。用户不会说“请执行MATCH (n:Person)-[r:married_to]-(m:Person) WHERE n.nameAnthony RETURN m.name”。所以导航层需要一个轻量级的“图查询编译器”。我们的做法是将常见问题模式如“X和Y的关系是什么”、“Z的上游依赖有哪些”、“找出所有受A影响的B类实体”预定义为模板每个模板绑定一个Cypher查询片段。当用户提问时先用小模型如Phi-3-mini做意图分类和槽位填充识别X、Y、Z、A、B再将填充后的参数注入对应模板生成最终可执行的Cypher。这个设计避免了让LLM直接生成Cypher带来的不可控风险语法错误、逻辑漏洞又保留了足够的灵活性。实测表明这种模板化编译器的意图识别准确率达94.7%远高于端到端LLM生成Cypher的72%。这三层不是线性串联而是形成反馈环导航层发现的查询失败案例如某类问题总找不到路径会反哺给编织层提示其需要补充某种关系类型编织层发现的实体融合困难会驱动感知层优化特定领域的NER模型。这才是一个真正活的、能进化的知识图谱系统。3. 核心细节解析与实操要点从Bridgerton Demo看真实落地的魔鬼细节3.1 Bridgerton Demo的启示一个“错得漂亮”的案例价值千金Devi在原文中用《布里奇顿》剧集构建的Demo表面看是个轻松的文学场景实则精准击中了Graph RAG最核心的价值点——身份混淆Identity Confusion的根治。让我们剥开这个案例的层层包装看看它背后隐藏的、对企业用户至关重要的工程细节。在传统RAG的“Lady in Silver”问题中模型出错的根本原因是它把“银色手套”、“彭伍德家族纹章”、“彭伍德勋爵遗孀”这些文本特征全部投射到向量空间的同一个“语义簇”里然后根据距离最近的点Lady Araminta做出判断。这是一种典型的基于表象的聚类谬误。而Graph RAG之所以成功关键在于其预处理阶段强制执行了实体唯一标识Entity Canonicalization和关系显式化Relationship Explicitation。具体到数据准备环节我们复现这个Demo时对原始文本做了如下结构化处理实体抽取与标准化从文本中识别出所有人物提及“Sophie Baek”、“the Lady in Silver”、“Miss Baek”、“Sophie”、“Lady S.”。通过规则姓氏“Baek”头衔“Lady”银色元素和上下文“she wore silver gloves to the ball where Lord Penwood was present”判定所有这些提及均指向同一实体。为其分配唯一ID:Person {id: person_sophie_baek, canonical_name: Sophie Baek, title: Lady, attributes: [silver_gloves]}。同理为“Lord Penwood”、“Penwood”、“the late Lord Penwood”创建唯一ID:Person {id: person_lord_penwood, canonical_name: Lord Penwood}。关系抽取与验证识别文本中明确的关系“Sophie Baekwas introduced toLord Penwood at the ball” →(sophie)-[:INTRODUCED_TO {event: ball, time: season_4_episode_1}]-(penwood)。识别隐含但可推断的关系“She fled the ball immediately after his death” → 结合常识死亡是事件推断出(sophie)-[:WAS_PRESENT_AT {event: Lord_Penwood_death}]-(penwood)并标记此关系为inferred供后续审计。最关键一步建立identity_link关系。当系统确认“the Lady in Silver”和“Sophie Baek”是同一人时它不是简单地合并节点而是创建一条特殊的、带有置信度的边(sophie)-[:IDENTITY_LINK {confidence: 0.98, source: textual_evidencecontextual_logic}]-(lady_in_silver)。这条边的存在使得任何查询“Lady in Silver”的路径都能无缝跳转到Sophie Baek的所有关系网络中。这个看似简单的步骤解决了企业知识管理中最头疼的“同物异名”问题。在金融风控场景中“Apple Inc.”、“AAPL”、“苹果公司”、“库比蒂诺巨头”可能分散在不同报告里在医疗健康领域“Type 2 Diabetes”、“T2D”、“成人发病型糖尿病”、“胰岛素抵抗综合征”指向同一疾病。Graph RAG通过强制的实体标准化和身份链接让这些碎片自动归位这是向量检索永远无法做到的“语义缝合”。3.2 工具链选型为什么我们弃用LangChain自研轻量编排器市面上很多Graph RAG教程第一步就是教你怎么用LangChain的KnowledgeGraphQAChain。坦白说我们在早期Poc阶段也这么干过结果在真实企业数据上栽了跟头。LangChain的抽象层虽然方便但它把图查询、上下文组装、LLM调用这几个关键环节耦合得太紧导致三个致命问题调试黑盒化、性能不可控、定制成本高。举个调试例子。当一个复杂查询返回空结果时LangChain的报错信息通常是“No results found for query”你根本不知道问题出在是实体识别漏掉了关键名词是Cypher查询语法写错了是图数据库里压根没存这个关系还是LLM在组装答案时把有效路径过滤掉了你得一层层扒开源码耗时费力。性能方面LangChain默认的KnowledgeGraphQAChain会把整个子图比如某个实体的所有邻居一股脑塞给LLM。在一个有10万节点的图谱里一次查询可能返回上千个节点和边光是序列化/反序列化就吃掉大量CPU更别说LLM的上下文长度限制了。我们曾用它处理一个供应链依赖查询输入token高达12000响应时间超过45秒完全无法接受。因此我们彻底重构了编排逻辑自研了一个极简的Python脚本不到300行核心思想是分而治之、按需加载# 伪代码示意核心逻辑 def graph_rag_query(user_question): # Step 1: 意图解析轻量模型 intent, slots lightweight_intent_classifier(user_question) # Step 2: 精准图查询只取必要路径 if intent direct_relation: # 只查两点间最短路径最多3跳 cypher fMATCH pshortestPath((a:Entity {{name: {slots[subject]}}})-[*..3]-(b:Entity {{name: {slots[object]}}})) RETURN p LIMIT 1 elif intent upstream_dependencies: # 只查上游3层依赖不展开下游 cypher fMATCH (target:Entity {{name: {slots[entity]}}})-[:DEPENDS_ON*..3]-(dep) RETURN dep # Step 3: 结构化结果组装非全文本而是摘要化 result neo4j_driver.run(cypher) structured_context summarize_graph_path(result) # 生成类似 A depends on B (via API), B depends on C (via DB) # Step 4: LLM精炼输入极简输出可控 final_answer llm.invoke(fQuestion: {user_question}\nContext: {structured_context}\nAnswer concisely:) return final_answer这个自研编排器带来了立竿见影的提升平均查询响应时间从45秒降至1.8秒调试时能清晰看到每一步的输入输出print(fStep 1 Intent: {intent}, Slots: {slots})更重要的是它把“图查询”这个核心能力完全暴露出来方便我们针对不同业务问题快速定制查询策略。比如对“合规审计”类问题我们增加一个audit_trail模式强制返回所有关系的source_document和timestamp属性满足可追溯性要求。这种颗粒度的控制是任何通用框架都无法提供的。4. 实操过程与核心环节实现手把手搭建一个可运行的企业级Graph RAG原型4.1 环境准备与最小可行数据集构建开始编码前我们必须确立一个“最小但完整”的验证闭环。目标很明确用不超过100行代码跑通从PDF解析、图谱构建、到问答的全流程。我们选择Python生态因为它在NLP和图数据库集成上最为成熟。以下是经过我们反复验证的、零踩坑的环境配置清单Python版本严格使用3.10.12。这是目前与llama-cpp-python、neo4j官方驱动兼容性最好的版本。3.11在某些Windows环境下会出现llama-cpp的DLL加载失败。核心依赖requirements.txtllama-cpp-python0.2.79 # 本地运行LLM无需GPU neo4j5.20.0 # Neo4j 5.x是当前最稳定的企业级图数据库 langchain-core0.2.12 # 只用其基础工具不用高级链 PyMuPDF1.24.5 # 解析PDF的黄金标准比pdfplumber快3倍支持表格 spacy3.7.4 # 领域NER配合en_core_web_sm模型 networkx3.3 # 图算法辅助用于路径分析图数据库直接下载Neo4j Desktop免费版足够创建一个新项目数据库名称设为graphrag_demo。关键配置在Settings中将dbms.memory.heap.initial_size和dbms.memory.heap.max_size都设为2G避免小内存机器OOM并确保dbms.connector.bolt.enabledtrue。最小数据集不要一上来就处理你的TB级数据。我们为你准备了一个sample_policy.pdf模拟一份5页的《员工信息安全政策》内容包含第1页总则定义“敏感数据”、“数据处理者”、“数据控制者”。第2页访问控制条款“所有员工必须使用双因素认证2FA登录OA系统”。第3页数据存储条款“客户个人信息不得存储于境外服务器”。第4页违规处罚条款“违反第2条者将处以警告或停职”。第5页附录列出所有系统名称OA系统、CRM系统、HR系统及其所属部门。这个5页PDF就是你的“Hello World”。它包含了实体员工、OA系统、2FA、关系must_use、must_not_store、violates、belongs_to、属性location: overseas, severity: warning足以验证整个Pipeline。4.2 核心代码实现四步走从PDF到可问答图谱下面的代码是我们团队在多个客户现场验证过的、可直接复制粘贴运行的完整实现。它没有一行废话每一个函数都承担明确职责。步骤1PDF解析与文本清洗parser.pyimport fitz # PyMuPDF import re def parse_pdf_to_text(pdf_path): 解析PDF智能处理页眉页脚和表格 doc fitz.open(pdf_path) full_text for page_num in range(len(doc)): page doc[page_num] # 提取文本忽略页眉页脚区域假设它们占页面顶部/底部10% text page.get_text(text, clipfitz.Rect(0, page.rect.height*0.1, page.rect.width, page.rect.height*0.9)) # 清洗合并被PDF打断的单词如 infor- mation - information text re.sub(r-\n, , text) # 清洗移除多余空格和换行但保留段落分隔 text re.sub(r\s, , text).strip() full_text f\n--- Page {page_num 1} ---\n{text}\n return full_text # 测试 if __name__ __main__: text parse_pdf_to_text(sample_policy.pdf) print(Parsed text length:, len(text)) print(First 200 chars:, text[:200])步骤2实体与关系抽取extractor.pyimport spacy from spacy.matcher import Matcher from typing import List, Dict, Tuple # 加载英文模型注意en_core_web_sm足够无需更大模型 nlp spacy.load(en_core_web_sm) # 定义规则匹配器用于识别强模式实体 matcher Matcher(nlp.vocab) # 匹配“双因素认证2FA” pattern_2fa [{LOWER: two}, {LOWER: factor}, {LOWER: authentication}, {IS_PUNCT: True, OP: ?}, {LOWER: 2fa}] matcher.add(2FA, [pattern_2fa]) # 匹配“客户个人信息” pattern_pii [{LOWER: customer}, {LOWER: personal}, {LOWER: information}] matcher.add(PII, [pattern_pii]) def extract_entities_relations(text: str) - List[Dict]: 核心抽取函数返回结构化三元组列表 doc nlp(text) triples [] # 1. 基础NER获取人名、组织、地点等 for ent in doc.ents: if ent.label_ in [PERSON, ORG, GPE, PRODUCT]: triples.append({ subject: ent.text.strip(), predicate: is_a, object: ent.label_, source: spacy_ner }) # 2. 规则匹配识别预定义模式 matches matcher(doc) for match_id, start, end in matches: span doc[start:end] pattern_name nlp.vocab.strings[match_id] triples.append({ subject: span.text.strip(), predicate: has_pattern, object: pattern_name, source: rule_matcher }) # 3. 关系抽取基于依存句法找“must”、“must not”、“shall”等情态动词引导的规则 for sent in doc.sents: # 查找情态动词 modal_verbs [token for token in sent if token.lemma_ in [must, shall, should, may, will]] for modal in modal_verbs: # 找到它的宾语通常是名词短语 obj None for child in modal.children: if child.dep_ in [dobj, attr, pobj]: # 直接宾语、表语、介词宾语 obj child break if obj and obj.text: # 尝试找到宾语的修饰语如“双因素认证” modifier .join([t.text for t in obj.subtree if t.pos_ NOUN or t.dep_ in [compound, amod]]) if modifier: triples.append({ subject: All employees, # 主语泛化 predicate: f{modal.lemma_}_use, # must_use, shall_use object: modifier.strip(), source: dependency_parse, sentence: sent.text.strip() }) return triples # 测试 if __name__ __main__: text parse_pdf_to_text(sample_policy.pdf) triples extract_entities_relations(text) print(Extracted triples count:, len(triples)) for t in triples[:5]: print(f{t[subject]} --{t[predicate]}-- {t[object]})步骤3图谱构建与加载loader.pyfrom neo4j import GraphDatabase import json class GraphLoader: def __init__(self, uri, user, password): self.driver GraphDatabase.driver(uri, auth(user, password)) def create_knowledge_graph(self, triples: List[Dict]): 将三元组批量写入Neo4j with self.driver.session() as session: # 创建节点去重 session.write_transaction(self._create_nodes, triples) # 创建关系 session.write_transaction(self._create_relationships, triples) staticmethod def _create_nodes(tx, triples): # 使用MERGE确保节点唯一基于name属性 for triple in triples: tx.run( MERGE (n:Entity {name: $name}) ON CREATE SET n.type $type, n.source $source, nametriple[subject], typeSubject, sourcetriple[source] ) tx.run( MERGE (n:Entity {name: $name}) ON CREATE SET n.type $type, n.source $source, nametriple[object], typeObject, sourcetriple[source] ) staticmethod def _create_relationships(tx, triples): for triple in triples: tx.run( MATCH (s:Entity {name: $subject}) MATCH (o:Entity {name: $object}) CREATE (s)-[r:RELATION {type: $predicate, source: $source}]-(o), subjecttriple[subject], objecttriple[object], predicatetriple[predicate], sourcetriple[source] ) # 测试加载 if __name__ __main__: # 假设你已安装Neo4j Desktop并启动了graphrag_demo数据库 loader GraphLoader(bolt://localhost:7687, neo4j, your_password) triples extract_entities_relations(parse_pdf_to_text(sample_policy.pdf)) loader.create_knowledge_graph(triples) print(Graph loaded successfully!)步骤4问答接口qa_engine.pyfrom neo4j import GraphDatabase from llama_cpp import Llama class GraphRAGEngine: def __init__(self, neo4j_uri, neo4j_user, neo4j_pass, llm_path): self.driver GraphDatabase.driver(neo4j_uri, auth(neo4j_user, neo4j_pass)) # 加载本地LLM这里用Qwen1.5-0.5B4GB显存即可运行 self.llm Llama(model_pathllm_path, n_ctx2048, n_threads4) def query(self, question: str) - str: # Step 1: 简单关键词提取作为图查询的起点 keywords self._extract_keywords(question) if not keywords: return I dont understand the question. # Step 2: 构建Cypher查询简化版只查直接关系 cypher fMATCH (s:Entity)-[r:RELATION]-(o:Entity) WHERE s.name CONTAINS {keywords[0]} OR o.name CONTAINS {keywords[0]} RETURN s.name, r.type, o.name LIMIT 5 # Step 3: 执行查询 with self.driver.session() as session: result session.run(cypher) records list(result) # Step 4: 组装上下文 context_lines [] for record in records: context_lines.append(f{record[s.name]} {record[r.type]} {record[o.name]}) context | .join(context_lines) if context_lines else No relevant information found. # Step 5: LLM精炼答案 prompt fYou are a helpful assistant. Answer the question based ONLY on the provided context. Question: {question} Context: {context} Answer: output self.llm(prompt, max_tokens128, stop[Question:, \n], echoFalse) return output[choices][0][text].strip() def _extract_keywords(self, question: str) - List[str]: # 极简关键词提取去掉停用词取前2个名词或专有名词 doc nlp(question) keywords [token.text for token in doc if not token.is_stop and token.pos_ in [NOUN, PROPN]] return keywords[:2] # 使用示例 if __name__ __main__: engine GraphRAGEngine( bolt://localhost:7687, neo4j, your_password, ./models/qwen1.5-0.5b-q4_k_m.gguf # 从HuggingFace下载 ) # 测试问题 test_questions [ What must employees use to log in to the OA system?, Where can customer personal information NOT be stored? ] for q in test_questions: print(fQ: {q}) print(fA: {engine.query(q)}\n)运行以上四步你将得到一个真正可交互的Graph RAG原型。它可能没有华丽的UI但每一次engine.query()的调用都在执行一次真实的图遍历和结构化推理。这就是Graph RAG的“心脏”在跳动。5. 常见问题与排查技巧实录那些文档里不会写的血泪教训5.1 “图谱构建成功但查询总是返回空”——90%的初学者都卡在这里这是Graph RAG新手最常遇到的“幽灵bug”。你看着Neo4j Browser里节点和关系都画得明明白白可一执行MATCH (n)-[r]-(m) RETURN n,r,m却什么也查不到。别急着怀疑代码先做这三件事检查节点属性名是否统一这是最高频的坑在loader.py的_create_nodes函数里我们用MERGE (n:Entity {name: $name})这意味着节点的唯一标识是name属性。但你在extractor.py里生成的triple[subject]可能包含空格、标点、大小写不一致。比如一个triples是{subject: OA system, predicate: must_use, object: 2FA}另一个是{subject: OA System, predicate: depends_on, object: AD Server}。这两个OA system和OA System在图数据库里就是两个完全不同的节点解决方案在extractor.py的extract_entities_relations函数末尾添加标准化步骤def standardize_entity_name(name: str) - str: # 移除所有标点转换为小写合并空格 name re.sub(r[^\w\s], , name) name re.sub(r\s, , name).strip().lower() return name # 在循环triples时应用 for triple in triples: triple[subject] standardize_entity_name(triple[subject]) triple[object] standardize_entity_name(triple[object])这样“OA system”、“OA System”、“oa-system”都会变成oa system完美统一。验证Cypher查询语法是否真的在执行不要相信IDE或文档里的示例。打开Neo4j Browser手动执行你代码里生成的Cypher语句。比如如果代码生成的是MATCH (s:Entity)-[r:RELATION]-(o:Entity) WHERE s.name CONTAINS oa system RETURN s,r,o就在Browser里粘贴执行。如果返回空说明问题在数据或查询逻辑如果返回了结果说明问题在Python代码的数据传递环节比如变量名写错、字符串拼接失败。一个实用技巧在qa_engine.py的query函数里把生成的cypher字符串print()出来直接复制到Browser里执行这是最高效的调试方式。警惕“关系方向”的认知偏差在extractor.py里我们定义{subject: employees, predicate: must_use, object: 2FA}这在逻辑上是“员工必须使用2FA”但在图谱中我们创建的是(employees)-[must_use]-(2FA)。这意味着查询“谁必须使用2FA”时你要查MATCH (s)-[r:must_use]-(o {name: 2fa}) RETURN s而查询“2FA被谁使用”时你要查MATCH (s {name: 2fa})-[:must_use]-(o) RETURN o。方向反了就什么都查不到。经验心得在设计关系谓词时一律采用“主动语态主语在前”的原则并在文档中明确记录每种谓词的方向含义。我们团队的规范是所有must_*、shall_*关系箭头都从“执行者”指向“被执行对象”。5.2 “LLM回答越来越啰嗦甚至开始编造图中不存在的关系”这是一个更隐蔽、但危害更大的问题。它往往出现在系统运行一段时间后表现为初期回答简洁准确后期变得冗长、添加无关细节甚至声称“A和B有C关系”而图谱里根本不存在C关系。这通常不是LLM变坏了而是上下文污染Context Pollution在作祟。根源在于qa_engine.py的query函数。我们把所有查询到的关系用 | .join(...)拼成一个长字符串作为上下文。当图谱变大一次查询可能返回几十条关系这个字符串就会变得极其冗长。LLM在处理超长上下文时注意力机制会衰减它开始“脑补”来填补信息空白或者把不同关系的主语、谓词、宾语错误地交叉组合。终极解决方案强制上下文摘要Context Summarization。我们不再把原始三元组喂给LLM而是用一个极小的、专门训练的摘要模型甚至可以用规则把多条关系压缩成一句高度凝练的话。例如查询“OA系统”的依赖可能返回(OA system)-[:must_use]-(2FA)(OA system)-[:depends_on]-(AD Server)(OA system)-[:stores_data_in]-(SQL Database)原始上下文是OA system must_use 2FA | OA system depends_on AD Server | OA system stores_data_in SQL Database68个字符。摘要后变成The OA system requires 2FA for login