知识图谱是什么
知识图谱把世界表示成"实体 + 关系"的网络:节点是事物,边是它们之间的连接。向量库存的是"长什么样的文本",图谱存的是"事实本身"。
知识的最小单元是三元组:(华芯科技, 供应, 电源管理芯片)。海量三元组连起来,就构成一张可遍历、可推理的网络。属性图模型(Neo4j 等采用)在此基础上允许节点与关系各自携带属性:
- 三元组(Triple)
- (主体, 谓词, 客体) 的最小知识单元,如 (华芯科技, 供应, 芯片)。
- 属性图(Property Graph)
- 节点与关系都可携带属性的图模型,Neo4j、NebulaGraph 均采用;LLM 应用层的事实标准。
- 本体(Ontology / Schema)
- 图谱的"建表规范":允许哪些节点标签、关系类型与属性。先定本体,再抽数据。
- 多跳(Multi-hop)
- 沿关系跨越多个节点的遍历。"间接供应商"是两跳,"风险传导链"是三跳以上。
- 实体消歧(Entity Resolution)
- 判断"华芯"与"华芯科技"是否同一实体、两家同名公司是否不同实体。图谱质量的第一决定因素。
- 图算法
- 在图结构上运行的分析:PageRank、社区检测(Leiden)、最短路径、中心度。GraphRAG 全局检索的地基。
| 维度 | 关系型(SQL) | 向量库 | 图数据库 |
|---|---|---|---|
| 数据形态 | 表、行、列 | 向量 + 元数据 | 节点、关系、属性 |
| 擅长 | 精确条件查询、事务、聚合统计 | 语义相似度近邻 | 多跳关系、路径发现、图算法 |
| 弱项 | 深 JOIN 性能急剧劣化 | 精确匹配弱、无关系推理 | 全库统计类查询不如 SQL |
| 典型场景 | 订单、账务 | 语义检索(第 3 章) | 风控传导、供应链、推荐、知识问答 |
第 3 章的结论是:向量 RAG 回答不了"全局聚合、多跳关系"类问题。本章就是那句话的完整解法:把同一批文档既建向量索引(局部事实题)又建图谱(关系与全局题),运行时按问题路由。两种索引各司其职,不是二选一。
本体设计与建模模式
本体是图谱的 Schema:规定有哪些节点类型、关系类型与属性。它的质量上限,就是整张图的上限。
从问题反推,而不是从数据正推
糟糕的本体设计通常从"文本里有什么就抽什么"开始,结果是类型爆炸、图变垃圾场。正确顺序是先写下高频问题清单,再反推结构:
- "哪些公司间接依赖稀土镨?" → 需要
(:Company)-[:SUPPLIES]->(:Product)-[:REQUIRES]->(:Material) - "华芯的替代供应商有哪些?" → 需要
(:Company)-[:ALTERNATIVE_TO]->(:Company)显式关系 - "某风险事件影响多大范围?" → 需要
(:Company)-[:LOCATED_IN]->(:Region)的层级结构
四个常用建模模式
| 模式 | 做法 | 使用时机 |
|---|---|---|
| 属性优先 | 信息先做成关系属性(如 share: 0.35) | 默认选择。属性不会被遍历,性能好 |
| 属性上移为节点 | 当属性本身要参与遍历时,升级为节点 | "按国家分组统计"频繁出现时,把 country 属性变成 (:Country) 节点 |
| 关系升级为节点 | 当"关系"本身要带丰富属性、被独立查询时 | 供应关系涉及合同、期限、金额三方面 → 建一个 (:Contract) 节点连接双方 |
| 层级与时间 | SUBCLASS_OF / PART_OF 组织类型;VALID_FROM / VALID_TO 记录时效 | 类目体系、关系随时间变化的场景(供应商变更史) |
用约束固化本体
Neo4j 中,唯一性约束既是数据质量的保险,也是隐式声明 Schema 的方式——它明确告诉所有写图的人(包括 LLM 抽取管道):"这些类型与键是这张图的骨架":
// 唯一性约束:同类型节点按 name 去重(同时自动建索引)
CREATE CONSTRAINT company_name IF NOT EXISTS
FOR (c:Company) REQUIRE c.name IS UNIQUE;
CREATE CONSTRAINT product_id IF NOT EXISTS
FOR (p:Product) REQUIRE p.id IS UNIQUE;
CREATE CONSTRAINT material_name IF NOT EXISTS
FOR (m:Material) REQUIRE m.name IS UNIQUE;
万物皆节点:把文本里每个名词都抽成节点,图被低价值节点淹没,遍历路径被噪声拉长。万能关系:用一种 RELATED_TO 连接一切,语义归零,查询无法约束。忽略方向:SUPPLIES 有明确方向,建模时不定方向,查询时全是意外。约束:节点类型控制在 5 到 10 个,关系类型 10 到 20 个,超出就先审视本体。
版本化你的本体(允许的类型清单 + 每类的定义与示例),放进抽取管道的提示词里。第 4 节会看到:LLM 建图的质量几乎完全取决于这份清单写得多清楚。
Neo4j 与 Cypher 入门
Neo4j 是事实标准的属性图数据库,Cypher 是它的查询语言。用“画 ASCII 图”的方式声明模式,数据库负责匹配。
本地起步只需要一个容器。浏览器地址用于可视化查询,Bolt 地址供程序连接:
docker run -d --name neo4j \
-p 7474:7474 -p 7687:7687 \
-e NEO4J_AUTH=neo4j/your-password \
neo4j:5
# 浏览器界面(可视化查询): http://localhost:7474
# 程序连接地址(Bolt): bolt://localhost:7687
Cypher 的核心思想是模式即查询:(:Company)-[:SUPPLIES]->(:Product) 用圆括号表示节点、方括号表示关系,箭头表示方向。你画出“要找的子图长什么样”,引擎去全图匹配:
// 写入一:CREATE,适合一次性手工造数据
CREATE (:Company {name: "华芯科技", country: "CN"})
CREATE (:Product {name: "电源管理芯片", id: "PM-8321"})
// 写入二:MERGE,幂等写入(查到即复用,查不到才创建)
// 生产管道只用它:重复执行不产生重复数据
MERGE (c:Company {name: "华芯科技"})
MERGE (p:Product {name: "电源管理芯片"})
MERGE (c)-[s:SUPPLIES]->(p)
ON CREATE SET s.since = 2021, s.share = 0.35
// 查询:MATCH 声明模式,WHERE 过滤,RETURN 投影
MATCH (c:Company)-[:SUPPLIES]->(p:Product)
WHERE c.country = "CN"
RETURN c.name, collect(p.name) AS products
ORDER BY size(products) DESC
LIMIT 5
| 语句 | 作用 | 备注 |
|---|---|---|
MATCH | 声明要匹配的子图模式 | 相当于 SQL 的 FROM + JOIN,但多跳不需要写 JOIN |
MERGE | 存在即复用,不存在即创建 | 建图管道唯一正确的写入方式 |
SET / ON CREATE SET | 更新属性 / 仅首次创建时设置 | 配合 MERGE 实现增量更新 |
DETACH DELETE | 删除节点及其全部关系 | 裸 DELETE 遇到带关系的节点会报错,这是保护 |
PROFILE | 显示查询执行计划 | 性能调优第一步,先看扫描了多少节点 |
抽取管道里用 CREATE,每重跑一次就多一整套重复节点,图很快不可用。永远用 MERGE,并配合第 2 节的唯一性约束双保险:约束拦截违规写入,MERGE 保证幂等。
用 LLM 构建知识图谱
传统 NLP 建图需要标注数据训练专用模型;LLM 把它变成一个提示词问题。质量护栏是本体:开放抽取必失控,约束抽取可生产。
抽取管道做三件事:实体识别(找出公司、产品、材料)、关系抽取(谁与谁是什么关系)、归一化(“华芯”归一到“华芯科技”)。手写方案用结构化输出逐项控制:
from typing import Literal
from pydantic import BaseModel, Field
from langchain.chat_models import init_chat_model
llm = init_chat_model("openai:gpt-4o-mini")
NODE_TYPES = Literal["Company", "Product", "Material", "Country", "Region"]
REL_TYPES = Literal["SUPPLIES", "REQUIRES", "PRODUCED_IN",
"LOCATED_IN", "ALTERNATIVE_TO"]
class Triple(BaseModel):
subject: str = Field(description="主体实体名,使用规范全称")
subject_type: NODE_TYPES
predicate: REL_TYPES
object: str = Field(description="客体实体名,使用规范全称")
object_type: NODE_TYPES
evidence: str = Field(description="支撑该三元组的原文片段")
class Extraction(BaseModel):
triples: list[Triple] = Field(description="文中全部符合本体的三元组")
extractor = llm.with_structured_output(Extraction)
def triples_from(text: str) -> list[Triple]:
return extractor.invoke(
"从文本抽取符合本体约束的三元组。只抽取文本明确陈述的事实,"
"不要推断;无符合项则返回空列表。\n\n" + text
).triples
抽取结果用 MERGE 幂等入库(参数化查询防止注入,双大括号是 format 转义):
MERGE_TMPL = """
MERGE (s:{stype} {{name: $subject}})
MERGE (o:{otype} {{name: $object}})
MERGE (s)-[r:{pred}]->(o)
ON CREATE SET r.evidence = $evidence
"""
def write_triples(tx, t: Triple):
tx.run(
MERGE_TMPL.format(stype=t.subject_type, otype=t.object_type,
pred=t.predicate),
subject=t.subject, object=t.object, evidence=t.evidence,
)
# with driver.session() as s: s.execute_write(write_triples, t)
现成方案:LLMGraphTransformer
LangChain 把上述流程封装成了 LLMGraphTransformer,直接从文档生成图结构并写入 Neo4j:
from langchain_experimental.graph_transformers import LLMGraphTransformer
from langchain_neo4j import Neo4jGraph
transformer = LLMGraphTransformer(
llm=init_chat_model("openai:gpt-4o-mini"),
allowed_nodes=["Company", "Product", "Material", "Country", "Region"],
allowed_relationships=[
"SUPPLIES", "REQUIRES", "PRODUCED_IN", "LOCATED_IN", "ALTERNATIVE_TO",
],
node_properties=["country"],
relationship_properties=["since", "share"],
)
graph_docs = transformer.convert_to_graph_documents(docs)
neo4j = Neo4jGraph(url="bolt://localhost:7687",
username="neo4j", password="your-password")
neo4j.add_graph_documents(graph_docs, include_source=True)
# include_source:每个抽取节点连回原文档节点,图谱事实可溯源
| 方案 | 优势 | 代价 |
|---|---|---|
| 手写结构化抽取 | 完全可控:evidence 溯源、置信度、自定义归一规则 | 提示词与 Schema 维护成本 |
| LLMGraphTransformer | 开箱即用,与 LangChain 生态无缝集成 | 定制空间小,复杂本体仍要手写 |
一,allowed 约束必开:开放抽取的类型爆炸是不可逆的(图脏了只能重建)。二,evidence 字段必带:每个三元组能指回原文,抽检与纠错才有抓手。三,批量抽检:每批随机抽 20 条人工验证准确率,低于 90% 先修提示词,不要急着入库。
Cypher 进阶:多跳与聚合
变量长度路径是图查询的“降维打击”:SQL 要写多层自连接的间接依赖,这里是一个星号。
变长路径:风险传导查询
// 稀土镨断供会波及哪些公司?(沿 SUPPLIES / REQUIRES 反向最多 3 跳)
MATCH path = (c:Company)-[:SUPPLIES|REQUIRES*1..3]->(m:Material {name: "稀土镨"})
RETURN c.name AS company,
[n IN nodes(path) | coalesce(n.name, n.id)] AS chain,
length(path) AS hops
ORDER BY hops
*1..3:关系重复 1 到 3 次,跳数运行时可查(length(path))。SUPPLIES|REQUIRES:竖线组合多种关系类型,表达“沿供应链任意边传导”。nodes(path):把路径展开成节点列表,用于展示完整传导链。
聚合与分组
// 每个国家掌握的材料与产品面:分组聚合一次完成
MATCH (c:Company)-[:SUPPLIES]->(p:Product)-[:REQUIRES]->(m:Material)
RETURN c.country AS country,
count(DISTINCT p) AS product_count,
collect(DISTINCT m.name)[..5] AS top_materials
ORDER BY product_count DESC
LIMIT 10
最短路径
// 两家公司之间最短的关联路径(最多 5 跳,任意关系方向)
MATCH p = shortestPath(
(a:Company {name: "华芯科技"})-[*..5]-(b:Company {name: "北方精密"})
)
RETURN [n IN nodes(p) | n.name] AS path, length(p) AS hops
无上限的 * 在连通图上等于遍历全图,几百万节点的库直接卡死。三重防护:永远写跳数上限(如 *1..3);起点锚定标签与索引属性(如 (:Company {name: ...}),命中第 2 节的唯一索引);上线前 PROFILE 看扫描量,节点扫描数应在千级而非百万级。
GraphRAG:图谱检索增强
图谱建好之后,检索变成两种模式:局部问题从实体邻域取上下文,全局问题用社区摘要聚合回答。这是第 3 章“GraphRAG 擅长什么”的落地实现。
局部检索:实体邻域展开
from langchain_neo4j import Neo4jGraph
from langchain.chat_models import init_chat_model
neo4j = Neo4jGraph(url="bolt://localhost:7687",
username="neo4j", password="your-password")
llm = init_chat_model("openai:gpt-4o-mini")
def graph_context(entity: str, limit: int = 50) -> str:
"""以实体为中心展开 1 跳邻域,拼成三元组文本。"""
rows = neo4j.query(
"""
MATCH (e {name: $entity})-[r]-(n)
RETURN e.name AS s, type(r) AS p, n.name AS o
LIMIT $limit
""",
{"entity": entity, "limit": limit},
)
return "\n".join(f"{row['s']} --{row['p']}--> {row['o']}" for row in rows)
def answer_from_graph(question: str, entity: str) -> str:
ctx = graph_context(entity)
return llm.invoke(
"依据以下知识图谱事实回答问题;图谱未覆盖的部分明确说明。\n\n"
"图谱事实:\n" + ctx + f"\n\n问题:{question}"
).content
入口处的实体链接(把问题里的“华芯”对应到图中的“华芯科技”)可以先用向量库检索实体名,再模糊匹配;别名表(第 8 节)是它的地基。
全局检索:社区摘要
离线阶段对图跑社区检测(Leiden 算法),把高度互联的节点聚成社区,逐社区生成 LLM 摘要并按层级向上归并;查询时把相关层级的社区摘要分发并行作答,再汇总成最终答案(map-reduce)。这正是 Microsoft GraphRAG 的核心机制,其开源实现开箱即用;自建轻量版也完全可行:社区检测 + 摘要 + 汇总三步,每步都只是普通 LLM 调用。
社区检测与摘要重建是持续成本(图更新后要重算)。只当“全库主题、风险全景、跨实体聚合”类问题是你的高频需求时才建;大多数系统的图谱部分用局部检索已经足够,全局类问题也可以退化为“向量粗筛 + 图谱验证”。
Text2Cypher 与混合架构
多跳问题的另一条路:不检索邻域,而是让 LLM 直接把自然问题翻译成 Cypher,交给数据库执行。与向量检索组合成完整的混合架构。
from langchain_neo4j import GraphCypherChain
qa = GraphCypherChain(
graph=neo4j,
llm=init_chat_model("openai:gpt-4o-mini"),
allow_dangerous_operations=False, # 默认拒绝写操作,保持开启
)
result = qa.invoke({"query": "哪些公司间接依赖稀土镨?列出传导链。"})
# 内部流程:读 graph.schema(节点/关系类型)→ 生成 Cypher → 执行 → 生成答案
链会自动把图的本体注入提示词,这也是第 2 节强调“本体是活文档”的另一个原因:本体写得清楚,生成的 Cypher 就正确。复杂本体可以补 cypher_prompt,加入惯用查询示例(few-shot)。
LLM 生成的查询是不可信代码。五条防线:连接使用只读账号(权限是最后一道墙);保留 allow_dangerous_operations=False;对生成语句做静态检查(拒绝 CREATE / DELETE / SET / CALL);强制注入 LIMIT;设置查询超时。缺任何一条,一次提示词注入就能改掉你的图。
混合架构:向量 + 图 + 关键词
生产系统通常三者并存,按问题类型路由。这是第 3 章 Adaptive RAG 的具体化:
| 问题类型 | 示例 | 路由到 |
|---|---|---|
| 局部事实题 | “PM-8321 的供应份额是多少?” | 向量检索(第 3 章管道) |
| 多跳关系题 | “稀土镨断供波及哪些公司?” | 图谱(Text2Cypher 或邻域扩展) |
| 全局聚合题 | “当前供应链的主要集中风险?” | 社区摘要,或向量粗筛 + 图谱验证 |
| 混合题 | “华芯的最新风险与关联新闻?” | 双路并行,上下文拼接 |
def retrieve(question: str) -> str:
"""按问题类型路由到不同检索器(分类器用结构化输出实现)。"""
kind = classify(question) # fact / relation / global
if kind == "fact":
return vector_context(question) # 第 3 章的混合检索管道
if kind == "relation":
entities = link_entities(question) # 向量库召回实体名
return "\n".join(graph_context(e) for e in entities)
return community_context(question) # 社区摘要
路由不是唯一选择:把图谱邻域直接作为“上下文扩展器”——向量检索命中文档后,取其中的实体在图上展开 1 跳邻域,把邻域三元组拼进提示词。文档语义与结构化事实同场出现,特别适合“读文档 + 顺藤摸瓜”的分析类问题。
质量控制与图谱维护
图谱是长期资产:错误关系比缺失关系更危险,因为下游会基于它做决策。维护三件事:消歧、增量、抽检。
实体消歧:别名与规范名
同一个实体多种称谓(“华芯” = “华芯科技” = “HX Tech”),不同实体相似称谓(两家“华芯”)。治理方案:每个节点一个规范名,其余称谓进 aliases 属性;实体链接时先查别名表:
// 规范名 + 别名表:抽取与查询的统一入口
MATCH (c:Company {name: "华芯科技"})
SET c.aliases = ["华芯", "HX Tech", "华芯半导体"];
// 实体链接:按规范名或别名都能命中
MATCH (c:Company)
WHERE c.name = $name OR $name IN c.aliases
RETURN c.name AS canonical;
真正困难的判定(两家同名公司是否同一家)交给 LLM 批量裁决:拉取候选对的上下文属性(国家、产品、地址),结构化输出 same / different / unsure,只自动合并高置信度项,存疑项进人工队列。
增量更新而不是全量重建
- 新文档入库:抽取 → MERGE(幂等),关系带
last_seen时间戳。 - 关系失效:定期把长期未见的供应关系标记
status: "stale"而不是直接删除,保留历史可回溯(配合 VALID_FROM / VALID_TO)。 - 本体变更:allowed 类型清单版本化;新增类型只影响增量,删除/改义类型需要迁移脚本与重建子图的成本评估。
抽检:给图谱定质量基线
- 关系准确率:随机抽 20-50 条三元组人工验证,目标 90% 以上(低于先修提示词与本体)。
- 实体歧义率:同名不同实体的重复节点数量(唯一约束应保证为 0)。
- 查询命中率:高频问题在图上能否取到非空结果,空结果说明抽取覆盖不足。
低置信度的抽取结果宁可不入库,或入库时带 confidence 属性并在查询时过滤。用户对“图谱说 A 供应 B”的错误记忆,远比“图谱不知道”伤害大:前者摧毁信任,后者只是留待补全。
生产化考量
从 demo 图到生产图,多出四件事:索引、备份、权限与规模预期。
索引与查询性能
- 唯一约束即索引:第 2 节的约束自动为 name / id 建索引,实体锚点查询全部命中。
- 补 RANGE 索引:高频过滤属性(country、status、updated_at)另建索引,避免全标签扫描。
- PROFILE 习惯:每条上线查询看一次执行计划,重点盯 NodeScan / Expand 的行数。
图 + 向量同库:Neo4j 向量索引
Neo4j 内置 HNSW 向量索引,文档块、实体与向量可以同库共存,第 7 节的混合架构不用再维护两个存储:
// 为 Document 节点的嵌入建向量索引
CREATE VECTOR INDEX doc_chunks IF NOT EXISTS
FOR (d:Document) ON (d.embedding)
OPTIONS {indexConfig: {
`vector.dimensions`: 1536,
`vector.similarity_function`: "cosine"
}}
// 相似检索:图与向量一次查询打通
CALL db.index.vector.queryNodes("doc_chunks", 5, $query_vector)
YIELD node, score
MATCH (node)-[:MENTIONS]->(e:Entity)
RETURN node.text, score, collect(e.name) AS entities
定时备份(neo4j-admin database dump);只读账号给所有 LLM 链;慢查询日志常开;堆内存监控。
单机 Neo4j 在亿级节点/十亿级关系内可用;超过后考虑集群或 NebulaGraph 等分布式图库,但那是 Rare 场景,别提前优化。
常见陷阱与 FAQ
起步五分钟可以用 NetworkX 在内存里建图,验证本体与查询思路;但 Cypher、索引、约束、权限、LangChain 集成、可视化浏览器这一整套生态是 Neo4j 的护城河,生产环境几乎不必犹豫。学习阶段直接上 Docker 版即可,两者成本都不高。
粗算:每千 token 文档一次抽取调用,外加抽检与消歧的少量调用。控制手段:只对高价值、关系密集的文档建图(其余只进向量库);小模型抽取 + 大模型抽检;增量 MERGE 而非全量重跑。把建图当成“给 20% 高价值文档的额外投资”,而不是全库标配。
不。主流形态是同一语料双索引:向量库覆盖局部事实题,图谱覆盖多跳与全局题,查询层按问题类型路由(第 7 节)。Neo4j 向量索引甚至可以把两者收进同一个库。真正需要取舍的是维护成本:团队规模小、问题类型单一时,先把向量 + 重排做扎实。
除非你的场景要对接开放数据(Wikidata、DBpedia)或学术知识工程,LLM 应用层用属性图模型就够了。RDF 三元组的表达能力更强,但工具链与开发体验对应用工程师不友好,LangChain 生态也围绕属性图构建。
按序排查:一是本体太宽,收紧 allowed 类型;二是提示词缺少 few-shot 示例,补上正反例(尤其是“不要抽推断”的反例);三是抽取后无验证环节,加 evidence 字段并抽检,错误模式回流到提示词。记住验收标准:关系准确率 90% 以上才入库。
严格说全图重算才正确,但工程上只重算受影响社区:变更节点所在社区及其上层社区重新摘要,未受影响的复用。这是 GraphRAG 全局检索的主要运维成本,也是评估“值不值得上全局检索”的核心变量(见第 6 节)。
实战练习与资源
以下练习用同一个供应链领域贯穿:从建图到查询到 GraphRAG,做完即拥有一个可展示的知识图谱项目。