基于知识图谱的LangChain应用实战

发布于 2024-10-18 14:53
浏览
0收藏

图检索增强生成(Graph RAG)正逐渐流行起来,成为传统向量搜索方法的有力补充。这种方法利用图数据库的结构化特性,将数据以节点和关系的形式组织起来,从而增强检索信息的深度和上下文关联性。


基于知识图谱的LangChain应用实战-AI.x社区


示例知识图谱

图在表示和存储多样化且相互关联的信息方面具有天然优势,能够轻松捕捉不同数据类型间的复杂关系和属性。而向量数据库在处理这类结构化信息时则显得力不从心,它们更擅长通过高维向量处理非结构化数据。在 RAG 应用中,结合结构化的图数据和非结构化的文本向量搜索,可以让我们同时享受两者的优势,这也是本文将要探讨的内容。

知识图谱的确很有用,但如何构建一个呢? 构建知识图谱通常是利用图数据表示的强大功能中最困难的一步。它需要收集和整理数据,这需要对领域知识和图建模有深刻的理解。为了简化这一过程,我们开始尝试使用大型语言模型(LLM)。LLM 凭借其对语言和上下文的深刻理解,可以自动化知识图谱创建过程中的大部分工作。通过分析文本数据,这些模型能够识别实体,理解它们之间的关系,并提出如何在图结构中最佳表示这些实体。基于这些实验,我们已经将图构建模块的首个版本集成到了 LangChain 中,本文将展示其应用。

相关代码已在 GitHub 上发布。

Neo4j 环境搭建

为了跟随本文的示例,您需要搭建一个 Neo4j 实例。最简单的方法是在 Neo4j Aura 上启动一个免费实例,它提供了 Neo4j 数据库的云版本。当然,您也可以通过下载 Neo4j Desktop 应用程序来创建一个本地数据库实例。

os.environ["OPENAI_API_KEY"] = "sk-"
os.environ["NEO4J_URI"] = "bolt://localhost:7687"
os.environ["NEO4J_USERNAME"] = "neo4j"
os.environ["NEO4J_PASSWORD"] = "password"

graph = Neo4jGraph()

此外,您还需要一个 OpenAI 密钥,因为我们将在本文中使用他们的模型。

数据导入

在本次演示中,我们将使用伊丽莎白一世的维基百科页面。我们可以利用 LangChain 加载器 轻松地从维基百科获取并分割文档。

# 读取维基百科文章
raw_documents = WikipediaLoader(query="Elizabeth I").load()

# 定义分块策略
text_splitter = TokenTextSplitter(chunk_size=512, chunk_overlap=24)
documents = text_splitter.split_documents(raw_documents[:3])

现在是时候根据获取的文档来构建图谱了。为此,我们开发了一个 LLMGraphTransformer 模块,它极大地简化了在图数据库中构建和存储知识图谱的过程。

llm=ChatOpenAI(temperature=0, model_name="gpt-4-0125-preview")
llm_transformer = LLMGraphTransformer(llm=llm)

# 提取图数据
graph_documents = llm_transformer.convert_to_graph_documents(documents)

# 存储到 neo4j
graph.add_graph_documents(
  graph_documents, 
  baseEntityLabel=True, 
  include_source=True
)

您可以指定知识图谱生成链使用哪种 LLM。目前,我们只支持 OpenAI 和 Mistral 的函数调用模型。不过,我们计划未来会扩展 LLM 的选择范围。在这个例子中,我们使用的是最新的 GPT-4。需要注意的是,生成的图谱质量很大程度上取决于您使用的模型。理论上,您应该选择能力最强的模型。LLM 图转换器返回的图文档可以通过 add_graph_documents 方法导入到 Neo4j。baseEntityLabel 参数为每个节点添加了一个额外的 __Entity__ 标签,以增强索引和查询性能。include_source 参数则将节点与其原始文档关联起来,便于数据追溯和理解上下文。

