网站搜索

使用 LangChain 构建 LLM RAG 聊天机器人


您可能与大型语言模型 (LLM) 进行过交互,例如 OpenAI 的 ChatGPT 背后的模型,并体验过它们回答问题、总结文档、编写代码等的卓越能力。虽然法学硕士本身就很出色,但只要具备一点编程知识,您就可以利用 LangChain 等库来创建自己的由法学硕士支持的聊天机器人,它几乎可以做任何事情。

在企业环境中,创建 LLM 支持的聊天机器人最流行的方法之一是通过检索增强生成 (RAG)。当您设计 RAG 系统时,您使用检索模型通常从数据库或语料库中检索相关信息,并将检索到的信息提供给法学硕士以生成上下文相关的响应。

在本教程中,您将扮演在大型医院系统工作的人工智能工程师的角色。您将在 LangChain 中构建一个 RAG 聊天机器人,该机器人使用 Neo4j 检索医院系统中有关患者、患者体验、医院位置、就诊、保险付款人和医生的数据。

在本教程中,您将学习如何

  • 使用LangChain构建自定义聊天机器人
  • 利用您对业务需求和医院系统数据的理解来设计聊天机器人
  • 使用图形数据库
  • 设置 Neo4j AuraDB 实例
  • 构建一个 RAG 聊天机器人,从 Neo4j 检索结构化非结构化数据
  • 使用FastAPIStreamlit部署您的聊天机器人

点击下面的链接下载该项目的完整源代码和数据:

演示:使用 LangChain 和 Neo4j 的 LLM RAG 聊天机器人

在本教程结束时,您将拥有一个为您的 LangChain 聊天机器人提供服务的 REST API。您还将拥有一个 Streamlit 应用程序,它提供了一个漂亮的聊天界面来与您的 API 进行交互:

在幕后,Streamlit 应用程序将您的消息发送到聊天机器人 API,聊天机器人生成响应并将其发送回 Streamlit 应用程序,后者将其显示给用户。

您稍后将深入了解聊天机器人可以访问的数据,但如果您急于测试它,您可以提出类似于侧边栏中给出的示例的问题:

您将学习如何处理每个步骤,从了解业务需求和数据到构建 Streamlit 应用程序。本教程中有很多内容需要解开,但不要感到不知所措。您将获得有关所介绍的每个概念的一些背景知识,以及可加深您理解的外部资源的链接。现在,是时候深入了解了!

先决条件

本教程最适合想要获得创建自定义聊天机器人实践经验的中级 Python 开发人员。除了中级 Python 知识之外,您还可以从对以下概念和技术的高级理解中受益:

  • 大型语言模型 (LLM) 和即时工程
  • 文本嵌入和矢量数据库
  • 图数据库和 Neo4j
  • OpenAI 开发者生态系统
  • REST API 和 FastAPI
  • 异步编程
  • Docker 和 Docker 组合

上面列出的任何内容都不是硬性先决条件,因此,如果您对其中任何一个都不了解,请不要担心。在此过程中,您将了解每个概念和技术。此外,学习这些先决条件的最好方法莫过于在本教程中亲自实现它们。

接下来,您将获得简短的项目概述并开始了解 LangChain。

项目概况

在本教程中,您将创建一些构成最终聊天机器人的目录。以下是每个目录的详细信息:

  • langchain_intro/ 将帮助您熟悉 LangChain 并为您提供构建演示中看到的聊天机器人所需的工具,并且它不会包含在您的最终聊天机器人中。您将在第 1 步中介绍这一点。

  • data/ 将原始医院系统数据存储为 CSV 文件。您将在步骤 2 中探索这些数据。在步骤 3 中,您将这些数据移动到 Neo4j 数据库中,您的聊天机器人将查询该数据库来回答问题。

  • hospital_neo4j_etl/ 包含一个脚本,可将 data/ 中的原始数据加载到 Neo4j 数据库中。您必须在构建聊天机器人之前运行它,并且您将在步骤 3 中了解有关设置 Neo4j 实例所需的所有信息。

  • chatbot_api/ 是您的 FastAPI 应用程序,它将您的聊天机器人作为 REST 端点提供服务,它是该项目的核心可交付成果。 chatbot_api/src/agents/chatbot_api/src/chains/ 子目录包含构成聊天机器人的 LangChain 对象。稍后你会了解什么是代理和链,但现在只需知道你的聊天机器人实际上是一个由链和函数组成的 LangChain 代理。

  • tests/ 包含两个脚本,用于测试聊天机器人回答一系列问题的速度。这将使您了解通过向 OpenAI 等 LLM 提供商发出异步请求可以节省多少时间。

  • chatbot_frontend/ 是与 chatbot_api/ 中的聊天机器人端点交互的 Streamlit 应用程序。这是您在演示中看到的 UI,您将在步骤 5 中构建它。

构建和运行聊天机器人所需的所有环境变量都将存储在 .env 文件中。您将把 hospital_neo4j_etl/chatbot_apichatbot_frontend 中的代码部署为 Docker 容器,并使用 Docker Compose 进行编排。如果您想在完成本教程的其余部分之前尝试聊天机器人,那么您可以下载材料并按照自述文件中的说明进行操作:

了解了项目概述和先决条件后,您就可以开始第一步了——熟悉 LangChain。

步骤一:熟悉浪链

在设计和开发聊天机器人之前,您需要了解如何使用LangChain。在本节中,您将通过构建医院系统聊天机器人的初步版本来了解 LangChain 的主要组件和功能。这将为您提供构建完整聊天机器人所需的所有工具。

使用您最喜欢的代码编辑器创建一个新的 Python 项目,并确保为其依赖项创建一个虚拟环境。确保您安装了 Python 3.10 或更高版本。激活您的虚拟环境并安装以下库:

(venv) $ python -m pip install langchain==0.1.0 openai==1.7.2 langchain-openai==0.0.2 langchain-community==0.0.12 langchainhub==0.1.14

您还需要安装 python-dotenv 来帮助您管理环境变量:

(venv) $ python -m pip install python-dotenv

Python-dotenv 将环境变量从 .env 文件加载到您的 Python 环境中,在开发聊天机器人时您会发现这很方便。但是,您最终将使用 Docker 部署聊天机器人,它可以为您处理环境变量,并且您将不再需要 Python-dotenv。

如果您尚未下载,则需要从本教程的材料或 GitHub 存储库中下载 reviews.csv

接下来,打开项目目录并添加以下文件夹和文件:

./
│
├── data/
│   └── reviews.csv
│
├── langchain_intro/
│   ├── chatbot.py
│   ├── create_retriever.py
│   └── tools.py
│
└── .env

data/ 中的 reviews.csv 文件是您刚刚下载的文件,您看到的其余文件应该是空的。

现在您已经准备好开始使用 LangChain 构建您的第一个聊天机器人了!

聊天模型

你可能已经猜到,LangChain的核心组成部分是LLM。 LangChain 提供了一个模块化接口,用于与 OpenAI、Cohere、HuggingFace、Anthropic、Together AI 等 LLM 提供商合作。在大多数情况下,您只需要 LLM 提供商提供的 API 密钥即可开始将 LLM 与 LangChain 结合使用。 LangChain还支持在您自己的机器上托管的LLM或其他语言模型。

在本教程中,您将使用 OpenAI,但请记住,有许多优秀的开源和闭源提供商。您始终可以测试不同的提供商并根据应用程序的需求和成本限制进行优化。在继续之前,请确保您已注册 OpenAI 帐户并且拥有有效的 API 密钥。

获得 OpenAI API 密钥后,将其添加到您的 .env 文件中:

OPENAI_API_KEY=<YOUR-OPENAI-API-KEY>

虽然您可以直接与 LangChain 中的 LLM 对象进行交互,但更常见的抽象是聊天模型。聊天模型在底层使用 LLM,但它们是为对话而设计的,并且它们与聊天消息而不是原始文本交互。

使用聊天消息,您可以向法学硕士提供有关您发送的消息类型的更多详细信息。所有消息都具有 rolecontent 属性。 角色告诉LLM谁正在发送消息,内容是消息本身。以下是最常用的消息:

  • HumanMessage:来自用户与语言模型交互的消息。
  • AIMessage:来自语言模型的消息。
  • SystemMessage:告诉语言模型如何行为的消息。并非所有提供程序都支持SystemMessage

还有其他消息类型,例如 FunctionMessageToolMessage,但在构建代理时您将了解更多有关这些类型的信息。

LangChain 中的聊天模型入门非常简单。要实例化 OpenAI 聊天模型,请导航到 langchain_intro 并将以下代码添加到 chatbot.py

import dotenv
from langchain_openai import ChatOpenAI

dotenv.load_dotenv()

chat_model = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)

首先导入 dotenvChatOpenAI。然后,您调用 dotenv.load_dotenv() 来读取并存储 .env 中的环境变量。默认情况下,dotenv.load_dotenv() 假定 .env 位于当前工作目录中,但如果 .env 则可以将路径传递到其他目录code> 位于其他地方。

然后,您使用 GPT 3.5 Turbo 作为基础 LLM 实例化一个 ChatOpenAI 模型,并将 温度 设置为 0。OpenAI 提供了多种具有不同价位、功能和性能的模型。表演。 GPT 3.5 Turbo 是一个很好的入门型号,因为它在许多用例中表现良好,并且比 GPT 4 及更高版本等最新型号更便宜。

要使用 chat_model,请打开项目目录,启动 Python 解释器,然后运行以下代码:

>>> from langchain.schema.messages import HumanMessage, SystemMessage
>>> from langchain_intro.chatbot import chat_model

