LLM 工程手册LLM ENGINEERING

CHAPTER 04知识结构化 · GRAPH

知识图谱

向量检索只会找"相似的片段",回答不了"谁间接依赖谁"。知识图谱把事实显式建模为节点与关系,配合 LLM 完成抽取、查询与 GraphRAG,补上 RAG 的关系推理短板。

开始阅读 难度进阶 先修第一、三章 3小时
04.1

知识图谱是什么

知识图谱把世界表示成"实体 + 关系"的网络:节点是事物,边是它们之间的连接。向量库存的是"长什么样的文本",图谱存的是"事实本身"。

知识的最小单元是三元组:(华芯科技, 供应, 电源管理芯片)。海量三元组连起来,就构成一张可遍历、可推理的网络。属性图模型(Neo4j 等采用)在此基础上允许节点与关系各自携带属性:

华芯科技 :Company {country: "CN"} 电源管理芯片 :Product {id: "PM-8321"} 稀土镨 :Material 中国 :Country SUPPLIES {share: 0.35, since: 2021} REQUIRES PRODUCED_IN LOCATED_IN 一行 Cypher 即可回答:"稀土镨断供会波及哪些公司?"——沿 REQUIRES 与 SUPPLIES 反向遍历
供应链知识图谱片段:节点带标签与属性,关系带类型、方向与属性。关系型数据库要写多层 JOIN 的"间接依赖"查询,图上是一条模式匹配。
三元组(Triple)
(主体, 谓词, 客体) 的最小知识单元,如 (华芯科技, 供应, 芯片)。
属性图(Property Graph)
节点与关系都可携带属性的图模型,Neo4j、NebulaGraph 均采用;LLM 应用层的事实标准。
本体(Ontology / Schema)
图谱的"建表规范":允许哪些节点标签、关系类型与属性。先定本体,再抽数据。
多跳(Multi-hop)
沿关系跨越多个节点的遍历。"间接供应商"是两跳,"风险传导链"是三跳以上。
实体消歧(Entity Resolution)
判断"华芯"与"华芯科技"是否同一实体、两家同名公司是否不同实体。图谱质量的第一决定因素。
图算法
在图结构上运行的分析:PageRank、社区检测(Leiden)、最短路径、中心度。GraphRAG 全局检索的地基。
维度关系型(SQL)向量库图数据库
数据形态表、行、列向量 + 元数据节点、关系、属性
擅长精确条件查询、事务、聚合统计语义相似度近邻多跳关系、路径发现、图算法
弱项深 JOIN 性能急剧劣化精确匹配弱、无关系推理全库统计类查询不如 SQL
典型场景订单、账务语义检索(第 3 章)风控传导、供应链、推荐、知识问答
与上一章的衔接

第 3 章的结论是:向量 RAG 回答不了"全局聚合、多跳关系"类问题。本章就是那句话的完整解法:把同一批文档既建向量索引(局部事实题)又建图谱(关系与全局题),运行时按问题路由。两种索引各司其职,不是二选一。

04.2

本体设计与建模模式

本体是图谱的 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 建图的质量几乎完全取决于这份清单写得多清楚。

04.3

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 入库是事故的起点

抽取管道里用 CREATE,每重跑一次就多一整套重复节点,图很快不可用。永远用 MERGE,并配合第 2 节的唯一性约束双保险:约束拦截违规写入,MERGE 保证幂等。

04.4

用 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% 先修提示词,不要急着入库。

04.5

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 看扫描量,节点扫描数应在千级而非百万级。

04.6

GraphRAG:图谱检索增强

图谱建好之后,检索变成两种模式:局部问题从实体邻域取上下文,全局问题用社区摘要聚合回答。这是第 3 章“GraphRAG 擅长什么”的落地实现。

问题 实体链接 局部检索(Local) 实体邻域 1-2 跳展开为三元组 全局检索(Global) 社区 A 社区 B 社区 C 社区摘要 map-reduce 汇总 LLM 生成 局部适合“X 与 Y 什么关系”,全局适合“全库主要主题/主要风险”
GraphRAG 双模式:局部检索从实体出发展开邻域;全局检索用社区摘要做 map-reduce。

局部检索:实体邻域展开

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 调用。

什么时候值得上全局检索

社区检测与摘要重建是持续成本(图更新后要重算)。只当“全库主题、风险全景、跨实体聚合”类问题是你的高频需求时才建;大多数系统的图谱部分用局部检索已经足够,全局类问题也可以退化为“向量粗筛 + 图谱验证”。

04.7

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)。

Text2Cypher 的安全底线

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 跳邻域,把邻域三元组拼进提示词。文档语义与结构化事实同场出现,特别适合“读文档 + 顺藤摸瓜”的分析类问题。

04.8

质量控制与图谱维护

图谱是长期资产:错误关系比缺失关系更危险,因为下游会基于它做决策。维护三件事:消歧、增量、抽检。

实体消歧:别名与规范名

同一个实体多种称谓(“华芯” = “华芯科技” = “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”的错误记忆,远比“图谱不知道”伤害大:前者摧毁信任,后者只是留待补全。

04.9

生产化考量

从 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 场景,别提前优化。

04.10

常见陷阱与 FAQ

一定要用 Neo4j 吗?

起步五分钟可以用 NetworkX 在内存里建图,验证本体与查询思路;但 Cypher、索引、约束、权限、LangChain 集成、可视化浏览器这一整套生态是 Neo4j 的护城河,生产环境几乎不必犹豫。学习阶段直接上 Docker 版即可,两者成本都不高。

LLM 建图的成本到底多高?

粗算:每千 token 文档一次抽取调用,外加抽检与消歧的少量调用。控制手段:只对高价值、关系密集的文档建图(其余只进向量库);小模型抽取 + 大模型抽检;增量 MERGE 而非全量重跑。把建图当成“给 20% 高价值文档的额外投资”,而不是全库标配。

图谱和向量库必须二选一吗?

不。主流形态是同一语料双索引:向量库覆盖局部事实题,图谱覆盖多跳与全局题,查询层按问题类型路由(第 7 节)。Neo4j 向量索引甚至可以把两者收进同一个库。真正需要取舍的是维护成本:团队规模小、问题类型单一时,先把向量 + 重排做扎实。

需要学 RDF / OWL 这些标准吗?

除非你的场景要对接开放数据(Wikidata、DBpedia)或学术知识工程,LLM 应用层用属性图模型就够了。RDF 三元组的表达能力更强,但工具链与开发体验对应用工程师不友好,LangChain 生态也围绕属性图构建。

LLM 抽取总出错怎么办?

按序排查:一是本体太宽,收紧 allowed 类型;二是提示词缺少 few-shot 示例,补上正反例(尤其是“不要抽推断”的反例);三是抽取后无验证环节,加 evidence 字段并抽检,错误模式回流到提示词。记住验收标准:关系准确率 90% 以上才入库。

图更新后社区摘要要重算吗?

严格说全图重算才正确,但工程上只重算受影响社区:变更节点所在社区及其上层社区重新摘要,未受影响的复用。这是 GraphRAG 全局检索的主要运维成本,也是评估“值不值得上全局检索”的核心变量(见第 6 节)。

04.11

实战练习与资源

以下练习用同一个供应链领域贯穿:从建图到查询到 GraphRAG,做完即拥有一个可展示的知识图谱项目。

延伸资源