原文:towardsdatascience.com/a-simple-strategy-to-improve-llm-query-generation-3178a7426c6f

2024 年 3 月,我在Real Python上撰写了一个教程,详细介绍了使用LangChain构建检索增强生成(RAG)聊天机器人的步骤。该聊天机器人正式命名为医院系统聊天机器人,使用Neo4j从包含有关患者、患者评论、医院位置、访问、保险支付者和医生信息的合成医院系统数据集中检索数据。

医院系统聊天机器人代理通过 FastAPI 托管,可以通过Streamlit应用程序访问——所有内容都打包在 Docker Compose 中:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/1b7e7d88f30ed82668f4b51ce4f75b5e.png

医院系统聊天机器人的演示。图片由作者提供。

所有代码均可在GitHub上找到,任何人都可以在Real Python上详细了解该项目。

GitHub – hfhoffman1144/langchain_neo4j_rag_app: 使用 LangChain 和…的知识图谱 RAG 应用程序

在撰写教程之后,我在 GitHub 上维护了该项目,以提高医院系统聊天机器人的功能并使其更适合生产环境。例如,我添加了单元测试,重构了代码以纠正已弃用的功能,创建了使用 GitHub actions 的部署,最近还集成了动态的少样本提示来提高 Cypher 查询生成。

在本教程中,我们将专注于通过动态的少样本提示来提高医院系统聊天机器人生成(Cypher)查询的能力。这项技术将使我们更接近一个更准确、适用于生产的聊天机器人,我们可以无缝地随着时间的推移对其进行更新。

聊天机器人概述

让我们从医院系统聊天机器人的简要概述开始。要深入了解聊天机器人或了解 Neo4j 和 LangChain 的基础知识,请参阅原始的Real Python 教程

在其核心,医院系统聊天机器人是一个LangChain 代理,可以访问多个工具来帮助它回答有关合成医院系统数据集的问题,该数据集最初来源于 Kaggle 上流行的医疗保健数据集。以下是项目当前功能的总结:

  • 工具调用: 聊天机器人代理使用多个工具,例如 LangChain 用于RAG和(模拟)API 调用。

  • 在非结构化数据上执行 RAG: 聊天机器人可以根据患者的评论回答有关患者体验的问题。患者评论使用OpenAI 嵌入模型嵌入并存储在Neo4j 向量索引中。目前,非结构化数据上的 RAG 是基础版本,没有实现像重排序或查询转换这样的高级 RAG 技术。

  • 在结构化数据上执行 RAG (Text-to-Cypher): 聊天机器人可以回答有关存储在 Neo4j 图数据库中的结构化医院系统数据的问题。如果聊天机器人代理认为可以通过查询 Neo4j 图来响应用户的自然语言查询,它将尝试生成并运行一个 Cypher 查询。

  • 通过 FastAPI 和 Streamlit 提供服务: 聊天机器人代理通过异步 FastAPI 端点托管,并通过 Streamlit 应用程序访问。

所有的东西都通过 Docker Compose 打包和编排——有关如何运行聊天机器人的更多详细信息请参阅 README。

langchain_neo4j_rag_app/README.md at main · hfhoffman1144/langchain_neo4j_rag_app

在这个教程中,我们将专注于改进在结构化数据上执行 RAG。为了查看示例,假设我们向聊天机器人提出以下问题:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/e89f4f73288dbf9ad898043eceb92aa2.png

医院系统聊天机器人回答问题。图片由作者提供。

要回答“医院系统中有多少家医院?”这个问题,底层的 LangChain 代理首先需要调用查询生成工具。以下是 API 日志中的样子:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/914d242f6d6f5b864c3d465e9fd09133.png

医院系统聊天机器人调用工具。图片由作者提供。

在这种情况下,代理调用了explore_hospital_database工具,这是一个 LangChain 链,它生成 Cypher 查询并在 Neo4j 数据库上运行。

为了正确回答我们的问题,explore_hospital_database必须生成并运行一个像这样的 Cypher 查询:

MATCH (h:Hospital) 
RETURN COUNT(h) AS total_hospitals

