DLAI-智能体驱动的知识图谱笔记-全-
DLAI 智能体驱动的知识图谱笔记(全)
001:课程介绍 🎯

在本节课中,我们将学习如何构建一个由多智能体系统驱动的知识图谱,该系统能够将您的结构化和非结构化数据转化为一个强大的知识网络。


欢迎来到与 Neo4j 合作推出的生成式知识图谱构建课程。在本课程中,您将设计一个多智能体系统,将您的结构化和非结构化数据转化为知识图谱。我发现知识图谱在那些信息存储和检索的准确性至关重要的高风险应用中非常有用。本课程将指导您,并为您提供一套构建这些知识图谱的强大工具。我很高兴本课程的讲师是 Andres Colliger,他是 Neo4j 的生成式人工智能开发者布道师。

谢谢,Andrew。我很高兴回到这里,并与您合作这门课程。简单来说,一个知识图谱系统会将您的文本文档分割成块,并将它们存储在向量数据库中。但除此之外,它还会将这些块放入图谱结构中,这些块随后会从中提取实体。例如,来自产品评论的一个文本块可能包含诸如产品、订单、交付问题或产品缺陷等实体。为了构建知识图谱,您需要从这些块中提取相关实体,然后通过边在图中连接这些块和实体。在这里,每条边代表一种关系,例如,它可能代表这个块提到了某个特定产品,或者该产品存在某个问题。实体将与相关的块一起被检索,为大型语言模型提供更相关的上下文,以生成更精确和准确的答案。您还可以将这种类型的图谱连接到另一个图谱,该图谱包含从结构化数据(如 CSV 文件)中提取的附加信息。
在本课程中,Andres 将引导您了解如何构建一个多智能体系统,帮助您完成构建此类知识图谱的所有工作。要将您的结构化和非结构化数据转化为知识图谱,您首先需要确定图谱模式,这意味着您可以从数据中提取哪些类型的实体或节点,以及它们之间存在哪些关系。一旦定义了模式,您就可以构建实际的图谱并将其存储在图形数据库中。您将不再主要依靠人工查看数据来寻找图谱模式,而是设计一个使用 Google 的 Agent Development Kit 的多智能体系统来完成这项工作。
在您学习了 ADK 的基本语法之后,您将一次设计一个智能体来构建您的系统。第一个智能体将与您对话,以提取您想要构建的图谱的目标和类型。根据该目标,一组智能体将专门从您的结构化数据中提取实体和关系。另一组智能体将处理您的非结构化数据。最后,最后一组智能体将连接这两个模型,并相应地构建图谱。
许多人共同努力创建了这门课程。我要感谢来自 Neo4j 的 Martin O‘hanlon 和 Adam Kowli,来自 DeepLearning.AI 的 Christopher Pooccostro 和 Harra Salami 也为本课程做出了贡献。在第一课中,您将了解更多关于知识图谱底层结构的知识,以及如何构建一个知识图谱并利用它来识别产品问题的根本原因。

听起来很棒。让我们开始吧。
本节课总结

在本节课中,我们一起学习了知识图谱的基本概念及其在高精度应用中的价值。课程介绍了如何利用多智能体系统,自动从结构化和非结构化数据中提取实体与关系,并构建成一个互联的知识网络。我们了解到,整个过程始于定义图谱模式,并通过专门的智能体分工协作来完成数据提取、处理和最终的图谱构建。下一课,我们将深入探讨知识图谱的具体结构及其在问题根因分析中的应用。
002:什么是知识图谱 📚
在本节课中,我们将学习知识图谱的含义,以及它如何帮助表示和检索数据中的关系。随后,我们将探索用于构建知识图谱的数据集。

概述
知识图谱是一种数据库,它以节点(代表实体,如人、产品)和关系(代表实体间的连接)的形式来表示信息。与传统的关联表不同,关系在知识图谱中是“一等公民”,它们本身就是具有语义含义的数据记录。这种结构使得通过模式匹配进行查询变得非常直观,尤其适合与擅长自然语言处理的大语言模型结合使用。
从关系型数据库到知识图谱
为了更好地理解知识图谱,我们先回顾一下关系型数据库及其模式。
我们可能都熟悉类似这样的场景:左边有一个表,右边有一个表,中间有一个连接表。在这个具体例子中,中间的表是连接表,它允许“人员”和“产品”之间建立多种连接。

让我们思考一下这两个表以及连接表。如果你提出一个问题:“这个人购买了哪些产品?”你首先需要从人员表开始,选择标识为“ABK”的这个人。然后,你需要通过连接表关联到“人员-产品”表,再连接到产品表。最终,你可以看到ABK购买了椅子、台灯和桌子。

如果你扩展这个问题,问:“还有谁购买了ABK购买过的产品?”你需要从ABK出发,连接到产品,再通过连接表关联回其他购买者,比如“EE”。你会发现EE也购买了一些ABK购买的产品,但EE还额外购买了一件产品。
进一步,我们可以问:“我们应该向ABK推荐购买什么产品?”这本质上是一个推荐查询的基础。思路是:找到与ABK有相似购买模式的其他人群,然后推荐那些人群购买过但ABK尚未购买的产品。这需要多次连接操作,过程会变得有些混乱。
引入图结构
实际上,有一种更清晰的方式来思考和查询这类问题:将其转化为图。

第一步是忽略所有不相关的记录。我们将暂时搁置那些灰色的、与当前查询无关的记录,专注于与“向ABK推荐产品”这个查询相关的数据。
然后,我们去掉中间所有的连接表记录,将它们转化为箭头,直接将ABK与他购买的产品连接起来,对EE也做同样的处理。稍作重新排列后,我们得到ABK在一侧,EE在另一侧,中间是他们共同购买的产品。同时,EE还连接着一个ABK未购买的产品。
现在,整个结构看起来更清晰,更容易理解发生了什么:ABK和EE都连接到中间的产品,但EE还额外连接着一个ABK未购买的产品。
使用Cypher进行模式匹配查询
我们可以通过将其重新表述为模式匹配来开始查询。使用一种名为Cypher的查询语言,它有点像具有模式匹配能力的SQL。
以下是描述我们想要查找的数据记录的模式:
- 匹配节点:我们匹配一个标签为“Person”、且属性“name”的值为“ABK”的节点。在图中,节点用圆括号表示。
- 匹配关系:然后,我们匹配一个类型为“PURCHASED”的关系(用箭头
-[:PURCHASED]->表示),从ABK指向一些产品节点。 - 返回结果:我们可以直接返回这些产品。
我们可以扩展这个模式,以包含购买了这些产品的其他人。既然我们已经从ABK匹配到了他购买的产品,我们也可以匹配指向这些产品的“PURCHASED”关系,从而找到其他购买者。
为了回答推荐问题,我们可以进一步扩展模式。我们匹配从ABK到某些产品,再到其他购买者,然后扩展到这些购买者购买的其他产品。关键是我们需要添加一个谓词,要求一个负向模式成立:即ABK没有购买过那些“其他产品”。用Cypher表示就是 WHERE NOT (abk)-[:PURCHASED]->(otherProduct)。
这样,我们就描述了一个既包含应存在的记录(ABK购买产品、其他人购买相同产品、其他人购买其他产品),也包含不应存在的记录(ABK购买其他产品)的模式。最终,“其他产品”集合将缩小到仅包含ABK未购买的产品,这些就是我们将要推荐给他购买的产品。
知识图谱的核心特性
那么,究竟什么是知识图谱?

它是一种将信息表示为节点(代表事物,如人、产品、博客等)和关系的数据库。关系不仅仅是使用连接表的约定,而是数据库中的“一等公民”,它们本身就是具有语义含义的数据记录,代表了两个节点如何连接,并增加了关于这两个节点的信息。
以下是核心概念:
- 节点和关系都拥有键值属性。
- 节点可以有多个标签。
- 关系总是有方向的,并且只有一个类型。
- 用于模式匹配的查询语言叫做 Cypher。
事实证明,这种结构非常便于映射到自然语言,也特别适合与生成式AI和大语言模型协同工作,因为大语言模型非常擅长处理自然语言。
回顾一下顶部的Cypher查询,你可以大声读出来并理解其含义:“匹配ABK,他是一个名为ABK的人,他购买了一些产品,这些产品也被其他人购买过,这些人还购买了一些ABK没有购买过的其他产品。”
结合结构化与非结构化数据
知识图谱另一个有趣的特点是,它非常便于将非结构化数据与结构化数据结合起来。
除了已有的结构化数据(如产品、人员),你还可以添加任何分块的文本,为其创建向量表示,并将其存储在数据库中。这样,你就可以将向量相似性搜索和模式匹配结合起来,实现各种强大的访问模式。
例如,假设你想进行根因分析。你是一家家具制造商,希望了解客户对产品的投诉。你要求数据分析团队找出哪些产品问题最多,问题的具体部分是什么,以及是否是部件本身的问题。
作为数据工程师,接到这个任务后,你会先问一些澄清性问题:我们做这个分析的真正目的是什么?有哪些可用数据?
在这个场景中,我们可能有一些CSV文件(物料清单),将产品一直连接到供应商。这些可能来自电子表格或不适合直接分析的关系型数据库。同时,你还有一些从互联网上抓取的用户评论数据。
为了进行分析,你需要构建一个知识图谱来连接所有这些数据。
目标数据模型
目标数据模型如下图所示。左侧米色方框代表可用的CSV文件,它们将转化为图中的节点或关系,这部分我们称之为领域图。

右下角的米色方框代表Markdown文件(用户评论)。这些Markdown文件将被分块,在图中创建一些文档节点。这部分我们称之为词汇图,它代表了原始的文本数据以及连接这些文本数据的结构。

连接这两部分的是中间的主体图。主体图包含我们从文本块中提取出的“主体”或实体。文本块中的内容会谈论产品、用户名等,所有这些都可能成为实体。我们找到这些实体,称之为主体。主体会通过“谓词”连接到“客体”。例如,用户“ABK” “喜爱” “某张桌子”。这就构成了一个“主体-谓词-客体”的三元组。
因为ABK提到了桌子,而桌子恰好对应我们结构化数据中的一个产品,我们就可以将从文本中提取的实体,与从CSV文件中获得的结构化数据连接起来。

总结来说,在高级层面上,你将得到一个包含多个子图的图谱:一个领域图(结构化数据)、一个主体图(从文本提取的实体关系)和一个词汇图(原始文本块及其结构)。
总结



本节课我们一起学习了知识图谱的基本概念。我们了解到,知识图谱通过节点和关系来组织信息,使得表达数据间的连接更加直观。与关系型数据库相比,它的模式匹配查询语言Cypher更接近自然语言。此外,知识图谱的强大之处在于能够无缝整合结构化数据(如CSV表格)和非结构化数据(如用户评论文本),形成领域图、主体图和词汇图相互连接的统一视图,为复杂分析(如推荐和根因分析)提供了强大的基础。在接下来的课程中,我们将动手实践,利用智能体来构建这样的知识图谱。
003:多智能体系统架构 🏗️

在本节课中,我们将学习如何设计一个多智能体系统,该系统将首先确定图谱模型,然后构建知识图谱。
概述
上一节我们明确了知识图谱的构建目标。本节中,我们将深入探讨为实现该目标而设计的多智能体系统的详细架构。我们将了解什么是智能体、多智能体系统的优势,以及本课程中将构建的具体系统流程。
什么是智能体?🤖
对于智能体有很多定义。从工程角度出发,我喜欢将智能体视为一种新颖的控制流操作符。本质上,它是一个循环。在循环内部,会调用一个大语言模型,该模型会决定它想要做什么。然后,基于这个决定,将任务交还给客户端或计算机端,实际上就是执行一个根据决策选择不同操作的 switch 语句。因此,在这个循环中,由于大语言模型的调用,产生了智能行为。但这种智能行为的执行仍然是通过代码完成的。

这种架构的强大之处在于,智能体非常强大,因为你可以利用大语言模型进行推理,并调用工具来执行任何可以用代码完成的任务。同时,它也具有适应性,因为大语言模型被赋予了记忆功能,可以通过对话或保存到记忆中的重要信息来学习已发生的事情,从而决定未来的行动。此外,入门相当容易,因为你主要通过提示词用自然语言描述智能体应该做什么。
然而,这种强大和便捷也带来了一些缺点。使用智能体可能较慢,因为调用远程大语言模型成本较高且速度较慢。它也是非确定性的,因为大语言模型本身就是非确定性的。随着时间的推移,令牌成本可能会累积得非常高。如果你在生产环境中长时间运行一个智能体,进行成千上万次调用,令牌成本会迅速变得非常昂贵。
但转向多智能体系统的一个优势是,你实际上可以缓解这里提到的一些缺点。
什么是多智能体系统?👥

