基础图谱增强检索生成(GraphRAG)——从自然语言问题生成 Cypher 查询

131 阅读13分钟

本章内容包括:

  • 查询语言生成基础
  • 查询语言生成在RAG流水线中的位置
  • 查询语言生成的实用方法
  • 使用基础模型实现text2cypher检索器
  • 针对text2cypher的专用(微调)大型语言模型

前几章我们已经涵盖了很多内容。我们学习了如何构建知识图谱、从文本中提取信息以及如何利用这些信息回答问题。我们还探讨了如何通过硬编码的Cypher查询来扩展和改进纯向量检索,从而为大型语言模型(LLM)提供更相关的上下文。本章将更进一步,学习如何从自然语言问题生成Cypher查询。这将使我们能够构建更灵活、更动态的检索系统,适应不同类型的问题和知识图谱。

注意:本章实现使用了我们称为“电影数据集”(Movies dataset)的数据集。有关该数据集及其多种加载方式的详细信息,请参见附录。

4.1 查询语言生成基础

所谓查询语言生成基础,是指将自然语言问题转换为可在数据库上执行的查询语言的过程。更具体地说,我们关注的是如何从自然语言问题生成Cypher查询。大多数大型语言模型了解Cypher是什么,并掌握其基本语法。本过程的主要挑战是生成既正确又与提问相关的查询,这需要理解问题的语义以及被查询知识图谱的模式(schema)。

如果不提供知识图谱的模式信息,LLM只能根据经验猜测节点、关系和属性的名称。提供模式后,它成为用户问题语义与图模型之间的映射,明确节点使用的标签、存在的关系类型、可用的属性以及节点所连接的关系类型。

从自然语言问题生成Cypher查询的工作流程可拆解为以下步骤(见图4.1):

  1. 获取用户提问。
  2. 获取知识图谱的模式信息。
  3. 定义其他有用信息,如术语映射、格式说明和少样本示例。
  4. 生成传递给LLM的提示(prompt)。
  5. 将提示传给LLM,生成Cypher查询。

image.png

4.2 查询语言生成在RAG流水线中的位置

在前面的章节中,我们了解了如何通过对知识图谱中无结构部分进行向量相似度搜索,获得相关回答。我们也看到,可以结合硬编码的Cypher查询来扩展向量相似度搜索,从而为大型语言模型(LLM)提供更相关的上下文。这些技术的一个限制是它们只能回答某些类型的问题。

例如,用户提出的问题是:“列出由斯蒂文·斯皮尔伯格执导的评分最高的前三部电影及其平均分。”这类问题无法仅靠向量相似度搜索回答,因为它需要对数据库执行特定类型的查询,而对应的Cypher查询可能如下(假设有合理的模式):

代码示例4.1 Cypher查询

MATCH (:Reviewer)-[r:REVIEWED]->(m:Movie)<-[:DIRECTED]-(:Director {name: 'Steven Spielberg'})
RETURN m.title, AVG(r.score) AS avg_rating
ORDER BY avg_rating DESC
LIMIT 3

该查询的重点不是找图中最相似的节点,而是以特定方式聚合数据。这说明我们希望针对某些查询类型使用自动生成的Cypher——当我们寻找的不仅仅是图中最相似节点,或者需要进行某种数据聚合时,生成的Cypher查询就派上用场。

下一章中,我们将探讨如何构建一个具备代理能力的系统,支持多个检索器,根据用户问题选择最合适的检索器,从而为用户提供最佳回答。

此外,text2cypher还可以作为一种“万能”检索器,用于处理系统中其他检索器难以匹配的查询类型。

4.3 查询语言生成的实用方法

在从自然语言生成Cypher查询时,有几点需要注意,以确保生成的查询既正确又相关。尤其当输入问题复杂或含糊,或者数据库模式元素命名不具语义时,LLM容易出现错误。

4.3.1 利用少样本示例进行上下文学习

少样本示例是提升LLM text2cypher表现的有效方法。即我们向LLM提供几个问题及其对应的Cypher查询示例,LLM便能学习生成类似的新查询。相比之下,零样本提示则完全不提供示例,LLM须凭自身能力生成查询。