这个简单的 Cypher 查询计算 Neo4j 数据库中医院节点的数量。如果工具可以在数据库中成功生成并运行此查询,结果将返回给代理并显示给用户。

对于任何想要深入了解 Text-To-Cypher 或探索如何将 LLM 与 Neo4j 集成的读者,我强烈推荐阅读 Tomaz Bratanic 的文章

在本教程的剩余部分,我们将专注于通过动态的少样本提示来提高代理生成 Cypher 查询的能力。为了说明为什么我们需要这样做,让我们首先探讨一些数据库查询生成的主要局限性。

查询生成的问题

LLM 查询生成任务,如 Text-To-SQL 和 Text-To-Cypher,因其能够抽象复杂的查询逻辑而受到欢迎。对于非技术利益相关者来说,LLM 查询生成可以帮助回答关键问题,而无需理解查询语言、请求分析师的报告或等待有人构建仪表板。相反,利益相关者可以通过自然语言查询提示聊天机器人,并将结果以可消化的文本摘要形式返回。

从高层次来看,数据库查询生成工作流程看起来大致如下:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/379b5bb7478096141ceac41c90d68a29.png

LLM 数据库查询生成工作流程。图片由作者提供。

下面是步骤的分解:

  1. 自然语言查询:用户首先将一个自然语言查询,如“Show me the average billing amount by state”(显示按州平均的账单金额),输入到一个界面,如聊天机器人。

  2. 提示:用户的自然语言查询被注入到一个提示中,告诉大型语言模型(LLM)生成一个数据库查询来回答自然语言查询。提示需要包含有关数据库的信息,例如模式定义、列定义和示例查询。

  3. LLM:将提示提供给 LLM,它试图在数据库中生成并运行一个查询。

  4. 总结性回复:如果查询执行成功,LLM 将以可消化的文本摘要形式将结果返回给用户。

这种方法的主要问题是提示必须包含足够关于数据库的信息,以便生成准确的查询。这项任务具有挑战性,因为大多数现实世界的数据库都有复杂的设计。例如,事务数据库和数据仓库通常存储数百或数千个相互关联的表,而图数据库可以有数百种节点和关系类型。

由于这个原因,如果静态提示包含足够关于数据库和示例查询的信息,对于 LLM 来说,要持续生成准确的查询是有挑战性的,甚至可能是不可能的。即使我们可以在 LLM 的上下文窗口中放入完整的数据库描述,它可能仍然会因缺乏相关的查询示例而遇到困难。

为了看到这个不足之处,考虑 Neo4j 为医院系统数据集创建的模式:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/dd66ff90ffa18d2142fd3a3704ff8dbf.png

医院系统图数据库模式。图片由作者提供

每个节点和关系也可以有属性。例如,Visit 节点有以下属性:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/dc92369d196bc7d9770ffc06cfacc1e3.png

访问节点属性。图片由作者提供。

这个 Neo4j 数据库相对简单,有六个不同的节点和关系类型。尽管你仍然可以在这种模式上编写复杂的查询,但现实世界的数据库将更加复杂,需要更复杂的查询。考虑到模式信息和一些示例,我们可能期望 LLM 在生成此数据库的查询方面表现良好。

现在假设我们为生成 Cypher 查询的 LLM 创建以下提示:

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 (e.g. [c:COVERED_BY] instead of [:COVERED_BY]). 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.

Warning:
- Never return a review node without explicitly returning all of the properties
besides the embedding property
- Make sure to use IS NULL or IS NOT NULL when analyzing missing properties.
- 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.

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'

If you're filtering on a string, make sure to lowercase the property and filter
value.

A visit is considered open if its status is 'OPEN' and the discharge date is
missing.

Use state abbreviations instead of their full name. For example, you should
change "Texas" to "TX", "Colorado" to "CO", "North Carolina" to "NC", and so on.

The question is:
{question}

这个提示有两个参数 - schemaquestionschema 参数应包含图中所有节点和关系的定义,而 question 参数是用户的自然语言查询。正如你所看到的,这个提示中没有查询的示例。

使用这个提示,我们可以向聊天机器人询问有关医院的基本问题:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/7a8d6a7b29b9a5c076017904a2a8ea8c.png

向医院系统聊天机器人提出一个查询生成问题。图片由作者提供。

聊天机器人成功地使用上述提示生成了一个查询,以计算系统中医院的数量。现在,让我们看看当我们向聊天机器人提出一个稍微复杂一点的问题时会发生什么:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/8c598c13818fb5d24469a8b175674c69.png

向医院系统聊天机器人提出一个它无法回答的查询生成问题。图片由作者提供。

针对这个问题的回答,Cypher 生成链生成了并尝试执行以下 Cypher 查询:

MATCH (v:Visit)
WHERE v.status = 'closed' AND v.admission_type = 'emergency'
RETURN AVG(duration(between(date(v.admission_date), date(v.discharge_date)))) 
AS average_duration_days

这个查询有两个问题:

  1. LLM 忽略了提示中关于“访问状态是以下之一:‘OPEN’, ‘DISCHARGED’”的信息。相反,它试图将访问状态过滤为closed

  2. LLM 试图使用一个名为 between() 的函数,但这个函数在 Neo4j 中不存在。

这些结果非常令人担忧,这不是一个孤立例子。在没有 Cypher 示例的提示中,Cypher 生成链在许多场景中生成准确查询的机会为零:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/179f5b7a6af34c850e48727083b561f2.png

Cypher 生成链,以及随后的医院系统聊天机器人,努力为各种问题生成 Cypher 查询。图片由作者提供。

我们可以通过在提示中包含示例问题和相应的 Cypher 查询来提高 Cypher 链的查询生成准确性。以下是提示的更新版本:

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 (e.g. [c:COVERED_BY] instead of [:COVERED_BY]). 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.

Example queries for this schema:

# 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 lower(p.name) = 'cigna'
RETURN phy.name AS physician_name, SUM(c.billing_amount) AS total_billed
ORDER BY total_billed
LIMIT 1

# How many non-emergency patients in North Carolina have written reviews?
match (r:Review)<-[:WRITES]-(v:Visit)-[:AT]->(h:Hospital)
where lower(h.state_name) = 'nc' and lower(v.admission_type) <> 'emergency'
return count(*)

# 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 lower(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

Warning:
- Never return a review node without explicitly returning all of the properties
besides the embedding property
- Make sure to use IS NULL or IS NOT NULL when analyzing missing properties.
- 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.

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'

If you're filtering on a string, make sure to lowercase the property and filter
value.

A visit is considered open if its status is 'OPEN' and the discharge date is
missing.

Use state abbreviations instead of their full name. For example, you should
change "Texas" to "TX", "Colorado" to "CO", "North Carolina" to "NC", and so on.

The question is:
{question}

这个新的提示包括四个示例问题及其相应的 Cypher 语句。让我们看看这如何改善对先前问题的回答:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/500b0c3e618b1f170268dc7587f419ea.png

像之前一样向医院系统聊天机器人提问,但使用新的 Cypher 生成提示。图片由作者提供。

以及其他两个问题:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/cda6299cac9ce1e56659da6378abbc34.png

像之前一样向医院系统聊天机器人提问,但使用新的 Cypher 生成提示。图片由作者提供。

虽然更新提示中的示例有助于 Cypher 生成链回答更多问题,但它仍然难以回答问题。

我们可以继续向提示中添加更多示例问题,但固定提示查询生成有两个基本缺陷:

  1. 提示中包含的许多示例查询与用户给出的当前自然语言查询不相关。

  2. 如果我们继续向提示中添加示例查询,我们将承担更多成本,并最终超过 LLM 的上下文窗口。

如果我们希望 LLM 生成可靠的查询,我们需要更好的策略——这就是动态少量提示的用武之地。

动态少量提示

在数据库查询生成中,动态少量提示是一种技术,它通过更新提示以包含与用户当前自然语言查询相关的示例数据库查询。以下图表说明了查询生成中动态少量提示的一般工作流程:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/b1d8a1d28669ae35e064c01fd64db6c2.png

数据库查询生成的动态少量提示。图片由作者提供。