那么,究竟什么是多智能体系统?它当然是一个由多个智能体为单一目标协同工作的系统。智能体通常被组织成一种层次结构,其中有一个顶层智能体管理整体流程,然后可以根据需要设置任意数量的子级智能体,它们负责工作的不同阶段或执行非常具体的任务。
智能体之间通过几种不同的方式交互。存在一个与用户进行的主要对话线程,用户可能通过发送消息来启动工作。然后,每个智能体都可以决定:这项工作是我能做,还是应该由其他人来做?因此,它可以将任务委托给另一个智能体。正如你在图示中看到的,根智能体能够委托给智能体A或智能体B。


在内部,智能体A和智能体B可以使用它们被赋予的工具来完成一些工作。智能体B的一个工具是独特的,即这里的P智能体。这个P函数本身将是一个智能体,但它被当作一个工具来调用。因此,工具有两种交互方式:要么是智能体之间相互委托,要么是一个作为工具运行的智能体被另一个智能体调用。在此过程中,每当发生这些转换时,都有可能与用户在特定的检查点进行交互。
我们将构建的多智能体系统 🛠️

接下来,我们来看看你将构建的多智能体系统具体是什么。你将构建一个知识图谱智能体的部分组件,重点是执行知识图谱构建的子智能体。
你将学习使用 Google 的 ADK 构建智能体的基础知识。你将了解什么是记忆以及如何使用它来记录关键信息。你将定义一些工具。当然,所有这些都将从开放式对话汇聚成可执行代码。
这个精美的图表展示了所有智能体、智能体的流程以及它们之间的交互。让我们逐步了解一下。

顶层:对话引导智能体 💬
在顶层,这个智能体负责管理用户与系统可能性的整体介绍,明确智能体的职责,并帮助用户理解从知识图谱构建到图谱检索的整个可能工作流程。因此,这本质上是一个对话式智能体,它本身不会执行任何实际工作,而是引导用户经历即将发生的不同工作阶段。
中层:工作流管理智能体 📋
下一层的智能体才是真正管理各个工作流程的。主要有三个工作流程。在左侧,我们有两个工作流程:结构化数据智能体和非结构化数据智能体。它们负责引导用户从想法到构建图谱的整个过程。如果它们完成了工作,那么右侧的第三个智能体——图谱RAG智能体,则负责帮助用户实际使用该图谱来回答问题。
但结构化数据智能体和非结构化数据智能体是工作流智能体。它们通过与用户交互并帮助他们完成从想法到描述图谱所需的各个步骤来执行工作,而这是通过委托给专门负责工作流程每个阶段的子智能体来实现的。你会看到这里有一些重复,结构化和非结构化数据智能体共享一些共同的子智能体,我们会在深入下一层时提到它们。
底层:执行具体工作的子智能体 ⚙️
以下是实际为结构化数据执行工作流程的子智能体。让我们逐一了解,同时需要注意的是这些智能体执行工作的输出。如果你把自己想象成一个被赋予实际执行这项任务的数据工程师,这些智能体中的每一个都类似于你自己在做的工作。
以下是各个子智能体的介绍:
-
用户意图智能体:当你被要求执行某项数据分析任务时,你会向提出要求的人提出一些问题,例如:“你能澄清一下你想要我做什么吗?告诉我你的目标是什么,你想让我执行哪种分析?” 事先明确这一点非常重要。这类似于工作的总体需求或方向。因此,这个用户意图智能体虽然是协作和对话式的,但其输出至关重要。我们正在捕获用户的目标以及他们希望从这项工作中获得什么。
-
文件建议智能体:基于用户意图智能体确立的工作方向和目标,该智能体将查看可用的数据文件,并尝试从中找出哪些文件对实际实现该目标有用。可能还有更多可用文件,甚至可能有多个数据源。在这里,我们将其简化为对磁盘上可用文件的建议。该智能体的输出最终是一个经用户批准的建议文件列表。

- 模式提议智能体:这实际上是一对以“批评者”模式形成的智能体。其中一个智能体负责提出模式可能是什么样子的建议,而下一个智能体则充当批评者,指出“也许那样想不太对,可以考虑这样或那样改变”。我们将在后面的课程中查看细节。但这里的核心思想是,这对智能体内部将循环探讨可能性,进行自我批评,其结果应该是一个良好的图谱模式,该模式使用前一个智能体批准的文件,并且符合第一个智能体设定的用户目标,从而产生一个能够回答对用户有用问题的图谱模式。
因此,所有这些工作的输出我们称之为图谱构建计划。它本身不是图谱,而是关于如何构建该图谱的描述。
对于非结构化数据工作流,前两个步骤与结构化数据工作流完全相同。我们从理解用户引入非结构化数据的意图开始,然后是文件建议智能体执行类似行为。但第三步是不同的。
在第三步中,与基于CSV文件设计模式不同,你只有文本。那么,如何从文本中创建图谱呢?这里的方法是,我们将有两个专门的智能体来浏览文本并识别所谓的实体(即文本中出现的人物、地点和事物),并为这些实体识别文本中描述它们的事实。例如,如果文本中有我对本地咖啡店的评论,它可能会揭示“Andrew 非常喜欢 Philz 的咖啡”。这些是可以从文本中提取的事实。这个智能体的目标是找出可以提取哪些类型的事实,而不是进行实际的提取,仅仅是描述可能性。因此,我们将称之为知识提取计划。
知识提取计划与来自结构化数据的图谱构建计划一起,为我们提供了所需的所有规则。利用图谱构建计划和图谱提取计划,右下角红色框中的工具(内部包含多个工具)实际上可以获取这些计划,执行提取和构建工作。它会循环所有构建规则来创建一个领域图谱,循环所有 Markdown 文件,将它们分块,进行向量嵌入,同时提取实体和事实,然后将这些连接到结构化数据上。


课程路线图 🗺️
在课程的第4到第8节,你将经历结构化数据的完整工作流程:从用户意图、文件建议到模式提议。然后,我们将跳到非结构化数据,只学习实体和事实类型提议。最后,在第8课中,我们将查看执行图谱构建本身的特定工具,它完成了所有繁重的工作,而之前的智能体则负责推理工作应该是什么。
总结

本节课中,我们一起学习了多智能体系统的基本概念和架构。我们了解到智能体是一种结合大语言模型推理和代码执行的新型控制流,而多智能体系统通过层次化分工和协作,可以更高效、更专业地完成复杂任务(如知识图谱构建)。我们详细拆解了将要构建的系统,包括顶层的对话引导、中层的工作流管理以及底层执行具体任务的各个子智能体及其输出。下一节课,我们将开始动手编码,使用 Google 的 ADK 构建你的第一个智能体。
004:Google ADK 入门 - 第一部分



在本节课中,我们将学习如何使用 Google 的 Agent Development Kit (ADK) 来构建多智能体系统。ADK 是一个用于开发智能体的框架。我们将从创建一个简单的智能体开始,逐步了解其核心组件和工作原理。
环境与库导入
要构建多智能体系统,你将使用 Google 的 ADK。首先,我们需要导入必要的库。
以下是构建智能体所需的核心库:
import os
from google.genai import Agent
from litellm import completion
from google.genai.extensions.litellm import LiteLLM
from google.genai.types import Content
from google.genai import MemorySessionService
from google.genai import Runner
from typing import List, Optional
这些导入涵盖了智能体定义、与大语言模型交互、内存管理以及运行环境等关键功能。
配置大语言模型
上一节我们介绍了所需的库,本节中我们来看看如何配置智能体将要使用的大语言模型。本课程中我们将使用 OpenAI。
llm = LiteLLM(model="gpt-4")
为了测试模型连接是否正常,我们可以发送一个简单的消息。
messages = [{"role": "user", "content": "Are you ready?"}]
response = completion(model="gpt-4", messages=messages)
print(response.choices[0].message.content)
如果一切正常,我们将收到来自 OpenAI 的响应,例如:“是的,我准备好了。今天有什么可以帮您的吗?” 这表明 OpenAI 已准备就绪。
集成 Neo4j 数据库
智能体系统通常需要与外部数据源交互。我们将使用 Neo4j 图数据库,并导入一个辅助库来简化 ADK 与 Neo4j 的集成。
from neo4j_adk import graph_db
这个 neo4j_adk 库封装了与 Neo4j 的交互逻辑,并将查询结果格式化为 ADK 期望的字典格式,包含 status(成功或错误)和 query_result 等字段。
现在,我们可以测试 Neo4j 连接。
result = graph_db.send_query("RETURN 'Neo4j is ready' AS message")
print(result)
运行后,你会看到结果被包装成一个格式良好的字典:{'status': 'success', 'query_result': [{'message': 'Neo4j is ready'}]}。
定义智能体的工具
工具是智能体与外界交互的手段。没有工具,智能体只能进行思考或对话,无法执行具体操作。
我们将定义一个简单的“问好”工具作为示例。
以下是 say_hello 工具的定义:
def say_hello(person_name: str):
"""
向指定名称的人格式化欢迎信息。
参数:
person_name (str): 需要问候的人的姓名。
返回:
dict: 一个包含状态和查询结果的字典。
"""
query = "RETURN 'Hello to you, ' + $person_name AS greeting"
params = {"person_name": person_name}
result = graph_db.send_query(query, params)
return result
工具的描述文档字符串至关重要,因为它会被传递给大语言模型,帮助模型理解工具的功能和用法。
我们可以直接测试这个工具:
print(say_hello("ADK"))
这将返回一个成功的 ADK 格式结果,其中包含查询结果:“Hello to you, ADK”。
注意:在查询中使用参数(如 $person_name)而非字符串拼接,是防止代码注入攻击的最佳实践。
创建智能体
现在我们已经有了一个基本工具,接下来可以定义一个使用该工具的智能体。
定义智能体需要几个核心组件。让我们看看在 Google ADK 中如何实现。
hello_agent = Agent(
name="hello_agent_v1",
model=llm,
description="一个友好的问候智能体,当用户提供姓名时,使用 say_hello 工具进行个性化问候。",
instruction="""你是一个乐于助人的助手,将与用户聊天。
你有一个工具:say_hello。
如果用户提供了他们的名字,请使用 say_hello 工具向他们致以自定义的问候。""",
tools=[say_hello]
)
参数说明:
name: 智能体名称和版本,便于调试和管理。model: 智能体使用的大语言模型。description: 描述智能体的职责,供其他智能体在委托任务时理解其用途。instruction: 给智能体的系统指令,类似于提示工程中的系统提示,指导其行为。tools: 智能体可以使用的工具列表。
运行智能体:执行环境
智能体需要一个执行环境来运行。这由一个 Runner 类管理,它负责事件循环、调用大语言模型、在智能体间传递结果,并协调各种服务(如内存服务)。
我们将手动设置这个环境,以便理解其工作原理,然后将其封装成一个便捷方法。
首先,逐步设置执行环境:
# 1. 设置内存服务,为智能体运行提供上下文和状态
memory = MemorySessionService()
# 2. 创建会话服务
session_service = memory.create_session(user_id="user1", session_id="session1")
# 3. 创建运行器,将智能体与执行环境绑定
runner = Runner(agent=hello_agent, app_name="HelloApp", session_service=session_service)
现在,我们可以模拟用户发送消息并驱动智能体运行一个事件循环。
# 用户消息
user_message = "Hello, I'm ABK."
print(f"User: {user_message}")
# 将消息包装成 ADK 期望的 Content 格式
content = Content(parts=[{"text": user_message}], role="user")
# 预设最终响应
final_response_text = None
# 运行单步事件循环
async for event in runner.run(app_name="HelloApp", user_id="user1", session_id="session1", content=content):
# 处理事件,例如打印日志
if event.final:
# 智能体表示处理完成
if event.content.parts:
final_response_text = event.content.parts[0].text
break
print(f"Agent: {final_response_text}")
执行后,智能体应调用 say_hello 工具并回复:“Hello to you, ABK.”
封装智能体调用器
由于我们会频繁地创建和运行智能体,因此将上述步骤封装成一个可重用的辅助类 AgentCaller 会非常方便。
class AgentCaller:
def __init__(self, agent, user_id="user1", session_id="session1", app_name="DemoApp"):
self.agent = agent
self.user_id = user_id
self.session_id = session_id
self.app_name = app_name
self.memory = MemorySessionService()
self.session_service = self.memory.create_session(user_id=user_id, session_id=session_id)
self.runner = Runner(agent=agent, app_name=app_name, session_service=self.session_service)
async def call(self, user_message: str):
content = Content(parts=[{"text": user_message}], role="user")
final_response_text = None
async for event in self.runner.run(app_name=self.app_name, user_id=self.user_id, session_id=self.session_id, content=content):
if event.final:
if event.content.parts:
final_response_text = event.content.parts[0].text
break
return final_response_text
# 工厂函数,便于创建 AgentCaller
def make_agent_caller(agent, **kwargs):
return AgentCaller(agent, **kwargs)
现在,我们可以使用这个封装类轻松地进行多轮对话测试。
# 创建问候智能体的调用器
hello_caller = make_agent_caller(hello_agent)
# 模拟对话
async def run_conversation():
response1 = await hello_caller.call("Hello, I'm ABK.")
print(f"Agent: {response1}")
response2 = await hello_caller.call("I am excited.")
print(f"Agent: {response2}")
# 运行对话
import asyncio
asyncio.run(run_conversation())

总结

本节课中我们一起学习了 Google ADK 的基础知识。我们了解了如何导入必要的库、配置大语言模型、集成 Neo4j 数据库,并完成了智能体构建的核心步骤:定义工具、创建智能体、设置执行环境以及运行智能体。最后,我们将复杂的运行逻辑封装成 AgentCaller 类,便于后续复用。你已经成功创建了一个能够使用工具进行交互的简单智能体。在接下来的课程中,我们将以此为基础,构建更复杂的多智能体系统。
005:多智能体系统与状态管理
在本节课中,我们将学习如何构建一个由多个智能体组成的团队,并让它们协同工作。我们将创建一个根智能体来协调两个子智能体,并引入状态(内存)管理,使智能体能够记住会话中的信息。
概述
上一节我们介绍了如何使用Google ADK创建一个基本的智能体。本节中,我们来看看如何创建多个智能体,并让它们组成一个团队协同工作。我们将构建一个包含根智能体和两个子智能体(问候和告别)的系统,并引入状态管理功能,使智能体能够记住用户信息。
导入库与设置LLM
首先,我们需要导入必要的库并设置我们将要使用的大语言模型。
# 导入所需库
import ...
# 设置OpenAI LLM
llm = ...
设置完成后,我们可以通过发送一条测试消息来进行快速检查,确保LLM已准备就绪。
创建多智能体系统
准备好一切后,现在可以开始创建多智能体系统。我们将创建多个专门的智能体,每个智能体都设计用于特定的功能。
以下是创建智能体团队所需的步骤:
- 定义子智能体的工具:我们将为问候和告别功能定义工具。
- 创建子智能体:基于定义的工具,创建专门的问候智能体和告别智能体。
- 创建根智能体:创建一个协调者(根智能体),负责接收用户请求并决定是自行处理还是委托给子智能体。
定义子智能体的工具
首先,我们定义子智能体将使用的工具。
第一个工具与上一课相同,是 say_hello 工具。它接收一个人名并生成问候消息作为响应。
def say_hello(person_name: str) -> str:
"""向指定的人问好。"""
return f"Hello to you, {person_name}."
接下来,我们将添加一个 say_goodbye 工具。这个工具比 say_hello 更简单,因为它不接受任何参数,只返回一个固定的告别消息。
def say_goodbye() -> str:
"""向用户告别。"""
return "Goodbye from Cypher."
创建子智能体
定义好这两个工具后,我们现在可以定义将使用这些工具的智能体。我们将有一个专门的问候智能体和一个专门的告别智能体。
在定义这些子智能体时,强调最佳实践非常重要:为每个智能体提供良好的描述和清晰的指令至关重要。一旦设置了任何多智能体系统,大部分时间将花在优化这些指令上。这回到了经典的提示工程。描述允许其他智能体了解该智能体的功能以及何时应该使用它(这称为委托),而指令则是让智能体自身理解其目的、任务以及可用的工具和何时使用它们。
这是我们的问候子智能体(say_hello_agent),它只能访问 say_hello 工具。
greeting_agent = Agent(
name="Greeting Agent",
description="专门处理简单问候(如 hello, hi)的智能体。",
instructions="你的职责是当用户以某种方式打招呼时进行回应。使用 say_hello 工具来生成个性化的问候。",
tools=[say_hello_tool],
llm=llm
)
接下来,我们添加告别智能体(farewell_agent)。它类似于问候智能体。
farewell_agent = Agent(
name="Farewell Agent",
description="专门处理告别(如 bye, goodbye, see you)的智能体。",
instructions="你的职责是当用户以某种方式告别时进行回应。例如,当用户使用‘bye’、‘goodbye’、‘thanks bye’或‘see you’等词语时。使用 say_goodbye 工具来生成告别消息。",
tools=[say_goodbye_tool],
llm=llm
)
创建根智能体(协调者)
现在我们有两个子智能体准备就绪,需要将它们组合起来。我们将通过定义根智能体来实现,根智能体将理解这两个子智能体并知道何时将任务委托给它们。
这类似于工具调用,但智能体知道它正在与另一个智能体对话,因此整个对话历史会被传递,并且对话的控制权也会传递给子智能体。所以它与工具调用略有不同,但核心理念是当前工作流程将从负责的智能体(根智能体)转移到其中一个子智能体。
我们将在给根智能体的指令中加倍强调这一点。根智能体被告知其工作是协调一个子智能体团队。
root_agent = Agent(
name="Friendly Team Coordinator",
description="协调问候和告别智能体团队的根智能体。",
instructions="""你是一个友好团队的协调者。你的主要目标是保持友好。
为了做到这一点,你有两个专门的子智能体:
1. 问候智能体:用于处理简单的问候,如‘hello’或‘hi’。
2. 告别智能体:用于处理告别,如‘bye’、‘goodbye’或‘see you’。
当从用户那里收到此类消息时,请委托给相应的子智能体来响应用户,而不是作为协调者直接响应。
你没有任何工具可以使用,只能聊天或委托给子智能体。""",
subagents=[greeting_agent, farewell_agent], # 关键:传入子智能体列表
llm=llm
)
这个顶层的协调者本身没有任何工具可用,它只能聊天或委托给子智能体。我们还在 subagents 键中传递了子智能体列表,即问候子智能体和告别子智能体。
与多智能体系统交互
现在我们已经将一个智能体团队组合成一个多智能体系统,让我们继续与它交互。
这与我们在第3课第1部分中所做的类似,我们有一个管理对话的异步函数。在这里,对话将与我们之前所做的相同。首先是“Hello, I am ABK.”,然后用户会说“Thanks bye.”。
我们还在此处添加了 verbose=True 参数,以便在响应中看到所有幕后发生的情况,帮助你理解用户消息的输入、委托过程、工具调用以及响应。
交互流程解析:
- 用户消息:
“Hello, I am ABK.” - 根智能体响应:顶层协调者(团队协调者)将调用子智能体。它采取的第一个行动是希望转移(委托)给一个子智能体,并且它将转移给问候子智能体。
- 问候子智能体行动:问候子智能体接管控制,查看记录,意识到现在需要处理此消息。它将通过进行工具调用来响应,调用
say_hello函数并传入参数person_name: “ABK”。 - 工具响应:函数返回
“Hello to you, ABK.”。 - 最终响应:问候子智能体完成处理,最终代理响应是工具返回的
“Hello to you, ABK.”。 - 第二条用户消息:
“Thanks bye.” - 委托与执行:当前控制权仍在问候子智能体,它意识到这不应由自己处理,于是转移给告别子智能体。告别子智能体接管后,调用
say_goodbye工具。 - 最终告别:工具返回
“Goodbye from Cypher.”,成为最终的代理响应。
这个过程详细展示了工具调用、智能体委托以及状态的整体变化。虽然信息量很大,但非常值得花时间理解,因为它会影响整体的智能体定义和编排方式。
引入状态(内存)管理
在我们的多智能体系统中再进一步。我们有了智能体的执行环境,有了一个可以协同工作的多智能体团队,它们可以相互委托任务,并且每个智能体也有可以使用的工具。
所有智能体系统的最后一个重要组成部分是拥有内存。内存只是特定会话中涉及的智能体(当然也包括用户本身)的内部状态。
在Google ADK中,默认的会话状态本质上是一个可用的字典,你可以在其中更新键值。当你更新这些键的值时,Google ADK会跟踪这些更改。它基本上跟踪状态的变化(增量),并更新整体会话状态,以在所有智能体之间保持一致(无论它们是并行运行还是顺序运行,因为其核心是一个异步系统)。
智能体有几种不同的方式与状态交互:
- 通过工具上下文(首选方法):每当调用工具时,都有一个额外的参数可用,即调用该工具的上下文。给定该工具上下文,工具本身就可以访问智能体的当前状态或当前内存,并可以使用该内存来做出不同的决策、创建不同的输出等。
- 通过输出键:智能体与状态交互的另一种方式是使用输出键。你可以获取智能体的最终响应,并将其保存到状态中,而不是仅仅将其作为智能体的响应返回。这是通过定义输出键来实现的。
设置具有状态功能的工具
在下一步中,我们将设置一些内存。我们目前一直使用内存中的会话,因此内存仅保存在RAM中,不会持久化到数据库。对于生产系统,持久化是更好的选择,但为了方便起见,使用内存中的状态完全可以。
我们将更新我们的两个工具,以实际利用该内存中的状态。对于 say_hello 和 say_goodbye,我们将更新这些工具,以利用工具上下文来更新会话状态或内存。
你应该一次更新一个,并仔细查看这里的差异。
首先,我们需要从Google ADK导入 ToolContext。
from google.adk.tools import ToolContext
然后,我们将 say_hello 函数更新为具有状态功能的 say_hello_stateful。
def say_hello_stateful(person_name: str, tool_context: ToolContext) -> str:
"""向指定的人问好,并更新会话状态。"""
# 更新会话状态中的用户名
tool_context.state[‘username‘] = person_name
print(f"[DEBUG] Updated session state ‘username‘ to: {person_name}")
# 返回问候语
return f"Hello to you, {person_name}."
它仍然接受用户名的参数,但现在有了这个名为 tool_context 的额外参数。工具上下文将让我们访问执行环境为会话传入的工具上下文。在工具上下文中,有一个 state 字典。我们将使用传入函数的名称来更新会话内存或会话状态中的 username 键。
接下来,以类似的方式定义 say_goodbye_stateful。
def say_goodbye_stateful(tool_context: ToolContext) -> str:
"""向用户告别,使用会话状态中的用户名(如果存在)。"""
# 从会话状态中获取用户名,如果不存在则使用默认值
username = tool_context.state.get(‘username‘, ‘there‘)
return f"Goodbye from Cypher, {username}."
say_goodbye_stateful 即使没有接收与人相关的参数,也会传入 tool_context。当Google ADK看到工具的最后一个参数是 ToolContext 时,它会自动将其注入到工具调用中。因为 say_hello_stateful 已经将 username 设置到了状态中,say_goodbye_stateful 实际上可以访问用户名。这样我们就记住了用户的姓名。
创建使用状态化工具的新智能体
然后,你可以为告别定义一个新的状态化智能体,同样,这与我们之前的告别智能体相同,但现在它将调用 say_goodbye_stateful 工具。
和以前一样,你可以通过拥有一个根级智能体(协调者智能体)来将这些组合成一个多智能体系统,该协调者将使用所有这些作为子智能体。
这与我们之前的根智能体完全相同,但现在它使用的所有子智能体最终都调用了基于状态的工具。
现在,你有了一个利用内存的多智能体系统。
测试状态管理
你可以尝试运行它。我们将使用 make_agent_caller 函数,传入新的状态化根智能体(顶层协调者),为其创建一个调用者。为了查看会话中的变化,我们将直接从调用者获取会话,并打印出创建时的初始状态。正如预期的那样,初始状态是空的。
现在定义一个对话,与我们一直进行的对话相同:先说你好,然后说再见。让我们看看会发生什么。
重要的是,我们看到初始状态是空的。我们期望一旦这些调用完成,初始状态将变为最终状态。当我们再次获取会话并从该会话获取状态时,最终状态应该包含由 say_hello 调用定义的用户名。
如果愿意,你甚至可以在此笔记本中设置一个交互式环境,设置一个小型辅助函数,循环获取用户消息并调用我们的调用者传入该消息。只要用户不输入“exit”消息,它就会一直运行。你可以尝试与它交互,看看能想出什么。当然,如果你愿意,可以更改任何代码,看看会产生什么影响。
总结
在本节课中,我们一起学习了如何构建一个多智能体系统。你创建了一个基本的单智能体来说“你好”,然后创建了一个多智能体系统,其中包含两个专门负责说“你好”和“再见”的子智能体,以及一个协调它们与用户交互的根智能体。此外,我们还引入了状态管理,使智能体能够跨对话记住信息(如用户名)。


掌握了这些,你就为继续进行实际的智能体知识图谱构建做好了准备。
006:理解用户意图 🎯


在本节课中,我们将学习如何定义系统中的第一个智能体——用户意图智能体。它的核心目标是帮助你构思可以构建的知识图谱类型,以及你希望从图谱中回答的问题。我们将通过编写代码,一步步实现这个智能体的功能。
智能体架构回顾与定位
上一节我们介绍了整体的知识图谱智能体架构。本节中,我们将聚焦于其中的“结构化数据智能体”子模块。这个子模块负责从简历文件中提取信息,并将这些数据最终转换为图谱。
我们将以渐进、协作的方式完成这项工作。第一步是理解用户的意图。在这个初始阶段,明确用户的目标和他们试图实现的内容描述至关重要,因为它将影响所有后续智能体的行为,为整个系统设定方向。
用户意图智能体的职责与工具
这个特定的智能体有一个主要工作:将“已批准的用户目标”保存到记忆中。为了实现这一点,它有两个工具可以使用。
以下是该智能体的工作流程:
- 感知用户目标:首先,它通过在与用户的对话互动中,感知用户的目标。
- 提出理解并确认:基于对用户需求的理解,它会调用工具来捕获这个理解,并告诉用户:“我认为您想要的是……”。用户可以选择说“是的,正确”或“不,不太对,再试一次”。
- 循环确认:如果用户要求重试,智能体将继续使用“设置感知到的用户目标”工具,直到用户认为正确并批准。
- 最终批准:只有当用户批准后,智能体才应调用“批准感知到的用户目标”工具。
关键点在于,“设置感知到的用户目标”工具本身不能设置已批准的目标。只有通过调用“批准”工具,我们才能确保有一个与用户的检查点,即用户已明确表示同意。
代码实现:设置与提示词构建
明确了智能体的细节后,我们将开始进行常规设置,导入所需的库,并设置语言模型进行快速完整性检查。
接下来,我们将开始定义用户意图智能体本身。我们将逐步描述要提供给智能体的提示词和指令。
智能体角色与目标
提示词的第一部分是定义智能体的角色和目标。我们将告诉智能体:你是一位知识图谱用例专家,主要目标是帮助用户构思知识图谱的用例。换句话说,你的工作是帮助用户构思他们想要实现的目标。
对话提示
提示词的下一部分我称之为“对话提示”。这有助于智能体在其角色和目标背景下,理解应如何展开工作。因为我们处于理解用户意图、与用户共同构思的背景下,所以可以给出一些基本建议,例如:如果用户不确定要做什么,你可以提出一些建议,特别是围绕知识图谱的经典用例。由于智能体是知识图谱用例专家,你可以解释一些用例,如社交网络、物流、推荐系统、欺诈检测,或者流行文化(如跟踪电影、书籍或音乐)。
用户目标的构成
由于用户目标对设定多智能体系统的整体方向至关重要,我特别强调了用户目标的构成。在提示词中,我描述用户目标包含两个部分:
- 图谱类型:例如,是创建社交图谱还是物流图谱?这被描述为最多三个词,用于描述我们正在创建的图谱。
- 图谱描述:提供几句话来描述该图谱的意图。例如,如果图谱类型是“美国货运物流”,描述可以是“一个用于货物的动态路由和交付系统”。
这相当于进行了一些小样本学习,向智能体(并通过智能体向语言模型)强调你试图实现的目标。因为这一点非常重要,值得在提示词本身以及稍后实际使用用户目标的工具描述中重复。
思维链指引
提示词的最后一部分是思维链指引。思维链可以很简单,比如“仔细思考,一步一步来”。但在这里,我们会更具体一些。我们希望语言模型遵循一些步骤,因此我们会非常具体地说:我希望你一步一步地做以下事情。智能体可能会有不同的执行方式,但当我们明确知道希望智能体做什么时,在这里具体说明非常有帮助,可以引导智能体的注意力,使其知道在用户初始互动后应如何继续。
最重要的步骤是:
- 理解用户目标(重申:包括图谱类型和描述)。
- 如果不确定,智能体应根据需要提出澄清问题。
- 只有当智能体认为自己理解了用户目标时,才应调用“设置感知到的用户目标”工具。这将把理解到的用户目标(包含图谱类型和描述两个部分)记录到记忆中。
- 最后,向用户呈现感知到的目标,并请求确认:“我认为您说的是……,对吗?”
- 如果用户同意,智能体才能调用“批准感知到的用户目标”工具。我们在这里提供了大量关于调用此工具的额外说明,以确保智能体真正理解这个工具的作用:调用此工具后,当前的感知目标将被保存在状态中的“已批准用户目标”键下。
我们将使用Python字符串模板将这些部分组合在一起,形成最终给智能体的提示词。
定义智能体工具
现在,我们可以继续定义工具。
第一个工具:设置感知到的用户目标
第一个工具是设置感知到的用户目标。通过在工具中设置此功能,并规定只能通过该工具将其感知到的值保存到记忆中,我们真正帮助智能体专注于理解用户目标意味着什么。因为它必须调用这个工具,并且知道它有两个组成部分(我们在提示词中已说明)。在工具定义本身中,它可以看到有两个参数需要传入:kind_of_graph(图谱类型)和graph_description(图谱描述)。同时,你会注意到我们将tool_context作为最后一个参数传入。如果你还记得上一课的内容,当最后一个参数是工具上下文时,ADK会在调用此工具时自动注入它。
我们在工具描述中重申了它的作用:保存感知到的用户目标,包括图谱类型及其描述。我们也描述了参数是什么,这些应该与之前在提示词指令中对智能体所说的完全一致。你说得越多,语言模型在调用工具时做错事的可能性就越小。
在工具内部,它非常简单。它所做的就是组装一个小字典,该字典由这两个组成部分(图谱类型和图谱描述)组成,作为可用的数据。这只是封装如何访问内存的一种非常简单的方式,同时也聚焦于对内存的访问。上下文状态会用这个字典更新,因为这是一个更新操作,ADK会看到状态发生了变化,并将这个增量传播给运行时环境中需要感知它的任何其他部分。
第二个工具:批准感知到的用户目标
接下来,你可以定义下一个工具,用于批准感知到的用户目标。这个工具只应在感知到的用户目标已被设置,并且用户也已表示“是的,我批准”之后被触发。如果这两个条件都为真,则应调用此工具。
在工具内部,只要可能且合理,就值得“信任但要验证”语言模型是否在做正确的事情。在这个工具中,它只应在用户批准后被调用。因此,我们再次向语言模型强调:只有在用户批准后才调用此工具。然后,如果用户通过调用此工具表示批准,该工具将把感知到的用户目标记录为已批准的用户目标。同样,我们会告诉智能体:只有在用户已明确批准感知到的用户目标时,才在工具内部调用此工具。
为了进行一点检查,因为我们要求感知到的用户目标必须已被设置,我们可以在做任何其他事情之前检查它是否已被设置。我们如何处理这个设置非常重要,因为对工具的调用可能成功也可能导致错误。这里,我们有一个非常具体的错误:如果感知到的用户目标不在当前上下文中(即不在工具上下文状态或智能体的记忆中),我们将从此工具返回一个错误。
我们提供的错误信息旨在帮助语言模型理解:哪里出错了,你应该怎么做来解决问题。因此,我们告诉语言模型:感知到的用户目标尚未设置,它应该先设置感知到的用户目标,或者如果它不确定用户的意图,应提出澄清问题。
这实际上是在所有不同的地方——从提示词到工具定义,再到工具返回的错误信息——反复向智能体强调,鼓励语言模型根据实际情况做正确的事情。
如果检查成功,我们要做的只是将状态从“感知到的用户目标”复制到“已批准的用户目标”。请注意,由于我们没有向“批准感知到的用户目标”工具传递参数,因此只有当存在感知到的用户目标时,已批准的用户目标才会被设置(通过复制)。之后,状态中应该同时存在感知到的和已批准的用户目标。
为了方便起见,我们将这两个工具添加到一个列表中,因为我们知道在创建实际的智能体本身时会传入一个列表。
创建用户意图智能体
现在我们已经有了两个工具(设置感知到的用户目标和批准感知到的用户目标),可以定义用户意图智能体了。
你将给它一个带有版本号的名称(这将是一个唯一的版本名),我们将使用之前定义的语言模型(在所有notebook中都将使用相同的语言模型)。描述非常重要:此用户意图智能体的意图是帮助用户构思知识图谱用例。这有助于整个多智能体系统知道何时使用此智能体本身。回想之前的架构图,这是工作流的一部分。通过使此描述与该智能体在工作流中扮演的角色相匹配,有助于实际管理工作流的协调器知道何时委托给此智能体。
在智能体内部,我们传入之前组装的完整智能体指令,当然,我们也会传入可用的工具列表。
现在,你已经有了一个完整的用户意图智能体,可以开始使用了。
与智能体交互
你可以导入make_agent_caller并创建一个针对用户意图智能体的调用器,我们称之为user_intent_caller。
需要注意的是,如果我们要多次运行此代码,最好回到这里重新初始化状态,以防事情偏离轨道。
让我们设置一个会话来与用户意图智能体交互。在开始实际交互之前获取会话,以便查看会话的当前状态(初始时通常为空)。
然后,我们将创建一个脚本化的对话,这里基本上只有两次调用。
第一次调用
用户声明他们想要一个物料清单图谱,并且它应该包含从供应商到成品的所有级别的物料清单。用户还指定他们希望这个物料清单图谱能够支持根本原因分析。
处理智能体响应
这是对话的一个重要部分。有时,当智能体收到用户的初始消息时,它可能会决定需要提出澄清问题。因此,如果它提出了,那么感知到的用户目标将不会被设置。智能体可能会回应说:“请告诉我更多关于你试图做的事情。” 我们在这里做一个假设:如果感知到的用户目标没有在会话状态中设置,我们假设语言模型提出了一个澄清问题。让我们以用户的身份提供一个澄清答案,并说:“我担心可能出现的制造或供应商问题。” 希望这足以满足智能体的要求,并鼓励它实际调用感知用户目标工具。
乐观情况与批准
乐观地看,如果我们到达这里,那么感知到的用户目标应该已被设置。我们将继续说:“嗯,这听起来很棒。我批准这个目标。” 你可能需要多次运行此代码,因为智能体可能不会立即设置感知到的用户目标;它可能在任何一天都决定在与用户进行更多对话后,才确定自己真正理解了用户目标。
在调试模式下运行批准用户目标的最终调用,你可以看到我们经历的会话过程:会话开始时,会话状态中没有任何内容,所以内存是空的。用户发送了他们的初始消息,智能体回应并实际提出了澄清问题。因为我们有那个检查点,我们决定发送额外的消息:“我担心可能出现的制造或供应问题。” 有了这个,看起来智能体然后决定有足够的信息继续,并理解了我们的目标。它告诉我们它认为我们试图构建的图谱类型,以及它认为我们想要从该图谱中得到什么的描述。最后,它正确地说:“这抓住了您的意图吗?” 这正是我们想要它做的。然后作为用户,我们将假设批准该目标。在批准该目标时,用户意图智能体进行了设置感知用户目标的调用,然后因为用户已批准,它调用了批准感知用户目标工具。你可以看到底部的最终响应:用户目标已成功批准,一切顺利。
现在,工作流知道了用户在这个多智能体系统中构建知识图谱试图实现的整体方向。
总结

本节课中,我们一起学习了如何构建“用户意图智能体”。我们定义了它的核心角色是帮助用户构思知识图谱用例,并详细设计了它的工作流程,包括感知用户目标和获取用户批准两个关键步骤。我们通过精心构造的提示词(包含角色目标、对话提示、目标构成和思维链)来引导智能体的行为,并实现了两个核心工具来与记忆系统交互。最后,我们通过一个具体的对话示例演示了智能体如何与用户协作,最终明确并锁定知识图谱的构建目标,为后续的智能体工作奠定了基础。
007:文件建议智能体 🗂️


