
简介KnowledgeGraph Builder是一套面向自然语言处理与知识图谱开发者的端到端开源工具能从非结构化文本中自动抽取实体、解析共指、识别关系并生成RDF三元组知识图谱有效缓解人工知识库覆盖率不足、维护成本高的问题。资源包共106个文件大小仅3.12MB包含38个Python脚本用于核心算法20个CSV数据集供验证10个Jupyter Notebook提供分步讲解另有C源文件、Shell脚本与配置文件整体结构清晰方便按模块学习和改造。目前已有299人下载学习适合有一定Python基础的NLP学习者、知识图谱方向研究生或工程师。内置《星球大战》文本作为演示案例完整覆盖实体识别、共指消解、关系抽取、实体链接等环节配合Notebook与数据文件可帮助读者快速跑通项目并在自备语料上实践深入理解无监督知识图谱构建技术细节。 说实话我第一次把一篇文章拆成一张图的时候感觉特别奇妙。这也正是 KnowledgeGraph_Builder 这个项目想做的事把散落的非结构化文本变成能查询、能计算的知识图。做这个项目之前我在整理一批产品文档和用户评论文档不短但想回答“谁和谁是什么关系”要比想象中难得多。后来干脆写了这个脚手架一路解决了清洗、实体识别、关系抽取、图谱入库最后终于能用图回答问题。如果你也在处理大量文本想快速抽出实体关系或者想弄懂知识图是怎么从零搭起来的这篇内容应该能给你不少可复用的思路。1. 为什么我要写 KnowledgeGraph_Builder1.1 非结构化文本为什么难处理一篇文章看起来很顺畅但计算机看到的只是一串字符。没有表格、没有外键、没有明确的关系字段。比如某篇手机评测里写“A14芯片的性能比上一代提升了20%同时续航时间长了两个小时”。要回答“谁提升了什么”“提升了多少”“和谁比”对模型来说其实是三个独立问题。实体、属性、数值、比较关系全部散落在语法结构里传统关键词检索只能把含有“A14”“性能”的句子找出来却不能直接告诉你结论更不可能画出一张关系图。这也是知识图价值的起点。知识图本质上是用“节点”和“边”重新表达文本节点是实体比如产品、公司、人物边是关系比如“发布”“包含”“提升了”。一旦文本被改写成这种结构很多问题就从“自然语言理解和匹配”变成了“图上的邻接查询”后者要简单得多。比如“哪些产品采用了A14芯片”在知识图里就是找“A14芯片”的所有“采用”类型入边再比如“某公司过去一年发布了什么”也只是一次简单的图遍历。1.2 知识图到底能解决什么问题我最终需要的不只是一个演示而是一个能快速落地到具体领域的“从文本到图”的工具链。KnowledgeGraph_Builder 的目标就是输入一堆文本输出由实体、关系、属性组成的图。不追求大而全但希望在一个垂直领域里能稳定跑通。做这个项目之前我试过直接用现成的知识图谱平台比如把文本丢进去自动抽取但效果很不稳定。主要原因在于通用模型不了解领域词汇比如产品代号、内部名称、版本号经常被识别成普通单词甚至会拆成多个词。后来我意识到不能指望一个黑盒模型解决所有抽取问题必须把“词典、规则、句法分析、可选的模型”组合起来让每一步都可解释、可调整。项目名称叫 Builder 而不是 Server也是这个原因它更像一套搭建知识图的脚手架而不是某种开箱即用的控制台。2. 整体设计从文本到图形的四个阶段整个 pipeline 很朴素清洗 - 分句 - 实体识别 - 关系抽取 - 入库/可视化。看起来像是几个 NLP 任务的简单拼装但实际做起来每个环节都有坑。先说整体设计后面再给代码。2.1 文本预处理与分句关系抽取基本是以句子为单位的所以分句质量直接影响结果。我刚开始直接用句号分割结果英文缩写、小数点、引号把句子切得稀碎。后来逐步加上规则只有句号后面跟着空格和大写字母时才认为是句子边界遇到换行、分号、问号、感叹号也切分。中文还要额外处理引号里的句号不能一看到“。”就断句。清洗阶段也要做扎实。我遇到最多的是从 PDF 或网页里复制出来的文本常有页眉页脚、表格噪点、多余空行甚至还有“Table 1: xxx”这类标题插在段落中间。我的做法是先把整段文本按块切分过滤掉内容过长或过短的异常块再统一空格和换行符最后把“图1”“表2”这类孤立标题单独标记避免它们混入正文形成错误实体。2.2 实体识别让字符变成节点实体识别的目标是给每个句子标注出“谁、什么”。我尝试过三条路纯正则和词典、预训练 NER 模型、大语言模型。最终选的是混合方案词典优先模型兜底。词典适合领域专有词比如产品名、公司名、技术名词正则擅长处理版本号、日期、货币金额。通用人名、地名、机构名交给预训练模型比如 spaCy 的en_core_web_sm或者中文场景用zh_core_web_sm。这样搭配的原因是纯规则召回低只要词典里没有的词就全丢纯模型对特定领域不准我测试的时候产品代号“KG-12”被识别成“12”和“KG”两个 token完全没有用。2.3 关系抽取让节点连起来关系抽取是整个项目最难的部分。我早期陷入过一个误区一上来就上复杂模型。后来发现在一个垂直领域里规则和句法模板往往能覆盖很大比例的高价值关系。最基础的模式是“触发词 句法位置”。比如句子“OpenAI released GPT-4”触发词是“released”它的主语是发布者宾语是发布物于是得到三元组(OpenAI, release, GPT-4)。同样的方式可以扩展到“发布”“推出”“包含”“导致”“提升”等动词。这一步不要求理解整段文章只要找到动词前后的核心名词短语即可。把规则定义好之后再用依存句法分析去定位主语和宾语的位置比单纯用空格切词靠谱得多。2.4 图谱存储与可视化让结构可被消费实体和关系都抽出来之后需要入图。小规模实验我用 NetworkX直接在内存里用 dict of list 存储方便算度、连通分量、社区发现。当数据量到了几十万条三元组我会导入 Neo4j因为 Cypher 查询邻接关系非常方便也能直接对接可视化前端。为了不把业务逻辑绑死在具体图数据库上我在项目里先定义了一个统一的三元组 JSON 格式类似{head: ..., relation: ..., tail: ...}后面再按需转成 NetworkX 的边或 Neo4j 的 relationship。这一步看似多了一层但在后面换存储、做图融合时省了很多事。3. 核心实现细节与代码拆解这一节给出两个关键实现混合实体识别器以及基于依存句法的关系抽取器。代码是简化版但思路和线上版本一致。3.1 实体识别器的实现思路我用 spaCy 作为基础框架用 EntityRuler 把领域词典注入到 pipeline 里。这样既能保留通用模型对“人、组织、地点”的识别能力又能保证“KnowledgeGraph_Builder”这类领域词不会被切碎。import spacy from spacy.pipeline import EntityRuler nlp spacy.load(en_core_web_sm) # 自定义领域实体 patterns [ {label: PROD, pattern: [{LOWER: knowledge}, {LOWER: graph}, {LOWER: builder}]}, {label: PROD, pattern: [{LOWER: neo4j}]}, {label: GPE, pattern: [{LOWER: san}, {LOWER: francisco}]}, ] ruler EntityRuler(nlp) ruler.add_patterns(patterns) nlp.add_pipe(entity_ruler, namedomain_ruler, beforener) text KnowledgeGraph_Builder is built on top of spaCy and can extract entities from Neo4j docs. doc nlp(text) for ent in doc.ents: print(ent.text, ent.label_)这段代码里有个细节值得说beforener不是随便写的。EntityRuler 如果放在 NER 之前会先标记领域词后面模型再做通用实体识别避免模型把已经合并好的专名拆开。如果放在 NER 之后那么“KnowledgeGraph_Builder”很可能已经被模型切成了两个或三个词再用 ruler 匹配就不容易了。在实际项目中词典不是手工敲进代码的而是从语料库自动统计加人工审核生成的。我会先用高频名词短语统计跑一遍抽出 top 500 词再过滤掉停用词和通用词最后把保留下来的词作为词典初稿。这份词典配合正则能解决大部分产品名和内部代号问题。3.2 基于依存句法的关系抽取实体识别得到节点之后关系抽取要解决边。我给出的方案是遍历句子的依存树找触发词再看触发词的主语和宾语。def extract_triples(doc): triples [] trigger_words {release, publish, include, cause, improve} for token in doc: lemma token.lemma_.lower() if lemma not in trigger_words: continue subject None object_ None for child in token.children: if child.dep_ in (nsubj, nsubjpass): subject child.text if child.dep_ in (dobj, attr, acomp): object_ child.text if subject and object_: # 简单清洗去掉空白和引号 triples.append((subject.strip().lower(), lemma, object_.strip().lower())) return triples # 示例 text OpenAI released GPT-4 in March 2023. doc nlp(text) print(extract_triples(doc)) # 预期输出: [(openai, release, gpt-4)]这里为什么不直接看token.text因为动词可能有不同时态比如 “released”“releases”“releasing”统一用lemma_做归一化图谱里就不会出现三种不同的“发布”边。对中文场景可以用 stanza 或者 HanLP 的依存句法原理是类似的。不过规则抽取的覆盖度有限。如果触发词不在列表里关系就抽不到。我的做法是给触发器加一层“泛化”某些后缀近义的词自动归并比如“发布”“推出”“上线”都映射到publish“提升”“增长”“增加”映射到increase。这样图里的关系类型不会爆炸后面的查询和统计才可控。4. 避坑指南我踩过的5个坑这部分是整篇文章里我最想分享的内容。因为这些坑在官方文档里几乎不会写但只要跑一遍真实数据就一定会遇到。4.1 同一实体有多种写法“Neo4j”“neo4j”“Neo4j Inc.”“Neo4j 图数据库”在原始文本里可能分别出现如果不做实体归一化图中就会出现多个其实指向同一现实对象的节点查询结果就会散掉。我后来加了一个实体归一化层先做大小写折叠再去掉常见后缀词比如“公司”“科技”“Inc.”“Corp.”然后用字符相似度做模糊匹配相似度超过 0.85 就自动合并。这个阈值是从实验结果里选的太低了会误合并“OpenAI”和“Open AI”太高了又合不了“KG_Builder”和“KG Builder”。4.2 关系方向和重复边关系方向是新手最容易忽略的问题。依存句法里主语自然成为关系的起点宾语是终点。但换一个被动句方向就反了。比如“GPT-4 was released by OpenAI”依存关系会显示 GPT-4 是 nsubj如果不做处理抽出来的三元组就成了(GPT-4, release, OpenAI)完全反了。我的解决办法是对被动结构做专门判断检测nsubjpass和auxpass如果出现就交换头尾实体。同时入库前去重同一对实体之间只保留最频繁的关系方向避免图中出现一对双向冗余边。4.3 数字、版本号被切碎领域文本中有大量类似“GPT-4”“iOS 17.2”“8GB”这种表示。预训练模型经常把版本号从主体上拆开比如“GPT”和“4”甚至“17”和“.2”会被当成两个 token。解决思路是正则预匹配在进入实体识别之前先把常见版本号、芯片型号、规格参数用正则标注成临时占位符比如把“iOS 17.2”整体替换为一个特殊标记__VER_1__等关系抽取完成后再替换回来。这个方法简单粗暴但能显著提升后续实体和关系抽取的准确性。4.4 图谱太稀疏连不起来我第一次跑完一套文档后图谱里出现了大量孤立节点。统计发现很多实体只在句子中出现一次没有和任何其他实体共现。这样的节点不仅增加存储还会让图算法变得很慢。我的做法是加一个“最小支持度”过滤实体至少在语料中出现两次以上才允许作为节点进入最终图谱关系至少出现过一次且两端节点都保留才作为边。虽然会丢掉一些低频信息但换来的是图谱密度和算法稳定性。对大多数业务问答场景来说低频知识靠检索去补更合适不适合塞进图里。4.5 处理速度慢规则和句法分析都比较慢尤其是跑大语料时spaCy 的完整 pipeline 在 CPU 上每分钟大概只能处理几千个词。实测下来瓶颈主要在依存句法分析而不是实体识别。优化手段有三层先用正则快速过滤不含触发词的句子减少进入依存分析的文本量再用 spaCy 的disable参数停用不用的组件比如disable[ner]在只做关系抽取时可以提速最后把分句、实体识别和关系抽取拆成三个独立任务用多进程并行处理。这样一套组合拳下来处理速度大约能提升 2 到 3 倍。5. 进阶让知识图更好用跑通基础 pipeline 之后我还在项目里做了两个方向上的扩展个人认为很值得。5.1 用 LLM 做开放关系抽取规则和依存句法能覆盖“发布”“包含”这类固定动词关系但处理不了“虽然 A 比 B 便宜但 B 续航更好”这种比较关系。这时候可以引入 LLM 做开放关系抽取。我的做法是把句子和已经识别出的实体列表一起发给 LLM让模型输出候选三元组返回 JSON 格式然后和规则抽取的结果做投票融合。只有规则和 LLM 都输出的三元组或者 LLM 输出且经过人工抽检的三元组才会进入最终图谱。这样做的好处是保留规则的可解释性同时利用 LLM 的语义泛化能力。需要注意控制成本通常我只会对规则抽不到的高价值句子调 LLM。5.2 图谱融合从单文档到多文档单文档抽取只是第一步真正落地时要处理多文档来源同一实体在不同文档里可能有不同表述甚至相互矛盾的信息。我在图谱融合时采用“先合并节点再合并边”的顺序先用实体归一化层把同义实体合并成一个 canonical 节点然后对不同来源的同一条关系做频次统计只有出现多次的关系才保留。如果两个文档对同一关系给出不同属性值比如“电池容量 4000mAh”和“电池容量 4200mAh”我会额外增加一个 evidence 列表把原始句子挂在边属性上查询时能直接看证据来源。这样图谱就不是一个冷冰冰的抽象结构而是可以追溯到原文的“知识索引”。最后再分享一点体会KnowledgeGraph_Builder 看起来是个小工具但真正做下来它逼着我把自然语言问题重新拆成了“实体、关系、属性、证据”四件事。别指望一个模型解决所有抽取问题一定要在自己的数据上反复调词典、补规则、看失败案例。图谱的质量不是靠模型刷分刷出来的而是靠一遍遍修正边界情况磨出来的。本文还有配套的精品资源点击获取