动态少量提示在原始查询生成工作流程之上增加了几个额外的步骤:

  1. 示例数据库查询:自然语言查询被嵌入并存储在向量索引中,与数据库查询一起作为元数据,使我们能够搜索与语义相关的自然语言查询并提取其对应的数据库查询。

  2. 向量索引:在推理时间,我们将用户的自然语言查询通过向量索引运行以提取语义相似的自然语言查询及其相应的数据库查询。理想情况下,这为我们提供了一组与回答当前自然语言查询相关的示例数据库查询。

  3. 提示:我们将自然语言查询和相关的示例数据库查询注入到提示中,创建一个随着用户的自然语言查询而变化的提示。

如果操作正确,动态少量提示克服了我们原始查询生成策略的限制。具体来说,我们不需要在提示中存储数百或数千个静态(可能不相关)的示例数据库查询。相反,示例将根据当前的自然语言查询进行更新。

动态少量提示:一种根据输入动态选择示例并将其格式化为模型最终提示的提示技术。

此外,这种策略使我们能够迭代地提高我们聊天机器人的查询生成准确性。如果聊天机器人对一个给定的自然语言查询生成了一个错误的数据库查询,而我们知道正确的数据库查询,我们可以将示例添加到我们的向量索引中。

为了更详细地了解其工作原理,让我们看看对医院系统聊天机器人进行代码更改以实现动态少量提示的情况。

实现

让我们回顾一下对医院系统聊天机器人主要代码的更新——完整的代码可在 GitHub 上找到。