在本节课中,我们将学习如何构建工作流中的第二个智能体——文件建议智能体。该智能体的核心任务是,根据已确定的用户目标,从结构化数据中筛选出用于构建领域知识图谱的相关文件。
概述
上一节我们介绍了用户意图智能体,它负责明确用户的目标。本节中,我们来看看文件建议智能体。该智能体将基于用户目标,从可用的文件列表中筛选出最相关的文件,并请求用户确认,最终生成一个“已批准文件列表”。
智能体工作流程与目标
文件建议智能体遵循与用户意图智能体相似的模式。其核心目标是创建“已批准文件列表”。智能体首先会审视所有可用文件,判断哪些与用户目标相关,然后询问用户:“您批准使用这些文件吗?” 如果用户同意,输出结果就是最终批准的文件列表。
为了实现这个目标,智能体配备了一系列工具来操作文件。
以下是文件建议智能体可用的工具列表:
- 列出导入文件:获取所有可用文件的列表。
- 采样文件:读取文件内容(约10行),以便评估其相关性。
- 设置建议文件:基于评估,将建议的文件列表记录到内存中。
- 获取建议文件:从内存中读取当前建议的文件列表。
- 批准建议文件:在用户确认后,将建议文件列表更新为已批准文件列表。
所有这些操作都基于前一个智能体(用户意图智能体)的工作成果。文件建议智能体通过调用 get_approved_user_goal 工具从会话内存中获取用户目标,而不是依赖对话历史记录。这确保了智能体始终基于明确、已批准的目标进行决策。
代码实现详解
现在,让我们深入代码,看看如何构建这个智能体。
1. 初始设置与库导入
首先进行常规设置,导入所需的库以及之前定义的辅助函数。
# 导入必要的库和辅助函数
import openai
from tools_module import get_approved_user_goal, get_neo4j_import_dir
# ... 其他导入
创建与OpenAI的连接并确保其正常工作。
2. 定义智能体指令
接下来,我们分步构建智能体的指令。首先定义其角色和目标。
agent_instructions = """
角色:你是一个建设性的评审者,负责审查文件列表。
目标:为构建知识图谱建议相关的文件。
"""
然后,提供如何完成任务的提示。具体任务是:基于已批准的用户目标描述,审查文件列表并找出哪些文件对构建此类图谱有用。智能体可以使用 sample_file 工具查看文件内容以确认其有用性,并且应主要关注如CSV或JSON之类的结构化数据文件。
最后,给出思维链指示。思维链包含两部分:
- 准备阶段:调用
get_approved_user_goal工具从会话内存中获取用户目标。 - 执行步骤:仔细思考,重复以下步骤直到完成:
- 步骤1:获取所有可用文件的列表。
- 步骤2:评估每个文件的相关性。
- 步骤3:使用
set_suggested_files工具将建议的文件记录到内存中。 - 步骤4:使用
get_suggested_files工具获取建议文件列表(这一步至关重要,它强制智能体专注于内存中的内容,而非对话历史)。 - 步骤5:将结果呈现给用户批准。如果用户批准,则调用
approve_suggested_files工具;如果用户有反馈,则带着反馈回到步骤1。
3. 定义智能体工具
现在,定义智能体将使用的各个工具函数。
获取已批准用户目标工具
此函数直接返回存储在状态中的已批准用户目标。
列出导入文件工具
此工具列出Neo4j可以访问的导入目录中的所有文件。关键点在于,智能体只能访问相对于该导入目录的文件路径,而非绝对路径。
def list_import_files(tool_context):
import_dir = get_neo4j_import_dir()
# 递归列出import_dir下的所有文件,并转换为相对路径列表
all_files = [os.path.relpath(f, import_dir) for f in glob.glob(os.path.join(import_dir, '**/*'), recursive=True) if os.path.isfile(f)]
# 将列表保存到当前状态
tool_context.state['all_available_files'] = all_files
return all_files
采样文件工具
此工具接收一个相对文件路径,并返回该文件的前100行文本。代码中包含安全检查:确保路径是相对的、路径存在于可用文件列表中、以及文件确实存在。
def sample_file(tool_context, file_path: str):
# 检查是否为绝对路径
if os.path.isabs(file_path):
return "错误:必须提供相对于导入目录的文件路径。"
# 检查文件是否在可用列表中
if file_path not in tool_context.state.get('all_available_files', []):
return f"错误:文件 '{file_path}' 不在可用文件列表中。"
# 构建完整路径并检查存在性
full_path = os.path.join(get_neo4j_import_dir(), file_path)
if not os.path.exists(full_path):
return f"错误:文件 '{full_path}' 不存在。"
# 读取文件内容
try:
with open(full_path, 'r', encoding='utf-8') as f:
content = ''.join([next(f) for _ in range(100)]) # 读取最多100行
return content
except Exception as e:
return f"读取文件时出错:{e}"
设置与获取建议文件工具
set_suggested_files:接收一个文件路径字符串列表,并将其保存到内存中。get_suggested_files:从内存中读取当前建议的文件列表。
批准建议文件工具
此工具在用户确认后调用。它会检查建议文件列表是否已存在于状态中,如果存在,则将其复制到“已批准文件”中。
def approve_suggested_files(tool_context):
suggested = tool_context.state.get('suggested_files')
if not suggested:
return "错误:未找到建议的文件列表。请先设置建议文件。"
tool_context.state['approved_files'] = suggested.copy()
return f"成功!已批准文件:{suggested}"
将所有工具定义放入一个列表,命名为 file_suggestion_agent_tools。
4. 创建并运行智能体
有了工具和提示,定义智能体就很简单了。
file_suggestion_agent = Agent(
tools=file_suggestion_agent_tools,
instructions=agent_instructions
)
现在,使用之前定义的 make_agent_call 辅助函数与智能体交互。关键点:我们需要初始化会话状态,包含已批准的用户目标,以模拟工作流中前一步已完成。
initial_state = {
'approved_user_goal': '进行供应链分析,为制造产品创建多级物料清单。'
}
response = make_agent_call(file_suggestion_agent, user_message="我们可以使用哪些文件进行导入?", initial_state=initial_state)
运行对话后,智能体会列出所有可用文件(可能包含CSV、Markdown等),并基于其目标,仅建议相关的CSV文件(如装配体、组件、供应商映射等)。用户回复“是的,我们开始吧”表示批准,智能体随后调用批准工具,将建议文件列表转为已批准文件列表。
总结
本节课中,我们一起学习了如何构建文件建议智能体。我们定义了它的角色、目标和详细的思维链指令,并实现了一系列关键工具,包括列出文件、采样内容、设置和批准文件列表。这个智能体能够基于明确的用户目标,从文件系统中智能筛选出相关数据文件,为后续的知识图谱构建步骤准备好数据基础。

至此,我们已经完成了结构化数据知识图谱构建工作流中的两个智能体:第一个明确用户意图,第二个基于该意图寻找支持数据。在下一课中,我们将继续下一步:根据用户意图和可用的文件,设计图谱可能的结构。
008:结构化数据的模式提案



