知识图谱构建实战:从文本到图谱的完整流水线

知识图谱构建工程手册:实体识别(spaCy/预训练模型)与实体链接消歧、基于规则/依赖解析/LLM 的三元组抽取、本体设计与 RDF→属性图映射、从文本清洗到图谱入库的完整流水线实现,附质量评估与增量更新方案。

导语:知识图谱的 1% 是模型,99% 是工程

很多人以为构建知识图谱是"训练一个抽取模型"的事。真实项目中,实体识别与链接、本体设计、数据清洗、入库去重、增量更新的工程量远大于模型本身。本文承接 知识图谱构建与应用 的概念篇,聚焦可落地的工程实现——从一份原始文档到一张可查询的 Neo4j 图谱。

一句话总结:知识图谱构建是"抽取(Extract)→ 链接(Link)→ 映射(Map)→ 入库(Load)→ 评估(Evaluate)“的闭环流水线,每一步都要处理真实世界的噪声。


1. 实体识别与链接

1.1 命名实体识别(NER)

中文 NER 的现实选择是预训练模型或 LLM API:

import spacy

nlp = spacy.load("zh_core_web_trf")  # transformer 中文模型
doc = nlp("2024年,华为在深圳发布了昇腾AI芯片,任正非是华为创始人。")

for ent in doc.ents:
    print(f"{ent.text}\t{ent.label_}\t{ent.start_char}-{ent.end_char}")
# 输出:
# 2024年    DATE
# 华为     ORG
# 深圳     GPE
# 昇腾AI芯片 PRODUCT
# 任正非   PERSON
# 华为     ORG

1.2 实体链接(Entity Linking)

NER 输出的是"字符串”,实体链接解决"字符串指代哪个唯一实体":

# 候选生成:字符串 → 候选实体(拼音/别名/精确匹配)
def generate_candidates(entity_text, entity_index):
    candidates = []
    # 1. 精确匹配
    if entity_text in entity_index:
        candidates += entity_index[entity_text]
    # 2. 拼音/别名校对(apoc.text.clean 同思路)
    cleaned = clean_name(entity_text)
    for alias, eids in entity_index.items():
        if cleaned == clean_name(alias):
            candidates += eids
    return candidates

# 候选消歧:用上下文向量相似度打分
def disambiguate(candidates, context_vec):
    scores = []
    for eid, desc_vec in candidates:
        sim = cosine_similarity(desc_vec, context_vec)
        scores.append((eid, sim))
    return max(scores, key=lambda x: x[1])

生产系统通常维护一张实体别名表((别名, 实体ID, 类型, 置信度)),Neo4j 里可以这样存:

// 别名索引:实体消歧的基础设施
CREATE (:Entity {id: "E-001", name: "华为技术有限公司", type: "Organization"})
CREATE (:EntityAlias {alias: "华为", entityId: "E-001", confidence: 0.9})
CREATE (:EntityAlias {alias: "Huawei", entityId: "E-001", confidence: 0.95})
CREATE INDEX entity_alias_name FOR (a:EntityAlias) ON (a.alias)

1.3 实体对齐与共指消解

同一实体的不同写法要合并:“华为"“华为技术有限公司"“Huawei” 指同一个公司。策略分三档:

策略方法精度/召回成本
精确/别名匹配词典 + 别名表高精度低召回低
模糊匹配编辑距离/Jaro-Winkler中低
向量聚类实体描述嵌入聚类中高召回高
# 模糊匹配:Jaro-Winkler 适合中文组织名
import jellyfish
print(jellyfish.jaro_winkler_similarity("华为", "华为公司"))   # 0.80
print(jellyfish.jaro_winkler_similarity("华为", "中兴"))       # 0.33

一句话总结:NER 只是"认出名字”,实体链接才是"指到唯一”——别名表和上下文消歧是链接质量的关键。


2. 三元组抽取

2.1 基于规则:精确但覆盖面有限

import re

RULES = [
    (r"(.+?)出生于(.+?)[,。;]", "BORN_IN"),
    (r"(.+?)毕业于(.+?)[,。;]", "GRADUATED_FROM"),
    (r"(.+?)是(.+?)的创始人[,。;]", "FOUNDED_BY"),
    (r"(.+?)成立于(.+?)[,。;]", "FOUNDED_ON"),
]

def rule_extract(text):
    triples = []
    for pattern, rel in RULES:
        for m in re.finditer(pattern, text):
            subj, obj = m.group(1).strip(), m.group(2).strip()
            triples.append((subj, rel, obj))
    return triples

print(rule_extract("任正非是华为的创始人,华为成立于1987年。"))
# [('任正非', 'FOUNDED_BY', '华为'), ('华为', 'FOUNDED_ON', '1987年')]

2.2 基于依赖解析:利用语法结构

用 spaCy 的中文依存句法找主谓宾:

import spacy

nlp = spacy.load("zh_core_web_trf")
doc = nlp("华为发布了昇腾AI芯片")

# 找 nsubj(主语)和 dobj(直接宾语)之间的动词
for token in doc:
    if token.dep_ == "nsubj":
        subj = token.text
        verb = token.head.text
        objs = [t.text for t in token.head.children if t.dep_ in ("dobj", "dative")]
        for obj in objs:
            print((subj, verb, obj))
# ('华为', '发布', '昇腾AI芯片')

2.3 基于 LLM:通用但需要约束

LLM 抽取的工程要点是输出约束 + 结构化 schema:

import json
from openai import OpenAI

client = OpenAI()

def llm_extract(sentence):
    prompt = f"""从句子中抽取所有事实三元组,只输出 JSON 数组。
每个三元组格式:{{"head": 主语, "relation": 关系, "tail": 宾语}}
允许的关系:BORN_IN, WORKS_AT, FOUNDED_BY, DEVELOPED, ACQUIRED, PART_OF
禁止臆造,句子中没有的事实不要输出。
句子:{sentence}"""
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        response_format={"type": "json_object"}
    )
    data = json.loads(resp.choices[0].message.content)
    return data.get("triples", [])

print(llm_extract("华为的创始人任正非在1987年创立了公司。"))

2.4 关系分类模型:有标注时的最优解

from transformers import AutoTokenizer, AutoModelForSequenceClassification

tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
model = AutoModelForSequenceClassification.from_pretrained(
    "bert-base-chinese", num_labels=12  # 12 种关系
)

def classify_relation(head, tail, sentence):
    text = f"[CLS]{sentence}[SEP]{head}[SEP]{tail}[SEP]"
    inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128)
    logits = model(**inputs).logits
    rel_id = logits.argmax().item()
    return ID_TO_RELATION[rel_id]

一句话总结:抽取手段是光谱——规则(高精度低覆盖)→ 依赖解析(利用语法)→ LLM(高覆盖需约束)→ 分类模型(有标注时最稳)。


3. 本体设计与属性图映射

3.1 本体层级设计

本体(Ontology)定义"有什么实体、什么关系、什么约束"。先用轻量清单再进代码:

# ontology.yaml:知识图谱的概念层
EntityTypes:
  - Organization: {props: [name, foundedYear, headquarters]}
  - Person: {props: [name, birthYear, nationality]}
  - Product: {props: [name, category]}
  - Technology: {props: [name, domain]}

Relations:
  - FOUNDED_BY:   {from: Organization, to: Person}
  - WORKS_AT:     {from: Person, to: Organization}
  - DEVELOPED:    {from: Organization, to: Product}
  - ACQUIRED:     {from: Organization, to: Organization}

Constraints:
  - "每家企业至少有一个名称"
  - "FOUNDED_BY 的尾实体必须是 Person"

3.2 RDF → 属性图映射

如果上游是 RDF 数据(Turtle/N-Triples),需要做模型转换:

@prefix ex: <http://example.org/> .
ex:Huawei ex:founder ex:RenZhengfei .
ex:Huawei ex:name "华为技术有限公司" .

映射到属性图:

// 概念映射:rdf:type → 标签;URI → id 属性;三元组 → 关系
MERGE (o:Organization {id: "ex:Huawei"})
  ON CREATE SET o.name = "华为技术有限公司"
MERGE (p:Person {id: "ex:RenZhengfei"})
MERGE (o)-[:FOUNDED_BY]->(p)

// 批量映射工具:neosemantics (n10s) 插件
CALL n10s.rdf.import.fetch("file:///data/graph.ttl", "Turtle")

3.3 属性图的本体落地

属性图的优势是把"类"变成标签、把"属性约束"变成索引与约束:

// 用约束落实本体规则
CREATE CONSTRAINT org_id_unique FOR (o:Organization) REQUIRE o.id IS UNIQUE
CREATE CONSTRAINT person_id_unique FOR (p:Person) REQUIRE p.id IS UNIQUE
CREATE CONSTRAINT product_id_unique FOR (p:Product) REQUIRE p.id IS UNIQUE

// 用索引支撑本体属性检索
CREATE INDEX org_name FOR (o:Organization) ON (o.name)

一句话总结:本体是知识图谱的"宪法"——先定概念层再谈数据层;RDF 强调互操作,属性图强调约束与性能,n10s 桥接两者。


4. 从文本到图谱的完整流水线

4.1 流水线架构

                    ┌──────────────────────────────────────┐
 文档/网页/PDF/DB    │   Pipeline (Python / Airflow / Dagster)│
 ──────────────────► │                                      │
                     │  ① 预处理   清洗·分段·语言检测         │
                     │  ② 抽取     NER + 关系抽取(规则/LLM)  │
                     │  ③ 链接     实体消歧·共指合并          │
                     │  ④ 映射     三元组 → 属性图 Cypher     │
                     │  ⑤ 入库     MERGE 幂等写入·批量        │
                     │  ⑥ 评估     抽样验证·覆盖率统计        │
                     │  ⑦ 增量     变更检测·版本管理          │
                     └──────────────────────────────────────┘
                                          │
                                          ▼
                                   ┌─────────────┐
                                   │   Neo4j KG   │
                                   │  索引+约束   │
                                   └─────────────┘

4.2 数据预处理与分段

import re

def preprocess(raw_text):
    # 去 HTML、去噪声
    text = re.sub(r"<[^>]+>", "", raw_text)
    text = re.sub(r"\s+", " ", text)
    # 段落切分(抽取的最小单元,保证共指上下文)
    paragraphs = [p.strip() for p in re.split(r"\n{2,}", text) if len(p.strip()) > 50]
    return paragraphs

4.3 入库:MERGE 幂等写入

抽取结果统一为 (head, relation, tail, source, confidence),入库用 MERGE 保证幂等:

// 批量写入(UNWIND + MERGE + 动态关系类型)
UNWIND $triples AS t
MERGE (h:Entity {id: t.head_id})
  ON CREATE SET h.name = t.head_name, h.type = t.head_type
MERGE (tl:Entity {id: t.tail_id})
  ON CREATE SET tl.name = t.tail_name, tl.type = t.tail_type
WITH h, tl, t
CALL apoc.merge.relationship(
  h, t.relation, {confidence: t.confidence}, {}, tl
) YIELD rel
SET rel.source = t.source
RETURN count(rel)

4.4 质量评估

抽取的质量评估三个维度:

维度指标方法
准确率三元组置信度人工抽样验证 + 模型置信度过滤
完整度覆盖的实体/关系比例与权威数据集对比(如 Wikidata)
一致性类型/值域不冲突约束检查(Neo4j CONSTRAINT 兜底)
// 一致性检查:FOUNDED_BY 的尾实体必须 Person
MATCH (o:Organization)-[:FOUNDED_BY]->(x)
WHERE NOT x:Person
RETURN o.id, x.id AS invalid_tail, "类型违反本体约束" AS issue

// 悬挂节点检查:无关系的孤立实体
MATCH (e:Entity)
WHERE NOT (e)--()
RETURN count(e) AS dangling_entities

4.5 增量更新与版本管理

# 增量策略:按变更批次入库,保留 provenance
def incremental_load(batch, batch_id):
    tx.run("""
        UNWIND $triples AS t
        MERGE (h:Entity {id: t.head_id})
        ...
        WITH h, tl, t, $batch_id AS bid
        CALL apoc.merge.relationship(h, t.relation, 
            {batchId: bid, confidence: t.confidence}, {}, tl)
        YIELD rel
        SET rel.validUntil = $validUntil   // 支持软删除/过期
    """, triples=batch, batch_id=batch_id, validUntil=None)

一句话总结:流水线的工程灵魂是"幂等(MERGE)+ 溯源(source/batchId)+ 约束(CONSTRAINT)"——三者保证图谱可重跑、可追溯、不脏。


5. 实战案例:企业资讯图谱

从公开新闻构建企业关系图谱:

# 案例数据:3 条新闻
news = [
    "华为在深圳发布了昇腾AI芯片,用于大模型训练。",
    "任正非创立了华为,总部位于深圳。",
    "阿里云推出了通义千问大模型,与昇腾芯片深度适配。",
]

流水线输出三元组:

headrelationtail来源
华为DEVELOPED昇腾AI芯片news1
华为BORN_IN(总部)深圳news2
任正非FOUNDED_BY华为news2
阿里云DEVELOPED通义千问大模型news3
昇腾AI芯片COMPATIBLE_WITH通义千问大模型news3

入图后可回答跨文档问题:

// 谁开发了大模型,且该模型与华为芯片适配?
MATCH (chip:Product {name: "昇腾AI芯片"})-[:COMPATIBLE_WITH]->(m:Product)
      <-[:DEVELOPED]-(vendor:Organization)
RETURN vendor.name AS 厂商, m.name AS 模型

// 深圳有哪些科技企业的产品?
MATCH (city:Entity {name: "深圳"})<-[:HEADQUARTERED_IN]-(o:Organization)
      -[:DEVELOPED]->(p:Product)
RETURN o.name, collect(p.name) AS products

一句话总结:案例证明流水线的价值——分散在三篇新闻里的知识,入图后一个 Cypher 查询就能完成跨文档推理。


6. 最佳实践与总结

工程清单:

  1. 别名表先行:实体链接的质量决定图谱质量,先建设别名/同义词基础设施
  2. 抽取手段分层:规则保底、LLM 扩充、人工审核兜底关键领域
  3. 本体约束化:把本体的"应该"翻译成 CONSTRAINT/INDEX 的"必须"
  4. MERGE 幂等:一切写入走 MERGE,让流水线可重跑
  5. 保留溯源:每个三元组带 source 和 confidence,方便回溯与降权
  6. 质量常态化:每次构建跑一致性检查 SQL,悬挂节点不积累

核心认知:

  1. 实体链接比 NER 更重要——“指到唯一"才是知识
  2. 抽取是手段,本体是框架,入库是工程
  3. 质量评估是持续动作,不是上线前的一锤子

延伸阅读:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「graphdb」更多文章

  1. 图数据库选型与集群运维:从 Causal Cluster 到分布式对比
  2. 图数据可视化与分析:从 Neo4j Bloom 到大规模前端渲染
  3. GraphRAG:知识图谱 + 向量检索的检索增强生成实践