暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Neo4j+知识图谱+LLM的智能聊天机器人开发指南

活水智能 2025-03-04
107

点击上方↗️活水智能关注 + 星标🌟

作者:Maninder Singh

编译:活水智能


 

最近,我为一家医疗保健行业的客户开发了一款解决方案。该客户积累了大量患者记录、研究论文和诊断报告,这些数据散落在数十个 Excel 文件和 PDF 文档中。每次需要分析时,分析师往往需要耗费数小时甚至数天时间,逐一比对这些数据源,才能回答诸如“哪种治疗方案对某类疾病的患者最有效?”这样的基础问题。

起初,我们尝试采用标准的检索增强生成(RAG)流程,结合嵌入技术和向量数据库。对于简单的查询任务,这种方法表现尚可;然而,当面对需要综合患者病史与治疗结果的复杂问题时,系统便显得力不从心。经过反思,我意识到:引入知识图谱或许是解决问题的关键。

在这篇博文中,我将分享我在该项目中使用知识图谱的学习心得,并尝试解释如何使用它们在结构化数据集上执行问答和检索增强生成(RAG)。(我很快会发布一篇关于如何使用 GraphDB 处理非结构化数据集的博文)。我将介绍三种关键方法,你可以使用这些方法通过 LLM 与你的 GraphDB 进行对话。为此,我将使用 Neo4j
 数据库进行演示。

使用知识图谱和 LLM 的基本问答流程
  1. 1. CypherQAChain:这是一种高效的方法,能够将自然语言问题自动转化为 Cypher 查询语言,从而大幅简化对图数据库的操作流程。通过这种方式,用户无需手动编写复杂的查询代码,即可快速获得精确的结果。

  2. 2. 高级查询:对于更复杂的问题,我们将探索一种结合实体提取和数据库值映射的技术。这种方法非常适合处理需要深入分析的细致查询。

  3. 3. 基于 RAG 的方法:为了提高相关性和检索效果,我们将把向量索引与知识图谱集成。通过这种方式,我们可以确保处理存储为节点属性的非结构化数据(如文本描述),以用于问答任务。

为了解释这些方法,我将使用一个 电影数据集
,其中包含有关电影、演员、导演、类型、IMDB 评分和发行日期的信息。通过这个数据集,你将了解如何有效地使用知识图谱和 LLM 与结构化数据集进行对话,以及如何在存储为节点属性的非结构化数据上进行 RAG。

在本节结束时,你不仅将理解这些技术,还将拥有在你的项目中自行实施它们的实践知识。

这篇博文可能有点长,所以你可以随意使用索引直接跳转到你想阅读的部分!

  1. 1. 知识图谱和 GraphDB 的基本概述

  2. 2. LLM 和 GraphDB 如何在问答应用中交互?

    • • LLM 用于将非结构化数据转换为图谱

  3. 3. 方法 1:使用 CypherChainQA 实际实现问答

    • • Neo4j 设置

    • • 数据预处理和创建图谱

    • • GraphCypherQAChain

  4. 4. 方法 2:高级查询

  5. 5. 方法 3:将 RAG 与 GraphDB (Neo4j) 结合使用

  6. 6. 结论