>>> messages = [
...     SystemMessage(
...         content="""You're an assistant knowledgeable about
...         healthcare. Only answer healthcare-related questions."""
...     ),
...     HumanMessage(content="What is Medicaid managed care?"),
... ]
>>> chat_model.invoke(messages)
AIMessage(content='Medicaid managed care is a healthcare delivery system
in which states contract with managed care organizations (MCOs) to provide
healthcare services to Medicaid beneficiaries. Under this system, MCOs are
responsible for coordinating and delivering healthcare services to enrollees,
including primary care, specialty care, hospital services, and prescription
drugs. Medicaid managed care aims to improve care coordination, control costs,
and enhance the quality of care for Medicaid beneficiaries.')

在此块中,您导入 HumanMessageSystemMessage 以及您的聊天模型。然后,您使用 SystemMessageHumanMessage 定义一个列表,并使用 chat_model.invoke() 通过 chat_model 运行它们。在后台,chat_model 向服务 gpt-3.5-turbo-0125 的 OpenAI 端点发出请求,结果以 AIMessage 形式返回。

正如您所看到的,聊天模型回答了 HumanMessage 中提供的什么是 Medicaid 管理式医疗?。您可能想知道聊天模型在此上下文中对 SystemMessage 做了什么。请注意当您提出以下问题时会发生什么:

>>> messages = [
...     SystemMessage(
...         content="""You're an assistant knowledgeable about
...         healthcare. Only answer healthcare-related questions."""
...     ),
...     HumanMessage(content="How do I change a tire?"),
... ]
>>> chat_model.invoke(messages)
AIMessage(content='I apologize, but I can only provide assistance
and answer questions related to healthcare.')

如前所述,SystemMessage 告诉模型如何行为。在本例中,您告诉模型仅回答与医疗保健相关的问题。这就是为什么它拒绝告诉您如何更换轮胎。通过文本指令控制法学硕士与用户的关系的能力非常强大,这是通过提示工程创建定制聊天机器人的基础。

虽然聊天消息是一个很好的抽象,并且有助于确保您向 LLM 提供正确类型的消息,但您也可以将原始字符串传递到聊天模型中:

>>> chat_model.invoke("What is blood pressure?")
AIMessage(content='Blood pressure is the force exerted by
the blood against the walls of the blood vessels, particularly
the arteries, as it is pumped by the heart. It is measured in
millimeters of mercury (mmHg) and is typically expressed as two
numbers: systolic pressure over diastolic pressure. The systolic
pressure represents the force when the heart contracts and pumps
blood into the arteries, while the diastolic pressure represents
the force when the heart is at rest between beats. Blood pressure
is an important indicator of cardiovascular health and can be influenced
by various factors such as age, genetics, lifestyle, and underlying medical
conditions.')

在此代码块中,您将字符串 What is BloodPress? 直接传递给 chat_model.invoke()。如果您想在此处不使用 SystemMessage 的情况下控制 LLM 的行为,则可以在字符串输入中包含指令。

接下来,您将学习一种模块化方法来指导模型的响应,就像您对 SystemMessage 所做的那样,从而更轻松地自定义聊天机器人。

提示模板

LangChain允许您使用提示模板为聊天机器人设计模块化提示。引用 LangChain 的文档,您可以将提示模板视为为语言模型生成提示的预定义配方。

假设您想构建一个聊天机器人,根据患者的评论回答有关患者体验的问题。提示模板可能如下所示:

>>> from langchain.prompts import ChatPromptTemplate

>>> review_template_str = """Your job is to use patient
... reviews to answer questions about their experience at a hospital.
... Use the following context to answer questions. Be as detailed
... as possible, but don't make up any information that's not
... from the context. If you don't know an answer, say you don't know.
...
... {context}
...
... {question}
... """

>>> review_template = ChatPromptTemplate.from_template(review_template_str)

>>> context = "I had a great stay!"
>>> question = "Did anyone have a positive experience?"

>>> review_template.format(context=context, question=question)
"Human: Your job is to use patient\nreviews to answer questions about
 their experience at a hospital.\nUse the following context to
 answer questions. Be as detailed\nas possible, but don't make
 up any information that's not\nfrom the context. If you don't
 know an answer, say you don't know.\n\nI had a great
 stay!\n\nDid anyone have a positive experience?\n"

您首先导入 ChatPromptTemplate 并定义 review_template_str,其中包含您将传递给模型的指令,以及变量 context LangChain 用大括号 ({}) 分隔的替换字段中的 >question。然后,您可以使用类方法 .from_template()review_template_str 创建一个 ChatPromptTemplate 对象。

实例化 review_template 后,您可以使用 review_template.format()contextquestion 传递到字符串模板中。结果可能看起来您只是做了标准的 Python 字符串插值,但提示模板具有许多有用的功能,允许它们与聊天模型集成。

请注意您之前对 review_template.format() 的调用如何生成一个以 Human 开头的字符串。这是因为 ChatPromptTemplate.from_template() 默认情况下假定字符串模板是人工消息。要更改此设置,您可以为您希望模型处理的每条聊天消息创建更详细的提示模板:

>>> from langchain.prompts import (
...     PromptTemplate,
...     SystemMessagePromptTemplate,
...     HumanMessagePromptTemplate,
...     ChatPromptTemplate,
... )

>>> review_system_template_str = """Your job is to use patient
... reviews to answer questions about their experience at a
... hospital. Use the following context to answer questions.
... Be as detailed as possible, but don't make up any information
... that's not from the context. If you don't know an answer, say
... you don't know.
...
... {context}
... """

>>> review_system_prompt = SystemMessagePromptTemplate(
...     prompt=PromptTemplate(
...         input_variables=["context"], template=review_system_template_str
...     )
... )

>>> review_human_prompt = HumanMessagePromptTemplate(
...     prompt=PromptTemplate(
...         input_variables=["question"], template="{question}"
...     )
... )

>>> messages = [review_system_prompt, review_human_prompt]
>>> review_prompt_template = ChatPromptTemplate(
...     input_variables=["context", "question"],
...     messages=messages,
... )
>>> context = "I had a great stay!"
>>> question = "Did anyone have a positive experience?"

>>> review_prompt_template.format_messages(context=context, question=question)
[SystemMessage(content="Your job is to use patient\nreviews to answer
 questions about their experience at a\nhospital. Use the following context
 to answer questions.\nBe as detailed as possible, but don't make up any
 information\nthat's not from the context. If you don't know an answer, say
 \nyou don't know.\n\nI had a great stay!\n"), HumanMessage(content='Did anyone
 have a positive experience?')]

在此块中,您将为 HumanMessageSystemMessage 导入单独的提示模板。然后,您定义一个字符串 review_system_template_str,它用作 SystemMessage 的模板。请注意如何在 review_system_template_str 中仅声明一个 context 变量。

由此,您可以创建 review_system_prompt,这是专门针对 SystemMessage 的提示模板。接下来,您为 HumanMessage 创建一个 review_ human_prompt。请注意 template 参数只是一个带有 question 变量的字符串。

然后,您将 review_system_promptreview_ human_prompt 添加到名为 messages 的列表中,并创建 review_prompt_template,这是包含以下内容的最终对象: SystemMessageHumanMessage 的提示模板。调用 review_prompt_template.format_messages(context=context, Question=question) 会生成一个包含 SystemMessageHumanMessage 的列表,可以将其传递到聊天模型。

要了解如何结合聊天模型和提示模板,您将使用 LangChain 表达式语言 (LCEL) 构建一条链。这可以帮助您解锁LangChain在聊天模型上构建模块化定制界面的核心功能。

链和LangChain表达语言(LCEL)

连接LangChain中的聊天模型、提示等对象的粘合剂就是链。链只不过是LangChain中对象之间的一系列调用。建链的推荐方法是使用 LangChain 表达式语言(LCEL)。

要了解其工作原理,请查看如何使用聊天模型和提示模板创建链:

import dotenv
from langchain_openai import ChatOpenAI
from langchain.prompts import (
    PromptTemplate,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
    ChatPromptTemplate,
)

dotenv.load_dotenv()

review_template_str = """Your job is to use patient
reviews to answer questions about their experience at
a hospital. Use the following context to answer questions.
Be as detailed as possible, but don't make up any information
that's not from the context. If you don't know an answer, say
you don't know.

{context}
"""

review_system_prompt = SystemMessagePromptTemplate(
    prompt=PromptTemplate(
        input_variables=["context"],
        template=review_template_str,
    )
)

review_human_prompt = HumanMessagePromptTemplate(
    prompt=PromptTemplate(
        input_variables=["question"],
        template="{question}",
    )
)
messages = [review_system_prompt, review_human_prompt]

review_prompt_template = ChatPromptTemplate(
    input_variables=["context", "question"],
    messages=messages,
)

chat_model = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)

review_chain = review_prompt_template | chat_model

第 1 行到第 42 行是您已经完成的操作。也就是说,您定义 review_prompt_template 这是一个用于回答有关患者评论问题的提示模板,并实例化 gpt-3.5-turbo-0125 聊天模型。在第 44 行中,您使用 | 符号定义 review_chain,该符号用于将 review_prompt_templatechat_model 链接在一起。

这将创建一个对象 review_chain,它可以在单个函数调用中通过 review_prompt_templatechat_model 传递问题。从本质上讲,这抽象了 review_chain 的所有内部细节,允许您与链进行交互,就像它是一个聊天模型一样。

保存更新的 chatbot.py 后,在您的基础项目文件夹中启动一个新的 REPL 会话。以下是使用 review_chain 的方法:

>>> from langchain_intro.chatbot import review_chain

>>> context = "I had a great stay!"
>>> question = "Did anyone have a positive experience?"

>>> review_chain.invoke({"context": context, "question": question})
AIMessage(content='Yes, the patient had a great stay and had a
positive experience at the hospital.')

在此块中,您像以前一样导入 review_chain 并定义 contextquestion。然后,您将带有键 contextquestion 的字典传递给 review_chan.invoke()。这会通过提示模板和聊天模型传递上下文问题以生成答案。

一般来说,LCEL 允许您使用管道符号 (|) 创建任意长度的链。例如,如果您想格式化模型的响应,那么您可以将输出解析器添加到链中:

import dotenv
from langchain_openai import ChatOpenAI
from langchain.prompts import (
    PromptTemplate,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
    ChatPromptTemplate,
)
from langchain_core.output_parsers import StrOutputParser

# ...

output_parser = StrOutputParser()

review_chain = review_prompt_template | chat_model | output_parser

在这里,您将一个 StrOutputParser() 实例添加到 review_chain,这将使模型的响应更具可读性。启动一个新的 REPL 会话并尝试一下:

>>> from langchain_intro.chatbot import review_chain

>>> context = "I had a great stay!"
>>> question = "Did anyone have a positive experience?"

>>> review_chain.invoke({"context": context, "question": question})
'Yes, the patient had a great stay and had a
positive experience at the hospital.'

此块与之前相同,只是现在您可以看到 review_chain 返回一个格式良好的字符串,而不是 AIMessage

链条的力量在于它们为您提供的创造力和灵活性。您可以将复杂的管道链接在一起来创建聊天机器人,最终得到一个在单个方法调用中执行管道的对象。接下来,您将另一个对象分层到 review_chain 中,以从矢量数据库中检索文档。

检索对象

review_chain 的目标是根据患者的评论回答有关患者在医院的体验的问题。到目前为止,您已手动将评论作为问题的上下文传递。虽然这适用于少量评论,但它的扩展性不佳。此外,即使您可以将所有评论放入模型的上下文窗口中,也不能保证它在回答问题时会使用正确的评论。

为了克服这个问题,你需要一只猎犬。检索相关文档并将其传递给语言模型来回答问题的过程称为检索增强生成(RAG)。

在此示例中,您将把所有评论存储在名为 ChromaDB 的矢量数据库中。如果您不熟悉此数据库工具和主题,请先查看 ChromaDB 的嵌入和矢量数据库,然后再继续。

您可以使用以下命令安装 ChromaDB:

(venv) $ python -m pip install chromadb==0.4.22

安装后,您可以使用以下代码创建包含患者评论的 ChromaDB 矢量数据库:

import dotenv
from langchain.document_loaders.csv_loader import CSVLoader
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

REVIEWS_CSV_PATH = "data/reviews.csv"
REVIEWS_CHROMA_PATH = "chroma_data"

dotenv.load_dotenv()

loader = CSVLoader(file_path=REVIEWS_CSV_PATH, source_column="review")
reviews = loader.load()

reviews_vector_db = Chroma.from_documents(
    reviews, OpenAIEmbeddings(), persist_directory=REVIEWS_CHROMA_PATH
)

在第 2 行到第 4 行中,导入创建矢量数据库所需的依赖项。然后,您定义 REVIEWS_CSV_PATHREVIEWS_CHROMA_PATH,它们分别是存储原始评论数据的路径和矢量数据库存储数据的路径。

稍后您将获得医院系统数据的概述,但现在您需要知道的是 reviews.csv 存储患者评论。 reviews.csv 中的 review 列是包含患者评论的字符串。

在第 11 行和第 12 行中,您使用 LangChain 的 CSVLoader 加载评论。在第 14 至 16 行中,您使用默认 OpenAI 嵌入模型从评论创建 ChromaDB 实例,并将评论嵌入存储在 REVIEWS_CHROMA_PATH 中。

接下来,打开终端并从项目目录运行以下命令:

(venv) $ python langchain_intro/create_retriever.py

它应该只需要一分钟左右的时间来运行,然后您可以开始对评论嵌入执行语义搜索:

>>> import dotenv
>>> from langchain_community.vectorstores import Chroma
>>> from langchain_openai import OpenAIEmbeddings

>>> REVIEWS_CHROMA_PATH = "chroma_data/"

>>> dotenv.load_dotenv()
True

>>> reviews_vector_db = Chroma(
...     persist_directory=REVIEWS_CHROMA_PATH,
...     embedding_function=OpenAIEmbeddings(),
... )

>>> question = """Has anyone complained about
...            communication with the hospital staff?"""
>>> relevant_docs = reviews_vector_db.similarity_search(question, k=3)

>>> relevant_docs[0].page_content
'review_id: 73\nvisit_id: 7696\nreview: I had a frustrating experience
at the hospital. The communication between the medical staff and me was
unclear, leading to misunderstandings about my treatment plan. Improvement
is needed in this area.\nphysician_name: Maria Thompson\nhospital_name:
Little-Spencer\npatient_name: Terri Smith'

>>> relevant_docs[1].page_content
'review_id: 521\nvisit_id: 631\nreview: I had a challenging time at the
hospital. The medical care was adequate, but the lack of communication
between the staff and me left me feeling frustrated and confused about my
treatment plan.\nphysician_name: Samantha Mendez\nhospital_name:
Richardson-Powell\npatient_name: Kurt Gordon'

>>> relevant_docs[2].page_content
'review_id: 785\nvisit_id: 2593\nreview: My stay at the hospital was challenging.
The medical care was adequate, but the lack of communication from the staff
created some frustration.\nphysician_name: Brittany Harris\nhospital_name:
Jones, Taylor and Garcia\npatient_name: Ryan Jacobs'

您导入调用 ChromaDB 所需的依赖项,并在 REVIEWS_CHROMA_PATH 中指定存储的 ChromaDB 数据的路径。然后,您可以使用 dotenv.load_dotenv() 加载环境变量,并创建一个指向矢量数据库的新 Chroma 实例。请注意,在连接到矢量数据库时,您必须再次指定嵌入函数。确保这与您用于创建嵌入的嵌入函数相同。

接下来,定义一个问题并在 reviews_vector_db 上调用 .similarity_search(),传入 questionk=3 。这会为问题创建一个嵌入,并在向量数据库中搜索与问题嵌入最相似的三个评论嵌入。在这种情况下,您会看到三篇评论,其中患者抱怨沟通,这正是您所要求的!

最后要做的是将评论检索器添加到review_chain,以便相关评论作为上下文传递到提示。具体方法如下:

import dotenv
from langchain_openai import ChatOpenAI
from langchain.prompts import (
    PromptTemplate,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
    ChatPromptTemplate,
)
from langchain_core.output_parsers import StrOutputParser
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain.schema.runnable import RunnablePassthrough

REVIEWS_CHROMA_PATH = "chroma_data/"

# ...

reviews_vector_db = Chroma(
    persist_directory=REVIEWS_CHROMA_PATH,
    embedding_function=OpenAIEmbeddings()
)

reviews_retriever  = reviews_vector_db.as_retriever(k=10)

review_chain = (
    {"context": reviews_retriever, "question": RunnablePassthrough()}
    | review_prompt_template
    | chat_model
    | StrOutputParser()
)

与之前一样,您导入 ChromaDB 的依赖项,指定 ChromaDB 数据的路径,然后实例化一个新的 Chroma 对象。然后,您可以通过在 reviews_vector_db 上调用 .as_retriever() 来创建 reviews_retriever,以创建将添加到 review_chain 的检索器对象代码>.由于您指定了 k=10,检索器将获取与用户问题最相似的 10 条评论。

然后,您可以将带有 contextquestion 键的字典添加到 review_chain 的前面。 review_chain 会将您的问题传递给检索器以提取相关评论,而不是手动传递context。将 question 分配给 RunnablePassthrough 对象可确保问题不变地传递到链中的下一步。

您现在拥有一个功能齐全的链条,可以根据患者的评论回答有关患者体验的问题。启动一个新的 REPL 会话并尝试一下:

>>> from langchain_intro.chatbot import review_chain

>>> question = """Has anyone complained about
...            communication with the hospital staff?"""
>>> review_chain.invoke(question)
'Yes, several patients have complained about communication
with the hospital staff. Terri Smith mentioned that the
communication between the medical staff and her was unclear,
leading to misunderstandings about her treatment plan.
Kurt Gordon also mentioned that the lack of communication
between the staff and him left him feeling frustrated and
confused about his treatment plan. Ryan Jacobs also experienced
frustration due to the lack of communication from the staff.
Shannon Williams also mentioned that the lack of communication
between the staff and her made her stay at the hospital less enjoyable.'

如您所见,您只需调用 review_chain.invoke(question) 即可从患者的评论中获取有关患者体验的检索增强答案。稍后您将通过在 Neo4j 中存储评论嵌入以及其他元数据来改进该链。

现在您已经了解了聊天模型、提示、链和检索,您已经准备好深入了解 LangChain 的最后一个概念——代理。

代理商

到目前为止,您已经创建了一个使用患者评论来回答问题的链。如果您希望聊天机器人还可以回答有关其他医院数据(例如医院等待时间)的问题,该怎么办?理想情况下,您的聊天机器人可以根据用户的查询在回答患者检查和等待时间问题之间无缝切换。为此,您需要以下组件:

  1. 您已创建的患者审核链
  2. 可以查询医院等待时间的功能
  3. 法学硕士知道何时应该回答有关患者体验的问题或查找等待时间的一种方法

为了实现第三个能力,你需要一个代理。

代理是一种语言模型,它决定要执行的一系列操作。与动作序列是硬编码的链不同,代理使用语言模型来确定要采取哪些动作以及按什么顺序。

在构建代理之前,创建以下函数来生成医院的虚假等待时间:

import random
import time

def get_current_wait_time(hospital: str) -> int | str:
    """Dummy function to generate fake wait times"""

    if hospital not in ["A", "B", "C", "D"]:
        return f"Hospital {hospital} does not exist"

    # Simulate API call delay
    time.sleep(1)

    return random.randint(0, 10000)

get_current_wait_time() 中,您传入医院名称,检查其是否有效,然后生成一个随机数来模拟等待时间。实际上,这可能是某种数据库查询或 API 调用,但这对于本演示来说具有相同的目的。

您现在可以创建一个代理,根据问题在 get_current_wait_time()review_chain.invoke() 之间做出决定:

import dotenv
from langchain_openai import ChatOpenAI
from langchain.prompts import (
    PromptTemplate,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
    ChatPromptTemplate,
)
from langchain_core.output_parsers import StrOutputParser
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain.schema.runnable import RunnablePassthrough
from langchain.agents import (
    create_openai_functions_agent,
    Tool,
    AgentExecutor,
)
from langchain import hub
from langchain_intro.tools import get_current_wait_time

# ...

tools = [
    Tool(
        name="Reviews",
        func=review_chain.invoke,
        description="""Useful when you need to answer questions
        about patient reviews or experiences at the hospital.
        Not useful for answering questions about specific visit
        details such as payer, billing, treatment, diagnosis,
        chief complaint, hospital, or physician information.
        Pass the entire question as input to the tool. For instance,
        if the question is "What do patients think about the triage system?",
        the input should be "What do patients think about the triage system?"
        """,
    ),
    Tool(
        name="Waits",
        func=get_current_wait_time,
        description="""Use when asked about current wait times
        at a specific hospital. This tool can only get the current
        wait time at a hospital and does not have any information about
        aggregate or historical wait times. This tool returns wait times in
        minutes. Do not pass the word "hospital" as input,
        only the hospital name itself. For instance, if the question is
        "What is the wait time at hospital A?", the input should be "A".
        """,
    ),
]

hospital_agent_prompt = hub.pull("hwchase17/openai-functions-agent")

agent_chat_model = ChatOpenAI(
    model="gpt-3.5-turbo-1106",
    temperature=0,
)

hospital_agent = create_openai_functions_agent(
    llm=agent_chat_model,
    prompt=hospital_agent_prompt,
    tools=tools,
)

hospital_agent_executor = AgentExecutor(
    agent=hospital_agent,
    tools=tools,
    return_intermediate_steps=True,
    verbose=True,
)

在此块中,您导入创建代理所需的一些附加依赖项。然后定义 Tool 对象列表。 工具是代理用来与函数交互的接口。例如,第一个工具名为 Reviews,如果问题符合 description 的条件,它会调用 review_chain.invoke()

请注意 description 如何向代理提供关于何时应调用该工具的指示。这就是良好的即时工程技能对于确保法学硕士使用正确的输入调用正确的工具至关重要的地方。

tools 中的第二个 Tool 名为 Waits,它调用 get_current_wait_time()。同样,代理必须知道何时使用 Waits 工具,以及根据描述向其中传递哪些输入。

接下来,使用 gpt-3.5-turbo-1106 作为语言模型来初始化 ChatOpenAI 对象。然后,您可以使用 create_openai_functions_agent() 创建 OpenAI 函数代理。这将创建一个旨在将输入传递给函数的代理。它通过返回存储函数输入及其相应值的有效 JSON 对象来实现此目的。

要创建代理运行时,请将代理和工具传递到 AgentExecutor 中。将 return_intermediate_stepsverbose 设置为 True 将允许您查看代理的思维过程及其调用的工具。

启动一个新的 REPL 会话来试用您的新代理:

>>> from langchain_intro.chatbot import hospital_agent_executor

>>> hospital_agent_executor.invoke(
...     {"input": "What is the current wait time at hospital C?"}
... )

> Entering new AgentExecutor chain...

Invoking: `Waits` with `C`

1374The current wait time at Hospital C is 1374 minutes.

> Finished chain.
{'input': 'What is the current wait time at hospital C?',
'output': 'The current wait time at Hospital C is 1374 minutes.',
'intermediate_steps': [(AgentActionMessageLog(tool='Waits',
tool_input='C', log='\nInvoking: `Waits` with `C`\n\n\n',
message_log=[AIMessage(content='', additional_kwargs={'function_call':
{'arguments': '{"__arg1":"C"}', 'name': 'Waits'}})]), 1374)]}

>>> hospital_agent_executor.invoke(
...     {"input": "What have patients said about their comfort at the hospital?"}
... )

> Entering new AgentExecutor chain...

Invoking: `Reviews` with `What have patients said about their comfort at the
hospital?`

Patients have mentioned both positive and negative aspects of their comfort at
the hospital. One patient mentioned that the hospital's dedication to patient
comfort was evident in the well-designed private rooms and comfortable furnishings,
which made their recovery more bearable and contributed to an overall positive
experience. However, other patients mentioned that the uncomfortable beds made
it difficult for them to get a good night's sleep during their stay, affecting
their overall comfort. Another patient mentioned that the outdated and
uncomfortable beds affected their overall comfort, despite the doctors being
knowledgeable and the hospital having a clean environment. Patients have shared
mixed feedback about their comfort at the hospital. Some have praised the well-designed
private rooms and comfortable furnishings, which contributed to a positive experience.
However, others have mentioned discomfort due to the outdated and uncomfortable beds,
affecting their overall comfort despite the hospital's clean environment and knowledgeable
doctors.

> Finished chain.
{'input': 'What have patients said about their comfort at the hospital?', 'output':
"Patients have shared mixed feedback about their comfort at the hospital. Some have
praised the well-designed private rooms and comfortable furnishings, which contributed
to a positive experience. However, others have mentioned discomfort due to the outdated
and uncomfortable beds, affecting their overall comfort despite the hospital's clean
environment and knowledgeable doctors.", 'intermediate_steps':
[(AgentActionMessageLog(tool='Reviews', tool_input='What have patients said about their
comfort at the hospital?', log='\nInvoking: `Reviews` with `What have patients said about
their comfort at the hospital?`\n\n\n', message_log=[AIMessage(content='',
additional_kwargs={'function_call': {'arguments': '{"__arg1":"What have patients said about
their comfort at the hospital?"}', 'name': 'Reviews'}})]), "Patients have mentioned both
positive and negative aspects of their comfort at the hospital. One patient mentioned that
the hospital's dedication to patient comfort was evident in the well-designed private rooms
and comfortable furnishings, which made their recovery more bearable and contributed to an
overall positive experience. However, other patients mentioned that the uncomfortable beds
made it difficult for them to get a good night's sleep during their stay, affecting their
overall comfort. Another patient mentioned that the outdated and uncomfortable beds affected
their overall comfort, despite the doctors being knowledgeable and the hospital having a clean
environment.")]}

您首先导入代理,然后调用 hospital_agent_executor.invoke() 并询问有关等待时间的问题。如输出所示,代理知道您正在询问等待时间,并将 C 作为输入传递给 Waits 工具。然后,Waits 工具调用 get_current_wait_time(hospital="C") 并向客服人员返回相应的等待时间。然后,代理使用此等待时间来生成其最终输出。

当您向代理询问患者体验评论时,会发生类似的过程,只不过这次代理知道调用 Reviews 工具,患者对他们在医院的舒适度有何评价 医院? 作为输入。 Reviews 工具使用您的完整问题作为输入来运行 review_chain.invoke(),代理使用响应来生成其输出。

这是一种深奥的能力。代理使语言模型能够执行几乎任何您可以为其编写代码的任务。想象一下您可以与代理一起构建的所有令人惊奇且具有潜在危险的聊天机器人。

您现在已经具备了构建自定义聊天机器人所需的所有必备 LangChain 知识。接下来,您将戴上人工智能工程师的帽子,了解构建医院系统聊天机器人所需的业务需求和数据。

到目前为止,您编写的所有代码都是为了教您LangChain的基础知识,它不会包含在您最终的聊天机器人中。请随意从第 2 步中的空目录开始,您将在其中开始构建聊天机器人。

第 2 步:了解业务需求和数据

在开始从事任何人工智能项目之前,您需要了解想要解决的问题,并制定解决方案的计划。这包括明确定义问题、收集需求、了解可用的数据和技术,以及与利益相关者设定明确的期望。对于此项目,您将首先定义问题并收集聊天机器人的业务需求。

了解问题和要求

想象一下,您是一名人工智能工程师,在美国一家大型医院系统工作。您的利益相关者希望更清楚地了解他们收集的不断变化的数据。 他们希望获得有关患者、就诊、医生、医院和保险付款人的临时问题的答案,而无需理解 SQL 等查询语言、向分析师请求报告或等待某人构建仪表板。

为了实现这一目标,您的利益相关者需要一个类似于 ChatGPT 的内部聊天机器人工具,它可以回答有关您公司数据的问题。在满足收集要求后,您将获得聊天机器人应回答的问题类型列表:

  • XYZ 医院目前的等待时间是多少?
  • 目前哪家医院的候诊时间最短?
  • 患者在哪些医院抱怨账单和保险问题?
  • 有患者抱怨医院不干净吗?
  • 患者对医生和护士如何与他们沟通有何评价?
  • 患者对 XYZ 医院的护理人员有何评价?
  • 2023 年向 Cigna 付款人收取的总账单金额是多少?
  • John Doe 医生治疗过多少患者?
  • 有多少次开放访问以及它们的平均持续时间是多少天?
  • 哪位医生的平均就诊天数最短?
  • 789号患者的住院费用是多少?
  • 2023 年哪家医院接待的 Cigna 患者最多?
  • 医院急诊的平均收费是多少?
  • 从 2022 年到 2023 年,哪个州的医疗补助访问量增幅最大?

您可以使用 SQL 等查询语言通过汇总统计信息来回答诸如2023 年向 Cigna 付款人收取的总账单金额是多少?之类的问题。至关重要的是,这些问题都有一个客观的答案。您可以运行预定义的查询来回答这些问题,但是每当利益相关者提出新的或稍微微妙的问题时,您都必须编写一个新的查询。为了避免这种情况,您的聊天机器人应该动态生成准确的查询。

有病人抱怨医院不干净吗?病人对医生和护士如何与他们沟通有何看法?等问题更加主观,可能有很多可接受的答案。您的聊天机器人需要阅读文档(例如患者评论)才能回答此类问题。

最终,您的利益相关者想要一个可以无缝回答主观和客观问题的单一聊天界面。这意味着,当提出问题时,您的聊天机器人需要知道所提出的问题类型以及要从哪个数据源获取。

例如,如果询问789号患者的住院费用是多少?,您的聊天机器人应该知道它需要查询数据库才能找到答案。如果被问到患者对医生和护士如何与他们沟通有何看法?,您的聊天机器人应该知道它需要阅读和总结患者的评论。

接下来,您将探索医院系统记录的数据,这可以说是构建聊天机器人的最重要的先决条件。

探索可用数据

在构建聊天机器人之前,您需要彻底了解它将用于响应用户查询的数据。这将帮助您确定什么是可行的以及您希望如何构建数据,以便您的聊天机器人可以轻松访问它。您将在本文中使用的所有数据都是综合生成的,其中大部分源自 Kaggle 上流行的医疗保健数据集。

实际上,以下数据集可能会作为表存储在 SQL 数据库中,但您将使用 CSV 文件来集中精力构建聊天机器人。本节将为您提供每个 CSV 文件的详细说明。

在继续本教程之前,您需要将属于此项目的所有 CSV 文件放入 data/ 文件夹中。确保您从材料中下载它们并将它们放入您的 data/ 文件夹中:

医院.csv

hospitals.csv 文件记录您公司管理的每家医院的信息。该档案共有30家医院、3个领域:

  • hospital_id:唯一标识医院的整数。
  • hospital_name:医院的名称。
  • hospital_state:医院所在的州。

如果您熟悉传统 SQL 数据库和星型模式,则可以将 hospitals.csv 视为维度表。维度表相对较短,包含为事实表中的数据提供上下文的描述性信息或属性。事实表记录有关维度表中存储的实体的事件,并且它们往往是较长的表。

在本例中,hospitals.csv 记录特定于医院的信息,但您可以将其连接到事实表中,以回答有关哪些患者、医生和付款人与医院相关的问题。当您探索 visits.csv 时,这一点会更加清晰。

如果您好奇,可以使用 Polars 等数据框架库检查 hospitals.csv 的前几行。确保您的虚拟环境中安装了 Polars,并运行以下代码:

>>> import polars as pl

>>> HOSPITAL_DATA_PATH = "data/hospitals.csv"
>>> data_hospitals = pl.read_csv(HOSPITAL_DATA_PATH)

>>> data_hospitals.shape
(30, 3)

>>> data_hospitals.head()
shape: (5, 3)
┌─────────────┬───────────────────────────┬────────────────┐
│ hospital_id ┆ hospital_name             ┆ hospital_state │
│ ---         ┆ ---                       ┆ ---            │
│ i64         ┆ str                       ┆ str            │
╞═════════════╪═══════════════════════════╪════════════════╡
│ 0           ┆ Wallace-Hamilton          ┆ CO             │
│ 1           ┆ Burke, Griffin and Cooper ┆ NC             │
│ 2           ┆ Walton LLC                ┆ FL             │
│ 3           ┆ Garcia Ltd                ┆ NC             │
│ 4           ┆ Jones, Brown and Murray   ┆ NC             │
└─────────────┴───────────────────────────┴────────────────┘

在此代码块中,您导入 Polars,定义 hospitals.csv 的路径,将数据读入 Polars DataFrame,显示数据的形状,并显示前 5 行。例如,这会向您显示 Walton, LLC 医院的 ID 为 2,并且位于佛罗里达州 FL

医生.csv

Physicians.csv 文件包含有关为您的医院系统工作的医生的数据。该数据集具有以下字段:

  • Physician_id:唯一标识每个医生的整数。
  • Physician_name:医生的姓名。
  • Physician_dob:医生的出生日期。
  • Physician_grad_year:医生从医学院毕业的年份。
  • medical_school:医生就读的医学院。
  • salary:医生的工资。

该数据可以再次被视为维度表,您可以使用 Polars 检查前几行:

>>> PHYSICIAN_DATA_PATH = "data/physicians.csv"
>>> data_physician = pl.read_csv(PHYSICIAN_DATA_PATH)

>>> data_physician.shape
(500, 6)

>>> data_physician.head()
shape: (5, 6)
┌──────────────────┬──────────────┬───────────────┬─────────────────────┬───────────────────────────────────┬───────────────┐
│ physician_name   ┆ physician_id ┆ physician_dob ┆ physician_grad_year ┆ medical_school                    ┆ salary        │
│ ---              ┆ ---          ┆ ---           ┆ ---                 ┆ ---                               ┆ ---           │
│ str              ┆ i64          ┆ str           ┆ str                 ┆ str                               ┆ f64           │
╞══════════════════╪══════════════╪═══════════════╪═════════════════════╪═══════════════════════════════════╪═══════════════╡
│ Joseph Johnson   ┆ 0            ┆ 1970-02-22    ┆ 2000-02-22          ┆ Johns Hopkins University School … ┆ 309534.155076 │
│ Jason Williams   ┆ 1            ┆ 1982-12-22    ┆ 2012-12-22          ┆ Mayo Clinic Alix School of Medic… ┆ 281114.503559 │
│ Jesse Gordon     ┆ 2            ┆ 1959-06-03    ┆ 1989-06-03          ┆ David Geffen School of Medicine … ┆ 305845.584636 │
│ Heather Smith    ┆ 3            ┆ 1965-06-15    ┆ 1995-06-15          ┆ NYU Grossman Medical School       ┆ 295239.766689 │
│ Kayla Hunter DDS ┆ 4            ┆ 1978-10-19    ┆ 2008-10-19          ┆ David Geffen School of Medicine … ┆ 298751.355201 │
└──────────────────┴──────────────┴───────────────┴─────────────────────┴───────────────────────────────────┴───────────────┘

从代码块中可以看到,Physicians.csv 中有 500 名医生。 Physicians.csv 的前几行让您了解数据的样子。例如,Heather Smith 的医生 ID 为 3,出生于 1965 年 6 月 15 日,1995 年 6 月 15 日从医学院毕业,就读于纽约大学格罗斯曼医学院,她的工资约为 295,239 美元。

付款人.csv

下一个文件 payers.csv 记录了您的医院向患者就诊收取费用的保险公司的信息。与 hospitals.csv 类似,它是一个包含几个字段的小文件:

  • payer_id:唯一标识每个付款人的整数。
  • payer_name:付款人的公司名称。

数据中仅有的五个付款人是 MedicaidUnitedHealthcareAetnaCignaBlue Cross 。您的利益相关者对付款人活动非常感兴趣,因此一旦与患者、医院和医生建立联系,payers.csv 将会很有帮助。

评论.csv

reviews.csv 文件包含患者对他们在医院的经历的评论。它有这些字段:

  • review_id:唯一标识评论的整数。
  • visit_id:一个整数,用于标识审核所涉及的患者就诊。
  • 评论:这是患者留下的自由形式文本评论。
  • Physician_name:治疗患者的医生的姓名。
  • hospital_name:患者所在的医院。
  • patent_name:患者姓名。

该数据集是您看到的第一个包含自由文本评论字段的数据集,您的聊天机器人应该使用它来回答有关评论详细信息和患者体验的问题。

reviews.csv 如下所示:

>>> REVIEWS_DATA_PATH = "data/reviews.csv"
>>> data_reviews = pl.read_csv(REVIEWS_DATA_PATH)

>>> data_reviews.shape
(1005, 6)

>>> data_reviews.head()
shape: (5, 6)
┌───────────┬──────────┬───────────────────────────────────┬─────────────────────┬──────────────────┬──────────────────┐
│ review_id ┆ visit_id ┆ review                            ┆ physician_name      ┆ hospital_name    ┆ patient_name     │
│ ---       ┆ ---      ┆ ---                               ┆ ---                 ┆ ---              ┆ ---              │
│ i64       ┆ i64      ┆ str                               ┆ str                 ┆ str              ┆ str              │
╞═══════════╪══════════╪═══════════════════════════════════╪═════════════════════╪══════════════════╪══════════════════╡
│ 0         ┆ 6997     ┆ The medical staff at the hospita… ┆ Laura Brown         ┆ Wallace-Hamilton ┆ Christy Johnson  │
│ 9         ┆ 8138     ┆ The hospital's commitment to pat… ┆ Steven Watson       ┆ Wallace-Hamilton ┆ Anna Frazier     │
│ 11        ┆ 680      ┆ The hospital's commitment to pat… ┆ Chase Mcpherson Jr. ┆ Wallace-Hamilton ┆ Abigail Mitchell │
│ 892       ┆ 9846     ┆ I had a positive experience over… ┆ Jason Martinez      ┆ Wallace-Hamilton ┆ Kimberly Rivas   │
│ 822       ┆ 7397     ┆ The medical team at the hospital… ┆ Chelsey Davis       ┆ Wallace-Hamilton ┆ Catherine Yang   │
└───────────┴──────────┴───────────────────────────────────┴─────────────────────┴──────────────────┴──────────────────┘

此数据集中有 1005 条评论,您可以查看每条评论与一次访问的关系。例如,ID 9的评论对应访问ID 8138,前几个词是“医院承诺拍……”。您可能想知道如何将评论与患者连接起来,或者更一般地说,如何将迄今为止描述的所有数据集相互连接起来。这就是 visits.csv 发挥作用的地方。

访问.csv

最后一个文件 visits.csv 记录了贵公司所服务的每次医院就诊的详细信息。继续星型模式的类比,您可以将 visits.csv 视为连接医院、医生、患者和付款人的事实表。以下是字段:

  • visit_id:医院就诊的唯一标识符。
  • patent_id:与就诊相关的患者 ID。
  • date_of_admission:患者入院的日期。
  • room_number:患者的房间号。
  • admission_type:“选择性”、“紧急”或“紧急”之一。
  • chief_complaint:描述患者住院主要原因的字符串。
  • primary_diagnosis:描述医生做出的初步诊断的字符串。
  • treatment_description:医生给出的治疗的文本摘要。
  • test_results:“不确定”、“正常”或“异常”之一。
  • discharge_date:患者出院的日期
  • Physician_id:治疗患者的医生的 ID。
  • hospital_id:患者所在医院的ID。
  • payer_id:患者使用的保险付款人的ID。
  • billing_amount:向付款人收取的访问费用金额。
  • visit_status:“OPEN”或“DISCHARGED”之一。

该数据集为您提供了回答有关每个医院实体之间关系的问题所需的一切。例如,如果您知道医生 ID,则可以使用 visits.csv 找出该医生与哪些患者、付款人和医院相关联。看看 visits.csv 在 Polars 中的样子:

>>> VISITS_DATA_PATH = "data/visits.csv"
>>> data_visits = pl.read_csv(VISITS_DATA_PATH)

>>> data_visits.shape
(9998, 15)

>>> data_visits.head()
shape: (5, 15)
┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│ patient ┆ date_of ┆ billing ┆ room_nu ┆ admissi ┆ dischar ┆ test_r ┆ visit_ ┆ physic ┆ payer_ ┆ hospit ┆ chief_ ┆ treatm ┆ primar ┆ visit_ │
│ _id     ┆ _admiss ┆ _amount ┆ mber    ┆ on_type ┆ ge_date ┆ esults ┆ id     ┆ ian_id ┆ id     ┆ al_id  ┆ compla ┆ ent_de ┆ y_diag ┆ status │
│ ---     ┆ ion     ┆ ---     ┆ ---     ┆ ---     ┆ ---     ┆ ---    ┆ ---    ┆ ---    ┆ ---    ┆ ---    ┆ int    ┆ script ┆ nosis  ┆ ---    │
│ i64     ┆ ---     ┆ f64     ┆ i64     ┆ str     ┆ str     ┆ str    ┆ i64    ┆ i64    ┆ i64    ┆ i64    ┆ ---    ┆ ion    ┆ ---    ┆ str    │
│         ┆ str     ┆         ┆         ┆         ┆         ┆        ┆        ┆        ┆        ┆        ┆ str    ┆ ---    ┆ str    ┆        │
│         ┆         ┆         ┆         ┆         ┆         ┆        ┆        ┆        ┆        ┆        ┆        ┆ str    ┆        ┆        │
╞═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪════════╪════════╪════════╪════════╪════════╪════════╪════════╪════════╪════════╡
│ 0       ┆ 2022-11 ┆ 37490.9 ┆ 146     ┆ Electiv ┆ 2022-12 ┆ Inconc ┆ 0      ┆ 102    ┆ 1      ┆ 0      ┆ null   ┆ null   ┆ null   ┆ DISCHA │
│         ┆ -17     ┆ 83364   ┆         ┆ e       ┆ -01     ┆ lusive ┆        ┆        ┆        ┆        ┆        ┆        ┆        ┆ RGED   │
│ 1       ┆ 2023-06 ┆ 47304.0 ┆ 404     ┆ Emergen ┆ null    ┆ Normal ┆ 1      ┆ 435    ┆ 4      ┆ 5      ┆ null   ┆ null   ┆ null   ┆ OPEN   │
│         ┆ -01     ┆ 64845   ┆         ┆ cy      ┆         ┆        ┆        ┆        ┆        ┆        ┆        ┆        ┆        ┆        │
│ 2       ┆ 2019-01 ┆ 36874.8 ┆ 292     ┆ Emergen ┆ 2019-02 ┆ Normal ┆ 2      ┆ 348    ┆ 2      ┆ 6      ┆ null   ┆ null   ┆ null   ┆ DISCHA │
│         ┆ -09     ┆ 96997   ┆         ┆ cy      ┆ -08     ┆        ┆        ┆        ┆        ┆        ┆        ┆        ┆        ┆ RGED   │
│ 3       ┆ 2020-05 ┆ 23303.3 ┆ 480     ┆ Urgent  ┆ 2020-05 ┆ Abnorm ┆ 3      ┆ 270    ┆ 4      ┆ 15     ┆ null   ┆ null   ┆ null   ┆ DISCHA │
│         ┆ -02     ┆ 22092   ┆         ┆         ┆ -03     ┆ al     ┆        ┆        ┆        ┆        ┆        ┆        ┆        ┆ RGED   │
│ 4       ┆ 2021-07 ┆ 18086.3 ┆ 477     ┆ Urgent  ┆ 2021-08 ┆ Normal ┆ 4      ┆ 106    ┆ 2      ┆ 29     ┆ Persis ┆ Prescr ┆ J45.90 ┆ DISCHA │
│         ┆ -09     ┆ 44184   ┆         ┆         ┆ -02     ┆        ┆        ┆        ┆        ┆        ┆ tent   ┆ ibed a ┆ 9 -    ┆ RGED   │
│         ┆         ┆         ┆         ┆         ┆         ┆        ┆        ┆        ┆        ┆        ┆ cough  ┆ combin ┆ Unspec ┆        │
│         ┆         ┆         ┆         ┆         ┆         ┆        ┆        ┆        ┆        ┆        ┆ and    ┆ ation  ┆ ified  ┆        │
│         ┆         ┆         ┆         ┆         ┆         ┆        ┆        ┆        ┆        ┆        ┆ shortn ┆ of     ┆ asthma ┆        │
│         ┆         ┆         ┆         ┆         ┆         ┆        ┆        ┆        ┆        ┆        ┆ ess o… ┆ inha…  ┆ , un…  ┆        │
└─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘

您可以看到记录了 9998 次访问以及上述 15 个字段。请注意,访问时可能会缺少 chief_complainttreatment_descriptionprimary_diagnosis。您必须牢记这一点,因为您的利益相关者可能没有意识到许多访问都缺少关键数据 - 这本身可能是一个有价值的见解!最后,请注意,当就诊仍处于开放状态时,discharged_date 将丢失。

您现在已经了解了将用于构建利益相关者想要的聊天机器人的数据。回顾一下,这些文件被分解以模拟传统 SQL 数据库的外观。每家医院、患者、医生、审查和付款人都通过 visits.csv 连接起来。

等待时间

您可能已经注意到,没有数据可以回答诸如XYZ 医院当前等待时间是多少?之类的问题 目前哪家医院的等待时间最短?。不幸的是,医院系统没有记录历史等待时间。您的聊天机器人必须调用 API 来获取当前的等待时间信息。稍后你会看到这是如何工作的。

了解业务需求、可用数据和 LangChain 功能后,您可以为您的聊天机器人创建设计。

设计聊天机器人

现在您已经了解了业务需求、数据和 LangChain 先决条件,您就可以设计聊天机器人了。良好的设计可以让您和其他人对构建聊天机器人所需的组件有概念性的了解。您的设计应清楚地说明数据如何流经聊天机器人,并且应在开发过程中作为有用的参考。

您的聊天机器人将使用多种工具来回答有关您的医院系统的各种问题。下面的流程图说明了如何实现此目的:

此流程图说明了数据如何通过聊天机器人移动,从用户的输入查询开始一直到最终响应。以下是每个组件的摘要:

  • LangChain代理:LangChain代理是聊天机器人的大脑。给定用户查询,代理决定调用哪个工具以及向工具提供什么作为输入。然后,代理观察工具的输出并决定返回给用户的内容——这就是代理的响应。
  • Neo4j AuraDB:您将在 Neo4j AuraDB 图形数据库中存储结构化医院系统数据和患者评论。您将在下一节中了解所有相关内容。
  • LangChain Neo4j Cypher Chain:该链尝试将用户查询转换为 Neo4j 的查询语言 Cypher,并在 Neo4j 中执行 Cypher 查询。然后该链使用 Cypher 查询结果回答用户查询。链的响应反馈给LangChain代理并发送给用户。
  • LangChain Neo4j 评论向量链:这与您在步骤 1 中构建的链非常相似,不同之处在于现在患者评论嵌入存储在 Neo4j 中。该链根据与用户查询语义相似的评论来搜索相关评论,并使用这些评论来回答用户查询。
  • 等待时间函数:与步骤1中的逻辑类似,LangChain代理尝试从用户查询中提取医院名称。医院名称作为输入传递给获取等待时间的 Python 函数,并将等待时间返回给代理。

举个例子,假设用户询问2023年有多少次紧急访问?LangChain代理将收到这个问题并决定将问题传递给哪个工具(如果有)。在这种情况下,代理应该将问题传递给LangChain Neo4j Cypher Chain。该链将尝试将问题转换为 Cypher 查询,在 Neo4j 中运行 Cypher 查询,并使用查询结果来回答问题。

一旦 LangChain Neo4j Cypher Chain 回答了问题,它会将答案返回给代理,代理将答案转发给用户。

考虑到这种设计,您就可以开始构建聊天机器人了。您的第一个任务是设置 Neo4j AuraDB 实例以供聊天机器人访问。

第 3 步:设置 Neo4j 图形数据库

正如您在步骤 2 中看到的,您的医院系统数据当前存储在 CSV 文件中。在构建聊天机器人之前,您需要将此数据存储在聊天机器人可以查询的数据库中。为此,您将使用 Neo4j AuraDB。

在学习如何设置 Neo4j AuraDB 实例之前,您将了解图形数据库的概述,并且您将了解为什么对于该项目来说使用图形数据库可能比关系数据库更好。

图数据库简要概述

图形数据库(例如 Neo4j)是设计用于表示和处理以图形形式存储的数据的数据库。图数据由节点关系以及属性组成。节点表示实体,关系连接实体,属性提供有关节点和关系的附加元数据。

例如,以下是在图表中表示医院系统节点和关系的方式:

该图具有三个节点 - 患者访问付款人PatientVisit 通过 HAS 关系连接,表示某医院患者来访。同样,就诊付款人通过COVERED_BY关系连接,表明保险付款人承担了医院就诊费用。

请注意关系是如何用指示其方向的箭头来表示的。例如,HAS 关系的方向告诉您患者可以访问,但访问不能访问患者。

节点和关系都可以具有属性。在此示例中,Patient 节点具有 id、name 和出生日期属性,COVERED_BY 关系具有服务日期和账单金额属性。像这样在图表中存储数据有几个优点:

  1. 简单性:在图数据库中对实体之间的真实关系进行建模是很自然的,从而减少了对需要多个连接操作来回答查询的复杂模式的需求。

  2. 关系:图数据库擅长处理复杂的关系。遍历关系非常高效,可以轻松查询和分析关联数据。

  3. 灵活性:图数据库是无模式的,可以轻松适应不断变化的数据结构。这种灵活性有利于不断发展的数据模型。

  4. 性能:在图数据库中检索连接的数据比在关系数据库中更快,特别是对于涉及具有多种关系的复杂查询的场景。

  5. 模式匹配:图数据库支持强大的模式匹配查询,使表达和查找数据中的特定结构变得更加容易。

当您的数据具有许多复杂关系时,与关系数据库相比,图数据库的简单性和灵活性使其更易于设计和查询。正如您稍后将看到的,在图数据库查询中指定关系非常简洁,并且不涉及复杂的联接。如果您感兴趣,Neo4j 在其文档中通过实际的示例数据库很好地说明了这一点。

由于这种简洁的数据表示形式,LLM 生成图形数据库查询时出错的空间更小。这是因为您只需告诉 LLM 有关图数据库中的节点、关系和属性的信息。与关系数据库相比,LLM 必须在整个数据库中导航并保留表模式和外键关系的知识,从而在 SQL 生成中留下更多的出错空间。

接下来,您将通过设置 Neo4j AuraDB 实例开始使用图形数据库。之后,您将把医院系统移至 Neo4j 实例中并学习如何查询它。

创建 Neo4j 帐户和 AuraDB 实例

要开始使用 Neo4j,您可以创建一个免费的 Neo4j AuraDB 帐户。登陆页面应该看起来像这样:

单击开始免费按钮并创建一个帐户。登录后,您应该会看到 Neo4j Aura 控制台:

单击新建实例并创建免费实例。应弹出与此类似的模式:

单击下载并继续后,应创建您的实例,并应下载包含 Neo4j 数据库凭据的文本文件。创建实例后,您将看到其状态为正在运行。应该还没有节点或关系:

接下来,打开使用 Neo4j 凭据下载的文本文件,并将 NEO4J_URINEO4J_USERNAMENEO4J_PASSWORD 复制到 .env 文件:

OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>

NEO4J_URI=<YOUR_NEO4J_URI>
NEO4J_USERNAME=<YOUR_NEO4J_URI>
NEO4J_PASSWORD=<YOUR_NEO4J_PASSWORD>

您将使用这些环境变量连接到 Python 中的 Neo4j 实例,以便您的聊天机器人可以执行查询。

您现在已准备好与 Neo4j 实例交互的一切。接下来,您将设计医院系统图形数据库。这将告诉您医院实体如何相关,并将告知您可以运行的查询类型。

设计医院系统图数据库

现在您已经拥有正在运行的 Neo4j AuraDB 实例,您需要决定要存储哪些节点、关系和属性。表示这一点的最流行的方法之一是使用流程图。根据您对医院系统数据的理解,您提出以下设计:

该图显示了医院系统数据中的所有节点和关系。考虑此流程图的一种有用方法是从 Patient 节点开始并遵循关系。 患者访问一家医院,并且医院 雇用一名医生治疗就诊,其承保保险付款人

以下是每个节点中存储的属性:

这些属性中的大多数直接来自您在步骤 2 中探索的字段。一个显着的区别是 Review 节点具有 embedding 属性,它是 review 节点的向量表示形式。患者姓名、医生姓名文本属性。这使您可以像使用 ChromaDB 一样对审阅节点进行矢量搜索。

以下是关系属性:

正如您所看到的,COVERED_BY 是唯一具有多个 id 属性的关系。 service_date 是患者就诊出院的日期,billing_amount 是向付款人收取的就诊金额。

现在您已经大致了解了将使用的医院系统设计,是时候将数据转移到 Neo4j 中了!

将数据上传到 Neo4j

通过运行 Neo4j 实例并了解要存储的节点、属性和关系,您可以将医院系统数据移至 Neo4j 中。为此,您将创建一个名为 hospital_neo4j_etl 的文件夹,其中包含一些空文件。您还需要在项目的根目录中创建一个 docker-compose.yml 文件:

./
│
├── hospital_neo4j_etl/
│   │
│   ├── src/
│   │   ├── entrypoint.sh
│   │   └── hospital_bulk_csv_write.py
│   │
│   ├── Dockerfile
│   └── pyproject.toml
│
├── .env
└── docker-compose.yml

您的 .env 文件应具有以下环境变量:

OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>

NEO4J_URI=<YOUR_NEO4J_URI>
NEO4J_USERNAME=<YOUR_NEO4J_URI>
NEO4J_PASSWORD=<YOUR_NEO4J_PASSWORD>

HOSPITALS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/hospitals.csv
PAYERS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/payers.csv
PHYSICIANS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/physicians.csv
PATIENTS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/patients.csv
VISITS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/visits.csv
REVIEWS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/reviews.csv

请注意,您已将所有 CSV 文件存储在 GitHub 上的公共位置。由于您的 Neo4j AuraDB 实例在云中运行,因此它无法访问本地计算机上的文件,您必须使用 HTTP 或将文件直接上传到您的实例。对于本示例,您可以使用上面的链接,或将数据上传到其他位置。

填充 .env 文件后,打开 pyproject.toml,它提供以 TOML 格式定义的配置、元数据和依赖项:

[project]
name = "hospital_neo4j_etl"
version = "0.1"
dependencies = [
   "neo4j==5.14.1",
   "retry==0.9.2"
]

[project.optional-dependencies]
dev = ["black", "flake8"]

该项目是一个简单的提取、转换、加载 (ETL) 过程,将数据移动到 Neo4j 中,因此它唯一的依赖项是 neo4j 和重试。 ETL 的主要脚本是 hospital_neo4j_etl/src/hospital_bulk_csv_write.py。此处包含完整脚本的时间太长,但您将了解 hospital_neo4j_etl/src/hospital_bulk_csv_write.py 执行的主要步骤。您可以从材料中复制完整的脚本:

首先,导入依赖项、加载环境变量并配置日志记录:

import os
import logging
from retry import retry
from neo4j import GraphDatabase

HOSPITALS_CSV_PATH = os.getenv("HOSPITALS_CSV_PATH")
PAYERS_CSV_PATH = os.getenv("PAYERS_CSV_PATH")
PHYSICIANS_CSV_PATH = os.getenv("PHYSICIANS_CSV_PATH")
PATIENTS_CSV_PATH = os.getenv("PATIENTS_CSV_PATH")
VISITS_CSV_PATH = os.getenv("VISITS_CSV_PATH")
REVIEWS_CSV_PATH = os.getenv("REVIEWS_CSV_PATH")

NEO4J_URI = os.getenv("NEO4J_URI")
NEO4J_USERNAME = os.getenv("NEO4J_USERNAME")
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD")

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s]: %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

LOGGER = logging.getLogger(__name__)

# ...

您从 neo4j 导入 GraphDatabase 类以连接到正在运行的实例。请注意,您不再使用 Python-dotenv 来加载环境变量。相反,您将环境变量传递到运行脚本的 Docker 容器中。接下来,您将根据您的设计定义将医院数据移动到 Neo4j 中的函数:

# ...

NODES = ["Hospital", "Payer", "Physician", "Patient", "Visit", "Review"]

def _set_uniqueness_constraints(tx, node):
    query = f"""CREATE CONSTRAINT IF NOT EXISTS FOR (n:{node})
        REQUIRE n.id IS UNIQUE;"""
    _ = tx.run(query, {})


@retry(tries=100, delay=10)
def load_hospital_graph_from_csv() -> None:
    """Load structured hospital CSV data following
    a specific ontology into Neo4j"""

    driver = GraphDatabase.driver(
        NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD)
    )

    LOGGER.info("Setting uniqueness constraints on nodes")
    with driver.session(database="neo4j") as session:
        for node in NODES:
            session.execute_write(_set_uniqueness_constraints, node)
    # ...

# ...

首先,您定义一个辅助函数 _set_uniqueness_constraints(),该函数创建并运行查询,强制每个节点具有唯一 ID。 在 load_hospital_graph_from_csv() 中,您实例化一个连接到 Neo4j 实例的驱动程序,并为每个医院系统节点设置唯一性约束。

请注意附加到 load_hospital_graph_from_csv()@retry 装饰器。如果 load_hospital_graph_from_csv() 由于任何原因失败,此装饰器将重新运行它一百次,两次尝试之间有十秒的延迟。当 Neo4j 出现间歇性连接问题(通常通过重新创建连接来解决)时,这会派上用场。但是,请务必检查脚本日志以查看错误是否重复发生多次。

接下来,load_hospital_graph_from_csv() 加载每个节点和关系的数据:

# ...

@retry(tries=100, delay=10)
def load_hospital_graph_from_csv() -> None:
    """Load structured hospital CSV data following
    a specific ontology into Neo4j"""

    # ...

    LOGGER.info("Loading hospital nodes")
    with driver.session(database="neo4j") as session:
        query = f"""
        LOAD CSV WITH HEADERS
        FROM '{HOSPITALS_CSV_PATH}' AS hospitals
        MERGE (h:Hospital {{id: toInteger(hospitals.hospital_id),
                            name: hospitals.hospital_name,
                            state_name: hospitals.hospital_state}});
        """
        _ = session.run(query, {})

   # ...

if __name__ == "__main__":
    load_hospital_graph_from_csv()

每个节点和关系都从各自的 csv 文件加载,并根据您的图形数据库设计写入 Neo4j。在脚本末尾,您在 name-main 习惯用法中调用 load_hospital_graph_from_csv(),所有数据都应填充到您的 Neo4j 实例中。

编写 hospital_neo4j_etl/src/hospital_bulk_csv_write.py 后,您可以定义一个 entrypoint.sh 文件,该文件将在 Docker 容器启动时运行:

#!/bin/bash

# Run any setup steps or pre-processing tasks here
echo "Running ETL to move hospital data from csvs to Neo4j..."

# Run the ETL script
python hospital_bulk_csv_write.py

该入口点文件在技术上对于该项目来说并不是必需的,但在构建容器时这是一个很好的做法,因为它允许您在运行主脚本之前执行必要的 shell 命令。

为 ETL 写入的最后一个文件是 Docker 文件。它看起来像这样:

FROM python:3.11-slim

WORKDIR /app

COPY ./src/ /app

COPY ./pyproject.toml /code/pyproject.toml
RUN pip install /code/.

CMD ["sh", "entrypoint.sh"]

Dockerfile 告诉您的容器使用 python:3.11-slim 发行版,将 hospital_neo4j_etl/src/ 中的内容复制到 /容器中的 app 目录,从 pyproject.toml 安装依赖项,然后运行 entrypoint.sh

您现在可以将此项目添加到 docker-compose.yml 中:

version: '3'

services:
  hospital_neo4j_etl:
    build:
      context: ./hospital_neo4j_etl
    env_file:
      - .env

ETL 将作为名为 hospital_neo4j_etl 的服务运行,并将使用 .env 中的环境变量运行 ./hospital_neo4j_etl 中的 Dockerfile。由于您只有一个容器,因此还不需要 docker-compose。不过,您将在下一节中添加更多容器来与 ETL 进行编排,因此开始使用 docker-compose.yml 会很有帮助。

要运行 ETL,请打开终端并运行:

$ docker-compose up --build

ETL 完成运行后,返回到 Aura 控制台:

单击打开,系统将提示您输入 Neo4j 密码。成功登录实例后,您应该看到类似以下的屏幕:

正如您在数据库信息下所看到的,所有节点、关系和属性均已加载。有 21,187 个节点和 48,259 个关系。您已准备好开始编写查询!

查询医院系统图

在构建聊天机器人之前,您需要做的最后一件事是熟悉 Cypher 语法。 Cypher 是 Neo4j 的查询语言,学习起来相当直观,特别是如果您熟悉 SQL。本节将介绍基础知识,这就是构建聊天机器人所需的全部内容。您可以查看 Neo4j 的文档以获取更全面的 Cypher 概述。

在 Cypher 中读取数据最常用的关键字是 MATCH,它用于指定要在图中查找的模式。最简单的模式是具有单个节点的模式。例如,如果您想查找写入图表的前五个患者节点,您可以运行以下 Cypher 查询:

MATCH (p:Patient)
RETURN p LIMIT 5;

在此查询中,您将匹配 Patient 节点。在 Cypher 中,节点总是用括号表示。 (p:Patient) 中的 p 是您稍后可以在查询中引用的别名。 RETURN p LIMIT 5; 告诉 Neo4j 仅返回五个患者节点。您可以在 Neo4j UI 中运行此查询,结果应如下所示:

表格视图显示返回的五个患者节点及其属性。如果您感兴趣,还可以探索图表和原始视图。

虽然在单个节点上进行匹配很简单,但有时这就是获得有用见解所需的全部内容。例如,如果您的利益相关者说给我访问 56 的摘要,则以下查询将为您提供答案:

MATCH (v:Visit)
WHERE v.id = 56
RETURN v;

此查询匹配 id 为 56 的 Visit 节点,由 WHERE v.id=56 指定。您可以在 WHERE 子句中过滤任意节点和关系属性。该查询的结果如下所示:

从查询输出中,您可以看到返回的 Visit 确实具有 id 56。然后您可以查看所有访问属性以得出访问的口头摘要——这就是你的 Cypher 链要做的事情。

节点匹配很棒,但 Cypher 的真正强大之处在于它匹配关系模式的能力。这使您能够深入了解复杂的关系,利用图形数据库的强大功能。继续使用 Visit 查询,您可能想知道 Visit 属于哪个 Patient。您可以从 HAS 关系中得到这一点:

MATCH (p:Patient)-[h:HAS]->(v:Visit)
WHERE v.id = 56
RETURN v,h,p;

此 Cypher 查询搜索具有 id 56 的 VisitPatient。您会注意到关系 HAS 被方括号而不是圆括号包围,其方向性由箭头指示。如果您尝试 MATCH (p:Patient)<-[h:HAS]-(v:Visit),查询将不会返回任何内容,因为 HAS 关系的方向不正确。

查询结果如下所示:

请注意,输出包括访问HAS关系和患者的数据。与仅匹配 Visit 节点相比,这可以为您提供更多洞察。如果您想查看在就诊期间哪些医生治疗了患者,您可以将以下关系添加到查询中:

MATCH (p:Patient)-[h:HAS]->(v:Visit)<-[t:TREATS]-(ph:Physician)
WHERE v.id = 56
RETURN v,p,ph

此语句 (p:Patient)-[h:HAS]->(v:Visit)<-[t:TREATS]-(ph:Physician) 告诉 Neo4j 查找其中 的所有模式>患者有一次就诊,由医生治疗。如果您想匹配进出 Visit 节点的所有关系,您可以运行以下查询:

MATCH (v:Visit)-[r]-(n)
WHERE v.id = 56
RETURN r,n;

现在请注意,关系 [r] 相对于 (v:Visit)(n) 没有方向。实质上,此匹配语句将查找进出Visit56 的所有关系,以及连接到这些关系的节点。结果如下:

这使您可以很好地了解与访问 56 相关的所有关系和节点。想想这种表示有多么强大。您不必像在关系数据库中那样执行多个 SQL 连接,而是通过三行简短的 Cypher 获取有关 Visit 如何连接到整个医院系统的所有信息。

您可以想象,随着更多的节点和关系被添加到图形数据库中,这将变得多么强大。例如,您可以记录哪些护士、药房、药物或手术与访问相关。您添加的每个关系都需要在 SQL 中进行另一次联接,但上面有关 Visit 56 的 Cypher 查询将保持不变。

本节中要介绍的最后一件事是如何在 Cypher 中执行聚合。到目前为止,您只查询了来自节点和关系的原始数据,但您也可以在 Cypher 中计算聚合统计数据。

假设您想回答以下问题:德克萨斯州 Aetna 承保的总访问次数和总账单金额是多少?以下是回答此问题的 Cypher 查询:

MATCH (p:Payer)<-[c:COVERED_BY]-(v:Visit)-[:AT]->(h:Hospital)
WHERE p.name = "Aetna"
AND h.state_name = "TX"
RETURN COUNT(*) as num_visits,
SUM(c.billing_amount) as total_billing_amount;

在此查询中,您首先匹配在 Hospital 发生且由 Payer 承保的所有 Visits。然后,您可以过滤到Payers(其name 属性为Aetna)和Hospitals(其state_nameTX 的 >。最后,COUNT(*) 计算匹配模式的数量,SUM(c.billing_amount) 为您提供总账单金额。输出如下所示:

结果显示,有 198 次访问符合此模式,总账单金额约为 5,056,439 美元。

您现在对 Cypher 基础知识以及可以回答的问题类型有了深入的了解。简而言之,Cypher 非常擅长匹配复杂的关系,而不需要详细的查询。您可以使用 Neo4j 和 Cypher 做更多事情,但是您在本节中获得的知识足以开始构建聊天机器人,这就是您下一步要做的事情。

第四步:在LangChain中构建Graph RAG聊天机器人

到目前为止,完成所有准备设计和数据工作后,您终于准备好构建聊天机器人了!您可能会注意到,借助 Neo4j 中存储的医院系统数据以及 LangChain 抽象的强大功能,构建聊天机器人并不需要太多工作。这是人工智能和机器学习项目中的一个共同主题——大部分工作是设计、数据准备和部署,而不是构建人工智能本身。

在开始之前,请将 chatbot_api/ 文件夹添加到您的项目中,其中包含以下文件和文件夹:

./
│
├── chatbot_api/
│   │
│   ├── src/
│   │   │
│   │   ├── agents/
│   │   │   └── hospital_rag_agent.py
│   │   │
│   │   ├── chains/
│   │   │   ├── hospital_cypher_chain.py
│   │   │   └── hospital_review_chain.py
│   │   │
│   │   ├── tools/
│   │   │   └── wait_times.py
│   │
│   └── pyproject.toml
│
├── hospital_neo4j_etl/
│   │
│   ├── src/
│   │   ├── entrypoint.sh
│   │   └── hospital_bulk_csv_write.py
│   │
│   ├── Dockerfile
│   └── pyproject.toml
│
├── .env
└── docker-compose.yml

您还需要向 .env 文件中添加更多环境变量:

OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>

NEO4J_URI=<YOUR_NEO4J_URI>
NEO4J_USERNAME=<YOUR_NEO4J_URI>
NEO4J_PASSWORD=<YOUR_NEO4J_PASSWORD>

HOSPITALS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/hospitals.csv
PAYERS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/payers.csv
PHYSICIANS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/physicians.csv
PATIENTS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/patients.csv
VISITS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/visits.csv
REVIEWS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/reviews.csv

HOSPITAL_AGENT_MODEL=gpt-3.5-turbo-1106
HOSPITAL_CYPHER_MODEL=gpt-3.5-turbo-1106
HOSPITAL_QA_MODEL=gpt-3.5-turbo-0125

您的 .env 文件现在包含指定您将用于聊天机器人的不同组件的 LLM 的变量。您已将这些模型指定为环境变量,以便您可以轻松地在不同的 OpenAI 模型之间切换,而无需更改任何代码。但请记住,每个法学硕士都可能受益于独特的提示策略,因此如果您计划使用不同的法学硕士套件,您可能需要修改提示。

您应该已经完成了 hospital_neo4j_etl/ 文件夹,并且 docker-compose.yml.env 与之前相同。打开chatbot_api/pyproject.toml并添加以下依赖项:

[project]
name = "chatbot_api"
version = "0.1"
dependencies = [
    "asyncio==3.4.3",
    "fastapi==0.109.0",
    "langchain==0.1.0",
    "langchain-openai==0.0.2",
    "langchainhub==0.1.14",
    "neo4j==5.14.1",
    "numpy==1.26.2",
    "openai==1.7.2",
    "opentelemetry-api==1.22.0",
    "pydantic==2.5.1",
    "uvicorn==0.25.0"
]

[project.optional-dependencies]
dev = ["black", "flake8"]

您当然可以使用这些依赖项的更新版本(如果可用),但请记住任何可能已弃用的功能。打开终端,激活虚拟环境,导航到 chatbot_api/ 文件夹,然后安装项目的 pyproject.toml 中的依赖项:

(venv) $ python -m pip install .

安装完所有内容后,您就可以构建评论链了!

创建 Neo4j 矢量链

在第 1 步中,您通过构建一个链来实际了解 LangChain,该链使用患者的评论来回答有关患者体验的问题。在本节中,您将构建一个类似的链,只不过您将使用 Neo4j 作为向量索引。

矢量搜索索引在 Neo4j 5.11 中作为公共测试版发布。它们允许您直接在图表上运行语义查询。这对于您的聊天机器人来说非常方便,因为您可以将评论嵌入存储在与结构化医院系统数据相同的位置。

在 LangChain 中,您可以使用 Neo4jVector 创建评论嵌入和链所需的检索器。这是创建评论链的代码:

import os
from langchain.vectorstores.neo4j_vector import Neo4jVector
from langchain_openai import OpenAIEmbeddings
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI
from langchain.prompts import (
    PromptTemplate,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
    ChatPromptTemplate,
)

HOSPITAL_QA_MODEL = os.getenv("HOSPITAL_QA_MODEL")

neo4j_vector_index = Neo4jVector.from_existing_graph(
    embedding=OpenAIEmbeddings(),
    url=os.getenv("NEO4J_URI"),
    username=os.getenv("NEO4J_USERNAME"),
    password=os.getenv("NEO4J_PASSWORD"),
    index_name="reviews",
    node_label="Review",
    text_node_properties=[
        "physician_name",
        "patient_name",
        "text",
        "hospital_name",
    ],
    embedding_node_property="embedding",
)

review_template = """Your job is to use patient
reviews to answer questions about their experience at a hospital. Use
the following context to answer questions. Be as detailed as possible, but
don't make up any information that's not from the context. If you don't know
an answer, say you don't know.
{context}
"""

review_system_prompt = SystemMessagePromptTemplate(
    prompt=PromptTemplate(input_variables=["context"], template=review_template)
)

review_human_prompt = HumanMessagePromptTemplate(
    prompt=PromptTemplate(input_variables=["question"], template="{question}")
)
messages = [review_system_prompt, review_human_prompt]

review_prompt = ChatPromptTemplate(
    input_variables=["context", "question"], messages=messages
)

reviews_vector_chain = RetrievalQA.from_chain_type(
    llm=ChatOpenAI(model=HOSPITAL_QA_MODEL, temperature=0),
    chain_type="stuff",
    retriever=neo4j_vector_index.as_retriever(k=12),
)
reviews_vector_chain.combine_documents_chain.llm_chain.prompt = review_prompt

在第 1 行到第 11 行中,您导入使用 Neo4j 构建审阅链所需的依赖项。在第 13 行中,您加载将用于评论链的聊天模型的名称,并将其存储在 HOSPITAL_QA_MODEL 中。第 15 行到第 29 行在 Neo4j 中创建向量索引。以下是每个参数的详细说明:

  • embedding:用于创建嵌入的模型 - 在本例中您使用的是 OpenAIEmeddings()
  • urlusernamepassword:您的 Neo4j 实例凭据。
  • index_name:为向量索引指定的名称。
  • node_label:要为其创建嵌入的节点。
  • text_node_properties:要包含在嵌入中的节点属性。
  • embedding_node_property:嵌入节点属性的名称。

一旦 Neo4jVector.from_existing_graph() 运行,您将看到 Neo4j 中的每个 Review 节点都有一个 embedding 属性,它是phycian_namepatent_nametexthospital_name 属性。这使您可以回答诸如哪些医院获得了积极评价?之类的问题。它还允许法学硕士告诉您哪些患者和医生写了与您的问题相符的评论。

第 31 行到第 50 行为您的审阅链创建提示模板,与步骤 1 中的操作方式相同。

最后,第 52 行到第 57 行使用 Neo4j 向量索引检索器创建评论向量链,该索引检索器从相似性搜索中返回 12 个评论嵌入。通过在 .from_chain_type() 中将 chain_type 设置为 "stuff",您可以告诉连锁店将所有 12 条评论传递给提示。您可以在 LangChain 的链文档中探索其他链类型。

您已准备好尝试新的评论链。导航到项目的根目录,启动 Python 解释器,然后运行以下命令:

>>> import dotenv
>>> dotenv.load_dotenv()
True

>>> from chatbot_api.src.chains.hospital_review_chain import (
...     reviews_vector_chain
... )

>>> query = """What have patients said about hospital efficiency?
...         Mention details from specific reviews."""

>>> response = reviews_vector_chain.invoke(query)

>>> response.get("result")
"Patients have mentioned different aspects of hospital efficiency in their
reviews. In Kevin Cox's review of Wallace-Hamilton hospital, he mentioned
that the hospital staff was efficient. However, he also mentioned a lack of
personalized attention and communication, which left him feeling neglected.
This suggests that while the hospital may have been efficient in terms of
completing tasks and providing services, they may have lacked in terms of
individualized care and communication with patients.
On the other hand, Beverly Johnson's review of Brown Inc. hospital mentioned
that the hospital had a modern feel and the staff was attentive. However,
she also mentioned that the bureaucratic procedures for check-in and
discharge were cumbersome. This suggests that while the hospital may have
been efficient in terms of its facilities and staff attentiveness, the
administrative processes may have been inefficient and caused inconvenience
for patients. It is important to note that the specific reviews do not
provide a comprehensive picture of hospital efficiency, as they focus on
specific aspects of the hospital experience."

在此块中,您导入 dotenv 并从 .env 加载环境变量。然后,您从 hospital_review_chain 导入 reviews_vector_chain 并通过有关医院效率的问题来调用它。您的连锁店的响应可能与此不同,但法学硕士应该返回一个很好的详细摘要,正如您所说的那样。

在此示例中,请注意响应中如何提及具体的患者和医院名称。发生这种情况是因为您在评论文本中嵌入了医院和患者姓名,因此法学硕士可以使用此信息来回答问题。

接下来,您将创建 Cypher 生成链,用于回答有关结构化医院系统数据的查询。

创建 Neo4j 密码链

正如您在步骤 2 中看到的,您的 Neo4j Cypher 链将接受用户的自然语言查询,将自然语言查询转换为 Cypher 查询,在 Neo4j 中运行 Cypher 查询,并使用 Cypher 查询结果响应用户的查询。您将利用 LangChain 的 GraphCypherQAChain 来实现此目的。

使用 LLM 生成准确的 Cypher 查询可能具有挑战性,特别是如果您有复杂的图表。因此,需要大量的即时工程来向法学硕士展示您的图形结构和查询用例。微调 LLM 以生成查询也是一种选择,但这需要手动管理和标记数据。

要开始创建 Cypher 生成链,请导入依赖项并实例化 Neo4jGraph

import os
from langchain_community.graphs import Neo4jGraph
from langchain.chains import GraphCypherQAChain
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate

HOSPITAL_QA_MODEL = os.getenv("HOSPITAL_QA_MODEL")
HOSPITAL_CYPHER_MODEL = os.getenv("HOSPITAL_CYPHER_MODEL")

graph = Neo4jGraph(
    url=os.getenv("NEO4J_URI"),
    username=os.getenv("NEO4J_USERNAME"),
    password=os.getenv("NEO4J_PASSWORD"),
)

graph.refresh_schema()

Neo4jGraph 对象是一个 LangChain 包装器,允许 LLM 在 Neo4j 实例上执行查询。您使用 Neo4j 凭据实例化 graph,然后调用 graph.refresh_schema() 将最近的更改同步到您的实例。

Cypher 生成链的下一个也是最重要的组成部分是提示模板。看起来是这样的:

# ...

cypher_generation_template = """
Task:
Generate Cypher query for a Neo4j graph database.

Instructions:
Use only the provided relationship types and properties in the schema.
Do not use any other relationship types or properties that are not provided.

Schema:
{schema}

Note:
Do not include any explanations or apologies in your responses.
Do not respond to any questions that might ask anything other than
for you to construct a Cypher statement. Do not include any text except
the generated Cypher statement. Make sure the direction of the relationship is
correct in your queries. Make sure you alias both entities and relationships
properly. Do not run any queries that would add to or delete from
the database. Make sure to alias all statements that follow as with
statement (e.g. WITH v as visit, c.billing_amount as billing_amount)
If you need to divide numbers, make sure to
filter the denominator to be non zero.

Examples:
# Who is the oldest patient and how old are they?
MATCH (p:Patient)
RETURN p.name AS oldest_patient,
       duration.between(date(p.dob), date()).years AS age
ORDER BY age DESC
LIMIT 1

# Which physician has billed the least to Cigna
MATCH (p:Payer)<-[c:COVERED_BY]-(v:Visit)-[t:TREATS]-(phy:Physician)
WHERE p.name = 'Cigna'
RETURN phy.name AS physician_name, SUM(c.billing_amount) AS total_billed
ORDER BY total_billed
LIMIT 1

# Which state had the largest percent increase in Cigna visits
# from 2022 to 2023?
MATCH (h:Hospital)<-[:AT]-(v:Visit)-[:COVERED_BY]->(p:Payer)
WHERE p.name = 'Cigna' AND v.admission_date >= '2022-01-01' AND
v.admission_date < '2024-01-01'
WITH h.state_name AS state, COUNT(v) AS visit_count,
     SUM(CASE WHEN v.admission_date >= '2022-01-01' AND
     v.admission_date < '2023-01-01' THEN 1 ELSE 0 END) AS count_2022,
     SUM(CASE WHEN v.admission_date >= '2023-01-01' AND
     v.admission_date < '2024-01-01' THEN 1 ELSE 0 END) AS count_2023
WITH state, visit_count, count_2022, count_2023,
     (toFloat(count_2023) - toFloat(count_2022)) / toFloat(count_2022) * 100
     AS percent_increase
RETURN state, percent_increase
ORDER BY percent_increase DESC
LIMIT 1

# How many non-emergency patients in North Carolina have written reviews?
MATCH (r:Review)<-[:WRITES]-(v:Visit)-[:AT]->(h:Hospital)
WHERE h.state_name = 'NC' and v.admission_type <> 'Emergency'
RETURN count(*)

String category values:
Test results are one of: 'Inconclusive', 'Normal', 'Abnormal'
Visit statuses are one of: 'OPEN', 'DISCHARGED'
Admission Types are one of: 'Elective', 'Emergency', 'Urgent'
Payer names are one of: 'Cigna', 'Blue Cross', 'UnitedHealthcare', 'Medicare',
'Aetna'

A visit is considered open if its status is 'OPEN' and the discharge date is
missing.
Use abbreviations when
filtering on hospital states (e.g. "Texas" is "TX",
"Colorado" is "CO", "North Carolina" is "NC",
"Florida" is "FL", "Georgia" is "GA", etc.)

Make sure to use IS NULL or IS NOT NULL when analyzing missing properties.
Never return embedding properties in your queries. You must never include the
statement "GROUP BY" in your query. Make sure to alias all statements that
follow as with statement (e.g. WITH v as visit, c.billing_amount as
billing_amount)
If you need to divide numbers, make sure to filter the denominator to be non
zero.

The question is:
{question}
"""

cypher_generation_prompt = PromptTemplate(
    input_variables=["schema", "question"], template=cypher_generation_template
)

仔细阅读cypher_ Generation_template的内容。请注意您如何向 LLM 提供非常具体的说明,说明在生成 Cypher 查询时应该做什么和不应该做什么。最重要的是,您将使用 schema 参数、一些示例查询以及一些节点属性的分类值向 LLM 展示您的图形结构。

您在提示模板中提供的所有详细信息都会提高 LLM 为给定问题生成正确 Cypher 查询的机会。如果您对所有这些细节的必要性感到好奇,请尝试使用尽可能少的细节创建您自己的提示模板。然后通过您的 Cypher 链运行问题,看看它是否正确生成 Cypher 查询。

从那里,您可以迭代更新您的提示模板,以纠正 LLM 难以生成的查询,但请确保您也了解您正在使用的输入令牌的数量。与您的评论链一样,您需要一个可靠的系统来评估提示模板以及链生成的 Cypher 查询的正确性。然而,正如您将看到的,上面的模板是一个很好的起点。

接下来,您为链的问答组件定义提示模板。该模板告诉 LLM 使用 Cypher 查询结果为用户的查询生成格式良好的答案:

# ...

qa_generation_template = """You are an assistant that takes the results
from a Neo4j Cypher query and forms a human-readable response. The
query results section contains the results of a Cypher query that was
generated based on a user's natural language question. The provided
information is authoritative, you must never doubt it or try to use
your internal knowledge to correct it. Make the answer sound like a
response to the question.

Query Results:
{context}

Question:
{question}

If the provided information is empty, say you don't know the answer.
Empty information looks like this: []

If the information is not empty, you must provide an answer using the
results. If the question involves a time duration, assume the query
results are in units of days unless otherwise specified.

When names are provided in the query results, such as hospital names,
beware  of any names that have commas or other punctuation in them.
For instance, 'Jones, Brown and Murray' is a single hospital name,
not multiple hospitals. Make sure you return any list of names in
a way that isn't ambiguous and allows someone to tell what the full
names are.

Never say you don't have the right information if there is data in
the query results. Always use the data in the query results.

Helpful Answer:
"""

qa_generation_prompt = PromptTemplate(
    input_variables=["context", "question"], template=qa_generation_template
)

该模板所需的详细信息比 Cypher 生成模板要少得多,并且仅当您希望 LLM 做出不同的响应,或者您注意到它没有按照您想要的方式使用查询结果时,您才需要修改它。创建 Cypher 链的最后一步是实例化 GraphCypherQAChain 对象:

# ...

hospital_cypher_chain = GraphCypherQAChain.from_llm(
    cypher_llm=ChatOpenAI(model=HOSPITAL_CYPHER_MODEL, temperature=0),
    qa_llm=ChatOpenAI(model=HOSPITAL_QA_MODEL, temperature=0),
    graph=graph,
    verbose=True,
    qa_prompt=qa_generation_prompt,
    cypher_prompt=cypher_generation_prompt,
    validate_cypher=True,
    top_k=100,
)

以下是 GraphCypherQAChain.from_llm() 中使用的参数的细分:

  • cypher_llm:用于生成 Cypher 查询的 LLM。
  • qa_llm:LLM 用于根据 Cypher 查询结果生成答案。
  • graph:连接到 Neo4j 实例的 Neo4jGraph 对象。
  • verbose:是否应打印您的链执行的中间步骤。
  • qa_prompt:回答问题/查询的提示模板。
  • cypher_prompt:生成Cypher查询的提示模板。
  • validate_cypher:如果为 true,则在运行之前将检查 Cypher 查询是否有错误并进行更正。请注意,这并不能保证 Cypher 查询有效。相反,它纠正使用正则表达式可以轻松检测到的简单语法错误。
  • top_k:要包含在 qa_prompt 中的查询结果数。

您的医院系统 Cypher 生成链已准备好使用!它的工作方式与您的评论链相同。导航到您的项目目录并启动一个新的 Python 解释器会话,然后尝试一下:

>>> import dotenv
>>> dotenv.load_dotenv()
True

>>> from chatbot_api.src.chains.hospital_cypher_chain import (
... hospital_cypher_chain
... )

>>> question = """What is the average visit duration for
... emergency visits in North Carolina?"""
>>> response = hospital_cypher_chain.invoke(question)


> Entering new GraphCypherQAChain chain...
Generated Cypher:
MATCH (v:Visit)-[:AT]->(h:Hospital)
WHERE h.state_name = 'NC' AND v.admission_type = 'Emergency'
AND v.status = 'DISCHARGED'
WITH v, duration.between(date(v.admission_date),
date(v.discharge_date)).days AS visit_duration
RETURN AVG(visit_duration) AS average_visit_duration
Full Context:
[{'average_visit_duration': 15.072972972972991}]

> Finished chain.

>>> response.get("result")
'The average visit duration for emergency visits in North
Carolina is 15.07 days.'

加载环境变量、导入hospital_cypher_chain并通过问题调用它后,您可以看到您的链回答问题所采取的步骤。花点时间欣赏一下您的链在生成 Cypher 查询时取得的一些成就:

  • Cypher 生成法学硕士从提供的图形模式中理解了就诊和医院之间的关系。
  • 即使您询问有关北卡罗来纳州的信息,法学硕士从提示中知道要使用州缩写NC
  • LLM 知道 admission_type 属性只有第一个字母大写,而 status 属性全部大写。
  • QA生成LLM从你的提示中知道查询结果是以天为单位的。

您可以尝试有关医院系统的各种查询。例如,这是一个转换为 Cypher 的相对具有挑战性的问题:

>>> question = """Which state had the largest percent increase
...            in Medicaid visits from 2022 to 2023?"""
>>> response = hospital_cypher_chain.invoke(question)


> Entering new GraphCypherQAChain chain...
Generated Cypher:
MATCH (h:Hospital)<-[:AT]-(v:Visit)-[:COVERED_BY]->(p:Payer)
WHERE p.name = 'Medicaid' AND v.admission_date >= '2022-01-01'
AND v.admission_date < '2024-01-01'
WITH h.state_name AS state, COUNT(v) AS visit_count,
     SUM(CASE WHEN v.admission_date >= '2022-01-01'
     AND v.admission_date < '2023-01-01' THEN 1 ELSE 0 END) AS count_2022,
     SUM(CASE WHEN v.admission_date >= '2023-01-01'
     AND v.admission_date < '2024-01-01' THEN 1 ELSE 0 END) AS count_2023
WITH state, visit_count, count_2022, count_2023,
     (toFloat(count_2023) - toFloat(count_2022)) / toFloat(count_2022) * 100
     AS percent_increase
RETURN state, percent_increase
ORDER BY percent_increase DESC
LIMIT 1
Full Context:
[{'state': 'TX', 'percent_increase': 8.823529411764707}]

> Finished chain.

>>> response.get("result")
'The state with the largest percent increase in Medicaid visits
from 2022 to 2023 is Texas (TX), with a percent increase of 8.82%.'

为了回答从 2022 年到 2023 年哪个州的医疗补助就诊百分比增幅最大?,法学硕士必须生成一个相当详细的 Cypher 查询,涉及多个节点、关系和过滤器。尽管如此,它还是能够得出正确的答案。

聊天机器人需要的最后一项功能是回答有关等待时间的问题,这就是您接下来要介绍的内容。

创建等待时间函数

聊天机器人需要的最后一项功能是回答有关医院等待时间的问题。如前所述,您的组织不会在任何地方存储等待时间数据,因此您的聊天机器人必须从外部源获取它。您将为此编写两个函数 - 一个模拟查找医院当前的等待时间,另一个查找等待时间最短的医院。

首先定义函数来获取医院当前的等待时间:

import os
from typing import Any
import numpy as np
from langchain_community.graphs import Neo4jGraph

def _get_current_hospitals() -> list[str]:
    """Fetch a list of current hospital names from a Neo4j database."""
    graph = Neo4jGraph(
        url=os.getenv("NEO4J_URI"),
        username=os.getenv("NEO4J_USERNAME"),
        password=os.getenv("NEO4J_PASSWORD"),
    )

    current_hospitals = graph.query(
        """
        MATCH (h:Hospital)
        RETURN h.name AS hospital_name
        """
    )

    return [d["hospital_name"].lower() for d in current_hospitals]

def _get_current_wait_time_minutes(hospital: str) -> int:
    """Get the current wait time at a hospital in minutes."""
    current_hospitals = _get_current_hospitals()

    if hospital.lower() not in current_hospitals:
        return -1

    return np.random.randint(low=0, high=600)


def get_current_wait_times(hospital: str) -> str:
    """Get the current wait time at a hospital formatted as a string."""
    wait_time_in_minutes = _get_current_wait_time_minutes(hospital)

    if wait_time_in_minutes == -1:
        return f"Hospital '{hospital}' does not exist."

    hours, minutes = divmod(wait_time_in_minutes, 60)

    if hours > 0:
        return f"{hours} hours {minutes} minutes"
    else:
        return f"{minutes} minutes"

您定义的第一个函数是 _get_current_hospitals(),它从 Neo4j 数据库返回医院名称列表。然后,_get_current_wait_time_months() 将医院名称作为输入。如果医院名称无效,_get_current_wait_time_months() 返回 -1。如果医院名称有效,_get_current_wait_time_months() 将返回 0 到 600 之间的随机整数,模拟等待时间(以分钟为单位)。

然后,您定义 get_current_wait_times(),它是 _get_current_wait_time_months() 的包装器,返回格式化为字符串的等待时间。

您可以使用 _get_current_wait_time_months() 定义第二个函数,用于查找等待时间最短的医院:

# ...

def get_most_available_hospital(_: Any) -> dict[str, float]:
    """Find the hospital with the shortest wait time."""
    current_hospitals = _get_current_hospitals()

    current_wait_times = [
        _get_current_wait_time_minutes(h) for h in current_hospitals
    ]

    best_time_idx = np.argmin(current_wait_times)
    best_hospital = current_hospitals[best_time_idx]
    best_wait_time = current_wait_times[best_time_idx]

    return {best_hospital: best_wait_time}

在这里,您定义 get_most_available_hospital() ,它对每个医院调用 _get_current_wait_time_minutes() 并返回等待时间最短的医院。请注意 get_most_available_hospital() 如何具有一次性输入 _。您的代理稍后将需要它,因为它旨在将输入传递到函数中。

以下是如何使用 get_current_wait_times()get_most_available_hospital()

>>> import dotenv
>>> dotenv.load_dotenv()
True

>>> from chatbot_api.src.tools.wait_times import (
...     get_current_wait_times,
...     get_most_available_hospital,
... )

>>> get_current_wait_times("Wallace-Hamilton")
'1 hours 35 minutes'

>>> get_current_wait_times("fake hospital")
"Hospital 'fake hospital' does not exist."

>>> get_most_available_hospital(None)
{'cunningham and sons': 24}

加载环境变量后,您可以调用 get_current_wait_times("Wallace-Hamilton"),它会返回 Wallace-Hamilton 医院当前的等待时间(以分钟为单位)。当您尝试get_current_wait_times("fake Hospital")时,您会收到一个字符串,告诉您数据库中不存在fake Hospital

最后,get_most_available_hospital() 返回一个字典,存储等待时间最短的医院的等待时间(以分钟为单位)。接下来,您将创建一个使用这些功能以及 Cypher 和审查链的代理,以回答有关医院系统的任意问题。

创建聊天机器人代理

如果你已经做到了这一步,请拍拍自己的背。您已经了解了很多信息,您终于准备好将所有信息拼凑在一起并组装将用作聊天机器人的代理。根据您给出的查询,您的代理需要在 Cypher 链、评论链和等待时间功能之间做出决定。

首先加载代理的依赖项,从环境变量中读取代理模型名称,然后从 LangChain Hub 加载提示模板:

import os
from langchain_openai import ChatOpenAI
from langchain.agents import (
    create_openai_functions_agent,
    Tool,
    AgentExecutor,
)
from langchain import hub
from chains.hospital_review_chain import reviews_vector_chain
from chains.hospital_cypher_chain import hospital_cypher_chain
from tools.wait_times import (
    get_current_wait_times,
    get_most_available_hospital,
)

HOSPITAL_AGENT_MODEL = os.getenv("HOSPITAL_AGENT_MODEL")

hospital_agent_prompt = hub.pull("hwchase17/openai-functions-agent")

请注意您如何导入 reviews_vector_chainhospital_cypher_chainget_current_wait_times()get_most_available_hospital()。您的代理将直接使用这些作为工具。 HOSPITAL_AGENT_MODEL 是 LLM,它将充当代理的大脑,决定调用哪些工具以及传递哪些输入。

您无需为代理定义自己的提示(您当然可以这样做),而是从 LangChain Hub 加载预定义的提示。 LangChain hub 允许您上传、浏览、拉取、测试和管理提示。在这种情况下,OpenAI 功能代理的默认提示效果很好。

接下来,您定义代理可以使用的工具列表:

# ...

tools = [
    Tool(
        name="Experiences",
        func=reviews_vector_chain.invoke,
        description="""Useful when you need to answer questions
        about patient experiences, feelings, or any other qualitative
        question that could be answered about a patient using semantic
        search. Not useful for answering objective questions that involve
        counting, percentages, aggregations, or listing facts. Use the
        entire prompt as input to the tool. For instance, if the prompt is
        "Are patients satisfied with their care?", the input should be
        "Are patients satisfied with their care?".
        """,
    ),
    Tool(
        name="Graph",
        func=hospital_cypher_chain.invoke,
        description="""Useful for answering questions about patients,
        physicians, hospitals, insurance payers, patient review
        statistics, and hospital visit details. Use the entire prompt as
        input to the tool. For instance, if the prompt is "How many visits
        have there been?", the input should be "How many visits have
        there been?".
        """,
    ),
    Tool(
        name="Waits",
        func=get_current_wait_times,
        description="""Use when asked about current wait times
        at a specific hospital. This tool can only get the current
        wait time at a hospital and does not have any information about
        aggregate or historical wait times. Do not pass the word "hospital"
        as input, only the hospital name itself. For example, if the prompt
        is "What is the current wait time at Jordan Inc Hospital?", the
        input should be "Jordan Inc".
        """,
    ),
    Tool(
        name="Availability",
        func=get_most_available_hospital,
        description="""
        Use when you need to find out which hospital has the shortest
        wait time. This tool does not have any information about aggregate
        or historical wait times. This tool returns a dictionary with the
        hospital name as the key and the wait time in minutes as the value.
        """,
    ),
]

您的客服人员可以使用四种工具:体验图表等待可用性体验图表工具从各自的链中调用.invoke(),而等待可用性 调用您定义的等待时间函数。请注意,许多工具描述都有少量提示,告诉代理何时应该使用该工具,并为其提供要传递哪些输入的示例。

与连锁店一样,良好的及时工程对于代理商的成功至关重要。您必须清楚地描述每个工具以及如何使用它,以便您的代理不会因查询而感到困惑。

最后一步是实例化您的代理:

# ...

chat_model = ChatOpenAI(
    model=HOSPITAL_AGENT_MODEL,
    temperature=0,
)

hospital_rag_agent = create_openai_functions_agent(
    llm=chat_model,
    prompt=hospital_agent_prompt,
    tools=tools,
)

hospital_rag_agent_executor = AgentExecutor(
    agent=hospital_rag_agent,
    tools=tools,
    return_intermediate_steps=True,
    verbose=True,
)

您首先使用 HOSPITAL_AGENT_MODEL 作为 LLM 来初始化 ChatOpenAI 对象。然后,您可以使用 create_openai_functions_agent() 创建 OpenAI 函数代理。这将创建一个由 OpenAI 设计的代理,用于将输入传递给函数。它通过返回存储函数输入及其相应值的 JSON 对象来实现此目的。

要创建代理运行时,请将代理和工具传递到 AgentExecutor 中。将 return_intermediate_steps 和 verbose 设置为 true 可以让您查看代理的思维过程及其调用的工具。

至此,您已经完成了医院系统代理的构建。要尝试一下,您必须导航到 chatbot_api/src/ 文件夹并从那里启动一个新的 REPL 会话。

您现在可以在命令行上尝试医院系统代理:

>>> import dotenv
>>> dotenv.load_dotenv()
True

>>> from agents.hospital_rag_agent import hospital_rag_agent_executor

>>> response = hospital_rag_agent_executor.invoke(
...     {"input": "What is the wait time at Wallace-Hamilton?"}
... )

> Entering new AgentExecutor chain...

Invoking: `Waits` with `Wallace-Hamilton`

54The current wait time at Wallace-Hamilton is 54 minutes.

> Finished chain.

>>> response.get("output")
'The current wait time at Wallace-Hamilton is 54 minutes.'

>>> response = hospital_rag_agent_executor.invoke(
...     {"input": "Which hospital has the shortest wait time?"}
... )

> Entering new AgentExecutor chain...

Invoking: `Availability` with `shortest wait time`

{'smith, edwards and obrien': 2}The hospital with the shortest
wait time is Smith, Edwards and O'Brien, with a wait time of 2 minutes.

> Finished chain.

>>> response.get("output")
"The hospital with the shortest wait time is Smith, Edwards
and O'Brien, with a wait time of 2 minutes."

加载环境变量后,您向代理询问等待时间。您可以准确地看到它对您的每个查询的响应。例如,当您询问“Wallace-Hamilton 的等待时间是多少?”时,它会调用 等待 工具并传递 Wallace-Hamilton作为输入。这意味着代理正在调用 get_current_wait_times("Wallace-Hamilton"),观察返回值,并使用返回值来回答您的问题。

要查看代理的全部功能,您可以向其询问有关需要患者审核才能回答的患者体验的问题:

>>> response = hospital_rag_agent_executor.invoke(
...     {
...         "input": (
...             "What have patients said about their "
...             "quality of rest during their stay?"
...         )
...     }
... )

> Entering new AgentExecutor chain...

Invoking: `Experiences` with `What have patients said about their quality of
rest during their stay?`

{'query': 'What have patients said about their quality of rest during their
stay?','result': "Patients have mentioned that the constant interruptions
for routine checks and the noise level at night were disruptive and made
it difficult for them to get a good night's sleep during their stay.
Additionally, some patients have complained about uncomfortable beds
affecting their quality of rest."}Patients have mentioned that the
constant interruptions for routine checks and the noise level at night
were disruptive and made it difficult for them to get a good night's sleep
during their stay. Additionally, some patients have complained about
uncomfortable beds affecting their quality of rest.

> Finished chain.

>>> response.get("output")
"Patients have mentioned that the constant interruptions for routine checks
and the noise level at night were disruptive and made it difficult for them
to get a good night's sleep during their stay. Additionally, some patients
have complained about uncomfortable beds affecting their quality of rest."

请注意,您从未在问题中明确提及评论或经历。根据工具描述,代理知道它需要调用体验。最后,您可以向代理提出一个需要 Cypher 查询来回答的问题:

>>> response = hospital_rag_agent_executor.invoke(
...     {
...         "input": (
...             "Which physician has treated the "
...             "most patients covered by Cigna?"
...         )
...     }
... )

> Entering new AgentExecutor chain...

Invoking: `Graph` with `Which physician has treated the most patients
covered by Cigna?`

> Entering new GraphCypherQAChain chain...
Generated Cypher:
MATCH (phy:Physician)-[:TREATS]->(v:Visit)-[:COVERED_BY]->(p:Payer)
WHERE p.name = 'Cigna'
WITH phy, COUNT(DISTINCT v) AS patient_count
RETURN phy.name AS physician_name, patient_count
ORDER BY patient_count DESC
LIMIT 1
Full Context:
[{'physician_name': 'Renee Brown', 'patient_count': 10}]

> Finished chain.
{'query': 'Which physician has treated the most patients covered by Cigna?',
'result': 'The physician who has treated the most patients covered by Cigna
is Dr. Renee Brown. She has treated a total of 10 patients.'}The
physician who has treated the most patients covered by Cigna is Dr. Renee
Brown. She has treated a total of 10 patients.

> Finished chain.

>>> response.get("output")
'The physician who has treated the most patients covered by
Cigna is Dr. Renee Brown.
She has treated a total of 10 patients.'

您的代理具有出色的能力,可以根据您的查询知道要使用哪些工具以及要传递哪些输入。这是功能齐全的聊天机器人。它有可能回答利益相关者根据给定的要求可能提出的所有问题,而且到目前为止,它似乎做得很好。

当您向聊天机器人询问更多问题时,您几乎肯定会遇到它调用错误工具或生成错误答案的情况。虽然修改提示可以帮助解决不正确的答案,但有时您可以修改输入查询来帮助您的聊天机器人。看一下这个例子:

>>> response = hospital_rag_agent_executor.invoke(
...     {"input": "Show me reviews written by patient 7674."}
... )

> Entering new AgentExecutor chain...

Invoking: `Experiences` with `Show me reviews written by patient 7674.`

{'query': 'Show me reviews written by patient 7674.', 'result': 'I\'m sorry,
but there are no reviews provided by a patient with the identifier "7674" in
the context given. If you have any other questions or need information about
the reviews provided, feel free to ask.'}I'm sorry, but there are no reviews
provided by a patient with the identifier "7674" in the context given. If
you have any other questions or need information about the reviews provided,
feel free to ask.

> Finished chain.

>>> response.get("output")
'I\'m sorry, but there are no reviews provided by a patient with the identifier
"7674" in the context given. If you have any other questions or need information
about the reviews provided, feel free to ask.'

在此示例中,您要求客服人员向您显示患者 7674 撰写的评论。您的客服人员调用体验,但没有找到您正在寻找的答案。虽然可以使用语义向量搜索找到答案,但您可以通过生成 Cypher 查询来查找与患者 ID 7674 对应的评论来获得准确的答案。为了帮助您的客服人员理解这一点,您可以在查询中添加其他详细信息:

>>> response = hospital_rag_agent_executor.invoke(
...     {
...         "input": (
...             "Query the graph database to show me "
...             "the reviews written by patient 7674"
...         )
...     }
... )

> Entering new AgentExecutor chain...

Invoking: `Graph` with `Show me reviews written by patient 7674`

> Entering new GraphCypherQAChain chain...
Generated Cypher:
MATCH (p:Patient {id: 7674})-[:HAS]->(v:Visit)-[:WRITES]->(r:Review)
RETURN r.text AS review_written

Full Context:
[{'review_written': 'The hospital provided exceptional care,
but the billing process was confusing and frustrating. Clearer
communication about costs would have been appreciated.'}]

> Finished chain.
{'query': 'Show me reviews written by patient 7674', 'result': 'Here
is a review written by patient 7674: "The hospital provided exceptional
care, but the billing process was confusing and frustrating. Clearer
communication about costs would have been appreciated."'}Patient 7674
wrote the following review: "The hospital provided exceptional
care, but the billing process was confusing and frustrating.
Clearer communication about costs would have been appreciated."

> Finished chain.

>>> response.get("output")
'Patient 7674 wrote the following review: "The hospital provided exceptional
care, but the billing process was confusing and frustrating. Clearer
communication about costs would have been appreciated."'

在这里,您明确告诉代理您想要查询图形数据库,该数据库会正确调用 Graph 来查找与患者 ID 7674 匹配的评论。在查询中提供更多详细信息是一种简单而有效的方法当您的代理明显调用了错误的工具时,为您的代理提供指导。

与您的评论和 Cypher 链一样,在将其提交给利益相关者之前,您需要提出一个评估代理的框架。您想要评估的主要功能是代理使用正确的输入调用正确的工具的能力,以及理解和解释其调用的工具的输出的能力。

在最后一步中,您将学习如何使用 FastAPI 和 Streamlit 部署医院系统代理。这将使任何调用 API 端点或与 Streamlit UI 交互的人都可以访问您的代理。

步骤5:部署LangChain Agent

最后,您拥有了一个可以正常运行的 LangChain 代理,它可以作为您的医院系统聊天机器人。您需要做的最后一件事就是让您的聊天机器人出现在利益相关者面前。为此,您将聊天机器人部署为 FastAPI 端点,并创建 Streamlit UI 来与端点交互。

在开始之前,请在项目的根目录中创建两个名为 chatbot_frontend/tests/ 的新文件夹。您还需要向 chatbot_api/ 添加一些其他文件和文件夹:

./
│
├── chatbot_api/
│   │
│   ├── src/
│   │   │
│   │   ├── agents/
│   │   │   └── hospital_rag_agent.py
│   │   │
│   │   ├── chains/
│   │   │   ├── hospital_cypher_chain.py
│   │   │   └── hospital_review_chain.py
│   │   │
│   │   ├── models/
│   │   │   └── hospital_rag_query.py
│   │   │
│   │   ├── tools/
│   │   │   └── wait_times.py
│   │   │
│   │   ├── utils/
│   │   │   └── async_utils.py
│   │   │
│   │   ├── entrypoint.sh
│   │   └── main.py
│   │
│   ├── Dockerfile
│   └── pyproject.toml
│
├── chatbot_frontend/
│   │
│   ├── src/
│   │   ├── entrypoint.sh
│   │   └── main.py
│   │
│   ├── Dockerfile
│   └── pyproject.toml
│
├── hospital_neo4j_etl/
│   │
│   ├── src/
│   │   ├── entrypoint.sh
│   │   └── hospital_bulk_csv_write.py
│   │
│   ├── Dockerfile
│   └── pyproject.toml
│
├── tests/
│   ├── async_agent_requests.py
│   └── sync_agent_requests.py
│
├── .env
└── docker-compose.yml

您需要 chatbot_api 中的新文件来构建 FastAPI 应用程序,并且 tests/ 有两个脚本来演示向代理发出异步请求的强大功能。最后,chatbot_frontend/ 包含与您的聊天机器人交互的 Streamlit UI 代码。您将首先创建一个 FastAPI 应用程序来为您的代理提供服务。

使用 FastAPI 为代理提供服务

FastAPI 是一个现代的高性能 Web 框架,用于基于标准类型提示使用 Python 构建 API。它具有许多出色的功能,包括开发速度、运行速度和强大的社区支持,使其成为为聊天机器人代理提供服务的绝佳选择。

您将通过 POST 请求为代理提供服务,因此第一步是定义您期望在请求正文中获取哪些数据以及请求返回哪些数据。 FastAPI 使用 Pydantic 执行此操作:

from pydantic import BaseModel

class HospitalQueryInput(BaseModel):
    text: str

class HospitalQueryOutput(BaseModel):
    input: str
    output: str
    intermediate_steps: list[str]

在此脚本中,您定义 Pydantic 模型 HospitalQueryInputHospitalQueryOutputHospitalQueryInput 用于验证 POST 请求正文是否包含 text 字段,表示聊天机器人响应的查询。 HospitalQueryOutput 验证发送回用户的响应正文,包括 inputoutputintermediate_step 字段。

FastAPI 的一大特色是其异步服务功能。由于您的代理调用托管在外部服务器上的 OpenAI 模型,因此在您的代理等待响应时总会存在延迟。这是您使用异步编程的绝佳机会。

您可以让代理连续发出多个请求并在收到响应时存储响应,而不是等待 OpenAI 响应每个代理的请求。如果您有多个问题需要代理回复,这将为您节省大量时间。

如前所述,Neo4j 有时可能会出现间歇性连接问题,通常可以通过建立新连接来解决。因此,您需要实现适用于异步函数的重试逻辑:

import asyncio

def async_retry(max_retries: int=3, delay: int=1):
    def decorator(func):
        async def wrapper(*args, **kwargs):
            for attempt in range(1, max_retries + 1):
                try:
                    result = await func(*args, **kwargs)
                    return result
                except Exception as e:
                    print(f"Attempt {attempt} failed: {str(e)}")
                    await asyncio.sleep(delay)

            raise ValueError(f"Failed after {max_retries} attempts")

        return wrapper

    return decorator

不用担心@async_retry的细节。您需要知道的是,如果异步函数失败,它将重试。接下来您将看到它的用途。

聊天机器人 API 的驱动逻辑位于 chatbot_api/src/main.py 中:

from fastapi import FastAPI
from agents.hospital_rag_agent import hospital_rag_agent_executor
from models.hospital_rag_query import HospitalQueryInput, HospitalQueryOutput
from utils.async_utils import async_retry

app = FastAPI(
    title="Hospital Chatbot",
    description="Endpoints for a hospital system graph RAG chatbot",
)

@async_retry(max_retries=10, delay=1)
async def invoke_agent_with_retry(query: str):
    """Retry the agent if a tool fails to run.

    This can help when there are intermittent connection issues
    to external APIs.
    """
    return await hospital_rag_agent_executor.ainvoke({"input": query})

@app.get("/")
async def get_status():
    return {"status": "running"}

@app.post("/hospital-rag-agent")
async def query_hospital_agent(query: HospitalQueryInput) -> HospitalQueryOutput:
    query_response = await invoke_agent_with_retry(query.text)
    query_response["intermediate_steps"] = [
        str(s) for s in query_response["intermediate_steps"]
    ]

    return query_response

您导入 FastAPI、代理执行程序、为 POST 请求创建的 Pydantic 模型和@async_retry。然后实例化一个 FastAPI 对象并定义 invoke_agent_with_retry(),这是一个异步运行代理的函数。 invoke_agent_with_retry() 上面的 @async_retry 装饰器确保该函数在失败之前会重试十次,并延迟一秒。

最后,您定义 query_hospital_agent(),它向位于 /hospital-rag-agent 的代理提供 POST 请求。此函数从请求正文中提取 text 字段,将其传递给代理,并将代理的响应返回给用户。

您将使用 Docker 提供此 API,并且需要定义以下入口点文件以在容器内运行:

#!/bin/bash

# Run any setup steps or pre-processing tasks here
echo "Starting hospital RAG FastAPI service..."

# Start the main application
uvicorn main:app --host 0.0.0.0 --port 8000

命令 uvicorn main:app --host 0.0.0.0 --port 8000 在您计算机上的端口 8000 上运行 FastAPI 应用程序。 FastAPI 应用程序的驱动 Dockerfile 如下所示:

# chatbot_api/Dockerfile

FROM python:3.11-slim

WORKDIR /app
COPY ./src/ /app

COPY ./pyproject.toml /code/pyproject.toml
RUN pip install /code/.

EXPOSE 8000
CMD ["sh", "entrypoint.sh"]

Dockerfile 告诉您的容器使用 python:3.11-slim 发行版,将 chatbot_api/src/ 中的内容复制到 /容器中的 app 目录,从 pyproject.toml 安装依赖项,然后运行 entrypoint.sh

您需要做的最后一件事是更新 docker-compose.yml 文件以包含 FastAPI 容器:

version: '3'

services:
  hospital_neo4j_etl:
    build:
      context: ./hospital_neo4j_etl
    env_file:
      - .env

  chatbot_api:
    build:
      context: ./chatbot_api
    env_file:
      - .env
    depends_on:
      - hospital_neo4j_etl
    ports:
      - "8000:8000"

此处添加从 ./chatbot_api 中的 Dockerfile 派生的 chatbot_api 服务。它依赖于 hospital_neo4j_etl 并将在端口 8000 上运行。

要运行 API 以及您之前构建的 ETL,请打开终端并运行:

$ docker-compose up --build

如果一切成功运行,您将在 http://localhost:8000/docs#/ 看到类似于以下内容的屏幕:

您可以使用文档页面来测试 hospital-rag-agent 端点,但您无法在此处发出异步请求。要查看端点如何处理异步请求,您可以使用 httpx 等库对其进行测试。

要了解异步请求为您节省了多少时间,请首先使用同步请求建立基准。创建以下脚本:

import time
import requests

CHATBOT_URL = "http://localhost:8000/hospital-rag-agent"

questions = [
   "What is the current wait time at Wallace-Hamilton hospital?",
   "Which hospital has the shortest wait time?",
   "At which hospitals are patients complaining about billing and insurance issues?",
   "What is the average duration in days for emergency visits?",
   "What are patients saying about the nursing staff at Castaneda-Hardy?",
   "What was the total billing amount charged to each payer for 2023?",
   "What is the average billing amount for Medicaid visits?",
   "How many patients has Dr. Ryan Brown treated?",
   "Which physician has the lowest average visit duration in days?",
   "How many visits are open and what is their average duration in days?",
   "Have any patients complained about noise?",
   "How much was billed for patient 789's stay?",
   "Which physician has billed the most to cigna?",
   "Which state had the largest percent increase in Medicaid visits from 2022 to 2023?",
]

request_bodies = [{"text": q} for q in questions]

start_time = time.perf_counter()
outputs = [requests.post(CHATBOT_URL, json=data) for data in request_bodies]
end_time = time.perf_counter()

print(f"Run time: {end_time - start_time} seconds")

在此脚本中,您导入请求时间,定义聊天机器人的URL,创建问题列表,并记录获得响应所需的时间列表中的所有问题。如果您打开终端并运行 sync_agent_requests.py,您将看到回答所有 14 个问题需要多长时间:

(venv) $ python tests/sync_agent_requests.py
Run time: 68.20339595794212 seconds

根据您的 Internet 速度和聊天模型的可用性,您可能会得到略有不同的结果,但您可以看到此脚本运行大约需要 68 秒。接下来,您将异步获得相同问题的答案:

import asyncio
import time
import httpx

CHATBOT_URL = "http://localhost:8000/hospital-rag-agent"

async def make_async_post(url, data):
    timeout = httpx.Timeout(timeout=120)
    async with httpx.AsyncClient() as client:
        response = await client.post(url, json=data, timeout=timeout)
        return response

async def make_bulk_requests(url, data):
    tasks = [make_async_post(url, payload) for payload in data]
    responses = await asyncio.gather(*tasks)
    outputs = [r.json()["output"] for r in responses]
    return outputs

questions = [
   "What is the current wait time at Wallace-Hamilton hospital?",
   "Which hospital has the shortest wait time?",
   "At which hospitals are patients complaining about billing and insurance issues?",
   "What is the average duration in days for emergency visits?",
   "What are patients saying about the nursing staff at Castaneda-Hardy?",
   "What was the total billing amount charged to each payer for 2023?",
   "What is the average billing amount for Medicaid visits?",
   "How many patients has Dr. Ryan Brown treated?",
   "Which physician has the lowest average visit duration in days?",
   "How many visits are open and what is their average duration in days?",
   "Have any patients complained about noise?",
   "How much was billed for patient 789's stay?",
   "Which physician has billed the most to cigna?",
   "Which state had the largest percent increase in Medicaid visits from 2022 to 2023?",
]

request_bodies = [{"text": q} for q in questions]

start_time = time.perf_counter()
outputs = asyncio.run(make_bulk_requests(CHATBOT_URL, request_bodies))
end_time = time.perf_counter()

print(f"Run time: {end_time - start_time} seconds")

async_agent_requests.py 中,您发出与在 sync_agent_requests.py 中执行的请求相同的请求,只不过现在您使用 httpx 异步发出请求。结果如下:

(venv) $ python tests/async_agent_requests.py
Run time: 17.766680584056303 seconds

同样,运行所需的确切时间可能因您而异,但您可以看到异步发出 14 个请求大约快了四倍。异步部署代理允许您扩展到高请求量,而无需增加基础设施需求。虽然总有例外,但当您的代码发出网络绑定请求时,异步提供 REST 端点通常是一个好主意。

通过此 FastAPI 端点的功能,您可以让任何可以访问该端点的人都可以访问您的代理。这对于将您的代理集成到聊天机器人 UI 中非常有用,这就是您接下来要使用 Streamlit 执行的操作。

使用 Streamlit 创建聊天 UI

您的利益相关者需要一种与代理交互的方式,而无需发出手动 API 请求。为了适应这一点,您将构建一个 Streamlit 应用程序,充当利益相关者和 API 之间的接口。以下是 Streamlit UI 的依赖项:

[project]
name = "chatbot_frontend"
version = "0.1"
dependencies = [
   "requests==2.31.0",
   "streamlit==1.29.0"
]

[project.optional-dependencies]
dev = ["black", "flake8"]

Streamlit 应用程序的驱动代码位于 chatbot_frontend/src/main.py 中:

import os
import requests
import streamlit as st

CHATBOT_URL = os.getenv("CHATBOT_URL", "http://localhost:8000/hospital-rag-agent")

with st.sidebar:
    st.header("About")
    st.markdown(
        """
        This chatbot interfaces with a
        [LangChain](https://python.langchain.com/docs/get_started/introduction)
        agent designed to answer questions about the hospitals, patients,
        visits, physicians, and insurance payers in  a fake hospital system.
        The agent uses  retrieval-augment generation (RAG) over both
        structured and unstructured data that has been synthetically generated.
        """
    )

    st.header("Example Questions")
    st.markdown("- Which hospitals are in the hospital system?")
    st.markdown("- What is the current wait time at wallace-hamilton hospital?")
    st.markdown(
        "- At which hospitals are patients complaining about billing and "
        "insurance issues?"
    )
    st.markdown("- What is the average duration in days for closed emergency visits?")
    st.markdown(
        "- What are patients saying about the nursing staff at "
        "Castaneda-Hardy?"
    )
    st.markdown("- What was the total billing amount charged to each payer for 2023?")
    st.markdown("- What is the average billing amount for medicaid visits?")
    st.markdown("- Which physician has the lowest average visit duration in days?")
    st.markdown("- How much was billed for patient 789's stay?")
    st.markdown(
        "- Which state had the largest percent increase in medicaid visits "
        "from 2022 to 2023?"
    )
    st.markdown("- What is the average billing amount per day for Aetna patients?")
    st.markdown("- How many reviews have been written from patients in Florida?")
    st.markdown(
        "- For visits that are not missing chief complaints, "
        "what percentage have reviews?"
    )
    st.markdown(
        "- What is the percentage of visits that have reviews for each hospital?"
    )
    st.markdown(
        "- Which physician has received the most reviews for this visits "
        "they've attended?"
    )
    st.markdown("- What is the ID for physician James Cooper?")
    st.markdown(
        "- List every review for visits treated by physician 270. Don't leave any out."
    )

st.title("Hospital System Chatbot")
st.info(
    "Ask me questions about patients, visits, insurance payers, hospitals, "
    "physicians, reviews, and wait times!"
)

if "messages" not in st.session_state:
    st.session_state.messages = []

for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        if "output" in message.keys():
            st.markdown(message["output"])

        if "explanation" in message.keys():
            with st.status("How was this generated", state="complete"):
                st.info(message["explanation"])

if prompt := st.chat_input("What do you want to know?"):
    st.chat_message("user").markdown(prompt)

    st.session_state.messages.append({"role": "user", "output": prompt})

    data = {"text": prompt}

    with st.spinner("Searching for an answer..."):
        response = requests.post(CHATBOT_URL, json=data)

        if response.status_code == 200:
            output_text = response.json()["output"]
            explanation = response.json()["intermediate_steps"]

        else:
            output_text = """An error occurred while processing your message.
            Please try again or rephrase your message."""
            explanation = output_text

    st.chat_message("assistant").markdown(output_text)
    st.status("How was this generated", state="complete").info(explanation)

    st.session_state.messages.append(
        {
            "role": "assistant",
            "output": output_text,
            "explanation": explanation,
        }
    )

学习 Streamlit 不是本教程的重点,因此您不会获得此代码的详细说明。不过,以下是此 UI 功能的高级概述:

  • 每次用户进行新查询时,都会存储并显示整个聊天记录。
  • UI 接受用户的输入并向代理端点发出同步 POST 请求。
  • 最近的客服响应显示在聊天底部并附加到聊天历史记录中。
  • 代理如何生成其提供给用户的响应的解释。这对于审核目的非常有用,因为您可以查看代理是否调用了正确的工具,并且可以检查该工具是否正常工作。

完成后,您将创建一个入口点文件来运行 UI:

#!/bin/bash

# Run any setup steps or pre-processing tasks here
echo "Starting hospital chatbot frontend..."

# Run the ETL script
streamlit run main.py

最后,用于为 UI 创建图像的 Docker 文件:

FROM python:3.11-slim

WORKDIR /app

COPY ./src/ /app

COPY ./pyproject.toml /code/pyproject.toml
RUN pip install /code/.

CMD ["sh", "entrypoint.sh"]

Dockerfile 与您之前创建的相同。这样,您就可以端到端运行整个聊天机器人应用程序了。

使用 Docker Compose 编排项目

此时,您已经编写了运行聊天机器人所需的所有代码。最后一步是使用 docker-compose 构建并运行您的项目。执行此操作之前,请确保项目目录中具有以下所有文件和文件夹:

./
│
├── chatbot_api/
│   │
│   │
│   ├── src/
│   │   │
│   │   ├── agents/
│   │   │   └── hospital_rag_agent.py
│   │   │
│   │   ├── chains/
│   │   │   │
│   │   │   ├── hospital_cypher_chain.py
│   │   │   └── hospital_review_chain.py
│   │   │
│   │   ├── models/
│   │   │   └── hospital_rag_query.py
│   │   │
│   │   ├── tools/
│   │   │   └── wait_times.py
│   │   │
│   │   ├── utils/
│   │   │   └── async_utils.py
│   │   │
│   │   ├── entrypoint.sh
│   │   └── main.py
│   │
│   ├── Dockerfile
│   └── pyproject.toml
│
├── chatbot_frontend/
│   │
│   ├── src/
│   │   ├── entrypoint.sh
│   │   └── main.py
│   │
│   ├── Dockerfile
│   └── pyproject.toml
│
├── hospital_neo4j_etl/
│   │
│   ├── src/
│   │   ├── entrypoint.sh
│   │   └── hospital_bulk_csv_write.py
│   │
│   ├── Dockerfile
│   └── pyproject.toml
│
├── tests/
│   ├── async_agent_requests.py
│   └── sync_agent_requests.py
│
├── .env
└── docker-compose.yml

您的 .env 文件应具有以下环境变量。其中大部分是您在本教程前面创建的,但您还需要为 CHATBOT_URL 添加一个新的,以便您的 Streamlit 应用能够找到您的 API:

OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>

NEO4J_URI=<YOUR_NEO4J_URI>
NEO4J_USERNAME=<YOUR_NEO4J_USERNAME>
NEO4J_PASSWORD=<YOUR_NEO4J_PASSWORD>

HOSPITALS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/hospitals.csv
PAYERS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/payers.csv
PHYSICIANS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/physicians.csv
PATIENTS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/patients.csv
VISITS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/visits.csv
REVIEWS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/reviews.csv

HOSPITAL_AGENT_MODEL=gpt-3.5-turbo-1106
HOSPITAL_CYPHER_MODEL=gpt-3.5-turbo-1106
HOSPITAL_QA_MODEL=gpt-3.5-turbo-0125

CHATBOT_URL=http://host.docker.internal:8000/hospital-rag-agent

要完成 docker-compose.yml 文件,您需要添加 chatbot_frontend 服务。您的最终 docker-compose.yml 文件应如下所示:

version: '3'

services:
  hospital_neo4j_etl:
    build:
      context: ./hospital_neo4j_etl
    env_file:
      - .env

  chatbot_api:
    build:
      context: ./chatbot_api
    env_file:
      - .env
    depends_on:
      - hospital_neo4j_etl
    ports:
      - "8000:8000"

  chatbot_frontend:
    build:
      context: ./chatbot_frontend
    env_file:
      - .env
    depends_on:
      - chatbot_api
    ports:
      - "8501:8501"

最后,打开终端并运行:

$ docker-compose up --build

一切构建并运行后,您可以通过 http://localhost:8501/ 访问 UI 并开始与聊天机器人聊天:

您已经构建了一个功能齐全的端到端医院系统聊天机器人。花一些时间向它提问,看看它擅长回答哪些问题,找出它失败的地方,并思考如何通过更好的提示或数据来改进它。您可以首先确保成功回答侧栏中的示例问题。

结论

恭喜您完成本深入教程!

您已成功设计、构建并服务了一个 RAG LangChain 聊天机器人,该机器人可以回答有关假医院系统的问题。当然,您可以通过多种方法来改进您在本教程中构建的聊天机器人,但您现在已经充分了解了如何将 LangChain 与您自己的数据集成,从而为您提供了构建各种自定义聊天机器人的创作自由。

在本教程中,您学习了如何:

  • 使用LangChain构建个性化聊天机器人
  • 通过符合业务需求利用可用数据,为假医院系统创建聊天机器人。
  • 考虑在聊天机器人设计中实施图形数据库
  • 为您的项目设置 Neo4j AuraDB 实例。
  • 开发一个能够从 Neo4j 获取结构化非结构化数据的 RAG 聊天机器人。
  • 使用 FastAPIStreamlit 部署您的聊天机器人。

您可以在支持材料中找到该项目的完整源代码和数据,您可以使用以下链接下载: