将Neo4j集成至LangChain生态系统
作者:Sec-Labs | 发布时间:
原文地址
https://towardsdatascience.com/integrating-neo4j-into-the-langchain-ecosystem-df0e988344d2
将Neo4j集成至LangChain生态系统
学习如何开发一个LangChain代理,使其能够以多种方式与Neo4j数据库进行交互

ChatGPT激发了全球并开创了新的AI革命。然而,最新的趋势似乎是为ChatGPT提供外部信息,以提高其准确性并使其能够回答公共数据集中没有答案的问题。在大型语言模型(LLMs)周围的另一个趋势是将它们转化为代理,使它们能够通过各种API调用或其他集成与其环境进行交互。
由于增强LLMs相对较新,因此尚没有很多开源库。然而,似乎用于构建围绕LLMs(如ChatGPT)的应用程序的“必备库”是名为LangChain的库。该库提供了通过为其提供各种工具和外部数据源来增强LLM的能力。它不仅可以通过访问外部数据来改善其响应,而且还可以充当代理并通过外部端点操纵其环境。
我偶然发现了Ibis Prevedello的一个LangChain项目,该项目使用图搜索通过提供额外的外部上下文来增强LLMs。
https://github.com/ibiscp/LLM-IMDB
Ibis的项目使用NetworkX库存储图形信息。我非常喜欢他的方法以及将图搜索轻松集成到LangChain生态系统中的方式。因此,我决定开发一个项目,将Neo4j图形数据库集成到LangChain生态系统中。
https://github.com/tomasonjo/langchain2neo4j
经过两周的编码,该项目现在可以让LangChain代理以三种不同的模式与Neo4j进行交互:
- 生成Cypher语句以查询数据库
- 相关实体的全文关键字搜索
- 向量相似度搜索
在本篇博客文章中,我将为您介绍我开发的每种方法的推理和实现过程。
环境设置
首先,我们将配置Neo4j环境。我们将使用Neo4j沙箱中的推荐项目中提供的数据集。最简单的解决方案是通过此链接创建一个Neo4j沙箱实例。但是,如果您更喜欢本地的Neo4j实例,您也可以恢复GitHub上可用的数据库转储。该数据集是MovieLens数据集的一部分[1],具体而言是小版本。
在实例化Neo4j数据库之后,我们应该有一个带有以下模式的图形填充。

接下来,您需要通过执行以下命令来克隆langchain2neo4j存储库:
git clone https://github.com/tomasonjo/langchain2neo4j
在下一步中,您需要创建一个.env文件,并按.env.example文件中所示填充neo4j和OpenAI凭据。
最后,您需要在Neo4j中创建一个全文索引,并通过运行以下命令导入电影标题嵌入:
sh seed_db.sh
如果您是Windows用户,则seed_db脚本可能不起作用。在这种情况下,我准备了一个Jupyter笔记本,可以帮助您作为shell脚本的替代方法来填充数据库。
现在,让我们跳转到LangChain集成。
据我所见,使用LangChain代理回答用户问题的最常见数据流程如下图所示:

当代理从用户接收到输入时,代理数据流程就会启动。代理然后向LLM模型发送请求,该请求包括用户问题和代理提示,代理提示是一组自然语言中的指令,代理应该遵循这些指令。接着,LLM以进一步的指令回应代理。最常见的第一次回应是使用任何可用工具从外部源获取额外的信息。但是,工具不仅限于只读操作。例如,您可以使用它们更新数据库。工具返回其他上下文后,会再次调用LLM,该调用包括新获得的信息。LLM现在可以选择生成返回给用户的最终答案,或者它可以决定需要通过其可用工具执行更多操作。
LangChain代理使用LLMs进行推理。因此,第一步是定义要使用的模型。目前,langchain2neo4j项目仅支持OpenAI的聊天完成模型,具体而言是GPT-3.5-turbo和GPT-4模型。
if model_name in ['gpt-3.5-turbo', 'gpt-4']:
llm = ChatOpenAI(temperature=0, model_name=model_name)
else:
raise Exception(f"Model {model_name} is currently not supported")
我还没有探索OpenAI之外的其他LLMs。但是,使用LangChain应该很容易,因为它与其他十多个LLMs进行了集成。我不知道存在那么多的LLMs。
集成相关文档:https://python.langchain.com/en/latest/modules/models/llms/integrations.html
接下来,我们需要添加以下行来添加对话记忆:
memory = ConversationBufferMemory(
memory_key="chat_history", return_messages=True)
LangChain支持多种类型的代理。例如,某些代理可以使用记忆组件,而其他代理则不能。由于目的是构建一个聊天机器人,因此我选择了Conversation Agent (for Chat Models)代理类型。LangChain库有趣的是,有一半的代码是用Python编写的,而另一半是提示工程。我们可以探索[会话代理使用的提示](https://github.com/hwchase17/langchain/blob/master/langchain/agents/ conversational_chat / prompt.py)。例如,代理有一些基本的指令必须遵循:
Assistant is a large language model trained by OpenAI. Assistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand. Assistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics. Overall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist.
作为补充,代理还有指令,如果需要,可以使用任何指定的工具。
Assistant can ask the user to use tools to look up information
that may be helpful in answering the users original question.
The tools the human can use are:
{{tools}}
{format_instructions}
USER'S INPUT - - - - - - - - - -
Here is the user's input
(remember to respond with a markdown code snippet of a
json blob with a single action, and NOTHING else):
{{{{input}}}}
有趣的是,提示说明代理可以要求用户使用工具查找其他信息。但是,用户不是人类,而是构建在LangChain库之上的应用程序。因此,在不需要人类干预的情况下,整个查找进一步信息的过程都是自动完成的。当然,我们可以根据需要更改提示。提示还包括LLMs应该使用的格式与代理通信。
请注意,代理提示不包括如果工具返回的上下文中没有提供答案,则代理不应回答问题的内容。
现在,我们需要定义可用的工具。如上所述,我准备了三种与Neo4j数据库交互的方法。
tools = [
Tool(
name="Cypher search",
func=cypher_tool.run,
description="""
Utilize this tool to search within a movie database,
specifically designed to answer movie-related questions.
This specialized tool offers streamlined search capabilities
to help you find the movie information you need with ease.
Input should be full question.""",
),
Tool(
name="Keyword search",
func=fulltext_tool.run,
description="Utilize this tool when explicitly told to use
keyword search.Input should be a list of relevant movies
inferred from the question.",
),
Tool(
name="Vector search",
func=vector_tool.run,
description="Utilize this tool when explicity told to use
vector search.Input should be full question.Do not include
agent instructions.",
),
]
工具的描述用于指定工具的功能,并通知代理何时使用它。此外,我们还需要指定工具期望的输入格式。例如,Cypher和向量搜索都期望完整的问题作为输入,而关键字搜索则期望一组相关电影作为输入。
LangChain与我在编码方面所用的方式非常不同。它使用提示指导LLMs为您完成工作,而不是自己编写代码。例如,关键字搜索指示ChatGPT提取相关电影并将其用作输入。我花了2个小时调试工具的输入格式,然后才意识到我可以使用自然语言指定它,LLM将处理其余部分。
还记得我提到过代理没有指令,不应回答答案没有在上下文中提供的问题吗?让我们来看看以下对话。

LLM根据工具描述决定无法使用任何工具检索相关上下文。但是的,LLM有很多默认信息,并且由于代理没有任何约束,即只能依赖外部信息,因此LLM可以独立形成答案。如果我们想强制执行不同的行为,我们需要更改代理提示。
生成Cypher语句
我已经使用OpenAI的聊天模型(如GPT-3.5-turbo和GPT-4)生成Cypher语句开发了一个与Neo4j数据库交互的聊天机器人。因此,我可以借鉴大部分想法来实现一种工具,使LangChain代理可以通过构建Cypher语句从Neo4j数据库检索信息。
旧模型,如text-davinci-003和GPT-3.5-turbo,作为few-shot Cypher生成器工作得更好,其中我们提供了几个Cypher示例,模型可以使用这些示例生成新的Cypher语句。但是,当我们仅提供图形模式时,GPT-4似乎效果更好。因此,由于可以使用Cypher查询提取图形模式,因此可以在理论上在任何图形模式上使用GPT-4,而无需人工进行手动工作。
我不会向您展示LangChain在幕后执行的内容。我们只需查看当LangChain代理决定使用Cypher语句与Neo4j数据库交互时执行的功能即可。
def _call(self, inputs: Dict[str, str]) -> Dict[str, str]:
chat_prompt = ChatPromptTemplate.from_messages(
[self.system_prompt] + inputs['chat_history'] + [self.human_prompt])
cypher_executor = LLMChain(
prompt=chat_prompt, llm=self.llm, callback_manager=self.callback_manager
)
cypher_statement = cypher_executor.predict(
question=inputs[self.input_key], stop=["Output:"])
# If Cypher statement was not generated due to lack of context
if not "MATCH" in cypher_statement:
return {'answer': 'Missing context to create a Cypher statement'}
context = self.graph.query(cypher_statement)
return {'answer': context}
Cypher生成工具将问题和聊天历史记录作为输入。然后,使用system消息、chat history和当前问题将输入组合。我为Cypher生成工具准备了以下system消息提示:
SYSTEM_TEMPLATE = """
You are an assistant with an ability to generate Cypher queries based off
example Cypher queries. Example Cypher queries are:\n""" + examples + """\n
Do not response with any explanation or any other information except the
Cypher query. You do not ever apologize and strictly generate cypher statements
based of the provided Cypher examples. Do not provide any Cypher statements
that can't be inferred from Cypher examples. Inform the user when you can't
infer the cypher statement due to the lack of context of the conversation
and state what is the missing context.
"""
提示工程更像是一门艺术而不是科学。在这个例子中,我们向LLM提供了一些Cypher语句示例,让它根据这些信息生成Cypher语句。此外,我们设置了一些约束,例如仅允许它构造可以从训练示例中推断出的Cypher语句。此外,我们不让模型道歉或解释其思想(但是,GPT-3.5-turbo不会听从该指令)。最后,如果问题缺乏上下文,我们允许模型回答该信息,而不是强制它生成Cypher语句。
在LLM构建Cypher语句之后,我们只需将其用于查询Neo4j数据库,并将结果返回给代理。这是一个示例流程。

当用户输入问题时,它将与代理提示一起发送到LLM。在这个例子中,LLM回复需要使用Cypher search工具。Cypher搜索工具构造一个Cypher语句并使用它来查询Neo4j。然后将查询结果传递回代理。接下来,代理向LLM发送另一个请求,同时提供新的上下文。由于上下文包含构建答案所需的信息,LLM形成最终答案并指示代理将其返回给用户。
当然,我们现在可以问一些跟进问题。

由于代理具有记忆功能,它知道第二个演员是谁,因此可以将该信息传递给Cypher搜索工具,以构造适当的Cypher语句。
查找相关三元组的关键字搜索
我从现有的知识图谱索引实现中获得了关键字搜索的想法,包括LangChain和GPT-index库。这两种实现非常相似。它们要求LLM从问题中提取相关实体,并在图形中搜索包含这些实体的任何三元组。因此,我认为我们可以在Neo4j中使用类似的方法。但是,虽然我们可以使用简单的MATCH语句搜索实体,但我决定使用Neo4j的全文索引更好。在使用全文索引找到相关实体之后,我们将返回三元组,并希望其中包含回答问题所需的相关信息。
def _call(self, inputs: Dict[str, str]) -> Dict[str, Any]:
"""Extract entities, look up info and answer question."""
question = inputs[self.input_key]
params = generate_params(question)
context = self.graph.query(
fulltext_search, {'query': params})
return {self.output_key: context}
请记住,代理已经有指令来解析相关电影标题并将其用作关键字搜索工具的输入。因此,我们不必处理这个问题。但是,由于问题中可能存在多个实体,因此我们必须构造适当的Lucene查询参数,因为全文索引基于Lucene。然后,我们只需查询全文索引并返回希望相关的三元组。我们使用的Cypher语句如下:
CALL db.index.fulltext.queryNodes("movie", $query)
YIELD node, score
WITH node, score LIMIT 5
CALL {
WITH node
MATCH (node)-[r:!RATED]->(target)
RETURN coalesce(node.name, node.title) + " " + type(r) + " " + coalesce(target.name, target.title) AS result
UNION
WITH node
MATCH (node)<-[r:!RATED]-(target)
RETURN coalesce(target.name, target.title) + " " + type(r) + " " + coalesce(node.name, node.title) AS result
}
RETURN result LIMIT 100
因此,我们获取全文索引返回的前五个相关实体。接下来,我们通过遍历它们的邻居生成三元组。我特别排除了RATED关系的遍历,因为它们包含不相关的信息。我还没有探索过,但我有一个很好的感觉,我们也可以指示LLM提供要调查的相关关系列表以及适当的实体,这将使我们的关键字搜索更加专注。可以通过明确指示代理来启动关键字搜索。

LLM被指示使用keyword search工具。此外,代理被告知向关键字搜索提供一个相关实体的列表作为输入,这个列表在本例中只有Pokemon。然后使用Lucene参数查询Neo4j。这种方法投出了更广泛的网,希望提取的三元组包含相关信息。例如,检索到的上下文包括有关Pokemon类型的信息,这是不相关的。仍然,它也有关于谁出演电影的信息,这使得代理能够回答用户的问题。
正如提到的,我们可以指示LLM生成与适当实体相关的关系类型列表,这可以帮助代理检索更相关的信息。
向量相似度搜索
向量相似度搜索是我们将要检查的最后一种与Neo4j数据库交互的模式。向量搜索目前很流行。例如,LangChain提供了与十多个向量数据库的集成。向量相似度搜索的思路是将一个问题嵌入到嵌入空间中,并根据问题和文档的嵌入相似性找到相关的文档。我们只需要谨慎使用相同的嵌入模型来生成文档和问题的向量表示。我在向量搜索实现中使用了OpenAI的嵌入。
def _call(self, inputs: Dict[str, str]) -> Dict[str, Any]:
"""Embed a question and do vector search."""
question = inputs[self.input_key]
embedding = self.embeddings.embed_query(question)
context = self.graph.query(
vector_search, {'embedding': embedding})
return {self.output_key: context}
因此,我们首先将问题嵌入。接下来,我们使用该嵌入来在数据库中查找相关电影。通常,向量数据库返回相关文档的文本。但是,我们正在处理图形数据库。因此,我决定使用三元组结构生成相关信息。使用的Cypher语句是:
WITH $embedding AS e
MATCH (m:Movie)
WHERE m.embedding IS NOT NULL AND size(m.embedding) = 1536
WITH m, gds.similarity.cosine(m.embedding, e) AS similarity
ORDER BY similarity DESC LIMIT 5
CALL {
WITH m
MATCH (m)-[r:!RATED]->(target)
RETURN coalesce(m.name, m.title) + " " + type(r) + " " + coalesce(target.name, target.title) AS result
UNION
WITH m
MATCH (m)<-[r:!RATED]-(target)
RETURN coalesce(target.name, target.title) + " " + type(r) + " " + coalesce(m.name, m.title) AS result
}
RETURN result LIMIT 100
这个Cypher语句与关键字搜索示例类似。唯一的区别是我们使用余弦相似度而不是全文索引来识别相关电影。当处理数万到数十万个文档时,这种方法已经足够好了。请记住,瓶颈通常是LLM,特别是如果您使用GPT-4。因此,如果您没有处理数百万个文档,您不必考虑多语种实现,在那里您既有向量数据库又有图形数据库,以便能够通过遍历图形来生成相关信息。

当代理被指示使用vector search工具时,第一步是将问题嵌入为向量。OpenAI的嵌入模型生成的向量表示维数为1536。因此,下一步是使用构造的向量,并通过计算问题和相关文档或节点之间的余弦相似度在数据库中搜索相关信息。同样,由于我们处理的是图形数据库,我决定以三元组的形式将信息返回给代理。
有趣的是,即使我们指示代理搜索指环王电影,向量相似度搜索也返回了有关霍比特人电影的信息。看起来,指环王和霍比特人电影在嵌入空间中是相近的,这是可以理解的。
总结
看起来,可以访问外部工具和信息的聊天机器人和生成代理是继原始ChatGPT热潮之后的下一个浪潮。具有向LLM提供附加上下文的能力可以极大地改善其结果。此外,代理的工具不仅限于只读操作,这意味着它们可以更新数据库甚至在亚马逊上下订单。大部分情况下,LangChain库似乎是目前用于实现生成代理的主要库。当您开始使用LangChain时,您可能需要在编码过程中进行一些转变,因为您需要将LLM提示与代码结合起来完成任务。例如,LLM和工具之间的消息可以使用自然语言指令来塑造和重新塑造,而不是使用Python代码。我希望这个项目可以帮助您将Neo4j这样的图形数据库的功能实现到您的LangChain项目中。
像往常一样,代码可以在GitHub上找到。