代码已发布在 GitHub(https://github.com/) 上。
我们将使用 Neo4j 作为底层图存储,它自带图形可视化功能。最简单的开始方式是使用免费的 Neo4j Aura 实例,它提供了 Neo4j 数据库的云实例。或者,你也可以通过下载 Neo4j Desktop 应用程序并创建本地数据库实例来设置本地环境。
fromlangchain_community.graphsimportNeo4jGraph
graph=Neo4jGraph(
url="bolt://54.87.130.140:7687",
username="neo4j",
password="cables-anchors-directories",
refresh_schema=False
)LLM 图谱转换器旨在提供一个灵活的框架,用于使用任何 LLM 构建图谱。由于目前有许多不同的提供商和模型,这项任务并不简单。幸运的是,LangChain 处理了许多标准化流程。至于 LLM 图谱转换器本身,它就像两种不同能力的工具结合在一起——能够在两种完全独立的模式下运行。
LLM 图谱转换器有两种模式,每种模式都旨在使用 LLM 在不同场景下从文档中生成图谱:
1.基于工具的模式(默认)
当 LLM 支持结构化输出或函数调用时,该模式利用 LLM 内置的with_structured_output方法来使用工具。工具规范定义了输出格式,确保以结构化、预定义的方式提取实体和关系。这在图的左侧显示了 Node 和 Relationship 类的代码。
2.基于提示的模式(后备)
在 LLM 不支持工具或函数调用的情况下,LLM 图谱转换器会退回到纯粹基于提示的方式。此模式使用少样本提示(few-shot prompting)来定义输出格式,引导 LLM 以文本为基础的方式提取实体和关系。然后通过自定义函数解析结果,将 LLM 的输出转换为 JSON 格式。此 JSON 用于填充节点和关系,与基于工具的模式类似,但这里完全由提示而非结构化工具引导。这在图的右侧显示了一个示例提示和结果 JSON 输出。
这两种模式确保了 LLM 图谱转换器能够适应不同的 LLM,无论是直接使用工具还是通过解析基于文本的提示输出来构建图谱。
注意:即使是支持工具或函数的模型,也可以通过设置属性_ignore_tools_usage=True来使用基于提示的提取模式。
我们最初选择了基于工具的提取方法,因为它减少了对大量提示工程和自定义解析函数的需求。在 LangChain 中,with_structured_output方法允许你使用工具或函数提取信息,输出通过 JSON 结构或 Pydantic 对象定义。我个人认为 Pydantic 对象更清晰,因此我们选择了它。
首先,我们定义一个Node类:
classNode(BaseNode):
id:str=Field(...,description="Nameorhuman-readableuniqueidentifier")
label:str=Field(...,description=f"Availableoptionsare{enum_values}")
properties:Optional[List[Property]]每个节点都有一个id(唯一标识符)、一个label(标签)和可选的properties(属性)。为了简洁,这里没有包括完整的描述。将 id 描述为人类可读的唯一标识符非常重要,因为一些 LLM 倾向于以更传统的方式理解 ID 属性,比如随机字符串或递增整数。而我们希望将实体名称用作 id 属性。我们还通过在label描述中列出可用标签类型来限制标签类型。此外,像 OpenAI 的模型一样,LLM 支持一个enum参数,我们也使用了它。
接下来,我们定义Relationship类:
classRelationship(BaseRelationship):
source_node_id:str
source_node_label:str=Field(...,description=f"Availableoptionsare{enum_values}")
target_node_id:str
target_node_label:str=Field(...,description=f"Availableoptionsare{enum_values}")
type:str=Field(...,description=f"Availableoptionsare{enum_values}")
properties:Optional[List[Property]]这是Relationship类的第二个版本。最初,我们为源节点和目标节点使用了嵌套的Node对象,但我们很快发现嵌套对象降低了提取过程的准确性和质量。因此,我们决定将源节点和目标节点展平为单独的字段,例如source_node_id和source_node_label,以及target_node_id和target_node_label。此外,我们在节点标签和关系类型的描述中定义了允许的值,以确保 LLM 遵循指定的图谱模式。
基于工具的提取方法使我们能够为节点和关系定义属性。以下是我们用于定义它们的类。
classProperty(BaseModel):
"""Asinglepropertyconsistingofkeyandvalue"""
key:str=Field(...,description=f"Availableoptionsare{enum_values}")
value:str每个Property都被定义为一个键值对。虽然这种方法很灵活,但也有其局限性。例如,我们无法为每个属性提供唯一的描述,也无法指定某些属性为必填而其他为可选,因此所有属性都被定义为可选。此外,属性并未为每种节点或关系类型单独定义,而是被所有类型共享。
我们还实现了一个详细的系统提示词,帮助指导提取过程。不过,根据我的经验,函数和参数的描述比系统消息对提取结果的影响更大。
目前,LLM Graph Transformer 中尚无简单方法来自定义函数或参数描述。
由于仅有少数商业化的 LLM 和 LLaMA 3 支持原生工具,我们为不支持工具的模型实现了一个备用方案。即使使用支持工具的模型,也可以通过设置ignore_tool_usage=True来切换到基于提示词的方法。
大部分基于提示词的提示工程和示例由 Geraldus Wilsen 提供。
在基于提示词的方法中,我们必须直接在提示词中定义输出结构。完整的提示词可以在这里找到。在本文中,我们只进行高层概述。我们首先定义系统提示词。
Youareatop-tieralgorithmdesignedforextractinginformationinstructuredformatstobuildaknowledgegraph.YourtaskistoidentifytheentitiesandrelationsspecifiedintheuserpromptfromagiventextandproducetheoutputinJSONformat.ThisoutputshouldbealistofJSONobjects,witheachobjectcontainingthefollowingkeys:
-**"head"**:Thetextoftheextractedentity,whichmustmatchoneofthetypesspecifiedintheuserprompt.
-**"head_type"**:Thetypeoftheextractedheadentity,selectedfromthespecifiedlistoftypes.
-**"relation"**:Thetypeofrelationbetweenthe"head"andthe"tail,"chosenfromthelistofallowedrelations.
-**"tail"**:Thetextoftheentityrepresentingthetailoftherelation.
-**"tail_type"**:Thetypeofthetailentity,alsoselectedfromtheprovidedlistoftypes.
Extractasmanyentitiesandrelationshipsaspossible.
**EntityConsistency**:Ensureconsistencyinentityrepresentation.Ifanentity,like"JohnDoe,"appearsmultipletimesinthetextunderdifferentnamesorpronouns(e.g.,"Joe,""he"),usethemostcompleteidentifierconsistently.Thisconsistencyisessentialforcreatingacoherentandeasilyunderstandableknowledgegraph.
**ImportantNotes**:
-Donotaddanyextraexplanationsortext.在基于提示词的方法中,一个关键区别在于我们要求 LLM 仅提取关系,而不是单独的节点。这意味着我们不会有孤立节点,而工具方法中可能会有。此外,由于缺乏原生工具支持的模型通常表现较差,我们不允许提取任何属性(无论是节点还是关系的属性),以简化提取输出。
接下来,我们为模型添加了一些少样本学习示例。
examples=[
{
"text"
"AdamisasoftwareengineerinMicrosoftsince2009,"
"andlastyearhegotanawardastheBestTalent"
),
"head":"Adam",
"head_type":"Person",
"relation":"WORKS_FOR",
"tail":"Microsoft",
"tail_type":"Company",
},
{
"text"
"AdamisasoftwareengineerinMicrosoftsince2009,"
"andlastyearhegotanawardastheBestTalent"
),
"head":"Adam",
"head_type":"Person",
"relation":"HAS_AWARD",
"tail":"BestTalent",
"tail_type":"Award",
},
...
]在这种方法中,目前不支持添加自定义的少样本学习示例或额外指令。唯一的自定义方式是通过prompt属性修改整个提示词。扩展自定义选项是我们正在积极考虑的方向。
接下来,我们将研究如何定义图谱模式。
在使用 LLM Graph Transformer 进行信息提取时,定义一个图谱模式对于指导模型构建有意义的结构化知识表示至关重要。一个定义良好的图谱模式会指定要提取的节点和关系类型,以及每个类型相关的属性。这种模式充当蓝图,确保 LLM 按照所需的知识图谱结构一致地提取相关信息。
在本文中,我们将使用玛丽·居里(Marie Curie)维基百科页面的开头段落进行测试,并在结尾添加一段关于罗宾·威廉姆斯(Robin Williams)的句子。
fromlangchain_core.documentsimportDocument
text="""
MarieCurie,7November1867–4July1934,wasaPolishandnaturalised-Frenchphysicistandchemistwhoconductedpioneeringresearchonradioactivity.
ShewasthefirstwomantowinaNobelPrize,thefirstpersontowinaNobelPrizetwice,andtheonlypersontowinaNobelPrizeintwoscientificfields.
Herhusband,PierreCurie,wasaco-winnerofherfirstNobelPrize,makingthemthefirst-evermarriedcoupletowintheNobelPrizeandlaunchingtheCuriefamilylegacyoffiveNobelPrizes.
Shewas,in1906,thefirstwomantobecomeaprofessorattheUniversityofParis.
Also,RobinWilliams.
"""
documents=[Document(page_content=text)]我们将在所有示例中使用 GPT-4o。
fromlangchain_openaiimportChatOpenAI
importgetpass
importos
os.environ["OPENAI_API_KEY"]=getpass.getpass("OpenAIapikey")
llm=ChatOpenAI(model='gpt-4o')首先,让我们看看在没有定义任何图谱模式的情况下,提取过程是如何进行的。
fromlangchain_experimental.graph_transformersimportLLMGraphTransformer
no_schema=LLMGraphTransformer(llm=llm)现在,我们可以使用异步的aconvert_to_graph_documents函数处理文档。推荐使用异步方式进行 LLM 提取,因为它允许并行处理多个文档。这种方法可以显著减少等待时间,并提高处理多个文档时的吞吐量。
data=awaitno_schema.aconvert_to_graph_documents(documents)
LLM Graph Transformer 的响应将是一个图谱文档,其结构如下:
[
GraphDocument(
nodes=[
Node(id="MarieCurie",type="Person",properties={}),
Node(id="PierreCurie",type="Person",properties={}),
Node(id="NobelPrize",type="Award",properties={}),
Node(id="UniversityOfParis",type="Organization",properties={}),
Node(id="RobinWilliams",type="Person",properties={}),
],
relationships=[
Relationship(
source=Node(id="MarieCurie",type="Person",properties={}),
target=Node(id="NobelPrize",type="Award",properties={}),
type="WON",
properties={},
),
Relationship(
source=Node(id="MarieCurie",type="Person",properties={}),
target=Node(id="NobelPrize",type="Award",properties={}),
type="WON",
properties={},
),
Relationship(
source=Node(id="MarieCurie",type="Person",properties={}),
target=Node(
id="UniversityOfParis",type="Organization",properties={}
),
type="PROFESSOR",
properties={},
),
Relationship(
source=Node(id="PierreCurie",type="Person",properties={}),
target=Node(id="NobelPrize",type="Award",properties={}),
type="WON",
properties={},
),
],
source=Document(
metadata={"id":"de3c93515e135ac0e47ca82a4f9b82d8"},
page_content="\nMarieCurie,7November1867–4July1934,wasaPolishandnaturalised-Frenchphysicistandchemistwhoconductedpioneeringresearchonradioactivity.\nShewasthefirstwomantowinaNobelPrize,thefirstpersontowinaNobelPrizetwice,andtheonlypersontowinaNobelPrizeintwoscientificfields.\nHerhusband,PierreCurie,wasaco-winnerofherfirstNobelPrize,makingthemthefirst-evermarriedcoupletowintheNobelPrizeandlaunchingtheCuriefamilylegacyoffiveNobelPrizes.\nShewas,in1906,thefirstwomantobecomeaprofessorattheUniversityofParis.\nAlso,RobinWilliams!\n",
),
)
]该图谱文档描述了提取的nodes和relationships。此外,提取的源文档被添加到了source键下。
我们可以使用 Neo4j 浏览器可视化输出,从而更清晰直观地理解数据。
上图展示了两次提取玛丽·居里段落的结果。在这种情况下,我们使用了带有工具提取的 GPT-4,它也允许孤立节点,如图所示。由于未定义图谱模式,LLM 在运行时决定提取哪些信息,这可能导致即使是同一段落的输出也有所不同。因此,一些提取结果比其他结果更详细,结构可能有所不同。例如,在左侧,玛丽被表示为诺贝尔奖的WINNER,而在右侧,她WON诺贝尔奖。
接下来,让我们尝试使用基于提示词的方法进行相同的提取。对于支持工具的模型,可以通过设置ignore_tool_usage参数启用基于提示词的提取。
no_schema_prompt=LLMGraphTransformer(llm=llm,ignore_tool_usage=True)
data=awaitno_schema.aconvert_to_graph_documents(documents)同样,我们可以在 Neo4j 浏览器中可视化两次单独的执行结果。
使用基于提示词的方法,我们不会看到任何孤立节点。然而,与之前的提取一样,模式可能因运行而异,导致相同输入的输出有所不同。
接下来,让我们看看如何通过定义图谱模式来帮助生成更一致的输出。
约束提取的图谱结构可以极大地提高一致性,因为它指导模型专注于特定的相关实体和关系。通过定义清晰的模式,可以提高提取的一致性,使输出更可预测并与实际需求对齐。这减少了运行之间的变化,并确保提取的数据遵循标准化结构,捕获预期的信息。通过一个定义良好的模式,模型不太可能忽略关键细节或引入意外元素,从而生成更简洁、更可用的图谱。
我们将从使用allowed_nodes参数定义要提取的节点类型开始。
allowed_nodes=["Person","Organization","Location","Award","ResearchField"]
nodes_defined=LLMGraphTransformer(llm=llm,allowed_nodes=allowed_nodes)
data=awaitallowed_nodes.aconvert_to_graph_documents(documents)在这里,我们定义了 LLM 应提取的五种节点类型,例如Person_、_Organization_、_Location等。我们在 Neo4j 浏览器中可视化两次单独的执行结果以进行比较。
通过指定预期的节点类型,我们实现了更一致的节点提取。然而,仍可能发生一些变化。例如,在第一次运行中,“放射性”被提取为研究领域,而在第二次运行中则没有。
由于我们尚未定义关系,它们的类型在运行之间也可能有所不同。此外,一些提取可能捕获的信息比其他提取更多。例如,玛丽和皮埃尔之间的MARRIED_TO关系并未在两次提取中都出现。
接下来,让我们探讨如何通过定义关系类型进一步提高一致性。
如前所述,仅定义节点类型仍允许关系提取的变化。为了解决这个问题,让我们探讨如何定义关系。第一种方法是使用可用类型的列表来指定允许的关系。
allowed_nodes=["Person","Organization","Location","Award","ResearchField"]
allowed_relationships=["SPOUSE","AWARD","FIELD_OF_RESEARCH","WORKS_AT","IN_LOCATION"]
rels_defined=LLMGraphTransformer(
llm=llm,
allowed_nodes=allowed_nodes,
allowed_relationships=allowed_relationships
)
data=awaitrels_defined.aconvert_to_graph_documents(documents)再次检查两次单独提取的结果。
通过同时定义节点和关系类型,我们的输出变得显著更一致。例如,玛丽始终被显示为获奖者、皮埃尔的配偶以及巴黎大学的教授。然而,由于关系被定义为通用列表,而没有限制它们可以连接的节点类型,因此仍可能发生一些变化。例如,FIELD_OF_RESEARCH关系有时可能出现在Person和ResearchField之间,而有时可能连接Award和ResearchField。此外,由于未定义关系方向,方向的一致性可能会有所不同。
为了解决无法指定关系连接的节点类型以及强制关系方向的问题,我们最近引入了一种新的关系定义选项,如下所示。
allowed_nodes=["Person","Organization","Location","Award","ResearchField"]
allowed_relationships=[
("Person","SPOUSE","Person"),
("Person","AWARD","Award"),
("Person","WORKS_AT","Organization"),
("Organization","IN_LOCATION","Location"),
("Person","FIELD_OF_RESEARCH","ResearchField")
]
rels_defined=LLMGraphTransformer(
llm=llm,
allowed_nodes=allowed_nodes,
allowed_relationships=allowed_relationships
)
data=awaitrels_defined.aconvert_to_graph_documents(documents)与将关系定义为简单的字符串列表不同,我们现在使用三元组格式,其中元素分别表示源节点、关系类型和目标节点。
让我们再次可视化结果。
使用三元组方法为提取的图谱提供了更一致的模式。然而,鉴于 LLM 的特性,提取的细节级别可能仍会有所变化。例如,在右侧,皮埃尔被显示为诺贝尔奖的获奖者,而在左侧,这一信息缺失。
对图谱模式的最终增强是为节点和关系定义属性。我们有两种选择。第一种是将node_properties或relationship_properties设置为true,允许 LLM 自主决定提取哪些属性。
allowed_nodes=["Person","Organization","Location","Award","ResearchField"]
allowed_relationships=[
("Person","SPOUSE","Person"),
("Person","AWARD","Award"),
("Person","WORKS_AT","Organization"),
("Organization","IN_LOCATION","Location"),
("Person","FIELD_OF_RESEARCH","ResearchField")
]
node_properties=True
relationship_properties=True
props_defined=LLMGraphTransformer(
llm=llm,
allowed_nodes=allowed_nodes,
allowed_relationships=allowed_relationships,
node_properties=node_properties,
relationship_properties=relationship_properties
)
data=awaitprops_defined.aconvert_to_graph_documents(documents)
graph.add_graph_documents(data)让我们检查结果。
我们允许 LLM 添加其认为相关的任何节点或关系属性。例如,它选择包括玛丽·居里的出生和死亡日期、她在巴黎大学的教授职位以及她两次获得诺贝尔奖的事实。这些额外的属性显著丰富了提取的信息。
第二种选择是定义我们希望提取的节点和关系属性。
allowed_nodes=["Person","Organization","Location","Award","ResearchField"]
allowed_relationships=[
("Person","SPOUSE","Person"),
("Person","AWARD","Award"),
("Person","WORKS_AT","Organization"),
("Organization","IN_LOCATION","Location"),
("Person","FIELD_OF_RESEARCH","ResearchField")
]
node_properties=["birth_date","death_date"]
relationship_properties=["start_date"]
props_defined=LLMGraphTransformer(
llm=llm,
allowed_nodes=allowed_nodes,
allowed_relationships=allowed_relationships,
node_properties=node_properties,
relationship_properties=relationship_properties
)
data=awaitprops_defined.aconvert_to_graph_documents(documents)
graph.add_graph_documents(data)属性被简单地定义为两个列表。让我们看看 LLM 提取了什么。
出生和死亡日期与之前的提取一致。然而,这次,LLM 还提取了玛丽在巴黎大学教授职位的开始日期。
属性确实为提取的信息增加了有价值的深度,但当前的实现存在一些限制:
•属性只能通过基于工具的方法提取。
•所有属性都被提取为字符串。
•属性只能全局定义,无法为每个节点标签或关系类型单独定义。
•没有选项来自定义属性描述,以指导 LLM 进行更精确的提取。
如果你认为我们已经找到了一种让 LLM 完美遵循定义模式的方法,我必须澄清事实。尽管我们在提示工程方面投入了大量努力,但让 LLM,尤其是性能较差的模型,完全准确地遵循指令仍然具有挑战性。为了解决这个问题,我们引入了一个后处理步骤,称为strict_mode,它会删除任何不符合定义图谱模式的信息,确保输出更干净和更一致。
默认情况下,strict_mode设置为True,但你可以通过以下代码禁用它:
LLMGraphTransformer(
llm=llm,
allowed_nodes=allowed_nodes,
allowed_relationships=allowed_relationships,
strict_mode=False
)在禁用严格模式的情况下,你可能会得到超出定义图谱模式的节点或关系类型,因为 LLM 有时会对输出结构进行创造性发挥。
从 LLM Graph Transformer 提取的图谱文档可以通过add_graph_documents方法导入到 Neo4j 等图数据库中,以便进一步分析和应用。我们将探索适合不同用例的多种数据导入选项。
可以使用以下代码将节点和关系导入到 Neo4j。
graph.add_graph_documents(graph_documents)
这种方法直接导入提供的图谱文档中的所有节点和关系。我们在整个博客文章中使用了这种方法来查看不同 LLM 和模式配置的结果。
大多数图数据库支持索引以优化数据导入和检索。在 Neo4j 中,索引只能为特定节点标签设置。由于我们可能无法提前知道所有节点标签,可以通过使用baseEntityLabel参数为每个节点添加一个次要基础标签来解决此问题。这样,我们仍然可以利用索引进行高效导入和检索,而无需为图谱中的每个可能节点标签设置索引。
graph.add_graph_documents(graph_documents,baseEntityLabel=True)
如前所述,使用baseEntityLabel参数会导致每个节点都有一个额外的__Entity__标签。
最后一个选项是导入提取的节点和关系的源文档。这种方法使我们能够跟踪每个实体出现在哪些文档中。可以通过include_source参数导入源文档。
graph.add_graph_documents(graph_documents,include_source=True)
检查导入的图谱后,我们应该看到类似的结果。
在此可视化中,源文档以蓝色突出显示,所有从中提取的实体通过MENTIONS关系连接。这种模式允许构建同时利用结构化和非结构化搜索方法的检索器。
在本文中,我们探讨了 LangChain 的 LLM Graph Transformer 及其构建知识图谱的双重模式。工具模式是我们的主要方法,它利用结构化输出和函数调用,减少提示工程并支持属性提取。而基于提示词的方法在工具不可用时非常有用,它依赖于少样本学习示例来指导 LLM。然而,基于提示词的提取不支持属性提取,也不会生成孤立节点。
我们观察到,定义清晰的图谱模式(包括允许的节点和关系类型)可以提高提取的一致性和性能。受约束的模式有助于确保输出符合所需结构,使其更可预测、可靠和适用。无论是使用工具还是提示词,LLM Graph Transformer 都能更有组织地将非结构化数据转化为结构化表示,从而支持更好的 RAG 应用和多跳查询处理。
代码已在 GitHub (https://github.com/tomasonjo/blogs/blob/master/llm/llm_graph_transformer_in_depth.ipynb)上提供。你还可以通过 Neo4j 的托管LLM Graph Builder应用程序在无代码环境中试用 LLM Graph Transformer。
| 欢迎光临 链载Ai (http://www.lianzai.com/) | Powered by Discuz! X3.5 |