这些少样本示例需针对具体知识图谱手动编写,特别适用于发现LLM误解模式或反复犯同类错误(如误将遍历当作属性等)时。

例如,若发现LLM试图直接读取电影节点的“国家”属性,而实际上“国家”是图中的一个节点,可在提示中加入如下少样本示例,引导LLM正确查询:

问题:“电影《The Matrix》是在哪个国家制作的?”
错误示例:MATCH (m:Movie {title: 'The Matrix'}) RETURN m.country
正确示例:

MATCH (m:Movie {title: 'The Matrix'})-[:PRODUCED_IN]->(c:Country) RETURN c.name

再加上类似示例:
问题:“电影《Ready Player One》是在哪个国家制作的?”
Cypher:

MATCH (m:Movie {title: 'Ready Player One'})-[:PRODUCED_IN]->(c:Country) RETURN c.name

有了这些示例,LLM能学习到获取国家名称的正确模式,从而纠正此类查询错误。

4.3.2 在提示中使用数据库模式展示知识图谱结构

知识图谱的模式对生成正确的Cypher查询至关重要。向LLM描述图谱模式有多种方式,Neo4j内部研究表明,具体格式并不关键。

模式应作为提示的一部分,明确说明图中有哪些标签、关系类型及属性。例如:

图数据库模式说明:  
仅使用模式中提供的关系类型和属性,不得使用未提供的类型和属性。

节点标签及属性示例:  
LabelA {property_a: STRING}

关系类型及属性示例:  
REL_TYPE {rel_prop: STRING}

关系示例:  
(:LabelA)-[:REL_TYPE]->(:LabelB)  
(:LabelA)-[:REL_TYPE]->(:LabelC)

是否展示完整知识图谱取决于模式大小及业务需求。自动从Neo4j推断模式可能开销较大,通常会对数据库进行采样并据此推断。

推断模式需使用Neo4j的APOC库中的过程,该库在Neo4j SaaS产品Aura及其他Neo4j发行版中均免费提供。以下代码展示了如何从Neo4j推断模式。

提示:更多APOC信息见:neo4j.com/docs/apoc/

代码示例4.2 从Neo4j推断模式

NODE_PROPERTIES_QUERY = """
CALL apoc.meta.data()
YIELD label, other, elementType, type, property
WHERE NOT type = "RELATIONSHIP" AND elementType = "node"
WITH label AS nodeLabels, collect({property:property, type:type}) AS properties
RETURN {labels: nodeLabels, properties: properties} AS output
"""

REL_PROPERTIES_QUERY = """
CALL apoc.meta.data()
YIELD label, other, elementType, type, property
WHERE NOT type = "RELATIONSHIP" AND elementType = "relationship"
WITH label AS relType, collect({property:property, type:type}) AS properties
RETURN {type: relType, properties: properties} AS output
"""

REL_QUERY = """
CALL apoc.meta.data()
YIELD label, other, elementType, type, property
WHERE type = "RELATIONSHIP" AND elementType = "node"
UNWIND other AS other_node
RETURN {start: label, type: property, end: toString(other_node)} AS output
"""

利用这些查询,我们可以获取图数据库的模式,并在提示中使用。运行查询后,将结果以结构化形式存储,便于后续生成模式字符串。

代码示例4.3 执行模式推断查询

def get_structured_schema(driver: neo4j.Driver) -> dict[str, Any]:
    node_labels_response = driver.execute_query(NODE_PROPERTIES_QUERY)
    node_properties = [
        data["output"]
        for data in [r.data() for r in node_labels_response.records]
    ]

    rel_properties_query_response = driver.execute_query(REL_PROPERTIES_QUERY)
    rel_properties = [
        data["output"]
        for data in [r.data() for r in rel_properties_query_response.records]
    ]

    rel_query_response = driver.execute_query(REL_QUERY)
    relationships = [
        data["output"]
        for data in [r.data() for r in rel_query_response.records]
    ]

    return {
        "node_props": {el["labels"]: el["properties"] for el in node_properties},
        "rel_props": {el["type"]: el["properties"] for el in rel_properties},
        "relationships": relationships,
    }

得到结构化响应后,可按需格式化模式字符串,方便在提示中使用和尝试不同格式。

代码示例4.4 格式化模式字符串

