知识图谱(Knowledge Graph, KG) 源于自然语言理解,其目标是用一种结构化的方式,来描述现实世界中的实体及其相互关系。它主要由两个核心要素构成:
- 节点(Nodes):代表现实世界中的“实体”(Entities),例如一个人、一部电影、一家公司或一个具体概念。
- 边(Edges):代表实体与实体之间的“关系”(Relations)。
这些元素共同构成了一个庞大的语义网络,其基本结构可以表示为 (实体)- [关系] -> (实体) 的三元组(Triples)。例如,“饺子”和“哪吒2”是两个实体,“导演”就是它们之间的关系,构成一个知识三元组:(饺子)- [导演] -> (哪吒2)。
知识图谱并非一个孤立的学术概念,它在工业界有着广泛且深入的应用,尤其是在需要深度结合领域知识的场景中。
俗话说“近朱者赤,近墨者黑”,在许多领域,一个实体的风险或属性,往往与其关联的其他实体有很强的相关性。知识图谱正是挖掘这种关联性的利器。
- 犯罪网络侦查:公安部门可以利用通话记录、社交关系、转账流水等信息构建犯罪嫌疑人网络。在这个网络中,如果某个节点与多个已知的犯罪分子有直接或间接的联系,那么他参与犯罪的可能性就大大增加。通过分析网络中的核心人物(连接数最多的节点)和资金流向,可以有效地打击整个犯罪团伙。
- 信用卡反欺诈:银行可以将申请人的信息(如电话、地址、公司)构建成一个庞大的关系网络。通过分析这张网络,可以识别出“欺诈团伙”——例如,多个申请人共享同一个联系电话或家庭住址,或者与已知的欺诈分子有紧密的社交关系。
- 工业设备运维:将设备的各种“故障现象”、“故障原因”、“解决方案”和“所需零件”构建成知识图谱。当设备出现问题时,系统可以根据上报的现象,在图谱中进行推理,快速定位可能的原因,并给出维修建议,甚至可以提示维修人员需要携带哪些工具和备件,从而提高维修效率。
- 医疗辅助诊断:医疗领域知识繁杂,可以通过构建“病症”、“疾病”、“检查项目”、“治疗方案”、“药品”之间的关系图谱。医生输入患者的症状后,系统可以辅助推荐需要进行的检查,并根据检查结果在图谱中推理,给出可能的诊断建议和治疗方案,帮助实现规范化诊疗。
对于通用领域的开放式聊天,大语言模型(LLM)已展现出强大的能力。但在许多垂直领域,基于知识图谱的问答系统(KBQA)因其答案的准确性和可解释性,仍然具有不可替代的价值。其工作流程通常如下:
- 意图识别:首先判断用户提问的意图。例如,“我想买一张明天上午的故宫门票”这个问题的意图是“票务预订”。
- 槽位填充 (实体抽取):从问题中抽取出关键信息,即“实体”。例如:
景点: 故宫,时间: 明天上午,数量: 一张。 - 知识查询:利用抽取出的实体,在知识图谱(或数据库)中进行精确查询。
- 回复生成:将查询到的结果,通过预设的模板生成自然语言回复。
这种方式虽然不如 LLM 灵活,但在机票预订、酒店查询、银行客服等业务逻辑明确的场景中,能够提供更加可靠和可控的服务。
如何从海量的、非结构化的文本(如新闻、财报、医疗记录)中,自动地构建出结构化的知识图谱,是整个技术流程的核心挑战。
传统的知识图谱构建过程主要依赖于两项关键的 NLP 技术:
- 命名实体识别 (Named Entity Recognition, NER):从文本中识别并抽取出特定类别的实体。例如,在“英伟达发布了专为 AI 设计的 Blackwell 芯片”这句话中,识别出“英伟达”(公司)、“Blackwell”(产品)。这些被抽取的实体将成为知识图谱中的 节点。
- 关系抽取 (Relation Extraction, RE):在识别出实体的基础上,进一步判断实体与实体之间存在何种语义关系。在上面的例子中,模型需要判断“英伟达”和“Blackwell”之间的关系是“发布”。这个关系将成为连接两个节点的 边。
通过对大量文本进行这两步处理,我们就能源源不断地抽取出知识三元组,最终汇聚成一个庞大的知识图谱。
随着大语言模型的兴起,传统的 NLP 任务流程正在被重塑。LLM 同样具备强大的实体识别和关系抽取能力,但这并不意味着对传统流程的简单替代,而是呈现出深度融合的趋势。
- 局限性与挑战:完全依赖 LLM 会面临成本高昂、数据隐私(使用闭源 API 时)、以及“幻觉”问题,即模型可能会编造事实。
- 融合方案:为了结合知识图谱的准确性和大模型的推理能力,微软提出了 GraphRAG。原理是将知识图谱作为一个可靠、可随时更新的 外部知识库,并基于图结构进行“子图检索”(如社区发现、路径搜索等),而非检索孤立事实。当用户提问时:
- 利用模型从问题中识别出核心实体与约束。
- 在图中检索与之高度相关的子图(社区/路径/邻域),获得准确且可解释的事实与关系。
- 将该子图的结构化信息作为上下文,连同原始问题一起输入给大语言模型,生成基于证据的答案。
知识图谱需要专门的数据库进行存储和查询,这类数据库被称为 图数据库 (Graph Database)。其中,与传统的关系型数据库(如 MySQL)相比,图数据库的优势在于其对“关系”的查询性能。对于需要进行多层关系遍历的复杂查询(例如,查询“我朋友的朋友”),图数据库的效果远超关系型数据库。而 Neo4j 就是目前比较流行的一款开源图数据库。
Neo4j 的数据模型主要包含以下几个概念:
-
节点 (Node):节点是图中的基本数据单元,用于表示现实世界中的实体,例如一个人、一家公司、一本书或一个账户。在关系型数据库中,节点可以类比为表中的一行。
-
标签 (Label):用于为节点分类或打上“类型”标记。一个节点可以拥有一个或多个标签。例如,一个节点可以同时拥有
:Person和:Author两个标签,表示这个人既是一个普通人,也是一位作者。 -
关系 (Relationship):这是图数据库的精髓所在,它以一种富有表现力的方式连接两个节点,并明确地定义了它们之间的联系。每个关系都具有以下特点:
- 有方向:关系总是从一个“起始节点”指向一个“结束节点”。
- 有类型:每个关系都必须有一个类型(例如
:FRIENDS_WITH,:PURCHASED),用来描述连接的性质。 - 可以拥有属性:和节点一样,关系也可以存储属性,例如,一个
:PURCHASED关系可以有一个date属性来记录购买日期。
-
属性 (Property):属性是以键值对(Key-Value)形式存储在节点和关系上的详细信息。键是字符串,值可以是各种基本数据类型(如字符串、数字、布尔值)或它们的数组。
这四个概念共同构成了一个灵活而强大的数据模型。
graph LR
A["Alice:Person {name: 'Alice'}"]
B["Bob:Person {name: 'Bob'}"]
A -- "KNOWS {since: 2020}" --> B
在上图中,
Alice和Bob是 节点,:Person是 标签,{name: 'Alice'}是 属性,KNOWS则是连接它们的 关系 类型,而{since: 2020}是这段关系上的 属性。
Cypher 是 Neo4j 的声明式图形查询语言,它的语法灵感来源于 SQL,但针对图的特性进行了优化。通过 Cypher,我们可以用一种直观且高效的方式来查询和操作图数据。
例如,要查找在电影《黑客帝国》(The Matrix) 中出演过的所有演员,可以使用以下查询:
MATCH (actor:Person)-[:ACTED_IN]->(movie:Movie {title: 'The Matrix'})
RETURN actor.name官方的 Cypher 语法速查表(在线版本)汇总了常用的命令、操作符和语法结构,可供读者快速查阅。
对于初学者和开发者,推荐以下两种主流的安装方式。
-
Neo4j Desktop (推荐用于本地学习)
- 安装:
- 访问 Neo4j 官网,在 “Neo4j for Desktop” 板块点击 “Download” 按钮。
- 页面会跳转至一个注册表单。可以填写任意信息,然后点击 “Download Desktop” 按钮,浏览器将自动开始下载安装包。
- 下载完成后,双击安装文件,程序会自动进行安装。
- 安装完成后首次启动,会看到许可协议界面,点击 “Continue” 即可完成最后的设置。
- 安装:
-
Docker (推荐用于服务器部署与跨平台开发)
- 安装: 只需一行命令即可完成拉取镜像和启动容器。
docker run \ --name my-neo4j \ -p 7474:7474 -p 7687:7687 \ -d \ -v $HOME/neo4j/data:/data \ -v $HOME/neo4j/logs:/logs \ --env NEO4J_AUTH=neo4j/password \ neo4j:latest - 参数说明:
-p 7474:7474: 将容器的 HTTP 端口映射到本机,用于浏览器访问。-p 7687:7687: 将容器的 Bolt 驱动端口映射到本机,用于代码连接。-v $HOME/neo4j/data:/data: 将数据目录挂载到本机,确保数据持久化。--env NEO4J_AUTH=neo4j/password: 设置数据库的初始用户名和密码(此处为neo4j/password)。neo4j:latest: 使用最新的官方镜像。
- 安装: 只需一行命令即可完成拉取镜像和启动容器。
安装好 Neo4j 后。我们就可以学习一些 Neo4j 的基本用法了。
在使用 Neo4j 进行开发时,首先需要在 Neo4j Desktop 中创建一个本地数据库实例(Instance)。这个过程非常直观。
-
创建实例:打开 Neo4j Desktop,在 “Local instances” 页面点击 “Create instance” 按钮。
-
配置实例:在弹出的窗口中,为实例命名(例如
base nlp),选择所需的 Neo4j 版本,并为默认用户neo4j设置一个能记住的密码。完成后点击 “Create”。 -
启动与连接:实例创建后会自动启动,状态显示为 “RUNNING”。此时,可以通过浏览器直接访问
http://127.0.0.1:7474来打开 Neo4j Browser。在浏览器打开的连接界面中,使用刚刚设置的密码进行连接。
数据库操作的核心无外乎增删查改(CRUD),下面来使用 Cypher,围绕一个菜品信息图谱的场景,逐一介绍这些基本操作。
为了方便演示,先设定好本次实践所需要用到的实体、属性和关系。
- 实体/标签 (Labels):
Ingredient: 食材,拥有name,category(类别),origin(产地),tags(标签,数组)等属性。Dish: 菜品,拥有name,cuisine(菜系)等属性。
- 关系 (Relationships):
(Dish)-[:包含]->(Ingredient): 表示某菜品包含某种食材,关系上可以有用量属性。(Dish)-[:主要食材]->(Ingredient): 表示某菜品的主要食材是某种食材。(Dish)-[:调味]->(Ingredient): 表示某菜品使用某种食材进行调味。
CREATE 语句用于在图中创建新的节点和关系。
创建节点的基本语法是 CREATE (变量:标签 {属性: 值})。
- 变量 (Variable): 如
pork,是一个临时名称,用于在同一条语句中引用该节点。如果后续不需要引用,可以省略。 - 标签 (Label): 如
Ingredient,用于对节点进行分类。 - 属性 (Properties): 一个包含键值对的 map/字典,用于描述节点的具体信息。
最基础的创建语句包含一个临时变量(pork)、一个标签(Ingredient)和一组属性。
CREATE (pork:Ingredient {name:'猪肉', category:'肉类', origin:'杭州'});如果在创建后不需要立刻使用这个节点(例如,在同一查询中创建关系),可以省略临时变量名,这样语法更简洁。
CREATE (:Ingredient {name:'土豆', category:'蔬菜', origin:'北京'});还可以在创建节点后,使用 RETURN 子句立即将其返回。这对于调试或确认节点是否按预期创建非常有用。RETURN n 会在结果面板中直接显示刚刚创建的 鸡蛋 节点的信息。
CREATE (n:Ingredient {name:'鸡蛋'}) RETURN n;执行上述三条命令后,数据库中就创建了三个 Ingredient 类型的节点。能够通过 Neo4j Browser 的可视化界面直观地看到这些新创建的数据。
点击左侧面板中的 Ingredient 标签,Neo4j Browser 会自动执行 MATCH (n:Ingredient) RETURN n LIMIT 25; 查询,并在主窗口中展示所有食材节点。如图 2.6 所示,点击其中一个节点(如“土豆”),右侧会显示其详细属性。这里可以观察到:
<id>字段:这是 Neo4j 为每个节点自动生成的内部唯一标识符。- Key-Value 结构:右侧的 “Key” 和 “Value” 两列展示了节点属性是以键值对的形式存储的。
- 自定义属性:
name、category、origin三个字段的值与前面CREATE语句中设定的值完全一致。
关系的创建通常需要先指定关系两端的节点,然后用 -[变量:类型 {属性}]-> 来定义关系。
- 关系必须有 方向 和 类型 (Type)。
- 小括号
()用于表示节点,中括号[]用于表示关系。
在实际应用中,常常需要一次性创建多个节点以及它们之间的关系。CREATE 语句支持通过逗号分隔,在一个查询中完成复杂图谱的构建。下面的例子将创建一个更复杂的菜品关系网络,以体现“多对多”的特性(一道菜包含多种食材,一种食材可用于多道菜)。
CREATE
// 创建食材节点
(rousi:Ingredient {name:'猪里脊'}),
(muer:Ingredient {name:'木耳'}),
(huluobo:Ingredient {name:'胡萝卜'}),
(qingjiao:Ingredient {name:'青椒'}),
// 创建菜品节点
(d1:Dish {name:'鱼香肉丝', cuisine:'川菜'}),
(d2:Dish {name:'木须肉', cuisine:'鲁菜'}),
// 创建关系
(d1)-[:包含 {amount:'250g'}]->(rousi), (d1)-[:包含]->(muer), (d1)-[:包含]->(huluobo),
(d2)-[:包含 {amount:'150g'}]->(rousi), (d2)-[:包含]->(muer),
// 创建双向关系
(rousi)-[:被用于]->(d1), (muer)-[:被用于]->(d1), (huluobo)-[:被用于]->(d1),
(rousi)-[:被用于]->(d2), (muer)-[:被用于]->(d2);这个查询语句做了以下几件事:
- 创建了 4 个
Ingredient节点:猪里脊、木耳、胡萝卜、青椒。 - 创建了 2 个
Dish节点:鱼香肉丝、木须肉。 - 创建了 5 条
包含关系:从菜品指向食材。 - 创建了 5 条
被用于关系:从食材指向菜品。这样既可以方便地查询“一道菜包含哪些食材”,也可以高效地反向查询“一种食材被用在了哪些菜里”。
执行 MATCH p=()-[:包含]->() RETURN p LIMIT 25; 查询可以可视化展示所有“包含”关系。点击关系(箭头),可以在右侧看到其详细信息,例如“鱼香肉丝”到“猪里脊”的关系上,就包含了在 CREATE 语句中定义的 amount: '250g' 这一属性。
MATCH 是 Cypher 中用于查询图数据的命令,它允许你描述你想要寻找的节点和关系的模式。
最简单的查询是匹配并返回图中的任意节点,可以使用 LIMIT 关键字限制返回数量,避免因数据量过大导致浏览器卡顿。
// 匹配并返回图中的任意 25 个节点
MATCH (n)
RETURN n
LIMIT 25;也可以根据标签和属性进行精确匹配。
// 匹配所有标签为 Ingredient,且名字为'猪里脊'的节点
MATCH (n:Ingredient {name:'猪里脊'}) RETURN n;WHERE 子句提供了更灵活的过滤能力,可以对节点的属性进行复杂的逻辑判断。
例如,查询名字是'猪里脊'或'鸡蛋'的 Ingredient 节点。
MATCH (n:Ingredient)
WHERE n.name IN ['猪里脊','鸡蛋']
RETURN n;也可以使用 AND、OR 等关键字构建复合查询条件。
// 复合条件:查询指定名称且类别为“肉类”的节点
MATCH (n:Ingredient)
WHERE n.name IN ['猪肉', '猪里脊', '鸡蛋'] AND n.category = '肉类'
RETURN n;默认情况下,RETURN n 会返回整个节点对象。也可以只返回节点的特定属性,并使用 AS 为返回的列起别名,使结果更具可读性。
MATCH (n:Ingredient)
WHERE n.name IN ['猪里脊','鸡蛋']
RETURN n.name AS 食材名称, n.category AS 类别;图数据库最强大的地方在于对关系的查询。例如,可以一次性查询“鱼香肉丝”和“木须肉”分别包含了哪些食材。
MATCH (d:Dish)-[:包含]->(i:Ingredient)
WHERE d.name IN ['鱼香肉丝', '木须肉']
RETURN d.name AS 菜品, collect(i.name) AS 食材列表;
collect()是一个聚合函数,可以将匹配到的多个同类结果(这里是食材名称i.name)收集到一个列表中。
在实际应用中,一个常见的操作是先找到图中已经存在的节点,然后为它们添加新的关系。这可以通过组合使用 MATCH 和 CREATE 来实现。
例如,我们已经创建了“鱼香肉丝”和“猪里脊”,现在想为它们添加一条“主要食材”的关系。
MATCH
(d:Dish {name:'鱼香肉丝'}),
(i:Ingredient {name:'猪里脊'})
MERGE
(d)-[r:主要食材]->(i)
RETURN d, i, r;这个模式确保了是在已有的、正确的实体之间建立关联,并通过
MERGE避免重复的关系。
可以使用 ORDER BY 子句对返回的结果进行排序。默认是升序 (ASC),也可以指定为降序 (DESC)。
// 查询所有食材,并按名称升序排序
MATCH (i:Ingredient)
RETURN i.name, i.category
ORDER BY i.name ASC;SET 语句用于修改或添加节点/关系的属性。它必须和 MATCH 配合使用,先找到要更新的实体,再进行修改。
MATCH (i:Ingredient {name:'猪肉'})
SET
i.is_frozen = true,
i.origin = '金华'
RETURN i;在构建知识图谱时,经常遇到这样的场景:如果某个节点已存在,则更新其属性;如果不存在,则创建它。MERGE 语句就可以解决这个问题。
MERGE 会根据你提供的模式在图中查找,如果找到匹配项,则执行 ON MATCH 部分;如果未找到,则执行 ON CREATE 部分,从而避免了重复创建实体。
// 查找名为'大蒜'的 Ingredient 节点
MERGE (n:Ingredient {name: '大蒜'})
// 如果不存在,则创建该节点,并设置创建时间和初始库存
ON CREATE SET
n.created = timestamp(),
n.stock = 100
// 如果已存在,则更新其库存、访问次数和访问时间
ON MATCH SET
n.stock = coalesce(n.stock, 0) - 1,
n.counter = coalesce(n.counter, 0) + 1,
n.accessTime = timestamp()
RETURN n;
coalesce(property, defaultValue)是一个非常有用的函数,它会检查属性property是否存在,如果存在则返回其值,否则返回defaultValue。
REMOVE 用于移除节点或关系上的某个属性。在下面的例子中,先用 MATCH 找到名为“大蒜”的节点,然后移除由 MERGE 命令在创建它时添加的 created 属性。
MATCH (i:Ingredient {name:'大蒜'})
REMOVE i.created
RETURN i;DELETE 用于删除节点和关系。但需要 特别注意:Neo4j 不允许直接删除一个还存在关联关系的节点。你必须先删除关系,才能删除节点。
// 错误示范:如果'大蒜'还有关系连着,这条语句会报错
MATCH (i:Ingredient {name:'大蒜'})
DELETE i;正确的做法有两种。第一种是先手动删除与节点相关的所有关系,然后再删除节点本身。
// 正确做法 1:先删除关系,再删除节点
MATCH (i:Ingredient {name:'大蒜'})-[r]-() // 匹配与'大蒜'相连的任意关系
DELETE r, i; // 先删除关系 r,再删除节点 i第二种做法更简洁,也是官方推荐的方式:使用 DETACH DELETE。它会自动删除指定节点以及所有与它直接相连的关系。
// 正确做法 2:使用 DETACH DELETE (推荐)
MATCH (i:Ingredient {name:'大蒜'})
DETACH DELETE i;此外,还可以通过节点的内部 ID 进行精确查找和删除。每个节点都有一个由 Neo4j 自动分配的唯一 ID,可以通过 id() 函数获取。
// 假设我们通过查询得知“大蒜”的 ID 为 5
MATCH (i:Ingredient)
WHERE id(i) = 5
DETACH DELETE i;如果想删除数据库中的所有节点和关系,可以使用以下命令:
// 匹配所有节点 n
MATCH (n)
// 强制删除节点 n 及其所有关系
DETACH DELETE n;在生产环境中,直接从数据库中物理删除(DELETE)数据是一种高风险操作。一种更安全、更常见的做法是“软删除”。软删除并非真的将数据移除,而是通过 SET 命令为其添加一个状态属性,将其标记为“已删除”或“不活跃”。
// 将“木耳”标记为不活跃
MATCH (i:Ingredient {name:'木耳'})
SET i.is_active = false;这样,在后续的查询中,只需要增加一个 WHERE i.is_active = true 的过滤条件,就能只使用那些“活跃”的数据,而被软删除的数据依然保留在数据库中,以备审计或恢复。
删除操作是高风险行为,尤其是在生产环境中。执行前请务必确认操作对象和范围,并做好数据备份。