默认情况下,LangChain 不支持 Cypher 查询生成的动态少量提示,因此我们必须修改用于创建 Text-To-Cypher 链的 [[GraphCypherQAChain](https://python.langchain.com/v0.2/docs/integrations/graphs/neo4j_cypher/)](https://python.langchain.com/v0.2/docs/integrations/graphs/neo4j_cypher/) 类。我们首先需要向 GraphCypherQAChain 添加一个检索器属性,该属性将检索要注入提示中的示例数据库查询:

# chatbot_api/src/langchain_custom/graph_qa/cypher.py

...

class GraphCypherQAChain(Chain):
    """Chain for question-answering against a graph by generating Cypher statements.

    *Security note*: Make sure that the database connection uses credentials
        that are narrowly-scoped to only include necessary permissions.
        Failure to do so may result in data corruption or loss, since the calling
        code may attempt commands that would result in deletion, mutation
        of data if appropriately prompted or reading sensitive data if such
        data is present in the database.
        The best way to guard against such negative outcomes is to (as appropriate)
        limit the permissions granted to the credentials used with this tool.

        See https://python.langchain.com/docs/security for more information.
    """

    graph: GraphStore = Field(exclude=True)
    cypher_generation_chain: Union[LLMChain, Runnable]
    qa_chain: Union[LLMChain, Runnable]
    graph_schema: str
    input_key: str = "query"  #: :meta private:
    output_key: str = "result"  #: :meta private:
    top_k: int = 10
    """Number of results to return from the query"""
    return_intermediate_steps: bool = False
    """Whether or not to return the intermediate steps along with the final answer."""
    return_direct: bool = False
    """Whether or not to return the result of querying the graph directly."""
    cypher_query_corrector: Optional[CypherQueryCorrector] = None
    """Optional cypher validation tool"""
    use_function_response: bool = False
    """Whether to wrap the database context as tool/function response"""
    cypher_example_retriever: Optional[VectorStoreRetriever] = None
    """Optional retriever to augment the prompt with example Cypher queries"""
    node_properties_to_exclude: Optional[list[str]] = None
    """Optional list of node properties to exclude from context in the QA prompt"""

...

在这里,我们添加了一个可选的 LangChain [VectorStoreRetriever](https://python.langchain.com/v0.1/docs/modules/data_connection/retrievers/)cypher_example_retriever 属性。我们将使用 cypher_example_retriever 将语义相关的 Cypher 查询注入到我们的提示中。

接下来,我们需要修改我们将用于实例化 GraphCypherQAChain.from_llm() 类方法:

# chatbot_api/src/langchain_custom/graph_qa/cypher.py

...

class GraphCypherQAChain(Chain):
    """Chain for question-answering against a graph by generating Cypher statements.

    *Security note*: Make sure that the database connection uses credentials
        that are narrowly-scoped to only include necessary permissions.
        Failure to do so may result in data corruption or loss, since the calling
        code may attempt commands that would result in deletion, mutation
        of data if appropriately prompted or reading sensitive data if such
        data is present in the database.
        The best way to guard against such negative outcomes is to (as appropriate)
        limit the permissions granted to the credentials used with this tool.

        See https://python.langchain.com/docs/security for more information.
    """

    ...

    cypher_example_retriever: Optional[VectorStoreRetriever] = None
    """Optional retriever to augment the prompt with example Cypher queries"""

    ...

    @classmethod
    def from_llm(
        cls,
        llm: Optional[BaseLanguageModel] = None,
        *,
        qa_prompt: Optional[BasePromptTemplate] = None,
        cypher_prompt: Optional[BasePromptTemplate] = None,
        cypher_llm: Optional[BaseLanguageModel] = None,
        cypher_example_retriever: Optional[VectorStoreRetriever] = None,
        qa_llm: Optional[Union[BaseLanguageModel, Any]] = None,
        exclude_types: List[str] = [],
        include_types: List[str] = [],
        validate_cypher: bool = False,
        qa_llm_kwargs: Optional[Dict[str, Any]] = None,
        cypher_llm_kwargs: Optional[Dict[str, Any]] = None,
        use_function_response: bool = False,
        function_response_system: str = FUNCTION_RESPONSE_SYSTEM,
        node_properties_to_exclude: Optional[list[str]] = None,
        **kwargs: Any,
    ) -> GraphCypherQAChain:
        """Initialize from LLM."""

        ...

        if cypher_example_retriever is not None:
            cypher_generation_chain = (
                {
                    "example_queries": itemgetter("question")
                    | cypher_example_retriever
                    | RunnableLambda(format_retrieved_documents),
                    "schema": itemgetter("schema"),
                    "question": itemgetter("question"),
                }
                | CYPHER_GENERATION_PROMPT_USE
                | cypher_llm
                | StrOutputParser()
            )
        else:
            cypher_generation_chain = LLMChain(
                llm=cypher_llm or llm,  # type: ignore[arg-type]
                **use_cypher_llm_kwargs,  # type: ignore[arg-type]
            )

        ...

        return cls(
            graph_schema=graph_schema,
            qa_chain=qa_chain,
            cypher_generation_chain=cypher_generation_chain,
            cypher_query_corrector=cypher_query_corrector,
            use_function_response=use_function_response,
            cypher_example_retriever=cypher_example_retriever,
            node_properties_to_exclude=node_properties_to_exclude,
            **kwargs,
        )

.from_llm() 的主要更改是我们根据用户指定的 cypher_example_retriever 属性创建不同的 cypher_generation_chain。这个链将 question 输入通过 cypher_example_retriever 传递,以从向量索引中提取相关的示例 Cypher 查询。我们使用名为 format_retrieved_documents() 的实用函数格式化示例查询,并将 schemaquestionexample_queries 传递到提示中。

如果创建 cypher_generation_chain 的代码看起来不熟悉,请查看 LangChain 关于 LangChain 表达式语言 (LCEL) 的文档。

这些是实施动态少量提示所需的对 GraphCypherQAChain 的主要更改。接下来,我们创建一个 GraphCypherQAChain 的实例,将其用作医院代理的工具:

# chatbot_api/src/chains/hospital_cypher_chain.py

import os
from langchain_community.graphs import Neo4jGraph
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain_community.vectorstores.neo4j_vector import Neo4jVector
from langchain_openai import OpenAIEmbeddings
from src.langchain_custom.graph_qa.cypher import GraphCypherQAChain

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

HOSPITAL_QA_MODEL = os.getenv("HOSPITAL_QA_MODEL")
HOSPITAL_CYPHER_MODEL = os.getenv("HOSPITAL_CYPHER_MODEL")
NEO4J_URI = os.getenv("NEO4J_URI")
NEO4J_USERNAME = os.getenv("NEO4J_USERNAME")
NEO4J_PASSWORD = os.getenv("NEO4J_Password")
NEO4J_CYPHER_EXAMPLES_INDEX_NAME = os.getenv("NEO4J_CYPHER_EXAMPLES_INDEX_NAME")
NEO4J_CYPHER_EXAMPLES_TEXT_NODE_PROPERTY = os.getenv(
    "NEO4J_CYPHER_EXAMPLES_TEXT_NODE_PROPERTY"
)
NEO4J_CYPHER_EXAMPLES_NODE_NAME = os.getenv("NEO4J_CYPHER_EXAMPLES_NODE_NAME")
NEO4J_CYPHER_EXAMPLES_METADATA_NAME = os.getenv("NEO4J_CYPHER_EXAMPLES_METADATA_NAME")

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

graph.refresh_schema()

cypher_example_index = Neo4jVector.from_existing_graph(
    embedding=OpenAIEmbeddings(),
    url=NEO4J_URI,
    username=NEO4J_USERNAME,
    password=NEO4J_PASSWORD,
    index_name=NEO4J_CYPHER_EXAMPLES_INDEX_NAME,
    node_label=NEO4J_CYPHER_EXAMPLES_TEXT_NODE_PROPERTY.capitalize(),
    text_node_properties=[
        NEO4J_CYPHER_EXAMPLES_TEXT_NODE_PROPERTY,
    ],
    text_node_property=NEO4J_CYPHER_EXAMPLES_TEXT_NODE_PROPERTY,
    embedding_node_property="embedding",
)

cypher_example_retriever = cypher_example_index.as_retriever(search_kwargs={"k": 8})

在导入依赖项和加载环境变量之后,我们实例化了一个Neo4jGraph对象,该对象连接到我们的 Neo4j 数据库。然后我们定义cypher_example_index – 一个连接到存储示例 Cypher 查询的向量索引的Neo4jVector对象。

下面是.from_existing_graph()的每个参数的描述:

  • embedding – 用于嵌入我们的示例自然语言查询的嵌入模型。我们将使用OpenAIEmbedding()

  • urlusernamepassword – 连接到 Neo4j 数据库所需的凭据。

  • index_name – 向量索引的名称。我们将我们的索引命名为questions

  • node_label – 分配给存储示例查询的节点的名称。我们将这些节点命名为问题

  • text_node_property—分配给自然语言查询/问题的节点属性名称。我们将使用问题

  • embedding_node_property – 存储嵌入问题的属性名称。

如果index_name在图中不存在,.from_existing_graph()将创建一个新的索引。否则,它将引用现有的索引。

然后,我们创建了一个检索器对象cypher_example_retriever,从cypher_example_index中创建,我们将使用它来搜索语义相关的问题。通过将search_kwargs={"k": 8}传递给.as_retriever(),我们实例化了一个检索器,它检索与输入最相似的 8 个问题。k参数可以调整以优化查询生成性能。

在此之后,我们定义了 Cypher 生成 LLM 使用的提示:

# chatbot_api/src/chains/hospital_cypher_chain.py

...

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 (e.g. [c:COVERED_BY] instead of [:COVERED_BY]). 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.

Example queries for this schema:
{example_queries}

Warning:
- Never return a review node without explicitly returning all of the properties
besides the embedding property
- Make sure to use IS NULL or IS NOT NULL when analyzing missing properties.
- 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.

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'

If you're filtering on a string, make sure to lowercase the property and filter
value.

A visit is considered open if its status is 'OPEN' and the discharge date is
missing.

Use state abbreviations instead of their full name. For example, you should
change "Texas" to "TX", "Colorado" to "CO", "North Carolina" to "NC", and so on.

The question is:
{question}
"""

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

与原始提示的唯一区别是,我们包含了一个参数example_queries,我们将使用它来存储从向量索引检索到的示例 Cypher 查询。我们还需要定义一个提示来总结 Cypher 查询结果作为对用户问题的答案,但在这里我们不涉及,因为它与原始链中的相同。

接下来,我们实例化 Cypher 生成链:

# chatbot_api/src/chains/hospital_cypher_chain.py

...

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

在这里,我们实例化了一个GraphCypherQAChain对象,并将cypher_example_retriever传递给它以检索语义相关的 Cypher 查询示例并将它们注入到提示中。

最后,我们将hospital_cypher_chain作为工具包含在最终的聊天机器人代理中。

# chatbot_api/src/chains/hospital_cypher_chain.py

import os
from typing import Any
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, tool
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents.format_scratchpad.openai_tools import (
    format_to_openai_tool_messages,
)
from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputParser
from src.chains.hospital_review_chain import reviews_vector_chain
from src.chains.hospital_cypher_chain import hospital_cypher_chain
from src.tools.wait_times import (
    get_current_wait_times,
    get_most_available_hospital,
)

HOSPITAL_AGENT_MODEL = os.getenv("HOSPITAL_AGENT_MODEL")

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

...

@tool
def explore_hospital_database(question: str) -> str:
    """
    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?".
    """

    return hospital_cypher_chain.invoke(question)

...

agent_tools = [
    explore_patient_experiences,
    explore_hospital_database,
    get_hospital_wait_time,
    find_most_available_hospital,
]

agent_prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            """
            You are a helpful chatbot designed to answer questions
            about patient experiences, patient data, hospitals,
            insurance payers, patient review statistics, hospital
            visit details, wait times, and availability for
            stakeholders in a hospital system.
            """,
        ),
        ("user", "{input}"),
        MessagesPlaceholder(variable_name="agent_scratchpad"),
    ]
)

agent_llm_with_tools = agent_chat_model.bind_tools(agent_tools)

hospital_rag_agent = (
    {
        "input": lambda x: x["input"],
        "agent_scratchpad": lambda x: format_to_openai_tool_messages(
            x["intermediate_steps"]
        ),
    }
    | agent_prompt
    | agent_llm_with_tools
    | OpenAIToolsAgentOutputParser()
)

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

我们的聊天机器人代理现在可以使用动态的少样本提示调用explore_hospital_database工具来回答需要 Cypher 查询的问题。

添加 Cypher 示例

要将 Cypher 示例添加到questions索引中,我们可以使用检索器的.add_texts()方法:

cypher_example_index = Neo4jVector.from_existing_graph(
    embedding=OpenAIEmbeddings(),
    url=NEO4J_URI,
    username=NEO4J_USERNAME,
    password=NEO4J_PASSWORD,
    index_name=NEO4J_CYPHER_EXAMPLES_INDEX_NAME,
    node_label=NEO4J_CYPHER_EXAMPLES_TEXT_NODE_PROPERTY.capitalize(),
    text_node_properties=[
        NEO4J_CYPHER_EXAMPLES_TEXT_NODE_PROPERTY,
    ],
    text_node_property=NEO4J_CYPHER_EXAMPLES_TEXT_NODE_PROPERTY,
    embedding_node_property="embedding",
)

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

node_id = cypher_example_index.add_texts(
  texts=[question],
  metadatas=[{"cypher": cypher_example}],
)

在这里,我们使用Neo4jVector.from_existing_graph()连接到我们的 Neo4j Cypher 示例索引。然后我们使用.add_texts()添加一个示例问题和相应的 Cypher 查询。texts参数指定了我们想要嵌入的属性——在这种情况下是用户的自然语言问题/查询。我们还需要将 Cypher 查询作为元数据存储,以便在搜索语义相似的问题时,我们可以提取它们对应的 Cypher 查询。

运行此操作后,我们应该在 Neo4j 数据库中看到一个新的 Question 节点:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/9c691de298680924277d59eb1920e67b.png

Neo4j 数据库中的问题节点。图片由作者提供。

这个 Question 节点具有 idquestioncypherembedding 属性。embedding 属性是 question 的向量版本,这是我们搜索与用户输入查询相关的相关问题时所使用的。

现在我们已经构建了一个利用动态少量提示的 Cypher 生成链,我们需要一种简单的方法来提供示例——这就是我们接下来要讨论的内容。

自助服务门户

现在我们已经构建了一个利用动态少量提示的 Cypher 生成链,我们需要一种简单的方法来提供示例。当然,我们可以加载一个预定义的示例查询数据集,但我们还希望构建一个系统,允许我们在不运行或更改任何代码的情况下,随时纠正查询生成错误。我们可以通过自助门户来实现这一点。

langchain_neo4j_rag_app/cypher_example_portal at main · hfhoffman1144/langchain_neo4j_rag_app

自助服务门户是一个 UI,允许我们上传 Cypher 查询到向量索引。为了了解这可能如何工作,让我们向聊天机器人提问一个它目前无法回答的问题:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/05f6516863dff25693e4b6ea46bbe332.png

向医院系统聊天机器人提问它无法回答的问题。图片由作者提供。

如果我们知道如何回答这个问题,我们可以使用自助服务门户来更新 Cypher 示例向量索引:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/550a9ef254039e052e8c3ed396ae97bb.png

Cypher 示例自助服务门户。图片由作者提供。

在这个第一个输入框中,我们输入聊天机器人无法生成正确 Cypher 查询的问题。为了表明 LLM 并不仅仅是复制示例查询,我们稍微改变了问题的内容。

在第二个输入框中,我们输入帮助 LLM 回答这个问题的 Cypher 查询。然后,我们按下 验证 按钮以执行输入检查:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/0733deac18d2363f16075d4ca4175044.png

Cypher 示例自助门户输入检查。图片由作者提供。

第一个输入检查通知我们示例问题不存在于向量索引中,因此我们不会添加重复项。然后,自助服务门户执行查询以确保其有效性。最后,门户显示与我们要上传的示例问题具有最高语义相似性的现有示例问题。这允许我们验证我们不会上传与现有问题具有相同语义意义的示例问题。

在我们按下 上传 按钮后,示例问题被添加到索引中:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/91f5535d483da692ad9b008fa9366fe0.png

Cypher 示例自助门户提交的问题。图片由作者提供。

现在,我们可以回到聊天机器人,询问它之前无法回答的相同问题:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/eea6df5764c87446520f88e7fcaa786f.png

这家医院系统聊天机器人成功回答一个问题,归功于动态的少样本提示。图片由作者提供。

太棒了!Cypher 示例检索器成功提取并注入了我们的新示例到提示中,允许 Cypher 生成链生成正确的查询。我们还可以询问示例问题的微小变化,以查看聊天机器人的响应:

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/6ded1fefa4a030b56698b15f504fceed.png

这家医院系统聊天机器人成功回答问题,归功于动态的少样本提示。图片由作者提供。

通过向索引中添加一个示例,我们已经使聊天机器人能够正确地回答一类新的问题。想象一下,如果我们添加更多示例,我们能够提高多少性能!

考虑事项和结束语

在本教程中,我们通过为 Cypher 查询生成实现动态的少样本提示,改进了医院系统聊天机器人。这一改进允许聊天机器人通过根据用户的当前自然语言查询动态选择和注入示例查询到提示中,从而生成更准确和相关的 Cypher 查询。

关键考虑事项:

  1. 上下文窗口限制:虽然动态的少样本提示通过仅包括相关示例有助于缓解上下文窗口限制的问题,但仍然非常重要的是监控和管理提示的大小,以确保它保持在 LLM 的上下文窗口内。

  2. 示例质量:查询生成的准确性严重依赖于存储在向量索引中的示例查询的质量和相关性。持续地整理和更新示例集对于维持和改进性能至关重要。

  3. 安全性:确保聊天机器人使用的数据库连接具有狭窄范围的凭证,以防止任何未经授权的数据访问或修改。实施强大的安全措施对于保护敏感数据至关重要。

  4. 用户反馈循环:实施自助门户允许用户提供反馈并纠正查询生成中的错误,创建一个持续改进的循环,随着时间的推移增强聊天机器人的能力。

  5. 性能监控:实施监控工具以跟踪查询生成过程的表现和准确性,有助于识别改进领域并确保聊天机器人满足用户期望。

通过实现动态的少样本提示并持续迭代系统,我们可以构建一个更准确、更可靠且适用于生产的医院系统聊天机器人。这种方法不仅提升了聊天机器人的当前性能,还提供了一个持续改进和可扩展性的框架。

参考文献

  1. 使用 LangChain 构建 LLM RAG 聊天机器人 – realpython.com/build-llm-rag-chatbot-with-langchain/
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