def get_schema(structured_schema: dict[str, Any]) -> str:
    def _format_props(props: list[dict[str, Any]]) -> str:
        return ", ".join([f"{prop['property']}: {prop['type']}" for prop in props])

    formatted_node_props = [
        f"{label} {{{_format_props(props)}}}"
        for label, props in structured_schema["node_props"].items()
    ]

    formatted_rel_props = [
        f"{rel_type} {{{_format_props(props)}}}"
        for rel_type, props in structured_schema["rel_props"].items()
    ]

    formatted_rels = [
        f"(:{element['start']})-[:{element['type']}]->(:{element['end']})"
        for element in structured_schema["relationships"]
    ]

    return "\n".join(
        [
            "Node labels and properties:",
            "\n".join(formatted_node_props),
            "Relationship types and properties:",
            "\n".join(formatted_rel_props),
            "The relationships:",
            "\n".join(formatted_rels),
        ]
    )

通过此函数,我们即可生成用于提示LLM的模式字符串。

4.3.3 添加术语映射,实现用户问题与模式的语义对应

LLM需要知道如何将用户问题中使用的术语映射到知识图谱模式中的术语。设计良好的图模式通常使用名词和动词作为标签和关系类型,使用形容词和名词作为属性。但即便如此,LLM有时仍会混淆不同术语的用法。

注意:这些映射是针对特定知识图谱的,应作为提示的一部分,不同知识图谱之间难以复用。

术语映射通常会随着您发现生成查询中因LLM未正确理解模式而导致的问题而逐步完善。

术语映射示例:

  • Persons(人物):当用户询问某人的职业时,指的是标签为Person的节点。
  • Movies(电影):当用户询问电影或影片时,指的是标签为Movie的节点。

4.3.4 格式指令

不同LLM对输出格式的处理方式不一。有的会在Cypher查询周围加代码标签,有的不会;有的会在查询前添加文本,有的则没有,等等。

为了让所有模型输出格式统一,可以在提示中添加格式指令。常用的指令是让LLM仅输出Cypher查询,不包含其他内容。

格式指令示例:

  • 不要在回答中包含任何解释或道歉。
  • 不要回答任何除了构造Cypher语句以外的问题。
  • 除生成的Cypher语句外,不要包含任何文本。
  • 仅返回Cypher语句,不要使用代码块。

4.4 使用基础模型实现 text2cypher 生成器

现在让我们把前面讲的内容付诸实践,使用基础模型实现一个 text2cypher 生成器。这里的任务本质上是构造一个包含知识图谱模式、术语映射、格式指令和少样本示例的提示(prompt),以便清晰地向大型语言模型(LLM)表达我们的意图。

本章后续内容将用 Neo4j Python 驱动和 OpenAI API 实现 text2cypher 生成器。跟进本章需要一个运行中的空白 Neo4j 实例,可以是本地安装或云托管,确保实例为空。相关实现可参见配套的 Jupyter 笔记本:github.com/tomasonjo/k…

让我们开始吧。

代码示例4.5 提示模板

prompt_template = """
Instructions:
Generate Cypher statement to query a graph database to get the data to answer the following user question.

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

Terminology mapping:
This section is helpful to map terminology between the user question and the graph database schema.
{terminology}
Examples:
The following examples provide useful patterns for querying the graph database.
{examples}

Format instructions:
Do not include any explanations or apologies in your responses.
Do not respond to any questions that might ask anything else than for you to
construct a Cypher statement.
Do not include any text except the generated Cypher statement.
ONLY RESPOND WITH CYPHER—NO CODE BLOCKS.

User question: {question}
"""

利用此提示模板,我们可以生成传递给LLM的完整提示。假设我们有如下用户问题、模式、术语映射和少样本示例。

代码示例4.6 完整提示示例

question = "Who directed the most movies?"

schema_string = get_schema(neo4j_driver)

terminology_string = """
Persons: When a user asks about a person by trade like actor, writer, director, producer,  or reviewer, they are referring to a node with the label 'Person'.
Movies: When a user asks about a film or movie, they are referring to a node with the label Movie.
"""

examples = [["Who are the two people acted in most movies together?", "MATCH (p1:Person)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person) WHERE p1 <> p2 RETURN p1.name, p2.name, COUNT(m) AS movieCount ORDER BY movieCount DESC LIMIT 1"]]