在本节课中,我们将学习如何为结构化数据设计知识图谱的模式。我们已经定义了用户目标和选定的数据文件,下一步是决定构成领域图谱的节点和边的类型。我们将建立一个由多个子智能体组成的循环,来迭代地优化这个图谱模型。
从用户意图到模式提案
上一节我们完成了文件建议和批准。现在,我们进入工作流的下一阶段:为图谱模式提出一个方案。这是“结构化数据智能体”的核心任务。
这个智能体在构建方式上引入了一些新思路。它内部实际上包含了多个智能体,特别是:
- 一个负责提出初步图谱方案的“提案智能体”。
- 一个负责审查和批评该方案的“批评智能体”。
这是一种在多智能体系统中非常常见的“批评者模式”。
在顶层的协调器中,它主要使用几个工具,其中一个特别有趣:将“精炼循环”本身作为一个工具。这个工具本身就是一个智能体,它协调着几个子智能体共同工作。
深入精炼循环
精炼循环是一个协调子智能体的智能体。它包含三个子智能体:
- 模式提案智能体:负责提出初步方案。
- 模式批评智能体:负责审查和批评方案。
- 检查状态并升级智能体:负责根据批评者的输出决定循环是否结束。
这些智能体会循环工作,直到达成最终结果。批评智能体审查提案,检查状态智能体则判断批评者是否满意。如果满意,则由它触发“升级”,终止循环。为了避免无限循环,我们可以设置最大迭代次数。如果无法达成共识,循环将终止,并向用户请求更多指导。
构建模式提案智能体
我们首先进行常规的导入并定义要使用的大语言模型。
智能体指令设计
接下来,我们重点看看提案智能体和批评智能体的指令设计。
对于提案智能体,我们定义其角色和目标为“属性图谱图数据建模专家”。指令中的一个新颖部分是反馈注入机制。我们使用类似XML的标签 {feedback} 来包裹反馈内容。这是一个模板变量,会被LangGraph根据上下文状态中的 feedback 变量值动态替换。初始时反馈为空。
然后,我们为智能体提供详细的指导,告诉它如何完成工作。这些指导比之前更详尽,因为我们需要将知识图谱构建的最佳实践编码到提示词中。
以下是核心指导原则:
- 总体原则:已批准文件列表中的每个文件都应成为图谱的一部分。
- 识别标识符:在CSV文件中寻找唯一标识符,并用它们来理解文件角色和构建图谱。
- 设计规则:提供判断文件应作为节点还是关系的语义提示(例如,文件名、标识符数量、是否引用其他文件标识符)。
- 节点规则:节点通常只有一个唯一标识符。如果文件有多个标识符,额外的标识符可能代表引用关系。
- 关系规则:关系分为“完整关系”和“引用关系”,并详细描述了各自的识别方法和图谱化方式。
- 连通性要求:最终的模式应该是一个完全连通的图,孤立的组件通常意味着存在问题。
思维链设计
我们为智能体设计了更精细的“思维链”指导,引导它一步步思考:
- 准备任务:基于已确定的用户目标、批准的文件列表以及可能已存在的当前构建计划来准备。
- 逐步分析:对每个批准的文件,逐步思考它是节点还是关系。
- 验证标识符:每当找到一个可能的标识符时,使用
search_file工具验证其在该文件内的唯一性。 - 应用设计规则:根据之前提供的设计规则,最终决定文件类型。
- 调用构建工具:
- 如果判定为节点文件,调用
propose_node_construction工具来记录如何将该文件转化为图谱节点。 - 如果判定为关系文件,调用
propose_relationship_construction工具。
- 如果判定为节点文件,调用
- 生成最终计划:处理完所有文件后,使用
get_proposed_construction_plan工具向用户呈现完整的构建计划。
工具定义
智能体需要使用一系列工具。许多工具(如获取用户目标、获取批准文件、文件采样)已在之前的课程中定义,我们可以直接导入。
以下是本阶段新定义的核心工具:
-
search_file工具:一个简化版的grep函数,用于在文件中搜索特定内容(如验证标识符唯一性),并返回匹配的行号和数量。def search_file(file_path: str, search_content: str) -> dict: # 读取文件,搜索内容,返回匹配信息 ... -
propose_node_construction工具:定义如何将源数据文件转化为节点。- 输入:文件路径 (
file_path) - 定义节点:
label:节点标签(如Person,Product),描述节点类别。unique_column_name:CSV中作为节点唯一标识的列名。proposed_properties:从CSV中选取哪些列作为节点的属性。
- 实现:该函数进行基础检查,然后创建一个类型为
node的“构建规则”字典对象,并以label为键添加到总的构建计划中。
construction_rule = { “type”: “node”, “source_file”: file_path, “label”: label, “unique_column”: unique_column_name, “properties”: proposed_properties } - 输入:文件路径 (
-
propose_relationship_construction工具:定义如何创建关系。- 输入:文件路径 (
file_path) - 定义关系:
relationship_type:关系类型(如SUPPLIES,PART_OF)。from_node_label/to_node_label:关系连接的起始和终止节点标签。from_node_column/to_node_column:CSV中对应起始和终止节点唯一标识的列名。properties:关系的属性。
- 实现:创建类型为
relationship的构建规则,并以relationship_type为键添加到构建计划中。
- 输入:文件路径 (
-
移除工具:提供
remove_node_construction和remove_relationship_construction工具,允许智能体在精炼循环中修正之前提出的规则。 -
计划获取与批准工具:
get_proposed_construction_plan工具用于汇总展示所有构建规则。approve_proposed_construction_plan工具用于批准最终的模式提案。
我们将所有工具组合成一个列表,提供给提案智能体。
创建并运行提案智能体
现在,我们可以定义模式提案智能体本身。我们创建一个LLM智能体,传入名称、描述、指令和工具列表。一个特别的设置是添加了一个 callback,用于在智能体被调用时打印日志,方便我们跟踪执行过程。
接着,我们创建执行环境,并初始化状态(包括用户目标、批准的文件列表以及初始为空的反馈)。然后,我们调用智能体,提示信息为:“如何导入这些文件以构建知识图谱?”
运行后,智能体成功输出了一份构建计划。例如,对于一组产品数据文件,它可能提出:
- 节点:
Assembly(组件)、Part(零件)、Product(产品)、Supplier(供应商)。 - 关系:
INCLUDED_IN:从Assembly到Product,表示组件被用于产品,包含assembly_name和quantity属性。PART_OF:从Part到Assembly,表示零件属于组件。SUPPLIED_BY:从Part到Supplier,表示零件由供应商提供。它正确地识别出part_supplier_mapping.csv是一个连接表,应转化为关系而非节点。
智能体还提供了详细的解释,表明它很好地理解了数据和我们的指导。
构建模式批评智能体
提案智能体完成了它的工作。现在,我们来看精炼循环的第二部分:批评智能体。
批评智能体同样被塑造为“属性图谱知识图谱建模专家”,但它的目标不是创建模型,而是批评已创建的模型。
批评者的指令
我们为批评者设定角色和目标,并给予它如何开展批评工作的提示。这些提示是从“如何进行良好的知识图谱建模”延伸出来的,但侧重于审查角度:
- 检查提议的唯一标识符是否真正唯一(可使用
search_file工具验证)。 - 检查是否有本应是关系的实体被定义成了节点,反之亦然。
- 验证图谱是否连通:应能手动从源数据追踪到构建的图谱,确保所有CSV文件都被用到,且图谱完全连通。
- 检查是否存在冗余的关系。提案可能过于“热情”地创建了不必要的逻辑关系。


批评者的思维链
批评者的思维链指导如下:
- 准备:获取用户目标、批准文件和当前提议的构建计划。
- 逐步审查:仔细检查每个节点和关系构建规则。
- 工具验证:使用可用工具验证这些规则的相关性和正确性。
- 做出裁决:
- 如果一切通过,用单个词
Valid响应。 - 如果模式有任何问题,用
Retry响应,并提供一个简洁的要点列表作为反馈,说明需要做出的修改。
- 如果一切通过,用单个词
这个反馈将被放入循环,传回给提案智能体,让它根据反馈进行调整。
创建批评智能体
批评智能体可以使用与提案智能体相同的工具集。我们创建这个LLM智能体时,一个关键设置是 output_key。我们将其设置为 feedback。这意味着当批评智能体完成工作并发出最终消息时,该消息内容会被自动保存到状态字典的 feedback 键下。这样,在后续的循环中,提案智能体就能获取到这个反馈信息。
组装精炼循环
我们已经定义了精炼循环中的两个核心子智能体。现在,我们需要定义循环本身,以及第三个自定义智能体:检查状态并升级智能体。
检查状态并升级智能体
这个智能体的工作很简单:决定循环是否结束。
- 逻辑:它检查状态中的
feedback内容。 - 判断:如果
feedback为空或内容为Valid,则意味着批评者满意,循环应停止(should_stop=True)。 - 行动:它产生一个特殊事件,其中包含一个
escalate动作。escalate的值根据should_stop决定。如果为真,则触发升级,跳出循环;如果为假,则继续循环。
创建循环智能体
最后,我们创建循环智能体。它是一个特殊的工作流智能体,接收一个子智能体列表(提案、批评、检查状态),并将它们放入一个循环中执行。这个智能体本身不进行推理,只负责协调。
一个关键参数是 max_iterations,我们将其设为2。这意味着循环最多执行两次。要么达成共识,要么在尝试两次后退出,避免无限循环。循环结束后,结果要么是一个确认的模式,要么需要用户进一步输入来指导如何设计模式。
运行精炼循环
我们为精炼循环创建执行环境,初始化状态,并调用它。运行过程会花费一些时间,因为它至少会完整执行一次“提案->批评”的流程。
在示例运行中,我们可能观察到:
- 循环开始,提案智能体首次运行并提出方案。
- 批评智能体启动,审查后可能给出
Retry反馈,指出关系存在重叠或数据不完整等问题。 - 由于设置了最大迭代次数为2,循环让提案智能体根据反馈再次运行。
- 批评智能体再次审查。在第二次迭代后,批评者可能仍然不满意。
- 此时,循环达到最大迭代次数而终止。
由于批评者未满意,在完整的架构中,控制流将返回给顶层的“模式提案协调器”。这正是引入人工干预的环节。协调器可以向用户反馈:“不确定如何生成模式,这是当前的进展和批评意见,您认为下一步该怎么办?” 在没有人工介入的当前演示中,我们可以尝试调整初始提示、重新运行或修改其他参数来获得更好的结果。
总结
本节课中,我们一起学习了如何构建一个多智能体协作的“精炼循环”,来为结构化数据设计知识图谱模式。
- 我们首先介绍了模式提案智能体,它利用详细的领域指导规则,分析数据文件并提出初步的图谱构建方案(节点和关系)。
- 接着,我们引入了模式批评智能体,负责从专业性、正确性和连通性等角度审查提案,并提供结构化反馈。
- 然后,我们通过检查状态并升级智能体和循环智能体,将前两者组织成一个迭代优化流程,并设置了安全边界(最大迭代次数)。
- 这个设计体现了智能体系统的核心模式之一——批评者模式,通过分工、协作与迭代,提升复杂任务(如图谱建模)的输出质量。
- 最后,我们看到了当自动循环无法达成共识时,工作流如何自然地悬停,等待人类专家的介入和指导,实现了人机协同的混合智能。

通过本课,你掌握了构建具有自我审查和迭代优化能力的智能体工作流的关键方法。
009:为非结构化数据设计模式提案 🧠


在本节课中,我们将学习如何为来自Markdown文件的非结构化数据设计知识图谱的构建模式。我们将创建两个专门的智能体来完成这项任务。
概述
上一节我们完成了从结构化数据(CSV文件)构建知识图谱的完整工作流。本节中,我们将转向处理非结构化数据的工作流。虽然起始步骤相似,但核心在于引入新的概念:实体与事实类型提案智能体。
这个智能体本身由两个专门的子智能体构成:
- 命名实体识别(NER)模式智能体
- 事实类型提取智能体
请注意,这些智能体的输出是如何执行知识提取的计划,而不是执行提取本身。
命名实体识别(NER)智能体 🏷️
首先,我们将定义命名实体识别智能体。命名实体识别是一项常见的自然语言处理任务,而大语言模型在此类语言任务上表现出色。
智能体指令定义
我们将从定义智能体的指令开始,并将其分解为几个部分组合在一起。
角色与目标
首先,定义智能体的角色和目标。这里,我们将其描述为专为自然语言处理设计的顶级算法。其目标是在文本中查找命名实体,但不实际提取这些实体。目标是识别存在哪些类型的实体。
提示与规则
接着,为智能体提供更多提示,明确我们寻找的目标。我们将描述什么是实体,并将其分为两类:
- 已知实体:这些实体存在于上一课已定义的结构化数据中。如果这些实体出现在文本中,我们也希望提取文本中的相应部分。
- 发现实体:这些实体可能不存在于已描述的图数据中,但可能与用户目标相关。如果它们频繁出现,则可能是有用的信息。
然后,提供更多关于如何识别已知实体和发现实体的设计规则,并像往常一样,提供几个示例来帮助智能体理解其目的。
思维链指引
指令的最后部分是思维链指引。这里我们将精确描述如何准备执行当前任务(即识别这些实体)。以下是它需要使用的工具来了解对其的期望:
- 给予完整上下文。
- 以下是建议的执行步骤序列:它知道有哪些文件可用,拥有
sampling_file工具。因此,使用该工具查看一些文件,发现并同时考虑已知实体和频繁提到的新实体。然后,汇总它认为合适的所有实体列表,并使用propose_entities工具提交该列表。 - 和之前一样,将返回给用户确认。然后会有一个单独的工具(此处称为
approve_proposed_entities)来实际进行批准。
最后,将所有部分组合成一个字符串,这将作为我们命名实体识别智能体的指令。
工具定义
指令定义好后,接下来提供工具本身的定义。这些工具将遵循我们在前几课中使用的相同模式。
实体提案与批准工具
我们将要求智能体首先提出一些建议,然后使用其他工具将这些建议转化为已批准的版本。因此,这里我们提议的是一个首先被提出、然后被批准的实体列表。当然,你也可以获取这些提案或批准。
已知类型获取工具
下一个需要的工具略有不同。它将是一个获取器工具,用于从上一课结构化数据提出的模式中获取已知类型。well_known_types工具最终将使用上一课提出的模式中为节点定义的标签。我们将从构建计划中提取它们,然后将其作为列表返回。
我们将导入在前几课中预定义的工具以及此处新增的工具。和之前一样,可以将它们组合成一个列表,作为智能体定义的一部分。
查看示例数据
为了了解智能体将处理的内容,可以直接在其中一个可用文件上调用sample_file函数并查看其内容。
查看这个Markdown文件,可以看到这是一系列Markdown格式的评论。它有标题,包含一些嵌入的值,如评分。可以看到用户名、位置,当然还有评论本身的文本。文件中只有少量评论,但足以演示其工作原理。
构建并运行智能体
现在,指令已定义,工具已定义,我们可以构建智能体本身了。
由于它是较长工作流的一部分,运行此智能体时需要对状态中已累积的内容有一些假设。因此,我们必须创建一个初始状态来测试此智能体。
初始状态需要包含以下几项:
- 已批准的用户目标
- 已批准的文件(此处是它将查看的Markdown文件)
- 构建计划(用于那些已知实体类型)
注意,我们省略了关系构建,因为此步骤不需要。
现在可以运行智能体了。我们将使用辅助模块中的make_agent_call,并发送一个简单的请求。基本上就是告诉它:“智能体,你能完成你的工作吗?将一些产品评论添加到知识图谱中,通过制造流程追踪产品投诉。”
智能体完成后,我们会查看会话状态,确保它确实提出了建议,并且希望它没有自动批准,而是在等待我们确认。
运行需要几分钟,因为智能体会去查看几个不同的文件,并尝试提出它认为最适合用于实体的标签集。
如果结果良好,智能体不仅会给出好的响应,还会正确更新会话状态(内存)。我们会看到proposed_entities,但还没有approved_entities。
如果结果令人满意,可以向同一智能体发送新消息:“我批准那些提议的实体。”完成后,我们应该会发现它确实将提议的实体转移到了已批准的实体中,并能在会话状态中看到。
事实类型提取智能体 🔍
接下来,我们将转向第二个智能体:事实类型提取子智能体。
顾名思义,与上一个智能体类似,我们将寻找可以提取的事物的类型,而不是进行提取本身。
智能体指令定义
同样,我们从指令开始,因为这是这两个智能体中最重要的部分。
角色与目标
对于此智能体,角色和目标方面,我们再次将其描述为顶级算法,将进行一些文本分析。但这里的目标略有不同:它将寻找可以提取的事实类型,不要实际提取那些事实,只找出可能的事实类型。
提示与规则
为了帮助智能体理解我们真正讨论的内容,我们将提供一些提示。重要的一点是:不要提议具体的个别事实,而是提议与用户目标相关的一般事实类型。举例说明总是一个好主意。例如:不要提议“ABK喜欢咖啡”,而是提议“人喜欢饮料”这种一般类型的事实。
这些构成了非常简单的句子,我们称之为三元组,形式为(主语,谓语,宾语)。注意这里我们用了括号,这是一种经典做法。我们将观察智能体是否实际采用这种格式并在响应中返回。
我们为其提供一些关于如何思考此问题、寻找什么以及如何使用工具的附加设计规则。
与上一个智能体的区别
与上一个智能体略有不同。上一个智能体有一个完整的提议实体类型列表;而这里,对于每个单独的事实,我们将一次添加一个作为单独的事实,而不是“这里是我们将要提议的事实集合”。这是一个细微的差别。其重要性最终体现在智能体的行为方式以及令牌的实际成本上:往返次数越多,成本会稍高一些。但如果能获得更好的结果,这可能是一个值得的权衡。
思维链指引
最后,添加思维链指引。和之前一样,我们将遵循这些指引的相同模式:描述如何准备任务,以及实际执行此任务的建议逐步方法。以这种方式使用工具,对部分文件进行采样,寻找与文本相关的主语和宾语,然后添加关于这些主语和宾语的提议事实。最终得到的结果应该是几个小的三词句子。
然后,将所有内容组合成一个字符串。
工具定义
现在,我们将花更多时间查看事实类型提取智能体的工具定义。这是因为它在此过程中进行了一些合理性检查,而这正是将这些功能分离到不同智能体的意图之一。
在上一个智能体(命名实体识别智能体)中,我们给出了一些关于如何寻找实体的指导,但对其发现的内容没有太多限制。在这里,我们将非常具体:它提议的事实必须与先前智能体定义的现有实体类型相匹配。以这种方式拆分,能更好地保证我们得到的事实能与想要的实体类型对应。
添加提议事实的工具
查看add_proposed_fact的定义,我们看到通过参数名称传递的是:一个现有的已批准主语标签、一个提议的谓语标签,然后是一个已批准的宾语标签。这再次构成了三元组的形式。
然而,这些三元组中的主语和宾语都应该已经存在于上一个智能体的结果中。因此,我们将围绕此设置一些防护措施:我们将从状态中获取approved_entities,并确保作为参数传入的已批准主语和宾语标签都在该列表中。如果不在,我们将从此工具调用中抛出错误,并告知该标签不存在,应该重试。
我们在此逐条处理的部分原因是为了能够逐条对智能体的行为进行一些纠正。
如果一切正常,我们会重新整理这个三元组,将其保存到当前谓词列表中,并从中创建一个事实列表。
其他工具
这里的另外两个函数只是遵循通常的模式:获取提议事实的列表,然后将提议的事实批准到已批准的事实列表中。
然后,可以将该工具列表放在一起,供事实智能体使用,现在我们可以继续构建它。
构建并运行智能体
构建智能体,由于我们已经完成了定义所有内容的艰苦工作,所以相当简单。现在可以继续运行它。
此智能体的初始状态与上一个智能体的初始状态相同,因此我们将利用这一点,直接复制上一个智能体的最终状态作为此智能体的初始状态。在完整的多智能体系统中,它们将按顺序运行,并以这种方式共享状态。
但为了单独运行它们,我们将复制状态,然后调用智能体。
用于启动的消息是要求它继续并提出一些建议。


智能体完成所有事实提议后,我们将查看会话状态。我们假设它已提出建议,然后检查是否尚未批准该建议。
如果一切顺利,我们将看到它提出的建议。然后,可以发送另一条消息批准该提议,这应该会将提议的事实类型转移到已批准的事实类型中。查看会话状态,我们会在末尾看到approved_fact_types。
总结
本节课中,我们一起学习了如何为来自非结构化数据(Markdown文件)的知识图谱构建设计模式。我们定义并运行了两个专门的子智能体:
- 命名实体识别(NER)智能体:负责分析文本,识别并提议与用户目标相关的实体类型(如“产品”、“问题”)。
- 事实类型提取智能体:在实体类型的基础上,分析文本中关于这些实体的陈述,提议可能的事实三元组类型(如“(产品,有,问题)”)。


这两个智能体协同工作,共同输出一个提取计划,为后续从非结构化文本中实际提取知识图谱的节点和关系奠定了基础。通过这种模块化的设计,我们可以更精细地控制提取逻辑,并确保实体与事实之间的对应关系。
010:知识图谱构建 - 第一部分


在本节课中,我们将学习如何根据之前制定的详细计划,构建一个具体的知识图谱。我们将定义并实现一系列工具,这些工具将按照智能体工作流指定的计划,执行从CSV文件加载数据到Neo4j图数据库中的机械过程。
概述
上一节我们完成了知识图谱构建计划的制定。本节我们将深入探讨构建计划的具体执行工具。我们将创建一个名为“构建领域图”的核心工具,它由一系列具有特定职责的辅助函数组成,用于处理数据导入、约束创建和关系建立等任务。
工具定义与实现
以下是构建领域图所需的一系列工具,我们将逐一介绍。
1. 创建唯一性约束
在开始导入数据之前,我们需要为数据库做好准备。第一步是确保为每个节点类型创建唯一性约束。这对应于CSV文件中具有唯一ID列的情况。
代码实现:
def create_uniqueness_constraint(label, unique_property_key):
constraint_name = f"constraint_{label}_{unique_property_key}"
query = f"""
CREATE CONSTRAINT {constraint_name} IF NOT EXISTS
FOR (n:`{label}`)
REQUIRE n.{unique_property_key} IS UNIQUE
"""
# 注意:此处使用了字符串拼接,在生产环境中应对输入进行净化处理
neo4j_query(query)
此函数接收节点标签和唯一属性键作为参数,并在Neo4j中创建相应的唯一性约束。由于Neo4j的约束创建语法不支持查询参数化,这里使用了字符串拼接,实际应用中应添加输入净化逻辑。
2. 从CSV文件加载节点
定义了约束后,我们需要能够从CSV文件加载节点数据。
代码实现:
def load_nodes_from_csv(source_file, label, unique_column, property_list):
query = """
LOAD CSV WITH HEADERS FROM $file_url AS row
CALL {
WITH row
MERGE (n:`{label}` {`{unique_column}`: row.`{unique_column}`})
WITH n, row
UNWIND $properties AS prop
SET n[prop] = row[prop]
} IN TRANSACTIONS OF 1000 ROWS
"""
# 构建完整的文件URL(相对于Neo4j的import目录)
file_url = f"file:///{source_file}"
params = {
"file_url": file_url,
"label": label,
"unique_column": unique_column,
"properties": property_list
}
neo4j_query(query, params)
此函数使用Neo4j的LOAD CSV语法从指定文件加载数据。它通过MERGE子句确保节点的唯一性,并使用UNWIND循环为节点设置所有指定的属性。整个过程以每1000行为一个批次进行,以处理大型文件。
3. 导入节点
对于每个CSV文件,我们需要按顺序调用上述两个函数:先创建唯一性约束,然后加载节点。
代码实现:
def import_nodes(construction_rule):
# 1. 创建唯一性约束
create_uniqueness_constraint(
label=construction_rule['label'],
unique_property_key=construction_rule['unique_column']
)
# 2. 从CSV加载节点
load_nodes_from_csv(
source_file=construction_rule['source_file'],
label=construction_rule['label'],
unique_column=construction_rule['unique_column'],
property_list=construction_rule['property_list']
)
此函数封装了节点导入的完整流程,直接从构建计划规则中提取所需参数。
4. 从CSV文件加载关系
节点导入完成后,我们可以开始建立节点之间的关系。关系本身不需要唯一性约束,其唯一性由两端的节点保证。
代码实现:
def load_relationships_from_csv(source_file, rel_type, from_label, from_column, to_label, to_column, property_list):
query = """
LOAD CSV WITH HEADERS FROM $file_url AS row
CALL {
WITH row
MATCH (from_node:`{from_label}` {`{from_column}`: row.`{from_column}`})
MATCH (to_node:`{to_label}` {`{to_column}`: row.`{to_column}`})
MERGE (from_node)-[r:`{rel_type}`]->(to_node)
WITH r, row
UNWIND $properties AS prop
SET r[prop] = row[prop]
} IN TRANSACTIONS OF 1000 ROWS
"""
file_url = f"file:///{source_file}"
params = {
"file_url": file_url,
"rel_type": rel_type,
"from_label": from_label,
"from_column": from_column,
"to_label": to_label,
"to_column": to_column,
"properties": property_list
}
neo4j_query(query, params)
此函数首先匹配CSV行中指定的“起始”节点和“目标”节点,然后使用MERGE创建它们之间的指定类型的关系。如果关系已存在,MERGE不会重复创建。同样,它也支持为关系设置属性。
5. 构建主图
最后,我们将所有工具整合到一个主函数中,它接收完整的构建计划,并确保以正确的顺序执行:先导入所有节点,再创建所有关系。
代码实现:
def construct_main_graph(construction_plan):
# 第一阶段:导入所有节点
for rule in construction_plan:
if rule['construction_type'] == 'node':
import_nodes(rule)
# 第二阶段:创建所有关系
for rule in construction_plan:
if rule['construction_type'] == 'relationship':
load_relationships_from_csv(
source_file=rule['source_file'],
rel_type=rule['relationship_type'],
from_label=rule['from_label'],
from_column=rule['from_column'],
to_label=rule['to_label'],
to_column=rule['to_column'],
property_list=rule.get('property_list', [])
)
执行与验证
工具定义完成后,我们可以使用一个具体的构建计划来运行construct_main_graph函数。假设我们有以下构建计划(具体内容取决于之前智能体的决策):
construction_plan = [
# 节点构建规则示例
{'construction_type': 'node', 'label': 'Product', 'source_file': 'products.csv', ...},
{'construction_type': 'node', 'label': 'Part', 'source_file': 'parts.csv', ...},
# 关系构建规则示例
{'construction_type': 'relationship', 'relationship_type': 'CONTAINS', 'from_label': 'Product', 'to_label': 'Assembly', ...},
{'construction_type': 'relationship', 'relationship_type': 'PART_OF', 'from_label': 'Part', 'to_label': 'Assembly', ...},
{'construction_type': 'relationship', 'relationship_type': 'SUPPLIED_BY', 'from_label': 'Part', 'to_label': 'Supplier', ...},
]
运行construct_main_graph(construction_plan)后,为了验证图谱是否按预期构建,我们可以执行一个Cypher查询来采样检查每种关系类型是否至少存在一个实例。
验证查询:
// 提取所有关系构建规则中的关系类型
WITH $construction_rules AS rules
UNWIND rules AS rule
// 对每种关系类型,在图中查找一个匹配的模式
CALL {
WITH rule
MATCH (from_node)-[r]->(to_node)
WHERE type(r) = rule.relationship_type
RETURN labels(from_node) AS from_labels, type(r) AS rel_type, labels(to_node) AS to_labels
LIMIT 1
}
RETURN from_labels, rel_type, to_labels
执行此查询后,如果输出包含如 (['Product'], 'CONTAINS', ['Assembly'])、(['Part'], 'PART_OF', ['Assembly']) 和 (['Part'], 'SUPPLIED_BY', ['Supplier']) 这样的结果,则证明图谱已成功按照构建计划创建。
总结


本节课中,我们一起学习了知识图谱构建工具的具体实现。我们定义了一系列工具函数,包括创建数据库约束、从CSV文件导入节点和关系,并将它们整合到一个主构建函数中。通过执行构建计划并运行验证查询,我们确认了知识图谱已根据规范成功构建。这个过程展示了如何将高级的构建计划转化为可执行的数据导入操作,是智能体工作流中关键的执行步骤。
011:知识图谱构建 – 第二部分 🧠



在本节课中,我们将继续知识图谱的构建工作。上一节课我们根据构建计划,从CSV文件创建了领域图。现在,我们将处理Markdown文件,将其分块成词汇图,并将提取的实体整合到主题图中。
概述


本节课我们将学习如何使用Neo4j GraphRAG库进行文本分块和实体提取,并了解实体解析的技术。这并非工作流中的智能体部分,整个过程将由工具和辅助函数完成。我们将定义两个核心工具:一个用于构建知识图谱,另一个用于关联主题节点和领域节点。

环境设置与数据准备
首先,我们需要导入必要的库并完成环境设置。



# 导入所需库
import ...
确保OpenAI和Neo4j环境已准备就绪。由于Neo4j在加载本笔记本时是全新的,我们需要加载上一节课创建的产品节点。我们有一个辅助函数 load_product_nodes 来完成此任务。


# 加载产品节点
load_product_nodes()
接下来,我们需要加载工作流前序部分创建的初始状态,包括构建计划、已批准的文件、已批准的实体和事实类型。
# 加载初始状态
construction_plan = load_construction_plan()
approved_files = load_approved_files()
approved_entities = load_approved_entities()
approved_fact_types = load_approved_fact_types()

知识图谱构建管道

现在,我们可以开始定义处理Markdown文件、进行分块和实体提取所需的所有函数。Neo4j GraphRAG库提供了一个便捷的 SimpleKGPipeline 类来完成这些处理。
了解SimpleKGPipeline接口



SimpleKGPipeline 需要以下组件:
- LLM:用于实体提取。
- Neo4j驱动:用于将图写入数据库。
- 嵌入模型:用于计算文本块的向量嵌入。
- PDF加载器:我们将使用自定义的Markdown加载器。
- 文本分割器:我们将使用自定义的分割器。
- 模式:基于上一节课提出的实体和事实类型。
- 提示词:指导LLM如何进行提取。
以下是创建管道实例的示例代码结构:


pipeline = SimpleKGPipeline(
llm=llm_instance,
driver=neo4j_driver,
embedder=embedder_instance,
pdf_loader=custom_markdown_loader,
text_splitter=custom_text_splitter,
schema=extraction_schema,
prompt=custom_prompt
)


端到端工作流



让我们了解一下Neo4j KG构建器的端到端工作流,以帮助理解这些组件的作用:
- 文档加载:使用自定义的Markdown加载器加载每个文档。
- 文本分块:文本分割器组件执行分块。
- 嵌入计算:为每个文本块计算向量嵌入。
- 实体与关系提取:LLM根据实体和关系提取器的配置分析每个块。
- 图构建:在内存中构建图。
- 图修剪(可选):使用图修剪器清理图。
- 图写入:KG写入器组件将内存中的图保存到Neo4j。
- 实体解析:实体解析器组件合并可能是同一实体的节点。



定义自定义组件

现在,我们可以开始定义需要传入管道的自定义函数。
自定义文本分割器


首先,我们设置一个自定义文本分割器。根据我们Markdown文件的结构(以H1标题开头,用 --- 分隔评论),我们将使用一个简单的正则表达式分割器。

以下是扩展 TextSplitter 基类的实现:


class CustomTextSplitter(TextSplitter):
def run(self, text, regex_pattern):
# 使用正则表达式分割文本
splits = re.split(regex_pattern, text)
# 将分割结果转换为TextChunk对象列表
text_chunks = [TextChunk(text=split, index=i) for i, split in enumerate(splits)]
return text_chunks
自定义Markdown数据加载器
类似地,我们将创建一个自定义的数据加载器来加载Markdown文件。它将扩展 DataLoaderBase 类,并提取文档标题作为元数据。


以下是 MarkdownDataLoader 的实现:
class MarkdownDataLoader(DataLoaderBase):
def extract_title(self, text):
# 使用正则表达式提取第一个H1标题作为文档标题
match = re.search(r'^# (.+)$', text, re.MULTILINE)
return match.group(1) if match else "Untitled"
def run(self, file_path):
# 从文件系统加载Markdown文本
with open(file_path, 'r') as f:
markdown_text = f.read()
# 提取标题
title = self.extract_title(markdown_text)
# 创建包含元数据和文本的DocumentInfo对象
doc_info = DocumentInfo(metadata={'title': title})
pdf_doc = PDFDocument(text=markdown_text)
return doc_info, pdf_doc

配置LLM、嵌入模型和驱动
我们需要为知识图谱构建管道设置LLM、嵌入模型和Neo4j驱动。我们将使用OpenAI。
# 创建LLM和嵌入模型实例
llm = OpenAILanguageModel(api_key=openai_api_key)
embedder = OpenAIEmbedder(api_key=openai_api_key)
# 从GDB单例获取Neo4j驱动
driver = get_neo4j_driver()







配置实体提取上下文


接下来,我们转向配置知识图谱实体提取所需的上下文,从实体模式开始。



定义实体模式
Neo4j GraphRAG包所需的实体模式包含几个部分:节点类型、关系类型和模式模式。

# 节点类型(来自已批准的实体)
node_types = approved_entities
# 关系类型(从已批准的事实类型中提取键)
relationship_types = list(approved_fact_types.keys())
# 模式模式(重新打包已批准的事实类型信息)
schema_patterns = []
for fact in approved_fact_types.values():
pattern = (fact['subject_label'], fact['predicate_label'].upper(), fact['object_label'])
schema_patterns.append(pattern)
# 完整的模式
kg_schema = {
'node_types': node_types,
'relationship_types': relationship_types,
'patterns': schema_patterns,
'strict_mode': True # 仅使用定义的类型,不添加新内容
}
创建自定义提示词



我们将创建一个自定义提示词,它会注入每个文本块的内容、模式以及文件级别的上下文。


首先,定义一个辅助函数来获取文件上下文(例如文件标题和简介)。

def get_file_context(file_text):
# 获取文件的前几行作为上下文
lines = file_text.split('\n')[:5]
return '\n'.join(lines)


然后,定义提示词模板。它包含LLM的角色和目标、设计说明、输出格式规范、模式定义以及文件上下文。
def create_contextualized_prompt(file_context):
prompt_template = """
角色:你是一个知识提取专家。
目标:从提供的文本块中提取实体和关系,并遵循给定的模式。
设计说明:仅提取模式中定义的实体和关系类型。为每个节点生成唯一ID。
输出格式:以JSON格式输出,包含节点列表(含id、标签和属性)和关系列表。
模式定义:
{schema_definition}
文件上下文:
{file_context}
文本块:
{chunk_text}
"""
# 在实际使用中,schema_definition和chunk_text会被动态注入
return prompt_template
构建并运行知识图谱构建器
配置好所有上下文和辅助函数后,我们可以创建知识图谱构建器并运行它。
创建知识图谱构建器辅助函数
由于我们要为每个文件创建一个知识图谱构建器(以便拥有基于文件内容的专门提示词),我们定义一个辅助函数。
def make_kg_builder(file_path):
# 获取文件级别的上下文
file_context = get_file_context_from_path(file_path)
# 创建上下文化的提示词
contextualized_prompt = create_contextualized_prompt(file_context)
# 创建SimpleKGPipeline实例
pipeline = SimpleKGPipeline(
llm=llm,
driver=driver,
embedder=embedder,
pdf_loader=MarkdownDataLoader(),
text_splitter=CustomTextSplitter(regex_pattern=r'\n---\n'),
schema=kg_schema,
prompt=contextualized_prompt
)
return pipeline

处理所有Markdown文件

现在,我们可以遍历所有已批准的文件,为每个文件创建构建器并运行处理。

for file_info in approved_files:
file_path = file_info['path']
print(f"正在处理文件: {file_path}")
# 创建知识图谱构建器
kg_builder = make_kg_builder(file_path)
# 运行构建器
kg_builder.run(file_path)



此循环完成后,我们将得到一个完整的词汇图(包含相互连接且连接到文档节点的文本块)和一个主题图(包含所有提取的实体及其关系)。

关联主题图与领域图

现在我们已经处理了所有文件,拥有了词汇图、主题图和领域图。然而,图谱还不完整,因为主题图和领域图没有连接。领域图是从CSV文件创建的,而主题图是从Markdown文件中提取数据创建的。
下一步是将我们从Markdown文件中提取的实体(主题图)与领域图连接起来。我们将定义一些工具来完成这个任务。

实体解析策略
对于主题图中的每种实体类型,我们将设计一种策略来与领域图中的正确节点关联。例如,主题图中具有产品名称的产品应与领域图中的产品关联。


为了实现这一点,我们需要做以下几件事:
- 查找主题图中所有唯一的实体标签。
- 查找领域图中所有唯一的节点标签。
- 尝试关联这两组标签之间的属性键,以确定如何匹配实体。
查找主题图中的唯一实体标签

首先,让我们查看主题图,看看Neo4j GraphRAG库处理后节点是什么样子。结果节点将带有一个额外的标签 __entity__label 来标识它们。


我们可以运行一个Cypher查询来查找不同的实体标签。


MATCH (n)
WHERE any(label IN labels(n) WHERE label CONTAINS '__entity__')
UNWIND labels(n) AS entity_label
RETURN DISTINCT entity_label


为了过滤掉下划线开头的内部标签(如 __entity__ 和 __kg_builder__),我们可以优化查询:

MATCH (n)
WHERE any(label IN labels(n) WHERE label CONTAINS '__entity__')
UNWIND labels(n) AS entity_label
WHERE NOT entity_label STARTS WITH '_'
RETURN DISTINCT entity_label
我们将这个查询包装成一个辅助函数 get_unique_entity_labels。


查找实体和领域节点的属性键



接下来,对于每个唯一的实体标签,我们还需要查找这些标签拥有的唯一属性键。



以下是查找主题图中某个实体标签唯一键的辅助函数:



def get_unique_entity_keys(entity_label):
query = """
MATCH (n:`%s`)
WHERE any(label IN labels(n) WHERE label CONTAINS '__entity__')
UNWIND keys(n) AS key
RETURN COLLECT(DISTINCT key) AS unique_keys
""" % entity_label
result = driver.run(query)
return result.data()[0]['unique_keys']



类似地,我们定义一个函数来查找领域图中某个标签的唯一属性键:
def get_unique_domain_keys(domain_label):
query = """
MATCH (n:`%s`)
WHERE NOT any(label IN labels(n) WHERE label CONTAINS '__entity__')
UNWIND keys(n) AS key
RETURN COLLECT(DISTINCT key) AS unique_keys
""" % domain_label
result = driver.run(query)
return result.data()[0]['unique_keys']
规范化属性键并关联键值对


为了便于比较,我们定义一个辅助函数来规范化属性键(例如,小写、去除空格、移除标签前缀)。
def normalize_key(label, key):
key = key.lower().strip()
# 如果键以标签前缀开头,则移除它
prefix = label.lower() + ' '
if key.startswith(prefix):
key = key[len(prefix):]
return key
然后,我们定义一个函数来关联给定标签的键。它使用 rapidfuzz 库计算文本相似度得分,并将高度相关的键配对。
from rapidfuzz import fuzz
def correlate_keys(label, entity_keys, domain_keys, similarity_threshold=0.9):
correlated_keys = []
for e_key in entity_keys:
for d_key in domain_keys:
norm_e_key = normalize_key(label, e_key)
norm_d_key = normalize_key(label, d_key)
# 计算相似度得分 (0-100),并转换为0-1的范围
similarity = fuzz.ratio(norm_e_key, norm_d_key) / 100.0
if similarity > similarity_threshold:
correlated_keys.append((e_key, d_key, similarity))
# 按相似度排序
correlated_keys.sort(key=lambda x: x[2], reverse=True)
return correlated_keys

使用Jaro-Winkler距离进行实体解析


最后,我们使用Cypher的 apoc.text.jaroWinklerDistance 函数,基于相似属性值来执行实体解析。该函数计算两个字符串之间的编辑距离,得分在0到1之间(0表示完全匹配)。

以下是一个Cypher查询示例,它查找产品实体中 name 和 product_name 属性值高度匹配的节点对,并创建 CORRESPONDS_TO 关系。

MATCH (entity:Product)
WHERE any(label IN labels(entity) WHERE label CONTAINS '__entity__')
MATCH (domain:Product)
WHERE NOT any(label IN labels(domain) WHERE label CONTAINS '__entity__')
WITH entity, domain,
apoc.text.jaroWinklerDistance(entity.name, domain.product_name) AS score
WHERE score < 0.1
MERGE (entity)-[r:CORRESPONDS_TO]->(domain)
ON CREATE SET r.created_at = timestamp()
ON MATCH SET r.updated_at = timestamp()
RETURN count(r) AS relationships_created
我们将这个查询包装成一个函数 resolve_entities。

连接所有实体


为了完整性,我们遍历所有唯一的实体标签,尝试将主题节点与相应的领域节点关联起来。


unique_entity_labels = get_unique_entity_labels()
for label in unique_entity_labels:
entity_keys = get_unique_entity_keys(label)
domain_keys = get_unique_domain_keys(label)
key_pairs = correlate_keys(label, entity_keys, domain_keys)
if key_pairs:
# 使用最相关的键对进行解析
best_entity_key, best_domain_key, _ = key_pairs[0]
resolve_entities(label, best_entity_key, best_domain_key, threshold=0.1)



总结


在本节课中,我们一起完成了知识图谱构建的第二部分。我们学习了:


- 配置知识图谱构建管道:使用Neo4j GraphRAG库的
SimpleKGPipeline,并为其定制了Markdown加载器、文本分割器、提取模式和提示词。 - 处理非结构化文本:遍历Markdown文件,进行分块、嵌入计算和实体/关系提取,从而构建了词汇图和主题图。
- 执行实体解析:通过比较主题图和领域图中实体的属性键和属性值,使用字符串相似度算法(如Jaro-Winkler距离)将两个图中的相同实体关联起来,创建了
CORRESPONDS_TO关系。

最终,我们成功地将从CSV文件构建的领域图、从Markdown文件构建的词汇图和主题图连接起来,形成了一个完整且互联的知识图谱。
012:结论
在本节课中,我们将回顾并总结整个课程的核心内容,了解如何构建一个多智能体系统来将结构化与非结构化数据转化为知识图谱。
课程概述
在本课程中,我们学习了如何构建一个多智能体系统,该系统能够将您的结构化与非结构化数据转化为知识图谱。


核心学习内容回顾
上一节我们介绍了系统的整体流程,本节中我们来总结构建过程中的关键环节。
以下是构建多智能体系统的核心步骤:
- 设置系统中的每个专用智能体:您学习了如何为系统中的每个智能体进行配置。
- 设计提示词与定义工具:您学习了应使用何种提示词,以及如何定义和分配工具给各个智能体。
- 在智能体间共享上下文:您学习了如何在不同的智能体之间有效地共享和传递上下文信息。
总结
本节课中,我们一起学习了构建一个用于知识图谱生成的多智能体系统的完整流程。从设置专用智能体、设计提示词与工具,到实现智能体间的上下文共享,您已经掌握了将各类数据转化为结构化知识图谱的关键技术。
恭喜您完成本课程,期待看到您运用所学知识构建出自己的项目。

浙公网安备 33010602011771号