您可以在 Neo4j 浏览器中查看生成的图谱。

基于知识图谱的LangChain应用实战-AI.x社区

结合混合(向量 + 关键字)和图检索方法。

请注意,这张图片仅为了清晰展示,只展示了生成图谱的一部分。

RAG 的混合检索

在图谱生成之后,我们将采用一种混合检索方法,结合向量和关键字索引以及图检索技术,用于 RAG 应用。

基于知识图谱的LangChain应用实战-AI.x社区

结合混合(向量 + 关键字)和图检索方法。

上图展示了一个检索过程,从用户提出问题开始,然后由 RAG 检索器处理。这个检索器结合了关键字和向量搜索来筛选非结构化文本数据,并将其与从知识图谱中提取的信息结合起来。由于 Neo4j 同时支持关键字和向量索引,您可以使用单一数据库系统实现所有三种检索方式。这些来源的数据将被送入 LLM,以生成并提供最终答案。

非结构化数据检索器

您可以使用 Neo4jVector.from_existing_graph 方法为文档添加关键字和向量检索功能。该方法为混合搜索方法配置了关键字和向量搜索索引,目标是标记为 Document 的节点。如果缺少文本嵌入值,它还会自动计算。

vector_index = Neo4jVector.from_existing_graph(
    OpenAIEmbeddings(),
    search_type="hybrid",
    node_label="Document",
    text_node_properties=["text"],
    embedding_node_property="embedding"
)

然后,您可以使用 similarity_search 方法来调用向量索引。

图检索器

另一方面,配置图检索器虽然更为复杂,但提供了更大的灵活性。在这个例子中,我们将使用全文索引来识别相关节点,然后返回它们的直接邻域。

基于知识图谱的LangChain应用实战-AI.x社区

图检索器示意图

图检索器首先识别输入中的相关实体。为了简化,我们指导 LLM 识别人物、组织和地点。为了实现这一点,我们将使用 LCEL 配合新加入的 with_structured_output 方法。

# 从文本中提取实体
class Entities(BaseModel):
    """识别实体相关信息。"""

    names: List[str] = Field(
        ...,
        descriptinotallow="文本中出现的所有人物、组织或商业实体的名称",
    )

prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "您正在从文本中提取组织和人物实体。",
        ),
        (
            "human",
            "请按照给定格式从以下输入中提取信息:{question}",
        ),
    ]
)

entity_chain = prompt | llm.with_structured_output(Entities)

让我们来实际测试一下:

entity_chain.invoke({"question": "阿梅莉亚·埃尔哈特在哪里出生?"}).names
# ['阿梅莉亚·埃尔哈特']

很好,现在我们能够在问题中识别出实体,接下来我们将使用全文索引将这些实体映射到知识图谱中。首先,我们需要定义一个全文索引,并创建一个函数来生成全文查询,这个查询允许一定程度的拼写错误,这里我们不详细展开。

graph.query(
    "CREATE FULLTEXT INDEX entity IF NOT EXISTS FOR (e:__Entity__) ON EACH [e.id]")

def generate_full_text_query(input: str) -> str:
    """
    为给定的输入字符串生成全文搜索查询。

    该函数构建一个适用于全文搜索的查询字符串。它通过将输入字符串分割成单词,并对每个单词附加一个相似性阈值(允许2个字符变化),然后使用 AND 运算符将它们组合起来。这对于将用户问题中的实体映射到数据库值非常有用,并且能够容忍一些拼写错误。
    """
    full_text_query = ""
    words = [word for word in remove_lucene_chars(input).split() if word]
    for word in words[:-1]:
        full_text_query += f"{word}~2 AND"
    full_text_query += f"{words[-1]}~2"
    return full_text_query.strip()

现在,让我们整合所有步骤。