full_prompt = prompt_template.format(
    question=question,
    schema=schema_string,
    terminology=terminology_string,
    examples="\n".join([f"Question: {e[0]}\nCypher: {e[1]}" for i, e in enumerate(examples)])
)
print(full_prompt)

执行此示例,生成的提示内容类似如下:

Instructions:
Generate Cypher statement to query a graph database to get the data to answer the following user question.

Graph database schema:
Use only the provided relationship types and properties in the schema.
Do not use any other relationship types or properties that are not provided in the schema.
Node properties:
Movie {tagline: STRING, title: STRING, released: INTEGER}
Person {born: INTEGER, name: STRING}

Relationship properties:
ACTED_IN {roles: LIST}
REVIEWED {summary: STRING, rating: INTEGER}

The relationships:
(:Person)-[:ACTED_IN]->(:Movie)
(:Person)-[:DIRECTED]->(:Movie)
(:Person)-[:PRODUCED]->(:Movie)
(:Person)-[:WROTE]->(:Movie)
(:Person)-[:FOLLOWS]->(:Person)
(:Person)-[:REVIEWED]->(:Movie)

Terminology mapping:
This section is helpful to map terminology between the user question and the graph database schema.
Persons: When a user asks about a person by trade like actor, writer, director, producer, or reviewer, they are referring to a node with the label 'Person'.
Movies: When a user asks about a film or movie, they are referring to a node with the label Movie.

Examples:
The following examples provide useful patterns for querying the graph database.
Question: Who are the two people who have acted in the most movies together?
Cypher: MATCH (p1:Person)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person) WHERE p1 <> p2 RETURN p1.name, p2.name, COUNT(m) AS movieCount ORDER BY movieCount DESC LIMIT 1

Format instructions:
Do not include any explanations or apologies in your responses.
Do not respond to any questions that might ask anything else than for you to construct a Cypher statement.
Do not include any text except the generated Cypher statement.
ONLY RESPOND WITH CYPHER—NO CODE BLOCKS.

User question: Who has directed the most movies?

有了这个提示,我们即可让LLM生成对应用户问题的Cypher查询。您可以复制此提示到LLM中试验其生成效果。

代码示例4.7 生成的Cypher查询

MATCH (p:Person)-[:DIRECTED]->(m:Movie)
RETURN p.name, COUNT(m) AS movieCount
ORDER BY movieCount DESC
LIMIT 1

4.5 针对 text2cypher 的专用(微调)大型语言模型

在 Neo4j,我们持续致力于通过微调提升 text2cypher 任务中大型语言模型(LLM)的性能。我们在 Hugging Face 上公开了训练数据集,地址是 huggingface.co/datasets/ne…。同时,我们基于开源LLM(如]() Gemma2、Llama 3.1)提供了微调模型,地址是 huggingface.co/neo4j

这些模型的性能虽然仍远逊于最新的 GPT 和 Gemini 等大型微调模型,但效率更高,适合在大型模型运行过慢的生产环境中使用。欢迎尝试这些模型,并结合少样本示例、模式、术语映射和格式指令来提升模型表现。关于我们的微调流程和经验,可以参考 mng.bz/MwDWmng.bz/a9v7mng.bz/yNWB

4.6 我们的收获与 text2cypher 的能力

通过本章的代码和内容,您应能为自己的知识图谱实现一个 text2cypher 检索器,生成针对多种问题的正确 Cypher 查询,并通过提供少样本示例、模式、术语映射和格式指令来提升性能。

当发现模型对某些类型问题表现不佳时,可以向提示中添加更多少样本示例,帮助模型学习如何生成正确查询。随着时间推移,您会注意到生成查询的质量不断提高,检索器也变得更可靠。

总结:

  • 查询语言生成与RAG流水线契合良好,是其他检索方法的重要补充,尤其适用于需要聚合数据或从图中获取特定信息的场景。
  • 查询语言生成的实用做法包括使用少样本示例、模式、术语映射和格式指令。
  • 我们可以用基础模型实现 text2cypher 检索器,并合理构造给LLM的提示。
  • 也可以使用专用(微调)LLM来执行 text2cypher,并提升其性能。