从文本中创建图表是非常令人兴奋的,但无疑也是充满挑战的。本质上,它是将非结构化文本转换为结构化数据的过程。虽然这种方法已经存在了一段时间,但随着大型语言模型(LLM)的出现,它获得了显著的关注,并更加深入人心。
在上面的图片中,你可以看到信息提取如何将原始文本转换为知识图谱。左侧是多个文档,显示了关于个人及其与公司关系的非结构化句子。右侧则是以实体及其连接构成的图谱形式表示相同的信息,展示了谁在哪些组织工作或创立了哪些组织。
但为什么你会从文本中提取结构化信息并将其表示为图谱呢?一个关键原因是支持检索增强生成(RAG)应用。虽然对非结构化文本使用文本嵌入模型是一种有用的方法,但在回答复杂的、需要理解多个实体之间联系的或多跳问题时,或者需要进行过滤、排序和聚合等结构化操作时,这种方法可能会力不从心。通过从文本中提取结构化信息并构建知识图谱,你不仅可以更有效地组织数据,还可以创建一个强大的框架来理解实体之间的复杂关系。这种结构化方法使得检索和利用特定信息变得更加容易,从而能够回答更多类型的问题,同时提供更高的准确性。
设置Neo4j环境
我们将使用Neo4j作为底层图谱存储,它自带了开箱即用的图谱可视化功能。最简单的入门方式是使用Neo4j Aura的免费实例,它提供了Neo4j数据库的云实例。或者,你也可以通过下载Neo4j Desktop应用程序并创建本地数据库实例来设置Neo4j数据库的本地实例。
from langchain_community.graphs import Neo4jGraph
graph = Neo4jGraph(
url="bolt://54.87.130.140:7687",
username="neo4j",
password="cables-anchors-directories",
refresh_schema=False
)
LLM图谱转换器
LLM图谱转换器旨在提供一个灵活的框架,以便使用任何大型语言模型(LLM)来构建图谱。鉴于市场上存在众多不同的提供商和模型,这项任务远非简单。幸运的是,LangChain介入并处理了大部分标准化流程。至于LLM图谱转换器本身,它就像两只猫叠穿在一件风衣里——具备在两种完全独立模式下运行的能力。
LLM图谱转换器有两种不同的工作模式,每种模式都设计用于在不同场景下使用LLM从文档中生成图谱。
这两种模式确保了LLM图谱转换器能够适应不同的LLM,使其能够直接使用工具构建图谱,或者通过解析基于文本的提示的输出来构建图谱。
基于工具的提取
我们最初选择基于工具的方法进行提取,因为它最大限度地减少了对大量提示工程和自定义解析函数的需求。在LangChain中,with_structured_output方法允许你使用工具或函数来提取信息,输出可以通过JSON结构或Pydantic对象来定义。就我个人而言,我发现Pydantic对象更清晰,所以我们选择了它。
我们首先定义一个Node(节点)类。
class Node(BaseNode):
id: str = Field(..., description="Name or human-readable unique identifier")
label: str = Field(..., description=f"Available options are {enum_values}")
properties: Optional[List[Property]]
每个节点都有一个id、一个标签以及可选的属性。为了简洁起见,我在这里没有包含完整的描述。将id描述为人类可读的唯一标识符很重要,因为一些LLM倾向于以更传统的方式理解ID属性,如随机字符串或递增整数。相反,我们希望使用实体的名称作为id属性。我们还通过简单地在label_description中列出它们来限制可用的标签类型。此外,像OpenAI这样的LLM支持枚举参数,我们也使用了这一功能。
接下来,我们来看看Relationship(关系)类。
class Relationship(BaseRelationship):
source_node_id: str
source_node_label: str = Field(..., description=f"Available options are {enum_values}")
target_node_id: str
target_node_label: str = Field(..., description=f"Available options are {enum_values}")
type: str = Field(..., description=f"Available options are {enum_values}")
properties: Optional[List[Property]]
这是Relationship(关系)类的第二次迭代。最初,我们为源节点和目标节点使用了嵌套的Node对象,但很快发现嵌套对象降低了提取过程的准确性和质量。因此,我们决定将源节点和目标节点展平成独立的字段——例如,source_node_id和source_node_label,以及target_node_id和target_node_label。此外,我们还在描述中定义了节点标签和关系类型的允许值,以确保LLM遵循指定的图谱模式。
基于工具的提取方法使我们能够为节点和关系都定义属性。下面是我们用来定义它们的类。
class Property(BaseModel):
"""A single property consisting of key and value"""
key: str = Field(..., description=f"Available options are {enum_values}")
value: str
每个属性都被定义为键值对。虽然这种方法很灵活,但也有其局限性。例如,我们无法为每个属性提供唯一的描述,也无法将某些属性指定为必填项,而其他属性为可选项,因此所有属性都被定义为可选项。此外,属性并不是为每个节点或关系类型单独定义的,而是跨所有节点和关系类型共享的。
我们还实现了一个详细的系统提示来帮助指导提取过程。然而,根据我的经验,函数和参数描述的影响往往比系统消息更大。
不幸的是,目前LLM Graph Transformer中没有简单的方法来定制函数或参数描述。
基于提示的提取
由于只有少数商业LLM和LLaMA 3支持原生工具,我们为不支持工具的模型实现了一个备用方案。即使在使用支持工具的模型时,你也可以通过设置ignore_tool_usage=True来切换到基于提示的方法。
基于提示的方法中的大部分提示工程和示例都是由Geraldus Wilsen贡献的。
在基于提示的方法中,我们必须在提示中直接定义输出结构。在这篇文章中,我们将只做一个高级概述。我们首先定义系统提示。
You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph. Your task is to identify the entities and relations specified in the user prompt from a given text and produce the output in JSON format. This output should be a list of JSON objects, with each object containing the following keys:
- **"head"**: The text of the extracted entity, which must match one of the types specified in the user prompt.
- **"head_type"**: The type of the extracted head entity, selected from the specified list of types.
- **"relation"**: The type of relation between the "head" and the "tail," chosen from the list of allowed relations.
- **"tail"**: The text of the entity representing the tail of the relation.
- **"tail_type"**: The type of the tail entity, also selected from the provided list of types.
Extract as many entities and relationships as possible.
**Entity Consistency**: Ensure consistency in entity representation. If an entity, like "John Doe," appears multiple times in the text under different names or pronouns (e.g., "Joe," "he"), use the most complete identifier consistently. This consistency is essential for creating a coherent and easily understandable knowledge graph.
**Important Notes**:
- Do not add any extra explanations or text.
在基于提示的方法中,一个关键的区别是我们要求LLM只提取关系,而不是单独的节点。这意味着与基于工具的方法不同,我们不会有任何孤立的节点。此外,由于缺乏原生工具支持的模型通常表现较差,我们不允许提取任何属性——无论是节点还是关系的属性,以使提取输出更简单。
接下来,我们向模型中添加了一些少样本示例。
examples = [
{
"text": (
"Adam is a software engineer in Microsoft since 2009, "
"and last year he got an award as the Best Talent"
),
"head": "Adam",
"head_type": "Person",
"relation": "WORKS_FOR",
"tail": "Microsoft",
"tail_type": "Company",
},
{
"text": (
"Adam is a software engineer in Microsoft since 2009, "
"and last year he got an award as the Best Talent"
),
"head": "Adam",
"head_type": "Person",
"relation": "HAS_AWARD",
"tail": "Best Talent",
"tail_type": "Award",
},
...
]
在这种方法中,目前不支持添加自定义的少样本示例或额外指令。唯一的定制方式是通过promptattribute修改整个提示。我们正在积极考虑扩展定制选项。
接下来,我们将看看如何定义图谱模式。
定义图谱模式
当使用LLM Graph Transformer进行信息提取时,定义一个图谱模式对于指导模型构建有意义且结构化的知识表示至关重要。一个定义良好的图谱模式指定了要提取的节点和关系类型,以及与之相关的任何属性。这个模式就像是一个蓝图,确保LLM以与所需知识图谱结构相一致的方式一致地提取相关信息。
在这篇文章中,我们将使用玛丽·居里维基百科页面的开头段落进行测试,并在末尾添加一句关于罗宾·威廉姆斯的句子。
from langchain_core.documents import Document
text = """
Marie Curie, 7 November 1867 – 4 July 1934, was a Polish and naturalised-French physicist and chemist who conducted pioneering research on radioactivity.
She was the first woman to win a Nobel Prize, the first person to win a Nobel Prize twice, and the only person to win a Nobel Prize in two scientific fields.
Her husband, Pierre Curie, was a co-winner of her first Nobel Prize, making them the first-ever married couple to win the Nobel Prize and launching the Curie family legacy of five Nobel Prizes.
She was, in 1906, the first woman to become a professor at the University of Paris.
Also, Robin Williams.
"""
documents = [Document(page_content=text)]
在所有示例中,我们也将使用GPT-4o。
from langchain_openai import ChatOpenAI
import getpass
import os
os.environ["OPENAI_API_KEY"] = getpass.getpass("OpenAI api key")
llm = ChatOpenAI(model='gpt-4o')
首先,让我们来看看在没有定义任何图谱模式的情况下,提取过程是如何工作的。
from langchain_experimental.graph_transformers import LLMGraphTransformer
no_schema = LLMGraphTransformer(llm=llm)
现在我们可以使用aconvert_to_graph_documents函数来处理文档,该函数是异步的。建议在使用LLM提取时使用异步,因为它允许并行处理多个文档。这种方法可以显著减少等待时间并提高吞吐量,尤其是在处理多个文档时。
data = await no_schema.aconvert_to_graph_documents(documents)await no_schema.aconvert_to_graph_documents(documents)
LLM Graph Transformer的响应将是一个图谱文档,其结构如下:
[
GraphDocument(
nodes=[
Node(id="Marie Curie", type="Person", properties={}),id="Marie Curie", type="Person", properties={}),
Node(id="Pierre Curie", type="Person", properties={}),
Node(id="Nobel Prize", type="Award", properties={}),
Node(id="University Of Paris", type="Organization", properties={}),
Node(id="Robin Williams", type="Person", properties={}),
],
relationships=[
Relationship(
source=Node(id="Marie Curie", type="Person", properties={}),
target=Node(id="Nobel Prize", type="Award", properties={}),
type="WON",
properties={},
),
Relationship(
source=Node(id="Marie Curie", type="Person", properties={}),
target=Node(id="Nobel Prize", type="Award", properties={}),
type="WON",
properties={},
),
Relationship(
source=Node(id="Marie Curie", type="Person", properties={}),
target=Node(
id="University Of Paris", type="Organization", properties={}
),
type="PROFESSOR",
properties={},
),
Relationship(
source=Node(id="Pierre Curie", type="Person", properties={}),
target=Node(id="Nobel Prize", type="Award", properties={}),
type="WON",
properties={},
),
],
source=Document(
metadata={"id": "de3c93515e135ac0e47ca82a4f9b82d8"},
page_content="\nMarie Curie, 7 November 1867 – 4 July 1934, was a Polish and naturalised-French physicist and chemist who conducted pioneering research on radioactivity.\nShe was the first woman to win a Nobel Prize, the first person to win a Nobel Prize twice, and the only person to win a Nobel Prize in two scientific fields.\nHer husband, Pierre Curie, was a co-winner of her first Nobel Prize, making them the first-ever married couple to win the Nobel Prize and launching the Curie family legacy of five Nobel Prizes.\nShe was, in 1906, the first woman to become a professor at the University of Paris.\nAlso, Robin Williams!\n",
),
)
]
图谱文档描述了提取的节点和关系。此外,提取的源文档被添加在source键下。
我们可以使用Neo4j浏览器来可视化输出结果,从而更清晰、更直观地理解数据。
上图展示了两次对关于玛丽·居里的同一段落进行的信息提取。在这种情况下,我们使用了基于工具的提取方法的GPT-4,该方法也允许存在孤立的节点,如图中所示。由于没有定义图谱模式,LLM在运行时决定要提取哪些信息,这可能导致输出存在差异,即使是从同一段落中提取也是如此。因此,一些提取结果比其他结果更详细,并且即使对于相同的信息,结构也可能不同。例如,在左侧,Marie被表示为诺贝尔奖的“WINNER”(获胜者),而在右侧,她则“WON”(赢得了)诺贝尔奖。
现在,让我们尝试使用基于提示的方法来进行相同的提取。对于支持工具使用的模型,你可以通过设置ignore_tool_usage参数来启用基于提示的提取。
no_schema_prompt = LLMGraphTransformer(llm=llm, ignore_tool_usage=True)True)
data = await no_schema.aconvert_to_graph_documents(documents)
同样,我们可以在Neo4j浏览器中可视化两次独立的执行结果。
在基于提示的方法中,我们不会看到任何孤立的节点。然而,与之前的提取一样,不同运行之间的模式可能会有所不同,导致对相同输入产生不同的输出。
接下来,让我们逐步了解定义图谱模式如何有助于产生更一致的输出。
定义允许的节点
限制提取的图结构可以带来很大的好处,因为它可以引导模型专注于特定、相关的实体和关系。通过定义一个清晰的模式,你可以提高提取之间的一致性,使输出更具可预测性,并与你实际需要的信息保持一致。这减少了运行之间的变异性,并确保提取的数据遵循标准化结构,捕获预期的信息。有了明确定义的模式,模型就不太可能忽略关键细节或引入意外的元素,从而产生更清晰、更可用的图谱。
我们将首先使用allowed_nodes参数来定义要提取的预期节点类型。
allowed_nodes = ["Person", "Organization", "Location", "Award", "ResearchField"]"Person", "Organization", "Location", "Award", "ResearchField"]
nodes_defined = LLMGraphTransformer(llm=llm, allowed_nodes=allowed_nodes)
data = await allowed_nodes.aconvert_to_graph_documents(documents)
在这里,我们定义了LLM(语言模型)应该提取五种类型的节点,如人物、组织、地点等。我们在Neo4j浏览器中可视化了两次独立的执行结果以便进行比较。
通过指定预期的节点类型,我们实现了更一致的节点提取。然而,仍然可能存在一些差异。例如,在第一次运行中,“放射性”被提取为研究领域,而在第二次运行中则没有。
由于我们还没有定义关系,它们的类型在不同运行之间也可能有所不同。此外,一些提取可能会捕获比其他提取更多的信息。例如,MARRIED_TOMarie 和 Pierre 之间的关系在两次提取中均不存在。
定义允许的关系
正如我们所观察到的,仅定义节点类型仍然允许在关系提取方面存在差异。为了解决这个问题,让我们探讨如何同时定义关系。第一种方法是通过列出可用的类型来指定允许的关系。
allowed_nodes = ["Person", "Organization", "Location", "Award", "ResearchField"]"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 = await rels_defined.aconvert_to_graph_documents(documents)
让我们再次查看两次独立的提取结果。
在定义了节点和关系之后,我们的输出结果变得显著更加一致。例如,Marie总是被显示为获奖者、皮埃尔的配偶,以及在巴黎大学工作。然而,由于关系是作为一般列表指定的,没有限制它们可以连接哪些节点,因此仍然存在一些差异。例如,FIELD_OF_RESEARCH(研究领域)关系可能会出现在Person(人物)和ResearchField(研究领域)之间,但有时它也会将Award(奖项)与ResearchField联系起来。此外,由于没有定义关系的方向,因此在方向一致性方面也可能存在差异。
为了解决无法指定关系可以连接哪些节点以及强制关系方向的问题,我们最近引入了一种定义关系的新选项,如下所示。
allowed_nodes = ["Person", "Organization", "Location", "Award", "ResearchField"]"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 = await rels_defined.aconvert_to_graph_documents(documents)
我们不再将关系简单地定义为字符串列表,而是现在使用三元组格式,其中三个元素分别表示源节点、关系类型和目标节点。
让我们再次可视化结果。
使用三元组方法为多次执行中提取的图谱提供了更加一致的模式。然而,鉴于大型语言模型(LLM)的特性,提取的细节程度仍可能存在一些差异。例如,在右侧,皮埃尔被显示为诺贝尔奖得主,而在左侧,这一信息则缺失。
定义属性
我们对图谱模式可以进行的最后一项增强是为节点和关系定义属性。在这里,我们有两个选项。第一个是将node_properties或relationship_properties设置为true,这允许LLM自主决定提取哪些属性。
allowed_nodes = ["Person", "Organization", "Location", "Award", "ResearchField"]"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 = await props_defined.aconvert_to_graph_documents(documents)
graph.add_graph_documents(data)
让我们查看结果。
我们已经启用了LLM(大型语言模型)来添加它认为相关的任何节点或关系属性。例如,它选择包括玛丽·居里的出生和死亡日期、她在巴黎大学担任教授的角色,以及她两次获得诺贝尔奖的事实。这些额外的属性极大地丰富了提取的信息。
我们拥有的第二个选项是定义我们想要提取的节点和关系属性。
allowed_nodes = ["Person", "Organization", "Location", "Award", "ResearchField"]"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 = await props_defined.aconvert_to_graph_documents(documents)
graph.add_graph_documents(data)
属性被简单地定义为两个列表。让我们看看LLM提取了什么。
出生和死亡日期与之前的提取保持一致。然而,这次LLM还提取了Marie在巴黎大学担任教授的起始日期。
属性确实为提取的信息增添了宝贵的深度,尽管当前的实现中存在一些限制:
严格模式
如果你认为我们已经找到了一种完美的方法让LLM无懈可击地遵循定义的模式,那么我必须澄清一下。虽然我们投入了大量的精力进行提示工程,但要让LLM,尤其是性能较差的LLM,完全准确地遵守指令是非常具有挑战性的。为了解决这个问题,我们引入了一个后处理步骤,称为strict_mode(严格模式),它会移除任何不符合定义图谱模式的信息,从而确保结果更加清晰和一致。
默认情况下,strict_mode设置为True,但你可以使用以下代码将其禁用:
LLMGraphTransformer(
llm=llm,
allowed_nodes=allowed_nodes,
allowed_relationships=allowed_relationships,
strict_mode=FalseFalse
)
当严格模式关闭时,你可能会得到定义图谱模式之外的节点或关系类型,因为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)True)
如前所述,使用baseEntityLabel参数将导致每个节点都拥有一个额外的__Entity__标签。
包含源文档
最后一个选项是同时导入提取的节点和关系的源文档。这种方法可以让我们追踪每个实体出现在哪些文档中。你可以使用include_source参数来导入源文档。
graph.add_graph_documents(graph_documents, include_source=True)(graph_documents, include_source=True)
在检查导入的图谱后,我们应该会看到类似这样的结果。
在这个可视化展示中,源文档以蓝色高亮显示,从其中提取的所有实体通过“MENTIONS”关系相连接。这种模式允许你构建能够同时利用结构化搜索和非结构化搜索方法的检索器。
总结
在这篇文章中,我们探索了LangChain的LLM Graph Transformer及其从文本中构建知识图谱的双重模式。基于工具的模式是我们的主要方法,它利用结构化输出和函数调用,减少了提示工程的工作量,并允许属性提取。同时,当工具不可用时,基于提示的模式就很有用,它依赖于少量的示例来指导LLM。然而,基于提示的提取不支持属性提取,也不会产生孤立的节点。
我们观察到,定义一个清晰的图谱模式,包括允许的节点和关系类型,可以提高提取的一致性和性能。一个受限的模式有助于确保输出符合我们期望的结构,使其更具可预测性、可靠性和适用性。无论使用工具还是提示,LLM Graph Transformer都能够使非结构化数据更有组织、更结构化地表示,从而实现更好的检索增强(RAG)应用和多跳查询处理。