# 全文索引查询
def structured_retriever(question: str) -> str:
    """
    收集问题中提到的实体的邻域信息
    """
    result = ""
    entities = entity_chain.invoke({"question": question})
    for entity in entities.names:
        response = graph.query(
            """CALL db.index.fulltext.queryNodes('entity', $query, 
            {limit:2})
            YIELD node,score
            CALL {
              MATCH (node)-[r:!MENTIONS]->(neighbor)
              RETURN node.id + ' - ' + type(r) + ' -> ' + neighbor.id AS 
              output
              UNION
              MATCH (node)<-[r:!MENTIONS]-(neighbor)
              RETURN neighbor.id + ' - ' + type(r) + ' -> ' +  node.id AS 
              output
            }
            RETURN output LIMIT 50
            """,
            {"query": generate_full_text_query(entity)},
        )
        result += "\n".join([el['output'] for el in response])
    return result

structured_retriever 函数首先识别用户问题中的实体,然后遍历这些实体,使用 Cypher 模板检索相关节点的邻域信息。让我们来实际测试一下!

print(structured_retriever("伊丽莎白一世是谁?"))
# 伊丽莎白一世 - BORN_ON -> 1533年9月7日
# 伊丽莎白一世 - DIED_ON -> 1603年3月24日
# 伊丽莎白一世 - TITLE_HELD_FROM -> 英格兰和爱尔兰女王
# 伊丽莎白一世 - TITLE_HELD_UNTIL -> 1558年11月17日
# 伊丽莎白一世 - MEMBER_OF -> 都铎王朝
# 伊丽莎白一世 - CHILD_OF -> 亨利八世
# 等等...

最终检索器

正如我们一开始提到的,我们将结合非结构化和图检索器来创建最终的上下文,这将传递给 LLM。

def retriever(question: str):
    print(f"搜索查询:{question}")
    structured_data = structured_retriever(question)
    unstructured_data = [el.page_content for el in vector_index.similarity_search(question)]
    final_data = f"""结构化数据:
{structured_data}
非结构化数据:
{"#Document ".join(unstructured_data)}
    """
    return final_data

由于我们使用的是 Python,我们可以使用 f-string 轻松地将输出合并。

定义 RAG 链

我们已经成功实现了 RAG 的检索组件。接下来,我们将引入一个提示,它利用混合检索器提供的上下文来生成响应,从而完成 RAG 链的实现。

template = """根据以下上下文回答问题:
{context}

问题:{question}
"""
prompt = ChatPromptTemplate.from_template(template)

chain = (
    RunnableParallel(
        {
            "context": _search_query | retriever,
            "question": RunnablePassthrough(),
        }
    )
    | prompt
    | llm
    | StrOutputParser()
)

最后,我们可以测试我们的混合 RAG 实现。

chain.invoke({"question": "伊丽莎白一世属于哪个家族?"})
# 搜索查询:伊丽莎白一世属于哪个家族?
# '伊丽莎白一世属于都铎王朝。'

我还加入了一个查询重写特性,使得 RAG 链能够适应允许后续问题的对话环境。鉴于我们使用了向量和关键字搜索方法,我们需要重写后续问题以优化搜索过程。

chain.invoke(
    {
        "question": "她何时出生?",
        "chat_history": [("伊丽莎白一世属于哪个家族?",
        "都铎王朝")],

    }
)
# 搜索查询:伊丽莎白一世何时出生?
# '伊丽莎白一世出生于1533年9月7日。'

您可以看到,'她何时出生?' 首先被重写为 '伊丽莎白一世何时出生?'。然后使用重写后的查询来检索相关上下文并回答问题。

总结

随着 LLMGraphTransformer 的引入,生成知识图谱的过程现在应该更加顺畅和易于访问,这使得任何想要通过知识图谱提供的深度和上下文来增强其基于 RAG 的应用的人更容易上手。这只是一个开始,因为我们计划进行更多的改进。

本文转载自 AI小智​,作者: AI小智

已于2024-10-21 09:35:04修改
收藏
回复
举报
回复
相关推荐