[*你可以在我的 GitHub 存储库中找到这篇博文中提到的所有相关代码。*](https://github.com/manindersingh120996/RAG-Related-Projects/blob/main/GraphRAG%20and%20Agentic%20Rag%20using%20Neo4j.ipynb)

知识图谱和 GraphDB 的基本概述

在我们开始使用知识图谱构建聊天系统之前,必须了解图数据库(GraphDB)的基本概念,以及为什么它们在涉及复杂、互连数据的场景中表现出色。

什么是图数据库(GraphDB)?

图数据库(GraphDB)是一种数据库,旨在将数据存储和管理为实体及其关系的网络。与传统的关系数据库使用行和列不同,GraphDB 使用以下主要组件存储数据:

这种结构使 GraphDB 成为处理高度关联数据的理想选择,尤其是在关系对全局理解至关重要的场景中,例如社交媒体平台。GraphDB 在此类应用中具备以下显著优势:

1. 关系的自然表示

在传统的关系数据库中,我们将用户存储在一个表中,并将他们的关系(如“关注”或“好友”)存储在另一个表中使用外键。例如:

  • • 用户表:每一行代表一个用户。

  • • 关系表:使用用户 ID 跟踪谁关注谁。

虽然这可行,但随着网络的增长,查询这些关系会变得复杂且缓慢。

在 GraphDB 中,这些关系直接建模为 节点(用户)之间的 。例如:

  • • 节点 A:代表用户 A。

  • • 节点 B:代表用户 B。

  • • :代表“用户 A 关注用户 B”。

用户 A 关注用户 B 的 Cypher 查询

这种方法使我们可以直接遍历关系,例如查找“共同好友”或“建议的连接”,而无需复杂的表连接。

2. 节点和关系的语义丰富性和属性

GraphDB 允许边携带有关关系的详细信息,从而将它们变成 知识图谱。例如,我们可以定义:

  • • “评论”用于与帖子的互动。

  • • “标记在”用于在照片或视频中提及。

  • • “对...做出反应”用于喜欢、爱或其他反应。

这种语义丰富性为数据添加了有意义的上下文。例如:

  • • 查询:“哪些用户在同一个项目上合作过?”

  • • 答案:遍历标记为“合作”的边以查找连接

此外,GraphDB 中的每个节点和边不仅具有节点和它们之间的关系,而且每个节点和边还携带属性,如下图所示,从而使它们成为知识图谱。

电影节点携带的属性

3. 动态更新

GraphDB 专为动态更新而设计。当新数据与现有节点和边的关系匹配时,系统会自动将其整合到现有结构中;否则,将创建新的节点和关系,确保数据实时更新。

考虑社交媒体网站的示例,该网站不断变化 - 形成新的友谊、共享帖子和添加评论。

  • • 添加新关系(如“关注”或“评论”)就像添加一条边一样简单。

  • • 删除关系(如“取消关注”)会删除边,而不会中断其他数据。

例如,当用户 A 关注用户 B 时,GraphDB 会立即通过在他们的相应节点之间创建一条新边来更新。这种动态性质有助于使网络与最新信息保持同步。

4. 遍历效率

GraphDB 针对 遍历算法 进行了优化,仅举几例:

  • • 深度优先搜索 (DFS):探索深层关系,例如两个用户之间的交互链。

  • • 广度优先搜索 (BFS):探索所有直接连接,例如查找用户的所有好友。

  • • Delta-Stepping 单源最短路径:有效地计算从单个源到所有其他节点的最短路径,这对于查找网络中最快的路线很有用。

  • • Dijkstra 源目标最短路径:查找两个特定节点之间的最短路径,非常适合确定两个用户之间最有效的连接。

例如,在社交媒体平台上,遍历算法在以下关键任务中发挥着重要作用:

  • • 好友推荐:通过分析共同好友网络,识别潜在的好友关系。

  • • 内容推荐:基于共享标签或点赞行为,发现相似的内容。

  • • 社区检测:挖掘具有相同兴趣爱好的用户群体。

5. 可解释性和可追溯性

在此,值得指出的是,GraphDB 使跟踪用户或实体如何连接变得容易。例如:

  • • 查询:“用户 A 如何连接到用户 C?”

  • • 答案:图谱可能会显示如下路径:

    • • 用户 A 关注用户 B(边 1)。

    • • 用户 B 评论了用户 C 的帖子(边 2)。

这种透明度对于调试、用户洞察以及建立对推荐算法的信任非常重要。

感谢您的阅读,如果本文对您有所帮助,欢迎分享。接下来,我们将继续探讨。

LLM 和 GraphDB 如何在问答应用中交互?

要构建基于知识图谱的智能高效聊天机器人系统,首先需要深入了解 LLM 与 GraphDB 的协同工作机制。

GraphDB(如 Neo4j)(如上所述)在存储和导航互连数据方面非常有效。它们使用节点(实体)、边(实体之间的关系)和属性(节点和边的属性)的结构来表示和管理复杂的关系。

而 LLM 是将类人、非结构化输入转换为有意义的结构化查询的主要大脑。例如,如果用户向聊天机器人询问“谁出演了电影《盗梦空间》?”,则 LLM 会将此自然语言查询转换为精确的查询语言(如 Neo4j 使用的 Cypher):

MATCH (m:Movie {title: "Inception"})-[:ACTED_IN]-(p:Person)
RETURN p.name;

然后,LLM 解释 GraphDB 检索的结果,并为用户提供对话式回复,例如:

*“《盗梦空间》的演员包括莱昂纳多·迪卡普里奥、艾伦·佩姬和约瑟夫·高登-莱维特。”*

随着编码和推理能力的最新进展,现代 LLM 已成为以下方面的卓越工具:

  1. 1. 自然语言理解:如上所示,将模糊或非结构化的用户查询转换为数据库的精确、可执行命令。

  2. 2. 生成类人回复:将从数据库检索的数据转换为对话式回复。

但它们的作用并不止于此。除了查询数据之外,LLM 还在通过处理非结构化数据来 创建
 知识图谱方面发挥着重要作用。

LLM 用于将非结构化数据转换为图谱

LLM 在 GraphDB 生态系统中最令人兴奋的应用之一是它们能够将 PDF、文档形式的非结构化信息转换为结构化图数据。这涉及识别文本中的实体(节点)和关系(边)以及与实体相关的属性,然后将它们表示为图谱。

示例:从文本中提取图数据

以一段文本为例:

“莱昂纳多·迪卡普里奥主演了克里斯托弗·诺兰导演的电影《盗梦空间》。”

使用像 LLMGraphTransformer 这样的工具,可以将文本转换为图谱,如下所示:

  • • 节点

    • • - 莱昂纳多·迪卡普里奥
      (人物)

    • • - 盗梦空间
      (电影)

    • • - 克里斯托弗·诺兰
      (人物)

  • • 关系

    • • - 莱昂纳多·迪卡普里奥
       → ACTED_IN
       → 盗梦空间

    • • - 克里斯托弗·诺兰
       → DIRECTED
       → 盗梦空间

请注意,图谱构建过程是非确定性的,因为 LLMGraphTransformer 或类似工具使用 LLM 作为其基础。因此,每次执行获得的结果可能略有不同,并且获得的图谱质量高度取决于所使用的 LLM 的类型。

LLMGraphTransformer 使用 LLM 来

  1. 1. 解析非结构化文本文档

  2. 2. 识别和分类实体(例如,人物、电影)

  3. 3. 建立这些实体之间有意义的关系。

图谱形成的有效性取决于所选的 LLM 模型,因为它会影响提取的图数据的准确性和粒度。在未来一篇关于使用非结构化数据进行图问答/RAG 的博文中,我将详细介绍这个主题。

方法 1:使用 CypherChainQA 实际实现问答

现在我们已经基本了解了 GraphDB 如何与 LLM 交互,现在让我们开始使用 Langchain 框架中的 CypherChainQA 进行 GraphDB 的第一个问答聊天机器人阶段。

Neo4j 设置:

对于我们的实现,我们将使用 Neo4j,这是一个强大的图数据库管理系统,以其处理和查询图数据的效率而闻名。Neo4j 提供了几个使其成为聊天机器人开发的绝佳选择的功能:

  • • Cypher 查询语言:Neo4j 使用 Cypher,这是一种声明式且用户友好的查询语言,可简化在图谱中检索和操作数据的过程。

  • • 易于集成:Neo4j 提供了一个 Python 驱动程序,可以轻松地从 LangChain(一个用于处理 LLM 的流行框架)直接连接和与数据库交互。

此外,Neo4j 与 LangChain 框架的良好集成使其非常适合构建基于图谱的聊天机器人系统。Neo4j 与 LangChain 的集成使我们能够:

让我们设置 Neo4j AuraDB(免费实例)

  • • 转到 Neo4j Aura 并登录或注册(您可以免费创建一个用于学习目的的单个实例 👌)。

  • • 创建一个新的数据库实例 (设置需要一些时间)

  • • 实例准备就绪后,记下并下载提示时显示的连接凭据 .txt

    • • URI(例如,bolt://<your_database>.databases.neo4j.io

    • • 用户名(默认:neo4j

    • • 密码(在设置期间生成)。

使用上述记下的凭据使用以下代码连接到 Neo4j DB:

from kaggle_secrets import UserSecretsClient
from langchain_community.graphs import Neo4jGraph

user_secrets = UserSecretsClient()
groq_api_key = user_secrets.get_secret("groq_api_key")
hf_api_key = user_secrets.get_secret("hf_api_key")
NEO4J_PASSWORD = user_secrets.get_secret("NEO4J_PASSWORD")
NEO4J_URI = user_secrets.get_secret("NEO4J_URI")
NEO4J_USERNAME = user_secrets.get_secret("NEO4J_USERNAME")

graph = Neo4jGraph(
    url=NEO4J_URI,
    username=NEO4J_USERNAME,
    password=NEO4J_PASSWORD)

print("Connected to Neo4j!")

数据预处理和创建图谱

现在我们已经与数据库建立了连接,现在我们可以导入数据集,对其进行一些预处理并更新 Neo4j 数据库。

步骤 1️⃣: 我们首先加载一个示例电影数据集。该数据集包含有关电影的关键信息,例如标题、导演、演员、类型和评分。让我们读取数据并快速浏览一下:

df = pd.read_csv("https://raw.githubusercontent.com/tomasonjo/blog-datasets/main/movies/movies_small.csv", nrows=20)
display(df.head(3))

print(df.shape)
print("="*40)
print(df.columns)
print("="*40)
print(df["title"][:20].values)

步骤 2️⃣: 为了使数据集对于 RAG 任务的查询和检索更有意义,我为每部电影创建了一个详细的描述列。这些描述列结合了各种列,如标题、导演、演员、类型、IMDb 评分和发行日期。

此外,我创建了额外的虚拟列。

details = df['title'][0]
details

df['movie_detail'] = df.apply(lambda row: f"Movie {row['title']} is directed by {row['director']}\
 having actors named {row['actors'].replace('|',' , ')} and it is movie of the \
genre in {row['genres'].replace('|',' , ')}. It has a rating of {row['imdbRating']} \
and it is released in {row['released']}",axis=1)
df.head()

# Add custom data for locations and similar movies
location = ["United States", "United States", "United States", "United States", "United States",
            "United States", "United States", "United States", "United States", "United Kingdom",
            "United States", "United States", "United States", "United States", "Malta",
            "United States", "United Kingdom", "United States", "United States", "United States"]
similar_movie = ["Finding Nemo", "Jumanji: Welcome to the Jungle", "The Bucket List", "The Best Man Holiday",
                 "Cheaper by the Dozen", "The Departed", "Notting Hill", "The Adventures of Huck Finn",
                 "Die Hard", "Mission Impossible", "Dave", "Dead and Loving It: Young Frankenstein",
                 "Spirit: Stallion of the Cimarron", "JFK",
                 "Pirates of the Caribbean: The Curse of the Black Pearl", "Goodfellas",
                 "Pride and Prejudice", "Pulp Fiction", "The Mask", "Speed"]

df['location'] = location
df['similar_movie'] = similar_movie

# Save the enriched dataset for graph construction
df.to_csv("movie.csv", sep=",", index=False)

步骤 3️⃣: 构建 Neo4j 图谱

现在我们将使用处理后的数据集更新 Neo4j 数据库。该图谱将包括 电影导演演员类型地点 和 类似电影 的节点。这些实体之间的关系也将相应地创建。

# creating below input value just to make sure not to execute this code again for creating graph accidentaly
value = input("Do You realy want to execute this cell and create the GraphDB again ? y/n")

movie_csv_path = 'movie.csv'
if value == 'y':
    graph.query("""
    LOAD CSV WITH HEADERS FROM 
    'https://raw.githubusercontent.com/manindersingh120996/RAG-Related-Projects/refs/heads/main/movie%20(1).csv'
    AS row

    MERGE (m:Movie {id:row.movieId})
    SET m.released = row.released,
        m.title = row.title,
        m.movie_detail = row.movie_detail,
        m.imdbRating = toFloat(row.imdbRating)

    FOREACH (director in split(row.director, '|') |
        MERGE (p:Person {name:trim(director)})
        MERGE (p)-[:DIRECTED]->(m))

    FOREACH (actor in split(row.actors, '|') |
        MERGE (p:Person {name:trim(actor)})
        MERGE (p)-[:ACTED_IN] ->(m))

    FOREACH (genre in split(row.genres, '|') |
        MERGE (g:Genre {name:trim(genre)})
        MERGE (m)-[:IN_GENRE]->(g))

    MERGE (l:location {name:trim(row.location)})
    MERGE (m)-[:WAS_TAKEN_IN]->(l)

    MERGE (s:SimilarMovies {name:trim(row.similar_movie)})
    MERGE (m)-[:IS_SIMILAR_TO]->(s)
    """)

为了创建图谱,我们使用 Cypher 查询语言,为此,我们循环遍历数据集行并创建:

  1. 1. 电影节点,具有标题、详细信息和 IMDb 评分等属性。

  2. 2. 导演节点,通过 DIRECTED
     关系链接到电影。

  3. 3. 演员节点,通过 ACTED_IN
     关系链接到电影。

  4. 4. 类型节点,通过 IN_GENRE
     关系链接到电影。

  5. 5. 地点节点,通过 WAS_TAKEN_IN
     关系链接到电影。

  6. 6. 类似电影节点,通过 IS_SIMILAR_TO
     关系链接到电影。

我们可以看到创建的图数据库的模式,如下所示:

graph.refresh_schema()
print(graph.schema)

图谱模式的输出

GraphCypherQAChain

GraphCypherQAChain 流程

现在我们已经设置了 Neo4j 数据库并使用所需的节点和关系对其进行了结构化,我们可以利用 GraphCypherQAChain。此链提供了一种直接有效的机制来与数据库交互,因为它接受一个问题,将其转换为 Cypher 查询,执行查询,并使用结果来回答原始问题。

为了确保 Cypher 查询既准确又有效,使用一种足够成熟的语言模型(LLM)来理解和生成 Cypher 语法至关重要。选择功能不足的 LLM 可能会导致:

为了获得最佳结果,建议使用为此目的训练的高级 LLM。

例如,在我使用 LLaMA 3.1 (8B) (此处 8B 表示参数量为 80 亿)进行的测试中,我无法获得令人满意的结果,但当我切换到 LLaMA 3.3 (70B) (此处 70B 表示参数量为 700 亿)后,我的系统能够回答更复杂的问题。

GraphCypherQAChain 的实现方式如下:

from langchain.chains import GraphCypherQAChain

# 这可以被看作是一个简单代理,因为这里只执行了两行代码
# 但它不像我们将要看到的对于问题 2 和问题 3 那样强大
# 问题变得更加复杂

"""
改进此过程的一种方法是使用更强大的语言模型
能够生成准确的 Cypher 查询。 例如,
我最初使用 LLaMA 3.1 (8B),它难以回答两个特定问题。
但是,升级到 LLaMA 3.3 (70B) 成功解决了第一个和第三个问题。
"""

cypher_execution_chain = GraphCypherQAChain.from_llm(graph=graph,  # 在这里,它检索图谱模式
                                    llm=llm_model,
                                    verbose=True,
                                    allow_dangerous_requests=True)

response = cypher_execution_chain.invoke({"query": q_one})
print(response)
print("\nLLM response:", response["result"])

虽然对于简单的问题它可以表现良好,但一旦问题变得复杂,它就无法回答,如下所示:

q_two = "What are the most common genres for movies released in 1995?"
response = cypher_execution_chain.invoke({"query": q_two})
print(response)
print("\nLLM response:", response["result"])

GraphCypherQAChain 在回答更复杂的问题时遇到困难,例如那些具有多个条件或间接关系的问题。这表明需要一个更强大和更智能的解决方案来处理这种情况。

感谢您的阅读,如果本文对您有所帮助,欢迎分享。接下来,我们将继续探讨。

在下一节 高级查询 中,我们将了解如何通过一些额外的步骤(如从用户输入中提取实体等)使问答系统更加强大。这些步骤可以逐步分解和处理复杂的查询,从而能够以更高的准确性和洞察力回答详细或分层的问题。

方法 2:高级查询:

正如我们上面观察到的,当用户问题变得更复杂时,例如那些涉及多个条件或间接关系的问题,CypherQAChain 难以提供答案。发生这种情况的原因是,虽然 CypherQAChain 了解数据库模式,但它不知道数据库中的实际数据。 例如,它可能识别出一个属性,如“标题”,但不知道存在哪些电影标题。这可能会导致查询无法返回有用的结果。为了解决这个问题,我添加了一个映射层,将用户输入与实际数据库值匹配,并使用这些值来创建更精确的查询,从而使系统更可靠。

高级查询问答流程

它的工作步骤如下:

步骤 1 检测用户输入中的实体: 对于所有用户输入查询,它会提取实体,如潜在的人名、电影名和年份。

from typing import List
# from langchain.chains.openai_functions import create_structured_output_chain
from langchain.chains import create_structured_output_runnable
from langchain_core.prompts import ChatPromptTemplate
# from langchain_core.pydantic_v1 import BaseModel, Field
from pydantic import BaseModel, Field, field_validator

class Entities(BaseModel):
    """Identifying information about entities and 
    extract person, movies, and years entitites from the text."""

    names: List[str] = Field(
        ...,
        description="All the person , year or movies appearing in the text",
    )

entity_extractor_model = llm_model.with_structured_output(Entities)

步骤 2:将实体映射到数据库值

一旦识别出实体,我们就将它们与实际数据库值匹配。为此,我使用了 LangChain 文档中的通用查询,以检索有关节点(如电影或人物)及其关系(例如,ACTED_IN、IN_GENRE)的详细信息。此查询提供了节点及其上下文的清晰、易于理解的摘要。

该过程涉及三个关键步骤:

  1. 1. 使用 $value
     将检测到的实体(电影、人物)插入到查询中。

  2. 2. 映射关系和连接的节点以呈现相关详细信息。

  3. 3. 为每个匹配的节点生成结构化摘要,以提高答案的清晰度和深度。

match_query = """
MATCH (m:Movie|Person)
WHERE m.title CONTAINS $value OR m.name CONTAINS $value OR m.released CONTAINS $value
MATCH (m)-[r:ACTED_IN|IN_GENRE]-(t)
WITH m, type(r) as type, collect(coalesce(t.name, t.title)) as names
WITH m, type+": "+reduce(s="", n IN names | s + n + ", ") as types
WITH m, collect(types) as contexts
WITH m, "type:" + labels(m)[0] + "\ntitle: "+ coalesce(m.title, m.name) 
       + "\nyear: "+coalesce(m.released,"") +"\n" +
       reduce(s="", c in contexts | s + substring(c, 0, size(c)-2) +"\n") as result
RETURN result
"""

def map_to_database(values)->str:
    """
    Maps the values to entities in the database and returns the mapping information.

    Args:
        values (list): A list of values to map to entities in the database.

    Returns:
        str: A string containing the mapping information of each value to entities in the 
    """
    result = ""
    for entity in values.names:
        response = graph.query(match_query, {"value": entity})
        # print(response)
        try:
            for values in response:
                result += f"{entity} maps to {values['result']}  in database\n\n" # Query the database to find the mapping for the entity
        except IndexError:
            pass
    return result

为了使此映射有效,我们使用了一个 Cypher 查询,该查询利用 CONTAINS
 子句来查找匹配项。为了获得更大的灵活性(以解决拼写错误),还可以应用模糊搜索或全文索引等技术。

步骤 3:生成上下文感知的 Cypher 查询

使用提取的信息和数据库模式,我们创建一个 Cypher 查询,该查询既准确又针对用户的问题量身定制。此自定义 Cypher 查询确保它考虑了数据库中的所有相关属性、关系和数据值。

from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# Generate Cypher statement based on natural language input
cypher_template = """Based on the Neo4j graph schema below, write a Cypher query that would answer the user's question:
{schema}

Entities in the question map to the following database values:
{entities_list}

Question: {question}

Note: Do not include any explanations or apologies in your responses.
Do not wrap the response in any backticks or anything else.
Respond with a Cypher statement only!

Cypher query:"""

cypher_prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "Given an input question, convert it to a Cypher query. No pre-amble.",
        ),
        ("human", cypher_template),
    ]
)

# cypher_prompt.invoke({'schema'})
chain = cypher_prompt | llm_model.bind(stop=["\nCypherResult:"]) | StrOutputParser()

步骤 4:根据数据库结果生成答案

现在我们有了一个生成目标 Cypher 查询的链,我们需要针对数据库执行 Cypher 查询并将数据库结果发送回 LLM 以生成最终答案。

from langchain.chains.graph_qa.cypher_utils import CypherQueryCorrector, Schema

# Cypher validation tool for relationship directions
corrector_schema = [
    Schema(el["start"], el["type"], el["end"])
    for el in graph.structured_schema.get("relationships")
]
cypher_validation = CypherQueryCorrector(corrector_schema)

# Generate natural language response based on database results
response_template = """Based on the the question, Cypher query, and Cypher response, write a natural language response:
Question: {question}
Cypher query: {query}
Cypher Response: {response}"""

response_prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "Given an input question and Cypher response, convert it to a natural"
            " language answer.Give the answer in Structured format such a Visually appealing to Read to user."
            "No pre-amble.",
        ),
        ("human", response_template),
    ]
)

chain_2 = response_prompt | llm_model | StrOutputParser()

将上面提到的所有代码组合到一个流程中,将为我们获取问题 2 的结果,而上面的方法 1 失败了,如下所示:

def questions_asked(question_asked:str):
    question = question_asked
    entities = entity_extractor_model.invoke(question)
    entities_list = map_to_database(entities)
    schema = graph.get_schema
    chain = cypher_prompt | llm_model.bind(stop=["\nCypherResult:"]) | StrOutputParser()
    cypher_query = chain.invoke({'schema': schema,
                 'entities_list': entities_list,
                 'question': question})
    
    print(f"Cypher Query Generated : {cypher_query}")
    print("="*40)
    graph.query(cypher_query)
    
    final_answer = chain_2.invoke({'response': graph.query(cypher_query),
                 'query': cypher_query,
                 'question': question})
    
    print(f'Final Answer for Question : "{question}" --> \n {final_answer}')

questions_asked(q_two)

我们发现这种方法成功地回答了 CypherQAChain 无法回答的问题。**关键的改进来自提取相关实体并将其映射到数据库,从而为 LLM 提供了有关数据的更好上下文。**
 这种额外的上下文使 LLM 能够生成更精确的 Cypher 查询,从而仅检索最相关的信息。

在我的实验中,这种方法在所有测试用例中都完美地工作,尽管我可能错过了一些边缘情况。

到目前为止,我们专注于 Cypher 查询生成、数据检索和答案生成。接下来,我们将深入研究如何使用 Neo4j GraphDB 构建基本的检索增强生成 (RAG) 流程。

方法三:使用 RAG 与 GraphDB (Neo4j)

到目前为止,我们主要关注的是结构化文本数据。但如果非结构化文本也是数据集的一部分呢? 知识图谱,例如 Neo4j,在这方面具有独特的优势。它们可以同时存储结构化和非结构化数据,并生成 RAG 任务所需的嵌入向量,所有这些功能都集中在一个平台中。 这简化了为 LLM 提供检索增强生成 (RAG) 所需上下文的过程。

GraphRAG 是一种使用知识图谱来增强上下文检索的 RAG 方法。 在本节中,我将演示如何使用 LangChain 和 Neo4j 实现 GraphRAG。 我们将使用预处理阶段生成的附加列 movie_details,该列由所有相关字段组合而成,来为 RAG 构建向量索引。

在 GraphRAG 中,如果仅使用单个属性来检索所有相关信息,建议使用您确信包含足够所需信息的属性,以满足预期问题。

步骤 1:创建向量嵌入

对于 GraphRAG,第一步是创建所选字段的向量嵌入,如下所示:我们将为 Movie Detail(电影详情) 列生成向量嵌入(即将文本转换为数值向量的技术),并将其存储到名为 movie_detail_embeddings(电影详情嵌入) 的新列中。随后,这些嵌入会被用来填充 GraphDB 中的 VECTOR INDEX(向量索引),以支持 RAG 任务中的高效检索。

import torch
device = 'cuda' if torch.cuda.is_available() else 'cpu'

embeddings = HuggingFaceEmbeddings(model_name="jinaai/jina-embeddings-v3",
                                  encode_kwargs = {'normalize_embeddings'True},
                                   model_kwargs = {'device': device,'trust_remote_code':'True',})

def embed_text(text:str)->List:
    """
    使用指定的模型嵌入给定的文本。

    参数:
        text (str): 要嵌入的文本。

    返回:
        List: 包含文本嵌入的列表。
    """

    response = embeddings.embed_query(text)
    return response

embedding_list = [embed_text(i) for i in df["movie_detail"]]
embedding_list
print("Number of vectors:"len(embedding_list))
print("Embedding dimension:"len(embedding_list[0]))
embedding_list[0][:5]

df["movie_detail_embeddings"] = embedding_list
df.head(3)

步骤 2:创建空的向量索引

在 neo4j 数据库中创建空的向量索引,我们稍后将使用步骤 1 中创建的嵌入来填充它。接下来,我们将创建一个空的向量索引,该索引的维度与嵌入模型保持一致,并指定用于检索的相似度函数,具体代码如下:

# 此处将在 GraphDB 中创建一个空的向量索引。设置输入值是为了防止重复执行该代码块。
value = input("确认是否要重新创建向量索引?(y/n)")
if value == 'y':
    graph.query("""
      CREATE VECTOR INDEX movie_detail_embeddings IF NOT EXISTS      // 如果不存在,则创建一个名为“movie_detail_embeddings”的向量索引
      FOR (m:Movie) ON (m.movie_detail_embeddings)                           // 索引 Movie 节点的 ‘movie_detail_embeddings’ 属性
      OPTIONS { indexConfig: {                                        // 设置索引选项
        `vector.dimensions`: 1024,                                    // 指定向量空间的维度(1024 维)
        `vector.similarity_function`: 'cosine'                        // 指定相似度函数为余弦相似度
      }}"""

    )

graph.query("""
SHOW VECTOR INDEXES  // 检索有关数据库中所有向量索引的信息
"""
)

步骤 3:填充向量索引,加速数据检索

使用步骤 1 中创建的向量嵌入填充向量索引,以加速后续的数据检索。

# 为了避免重复创建/填充向量索引,本单元格要求用户确认是否执行。
value = input("确认是否要重新填充向量索引?(y/n)")
if value == 'y':
    for index, row in df.iterrows():
        movie_id = row['movieId']
        embedding = row['movie_detail_embeddings']
        graph.query(f"MATCH (m:Movie {{id: '{movie_id}'}}) SET m.movie_detail_embeddings = {embedding}")

现在我们已经使用所需的向量索引和嵌入创建并更新了 GraphDB,现在我们可以使用以下代码对输入查询执行向量搜索:

result = graph.query("""
with $question_embedding as question_embedding
CALL db.index.vector.queryNodes(
    'movie_detail_embeddings',
    $top_k,
    question_embedding
    ) YIELD node AS movie, score
RETURN movie.title, movie.movie_detail, score
"""
,
params={
    "question_embedding":question_embeddings,
    "top_k":3
})

import pprint
pprint.pprint(result)

prompt = f"# Question:\n{question}\n\n# Graph DB search results:\n{result}"
messages = [
    {"role""system""content"str(
        "您将获得用户问题以及该问题在 Neo4j 图数据库上的搜索结果。请给出清晰准确的答案。"
    )},
    {"role""user""content": prompt}
]

response = llm_model.invoke(messages)
print(response.content)

上面的完整代码可以编译成一个简单的管道,如下所示:

def rag_pipeline(question:str):
    question_embeddings = embed_text(question)
    result = graph.query("""
with $question_embedding as question_embedding
CALL db.index.vector.queryNodes(
    'movie_detail_embeddings',
    $top_k,
    question_embedding
    ) YIELD node AS movie, score
RETURN movie.title, movie.tagline, score
"""
,
params={
    "question_embedding":question_embeddings,
    "top_k":5
})
    prompt = f"# Question:\n{question}\n\n# Graph DB search results:\n{result}"
    messages = [
    {"role""system""content"str(
        "您将获得用户问题以及该问题在 Neo4j 图数据库上的搜索结果。请给出清晰准确的答案。"
    )},
    {"role""user""content": prompt}
]
    response = llm_model.invoke(messages)
    return response

answer = rag_pipeline("What all are the movies about the Adventures ?")

既然我们已经探索了 GraphDB 上的检索增强生成 (RAG),那么重要的是要注意,虽然 RAG 和查询生成本身都很强大,但在检索相关内容方面都存在局限性。 为了克服这些挑战并构建更可靠的系统,可以使用混合方法:

  1. 1. Agentic 工作流:允许代理根据用户的问题动态选择 RAG 或查询生成。

  2. 2. 并行执行:这种混合方法结合了结构化数据的精确性和非结构化数据的丰富性,通过并行执行 RAG 和查询生成流程,并将其结果统一融合,从而弥补各自单独运用时可能存在的局限,构建一个更加可靠的系统。

这种混合方法使用结构化和非结构化数据,从而创建一个能够有效处理复杂查询的强大系统。

结论

在这篇博文中,我分享了我所了解的关于知识图谱、它们如何与 LLM 协同工作,以及如何使用它们为结构化数据构建聊天机器人。 我介绍了简单的方法,如 CypherQAChain,以及更高级的方法,如 GraphRAG,用于处理非结构化数据。 这些技术帮助我自动化了一个医疗保健项目,节省了数小时的人工操作时间,并在几分钟内提供了准确的答案。

我的目标是保持简单,以便初学者可以使用知识图谱开始使用聊天机器人。 通过尝试这些方法和代理工作流,您可以构建一个既高效又灵活的系统,能够轻松应对复杂查询。

这只是使用结构化数据的一个例子。 非结构化数据(如 PDF、文档甚至视频中的文本)是完全不同的挑战。 下一篇博文将介绍如何处理非结构化数据并构建知识图谱,敬请期待!

 



学习资源


若要了解更多知识图谱或图数据库相关教学,你可以查看公众号的其他文章:




活水智能成立于北京,致力于通过AI教育、AI软件和社群提高知识工作者生产力。中国AIGC产业联盟理事。

活水现有AI线下工作坊等10+门课程,15+AI软件上线,多款产品在研。知识星球中拥有2600+成员,涵盖大厂程序员、公司高管、律师等各类知识工作者。在北、上、广、深、杭、重庆等地均有线下组织。

欢迎加入我们的福利群,每周都有一手信息、优惠券发放、优秀同学心得分享,还有赠书活动~

👇🏻👇🏻👇🏻

 点击阅读原文,立即入群

文章转载自活水智能,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论