图检索增强生成-GraphRAG-精要-全-
图检索增强生成(GraphRAG)精要(全)
原文:
zh.annas-archive.org/md5/2c8245e0e83797ed40746ad357b9e3f5译者:飞龙
第一章:提高 LLM 的准确性
本章涵盖
-
大型语言模型
-
大型语言模型的局限性
-
持续微调模型的不足
-
检索增强生成
-
结合结构化和非结构化数据以支持所有类型的问题
大型语言模型(LLMs)在多个领域展现出了令人印象深刻的能力,但它们存在显著的局限性,影响了它们的实用性,尤其是在需要生成准确和最新信息的情况下。解决这些局限性的一个广泛采用的方法是检索增强生成(RAG),这是一种将 LLM 与外部知识库相结合的工作流程,以提供准确和最新的响应。通过在运行时从可信来源提取数据,RAG 可以显著减少,尽管不能完全消除,幻觉,这是 LLMs 最持久的挑战之一。此外,RAG 允许系统无缝地将通用知识与模型预训练数据中可能没有得到良好表示的特定领域信息相结合。尽管有这些优势,RAG 的实现往往只关注非结构化数据,而忽略了如知识图谱这样的结构化来源的潜力。
知识图谱是实体、它们的属性及其关系的结构化表示,提供了一个语义框架,将结构化和非结构化数据连接起来。例如,客户支持记录是不结构化的文本,而产品目录或用户数据库是结构化的。连接它们意味着使系统能够将关于“我最近的笔记本电脑订单”的对话提及与精确型号、购买日期和保修状态的记录相连接。知识图谱通过实现准确、丰富上下文和互联的信息检索——例如,将关于药物相互作用的客户查询实时链接到结构化的医疗指南、先前案例研究和患者的病史——成为 RAG 的一个关键组件。将知识图谱集成到 RAG 管道中可以克服 LLM 的局限性,增强数据检索,并促进在医疗保健、金融和技术支持等领域的跨领域数据类型管理和使用的整体方法。
本书面向希望构建更稳健、可解释和强大的 RAG 系统的开发者、研究人员和数据从业者。您将学习如何通过知识图谱增强现有的 RAG 架构,以及如何从头开始构建新的 GraphRAG 管道。在这个过程中,您将在数据建模、图构建、检索工作流程和系统评估方面获得实际技能。
在阅读完本书之后,您将清楚地了解 LLMs、RAG 和知识图谱如何交叉,以创建能够解决复杂查询并交付准确、可靠和可解释结果的稳健系统。
1.1 LLMs 简介
到现在为止,你可能已经遇到或听说过 ChatGPT,这是对话式 AI 中最突出的例子之一。ChatGPT 是由 OpenAI 开发的一个对话式用户界面,由 LLM(如 GPT-4,OpenAI et al.,2024)提供支持。LLM 建立在 Transformer 架构(Vaswani et al.,2017)之上,这使得它们能够高效地处理和生成文本。这些模型在大量文本数据上训练,使它们能够学习模式、语法、上下文,甚至一定程度上的推理。训练过程涉及向模型提供包含各种文本的大型数据集,主要目标是使模型能够准确预测序列中的下一个单词。这种广泛的接触使模型能够根据从数据中学到的模式理解和生成类似人类的文本。例如,如果你将“Never gonna”作为输入给 LLM,你可能会得到类似于图 1.1 所示的响应。
图 1.1 展示了一个 LLM 处理输入“Never gonna”并生成输出“give you up”的过程。这突显了 LLM 如何依赖于在训练期间学习的模式和相关联,例如从常见的文化参考中得出的,包括流行音乐。这些响应的质量和相关性在很大程度上取决于训练数据集的多样性和深度,这决定了 LLM 识别和复制此类模式的能力。

图 1.1 LLM 被训练来预测下一个单词。
虽然 LLM 在生成上下文相关的文本方面表现出色,但它们远不止是自动完成系统。它们遵循指令和适应广泛任务的能力令人印象深刻。例如,如图 1.2 所示,你可以要求 ChatGPT 以特定风格就特定主题生成俳句。这种能力不仅体现了模式识别,还体现了对特定任务指令的理解,能够产生超越简单文本预测的创造性和细微的输出。

图 1.2 使用 ChatGPT 写俳句
LLM 遵循指令和生成多样化、复杂输出的能力,无论是创作俳句还是提供结构化响应,都超越了简单地预测序列中的下一个单词。这种理解和执行详细指令的能力使 LLM 非常适合广泛的任务。在这本书中,你将利用这种遵循指令的能力来设计和优化 RAG 管道。通过利用遵循指令的能力,你可以更有效地集成检索组件,针对特定上下文定制响应,并优化你的系统以提高准确性和可用性。
ChatGPT 的通用知识范围同样引人注目。例如,图 1.3 展示了当被提示关于第一次载人登月时 ChatGPT 的响应。

图 1.3 从 ChatGPT 检索事实信息
如果你用来自 NASA 或维基百科的外部信息来验证这个回答,你可以观察到模型产生了一个准确无误的回答,没有错误信息。这样的回答可能会让你觉得 LLM 构建了一个庞大的事实数据库,可以从其中检索信息。然而,模型并不存储其训练数据集中的具体事实、事件或信息。相反,它发展了其训练语言复杂数学表示。记住,LLMs 基于 Transformer,这是一种基于神经网络的深度学习架构,用于预测下一个单词,如图 1.4 所示。
图 1.4 展示了神经网络预测序列中的下一个单词,这与 LLMs 的工作方式相似。中心部分显示了具有多层神经元的网络,通过代表信息流的线条连接。每个连接都有一个权重,例如示例值 0.04,它影响连接的强度。在训练过程中,模型学习这些权重的值以提高其预测能力。当被问及一个具体的历史事件时,LLM 不会从其训练数据中回忆起该事件。相反,它根据其神经网络中学习的权重生成一个回答,类似于预测序列中的下一个单词。因此,虽然 LLMs 可以提供看似知识渊博的答案,但它们的回答是基于这些学习的权重,而不是明确的记忆。正如 Andrej Karpathy 所说:“我们多少理解了它们(LLMs)构建并维护某种知识数据库,但即使这个知识库也非常奇怪、不完美且奇怪”(www.youtube.com/watch?v=zjkBMFhNj_gat12:40)。

图 1.4 基于输入序列单词预测下一个单词的神经网络
1.2 LLMs 的限制
LLMs 代表了人工智能进化中的一个里程碑,在众多应用领域提供了令人瞩目的能力。然而,正如任何变革性技术一样,它们也面临着挑战和限制。在接下来的部分,我们将深入探讨一些这些限制及其影响。
1.2.1 知识截止问题
最明显的限制是 LLMs 对其训练数据集中未包含的事件或信息一无所知。目前,ChatGPT 了解的信息截止到 2023 年 10 月。例如,如果你问 ChatGPT 关于 2024 年发生的事件,你会得到类似于图 1.5 所示的回答。
在大型语言模型的背景下,知识截止日期指的是模型训练数据中包含信息的最新点。模型可以访问包含有关截止日期之前事件信息的广泛文本数据,这些数据来自不同的来源,它利用这些数据生成回复和提供信息。任何在截止日期之后发生或发表的事情,由于它没有被包含在训练数据集中,因此模型不知道,因此它不能提供关于截止日期之后发生的事件、发展或研究的信息。

图 1.5 知识截止日期声明示例
1.2.2 过时信息
另一个不那么明显的限制是,大型语言模型有时会提供过时的回复。尽管它们可以提供详细和准确的信息,直到它们的截止日期,但它们可能不会反映最近的发展。例如,截至 2023 年底,马克·库班将其在达拉斯小牛队股份中的多数股权出售给阿德尔森家族和杜蒙特家族,同时保留少数股权。这一重大更新突显了过去正确的信息如何变得过时。例如,在关于达拉斯小牛队的查询中,图 1.6 中显示的回复反映了库班是唯一的所有者,这已经不再准确(Rader,2023)。

图 1.6 有时 ChatGPT 会回复过时的信息。
这突显了定期更新模型训练数据或使它们能够访问实时信息的重要性。随着事件和事实的不断演变,即使是所有权结构这样的小细节也可能对我们感知一个组织或个人产生重大影响。这种限制强调了确保人工智能系统在动态环境中保持准确和相关的必要性。
1.2.3 纯幻觉
另一个大型语言模型广为人知的限制是,它们倾向于提供自信、肯定的答案——即使这些答案包含错误或编造的信息。人们可能会假设,尽管它们的截止日期,这些模型提供的数据直到那个点都是准确的。然而,甚至关于截止日期之前发生的事件的信息也可能不可靠。
这种情况的一个显著例子发生在美国律师提交了由 ChatGPT 生成的虚假、虚构的法律引证到法庭,而他们并不知道这些引证是由 ChatGPT 生成的(Neumeister,2023)。这类自信的不准确通常被称为幻觉,即模型输出看似合理但实际上是错误或完全编造的信息。外部引用,如 URL、学术引用或 WikiData IDs 这样的标识符,特别容易产生这种行为。
幻觉发生是因为 LLMs(大型语言模型)不是推理引擎。它们是基于其训练数据中的模式训练出来的概率语言模型,预测出听起来像是一个好的下一个标记。它们不知道事实,就像人类那样。相反,它们通过猜测最可能的后续内容来生成文本,无论其是否真实。这种统计模式匹配与实际理解之间的基本差异是 LLMs 与人类认知的区别。
为了说明这一点,我们可以要求 ChatGPT 提供达拉斯小牛 NBA 球队的 WikiData ID。如图 1.7 所示,模型自信地返回了一个标识符——但它是不正确的。

图 1.7 ChatGPT 可以生成包含错误信息的回复。
模型断然回复了一个遵循 WikiData 格式的 ID。然而,如果你验证这个信息,你会观察到 Q152232 是电影《Womanlight》的 WikiData ID(www.wikidata.org/wiki/Q152232)。因此,用户必须认识到,尽管 LLMs 通常很有信息量,但它们并非完美无缺,可能会产生错误信息。批判性地对待它们的回复,并通过可靠的外部来源验证其准确性至关重要,特别是在精确性和事实正确性至关重要的环境中。
1.2.4 缺乏私人信息
如果你正在使用 LLM 构建一个公司聊天机器人,你可能会希望它能够回答涉及内部或专有信息的问题,这些信息不是公开可用的。在这种情况下,即使信息或事件发生在 LLM 的知识截止日期之前,它们也不会成为其训练数据的一部分。因此,该模型无法为这类查询生成准确的回复,如图 1.8 所示。

图 1.8 ChatGPT 在训练期间没有访问某些私人或机密信息。
一种可能的解决方案是将公司的内部信息公开,希望它被包含在 LLM 的训练数据集中。然而,这种方法既不实用也不安全。相反,我们将探索并展示更有效的策略来克服这些限制,同时保持数据隐私和控制。
关于 LLMs 其他局限性的说明
虽然这本书将重点关注 LLMs 在提供事实正确和最新信息方面的局限性,但承认 LLMs 也有其他限制是很重要的。其中一些包括
-
回复中的偏见——LLMs 有时会生成带有偏见的回复,反映了训练数据中存在的偏见。
-
缺乏理解和上下文——尽管 LLMs 非常复杂,但它们并不真正理解文本。它们根据从数据中学习到的模式处理语言,这意味着它们可能会错过细微差别和上下文细微之处。
-
易受提示注入攻击的影响—LLMs 容易受到提示注入攻击,恶意用户精心设计输入以操纵模型生成不适当、有偏见或有害的响应。这种漏洞对确保 LLM 应用在现实场景中的安全和完整性构成了重大挑战。
-
不一致的响应—大型语言模型(LLMs)在多次交互中可能会对同一问题给出不同的答案。这种不一致性源于它们的概率性质和缺乏持久记忆,这可能会阻碍它们在需要稳定性和可重复性的应用中的有用性。
本书致力于探索和解决 LLMs 在生成事实准确和最新响应方面的具体局限性。尽管我们认识到 LLMs 的其他局限性,但我们的讨论将不会涉及它们。
1.3 克服 LLMs 的局限性
LLMs 是强大的工具,但它们在处理特定领域的问题或访问专业、最新知识时往往面临局限性。在商业环境中实施类似 ChatGPT 的应用需要既精确又符合事实的输出。为了克服这些挑战,我们可以通过监督微调和 RAG 等方法将特定领域的知识注入 LLMs。在本节中,我们将探讨这些方法是如何工作的,以及如何将它们应用于将特定领域知识注入 LLMs。
1.3.1 监督微调
起初,我们中的许多人认为我们可以通过额外的训练来克服 LLMs 的局限性。例如,我们可以通过持续更新模型来克服知识截止日期的局限性。然而,为了有效地解决这个问题,我们首先需要更好地理解 LLMs 的训练。像 ChatGPT 这样的 LLMs 的训练可以分解为以下四个阶段,如 Andrew Karpathy 所述(www.youtube.com/watch?v=bZQun8Y4L2A):
-
预训练—模型阅读大量文本,通常超过万亿个标记,以学习基本语言模式。它练习预测句子中下一个单词。这是基础步骤,就像在学习写作之前先学习词汇和语法一样。这是最资源密集的阶段,可能需要数千个 GPU,并且可能需要数月的持续训练。
-
监督微调—模型被提供高质量对话的特定示例,以提高其像有帮助的助手一样响应的能力。它继续练习语言,但现在专注于生成有用和准确的响应。把它想象成从基本语言学习到练习对话技能的转变。这比预训练需要显著更少的资源,对于较小的 LLMs,现在甚至可以在单个笔记本电脑上运行。
-
奖励建模 — 模型通过比较对同一问题的不同答案来学习区分好与坏的回答。这就像有一个教练向模型展示良好表现的样子,以便它能努力复制这种质量。
-
强化学习 — 模型通过与用户或模拟环境互动,根据反馈进一步细化其回答。这类似于学习一项运动:不仅通过练习,还要通过实际比赛并从经验中学习。
由于预训练阶段成本高昂且耗时,因此不适合持续更新,因此想法是利用监督微调阶段来克服 LLM 的局限性。在监督微调阶段,你向语言模型提供特定输入提示的示例,以及你希望模型产生的相应期望输出。图 1.9 展示了这样一个示例。

图 1.9 监督微调数据集的样本记录
图 1.9 展示了一个可用于微调 LLM 的问答对示例。在这个例子中,输入提示或问题是关于哪个队伍赢得了 2023 年 NBA 总冠军,相应的答案是丹佛掘金队。理论上是,通过这个例子,LLM 会将其包含在其语言的数学表示中,并能够回答围绕 2023 年 NBA 冠军的问题。一些研究已经表明,监督微调可以提高 LLM 的真实性(Tian 等人,2023 年)。然而,其他使用不同方法的研究也表明,LLM 在通过微调学习新事实信息方面存在困难(Ovadia 等人,2023 年)。
虽然监督微调可以增强模型的整体知识,但它仍然是一个复杂且不断发展的研究领域。因此,在当前技术发展阶段,在生产环境中部署一个可靠、经过微调的语言模型面临着重大挑战。幸运的是,存在一种更高效、更简单的方法来解决 LLM 的知识局限性。
1.3.2 检索增强生成
提高 LLM 准确率并克服其局限性的第二种策略是 RAG 工作流程,该流程结合了 LLM 和外部知识库,以提供准确和最新的回答。而不是依赖于 LLM 的内部知识,相关事实或信息直接在输入提示中提供(Lewis 等人,2020 年)。这个概念(RAG)利用了 LLM 在理解和生成自然语言方面的优势,而事实信息则通过提示提供,从而减少了对外部知识库的依赖,并因此减少了幻觉。
RAG 工作流程分为两个主要阶段:
-
检索
-
增强生成
在检索阶段,相关信息从外部知识库或数据库中定位。在增强生成阶段,检索到的信息与用户的输入相结合,以增强提供给 LLM 的上下文,使其能够生成基于可靠、外部事实的响应。RAG 工作流程如图 1.10 所示。

图 1.10 将相关信息作为输入提供给 LLM
如前所述,LLM 擅长理解自然语言并遵循提示中的指令。在 RAG 工作流程中,目标转向任务导向的响应生成,其中 LLM 遵循一系列指令。该过程涉及使用检索工具从特定知识库中检索相关文档。然后,LLM 根据提供的文档生成答案,确保响应准确、上下文相关,并符合特定指南。这种系统方法将答案生成过程转化为一个目标任务,即检查和使用检索到的信息来生成最终答案。在输入提示中提供事实信息的示例如图 1.11 所示。
图 1.11 展示了 LLM 如何遵循 RAG 工作流程的提示指令的示例。提示强调了使用检索到的上下文确保准确和相关性响应的重要性,并且可以分解为
-
提供的内容 — 一个事实陈述,引入相关信息——在这种情况下,确定丹佛掘金队以 4:1 战胜迈阿密热火队成为 2023 年 NBA 冠军。这作为 LLM 的知识库输入。
-
用户查询 — 一个具体的问题,“谁赢得了 2023 年 NBA 冠军?”这个问题指导 LLM 从提供的内容中提取相关信息。
-
生成的答案 — LLM 的响应与检索到的上下文一致:“丹佛掘金队赢得了 2023 年 NBA 冠军。”

图 1.11 将相关信息作为提示的一部分提供
你可能会想知道,如果用户必须提供内容和问题,RAG 过程的优点是什么。在实践中,检索系统独立于用户操作。用户只需要提供问题,而检索过程在幕后进行,如图 1.12 所示。

图 1.12 将用户和知识库中的相关信息填充到提示模板中,然后传递给 LLM 生成最终答案
在 RAG 过程中,用户首先提出一个问题。在幕后,系统将这个问题转换为一个搜索查询,并从公司文档、知识文章或数据库等来源检索相关信息。高级检索算法找到最合适的内容,然后将这些内容与原始问题结合,形成一个丰富的提示。这个提示被发送到一个 LLM,LLM 根据问题和检索到的上下文生成响应。整个检索过程是自动的,除了用户原始的问题外,不需要额外的输入。这使得 RAG 既无缝又有效,提高了事实准确性,同时减少了幻觉答案的可能性。
RAG 方法因其简单性和高效性而获得了主流的认可。现在,它也成为了 ChatGPT 界面的一个部分,其中 LLM 可以在生成最终答案之前使用网络搜索来查找相关信息。ChatGPT 付费版本的用户可能对图 1.13 中描述的 RAG 过程比较熟悉。

图 1.13 ChatGPT 使用网络搜索来查找相关信息以生成一个最新的答案。
虽然 ChatGPT 中 RAG 的确切实现并未公开披露,但我们仍可以尝试推断其内部的工作原理。当 LLM 出于任何原因决定需要获取额外信息时,它可以向网络搜索输入一个查询。我们不清楚它如何精确地导航搜索结果,从网页中解析信息,或者决定已经检索到足够的信息。然而,我们知道它使用了2023 NBA championship winner关键词作为网络搜索的输入,并根据官方 NBA 网站(www.nba.com/playoffs/2023/the-finals)上的信息生成了最终响应。
1.4 知识图谱作为 RAG 应用的数据存储
在计划实施 RAG 应用时,选择合适的存储解决方案非常重要。虽然有许多数据库选项,但我们认为知识图谱和图数据库特别适合大多数 RAG 应用。知识图谱是一种使用节点表示概念和实体,以及使用关系连接这些节点的数据结构。一个示例知识图谱如图 1.14 所示。

图 1.14 知识图谱可以在单个数据库系统中存储复杂的有结构和无结构数据。
知识图谱非常灵活,能够存储结构化信息(例如员工详情、任务状态和公司层级)和非结构化信息(例如文章内容)。如图 1.14 所示,这种双重能力使它们特别适合复杂的 RAG 应用。结构化数据允许进行精确和高效的查询以回答诸如“有多少任务分配给特定员工?”或“哪些员工向特定经理汇报?”等问题。例如,在图 1.14 中,如“Sam Altman 是 OpenAI 的首席执行官”或“John Doe 自 2023 年 1 月 1 日起一直是 OpenAI 的员工”这样的结构化数据可以直接查询以回答“OpenAI 的首席执行官是谁?”或“John Doe 在公司工作多久了?”等问题。同样,如“John Doe 被分配给一个状态为完成的任务”这样的结构化关系可以启用精确查询,例如“哪些任务已被员工完成?”或“在 OpenAI 中谁被分配到特定任务?”这种能力对于从复杂、相互关联的数据中生成可操作的见解至关重要。
另一方面,非结构化数据,如文章文本,通过提供丰富的上下文信息来补充结构化数据,增加了深度和细微差别。例如,图 1.14 中的非结构化文章节点提供了关于新 LLM 模型和嵌入的详细信息,但没有结构化框架,它无法回答诸如“这篇文章与 OpenAI 员工有何关联?”这样的特定查询。
重要的是,仅凭非结构化数据无法回答所有类型的问题。虽然它可以提供开放式或模糊查询的见解,但它缺乏进行精确操作(如过滤、计数或聚合)所需的架构。例如,回答“公司内有多少任务已完成?”或“哪些员工被分配到与 OpenAI 相关的任务?”需要如图 1.14 右侧所示的结构化关系和属性。没有结构化数据,这类查询将需要详尽的文本解析和推理,这既计算成本高又往往不够精确。通过在相同框架中整合结构和非结构化信息,知识图谱能够无缝融合这两个世界,使它们成为在 RAG 应用中高效且准确地回答广泛问题的强大工具。此外,非结构化和结构化数据之间的显式连接解锁了高级检索策略,如将文本中的实体链接到图节点或用源段落上下文化结构化结果,这些策略单独使用任何一种数据类型都难以或无法实现。
摘要
-
LLMs,如 ChatGPT,建立在 Transformer 架构之上,通过从大量文本数据中学习模式,使它们能够高效地处理和生成文本。
-
虽然 LLM 在自然语言理解和生成方面表现出惊人的能力,但它们存在固有的局限性,例如知识截止点、可能生成过时或虚构信息,以及无法访问私人或特定领域知识。
-
由于资源限制和定期更新模型的复杂性,对 LLM(大型语言模型)进行持续微调以增强其内部知识库并不可行。
-
RAG 通过将外部知识库与 LLM 结合来解决其局限性,通过直接将相关事实注入输入提示中,提供准确、内容丰富的响应。
-
RAG(检索增强生成)的实现传统上侧重于非结构化数据源,限制了其在需要结构化、精确和相互关联信息任务中的范围和有效性。
-
知识图谱通过节点和关系来表示和连接实体与概念,整合结构化和非结构化数据,以提供全面的数据表示。
-
将知识图谱集成到 RAG 工作流程中增强了其检索和组织上下文相关数据的能力,使 LLM 能够生成准确、可靠和可解释的响应。
第二章:向量相似度搜索和混合搜索
本章涵盖
-
嵌入、嵌入模型、向量空间和向量相似度搜索简介
-
向量相似度在 RAG 应用中的位置
-
使用向量相似度搜索的 RAG 应用的实用操作指南
-
向 RAG 应用中添加全文搜索以查看启用混合搜索方法如何提高结果
创建知识图谱可能是一个迭代过程,你从非结构化数据开始,然后向其添加结构。当你拥有大量非结构化数据并希望开始使用它来回答问题时,这通常是这种情况。
本章将探讨如何使用 RAG 通过非结构化数据来回答问题。我们将探讨如何使用向量相似度搜索和混合搜索来找到相关信息,以及如何使用这些信息来生成答案。在后续章节中,我们将探讨我们可以使用哪些技术来改进检索器和生成器,以便在数据具有一些结构时获得更好的结果。
在数据科学和机器学习中,嵌入模型和向量相似度搜索是处理复杂数据的重要工具。本章将探讨这些技术如何将复杂的文本和图像等数据转换为称为嵌入的统一格式。
在本章中,我们将介绍嵌入模型和向量相似度搜索的基础知识,解释为什么它们是有用的,如何使用它们,以及它们在 RAG 应用中帮助解决的挑战。为了跟上进度,你需要访问一个正在运行的、空白的 Neo4j 实例。这可以是一个本地安装或云托管实例;只需确保它是空的。你可以直接在附带的 Jupyter 笔记本中跟随实现,笔记本地址为:github.com/tomasonjo/kg-rag/blob/main/notebooks/ch02.ipynb。
2.1 RAG 架构的组件
在 RAG 应用中,有两个主要组件:一个检索器和一个生成器。检索器找到相关信息,生成器使用这些信息来创建响应。向量相似度搜索在检索器中用于找到相关信息;这将在稍后进行更详细的解释。让我们深入了解这两个组件。
2.1.1 检索器
检索器是 RAG 应用的第一个组件。它的目的是找到相关信息并将这些信息传递给生成器。检索器如何找到相关信息并未在 RAG 框架中暗示,但最常见的方式是使用向量相似度搜索。让我们看看为检索器准备数据以成功使用向量相似度搜索需要哪些内容。
向量索引
虽然向量索引对于向量相似度搜索不是严格必需的,但它被高度推荐。向量索引是一种数据结构(如映射),以使其易于搜索相似向量的方式存储向量。当使用向量索引时,检索方法通常被称为近似最近邻搜索。这是因为向量索引不找到确切的最近邻,而是找到非常接近最近邻的向量。这是速度和精度之间的权衡。向量索引比暴力搜索快得多,但精度不如暴力搜索。
向量相似度搜索功能
向量相似度搜索功能是一个函数,它接受一个向量作为输入,并返回一个相似向量的列表。这个函数可能使用向量索引来找到相似向量,也可能使用其他(暴力)方法。重要的是它返回一个相似向量的列表。
两种最常见的向量相似度搜索功能是余弦相似度和欧几里得距离。欧几里得距离代表文本的内容和强度,在本书的大部分情况下并不重要。余弦相似度是两个向量之间角度的度量。在我们的文本嵌入案例中,这个角度代表了两篇文本在意义上相似的程度。余弦相似度函数接受两个向量作为输入,并返回一个介于 0 和 1 之间的数字;0 表示向量完全不同,1 表示它们完全相同。余弦相似度被认为是文本聊天机器人的最佳匹配,也是本书中我们将使用的方法。
嵌入模型
文本语义分类的结果被称为嵌入。任何你想通过向量相似度搜索匹配的文本都必须转换为嵌入。这是通过嵌入模型完成的,并且在整个 RAG 应用中保持嵌入模型不变非常重要。如果你想更改嵌入模型,你必须重新填充向量索引。
嵌入是数字列表,列表的长度称为嵌入维度。嵌入维度很重要,因为它决定了嵌入可以包含多少信息。嵌入维度越高,处理嵌入的计算成本就越高,无论是生成嵌入还是执行向量相似度搜索。
嵌入是将复杂数据表示为更低维空间中一组数字的方法。把它想象成将数据转换成计算机可以轻松理解和处理的形式。
嵌入模型提供了一种统一的方式来表示不同类型的数据。嵌入模型的输入可以是任何复杂的数据,输出是一个向量。例如,在处理文本时,嵌入模型将单词或句子转换为向量,这些向量是数字列表。该模型经过训练以确保这些数字列表捕捉到原始单词的基本方面,如它们的含义或上下文。
文本分块
文本分块是将文本拆分成更小片段的过程。这样做是为了提高检索器的准确性。存在更小的文本片段意味着嵌入更窄且更具体;因此,检索器在搜索时将找到更多相关信息。
文本分块非常重要,而且不容易做对。你需要考虑如何拆分文本:应该是句子、段落、语义意义,还是其他什么?你应该使用滑动窗口,还是使用固定大小?块的大小应该是多少?
对于这些问题,没有正确答案,这取决于用例、数据和领域。但思考这些问题并尝试不同的方法来找到最适合你用例的最佳解决方案是很重要的。
检索器管道
一旦所有部件都到位,检索器管道相当简单。它将查询作为输入,使用嵌入模型将其转换为嵌入,然后使用向量相似度搜索功能来找到相似的嵌入。在简单情况下,检索器管道随后只返回源块,然后这些块被传递给生成器。但在大多数情况下,检索器管道需要进行一些后处理以找到传递给生成器的最佳块。我们将在下一章中介绍更高级的策略。
2.1.2 生成器
生成器是 RAG 应用中的第二个组件。它使用检索器找到的信息来生成响应。生成器通常是一个 LLM,但 RAG 相对于微调或依赖模型的基础知识的一个好处是模型不需要那么大。这是因为检索器找到了相关信息,所以生成器不需要知道一切。它确实需要知道如何使用检索器找到的信息来创建响应。这是一个比知道一切小得多的任务。
因此,我们使用语言模型的能力来生成文本,而不是它的知识。这意味着我们可以使用更小的语言模型,它们运行更快且成本更低。这也意味着我们可以相信语言模型将基于检索器找到的信息来生成响应,因此会少创造一些东西,幻觉也更少。
2.2 使用向量相似度搜索的 RAG
实现使用向量相似度搜索的 RAG 应用程序需要一些组件。我们将在本章中逐一介绍它们。目标是展示如何使用向量相似度搜索实现 RAG 应用程序,以及如何使用检索器找到的信息来生成响应。图 2.1 说明了完成 RAG 应用程序的数据流。

图 2.1 使用向量相似度搜索的此 RAG 应用程序的数据流
我们需要将应用程序分为两个阶段:
-
数据设置
-
查询时间
我们将首先查看数据设置,然后我们将查看在查询时间应用程序将执行的操作。
2.2.1 应用程序数据设置
从前面的章节中,我们知道我们需要对数据进行一些处理,以便能够在运行时将其放置在嵌入模型向量空间中执行向量相似度搜索。所需的部分包括
-
文本语料库
-
文本分块函数
-
嵌入模型
-
具有向量相似度搜索能力的数据库
我们将逐一介绍这些组件,并展示它们如何有助于应用程序数据设置。
数据将存储在数据库中的文本块中,向量索引将填充文本块的嵌入。稍后,在运行时,当用户提出一个问题,该问题将使用与文本块相同的嵌入模型进行嵌入,然后使用向量索引来找到相似文本块。图 2.2 显示了应用程序数据设置的数据流。

图 2.2 应用程序数据设置管道中的组件
2.2.2 文本语料库
在这个例子中我们将使用的文本是一篇题为“爱因斯坦的专利和发明”(Caudhuri,2017)的论文。尽管 LLM 对阿尔伯特·爱因斯坦非常了解,但我们通过提出非常具体的问题,并将它们与我们从论文中得到的答案与从 LLM 得到的答案进行比较,来展示 RAG 是如何工作的。
2.2.3 文本分块
如果一个 LLM 拥有足够大的上下文窗口,我们可以将整篇论文作为一个单独的块。但为了获得更好的结果,我们将论文分成更小的块,并使用每几百个字符作为一个块。最佳的块大小因情况而异,所以请确保对不同块大小进行实验。
在这种情况下,我们还想在块之间有一些重叠。这是因为我们希望能够找到跨越多个块的答案。所以我们将使用一个大小为 500 个字符、重叠为 40 个字符的滑动窗口。这将使索引稍微大一些,但也会使检索器更准确。
为了帮助嵌入模型更好地分类每个块的语义,我们将在空格处进行分块,这样每个块的开始和结束处就不会有断词。这个函数接受一个文本、块大小(字符数)、重叠(字符数),以及一个可选参数,是否在任意字符或仅空白处分割,并返回一个块列表。
列表 2.1 文本分割函数
def chunk_text(text, chunk_size, overlap, split_on_whitespace_only=True): #1
chunks = []
index = 0
while index < len(text):
if split_on_whitespace_only:
prev_whitespace = 0
left_index = index - overlap
while left_index >= 0:
if text[left_index] == " ":
prev_whitespace = left_index
break
left_index -= 1
next_whitespace = text.find(" ", index + chunk_size)
if next_whitespace == -1:
next_whitespace = len(text)
chunk = text[prev_whitespace:next_whitespace].strip()
chunks.append(chunk)
index = next_whitespace + 1
else:
start = max(0, index - overlap + 1)
end = min(index + chunk_size + overlap, len(text))
chunk = text[start:end].strip()
chunks.append(chunk)
index += chunk_size
return chunks
chunks = chunk_text(text, 500, 40) #2
print(len(chunks)) # 89 chunks in total #3
1 定义分割文本的功能
2 调用函数并获取块
3 打印块列表的长度。函数的大部分只是为了确保我们不分割单个单词,而只是在空格上分割。
2.2.4 嵌入模型
在选择嵌入模型时,重要的是要考虑你想要匹配哪种类型的数据。在这种情况下,我们想要匹配文本,所以我们将使用文本嵌入模型。在这本书的整个过程中,我们将使用来自 OpenAI 的嵌入模型和 LLMs,但外面有许多替代方案。来自 Hugging Face 的all-MiniLM-L12-v2通过 Sentence Transformers(mng.bz/nZZ2)是 OpenAI 嵌入模型的一个很好的替代品,它非常易于使用,并且可以在你的本地 CPU 上运行。
一旦我们决定了一个嵌入模型,我们需要确保在整个 RAG 应用程序中使用相同的模型。这是因为向量索引是由嵌入模型中的向量填充的,所以如果我们更改嵌入模型,我们需要重新填充向量索引。要使用 OpenAI 的嵌入模型嵌入块,我们将使用以下代码。
列表 2.2 嵌入块
def embed(texts): #1
response = open_ai_client.embeddings.create(
input=texts,
model="text-embedding-3-small",
)
return list(map(lambda n: n.embedding, response.data))
embeddings = embed(chunks) #2
print(len(embeddings)) # 89, matching the number of chunks #3
print(len(embeddings[0])) # 1536 dimensions #4
1 定义嵌入块的功能
2 调用函数并获取嵌入向量
3 打印嵌入向量列表的长度
4 打印第一个嵌入向量的长度
2.2.5 带有向量相似度搜索功能的数据库
现在我们有了嵌入向量,我们需要将它们存储起来,以便稍后进行相似度搜索。在这本书中,我们将使用 Neo4j 作为我们的数据库,因为它具有内置的向量索引,并且易于使用;在本书的后面部分,我们将使用 Neo4j 的图功能。
在这个阶段我们将使用的模型相当简单。我们将有一个单一的节点类型Chunk,具有两个属性:text和embedding。text属性将保存块中的文本,而embedding属性将保存块的嵌入。

图 2.3 数据模型
图 2.3 显示了将用于演示如何使用向量相似度搜索实现 RAG 应用程序的简单数据模型。
首先,让我们创建一个向量索引。需要记住的一点是,当我们创建向量索引时,我们需要定义向量将具有的维度数。如果你在未来任何时候更改输出不同维度数的嵌入模型,你需要重新创建向量索引。
如我们在代码列表 2.2 中看到的,我们使用的嵌入模型输出的是 1,536 维度的向量,因此当我们创建向量索引时,我们将使用这个维度数。
列表 2.3 在 Neo4j 中创建向量索引
driver.execute_query("""CREATE VECTOR INDEX pdf IF NOT EXISTS
FOR (c:Chunk)
ON c.embedding""")
我们将命名向量索引为pdf,它将用于在embedding属性上索引类型为Chunk的节点,使用余弦相似度搜索功能。
现在我们有了向量索引,我们可以用嵌入来填充它。我们将使用 Cypher 来完成这项工作,首先为每个块创建一个节点,然后在节点上设置text和embedding属性,使用 Cypher 循环。我们还在每个:Chunk节点上存储一个索引,这样我们就可以轻松地稍后找到块。
列表 2.4 在 Neo4j 中存储块和填充向量索引
cypher_query = '''
WITH $chunks as chunks, range(0, size($chunks)) AS index
UNWIND index AS i
WITH i, chunks[i] AS chunk, $embeddings[i] AS embedding
MERGE (c:Chunk {index: i})
SET c.text = chunk, c.embedding = embedding
'''
driver.execute_query(cypher_query, chunks=chunks, embeddings=embeddings)
要检查数据库中有什么,我们可以运行这个 Cypher 查询来获取索引为 0 的:Chunk节点。
列表 2.5 从 Neo4j 中的块节点获取数据
records, _, _ = driver.execute_query(
↪ "MATCH (c:Chunk) WHERE c.index = 0 RETURN c.embedding, c.text")
print(records[0]["c.text"][0:30])
print(records[0]["c.embedding"][0:3])
2.2.6 执行向量搜索
现在我们已经将向量索引填充了嵌入,我们可以执行向量相似度搜索。首先,我们需要嵌入我们想要回答的问题。我们将使用与块相同的嵌入模型,并且我们将使用与嵌入块相同的函数。
列表 2.6 嵌入用户问题
question = "At what time was Einstein really interested↪
↪ in experimental works?"
question_embedding = embed([question])[0]
现在我们已经将问题嵌入,我们可以使用 Cypher 执行向量相似度搜索。
列表 2.7 在 Neo4j 中执行向量搜索
query = '''
CALL db.index.vector.queryNodes('pdf', 2, $question_embedding) YIELD node↪
↪ AS hits, score
RETURN hits.text AS text, score, hits.index AS index
'''
similar_records, _, _ = driver.execute_query(query, question_embedding=question_embedding)
查询返回最相似的两个块,我们可以打印结果以查看我们得到了什么。代码将打印以下文本块及其相似度得分。
列表 2.8 打印结果
for record in similar_records:
print(record["text"])
print(record["score"], record["index"])
print("======")
upbringing, his interest in inventions and patents was not unusual.
Being a manufacturer’s son, Einstein grew upon in an environment of↪
↪ machines and instruments.
When his father’s company obtained the contract to illuminate Munich city↪
↪ during beer festival, he
was actively engaged in execution of the contract. In his ETH days↪
↪ Einstein was genuinely interested
in experimental works. He wrote to his friend, “most of the time I worked↪
↪ in the physical laboratory,
fascinated by the direct contact with observation.” Einstein's
0.8185358047485352 42
======
instruments. However, it must also be
emphasized that his main occupation was theoretical physics. The↪
↪ inventions he worked upon were
his diversions. In his unproductive times he used to work upon on solving↪
↪ mathematical problems (not
related to his ongoing theoretical investigations) or took upon some↪
↪ practical problem. As shown in
Table. 2, Einstein was involved in three major inventions; (i)↪
↪ refrigeration system with Leo Szilard, (ii)
Sound reproduction system with Rudolf Goldschmidt and (iii) automatic↪
↪ camera
0.7906564474105835 44
======
从打印结果中,我们可以看到匹配的块、它们的相似度得分和它们的索引。下一步是使用这些块通过 LLM 生成一个答案。
2.2.7 使用 LLM 生成答案
当与 LLM 通信时,我们有传递所谓的“系统消息”的能力,其中我们可以传递给 LLM 的指令。我们还传递一个“用户消息”,它包含原始问题,在我们的情况下,是问题的答案。
在用户消息中,我们传递给 LLM 想要使用的块,我们通过传递在列表 2.8 中相似搜索中找到的相似块的text属性来完成。
列表 2.9 LLM 上下文
system_message = "You're an Einstein expert, but can only use the provided
↪ documents to respond to the questions."
user_message = f"""
Use the following documents to answer the question that will follow:
{[doc["text"] for doc in similar_records]}
---
The question to answer using information only from the above documents:
↪ {question}
"""
让我们现在使用 LLM 来生成一个答案。
列表 2.10 使用 LLM 生成答案
print("Question:", question)
stream = open_ai_client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": system_message},
{"role": "user", "content": user_message}
],
stream=True,
)
for chunk in stream:
print(chunk.choices[0].delta.content or "", end="")
这将流式传输 LLM 生成的结果,我们可以看到生成时的结果。
列表 2.11 LLM 的答案
Question: At what time was Einstein really interested in experimental works?
During his ETH days, Einstein was genuinely interested in experimental works.
哇,看看这个!LLM 能够根据检索器找到的信息生成一个答案。
2.3 向 RAG 应用添加全文搜索以实现混合搜索
在上一节中,我们看到了如何使用向量相似度搜索来实现一个 RAG 应用。虽然纯向量相似度搜索可以带你走很长的路,并且比普通的全文搜索有巨大的改进,但它通常不足以产生足够高质量、准确性和性能,以满足生产用例。
在本节中,我们将探讨如何改进检索器以获得更好的结果。我们将考虑如何向 RAG 应用添加全文搜索以实现混合搜索。
2.3.1 全文搜索索引
全文搜索,数据库中的一种文本搜索方法,已经存在很长时间了。它通过关键词在数据中搜索匹配项,而不是在向量空间中的相似度。要在全文搜索中找到匹配项,搜索词必须与数据中的单词完全匹配。
要启用混合搜索,我们需要向数据库中添加一个全文搜索索引。大多数数据库都有某种形式的全文搜索索引,在这本书中我们将使用 Neo4j 的全文搜索索引。
列表 2.12 在 Neo4j 中创建全文索引
driver.execute_query("CREATE FULLTEXT INDEX PdfChunkFulltext FOR (c:Chunk)
↪ ON EACH [c.text]")
在这里,我们在 :Chunk 节点的 text 属性上创建一个名为 PdfChunkFulltext 的全文索引。
2.3.2 执行混合搜索
混合搜索的想法是我们执行向量相似度搜索和全文搜索,然后合并结果。为了能够比较两种不同匹配的分数,我们需要对分数进行归一化。我们通过将分数除以每个搜索的最高分数来实现这一点。
列表 2.13 在 Neo4j 中执行混合搜索
hybrid_query = '''
CALL {
// vector index
CALL db.index.vector.queryNodes('pdf', $k, $question_embedding)↪
↪ YIELD node, score
WITH collect({node:node, score:score}) AS nodes, max(score) AS max
UNWIND nodes AS n
// Normalize scores
RETURN n.node AS node, (n.score / max) AS score
UNION
// keyword index
CALL db.index.fulltext.queryNodes('ftPdfChunk', $question, {limit: $k})
YIELD node, score
WITH collect({node:node, score:score}) AS nodes, max(score) AS max
UNWIND nodes AS n
// Normalize scores
RETURN n.node AS node, (n.score / max) AS score
}
// deduplicate nodes
WITH node, max(score) AS score ORDER BY score DESC LIMIT $k
RETURN node, score
'''
我们编写了一个联合 Cypher 查询,首先执行向量相似度搜索,然后执行全文搜索。然后我们去重结果并返回前 k 个结果。
列表 2.14 在 Neo4j 中调用混合搜索
similar_hybrid_records, _, _ = driver.execute_query(hybrid_query,
↪ question_embedding=question_embedding, question=question, k=4)
for record in similar_hybrid_records:
print(record["node"]["text"])
print(record["score"], record["node"]["index"])
print("======")
列表 2.15 混合搜索的答案
CH-Switzerland
Considering Einstein’s upbringing, his interest in inventions and patents↪
↪ was not unusual.
Being a manufacturer’s son, Einstein grew upon in an environment of↪
↪ machines and instruments.
When his father’s company obtained the contract to illuminate Munich city↪
↪ during beer festival, he
was actively engaged in execution of the contract. In his ETH days↪
↪ Einstein was genuinely interested
in experimental works. He wrote to his friend, “most of the time I worked↪
↪ in the physical laboratory,
fascinated by the direct contact with observation.” Einstein's
1.0 42
======
Einstein
left his job at the Patent office and joined the University of Zurich on↪
↪ October 15, 1909\. Thereafter, he
continued to rise in ladder. In 1911, he moved to Prague University as a↪
↪ full professor, a year later, he
was appointed as full professor at ETH, Zurich, his alma-mater. In 1914,↪
↪ he was appointed Director of
the Kaiser Wilhelm Institute for Physics (1914–1932) and a professor at↪
↪ the Humboldt University of
Berlin, with a special clause in his contract that freed him from↪
↪ teaching obligations. In the meantime,
he was working for
0.9835733295862473 31
======
在这里,我们可以看到由于归一化,最上面的结果得到了 1.0 的分数。这意味着最上面的结果与向量相似度搜索的最上面的结果相同。但我们也可以看到第二个结果不同。这是因为全文搜索找到了比向量相似度搜索更好的匹配。
2.4 总结思考
在本章中,我们探讨了向量相似度搜索是什么,它由哪些组件组成,以及它如何适应 RAG 应用。然后我们添加全文搜索来提高检索器的性能。
通过使用向量相似度搜索和全文搜索,我们可以比只使用其中之一获得更好的结果。虽然这种方法在某些情况下可能效果很好,但由于我们使用非结构化数据来检索信息,所以使用混合搜索的质量、准确性和性能仍然相当有限。文本中的引用并不总是被捕获,周围的环境也不总是足以让 LLMs 理解文本的意义以生成好的答案。
在下一章中,我们将探讨如何改进检索器以获得更好的结果。
摘要
-
RAG 应用由一个检索器和生成器组成。检索器找到相关信息,生成器使用这些信息来创建响应。
-
文本嵌入在向量空间中捕捉文本的意义,这使得我们可以使用向量相似度搜索来找到相似文本。
-
通过向 RAG 应用添加全文搜索,我们可以启用混合搜索来提高检索器的性能。
-
向量相似度搜索和混合搜索在特定情况下可以很好地工作,但随着数据复杂性的增加,它们的质量、准确性和性能仍然相当有限。
第三章:高级向量检索策略
本章涵盖
-
查询重写技术
-
高级文本嵌入策略
-
实现父文档检索
在本书的第二章中,你学习了文本嵌入和向量相似度搜索的基础知识。通过将文本转换为数值向量,你看到了机器如何理解内容的语义意义。结合文本嵌入和向量相似度搜索技术,可以从大量文档中优化并准确地检索相关非结构化文本,从而在 RAG 应用中提供更准确和更新的答案。假设你已经按照第二章的描述实现了并部署了一个 RAG 应用。经过一些测试后,你和 RAG 应用的用户注意到,由于检索到的文档中存在不完整或不相关信息,生成的答案的准确性不足。因此,你被分配了增强检索系统以提高生成答案准确性的任务。
与任何技术一样,文本嵌入和向量相似度搜索的基本实现可能无法产生足够的检索准确性和召回率。由于术语或上下文的不同,从用户查询生成的嵌入可能并不总是与包含所需关键信息的文档的嵌入紧密对齐。这种差异可能导致与查询意图高度相关的文档被忽视,因为查询的嵌入表示未能捕捉到所寻求信息的本质。
提高检索准确性和召回率的一种策略是重写用于查找相关文档的查询。查询重写方法旨在通过以更好地符合目标文档的语言和上下文的方式重构查询,来弥合用户查询和信息丰富的文档之间的差距。这种查询细化提高了找到包含相关信息的文档的机会,从而提高了对原始查询的响应的准确性。查询重写策略的例子有假设性文档检索器(Gao et al., 2022)或回退提示(Zheng et al., 2023)。回退提示策略在图 3.1 中进行了可视化。

图 3.1 使用回退技术进行查询重写以提高向量检索准确度
图 3.1 概述了一个过程,其中用户的查询被转换以改善文档检索结果,这种技术被称为回退提示。在所提出的场景中,用户就埃斯特拉·利奥波德在特定时间段内的教育历史提出了一个详细的问题。然后,这个初始问题被一个具有查询重写能力的语言模型(如 GPT-4)处理,将其改写为一个更一般的问题,关于埃斯特拉·利奥波德的教育背景。这一步骤的目的是在搜索过程中撒更宽的网,因为重写的查询更有可能与可能包含所需信息的各种文档相匹配。
提高检索准确性的另一种方法是改变文档嵌入策略。在上一章中,你嵌入了一段文本,检索了相同的文本,并将其作为输入到 LLM(大型语言模型)中生成答案。然而,向量检索系统是灵活的,因为你不仅限于嵌入你计划检索的确切文本。相反,你可以嵌入更好地代表文档意义的文本,例如更具情境相关性的部分、合成问题或改写版本。这些替代方案可以更好地捕捉关键思想和主题,从而实现更准确和相关的检索。图 3.2 展示了两个高级嵌入策略的示例。

图 3.2 假设性问题与父文档检索策略
图 3.2 的左侧展示了假设性问题策略。在假设性问题-嵌入策略中,你必须确定文档中的信息可以回答的问题。例如,你可以使用 LLM 生成假设性问题,或者你可以使用你的聊天机器人的对话历史来提出文档可以回答的问题。其想法是,而不是嵌入原始文档本身,你嵌入文档可以回答的问题。例如,图 3.2 中用向量 [1,2,3,0,5] 编码的问题“利奥波德在加州大学学习了什么?”当用户提出问题时,系统计算查询的嵌入并搜索预计算的查询嵌入中的最近邻。目标是定位与用户问题密切匹配且语义相似的问题。然后,系统检索包含可以回答这些相似问题的信息的文档。本质上,假设性问题-嵌入策略涉及嵌入文档可以回答的潜在问题,并使用这些嵌入来匹配和检索用户查询的相关文档。
图 3.2 的右侧说明了父文档-嵌入策略。在这种方法中,原始文档—被称为父文档—被拆分成更小的单元,称为子块,通常基于固定的标记计数。不是将整个父文档作为一个单一单元嵌入,而是为每个子块计算一个单独的嵌入。例如,块“Leopold 获得了她的植物学硕士学位”可能被嵌入为向量 [1, 0, 3, 0, 1]。当用户提交查询时,系统将其与这些子嵌入进行比较,以找到最相关的匹配。然而,系统不仅返回匹配的块,还检索与它关联的整个原始父文档。这允许语言模型在完整的信息上下文中操作,增加了生成准确和完整答案的机会。
这种策略解决了嵌入长文档的常见限制:当你嵌入完整的父文档时,结果向量可能会通过平均模糊不同的观点,使得有效地匹配特定查询变得困难。相比之下,将文档拆分成更小的块可以允许更精确的匹配,同时仍然在需要时使系统能够返回完整的上下文。
其他提高检索准确性的策略
除了改变文档嵌入策略之外,还有其他几种技术可以增强检索准确性:
-
微调文本嵌入模型—通过在特定领域的数据上调整嵌入模型,你可以提高其捕捉用户查询上下文的能力,从而与相关文档实现更接近的语义匹配。请注意,微调通常需要更多的计算和基础设施。此外,一旦模型更新,所有现有的文档嵌入都必须重新计算以反映这些变化—对于大型文档库来说,这可能非常耗费资源。
-
重排序策略—在检索到一组初始文档后,重排序算法可以根据用户意图的相关性对它们进行重新排序。这一轮处理通常使用更复杂的模型或评分启发式方法来细化结果。重排序有助于揭示最相关的内容,即使初始匹配不是最优的。
-
基于元数据的上下文过滤—许多文档包含结构化元数据,如作者、发布日期、主题标签或来源类型。根据这些元数据应用过滤器—无论是手动还是作为检索流程的一部分—可以在语义分析之前显著缩小候选文档的范围。
-
匹配,提高精确度。例如,关于最近政策更新的查询可以限制在去年发布的文档中。
-
混合检索(关键词+密集向量搜索)—结合稀疏检索(例如,基于关键词的搜索)和密集向量检索(语义搜索)可以兼得两者之长。关键词搜索擅长精确匹配和罕见术语,而密集检索则捕捉更广泛的意义。混合系统可以合并和重新排序两种方法的结果,以最大化召回率和精确率。
虽然所有这些策略都可以提高检索质量,但详细的实现指南超出了本书的范围,除了混合检索,这在第二章中已介绍。
在本章的剩余部分,我们将从概念转向代码,逐步介绍实现过程。为了跟上,你需要访问一个运行中的空白 Neo4j 实例。这可以是一个本地安装或云托管实例;只需确保它是空的。你可以直接在附带的 Jupyter 笔记本中跟随实现,笔记本地址为:github.com/tomasonjo/kg-rag/blob/main/notebooks/ch03.ipynb。
想象你已经实现了第二章中的基本 RAG 系统,但检索准确性还不够高。响应缺乏相关性或遗漏了重要背景,你怀疑系统没有检索到最有用的文档来支持高质量的答案。为了解决这个问题,你决定通过添加回溯提示步骤来提高查询本身的质量,以增强现有的 RAG 管道。此外,你将切换到父文档检索策略。这种方法通过匹配更小的块来提供更精细和准确的检索,同时仍然提供完整的父文档作为背景。
这些改进旨在提高检索内容的关联性和生成答案的整体准确性。
3.1 回溯提示
如前所述,回溯提示是一种查询重写技术,旨在提高向量检索的准确性。原始论文(Zheng 等人,2023 年)中的一个例子展示了这一过程:具体查询“Thierry Audel 在 2007 年至 2008 年间为哪支球队效力?”被扩展为“Thierry Audel 在其职业生涯中为哪些球队效力?”以提高向量搜索的精确性,从而提高生成答案的准确性。通过将详细问题转化为更广泛、更高层次查询,回溯提示简化了向量搜索过程。其理念是,更广泛的查询通常包含更全面的信息范围,这使得模型更容易识别相关事实,而不会因具体细节而陷入困境。
作者使用了 LLM 进行查询重写任务,如图 3.3 所示。

图 3.3 使用 LLM 的回溯方法重写查询
LLM 对于查询重写任务非常适用,因为它们在自然语言理解和生成方面表现出色。你不需要为每个任务训练或微调新的模型。相反,你可以在输入提示中提供任务指令。
回退提示论文的作者使用了以下列表中的系统提示来指导 LLM 如何重写输入查询。
列表 3.1 LLM 生成回退问题的系统提示
stepback_system_message = f"""
You are an expert at world knowledge. Your task is to step back #1
and paraphrase a question to a more generic step-back question, which
is easier to answer. Here are a few examples
"input": "Could the members of The Police perform lawful arrests?" #2
"output": "what can the members of The Police do?"
"input": "Jan Sindel’s was born in what country?"
"output": "what is Jan Sindel’s personal history?"
"""
1 查询重写指令
2 少样本示例
列表 3.1 中的系统提示首先给 LLM 一个简单指令,将用户的提问重写为一个更通用的、回退版本。这种指令本身被称为 零样本提示,它完全依赖于 LLM 的一般能力和对任务的了解,而不提供任何示例。然而,为了更有效地引导模型并确保结果的一致性,作者选择通过几个期望释义行为的示例来扩展提示。这种技术被称为 少样本提示,其中在提示中包含少量示例(通常是两到五个)来展示任务。少样本提示通过将任务锚定在具体实例中,有助于 LLM 更好地理解预期的转换,从而提高输出质量和可靠性。
要实现查询重写,你只需要将列表 3.1 中的系统提示与用户的提问一起发送给 LLM。这个任务的特定功能将在下一个列表中概述。
列表 3.2 生成回退问题的函数
def generate_stepback(question: str):
user_message = f"""{question}"""
step_back_question = chat(
messages=[
{"role": "system", "content": stepback_system_message},
{"role": "user", "content": user_message},
]
)
return step_back_question
你现在可以通过执行下面的代码来测试回退提示生成。
列表 3.3 执行回退提示函数
question = "Which team did Thierry Audel play for from 2007 to 2008?"
step_back_question = generate_stepback(question)
print(f"Stepback results: {step_back_question}")
# Stepback results: What is the career history of Thierry Audel?
列表 3.3 中的结果展示了回退提示生成函数的成功执行。通过将关于 Thierry Audel 2007 年至 2008 年团队的特定查询转换为关于他整个职业生涯的更广泛问题,该函数有效地扩展了上下文,并应提高检索准确性和召回率。
练习 3.1
为了探索回退提示生成的有效性,尝试将其应用于各种问题,并观察它如何扩展上下文。你还可以更改系统提示,观察它如何影响输出。
3.2 父文档检索器
父文档检索策略涉及将大文档分成更小的部分,计算每个部分的嵌入而不是整个文档,并使用这些嵌入来更准确地匹配用户查询,最终检索整个文档以提供丰富的上下文响应。然而,由于你不能直接将整个 PDF 输入到 LLM 中,你首先需要将 PDF 分成父文档,然后将这些父文档进一步分成子文档进行嵌入和检索。父文档和子文档的图表示如图 3.4 所示。

图 3.4 父文档图表示
图 3.4 展示了一种基于图的方法来存储和组织文档,以实现父文档检索策略。在顶部,一个 PDF 节点代表整个文档,带有标题和标识符。此节点连接到多个父文档节点。在这个例子中,你将使用 2,000 个字符的限制来分割 PDF 成父文档。这些父文档节点反过来又连接到子文档节点,每个子节点包含对应父节点文本的 500 个字符块。子节点有一个表示文本子块的嵌入向量,用于检索目的。
我们将使用与第二章相同的文本,这是一篇由 Asis Kumar Chaudhuri 撰写的论文,标题为“爱因斯坦的专利和发明”(arxiv.org/abs/1709.00666)。此外,在将文档分割成更小的部分进行处理时,最好首先根据结构元素如段落或章节来分割文本。这种方法保持了内容的连贯性和上下文,因为段落或章节通常封装了完整的思想或主题。因此,我们将首先将 PDF 文本分割成章节。
列表 3.4 使用正则表达式将文本分割成章节
import re
def split_text_by_titles(text):
# A regular expression pattern for titles that
# match lines starting with one or more digits, an optional uppercase letter,
# followed by a dot, a space, and then up to 60 characters
title_pattern = re.compile(r"(\n\d+[A-Z]?\. {1,3}.{0,60}\n)", re.DOTALL)
titles = title_pattern.findall(text)
# Split the text at these titles
sections = re.split(title_pattern, text)
sections_with_titles = []
# Append the first section
sections_with_titles.append(sections[0])
# Iterate over the rest of the sections
for i in range(1, len(titles) + 1):
section_text = sections[i * 2 - 1].strip() + "\n" +
↪ sections[i * 2].strip()
sections_with_titles.append(section_text)
return sections_with_titles
sections = split_text_by_titles(text)
print(f"Number of sections: {len(sections)}")
# Number of sections: 9
列表 3.4 中的 split_text_by_titles 函数使用正则表达式按章节分割文本。该正则表达式基于这样一个事实,即文本中的章节组织为一个编号列表,其中每个新的章节以一个数字和一个可选字符开始,后跟一个点和章节标题。split_text_by_titles 函数的输出是九个章节。如果你检查 PDF,你会注意到只有四个主要章节。然而,还有四个子章节(3A–3D)描述了一些专利,如果你将引言摘要视为一个单独的章节,那么总共是九个章节。
在继续使用父文档检索器之前,你需要计算每个章节的标记数,以便更好地理解它们的长度。你将使用由 OpenAI 开发的 tiktoken 包来计算给定文本中的标记数。
列表 3.5 计算章节中的标记数
def num_tokens_from_string(string: str, model: str = "gpt-4") -> int:
"""Returns the number of tokens in a text string."""
encoding = tiktoken.encoding_for_model(model)
num_tokens = len(encoding.encode(string))
return num_tokens
for s in sections:
print(num_tokens_from_string(s))
# 154, 254, 4186, 570, 2703, 1441, 194, 600
大多数章节的大小相对较小,最多 600 个标记,这适合大多数 LLM 上下文提示。然而,第三章节有超过 4,000 个标记,这可能导致在 LLM 生成过程中的标记限制错误。因此,你必须将章节分割成父文档,其中每个文档最多有 2,000 个字符。你将使用上一章的 chunk_text 来实现这一点。
列表 3.6 将章节分割成最大长度为 2,000 个字符的父文档
parent_chunks = []
for s in sections:
parent_chunks.extend(chunk_text(s, 2000, 40))
练习 3.2
使用num_tokens_from_string函数确定每个父文档的标记数。标记数可以帮助您决定预处理中的额外步骤。例如,超过合理标记数的较长的部分应进一步拆分。另一方面,如果某些部分异常简短,包含 20 个标记或更少,您应考虑完全删除它们,因为它们可能不会增加任何信息价值。
而不是在后续步骤中拆分子块并导入它们,您将一次性执行拆分和导入操作。在单个步骤中执行这两个操作允许您跳过稍微复杂一些的存储中间结果的中间数据结构。在导入图之前,您需要定义导入 Cypher 语句。导入父文档结构的 Cypher 语句相对简单。
列表 3.7 用于导入父文档策略图的 Cypher 查询
cypher_import_query = """ #1
MERGE (pdf:PDF {id:$pdf_id}) #2
MERGE (p:Parent {id:$pdf_id + '-' + $id})
SET p.text = $parent
MERGE (pdf)-[:HAS_PARENT]->(p) #3
WITH p, $children AS children, $embeddings as embeddings
UNWIND range(0, size(children) - 1) AS child_index
MERGE (c:Child {id: $pdf_id + '-' + $id + '-' + toString(child_index)})
SET c.text = children[child_index], c.embedding = embeddings[child_index]
MERGE (p)-[:HAS_CHILD]->(c);
"""
1 根据 id 属性合并 PDF 节点
2 合并父节点并设置其文本属性
3 合并每个父节点的多个子节点
列表 3.7 中的 Cypher 语句首先合并一个PDF节点。接下来,它使用唯一 ID 合并Parent节点。然后,Parent节点通过HAS_PARENT关系链接到PDF节点,并设置text属性。最后,它遍历子文档列表。为列表中的每个元素创建一个Child节点,设置文本和嵌入属性,并通过HAS_CHILD关系将其链接到其Parent节点。
现在一切准备就绪,您可以将父文档结构导入到图数据库中。
列表 3.8 将父文档数据导入到图数据库中
for i, chunk in enumerate(parent_chunks):
child_chunks = chunk_text(chunk, 500, 20) #1
embeddings = embed(child_chunks) #2
# Add to neo4j
neo4j_driver.execute_query( #3
cypher_import_query,
id=str(i),
pdf_id='1709.00666'
parent=chunk,
children=child_chunks,
embeddings=embeddings,
)
1 将父文档拆分为子块
2 计算子块的文本嵌入
3 导入到 Neo4j
列表 3.8 中的代码首先遍历父文档块。每个父文档块使用chunk_text函数拆分为多个子块。然后,代码使用embed函数计算这些子块的文本嵌入。在嵌入生成之后,execute_query方法将数据导入到 Neo4j 图数据库中。
您可以通过在 Neo4j 浏览器中运行以下列表中的 Cypher 语句来检查生成的图结构。
列表 3.9 在子节点上创建向量索引
MATCH p=(pdf:PDF)-[:HAS_PARENT]->()-[:HAS_CHILD]->()
RETURN p LIMIT 25
列表 3.9 中的 Cypher 语句生成了图 3.5 所示的图。此图可视化显示了一个中心 PDF 节点连接到多个父节点,说明了文档与其部分之间的层次关系。每个父节点进一步连接到多个子节点,表明文档结构中将部分拆分为更小的块。
为了确保文档嵌入的有效比较,您将添加一个向量索引。
列表 3.10 在子节点上创建向量索引
driver.execute_query("""CREATE VECTOR INDEX parent IF NOT EXISTS
FOR (c:Child)
ON c.embedding""")
列表 3.10 中生成向量索引的代码与第二章中使用的代码相同。在这里,你在Child的embedding属性上创建了一个向量索引。

图 3.5 Neo4j 浏览器中导入数据的一部分的图可视化
3.2.1 检索父文档策略数据
在导入数据和定义向量索引后,你可以专注于实现检索部分。要从图中检索相关文档,你必须定义以下列表中描述的检索 Cypher 语句。
列表 3.11 父文档检索 Cypher 语句
retrieval_query = """
CALL db.index.vector.queryNodes($index_name, $k * 4, $question_embedding) #1
YIELD node, score #2
MATCH (node)<-[:HAS_CHILD]-(parent) #3
WITH parent, max(score) AS score
RETURN parent.text AS text, score
ORDER BY score DESC #4
LIMIT toInteger($k)
"""
1 向量索引搜索
2 遍历到父文档
3 去重父文档
4 确保最终限制
列表 3.11 中的 Cypher 语句首先在图数据库中执行基于向量的搜索,以识别与指定问题嵌入紧密相关的子节点。你可以看到,在初始向量搜索中我们检索了k * 4个文档。在初始向量搜索中使用k * 4值的原因是,你预计会有多个来自向量搜索的相似子节点实际上属于同一个父文档。因此,去重父文档变得至关重要。如果没有去重,结果集可能会包含多个针对同一父文档的条目,每个条目对应于该父文档的不同子节点。然而,为了保证最终有k个唯一的父文档,你从一个更大的k * 4个子节点池开始,从而创建了一个安全缓冲区。在 Cypher 语句的末尾,你强制执行最终的k限制。
利用列表 3.11 中的 Cypher 语句从数据库中检索父文档的函数如下所示。
列表 3.12 父文档检索函数
def parent_retrieval(question: str, k: int = 4) -> List[str]:
question_embedding = embed([question])[0]
similar_records, _, _ = neo4j_driver.execute_query(
retrieval_query,
question_embedding=question_embedding,
k=k,
index_name=index_name,
)
return [record["text"] for record in similar_records]
列表 3.12 中的parent_retrieval函数首先为给定的问题生成一个文本嵌入,然后使用之前提到的 Cypher 语句从数据库中检索最相关的文档列表。
3.3 完整的 RAG 管道
管道的最后一部分是生成答案的函数。
列表 3.13 使用 LLM 生成答案
system_message = "You're en Einstein expert, but can only use the↪
↪ provided documents to respond to the questions."
def generate_answer(question: str, documents: List[str]) -> str:
user_message = f"""
Use the following documents to answer the question that will follow:
{documents}
---
The question to answer using information only from the above↪
↪ documents: {question}
"""
result = chat(
messages=[
{"role": "system", "content": system_message},
{"role": "user", "content": user_message},
]
)
print("Response:", result)
列表 3.13 中的代码与第二章中的代码相同。你将问题以及相关文档传递给 LLM,并提示它生成答案。
在实现了回溯提示和父文档检索之后,你就可以将所有这些内容整合到一个单独的函数中。
列表 3.14 完整的带有回溯提示的父文档检索 RAG 管道
def rag_pipeline(question: str) -> str:
stepback_prompt = generate_stepback(question)
print(f"Stepback prompt: {stepback_prompt}")
documents = parent_retrieval(stepback_prompt)
answer = generate_answer(question, documents)
return answer
列表 3.14 中的rag_pipeline函数接收一个问题作为输入并创建一个回溯提示。然后,它根据回溯提示检索相关文档,并将这些文档与原始问题一起传递给 LLM 以生成最终答案。
你现在可以测试rag_pipeline的实现。
列表 3.15 带有回溯提示的完整父文档检索器 RAG 流程
rag_pipeline("When was Einstein granted the patent for his blouse design?")
# Stepback prompt: What are some notable achievements in Einstein's life?
# Response: Einstein was granted the patent for his blouse design on October 27, 1936.
练习 3.3
通过询问关于爱因斯坦生平的其他问题来评估 rag_pipeline 实现的效果。此外,您还可以移除回溯提示步骤,以比较它是否改善了结果。
恭喜!您已成功通过结合查询重写和父文档检索实现了高级向量搜索检索策略。
摘要
-
通过将用户查询与目标文档的语言和上下文更紧密地对齐,查询重写可以提高文档检索的准确性。
-
像假设文档检索器和回溯提示这样的技术有效地弥合了用户意图与文档内容之间的差距,减少了遗漏相关信息的机会。
-
通过嵌入不仅精确文本,还包括上下文相关的摘要或释义,可以捕捉文档的精髓,从而提高检索系统的有效性。
-
通过实施假设问题嵌入和父文档检索等策略,可以实现查询与文档之间更精确的匹配,增强检索信息的关联性和准确性。
-
将文档拆分为更小、更易于管理的块以进行嵌入,允许采用更细粒度的信息检索方法,确保特定查询找到最相关的文档部分。
第四章:从自然语言问题生成 Cypher 查询
本章涵盖
-
查询语言生成的基础知识
-
查询语言生成在 RAG 管道中的位置
-
查询语言生成的实用技巧
-
使用基础模型实现 text2cypher 检索器
-
用于文本 2cypher 的专业(微调)LLM
在前面的章节中,我们已经覆盖了很多内容。我们学习了如何构建知识图谱,从文本中提取信息,并使用这些信息来回答问题。我们还探讨了如何通过使用硬编码的 Cypher 查询来扩展和改进普通的向量搜索检索,从而为 LLM 获取更多相关的上下文。在本章中,我们将更进一步,学习如何从自然语言问题生成 Cypher 查询。这将使我们能够构建一个更灵活和动态的检索系统,能够适应不同类型的问题和知识图谱。
注意:在本章的实现中,我们使用所谓的“电影数据集”。有关数据集的更多信息以及各种加载方式,请参阅附录。
4.1 查询语言生成的基础知识
当我们谈论查询语言生成的基础知识时,我们指的是将自然语言问题转换为可以在数据库上执行的语言的过程。更具体地说,我们感兴趣的是从自然语言问题生成 Cypher 查询。大多数 LLM 都知道 Cypher 是什么,也知道该语言的基本语法。在这个过程中,主要挑战是生成一个既正确又与所提问题相关的查询。这需要理解问题的语义以及被查询的知识图谱的模式。
如果我们不提供知识图谱的模式,LLM 只能假设节点、关系和属性的名称。当提供模式时,它充当用户问题的语义与所使用的图模型之间的映射——节点上使用的标签、存在的关联类型、可用的属性以及节点连接到的关联类型。
从自然语言问题生成 Cypher 查询的工作流程可以分解为以下步骤(图 4.1):
-
从用户那里检索问题。
-
检索知识图谱的模式。
-
定义其他有用的信息,如术语映射、格式说明和少量示例。
-
为 LLM 生成提示。
-
将提示传递给 LLM 以生成 Cypher 查询。

图 4.1 从自然语言问题生成 Cypher 查询的工作流程
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 来执行某些类型的查询——当我们寻找的不是图中最相似的节点,或者我们想要以某种方式聚合数据时。在下一章中,我们将探讨如何创建一个代理系统,我们可以提供多个检索器,并为每个用户问题选择最合适的一个,以便能够向用户提供最佳响应。
Text2cypher 也可以作为“万能”检索器,用于那些在系统中没有其他检索器能提供良好匹配的问题类型。
4.3 查询语言生成的实用技巧
当从自然语言问题生成 Cypher 查询时,有一些事情需要考虑,以确保生成的查询是正确且相关的。LLM 在生成 Cypher 查询时容易出错,尤其是当输入问题复杂或含糊不清,或者数据库模式元素没有语义命名时。
4.3.1 使用少量示例进行上下文学习
少量示例是提高 LLM 在 text2cypher 中性能的绝佳方式。这意味着我们可以向 LLM 提供一些问题和它们相应的 Cypher 查询的示例,LLM 将学会为新的问题生成类似的查询。相比之下,零示例是在我们不向 LLM 提供任何示例的情况下,它必须在没有任何提示的情况下生成查询。
几个示例是针对查询的知识图谱特定的,因此需要为每个知识图谱手动创建。这在您意识到 LLM 误解了模式或经常犯相同类型的错误(期望一个属性而实际上应该是遍历等)时非常有用。
假设您检测到 LLM 正在尝试读取电影的制作国家,并且它在电影节点上寻找一个属性,但实际上国家是图中的一个节点。然后您可以在提示中添加少量示例,让 LLM 知道如何获取国家名称:
电影《黑客帝国》是在哪个国家制作的?
MATCH (m:Movie {title: 'The Matrix'}) RETURN m.country
这可以通过在提示 LLM 的几个示例中添加以下内容来解决:
电影《黑客帝国》是在哪个国家制作的?
示例
问题:电影《头号玩家》是在哪个国家制作的?
Cypher:MATCH (m:Movie { title: '头号玩家' })-[:PRODUCED_IN]→(c:Country) RETURN c.name
MATCH (m:Movie {title: 'The Matrix'})-[:PRODUCED_IN]->(c:Country)
↪ RETURN c.name
这不仅会解决这个具体问题,而且由于我们现在有一个清晰的例子让 LLM 看到模式以获取国家名称,所以也会解决类似的问题。
4.3.2 在提示中使用数据库模式向 LLM 展示知识图谱的结构
知识图谱的模式对于生成正确的 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 数据库中推断模式。
小贴士:您可以在neo4j.com/docs/apoc/了解更多关于 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
"""
使用这些查询,我们现在可以获取图数据库的模式并将其用于提示 LLM。让我们运行这些查询并以结构化的方式存储结果,这样我们就可以稍后生成前面的模式字符串。
列表 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 没有正确理解模式而导致的生成查询问题时。
术语映射:
人物:当用户询问一个职业人物时,他们指的是具有 Person 标签的节点。电影:当用户询问一部电影或电影时,他们指的是具有 Movie 标签的节点。
4.3.4 格式说明
不同的 LLM 以不同的方式输出响应。其中一些在 Cypher 查询周围添加代码标签,而另一些则没有。一些在 Cypher 查询之前添加文本;而另一些则没有,等等。
要使它们都以相同的方式输出,你可以在提示中添加格式说明。有用的说明是尝试让 LLM 只输出 Cypher 查询,而不输出其他任何内容。
格式说明:
不要在响应中包含任何解释或道歉。不要回答任何可能要求你构建 Cypher 语句之外的问题。不要包含任何文本,除了生成的 Cypher 语句。只以 CYPHER 回答,不要包含代码块。
4.4 使用基础模型实现 text2cypher 生成器
让我们将所有这些应用到实践中,并使用基础模型实现一个 text2cypher 生成器。这里的任务基本上是形成一个包含模式、术语映射、格式说明和少量示例的提示,以便向 LLM 明确我们的意图。
在本章的剩余部分,我们将使用 Neo4j Python 驱动程序和 OpenAI API 实现一个 text2cypher 生成器。为了跟随,你需要访问一个运行中的、空白的 Neo4j 实例。这可以是一个本地安装或云托管实例;只需确保它是空的。你可以直接在附带的 Jupyter 笔记本中跟随实现,笔记本地址如下:github.com/tomasonjo/kg-rag/blob/main/notebooks/ch04.ipynb。
让我们深入探讨。
列表 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)
如果我们执行这个示例,提示输出将看起来像这样:
说明:生成 Cypher 语句以查询图数据库以获取回答以下用户问题的数据。
图数据库模式:仅使用模式中提供的关系类型和属性。不要使用模式中未提供的任何其他关系类型或属性。节点属性:
Movie {tagline: STRING, title: STRING, released: INTEGER}
Person {born: INTEGER, name: STRING}
关系属性:
ACTED_IN {roles: LIST}
REVIEWED {summary: STRING, rating: INTEGER}
关系:
(:Person)-[:ACTED_IN]->(:Movie)
(:Person)-[:DIRECTED]->(:Movie)
(:Person)-[:PRODUCED]->(:Movie)
(:Person)-[:WROTE]->(:Movie)
(:Person)-[:FOLLOWS]->(:Person)
(:Person)-[:REVIEWED]->(:Movie)
术语映射:本节有助于在用户问题和图数据库模式之间映射术语。
人物:当用户询问像演员、作家、导演、制片人或评论家这样的职业人物时,他们指的是带有标签'Person'的节点。电影:当用户询问电影或影片时,他们指的是带有标签 Movie 的节点。
示例:以下示例提供了查询图数据库的有用模式。问题:哪两位演员共同出演了最多的电影?
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
格式说明:在您的回答中不要包含任何解释或道歉。不要回答任何可能要求您构建 Cypher 语句之外的问题。不要包含任何除生成的 Cypher 语句之外的文字。只回答 CYPHER——不要使用代码块。
用户问题:谁执导的电影最多?
使用这个提示,我们现在可以生成用户问题的 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 优化的(微调的)LLM
在 Neo4j,我们正在通过微调不断改进 text2cypher 的 LLM 性能。我们 Hugging Face 上的开源训练数据可在huggingface.co/datasets/neo4j/text2cypher找到。我们还提供基于开源 LLM(如 Gemma2、Llama 3.1)的微调模型,可在huggingface.co/neo4j找到。
这些模型在性能上仍然远远落后于像最新 GPT 和 Gemini 模型这样的微调大型模型,但它们效率更高,可以在大型模型太慢的生产系统中使用。大胆尝试它们,并参考少量示例、模式、术语映射和格式说明来提高模型性能。有关我们的微调过程和学习,更多信息请参阅mng.bz/MwDW、mng.bz/a9v7和mng.bz/yNWB。
4.6 我们学到的东西以及 text2cypher 能做什么
在本章的代码和信息的基础上,您应该能够为您的知识图谱实现一个 text2cypher 检索器。您应该能够让它为广泛的问题生成正确的 Cypher 查询,并通过提供少量示例、模式、术语映射和格式说明来提高其性能。
随着你识别出它难以应对的问题类型,你可以向提示中添加更多少样本示例,以帮助它学习如何生成正确的查询。随着时间的推移,你会发现生成的查询质量有所提高,检索器变得更加可靠。
摘要
-
查询语言生成与 RAG(Retrieval-Augmented Generation)管道很好地结合,作为其他检索方法的补充,尤其是在我们想要聚合数据或从图中获取特定数据时。
-
查询语言生成的有用实践包括使用少样本示例、模式、术语映射和格式说明。
-
我们可以使用基础模型实现一个文本到 Cypher 的检索器,并将提示结构化到 LLM 中。
-
我们可以使用专门(微调)的 LLM(大型语言模型)进行文本到 Cypher 的转换,并提高其性能。
第五章:代理 RAG
本章涵盖
-
代理 RAG 是什么
-
我们为什么需要代理 RAG
-
如何实现代理 RAG
在前面的章节中,我们看到了如何使用不同的向量相似度搜索方法来查找相关数据。使用相似度搜索,我们可以在非结构化数据源中找到相关数据,但具有结构的数据往往比非结构化数据更有价值,因为结构本身包含信息。
向数据添加结构可以是一个逐步的过程。我们可以从一个简单的结构开始,然后随着进行添加更复杂的结构。我们在上一章中看到了这一点,我们从一个简单的图数据开始,然后向其中添加了更复杂的结构。
代理 RAG 系统(见图 5.1)是一个提供多种检索代理的系统,这些代理可以检索回答用户问题所需的数据。代理 RAG 系统的起始界面通常是一个检索路由器,其任务是找到最适合执行当前任务的检索器(或检索器)。
实现代理 RAG 系统的一种常见方式是利用 LLM 使用工具的能力(有时称为 函数调用)。并非所有 LLM 都具备这种能力,但 OpenAI 的 GPT-3.5 和 GPT-4 就有,这就是我们在本章中要使用的。这可以通过大多数 LLM 使用 ReAct 方法(见 arxiv.org/abs/2210.03629)来实现,但随着时间的推移,目前的趋势是这一功能将适用于所有 LLM。

图 5.1 使用代理 RAG 的应用程序数据流
5.1 什么是代理 RAG?
代理系统在复杂性和复杂性方面各不相同,但核心思想是系统能够代表用户执行任务。在本章中,我们将探讨一个基本的代理系统,其中系统只需选择使用哪个检索器,并决定找到的上下文是否回答了问题。在更高级的系统中,系统可能会制定计划执行什么类型的任务来解决当前任务。从本章的基本内容开始是一个理解代理系统核心概念的好方法,对于 RAG 任务,这通常就是你所需要的。
代理 RAG 是一个系统,其中提供了多种检索代理来检索回答用户问题所需的数据。成功的代理 RAG 系统需要几个基础部分:
-
检索路由器 — 一个接收用户问题并返回最佳检索器(或检索器)的功能
-
检索代理 — 实际上可以用来检索回答用户问题所需数据的检索器
-
答案评论员 — 一个接收检索器答案并检查原始问题是否得到正确回答的功能
5.1.1 检索代理
检索代理是实际用于检索回答用户问题所需数据的检索器。这些检索器可以是非常广泛的,例如向量相似度搜索,或者非常具体的,例如一个硬编码的数据库查询模板,它接收参数,例如第 5.1.2 节中提到的检索路由器。
在大多数代理 RAG(检索增强生成)系统中,一些通用的检索代理是相关的,如向量相似度搜索和 text2cypher。前者适用于非结构化数据源,后者适用于图数据库中的结构化数据,但在现实世界的生产系统中,要使任何这些达到用户期望的水平并不简单。
正因如此,我们需要专门的检索器,它们非常狭窄但执行得非常好。随着我们识别出通用检索器在生成查询以回答问题时遇到的问题,这些专门的检索器可以随着时间的推移而构建。
5.1.2 检索路由器
为了选择适合工作的正确检索器,我们有一个叫做检索路由器的机制。检索路由器是一个函数,它接收用户问题并返回最佳检索器。路由器如何做出这个决定可能有所不同,但通常使用 LLM 来做出这个决定。
假设我们有一个类似“法国的首都是什么?”的问题,并且我们编写了两个可用的检索代理(这两个代理都从数据库中检索答案):
-
capital_by_country——一个接收国家名称并返回该国家首都的检索器 -
country_by_capital——一个接收首都名称并返回该首都所在国家的检索器
这两个检索器都可以是硬编码的数据库查询,接收国家或首都作为参数。
检索路由器可以是一个 LLM(大型语言模型),它接收用户问题并返回最佳检索器。在这种情况下,LLM 可以返回带有"France"(法国)作为提取参数的capital_by_country检索器。因此,实际调用检索器的代码将是capital_by_country("France")。
这是一个简单的例子,但在现实世界的场景中,可能会有许多检索器可用。检索路由器可能是一个复杂的函数,它使用 LLM 来选择最适合工作的最佳检索器。
5.1.3 回答批评者
回答批评者是一个函数,它接收检索器的答案并检查原始问题是否被正确回答。回答批评者是一个阻塞函数,如果答案不正确或不完整,它可以阻止答案返回给用户。
如果一个不完整或不正确的答案被阻止,答案批评者应生成一个新问题,该问题可用于检索正确答案,并进入另一轮检索正确答案。可能的情况是正确答案在数据源中不可用,因此需要从这个循环中设置一些退出标准;答案批评者应能够处理这种情况,并在这种情况下向用户返回消息,告知答案不可用。
5.2 为什么我们需要代理 RAG?
代理 RAG 有用的一个领域是我们有多种数据源,并且希望为工作使用最佳数据源。另一个常见用途是当数据源非常广泛或复杂,我们需要专门的检索器来一致地检索所需数据时。
如本书前面所述,通用的检索器,如向量相似度搜索,可以在非结构化数据源中找到相关数据。当我们有如图数据库这样的结构化数据源时,我们可能会使用在第四章中介绍的通用检索器,如 text2cypher。如果数据非常复杂,像 text2cypher 这样的工具在生成正确查询时可能会遇到问题。在这种情况下,可以使用专门的检索器来检索正确数据。例如,这可能是一个窄范围的 text2cypher 检索器或一个硬编码的数据库查询,该查询接受参数。
随着时间的推移,我们可以识别出像 text2cypher 这样的工具在生成查询以回答问题时遇到的问题,并为这些问题构建专门的检索器,并将 text2cypher 作为没有良好特定检索器匹配情况下的通用检索器使用。
这就是代理 RAG 可以发挥作用的地方。有多种检索器可供选择,我们需要为工作选择最佳的检索器,并在将其返回给用户之前评估答案。在生产环境中,这非常有用,可以保持系统性能高和答案质量一致。
5.3 如何实现代理 RAG
在本节中,我们将介绍如何实现代理 RAG 系统的基本部分。您可以直接在附带的 Jupyter 笔记本中跟随实现,笔记本地址如下:github.com/tomasonjo/kg-rag/blob/main/notebooks/ch05.ipynb。
注意:在本章的实现中,我们使用我们所说的“电影数据集”。有关数据集的更多信息以及各种加载方式,请参阅附录。
5.3.1 实现检索工具
在我们可以将用户输入路由到由正确检索器(s)处理之前,我们需要让检索器可供路由器选择。检索器可以是广泛的,如向量相似度搜索,也可以是非常具体的,如接受参数的硬编码数据库查询模板。
在这个实际示例中,我们将使用一个简单的检索器列表:两个使用 Cypher 模板通过标题和演员名称获取电影,以及一个使用 text2cypher 处理所有其他问题的检索器。如前所述,有用的检索器集合因系统而异,应根据需要随时间添加以提高应用程序的性能。
列表 5.1 可用的检索器工具
text2cypher_description = {
"type": "function",
"function": {
"name": "text2cypher",
"description": "Query the database with a user question. When other tools don't fit, fallback to use this one.",
"parameters": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "The user question to find the answer for",
}
},
"required": ["question"],
},
},
}
def text2cypher(question: str):
"""Query the database with a user question."""
t2c = Text2Cypher(neo4j_driver)
t2c.set_prompt_section("question", question)
cypher = t2c.generate_cypher()
records, _, _ = neo4j_driver.execute_query(cypher)
return [record.data() for record in records]
movie_info_by_title_description = {
"type": "function",
"function": {
"name": "movie_info_by_title",
"description": "Get information about a movie by providing the title",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "The movie title",
}
},
"required": ["title"],
},
},
}
def movie_info_by_title(title: str):
"""Return movie information by title."""
query = """
MATCH (m:Movie)
WHERE toLower(m.title) CONTAINS $title
OPTIONAL MATCH (m)<-[:ACTED_IN]-(a:Person)
OPTIONAL MATCH (m)<-[:DIRECTED]-(d:Person)
RETURN m AS movie, collect(a.name) AS cast, collect(d.name) AS directors
"""
records, _, _ = neo4j_driver.execute_query(query, title=title.lower())
return [record.data() for record in records]
movies_info_by_actor_description = {
"type": "function",
"function": {
"name": "movies_info_by_actor",
"description": "Get information about a movie by providing an actor",
"parameters": {
"type": "object",
"properties": {
"actor": {
"type": "string",
"description": "The actor name",
}
},
"required": ["actor"],
},
},
}
def movies_info_by_actor(actor: str):
"""Return movie information by actor."""
query = """
MATCH (a:Person)-[:ACTED_IN]->(m:Movie)
OPTIONAL MATCH (m)<-[:ACTED_IN]-(a:Person)
OPTIONAL MATCH (m)<-[:DIRECTED]-(d:Person)
WHERE toLower(a.name) CONTAINS $actor
RETURN m AS movie, collect(a.name) AS cast, collect(d.name) AS directors
"""
records, _, _ = neo4j_driver.execute_query(query, actor=actor.lower())
return [record.data() for record in records]
注意neo4j_driver和text2cypher是可以在本书的代码仓库中找到的实现导入。
注意:本书编写时,之前的检索器定义遵循 OpenAI 的工具格式。
我们需要小心描述检索器给 LLM 的方式。我们需要确保 LLM 理解检索器并能决定使用哪个检索器。参数的描述也非常重要,以便 LLM 能够正确调用检索器。
注意,LLM 不能实际调用您的检索器;它只能决定使用哪个检索器以及传递给检索器的参数。实际调用检索器需要由调用 LLM 的系统来完成,我们将在下一节中看到。
关于通用检索工具的说明
我们几乎总是包含在我们智能 RAG 系统中的通用检索工具是,当问题的答案已经在问题或其他上下文部分中给出时,将被调用的工具。这个工具通常是一个简单的函数,它从问题或上下文中提取答案并返回它。
一个例子可能是一个像“Dave Smith 的姓氏是什么?”这样的问题。这就是检索器工具可能的样子。
列表 5.2 已在上下文中提供答案的通用检索工具
answer_given_description = {
"type": "function",
"function": {
"name": "answer_given",
"description": "If a complete answer to the question is already provided in the conversation, use this tool to extract it.",
"parameters": {
"type": "object",
"properties": {
"answer": {
"type": "string",
"description": "The answer to the question",
}
},
"required": ["answer"],
},
},
}
def answer_given(answer: str):
"""Extract the answer from a given text."""
return answer
5.3.2 实现检索器路由器
检索器路由器是智能 RAG 系统的核心部分。其任务是接收用户问题并返回用于使用的最佳检索器。
在实现检索路由器时,我们将使用一个大型语言模型(LLM)来帮助我们完成任务。我们将向 LLM 提供一个检索器列表和用户问题,然后 LLM 将返回用于为每个问题找到答案的最佳检索器。为了简化,我们将使用具有官方工具/函数调用支持的 LLM,例如 OpenAI 的 GPT-4o。其他 LLM 也可以实现此功能,但实现方式可能不同。
在我们深入研究路由功能之前,我们需要查看一些必要的部分,以便能够成功构建一个智能 RAG 系统。这些部分包括
-
处理工具调用
-
持续查询更新
-
将问题路由到相关检索器
代表 LLM 处理工具调用
当 LLM 返回要使用的最佳检索器时,系统需要调用检索器。这可以通过一个接收检索器和参数并调用检索器的函数来完成。以下列表展示了该函数可能的样子。
列表 5.3 检索器调用函数
def handle_tool_calls(tools: dict[str, any], llm_tool_calls: list[dict[str, any]]):
output = []
if llm_tool_calls:
for tool_call in llm_tool_calls:
function_to_call = tools[tool_call.function.name]["function"]
function_args = json.loads(tool_call.function.arguments)
res = function_to_call(**function_args)
output.append(res)
return output
我们传递的tools是一个字典,其中键是工具的名称,值是实际要调用的函数。llm_tool_calls是一个 LLM 决定要使用的工具及其传递给工具的参数的列表。LLM 可以决定它想要对单个问题进行多次函数调用。llm_tool_calls参数的形状如下:
[
{
"function": {
"name": "answer_given",
"arguments": "{\"answer\": \"Dave Smith\"}"
}
}
]
持续查询更新
当我们稍后到达检索器路由器函数部分时,我们会看到我们将按顺序逐个将问题发送给 LLM。这是一个故意的选择,以便让 LLM 更容易单独处理每个问题,并使将问题路由到正确的检索器更容易。
将问题按顺序发送的一个额外好处是,我们可以使用前一个问题的答案来重写下一个问题。如果用户提出的问题依赖于前一个问题的答案,这可能很有用。
考虑以下示例:“谁赢得了最多的奥斯卡奖项,这个人还活着吗?”这个问题的重写可以是“谁赢得了最多的奥斯卡奖项?”以及“这个人还活着吗?”其中第二个问题依赖于第一个问题的答案。
因此,一旦我们得到了第一个问题的答案,我们希望用新的信息更新剩余的问题。这可以通过调用一个带有原始问题和检索器答案的查询更新器来完成。查询更新器会使用新的信息更新现有的问题。
列表 5.4 查询更新说明
query_update_prompt = """
You are an expert at updating questions to make them more atomic, specific, and easier to find the answer to.
You do this by filling in missing information in the question, with the extra information provided to you in previous answers.
You respond with the updated question that has all information in it.
Only edit the question if needed. If the original question already is atomic, specific, and easy to answer, you keep the original.
Do not ask for more information than the original question. Only rephrase the question to make it more complete.
JSON template to use:
{
"question": "question1"
}
"""
查询更新器使用原始问题和检索器的答案被调用。输出是更新后的问题,我们指示 LLM 以 JSON 格式返回更新后的问题。重要的是 LLM 不要要求比原始问题更多的信息——只需重新措辞问题以使其更完整。
列表 5.5 查询更新函数
def query_update(input: str, answers: list[any]) -> str:
messages = [
{"role": "system", "content": query_update_prompt},
*answers,
{"role": "user", "content": f"The user question to rewrite: '{input}'"},
]
config = {"response_format": {"type": "json_object"}}
output = chat(messages, model = "gpt-4o", config=config, )
try:
return json.loads(output)["question"]
except json.JSONDecodeError:
print("Error decoding JSON")
return []
在此基础上,我们可以随着进程更新问题,并确保问题尽可能完整,并尽可能容易找到问题的答案。
路由问题
检索器路由的最后一部分实际上是路由问题到正确的检索器。这是通过调用 LLM 并传递问题和可用工具来完成的,LLM 将返回每个问题的最佳检索器。
首先,我们需要在我们的工具字典中提供我们的工具,这样我们就可以将它们传递给 LLM,同时当需要调用工具时也可以找到它们。让我们首先定义我们可用的工具。
列表 5.6 可用检索器工具字典
tools = {
"movie_info_by_title": {
"description": movie_info_by_title_description,
"function": movie_info_by_title
},
"movies_info_by_actor": {
"description": movies_info_by_actor_description,
"function": movies_info_by_actor
},
"text2cypher": {
"description": text2cypher_description,
"function": text2cypher
},
"answer_given": {
"description": answer_given_description,
"function": answer_given
}
}
在这里,我们将工具描述和实际功能分组到一个字典中,这样我们可以在需要实际调用工具时轻松找到它们。让我们开始向 LLM 的提示,其中我们描述其任务。
列表 5.7 检索路由器指令
tool_picker_prompt = """
Your job is to choose the right tool needed to respond to the user question.
The available tools are provided to you in the request.
Make sure to pass the right and complete arguments to the chosen tool.
"""
这是一个相当简短的提示,但足以指导 LLM 选择正确的检索器来完成工作,因为内置的工具/函数调用支持。接下来,我们将查看调用 LLM 的函数。
列表 5.8 检索路由器函数
def route_question(question: str, tools: dict[str, any], answers: list[dict[str, str]]):
llm_tool_calls = tool_choice(
[
{
"role": "system",
"content": tool_picker_prompt,
},
*answers,
{
"role": "user",
"content": f"The user question to find a tool to answer: '{question}'",
},
],
model = "gpt-4o",
tools=[tool["description"] for tool in tools.values()],
)
return handle_tool_calls(tools, llm_tool_calls)
此函数接收一个单独的问题、可用工具和前一个问题提供的答案。然后,它使用问题和工具调用 LLM,LLM 将返回用于问题的最佳检索器。函数的最后一行是调用我们之前看到的 handle_tool_calls 函数,该函数实际调用检索器。
检索路由器的最后一部分是将所有先前部分整合在一起,从用户输入到答案的全过程。我们想要确保有一个循环遍历所有问题,并在过程中更新问题以包含新的信息。
列表 5.9 代理 RAG 函数
def handle_user_input(input: str, answers: list[dict[str, str]] = []):
updated_question = query_update(input, answers)
response = route_question(updated_question, tools, answers)
answers.append({"role": "assistant", "content": f"For the question: '{updated_question}', we have the answer: '{json.dumps(response)}'"})
return answers
这里需要注意的是,handle_user_input 函数可以可选地接收一个答案列表。我们将在 5.3.3 节中讨论这一点。
在此基础上,我们拥有一个完整的代理 RAG 系统,该系统能够接收用户输入并返回答案。系统构建的方式允许根据需要扩展更多的检索器。
我们需要实现一个额外的部分来使系统完整,那就是答案批评家。
5.3.3 实现答案批评家
答案批评家的任务是接收所有来自检索器的答案,并检查原始问题是否得到正确回答。LLM 是非确定性的,在重写问题、更新问题和路由问题时可能会出错,因此我们希望设置这个检查以确保我们确实收到了所需答案。
以下列表显示了针对答案批评家的 LLM 指令。
列表 5.10 答案批评家指令
answer_critique_prompt = """
You are an expert at identifying if questions have been fully answered or if there is an opportunity to enrich the answer.
The user will provide a question, and you will scan through the provided information to see if the question is answered.
If anything is missing from the answer, you will provide a set of new questions that can be asked to gather the missing information.
All new questions must be complete, atomic, and specific.
However, if the provided information is enough to answer the original question, you will respond with an empty list.
JSON template to use for finding missing information:
{
"questions": ["question1", "question2"]
}
"""
我们遵循之前的模式,使用 JSON 格式和 LLM 的指令。
接下来,我们将查看调用 LLM 的函数。
列表 5.11 答案批评家函数
def critique_answers(question: str, answers: list[dict[str, str]]) -> list[str]:
messages = [
{
"role": "system",
"content": answer_critique_prompt,
},
*answers,
{
"role": "user",
"content": f"The original user question to answer: {question}",
},
]
config = {"response_format": {"type": "json_object"}}
output = chat(messages, model="gpt-4o", config=config)
try:
return json.loads(output)["questions"]
except json.JSONDecodeError:
print("Error decoding JSON")
return []
此函数接收原始问题和检索器提供的答案,并调用 LLM 检查原始问题是否得到正确回答。如果问题没有正确回答,LLM 将返回一系列可以提出的新问题,以收集缺失的信息。
如果我们收到一系列新问题,我们可以再次通过检索路由器来获取缺失信息。我们还应该设置一些退出标准,以避免陷入无法从检索器中获得原始问题答案的循环。
5.3.4 整合所有部分
到目前为止,我们已经实现了检索代理、检索路由器和答案批评家。最后一步是将所有这些部分整合到一个主函数中,该函数接收用户输入并返回答案,如果答案可用的话。
以下列表显示了主要功能可能的样子。让我们从对 LLM 的指令开始。
列表 5.12 代理型 RAG 主要指令
main_prompt = """
Your job is to help the user with their questions.
You will receive user questions and information needed to answer the questions
If the information is missing to answer part of or the whole question, you will say that the information
is missing. You will only use the information provided to you in the prompt to answer the questions.
You are not allowed to make anything up or use external information.
"""
非常重要的是,大型语言模型(LLM)在回答问题时只能使用其提示中提供的信息。这是为了保证系统的连贯性,以及我们能够信任它提供的答案。
接下来,我们将查看主要功能。
列表 5.13 代理型 RAG 主要功能
def main(input: str):
answers = handle_user_input(input)
critique = critique_answers(input, answers)
if critique:
answers = handle_user_input(" ".join(critique), answers)
llm_response = chat(
[
{"role": "system", "content": main_prompt},
*answers,
{"role": "user", "content": f"The user question to answer: {input}"},
],
model="gpt-4o",
)
return llm_response
主要功能将用户输入通过代理型 RAG 系统运行,并将答案返回给用户。如果答案不完整或不正确,评估功能将返回一系列新问题,这些问题可以用来收集缺失的信息。
我们只对答案进行一次评估;如果评估后答案仍然不完整或不正确,我们将原样返回答案给用户,并依赖 LLM 让用户知道哪些信息不完整。
摘要
-
代理型 RAG 是一个提供多种检索代理的系统,用于检索回答用户问题所需的数据。
-
代理型 RAG 系统的主要界面通常是某种用例或检索路由器,其任务是找到最适合执行当前任务的检索器(或检索器组)。
-
代理型 RAG 系统的基本部分包括检索代理、检索路由器和答案评估器。
-
代理型 RAG 系统的主要部分可以使用具有工具/函数调用支持的 LLM 来实现。
-
检索代理可以是通用的或专门的,应根据需要逐步添加,以改善应用程序的性能。
-
答案评估器是一个函数,它接收检索器提供的答案,并检查原始问题是否得到正确回答。
第六章:使用 LLM 构建知识图谱
本章涵盖
-
结构化数据提取
-
提取的不同方法
在本章中,您将探索使用 LLM 从非结构化来源,如文本文档,构建知识图谱的过程。重点将放在 LLM 如何从原始文本中提取和结构化数据,将其转换为构建知识图谱的可使用格式。
在前面的章节中,您学习了文档块分割、嵌入和检索的基本技术(第二章),以及提高检索准确性的更高级方法(第三章)。然而,正如您在第四章中学到的,在需要将数据进行结构化以回答需要过滤、计数或聚合操作的问题的情况下,仅依赖文本嵌入会导致挑战。为了解决仅使用文本嵌入的局限性,您将学习如何使用 LLM 将非结构化数据转换为适合知识图谱构建的结构化格式,进行自动数据提取。到本章结束时,您将能够从原始文本中提取结构化信息,为提取的数据设计知识图谱模型,并将这些数据导入图数据库。
您将从探索法律文件检索中一个常见的挑战——管理多个合同及其条款——开始,并了解结构化数据提取如何提供解决方案。在整个章节中,您将跟随示例,说明这个过程,并逐步引导您通过从非结构化文本构建知识图谱的工作流程。
6.1 从文本中提取结构化数据
在线找到的大部分信息,甚至在公司内部,都存在于各种文档等非结构化格式中。然而,在某些情况下,仅使用文本嵌入的简单检索技术不足以解决问题。法律文件就是一个例子。
例如,如果您在询问与 ACME 公司签订的合同中的付款条款,确保条款确实来自该特定合同,而不是其他合同,是至关重要的。当您简单地跨多个法律文件进行块分割和检索时,检索到的最上面的k个块可能来自不同的、无关的文档,导致混淆,如图 6.1 所示。

图 6.1 基本的向量检索策略可能会从各种合同中返回块。
图 6.1 说明了合同文档是如何被分解成文本块并使用文本嵌入进行索引的。当最终用户提出特定问题,例如关于某个特定合同的付款条款时,系统会检索最相关的文本块。然而,如果多个合同包含不同的付款条款,检索过程可能会无意中从多个文档中提取信息,将目标合同的有关部分与来自其他合同的无关部分混合。这是因为系统专注于根据相似性检索排名最高的文本块,而并不总是区分这些块是否来自正确的合同。结果,包含“付款”或“条款”等关键词但属于不同合同的块可能会被包括在内,导致对条款的碎片化和不一致的看法。当 LLM 试图将这些混合块综合成一个连贯的答案时,这种混淆可能会产生,最终增加不准确或误导性信息的风险。
此外,考虑以下问题:我们目前与 ACME 公司有多少活跃合同?要回答这个问题,你首先需要根据合同的有效状态过滤所有合同,然后计算相关合同的数量。这类查询类似于传统的商业智能问题,而文本嵌入方法在这些方面存在不足。
文本嵌入主要是为了检索语义相似的内容,而不是处理过滤、排序或聚合数据等操作。要处理这些操作,需要结构化数据,因为仅凭文本嵌入本身并不适合这些操作。
对于某些领域,在实施 RAG 应用时,对数据进行结构化至关重要。幸运的是,由于 LLMs 对自然语言的深入理解,它们在从文本中提取结构化数据方面表现出色,这使得它们能够准确识别相关信息。它们可以通过特定的提示进行微调或引导,以定位和提取所需的数据点,将非结构化信息转换为表格或键值对等结构化格式。使用 LLMs 进行结构化数据提取在处理大量文档时尤其有用,因为手动识别和组织此类信息将非常耗时且劳动密集。通过自动化提取过程,LLMs 使企业能够将非结构化信息转换为可操作的、结构化数据,这些数据可以用于进一步分析或 RAG 应用。
假设您在一家公司作为软件工程师工作,并且您是负责构建一个能够根据公司法律文件回答问题的聊天机器人的团队的一员。由于这是一个大规模项目,团队被分为两组:一组专注于数据准备,另一组负责实施第四章和第五章中描述的检索系统。您被分配到数据准备团队,您的任务是处理法律文件并提取结构化信息。这些信息将被用于构建知识图谱,遵循图 6.2 中可视化的工作流程。

图 6.2 使用 LLM 提取结构化数据信息构建知识图谱
图 6.2 中可视化的工作流程从合同文档作为输入开始,使用 LLM 进行处理以提取结构化信息。在法律领域,您可以提取各种细节,例如涉及方、日期、条款等。在这里,结构化输出以 JSON 格式表示,然后这些结构化信息被存储在 Neo4j 中,它将作为法律聊天机器人数据检索的基础。
这两个示例突出了简单文本嵌入在处理特定、结构化查询时的局限性,例如在合同中询问付款条款或计算活跃协议。在这两种情况下,准确的答案需要结构化数据,而不是仅仅依赖于非结构化文本的语义相似性。在本章的剩余部分,我们将更深入地探讨如何有效地使用 LLM 从复杂文档中提取结构化数据,以及这种结构化输出在构建用于高级检索任务的可信知识图谱中的关键作用。为了跟上进度,您需要访问一个运行中的空白 Neo4j 实例。这可以是一个本地安装或云托管实例;只需确保它是空的。您可以直接在以下提供的 Jupyter 笔记本中查看实现:github.com/tomasonjo/kg-rag/blob/main/notebooks/ch06.ipynb。
让我们深入探讨。
6.1.1 结构化输出模型定义
从文本中提取结构化数据不是一个新想法;这多年来一直是数据处理中的一个关键任务。历史上,这个过程被称为信息提取,需要复杂的系统,通常依赖于多个机器学习模型协同工作。这些系统通常成本高昂且难以维护,需要一支由熟练工程师和领域专家组成的团队来确保它们正确运行。由于这些原因,只有拥有大量资源的组织才能负担得起实施此类解决方案。高昂的成本和技术壁垒使得许多企业和个人无法接触。然而,LLMs 的进步极大地简化了这一过程。如今,用户可以提示 LLM 提取结构化信息,而无需构建和训练多个模型,技术门槛大大降低。这种转变为结构化数据提取打开了广泛的应用场景。
使用 LLMs 提取结构化数据已成为如此常见的用例,以至于 OpenAI 在其 API 中引入了结构化输出功能,以简化并标准化这一过程。此功能允许开发者在事先定义期望的输出格式,确保模型的响应符合特定的结构。结构化输出不是一个单独的库;它是 OpenAI API 的内置功能,可以通过函数调用或模式定义来访问。例如,在 Python 中,开发者通常使用 Pydantic 等库来定义数据模式。然后,这些模式可以传递给模型,指导它产生符合指定格式的输出,如下面的列表所示。
列表 6.1 使用 Pydantic 库定义期望的输出
from pydantic import BaseModel
class CalendarEvent(BaseModel):
name: str
date: str = Field(..., description="The date of the event. Use yyyy-MM-dd format")
participants: list[str]
列表 6.1 中的CalendarEvent类代表了一种结构化的方式来捕捉关于事件的信息。它包括事件名称、事件发生的日期以及参与者列表。通过明确定义这些属性,它确保任何事件数据都符合这种结构,使得以可靠和一致的方式提取和处理事件信息变得更加容易。属性可用的类型有
-
字符串
-
数字
-
布尔
-
整数
-
对象
-
数组
-
枚举
-
anyOf
让我们考察一下date属性的定义。
列表 6.2 date属性
date: str = Field(..., description="The date of the event. Use yyyy-MM-dd format")
列表 6.2 中的代码提供了如何提取date属性数据的说明。将属性命名为date向模型发出信号,使其关注与日期相关的信息。通过使用str类型,我们指定提取的信息应以字符串形式表示,因为没有可用的本地日期时间类型。此外,description说明了期望的yyyy-MM-dd格式。这一步至关重要,因为尽管模型知道它正在处理字符串,但描述确保日期遵循特定的格式。没有这种指导,仅str类型可能不足以传达预期的输出结构。
结构化输出通过确保 LLM 的响应遵循预定义的架构,显著简化了开发过程。这减少了后处理和验证的需求,使得开发者能够专注于在系统中使用数据。该功能提供了类型安全,确保响应始终正确格式化,并消除了实现一致输出的复杂提示需求,使整个过程更加高效和可靠。
从法律文件中提取结构化输出的第一步是定义需要提取的合同数据模型。由于你不是法律专家,而是软件工程师,因此咨询具有领域知识的人来确定哪些信息最重要是重要的。此外,与最终用户交谈,了解他们想要回答的具体问题,可以提供宝贵的见解。
在这些初步讨论之后,你提出了以下列表所示的合同数据模型。
列表 6.3 使用 Pydantic 对象定义期望的输出
class Contract(BaseModel):
"""
Represents the key details of the contract. #1
"""
contract_type: str = Field(
...,
description="The type of contract being entered into.",
enum=contract_types, #2
)
parties: List[Organization] = Field( #3
...,
description="List of parties involved in the contract, with details of each party's role.",
)
effective_date: str = Field(
...,
description="The date when the contract becomes effective. Use yyyy-MM-dd format.", #4
)
term: str = Field(
...,
description="The duration of the agreement, including provisions for renewal or termination.",
)
contract_scope: str = Field(
...,
description="Description of the scope of the contract, including rights, duties, and any limitations.",
)
end_date: Optional[str] = Field( #5
...,
description="The date when the contract becomes expires. Use yyyy-MM-
↪ dd format.",
)
total_amount: Optional[float] = Field(
..., description="Total value of the contract."
)
governing_law: Optional[Location] = Field(
..., description="The jurisdiction's laws governing the contract."
)
1 提取对象的描述
2 使用 enum 定义 LLM 可以使用的可能值
3 在本例中,属性可以是对象,如组织。
4 由于 datetime 类型不可用,你需要定义要提取的日期格式。
5 你可以使用 Optional 来定义可能不在所有合同中出现的属性。
类名Contract以及简洁的文档字符串“表示合同的详细信息”,为 LLM 提供了高级理解,即期望的输出应捕获关键合同信息。这指导模型专注于提取和组织关键细节,例如合同类型、相关方、日期和财务信息。
通常,属性可以分为强制性和可选性。当一个属性是可选的时,你用Optional类型来指定,这表示 LLM 该信息可能存在也可能不存在。当信息可能缺失时,标记属性为可选是至关重要的,否则,一些 LLM 可能会尝试填充空白而虚构值。例如,total_amount是可选的,因为一些合同仅仅是无货币交换的协议。相反,effective_date属性是强制性的,因为你期望每个合同都有一个起始日期。
注意,每个属性都包含一个description值,以向 LLM 提供清晰的指导,确保它准确提取所需信息。即使某些属性看似明显,这也是一个好习惯。在某些情况下,你可能还希望指定特定属性的允许值。你可以通过使用enum参数来实现这一点。例如,contract_type属性使用enum参数来告知 LLM 应用的具体类别。以下列表包含了contract_type参数的可用值。
列表 6.4 合同类型枚举值
contract_types = [
"Service Agreement",
"Licensing Agreement",
"Non-Disclosure Agreement (NDA)",
"Partnership Agreement",
"Lease Agreement"
]
显然,列表 6.4 并不详尽,因为还有其他选项可以包含在内。
一些属性可能更复杂,可以定义为自定义对象。例如,parties属性是一个Organization对象的列表。使用列表是因为合同通常涉及多个当事人,而自定义对象允许提取比特定属性简单的字符串更多的信息。以下列表中的代码定义了Organization对象。
列表 6.5 自定义Organization对象
class Organization(BaseModel):
"""
Represents an organization, including its name and location.
"""
name: str = Field(..., description="The name of the organization.")
location: Location = Field(
..., description="The primary location of the organization."
)
role: str = Field(
...,
description="The role of the organization in the contract, such as
↪ 'provider', 'client', 'supplier', etc.", #1
)
1 如果您没有提供所有可能的值,而是只提供示例,您可以在描述中提供可能的值而不是枚举。
列表 6.5 中的Organization对象捕捉了参与合同的组织的关键细节,包括其名称、主要位置和角色。location属性是一个嵌套的Location对象,允许我们将信息结构化到城市、州和国家等值。如您所见,我们可以有嵌套对象,但典型的建议是避免过多层级的嵌套对象以获得更好的性能。对于role属性,我们提供了“提供者”和“客户”等示例,但选择不使用枚举以避免限制值。这种灵活性很重要,因为确切的角色可能各不相同,并且并不完全可预测。通过这种方式定义组织,LLM 被引导提取关于参与方的更详细和结构化的信息。
最后,您需要定义Location对象。
列表 6.6 自定义Location对象
class Location(BaseModel):
"""
Represents a physical location including address, city, state, and country.
"""
address: Optional[str] = Field(
..., description="The street address of the location."
)
city: Optional[str] = Field(..., description="The city of the location.")
state: Optional[str] = Field(
..., description="The state or region of the location."
)
country: str = Field(
...,
description="The country of the location. Use the two-letter ISO standard.", #1
)
1 LLM 熟悉用于国家的 ISO 标准,因此您可以指示模型根据特定标准标准化值。
Location对象代表一个物理地址,它捕捉了诸如街道地址、城市、州或地区以及国家等详细信息。除了country属性外,所有属性都是可选的,这允许在完整的位置详细信息可能不可用的情况下具有灵活性。对于country属性,我们指导 LLM 使用两字母的 ISO 标准,以确保一致性,并使其在不同系统之间的工作和加工更加容易。这种结构使得 LLM 能够在必要时提取标准化的、可用的信息,同时允许存在不完整或部分数据。
您现在已定义了合同数据模型,该模型可用于从公司的合同中提取相关信息。此模型将作为指导 LLM 进行结构化数据提取的蓝图。在明确了解数据结构的基础上,现在是时候探索如何有效地提示 LLM 提取这些信息了。
6.1.2 结构化输出提取请求
在定义了合同数据模型后,您现在有一个数据定义,LLM 可以遵循以提取结构化信息。下一步是确保 LLM 确切地了解如何以一致格式输出这些数据。这正是 OpenAI 的 Structured Outputs 功能发挥作用的地方。通过使用此功能,您可以引导 LLM 的行为,使其输出严格遵循合同模型的数据,同时使用在前面章节中引入的相同聊天模板。
结构化输出文档(mng.bz/oZZp)使用系统消息来额外引导 LLM(大型语言模型)专注于当前任务。通过使用以下列表中所示的系统消息,您可以提供明确的指令以有效地引导模型的行为。
列表 6.7 结构化输出提取的系统消息
system_message = """
You are an expert in extracting structured information from legal documents and contracts.
Identify key details such as parties involved, dates, terms, obligations, and legal definitions.
Present the extracted information in a clear, structured format. Be concise, focusing on essential
legal content and ignoring unnecessary boilerplate language. The extracted data will be used to address
any questions that may arise regarding the contracts."""
提供精确指令以构建理想的系统消息很困难。明确的是,您应该定义领域并为 LLM 提供有关输出将如何使用的上下文。除此之外,这通常归结为试错。
最后,您定义一个函数,该函数接受任何文本作为输入,并输出一个根据合同数据模型定义的字典。
列表 6.8 结构化输出提取的系统消息
def extract(document, model="gpt-4o-2024-08-06", temperature=0):
response = client.beta.chat.completions.parse(
model=model,
temperature=temperature,
messages=[
{"role": "system", "content": system_message}, #1
{"role": "user", "content": document}, #2
],
response_format=Contract, #3
)
return json.loads(response.choices[0].message.content)
1 将系统消息作为第一条消息传递
2 将文档作为用户消息传递,不附加任何额外指令。
3 使用 response_format 参数定义输出格式。
列表 6.8 中的 extract 函数处理一个文本文档,并根据合同数据模型返回一个字典。该函数利用了撰写时最新的 GPT-4o 模型,该模型支持结构化输出。该函数发送一个系统消息以引导 LLM,然后是未经修改的原始用户提供的文档文本。然后根据Contract数据模型格式化响应,并以字典形式返回。
为了看到这个过程在实际中的应用,现在让我们看看如何使用真实世界的数据集应用这种方法。由于保密性,访问专有合同可能很困难,因此您将使用一个名为 Contract Understanding Atticus Dataset(CUAD)的公共数据集。
6.1.3 CUAD 数据集
虽然所有公司都有合同和法律文件,但由于其中包含的信息具有敏感性,这些文件通常不会公开。为了演示目的,我们将使用 CUAD 数据集(Hendrycks 等人,2021 年)中的一个文本文件。CUAD 是为训练 AI 模型理解和审查法律合同而创建的专业语料库。
以下列表显示了一个改进版本。合同可在本书的配套 GitHub 存储库中找到,从而消除了下载整个数据集的需要。代码处理打开文件和读取其内容。
列表 6.9 读取合同文本文件
with open('../data/license_agreement.txt', 'r') as file:
contents = file.read() #1
1 读取文件
您现在可以通过执行以下列表中所示的代码来处理合同。
列表 6.10 从文本中提取结构化信息
data = extract(contents)
print(data)
结果将类似于以下列表。
列表 6.11 提取结果
{'contract_type': 'Licensing Agreement',
'parties': [{'name': 'Mortgage Logic.com, Inc.',
'location': {'address': 'Two Venture Plaza, 2 Venture',
'city': 'Irvine',
'state': 'California',
'country': 'US'},
'role': 'Client'},
{'name': 'TrueLink, Inc.',
'location': {'address': '3026 South Higuera',
'city': 'San Luis Obispo',
'state': 'California',
'country': 'US'},
'role': 'Provider'}],
'effective_date': '1999-02-26',
'term': "1 year, with automatic renewal for successive one-year periods unless terminated with 30 days' notice prior to the end of the term.",
'contract_scope': 'TrueLink grants Mortgage Logic.com a nonexclusive license to use the Interface for origination, underwriting, processing, and funding of consumer finance receivables. TrueLink will provide hosting services, including storage, response time management, bandwidth, availability, access to usage statistics, backups, internet connection, and domain name assistance. TrueLink will also provide support services and transmit credit data as permitted under applicable agreements and laws.',
'end_date': None,
'total_amount': None,
'governing_law': {'address': None,
'city': None,
'state': 'California',
'country': 'US'}}
提取的合同数据被组织到结构化字段中,尽管并非所有属性都完全填充。例如,一些字段如end_date(结束日期)和total_amount(总金额)被标记为None,表示信息缺失或未指定。同时,如contract_scope(合同范围)这样的属性包含更详细、描述性的文本,概述了协议的操作细节,如提供的服务和责任。结构包括对涉及各方、他们的角色和位置的清晰分解。合同还指定了其开始日期和续约条件,但其他财务或终止细节仍然未定义,因为它们在合同中缺失。
练习 6.1
下载 CUAD 数据集并探索基于不同类型合同的创建各种合同数据模型。一旦你定义了不同的模型,你可以通过分析它们如何捕捉和分类合同中的关键法律信息来测试和改进它们。
在本节中,你成功使用 CUAD 数据集和之前定义的合同数据模型从合同文档中提取了结构化数据。LLM 被引导识别关键合同细节,结果以结构化的方式格式化,使你能够组织重要信息,如合同类型、各方和条款。这个过程展示了 LLM 如何有效地将非结构化法律文件转换为可操作的数据。
现在你已经看到了如何从法律合同中提取结构化信息,下一节将重点介绍如何将此数据集成到知识图谱中。
6.2 构建图
作为本章的最后一步,你将把提取的结构化输出导入 Neo4j。这遵循了导入结构化数据的标准方法。首先,你应该设计一个合适的图模型,以表示你的数据中的关系和实体。图建模超出了本书的范围,但你可以使用 LLM 来帮助定义图模式或查看其他学习材料,如 Neo4j 图学院。
合同图模型的示例如图 6.3 所示,你将在本步骤中使用它。图模型表示一个合同系统,包含三个主要实体:Contract(合同)、Organization(组织)和Location(位置)。Contract节点存储诸如其 ID、类型、生效日期、期限、总金额、管辖法律和范围等详细信息。
组织通过HAS_PARTY关系与合同相连接,每个组织都有一个到Location节点的HAS_LOCATION关系,它捕捉组织的地址、城市、州和国家。位置作为单独的节点表示,以适应单个组织可能有多个地址的可能性。
现在你已经定义了图模型,下一步是开始构建知识图谱的过程。这涉及到几个关键步骤,每个步骤将在以下子节中详细说明。首先,你将定义唯一约束和索引以确保数据完整性并提高性能。之后,你将使用 Cypher 语句将结构化合约数据导入 Neo4j。一旦数据加载,你将可视化图谱以确认所有实体和关系都正确表示。最后,我们将讨论重要的数据精炼任务,例如实体解析,这确保了同一现实世界实体的不同表示被正确合并,并且我们将简要介绍如何在图中处理结构化和非结构化数据。

图 6.3 合约图模型
6.2.1 数据导入
在适用的地方定义唯一约束和索引是一种最佳实践,因为它不仅确保了图谱的完整性,还增强了查询性能。以下列表中的代码定义了Contract、Organization和Location节点的唯一约束。
列表 6.12 定义唯一约束
neo4j_driver.execute_query(
"CREATE CONSTRAINT IF NOT EXISTS FOR (c:Contract) REQUIRE c.id IS UNIQUE;"
)
neo4j_driver.execute_query(
"CREATE CONSTRAINT IF NOT EXISTS FOR (o:Organization) REQUIRE o.name IS UNIQUE;"
)
neo4j_driver.execute_query(
"CREATE CONSTRAINT IF NOT EXISTS FOR (l:Location) REQUIRE l.fullAddress IS UNIQUE;"
)
接下来,你需要准备一个导入 Cypher 语句,该语句将字典输出加载到 Neo4j 中,遵循图 6.3 中概述的图模式。导入 Cypher 语句如下所示。
列表 6.13 定义导入 Cypher 语句
import_query = """WITH $data AS contract_data
MERGE (contract:Contract {id: randomUUID()}) #1
SET contract += {
contract_type: contract_data.contract_type,
effective_date: contract_data.effective_date,
term: contract_data.term,
contract_scope: contract_data.contract_scope,
end_date: contract_data.end_date,
total_amount: contract_data.total_amount,
governing_law: contract_data.governing_law.state + ' ' +
contract_data.governing_law.country
}
WITH contract, contract_data
UNWIND contract_data.parties AS party #2
MERGE (p:Organization {name: party.name})
MERGE (loc:Location {
fullAddress: party.location.address + ' ' +
party.location.city + ' ' +
party.location.state + ' ' +
party.location.country})
SET loc += {
address: party.location.address,
city: party.location.city,
state: party.location.state,
country: party.location.country
}
MERGE (p)-[:LOCATED_AT]->(loc) #3
MERGE (p)-[r:HAS_PARTY]->(contract) #4
SET r.role = party.role
"""
1 使用随机 UUID 作为唯一标识符创建合约节点
2 创建党派节点及其位置
3 将党派与其位置链接
4 将党派与合约链接
解释 Cypher 语句,如列表 6.13 中的语句,超出了本书的范围。然而,如果你需要帮助,LLMs 可以帮助澄清细节并提供对 Cypher 语句的更深入理解。然而,我们想强调的是,由于使用了randomUUID()来生成合约 ID,列表 6.13 中的查询不是幂等的。因此,多次运行查询将在数据库中创建重复的合约条目,每个条目都有一个唯一的 ID。
现在一切准备就绪,你可以执行以下列表中的代码,将合约导入 Neo4j。
列表 6.14 将合约数据导入 Neo4j
neo4j_driver.execute_query(import_query, data=data)
导入成功后,你可以打开 Neo4j 浏览器来探索生成的图谱,它应该与图 6.4 中显示的可视化非常相似。

图 6.4 合约图数据可视化
图 6.4 中的可视化展示了一个图谱,其中中心“许可协议”(代表合约)通过HAS_PARTY关系与两个组织“Mortgage Logic.com, Inc.”和“TrueLink, Inc.”相连。每个组织进一步通过LOCATED_AT关系连接到一个代表其位置的“US”节点。
6.2.2 实体解析
您已成功导入图,但您的工作还没有完成。在大多数情况下,尤其是在处理自然语言处理或 LLM 驱动的数据处理时,某些程度的数据清理是必要的。在这个清理过程中,实体识别是最关键的一步。实体识别是指在一个数据集或知识图中识别和合并同一现实世界实体的不同表示的过程。当处理大型且多样化的数据集时,由于拼写变化、不同的命名规范或数据格式中的微小差异等原因,同一实体可能以多种形式出现是很常见的,如图 6.5 所示,我们看到了代表同一实体变体的三个节点。这三个名称是
-
UTI 资产管理公司
-
UTI 资产管理公司有限公司
-
UTI 资产管理有限公司

图 6.5 潜在重复项
在这种情况下,实体识别涉及识别所有这些变体都指的是同一现实世界组织,尽管在命名规范(如“Limited”与“Ltd”)上存在细微差异。实体识别的目标是将这些不同的引用统一到图中的一个单一、连贯的节点中。这不仅提高了数据完整性,还增强了图进行更准确推理和关系的能力。实体识别中使用的技巧包括字符串匹配、聚类算法,甚至使用每个实体周围上下文来检测和解决重复的机器学习方法。
需要注意的是,实体识别高度依赖于具体用例和领域。一个通用的、一刀切解决方案很少能奏效,因为每个领域都有自己的命名规范、数据架构以及实体表示的细微差别。例如,在金融数据集中解决组织的方法和阈值可能在对医疗保健环境中的生物实体进行处理时产生次优结果。因此,最有效的策略之一是开发特定领域的本体或规则,以反映您的特定数据环境。此外,使用领域专家来定义匹配标准,并使用迭代反馈循环——其中潜在的匹配项被验证或修正——可以大大提高准确性。通过结合领域专业知识与上下文感知的机器学习或聚类技术,您可以开发出更稳健和灵活的实体识别方法。这将确保您捕捉到在您独特的数据环境中最重要的细微细节。
6.2.3 将非结构化数据添加到图中
知识图谱越来越多地被用来存储结构化和非结构化数据,随着 LLMs(大型语言模型)的出现,这种场景变得更加普遍。在这种情况下,LLMs 可以用来从非结构化来源,如文本文档中提取结构化数据。然而,在图中存储原始的非结构化文档和提取的结构化数据,既保留了原始数据的丰富性,又使得对提取信息的查询和分析更加精确。图 6.6 展示了将结构化和非结构化信息结合的扩展图模式。

图 6.6 增强的图模型,包含非结构化数据
当将非结构化数据纳入图中时,通常使用基于标记计数或词长的基础分块策略来将文本分割成可管理的段。虽然这种朴素的方法适用于通用用例,但某些领域,如法律合同,则受益于更专业的分块方法。例如,通过条款分割合同可以保留其语义结构,并提高下游分析的质量。这种更智能的方法允许图捕获更有意义的关系,从而实现更深入的见解和更准确的推断。
本章已指导您使用 LLMs 从非结构化数据构建知识图谱。您探讨了文本嵌入在处理结构化查询方面的局限性,并学习了结构化数据提取是如何提供解决方案的。通过定义数据模型、提示 LLMs 进行提取以及将结果导入图数据库,您了解了如何将原始文本转换为知识图谱的可用数据。此外,我们还涵盖了实体解析和结合结构化和非结构化数据以获得更深入见解的关键任务。有了这些知识,您现在可以在实际场景中应用结构化数据提取。
摘要
-
简单地对文档进行分块以进行检索可能会导致不准确或混合的结果,尤其是在法律文件等文档边界重要的领域。
-
过滤、排序和聚合等检索任务需要结构化数据,因为仅凭文本嵌入不适合此类操作。
-
LLMs 在从非结构化文本中提取结构化数据,并将其转换为表格或键值对等可用格式方面非常有效。
-
LLMs 中的结构化输出特征允许开发者定义模式,确保响应遵循特定的格式,并减少后处理的需求。
-
定义一个清晰的数据模型,具有合同类型、各方和日期等属性,对于指导 LLMs 准确提取相关信息至关重要。
-
在知识图谱中进行实体解析对于合并同一实体的不同表示非常重要,这有助于提高数据的一致性和准确性。
-
在知识图谱中将结构化和非结构化数据相结合,既保留了原始资料的丰富性,又使得查询更加精确。
第七章:微软的 GraphRAG 实现
本章涵盖
-
介绍微软的 GraphRAG
-
提取和总结实体及其关系
-
计算和总结实体社区
-
实施全局和局部搜索技术
在第六章中,你学习了如何从法律文件中提取结构化信息来构建知识图谱。在本章中,你将探索使用微软的 GraphRAG(Edge 等人,2024)方法的一个略有不同的提取和处理管道。这个端到端示例仍然构建知识图谱,但更侧重于实体及其关系的自然语言摘要。整个管道在图 7.1 中进行了可视化。

图 7.1 微软的 GraphRAG 管道。(图片来自 Edge 等人,2024 年,CC BY 4.0 许可)
微软的 GraphRAG(MS GraphRAG:github.com/microsoft/graphrag)的一个关键创新是它使用一个 LLM 通过两阶段过程构建知识图谱。在第一阶段,从源文档中提取和总结实体及其关系,形成知识图谱的基础,如图 7.1 中的步骤所示。MS GraphRAG 的独特之处在于,一旦知识图谱构建完成,就会检测图社区,并为紧密相关的实体组生成特定领域的总结。这种分层方法将来自各种文本片段的碎片化信息转化为关于指定实体、关系和社区的信息的连贯和组织化表示。
这些实体和社区层面的总结可以用于在 RAG 应用中响应用户查询提供相关信息。拥有这样一个结构化的知识图谱,可以应用多种检索方法。在本章中,你将探索 MS GraphRAG 论文中描述的全局和局部搜索检索方法。
7.1 数据集选择
MS GraphRAG 旨在通过提取关键实体并生成连接多个文本片段信息的摘要来处理非结构化文本文档。为了确保有意义的见解,我们的数据集不仅应该包含丰富的实体信息,还应该包含跨多个片段的实体数据。由于实体类型是 MS GraphRAG 的可配置方面之一,因此必须在事先定义。相关的实体通常包括人、组织和地点,但也可以扩展到医学中的基因和通路或法律中的法律条款等特定领域的概念。
为了对实体类型做出明智的决策,探索数据集并确定你想要回答的问题类型非常重要。实体类型的选择决定了整个下游过程,影响提取、链接和总结的质量。
例如,MS GraphRAG 论文使用了播客和新闻文章的数据集。在两种情况下,人们、组织、地点等实体都经常被提及。此外,根据主题,如游戏或健康生活方式播客,你可能还想包括特定领域的实体,如游戏标题、健康状况或营养概念,以确保全面提取和分析。
在这里,我们使用 《奥德赛》 来评估 MS GraphRAG,因为它包含丰富的叙事,涉及人、神、神秘武器等等。此外,像尤利西斯这样的关键实体出现在多个文本块中,使其成为测试实体提取和跨块摘要的合适数据集。
在本章剩余部分,你将实现 MS GraphRAG 方法。为了跟上进度,你需要访问一个运行中的空白 Neo4j 实例。这可以是一个本地安装或云托管实例;只需确保它是空的。你可以在附带的 Jupyter 笔记本中直接跟随实现,该笔记本位于 github.com/tomasonjo/kg-rag/blob/main/notebooks/ch07.ipynb。
让我们开始吧。
7.2 图索引
在这里,你将构建知识图谱并生成实体和社区摘要。在整个构建过程中,你将探索每个步骤的关键考虑因素,包括实体选择、图连通性以及这些选择如何影响摘要和查询的质量。
首先,从古腾堡项目加载 《奥德赛》 (www.gutenberg.org/ebooks/1727)。
列表 7.1 加载《奥德赛》
url = "https://www.gutenberg.org/cache/epub/1727/pg1727.txt"
response = requests.get(url)
文本准备就绪后,你现在可以遍历 MS GraphRAG 管道。
7.2.1 块分割
《奥德赛》 由 24 本不同长度的书籍组成。你的第一个任务是移除前言和脚注,然后将文本分割成单独的书籍,如下面的列表所示。这种方法遵循叙事的自然分割,为文本提供了一种语义上有意义的结构化方式。
列表 7.2 移除前言和脚注并将文本分割成书籍
def chunk_into_books(text: str) -> List[str]:
return (
text.split("PREFACE TO FIRST EDITION")[2]
.split("FOOTNOTES")[0]
.strip()
.split("\nBOOK")[1:]
)
books = chunk_into_books(response.text)
现在,你需要检查每本书中的标记数,以确定是否需要进一步的块分割。以下列表中的代码提供了书籍标记计数的基本统计信息。
列表 7.3 计算书籍中的标记数
token_count = [num_tokens_from_string(el) for el in books]
print(
f"""There are {len(token_count)} books with token sizes:
- avg {sum(token_count) / len(token_count)}
- min {min(token_count)}
- max {max(token_count)}
"""
)
在 24 本书中的标记数差异很大,平均为 6,515 个标记,最小为 4,459,最大为 10,760。鉴于这个范围,进一步的块分割是必要的,以确保没有单个部分超过合理的标记限制。
但合理的块大小是多少呢?MS GraphRAG 背后的研究人员比较了不同的块大小,并分析了它们对提取实体总数的影响。这种比较的结果如图 7.2 所示。

图 7.2 分块大小和自我反思迭代对实体提取的影响。(图片来自 Edge 等人,2024 年,根据 CC BY 4.0 许可)
图 7.2 中的结果显示,较小的分块大小通常提取更多的实体引用。表示 600 个标记分块大小的线条始终是最高的,而 2,400 个标记分块大小是最低的。这表明将文本分成更小的块可以让 LLM 检测到比使用更大的块更多的实体。此外,图 7.2 还显示,增加自我反思迭代的次数,即在同一文档上的额外提取遍历,会导致所有分块大小下检测到的实体引用更多。这种模式表明重复遍历使 LLM 能够提取在早期迭代中可能被遗漏的更多实体。
假设你已经决定使用 1,000 字限制(基于空格分割)并重叠 40 个单词来分块书籍,如下所示。
列表 7.4 分块书籍
chunked_books = [chunk_text(book, 1000, 40) for book in books]
书籍已经分块,你可以进行下一步。
7.2.2 实体和关系提取
第一步是提取实体和关系。我们可以从他们论文的附录中借用 MS GraphRAG 提示。实体和关系提取提示的指令部分在“实体和关系提取指令”中显示。
实体和关系提取指令
-目标-
给定一个可能与此活动相关的文本文档和实体类型列表,从文本中识别出那些类型的所有实体以及识别出的实体之间的关系。
-步骤-
-
识别所有实体。对于每个识别的实体,提取以下信息:
-
entity_name: 实体的名称,首字母大写
-
entity_type: 以下类型之一:[{entity_types}]
-
entity_description: 实体属性和活动的全面描述
-
-
将每个实体格式化为("entity"{tuple_delimiter}<entity_name>{tuple_delimiter} <entity_type>{tuple_delimiter}<entity_description>)
-
- 从步骤 1 中识别的实体中,识别所有(源实体,目标实体)对,它们彼此明显相关。对于每一对相关实体,提取以下信息:
-
source_entity: 源实体的名称,如步骤 1 中识别的那样
-
target_entity: 目标实体的名称,如步骤 1 中识别的那样
-
relationship_description: 解释为什么你认为源实体和目标实体彼此相关
-
relationship_strength: 表示源实体和目标实体之间关系强度的数值分数
-
将每个关系格式化为("relationship"{tuple_delimiter}<source_entity> {tuple_delimiter}<target_entity>{tuple_delimiter}<relationship_description> {tuple_delimiter}<relationship_strength>)
-
- 以英文形式返回输出,作为步骤 1 和 2 中识别的所有实体和关系的单个列表。使用{record_delimiter}作为列表分隔符。
-
- 完成后,输出
实体和关系提取的说明侧重于通过识别指定类型的实体及其关系,从文本文档中提取结构化知识。实体类型的列表作为变量entity_types传入。提示指示 LLM 提取实体,按类型分类,并提供详细描述。然后,它识别明显相关的实体对,解释它们之间的关系,并分配关系强度分数。最后,它以结构化、分隔的格式返回所有提取的实体和关系。这只是完整提示的一部分,它还包括少量示例和输出示例,但那些内容过于广泛,无法包含在本书中。
练习 7.1
在运行提取之前,花点时间考虑对《奥德赛》来说哪些实体类型最有用。由于实体类型的列表必须预先定义,请考虑叙事的关键元素,如您想要提取的角色、地点、物体和事件。尝试定义一组实体类型,以捕捉文本中最有意义的关系。
要从《奥德赛》中提取有意义的实体,假设您已决定使用以下实体类型:
-
PERSON -
ORGANIZATION -
LOCATION -
GOD -
EVENT -
CREATURE -
WEAPON_OR_TOOL
一些实体类型,如PERSON和GOD,相对明确,因为它们指的是人类和神祇的明确类别。然而,其他类型,如EVENT和LOCATION,则更为模糊。一个EVENT可以指从单一动作到整个战争,这使得建立严格的分类边界变得困难。同样,LOCATION可以指一个广泛的类别,如一个国家、一个特定的城市,甚至是一个城市内的特定地点。这种可变性使得一致的分类更具挑战性,但也为 LLM 提供了更多的灵活性。
使用这些预定义的实体类型,您现在将实现提取函数。
列表 7.5 实体和关系提取
ENTITY_TYPES = ["PERSON", "ORGANIZATION", "LOCATION",
"GOD", "EVENT", "CREATURE", "WEAPON_OR_TOOL"]
def extract_entities(text: str) -> List[Dict]: #1
messages = [
{"role": "user",
"content": ch07_tools.create_extraction_prompt(ENTITY_TYPES, text)}, #2
]
output = chat(messages, model = "gpt-4o") #3
return ch07_tools.parse_extraction_output(output) #4
1 选择实体类型
2 将提取提示作为用户消息传递
3 LLM API 调用
4 将输出解析为字典
列表 7.5 中的代码通过首先定义要识别的实体类型来提取实体和关系。然后,它使用这些类型和输入文本生成一个提取提示,将提示发送到 LLM,并将响应处理成结构化的字典格式。
使用 7.5 列表中的函数,您将只为《奥德赛》的第一本书提取实体和关系。如果需要,您可以增加要分析的书籍数量以分析文本的更大部分。此提取的代码如下所示。
列表 7.6 提取实体和关系
number_of_books = 1
for book_i, book in enumerate( #1
tqdm(chunked_books[:number_of_books], desc="Processing Books") #1
): #1
for chunk_i, chunk in enumerate(tqdm(book, desc=f"Book {book_i}", leave=False)):
nodes, relationships = extract_entities(chunk) #2
neo4j_driver.execute_query( #3
ch07_tools.import_nodes_query,
data=nodes,
book_id=book_i,
text=chunk,
chunk_id=chunk_i,
)
neo4j_driver.execute_query( #4
ch07_tools.import_relationships_query,
data=relationships
)
1 定义要处理的书籍数量
2 提取实体和关系
3 导入实体
4 导入关系
列表 7.6 中的函数处理一组书籍,从每个片段中提取实体和关系。然后它将实体导入 Neo4j,接着是它们的关系,构建文本的结构化图表示。
首先回顾提取的实体和关系。您可以使用以下列表中的代码计算实体和关系的总数。
列表 7.7 计算提取的节点和关系数量
data, _, _ = neo4j_driver.execute_query(
"""MATCH (:`__Entity__`)
RETURN 'entity' AS type, count(*) AS count
UNION
MATCH ()-[:RELATIONSHIP]->()
RETURN 'relationship' AS type, count(*) AS count
"""
)
print([el.data() for el in data])
图中包含 66 个实体和 182 个关系,尽管这些数字可能在执行之间有所不同。MS GraphRAG 专注于提取实体及其关系的详细描述。例如,让我们检查对角色ORESTES提取的描述。
列表 7.8 检查生成的ORESTES描述
data, _, _ = neo4j_driver.execute_query(
"""MATCH (n:PERSON)
WHERE n.name = "ORESTES"
RETURN n.description AS description"""
)
print([el.data()['description'] for el in data])
当检查角色ORESTES的提取描述时,如列表 7.8 所示,结果可能如下所示:
-
奥雷斯特斯是阿伽门农的儿子,他杀死了埃癸斯托斯。
-
奥雷斯特斯是一个被期望向埃癸斯托斯复仇的人。
-
奥雷斯特斯因杀死埃癸斯托斯为父亲复仇而受到赞扬。
-
奥雷斯特斯是阿伽门农的儿子,他杀死了埃癸斯托斯。
-
奥雷斯特斯是一个被期望向埃癸斯托斯复仇的人。
-
奥雷斯特斯因杀死埃癸斯托斯为父亲复仇而受到赞扬。
虽然一些描述重复了相同的事实,但它们共同包含了所有关键细节,并确保在特定实体的不同文本片段中不会丢失重要信息。
类似地,一对实体可以有多种关系。您可以使用以下列表中的代码探索具有最多关系的实体对。
列表 7.9 检查生成的关系描述
data, _, _ = neo4j_driver.execute_query(
"""MATCH (n:__Entity__)-[:RELATIONSHIP]-(m:__Entity__)
WITH n,m, count(*) AS countOfRels
ORDER BY countOfRels DESC LIMIT 1
MATCH (n)-[r:RELATIONSHIP]-(m)
RETURN n.name AS source, m.name AS target, countOfRels, collect(r.description) AS descriptions
"""
)
print([el.data() for el in data])
具有最多关系的实体对是忒勒玛科斯和雅典娜,总共有 14 个关系。他们的互动贯穿了叙事的各个时刻,突出了雅典娜作为忒勒玛科斯的神圣向导和导师的角色。
以下是从中提取的五条关系描述:
-
忒勒玛科斯在宴会上悄悄地对雅典娜说话。
-
雅典娜化身为他人,向忒勒玛科斯提供建议和鼓励,给他勇气,并让他想起他的父亲。
-
雅典娜让忒勒玛科斯的母亲入睡,显示了她的神圣影响力。
-
雅典娜正在对忒勒玛科斯说话,为他提供指导和安慰。
-
雅典娜化身为门特斯,在门口受到忒勒玛科斯的欢迎。
虽然一些描述包含重叠的细节,但它们加强了雅典娜作为导师和神圣保护者的角色,逐渐塑造了忒勒玛科斯的旅程。
7.2.3 实体和关系摘要
为了避免提取知识中的不一致性、冗余和碎片化,MS GraphRAG 使用 LLM 合并同一实体或关系的多个描述以生成简洁的摘要。该模型不是单独处理每个描述,而是综合所有描述的信息,确保关键上下文细节在一个单一、丰富的表示中得以保留。这种方法提高了清晰度,减少了重复,并提供了对实体及其关系的更全面的理解。
再次强调,您可以像“实体和关系摘要说明”中所示的那样重用论文中的摘要提示。
实体和关系摘要说明
你是一个负责生成以下提供的数据的全面摘要的有用助手。给定一个或两个实体,以及一系列描述,所有这些都与同一个实体或实体组相关。请将这些描述合并成一个单一、全面的描述。确保包含从所有描述中收集的信息。如果提供的描述存在矛盾,请解决这些矛盾并提供一个单一、连贯的摘要。确保以第三人称撰写,并包含实体名称,以便我们有完整的背景信息。
#######
-数据-
实体:{entity_name}
描述列表:{description_list}
#######
输出:
“实体和关系摘要说明”中的提示指导 LLM 通过合并一个实体或一对实体的多个描述来生成一个单一、连贯的摘要。它确保包含所有相关细节,同时解决矛盾和消除冗余。输出以第三人称撰写,并明确命名实体以保持清晰和上下文。
使用“实体和关系摘要说明”中的提示,您可以生成具有多个描述的所有实体的摘要。摘要实体描述的代码可以在以下列表中找到。
列表 7.10 实体摘要
candidates_to_summarize, _, _ = neo4j_driver.execute_query(
"""MATCH (e:__Entity__) WHERE size(e.description) > 1 #1
RETURN e.name AS entity_name, e.description AS description_list"""
)
summaries = []
for candidate in tqdm(candidates_to_summarize, desc="Summarizing entities"):
messages = [ #2
{
"role": "user",
"content": ch07_tools.get_summarize_prompt(
candidate["entity_name"], candidate["description_list"]
),
},
]
summary = chat(messages, model="gpt-4o") #3
summaries.append(
{"entity": candidate["entity_name"], "summary": summary}
)
ch07_tools.import_entity_summary(neo4j_driver, summaries)
1 获取具有多个描述的实体
2 构建提示
3 生成实体摘要
列表 7.10 中的代码查询 Neo4j 数据库以找到具有多个描述的实体,然后使用 LLM 生成统一的摘要。您可以通过运行以下列表中的代码来查看ORESTES的摘要描述。
列表 7.11 检查ORESTOS生成的摘要
summary, _, _ = neo4j_driver.execute_query(
"""MATCH (n:PERSON)
WHERE n.name = "ORESTES"
RETURN n.summary AS summary""")
print(summary[0]['summary'])
结果显示在“ORESTES 生成的摘要”中。
ORESTES 生成的摘要
奥雷斯特斯是阿伽门农的儿子,以杀死埃癸斯托斯来为父亲的死亡报仇而闻名。他本应向埃癸斯托斯报仇,因为埃癸斯托斯是阿伽门农谋杀的凶手。奥雷斯特斯因履行这一期望并成功杀死埃癸斯托斯,他的父亲凶手,而受到赞扬。
摘要过程已成功生成一个连贯且丰富的实体描述,正如“ORESTES 的生成摘要”所示。通过合并多个描述,我们确保了关键细节得到保留,同时减少了冗余。
接下来,我们将应用相同的摘要方法到关系上,将多个关系描述合并成一个单一、全面的摘要。结果如下所示。
列表 7.12 关系摘要
rels_to_summarize, _, _ = neo4j_driver.execute_query(
"""MATCH (s:__Entity__)-[r:RELATIONSHIP]-(t:__Entity__) #1
WHERE id(s) < id(t)
WITH s.name AS source, t.name AS target,
collect(r.description) AS description_list,
count(*) AS count
WHERE count > 1
RETURN source, target, description_list"""
)
rel_summaries = []
for candidate in tqdm(rels_to_summarize, desc="Summarizing relationships"):
entity_name = f"{candidate['source']} relationship to {candidate['target']}"
messages = [ #2
{
"role": "user",
"content": ch07_tools.get_summarize_prompt(
entity_name, candidate["description_list"]
),
},
]
summary = chat(messages, model="gpt-4o") #3
rel_summaries.append({"source": candidate["source"], "target": candidate["target"], "summary": summary})
ch07_tools.import_rels_summary(neo4j_driver, summaries) #4
1 检索具有多个关系的节点对
2 构建提示
3 使用 LLM 生成关系摘要
4 将结果存储到 Neo4j
列表 7.12 中的代码识别数据库中共享多个关系的实体对,并使用 LLM 将它们的描述合并成一个摘要。通过合并关系描述,该过程确保了实体之间关键交互的全面捕捉,同时消除了冗余。一旦生成,摘要关系将存储回数据库。
你可以评估生成的TELEMACHUS和MINERVA之间的关系,如下所示。
列表 7.13 评估TELEMACHUS和MINERVA之间摘要的关系
data, _, _ = neo4j_driver.execute_query(
"""MATCH (n:__Entity__)-[r:SUMMARIZED_RELATIONSHIP]-(m:__Entity__)
WHERE n.name = 'TELEMACHUS' AND m.name = 'MINERVA'
RETURN r.summary AS description
"""
)
print(data[0]["description"])
列表 7.13 中的代码结果可以在“TELEMACHUS 和 MINERVA 之间关系的生成摘要”中找到。
TELEMACHUS 和 MINERVA 之间关系的生成摘要
Minerva 在 Telemachus 的生活中扮演着至关重要的角色,在他踏上寻找父亲 Ulysses 的旅程时提供指导和支持。在一次宴会上,Telemachus 安静地对 Minerva 说话,表明他们之间有着亲密和信任的关系。Minerva 经常以 Mentes 等伪装身份出现,向 Telemachus 提供建议和鼓励,在他心中灌输寻求父亲信息的勇气和决心。她就他打算的航行提供建议,展示了她对这一事业的承诺。此外,当 Minerva 让 Telemachus 的母亲入睡时,她的神圣影响显而易见,进一步展示了她在 Telemachus 生活中的保护和支持角色。
通过对实体和关系的综合摘要,你已经成功完成了 MS GraphRAG 索引的第一阶段。通过跨文本块合并信息,你创建了一个更连贯和丰富的提取知识表示。
实体和关系摘要的考虑因素
当处理大型数据集时,你可能会遇到所谓的超级节点。超级节点是在多个数据块中出现的实体,并且拥有大量的关系。例如,如果你要处理全部的古希腊历史,像“雅典”这样的节点就会积累大量的关系和描述。如果没有排名机制,总结这样的节点可能会导致输出过长,或者更糟糕的是,一些描述甚至可能无法完全包含在提示中。为了处理这种情况,你需要实现一个过滤或排名策略,以优先考虑最相关的描述,确保总结既简洁又具有信息性。
现在你可以继续进行下一阶段。
7.2.4 社区检测和总结
图索引过程的第二阶段专注于社区检测和总结。社区是一组实体,它们彼此之间比与其他图中的实体连接得更紧密。社区检测的结果如图 7.3 所示。

图 7.3 社区检测结果的示例
图 7.3 展示了节点被分组到不同的社区中,每个社区代表了一组紧密连接的实体,它们内部关系更强。一些社区很好地融入了整个图,而其他社区则显得更加孤立,形成了不连接的子图。识别这些集群有助于揭示数据集中的潜在结构、主题或关键群体。例如,在像《奥德赛》这样的叙事中,一个社区可能会围绕参与特定事件或地点的角色形成。通过检测和总结这些社区,我们可以捕捉到超越单个实体连接的高级关系和洞察。
列表 7.14 中的代码应用了 Louvain 方法,这是一种社区检测算法,用于在图中识别紧密连接的实体组。(原始论文实现中使用了 Leiden,并在 GDS 库中也可用)然后,检测到的社区被存储为节点属性,以便进行下游处理。
列表 7.14 使用 Louvain 算法计算社区
community_distribution = ch07_tools.calculate_communities(neo4j_driver)
print(f"""There are {community_distribution['communityCount']} communities with distribution:
{community_distribution['communityDistribution']}""")
在图中使用了 Louvain 方法检测到 9 个社区,社区的大小从 2 到 13 个节点不等。从列表 7.14 中检测到的社区的数量和大小可能会根据图结构的变化而变化,例如提取的实体和关系的数量。此外,Louvain 方法不是确定性的,这意味着即使输入相同,由于算法的优化过程,检测到的社区在运行之间可能会有轻微的变化。
层次社区结构
MS GraphRAG 论文利用 Louvain 算法的层次性质,在多个粒度级别上捕捉社区结构。这允许在大型图中分析广泛的和细粒度的社区。然而,由于我们正在处理一个较小的图,我们将专注于单一级别的社区检测,并跳过层次方面。
现在您可以将总结提示应用于生成每个检测到的社区的简洁概述。提示的说明部分可在“社区总结说明”中找到。
社区总结说明
您是一个 AI 助手,帮助人类分析师执行一般信息发现。信息发现是识别和评估与网络中某些实体(例如组织和个人)相关联的相关信息的过程。
目标:根据属于社区的实体列表以及它们之间的关系和可选的关联声明,编写一个社区的全面报告。该报告将用于向决策者通报与社区及其潜在影响相关的信息。报告内容包括社区关键实体的概述,它们的法律合规性、技术能力、声誉和值得注意的声明。
报告结构
报告应包括以下部分:
-
标题:代表社区关键实体的社区名称 - 标题应简短但具体。在可能的情况下,将代表性的命名实体包含在标题中。
-
摘要:社区整体结构的执行摘要,其实体之间的关系,以及与其实体相关的显著信息。
-
影响严重性评级:表示社区内实体造成的影响严重性的 0-10 之间的浮点分数。影响是社区评分的重要性。
-
评级说明:给出一个单句解释 IMPACT 严重性评级的含义。
-
详细发现:关于社区的 5-10 个关键见解的列表。每个见解应有一个简短的摘要,后跟根据以下扎根规则解释的多个段落。
“社区总结说明”中的提示指导 AI 助手生成检测到的社区的有序总结,确保它们捕捉到关键实体、关系和显著的见解。目标是生成高质量的总结,这些总结可以有效地用于下游的 RAG。
社区总结的完整提示包括输出说明和几个示例,以保持生成的总结的一致性和相关性。
在确定了社区并建立了结构化总结提示后,我们现在可以为每个检测到的社区生成全面的总结。这些社区总结综合了关键实体、关系和重要的见解。
下面的列表中的代码处理检测到的社区,并将摘要提示应用于生成有意义的描述。
列表 7.15 生成社区摘要
community_info, _, _ = neo4j_driver.execute_query(ch07_tools.community_info_query) #1
communities = []
for community in tqdm(community_info, desc="Summarizing communities"):
messages = [ #2
{
"role": "user",
"content": ch07_tools.get_summarize_community_prompt(
community["nodes"], community["rels"]
),
},
]
summary = chat(messages, model="gpt-4o") #3
communities.append(
{
"community": json.loads(ch07_tools.extract_json(summary)), #4
"communityId": community["communityId"],
"nodes": [el["id"] for el in community["nodes"]],
}
)
neo4j_driver.execute_query(ch07_tools.import_community_query, data=communities) #5
1 从数据库检索社区信息
2 构建提示
3 LLM 调用
4 将输出解析为字典
5 将结果存储到数据库
您现在可以使用列表 7.16 中显示的代码检查生成的社区摘要示例。这将提供一个具体的例子,说明摘要过程如何捕捉社区中的关键实体、关系和洞察。
列表 7.16 检索示例社区摘要
data, _, _ = neo4j_driver.execute_query(
"""MATCH (c:__Community__)
WITH c, count {(c)<-[:IN_COMMUNITY]-()} AS size
ORDER BY size DESC LIMIT 1
RETURN c.title AS title, c.summary AS summary
"""
)
print(f"Title: {data[0]['title']})
print(f"Summary: {data[0]["summary"]}")
结果可以在“生成的摘要”中找到,涉及TELEMACHUS和MINERVA之间的关系。
生成的社区摘要
米涅瓦、忒勒玛科斯和伊萨卡家庭 该社区以米涅瓦、忒勒玛科斯和尤利西斯的家庭为中心,涉及重要的互动,包括神圣的指导、家庭忠诚和求婚者的挑战。米涅瓦在向忒勒玛科斯提供建议方面扮演着关键角色,他决心找到他的父亲并恢复家园的秩序。这些实体之间的关系突出了智慧、勇气和坚韧的主题。
在更大的图中处理大型社区
当处理更大的图时,社区可能会变得太大而无法有效处理。如果一个社区包含太多实体和关系,包括所有这些在摘要提示中可能会超过令牌限制或产生过长的摘要。为了解决这个问题,应实施一个排名机制,以仅选择最相关的实体和关系。这确保了摘要保持简洁、信息丰富,并且对下游 RAG 应用有用。
恭喜!您已成功完成图索引步骤。
7.3 图检索器
图索引过程完成后,我们现在进入图检索阶段。此阶段专注于从结构化图中检索相关信息以有效回答查询。虽然有许多可能的检索策略,但我们将关注两种主要方法:局部搜索和全局搜索。局部搜索从检测到的社区中紧密相连的实体检索信息,而全局搜索则考虑整个图结构以找到最相关的信息。
7.3.1 全局搜索
在 GraphRAG 中,全局搜索使用社区摘要作为中间响应,以有效地回答需要在整个数据集上聚合信息的查询。这种方法不是基于向量相似性检索单个文本块,而是利用预先计算的社区级摘要来生成结构化响应。全局搜索的流程图在图 7.4 中可视化。

图 7.4 全局搜索
图 7.4 中的过程遵循 map-reduce 方法:
-
地图步骤—给定用户查询和可选的对话历史,GraphRAG 从图社区层次结构中的指定级别检索由 LLM 生成的社区报告。在您的实现中,图结构只有一个社区级别,这意味着所有检测到的组都存在于相同的层次深度。这些报告被分割成可管理的文本块,每个块都由 LLM 处理以生成中间响应。每个响应都包含一个要点列表,每个要点都附有数值重要性评分。
-
减少步骤—过滤和汇总所有中间响应中最重要的一点。然后,这些精炼的见解作为 LLM 的最终上下文,综合回答用户查询。通过将数据集结构化为语义上有意义的集群,GraphRAG 能够实现高效且连贯的检索,即使对于广泛的主题查询也是如此。
社区层次结构结构
响应的质量取决于用于获取社区报告的社区层次结构的级别。低级别社区提供详细报告,导致更全面的响应,但它们也增加了 LLM 调用次数和处理时间。高级别社区,具有更抽象的总结,可能更有效率,但风险丢失细节。平衡细节和效率是优化全局搜索性能的关键。
地图步骤使用以下系统提示,如“检索器地图部分的系统提示”所示。
检索器地图部分的系统提示
—角色—
您是一个有用的助手,回答有关提供的数据表中的问题的提问。
—目标—
生成一个由要点列表组成的响应,以响应用户的问题,总结输入数据表中的所有相关信息。
您应将下表中提供的数据作为生成响应的主要上下文。如果您不知道答案,或者输入的数据表不包含足够的信息来提供答案,只需说明即可。不要编造任何内容。
响应中的每个要点应包含以下元素:
-
描述:对该点的全面描述。
-
重要性评分:一个介于 0-100 之间的整数评分,表示该要点在回答用户问题中的重要性。一种“我不知道”类型的响应应得 0 分。
响应应按以下格式 JSON 格式化:{{ "points": [ {{"description": "点 1 的描述 [数据:报告(报告 ID)]", "score": score_value}}, {{"description": "点 2 的描述 [数据:报告(报告 ID)]", "score": score_value}} ] }}
响应应保留原意和使用诸如“应当”、“可以”或“将会”之类的情态动词。
支持数据的要点应列出相关报告作为参考,如下所示:“这是一个由数据参考支持的示例句子 [数据:报告(报告 ID)]”
不要在单个引用中列出超过 5 个记录 ID。相反,列出前 5 个最相关的记录 ID,并添加“+更多”以表明还有更多。
例如:“X 先生是 Y 公司的所有者,并受到许多不当行为的指控[数据:报告(2,7,64,46,34,+更多)]。他也是 X 公司的 CEO[数据:报告(1,3)]”
其中 1,2,3,7,34,46 和 64 代表在提供的表中相关数据报告的 ID。
不要包含没有提供支持证据的信息。
—数据表—
{context_data}
地图系统提示指导 LLM 根据用户查询从提供的上下文中提取关键点。每个关键点包括一个描述和一个重要性分数(0-100),反映其与查询的相关性。响应格式为 JSON,并引用支持数据报告 ID。如果信息不足,响应必须表明这一点,而不进行推测。
现在你将检查检索器的减少步骤,如“检索器减少部分的系统提示”所示。
检索器减少部分的系统提示
—角色—
你是一个有用的助手,通过综合多位分析师的观点来回答关于数据集的问题。
—目标—
生成目标长度和格式的响应,以回答用户的问题,总结多位分析师从数据集的不同部分关注的报告。
注意,以下提供的分析师报告按重要性递减顺序排列。
如果你不知道答案,或者提供的报告没有足够的信息来提供答案,只需说明即可。不要编造任何内容。
最终响应应从分析师报告中删除所有无关信息,并将清洗后的信息合并成一个综合答案,提供所有关键点和适当于响应长度和格式的含义解释。
根据长度和格式适当添加章节和评论到响应中。以 Markdown 格式编排响应。
响应应保留原句中“应当”、“可以”或“将会”等情态动词的原意和用法。
响应还应保留分析师报告中先前包含的所有数据引用,但不要提及分析过程中的多位分析师的角色。
不要在单个引用中列出超过 5 个记录 ID。相反,列出前 5 个最相关的记录 ID,并添加“+更多”以表明还有更多。
例如:
“X 先生是 Y 公司的所有者,并受到许多不当行为的指控[数据:报告(2,7,34,46,64,+更多)]。他也是 X 公司的 CEO[数据:报告(1,3)]”
其中 1,2,3,7,34,46 和 64 代表相关数据记录的 ID(不是索引)。
不要包含没有提供支持证据的信息。
—目标响应长度和格式—
{response_type}
减少系统提示指导 LLM 综合多个分析师报告的关键要点,这些报告按重要性排序。回应必须以 Markdown 格式呈现,适当结构化以适应目标长度和格式,并排除无关细节。它保留所有引用的数据,同时避免推测性答案。最终输出将报告中的见解整合和提炼成一个连贯、全面的回应。
现在我们可以将地图和减少提示合并成一个全局搜索函数。
列表 7.17 全局搜索
def global_retriever(query: str, rating_threshold: float = 5) -> str:
community_data, _, _ = neo4j_driver.execute_query( #1
"""
MATCH (c:__Community__)
WHERE c.rating >= $rating
RETURN c.summary AS summary
""",
rating=rating_threshold,
)
print(f"Got {len(community_data)} community summaries")
intermediate_results = []
for community in tqdm(community_data, desc="Processing communities"):
intermediate_messages = [ #2
{
"role": "system",
"content": ch07_tools.get_map_system_prompt(community["summary"]),
},
{
"role": "user",
"content": query,
},
]
intermediate_response = chat(intermediate_messages, model="gpt-4o")
intermediate_results.append(intermediate_response)
final_messages = [ #3
{
"role": "system",
"content": ch07_tools.get_reduce_system_prompt(intermediate_results),
},
{"role": "user", "content": query},
]
summary = chat(final_messages, model="gpt-4o")
return summary
1 获取所有评分高于阈值的社区
2 对于每个社区,获取一个中间回应
3 使用所有中间回应作为上下文生成最终答案
列表 7.17 中的global_retriever函数通过使用社区摘要来生成结构化回应,实现了全局搜索方法。它遵循以下三个步骤:
-
检索相关社区——该函数查询 Neo4j 数据库,检索评分达到或超过指定阈值的社区摘要。这确保只有最相关的社区对最终答案做出贡献。
-
生成中间回应——对于每个社区,使用地图系统提示生成一个中间回应。模型在处理社区摘要的同时,与用户的查询一起提取关键点。
-
聚合并生成最终答案——然后应用减少系统提示来综合所有中间回应,形成一个连贯的最终答案,确保保留最重要的要点并正确地组织结构。
现在我们可以用一个例子来测试这个函数。
列表 7.18 全局搜索示例
print(global_retriever("What is this story about?"))
列表 7.18 的结果可以在“使用全局搜索对‘这个故事是关于什么的?’的回应”中找到。
使用全局搜索对‘这个故事是关于什么的?’的回应
这个故事围绕着涉及米涅瓦、忒勒玛科斯和尤利西斯家庭的复杂社区动态展开。主要主题包括神灵的指引、家族忠诚和求婚者的挑战。米涅瓦在向忒勒玛科斯提供建议方面扮演着关键角色,忒勒玛科斯决心找到他的父亲尤利西斯,并恢复家园的秩序。人物之间的关系强调了智慧、勇气和坚韧的主题。
此外,叙事还突出了门特斯的作用,他是塔菲亚人的首领,被认为是安基阿卢斯的儿子。门特斯参与了一次前往铁矿石闻名的特梅萨的航行,并声称对塔菲亚人拥有王权[数据:报告(1)]。故事还围绕着希腊神话中的关键人物奥德修斯,以及他与其他重要实体(如阿开亚人、莱尔提斯和神灵)的联系。故事中的人物关系突出了神灵对人类事务的影响,展示了神灵如何影响希腊英雄的生活[数据:报告(1,2,3,4,5)]。
此外,叙述探索了涉及奥林匹斯神宙斯、埃癸斯托斯、阿伽门农、奥雷斯特斯和墨丘利的神话元素。它突出了神干预、背叛和复仇的主题。奥林匹斯神宙斯讨论了埃癸斯托斯的行为,他因背叛和谋杀阿伽门农而臭名昭著,而奥雷斯特斯在墨丘利的警告下,通过杀死埃癸斯托斯为父亲的死报仇 [数据:报告(1,2,3,4,5)]。这些相互关联的故事编织了一幅丰富的神话和英雄元素图案,强调了这些传奇人物持久的历史遗产和面临的挑战。
由全局搜索方法生成的“使用全局搜索对‘这个故事是关于什么的?’的响应”提供了故事的有序总结,通过综合多个片段中的关键主题和关系。它突出了主要人物密涅瓦、忒勒玛科斯和尤利西斯——以及他们在叙事中的角色,强调了神的指引、家族忠诚和尤利西斯家庭的挑战。
练习 7.2
尝试使用全局搜索功能运行不同类型的查询。提出需要综合多个社区摘要信息的问题,例如:“这个故事中的主要冲突是什么?”
7.3.2 本地搜索
本地搜索方法通过将结构化知识图谱数据与源文档中的非结构化文本相结合,增强了 LLM 的响应。这种方法对于以实体为中心的查询特别有效,例如“矢车菊有什么治疗特性?”在这种情况下,需要深入了解特定实体及其关系。本地搜索方法可以在图 7.5 中找到。

图 7.5 本地搜索
当用户提交查询时,图 7.5 中可视化的系统首先使用向量搜索在知识图谱中识别语义相关的实体。这些实体作为检索相关信息(包括直接连接的实体、关系和社区报告的摘要)的入口点。此外,还提取了与这些实体相关的输入文档中的文本块。检索到的数据被排序和过滤,以确保最终响应中只包含最相关的信息。
要实现本地搜索,我们首先需要计算实体的文本嵌入并创建一个向量索引。这使我们能够根据用户的查询高效地检索最相关的实体。通过将实体描述和关系嵌入到向量空间中,我们可以使用相似性搜索来识别哪些实体与输入最密切相关。一旦找到这些相关实体,它们就作为检索更多结构和非结构化数据的入口点。计算这些嵌入和构建向量索引的代码如下所示。
列表 7.19 为数据库中所有实体生成文本嵌入
entities, _, _ = neo4j_driver.execute_query(
"""
MATCH (e:__Entity__)
RETURN e.summary AS summary, e.name AS name #1
"""
)
data = [{"name": el["name"], "embedding": embed(el["summary"])[0]} for el in entities] #2
neo4j_driver.execute_query( #3
"""
UNWIND $data AS row
MATCH (e:__Entity__ {name: row.name})
CALL db.create.setNodeVectorProperty(e, 'embedding', row.embedding)
""",
data=data,
) #4
neo4j_driver.execute_query(
"""
CREATE VECTOR INDEX entities IF NOT EXISTS
FOR (n:__Entity__)
ON (n.embedding)
""",
data=data,
)
1 获取实体及其摘要
2 基于实体摘要计算嵌入
3 将嵌入存储到数据库中
4 创建向量索引实体
列表 7.19 中的代码从数据库中检索所有实体及其摘要,根据每个实体的摘要计算文本嵌入,并将嵌入存储回数据库。最后,它创建一个向量索引以实现实体嵌入上的高效相似性搜索。
本地搜索最终实现为一个 Cypher 语句,该语句通过向量搜索扩展了初始的相关节点集,包括它们的连接实体、文本片段、摘要和关系。此 Cypher 语句在以下列表中显示。
列表 7.20 本地搜索的 Cypher 语句
local_search_query = """
CALL db.index.vector.queryNodes('entities', $k, $embedding)
YIELD node, score
WITH collect(node) as nodes #1
WITH collect {
UNWIND nodes as n
MATCH (n)<-[:HAS_ENTITY]->(c:__Chunk__)
WITH c, count(distinct n) as freq
RETURN c.text AS chunkText
ORDER BY freq DESC
LIMIT $topChunks
} AS text_mapping,
collect { #2
UNWIND nodes as n
MATCH (n)-[:IN_COMMUNITY]->(c:__Community__)
WITH c, c.rank as rank, c.weight AS weight
RETURN c.summary
ORDER BY rank, weight DESC
LIMIT $topCommunities
} AS report_mapping,
collect { #3
UNWIND nodes as n
MATCH (n)-[r:SUMMARIZED_RELATIONSHIP]-(m)
WHERE m IN nodes
RETURN r.summary AS descriptionText
ORDER BY r.rank, r.weight DESC
LIMIT $topInsideRels
} as insideRels,
collect { #4
UNWIND nodes as n
RETURN n.summary AS descriptionText
} as entities
RETURN {Chunks: text_mapping, Reports: report_mapping,
Relationships: insideRels,
Entities: entities} AS text
"""
1 获取相关文本片段
2 获取相关社区描述
3 获取相关关系
4 获取实体摘要
列表 7.20 中检索到的所有对象,如文本片段、社区描述、关系和实体摘要,都进行了排序并限制数量,以确保提示信息保持可管理。文本片段根据与相关实体的关联频率进行排序,并限制在 topChunks 的顶部。社区描述按排名和权重排序,仅选择 topCommunities。关系按其重要性进行排序,并限制在 topInsideRels 内。最后,实体摘要没有额外的排名约束被检索。这确保了只包含最相关的信息在响应中。
最后,你需要定义总结提示,这再次借鉴自论文,并在“本地搜索的系统提示”中展示。
本地搜索的系统提示
—角色—
你是一个响应关于提供的表格中数据的询问的有帮助的助手。
—目标—
生成一个目标长度和格式的响应,该响应针对用户的问题,总结输入数据表中适当长度和格式的所有信息,并融入任何相关的通用知识。
如果你不知道答案,只需说“不知道”。不要编造任何内容。
由数据支持的项目应按以下方式列出其数据引用:
“这是一个由多个数据引用支持的示例句子 [数据:<数据集名称> (记录 ID); <数据集名称> (记录 ID)]。”
不要在单个引用中列出超过 5 个记录 ID。相反,列出前 5 个最相关的记录 ID,并添加“+更多”以表明还有更多。
例如:
“X 人是 Y 公司的所有者,并受到许多不当行为的指控 [数据:来源 (15, 16), 报告 (1), 实体 (5, 7); 关系 (23); 陈述 (2, 7, 34, 46, 64, +更多)]。”
其中 15、16、1、5、7、23、2、7、34、46 和 64 代表相关数据记录的 ID(不是索引)。
不要包含没有提供支持证据的信息。
—目标响应长度和格式—
{response_type}
—数据表—
{context_data}
“本地搜索的系统提示”中的这个系统提示旨在根据结构化数据表生成响应,同时保持准确性和透明度。它指示助手综合与用户查询相关的信息,确保主张有明确的数据参考支持。引用数据源格式的格式强制执行结构化方法,限制每个参考的记录 ID 数量,并在适用时指出额外的支持记录。提示还强调,如果答案在提供的数据中找不到,助手应明确指出,而不是编造信息。
在此基础上,你现在可以实施本地搜索。
列表 7.21 本地搜索实现
def local_search(query: str) -> str:
context, _, _ = neo4j_driver.execute_query( #1
local_search_query,
embedding=embed(query)[0],
topChunks=topChunks,
topCommunities=topCommunities,
topInsideRels=topInsideRels,
k=k_entities,
)
context_str = str(context[0]["text"]) #2
local_messages = [ #3
{
"role": "system",
"content": ch07_tools.get_local_system_prompt(context_str),
},
{
"role": "user",
"content": query,
},
]
final_answer = chat(local_messages, model="gpt-4o") #4
return final_answer
1 使用本地搜索 Cypher 语句获取上下文
2 将上下文字符串化
3 构建提示
4 生成最终答案
列表 7.21 通过首先使用向量搜索和 Cypher 查询从知识图谱中检索相关上下文来实现本地搜索。然后,提取的上下文被转换为字符串,并纳入一个旨在指导 LLM 生成有见地响应的结构化提示中。最后,将提示发送到模型以生成最终答案。
你现在可以使用这个实现进行测试。
列表 7.22 本地搜索实现
print(local_search("Who is Ulysses?"))
列表 7.22 的结果可以在“使用本地搜索对‘尤利西斯是谁?’的响应”中找到。
使用本地搜索对“尤利西斯是谁?”的响应
这个故事围绕着以米涅瓦、忒勒玛科斯和尤利西斯的家族为核心人物的社区错综复杂的动态。主要主题包括神灵的指引、家族的忠诚以及求婚者带来的挑战。米涅瓦在向忒勒玛科斯提供建议方面扮演着至关重要的角色,忒勒玛科斯决心找到他的父亲尤利西斯,并恢复家园的秩序。人物之间的关系强调了智慧、勇气和坚韧的主题。
此外,叙事还突出了门特斯的作用,他是泰菲亚人的首领,被认为是安基阿卢斯的儿子。门特斯参与了一次前往以铁矿石货物闻名的特梅萨的航行,并声称对泰菲亚人拥有王权[数据:报告(1)]。故事还围绕着希腊神话中的关键人物奥德修斯,以及他与其他重要实体(如阿开亚人、莱耳忒斯和神祇)的联系。故事中的人物关系突出了神灵干预对人类事务的影响,展示了神祇如何影响希腊英雄的生活[数据:报告(1,2,3,4,5)]。
此外,叙事探讨了涉及奥林匹斯神宙斯、埃癸斯托斯、阿伽门农、奥雷斯特斯和墨丘利的神话元素。它突出了神干预、背叛和复仇的主题。奥林匹斯神宙斯讨论了埃癸斯托斯的行为,他因背叛和谋杀阿伽门农而臭名昭著,而奥雷斯特斯在墨丘利的警告下,通过杀死埃癸斯托斯为父亲的死报仇[数据:报告(1,2,3,4,5)]。这些相互关联的故事编织了一幅丰富的神话和英雄元素画卷,强调了这些传奇人物持久的历史遗产和面临的挑战。
在“使用本地搜索对‘尤利西斯是谁?’进行响应”的示例中,展示了本地搜索如何从知识图谱中检索和综合相关信息,以提供详细、有充分支持的答案。通过整合连接的实体、关系和社区摘要,系统确保响应既包含叙事背景,又包含事实深度。
练习 7.3
尝试使用本地搜索功能运行不同类型的查询。
在这样的图索引中,可以实现不同的检索策略。例如,社区摘要可以单独嵌入并用作独立的向量检索器,根据查询的焦点进行更有针对性的检索。
恭喜!您已成功实现完整的 MS GraphRAG。
摘要
-
MS GraphRAG 使用两阶段过程,首先从源文档中提取和总结实体和关系,然后进行社区检测和总结,以创建一致的知识表示。
-
提取过程使用 LLM 来识别实体,根据预定义的类型(例如,
PERSON,GOD,LOCATION)对它们进行分类,并生成实体及其关系的详细描述,包括关系强度分数。 -
通过基于 LLM 的摘要,将来自多个文本块中的实体和关系描述合并,以创建统一、非冗余的表示,同时保留关键信息。
-
该系统使用类似 Louvain 方法的算法检测紧密连接的实体社区,然后生成社区级别的摘要以捕捉更高层次的主题和关系。
-
全局搜索使用社区摘要通过映射-归约方法来回答广泛的、主题性的查询。
-
本地搜索结合向量相似性搜索和图遍历来回答以实体为中心的查询。
-
检索的有效性取决于因素,如块大小、实体类型选择和社区检测参数,较小的块通常会导致更全面的实体提取。
-
系统通过排名机制处理潜在的扩展挑战,在保持上下文相关性的同时管理大量实体、关系和社区。
第八章:RAG 应用评估
本章涵盖
-
基准测试 RAG 应用和代理能力
-
设计评估数据集
-
应用 RAGAS 度量:召回率、忠实度、正确性
在本章中,您将探讨使用精心构建的基准问题评估 RAG 应用性能的重要性。随着您的 RAG 管道变得更加复杂和高级,确保代理的答案在广泛的查询中既准确又连贯变得至关重要。基准评估提供了衡量代理能力所需的系统,同时也有助于明确定义和界定代理的范围。
评估 RAG 应用涉及多种方法,每种方法都针对应用的各个步骤,如图 8.1 所示,它展示了由具有检索能力的 LLM 驱动的问答系统的管道的高级概述。它从用户向系统提出问题开始。然后 LLM 确定最合适的检索工具来获取必要的信息。这一步至关重要,可以评估工具选择过程的准确性。

图 8.1 评估 RAG 管道的不同步骤
在本书中,您已经实现了各种检索工具设计,从向量搜索开始,逐步发展到更结构化的方法,如 text2cypher 和 Cypher 模板。每种检索方法都满足不同的需求:
-
向量搜索有效地检索语义相关的文档。
-
Cypher 模板允许对数据库进行精确、结构化的查询。
-
Text2cypher 允许动态和灵活的查询,得益于基于图检索的表达能力。
评估 LLM 选择的工具以及它如何与查询需求相匹配对于优化检索性能至关重要。
一旦选择了适当的工具,它就会从知识库中检索相关的上下文或数据。检索到的上下文与用户问题的相关性是另一个关键的评估点。一个精心选择的检索方法应确保检索到的上下文既准确又足以回答查询。
使用检索到的上下文,LLM 生成一个答案,然后将其呈现给用户。在这个阶段,我们不仅可以评估生成的响应的连贯性和准确性,还可以评估模型理解和有效整合提供上下文的能力。一个特别重要的评估标准是 LLM 在给定正确上下文时是否产生正确的答案。这使我们能够将模型的推理和综合能力与检索性能分开来衡量。
此外,可以整体评估整个管道,以衡量其在提供准确和上下文相关答案方面的有效性。通过分析不同阶段的失败——工具选择、检索相关性和最终响应生成——我们可以迭代地改进检索机制以及 LLM 利用检索信息的能力。
假设你负责评估第五章中实现的 LLM 代理的性能。为了更深入地了解其有效性,你将使用 RAGAS Python 库来设计和执行基准分析。但首先,你需要设计基准数据集。在本章的剩余部分,我们将从概念到代码,逐步介绍实现过程。为了跟上进度,你需要访问一个运行的 Neo4j 实例。这可以是一个本地安装或云托管实例。在本章的实现中,我们使用所谓的“电影数据集”。有关数据集的更多信息以及各种加载方式,请参阅附录。你可以直接在附带的 Jupyter 笔记本中跟随实现,笔记本链接如下:github.com/tomasonjo/kg-rag/blob/main/notebooks/ch08.ipynb。
让我们深入探讨。
8.1 设计基准数据集
创建基准数据集需要设计输入查询,以测试系统决策和响应生成的各个方面。由于 RAG 管道中的每一步都发挥着至关重要的作用,数据集应包括各种挑战不同组件的多样化问题:
-
工具选择评估 — 一些查询应评估系统是否选择了正确的检索方法,确保它识别出最相关的信息来源。
-
实体和值映射 — 其他查询可能专注于测试特定任务,例如将用户输入中的实体或值映射到数据库中的相应条目。
-
多步检索场景 — 一些代理具有执行多个检索步骤的能力,其中最初检索的数据作为第二个检索步骤的输入。基准测试应包括系统需要细化或扩展第一个检索以完全回答查询的场景。这些场景对于回答依赖于动态链式多个查询的复杂问题尤为重要。
-
边缘情况和功能覆盖 — 要全面理解系统性能,基准测试必须涵盖所有功能以及已知的边缘情况。这包括处理模糊查询、长尾概念以及可能适用多种检索方法的场景。
-
对话可用性 — 此外,评估代理处理问候、澄清模糊查询以及有效地传达其能力以确保流畅且用户友好的体验可能也很有用。
通过系统地基准测试这些方面,我们能够更清楚地了解代理在不同条件下的表现。这允许我们进行有针对性的改进,确保在实际部署中的鲁棒性和可靠性。
8.1.1 设计测试示例
为了全面评估系统,您需要定义良好的端到端测试示例。每个示例都包含一个问题及其相应的真实响应,如图 8.2 所示,以确保系统的输出可以可靠地评估。

图 8.2 基准测试示例
我们可以不提供静态字符串作为预期的答案,而是使用 Cypher 查询来动态定义真实响应。由于我们处理的是图数据库,这种方法提供了一个显著的优势:即使底层数据发生变化,基准测试仍然有效。这确保了如图 8.3 所示的测试用例在时间上保持准确,无需不断更新。

图 8.3 以 Cypher 语句作为真实响应的基准测试示例
在设计基准数据集时,您应包括多样化的示例以评估代理性能的不同方面。例如,您可以评估代理如何对“你好”这样的问候做出响应,如何向用户提供指导,或如何处理无关查询,如表 8.1 所示。
表 8.1 测试简单问候和无关问题的基准示例
| 问题 | Cypher |
|---|---|
| Hello | RETURN “问候并提醒它只能回答与电影相关的问题。” |
| 你能做什么? | RETURN “回答与电影及其演员相关的问题。” |
| 西班牙的天气怎么样? | RETURN “无关问题,因为我们只能回答与电影及其演员相关的问题。” |
此表提供了代理对简单问候、用户指导请求和无关查询的响应示例。它展示了您如何使用简单的RETURN Cypher 语句来定义不需要在数据库中查找信息的静态答案。例如,当被问候“你好”时,代理会回复一个问候并提醒其范围。如果被问及它能做什么,它会澄清它回答有关电影及其演员的问题。对于关于天气等无关查询,代理会简单地声明它只处理与电影相关的问题。
接下来,我们可以定义一组问题来评估工具的使用以及 LLM 使用这些工具生成准确答案的能力。示例如表 8.2 所示。
表 8.2 测试工具使用和价值映射的基准示例
| 问题 | Cypher |
|---|---|
| Who acted in Top Gun? | RETURN "MATCH (p:Person)-[:ACTED_IN]→(m:Movie {title: "Top Gun"}) RETURN p.name" |
| 谁在《壮志凌云》中出演? | RETURN "MATCH (p:Person)-[:ACTED_IN]→(m:Movie {title: "Top Gun"}) RETURN p.name" |
| 在哪些电影中汤姆·汉克斯出演过? | MATCH (p:Person {name: "Tom Hanks"})-[:ACTED_IN]→(m:Movie) RETURN m.title |
| 在哪些电影中汤姆·汉克斯出演过? | MATCH (p:Person {name: "Tom Hanks"})-[:ACTED_IN]→(m:Movie) RETURN m.title |
表 8.2 中的示例展示了 LLM 需要使用现有工具从数据库中检索相关数据的情况。在这里,LLM 应利用两个关键工具:一个用于通过演员查找电影,另一个用于通过电影查找演员,以确保快速和可靠的响应。
此外,这些示例使我们能够评估代理将用户输入映射到数据库值的效果。对于知名电影和演员,LLM 通常能够基于其预训练直接生成正确的查询。然而,对于不太知名或私人数据集,一个专门的映射系统对于准确实体解析至关重要。实施这样一个系统确保用户输入正确地链接到数据库条目,从而提高准确性和可靠性。
您还应包括一些 LLM 需要使用 text2cypher 工具的示例,如表 8.3 所示。
表 8.3 测试涉及聚合和过滤的查询的基准示例
| 问题 | Cypher |
|---|---|
| 哪位演员出演的电影最多? | MATCH (p:Person)-[:ACTED_IN]→(m:Movie) RETURN p.name, COUNT(m) AS movieCount ORDER BY movieCount DESC LIMIT 1 |
| 列出在 1940 年之前出生的人。 | MATCH (p:Person) WHERE p.born < 1940 RETURN p.name |
| 1965 年出生并执导过电影的有哪些人? | MATCH (p:Person)-[:DIRECTED]→(m:Movie) WHERE p.born = 1965 RETURN p.name |
表 8.3 包括涉及聚合、过滤和关系的查询,例如找到电影角色最多的演员、列出在特定年份之前出生的人,以及识别在特定年份出生的导演。由于没有实现专门的工具来处理这些查询,LLM 必须依赖 text2cypher 根据提供的图模式构建适当的 Cypher 语句。
您还应该测试边缘情况,例如在表 8.4 中展示的,相关数据缺失但仍在域内的查询。
表 8.4 测试数据缺失的查询的基准示例
| 问题 | Cypher |
|---|---|
| 哪部电影获得了最多的奥斯卡奖? | RETURN “This information is missing” |
基准测试将非常依赖于您代理的功能。具体能力,如检索策略、推理方法和结构化输出处理,将影响基准在评估性能方面的有效性。在设计基准时,确保全面覆盖代理的功能至关重要。通过结合各种示例,您可以有效地测试代理处理不同挑战的能力。
基准测试总共有 17 个示例,其中一些未在此展示。您现在可以评估它们。
8.2 评估
为了评估你的基准性能,你将使用 RAGAS,这是一个为评估 RAG 系统而设计的框架。如前所述,评估重点在于三个关键指标,接下来将讨论。
8.2.1 上下文回忆
上下文回忆衡量使用“上下文回忆评估”中的提示成功检索到的相关信息数量。高分表示检索系统有效地捕捉了回答查询所需的所有必要上下文。
上下文回忆评估
目标:给定一个上下文和一个答案,分析答案中的每一句话,并判断这句话是否可以归因于给定的上下文。使用仅包含'Yes'(1)或'No'(0)的二进制分类。输出带有理由的 JSON。
“上下文回忆评估”中的提示确保生成的答案中的每一句话都明确地得到了检索到的上下文的支持。通过这样做,它有助于评估检索系统捕捉相关信息的有效性。
接下来,忠实度评估确保生成的响应与检索到的内容保持事实一致。
8.2.2 忠实度
忠实度评估是否生成的响应与检索到的上下文保持事实一致。如果一个响应的所有主张都可以直接由提供的文档支持,则认为它是忠实的,从而最小化幻觉的风险。忠实度评估使用两步过程。在第一步中,它使用“忠实度语句分解”中的提示将答案分解为原子语句,确保每个信息单元都是清晰和自包含的,从而使得验证变得更容易。
忠实度语句分解
目标:给定一个问题和一个答案,分析答案中每一句话的复杂性。将每一句话分解为一个或多个完全可理解的语句。确保任何语句中都不使用代词。以 JSON 格式输出。
一旦生成语句,它将使用“忠实度评估”中的提示来评估它们的忠实度。
忠实度评估
目标:根据给定的上下文判断一系列语句的忠实度。对于每个语句,如果可以从上下文中直接推断出该语句,则返回 1,如果无法直接从上下文中推断出该语句,则返回 0。
“忠实度评估”中的提示检查生成的响应中的语句是否在检索到的上下文中具有事实基础。它确保模型不会引入未经支持的断言。
最后,我们通过将生成的响应与基准事实进行比较来评估答案的正确性。
8.2.3 答案正确性
答案正确性评估了响应如何准确地、完整地回答用户的查询。它考虑了事实准确性以及相关性,以确保响应与问题的意图一致。答案正确性使用与忠实度相同的过程生成陈述,然后使用“答案正确性评估”中的提示进行评估。
答案正确性评估
目标:给定一个真实响应和一个答案陈述,分析每个陈述并将其分类到以下类别之一:
TP(真阳性):答案中存在的陈述也在真实响应中的一个或多个陈述中得到直接支持。FP(假阳性):答案中存在的陈述但在真实响应中的任何陈述都没有得到直接支持。FN(假阴性):在真实响应中找到但在答案中不存在的陈述。
每个陈述只能属于这些类别之一。为每个分类提供理由。
“答案正确性评估”中的提示确保通过系统地比较生成的陈述与真实响应,响应既在事实上正确又与预期的答案一致。
通过分析这些指标,你可以确定系统检索相关数据、保持事实一致性和生成正确响应的程度。这种评估将有助于识别潜在的弱点,如缺失的上下文、不一致或不准确的答案,从而实现迭代改进和性能提升。
8.2.4 加载数据集
基准数据集以 CSV 文件的形式提供在附带的存储库中,这使得加载和使用变得容易,如下面的列表所示。
列表 8.1 从 CSV 加载基准数据集
test_data = pd.read_csv("../data/benchmark_data.csv", delimiter=";")
8.2.5 运行评估
为了评估系统的性能,你需要为基准数据集生成答案,并将它们与预期的真实响应进行比较。首先,你需要通过执行相应的 Cypher 语句并使用代理生成答案来获取真实响应,如列表 8.2 所示。此外,你必须记录延迟和检索到的上下文,以分析系统的效率和相关性。
列表 8.2 生成答案和真实响应
answers = []
ground_truths = []
latencies = []
contexts = []
for i, row in tqdm(test_data.iterrows(), total=len(test_data), desc="Processing rows"):
ground_truth, _, _ = neo4j_driver.execute_query(row["cypher"]) #1
ground_truths.append([str(el.data()) for el in ground_truth])
start = datetime.now()
try:
answer, context = get_answer(row["question"]) #2
context = [el['content'] for el in context]
except Exception:
answer, context = None, []
latencies.append((datetime.now() - start).total_seconds()) #3
answers.append(answer)
contexts.append(context)
#4
test_data['ground_truth'] = [str(el) for el in ground_truths]
test_data['answer'] = answers
test_data['latency'] = latencies
test_data['retrieved_contexts'] = contexts
1 提供的 Cypher 语句返回真实响应。
2 执行代理以生成对问题的响应
3 计算延迟
4 将结果存储回数据框
现在我们已经收集了所有必要的输入数据,包括生成的答案和真实响应,我们可以继续进行评估。
列表 8.3 评估生成的答案和检索到的上下文
dataset = Dataset.from_pandas(test_data.fillna("I don't know")) #1
result = evaluate( #2
dataset,
metrics=[ #3
answer_correctness,
context_recall,
faithfulness,
],
)
1 将缺失的响应答案更改为“我不知道”
2 使用 RAGAS 框架运行评估
3 相关指标
列表 8.3 中的此代码使用 RAGAS 框架运行评估,该框架需要非空值,因此您用“我不知道”填充缺失的响应。然后根据答案正确性、上下文召回和忠实度评估生成的答案。
最后一步是分析结果,以了解系统的性能。
8.2.6 观察结果
您可以通过查看 8.5 中的整体总结来了解代理的性能概述。
表 8.5 基准总结
answer_correctness |
context_recall |
faithfulness |
|---|---|---|
| 0.7774 | 0.7941 | 0.9657 |
表 8.5 中的结果提供了基于三个关键指标的系统性能的整体评估。答案正确性得分为 0.7774,模型大多数时候都能正确回答,但仍有大约四分之一的情况未能命中目标。上下文召回得分为 0.7941 表明,虽然检索系统做得相当不错,但它偶尔无法检索到所有必要的信息,这可能会影响整体准确性。另一方面,忠实度得分为 0.9657 非常出色,这意味着模型很少编造信息,并始终忠于检索到的上下文。
总体而言,高忠实度得分表明模型不会引入错误信息,但答案正确性和上下文召回的较低得分表明,改进检索机制可以提高响应准确性。增强检索覆盖范围和改进 LLM 构建答案的方式可以提高整体性能。这些见解可以指导进一步的优化,例如改进检索系统、改进查询重构或为模糊查询实现更好的实体映射。
您可以使用以下列表中的代码进一步分析每个响应,以识别改进的区域。
列表 8.4 提取指标并将它们添加到数据框中
for key in ["answer_correctness", "context_recall", "faithfulness"]:
test_data[key] = [el[key] for el in result.scores]
test_data
由于完整的响应太大,无法包含在书中,但通过分析个别示例,我们可以得出几个关键结论。一个明显的模式是,对于不需要 text2cypher 的查询,延迟显著降低,因为避免额外的 LLM 调用可以加快响应速度。另一个观察结果是,由于我们依赖 LLM 作为评判者,一些分数可能看起来不一致,例如在 Hello 示例中。
一个明显的局限性是,系统无法回答“在所有演员中,谁的名字最长?”这个问题。这是因为模型没有生成适当 Cypher 查询的能力。为了解决这个问题,您可以添加一些示例来指导 text2cypher,或者实施一个专门用于处理此类查询的工具。
此分析展示了基准如何帮助我们评估结果并就未来的改进做出明智的决定。随着系统的发展,基准数据集应继续增长,以确保持续的改进和更好的性能。
在整本书中,你探索了如何构建知识图谱 RAG 系统。你学习了不同的检索策略如何使你的代理从结构化或非结构化数据中检索相关信息。了解何时使用向量搜索或 Cypher 模板等方法对于设计高效且准确系统至关重要。
通过实施和改进检索策略,你现在有了构建基于知识图谱的强大代理的基础。你已经看到了结构化查询如何提高精确性,以及检索选择如何影响答案质量,你还学会了如何系统地评估性能。本章介绍了基准测试作为衡量准确性、召回率和忠实度的一种方式,为你提供了持续改进代理的工具。
8.3 下一步
你现在拥有了构建和改进由知识图谱驱动的智能检索系统的知识和工具。无论你是创建复杂的问答代理,还是为特定领域定制检索管道,你都有设计稳健、高性能、知识驱动 AI 系统的坚实基础。
LLMs 正在迅速改进,不仅在于它们理解和生成语言的能力,还在于它们如何有效地使用外部工具进行数据检索、转换和处理。随着这些模型能力的增强,它们将能够以最小的提示完成越来越复杂的任务。然而,它们的有效性仍然取决于你提供的工具的质量、设计和集成。你的任务是深思熟虑且高效地实施这些工具,确保它们非常适合你系统的目标和限制。
在这个基础上,你现在可以开始构建自己的代理 GraphRAG 系统。你能够以各种方式处理非结构化数据:你可以直接嵌入文本以实现基于相似性的快速检索,或者更进一步,提取结构化信息——如实体、关系和事件——以填充支持更精确、语义和跨跳查询的知识图谱。通过结合这些方法,你可以构建不仅找到相关信息,而且真正理解它的检索系统,为强大的、具有上下文感知的 AI 应用铺平道路。
摘要
-
评估 RAG 管道对于确保答案的准确性和一致性至关重要。基准评估有助于衡量性能并定义代理的能力。
-
评估过程涉及评估各个阶段:检索工具选择、上下文检索相关性、答案生成质量以及整体系统有效性。
-
一个结构良好的基准数据集应包括多样化的查询,以测试检索准确性、实体映射、问候语的处理、无关查询以及各种基于 Cypher 的数据库查找。
-
与静态预期答案不同,使用 Cypher 查询作为基准确保即使底层数据发生变化,基准仍然有效。
-
环境回忆衡量系统检索相关信息的能力。
-
忠实度评估生成的答案是否与检索到的内容在事实上保持一致。
-
回答正确性评估响应是否完全准确地回答了查询。
附录 A Neo4j 环境
在这本书中,你将通过使用 Neo4j 的实际示例学习图论和算法。我(Oskar)选择 Neo4j 是因为我有超过五年的使用经验,构建和分析图。
Neo4j 是一个原生图数据库,从头开始构建以存储、查询和操作图数据。它使用 Java 实现,可以通过 Cypher 查询语言从其他语言编写的软件中访问,通过事务性 HTTP 端点或二进制 Bolt 协议。在 Neo4j 中,数据以节点和关系的形式存储,它们在数据库中都是一等公民。节点代表实体,如人或企业,而关系代表这些实体之间的连接。节点和关系可以具有属性,这些属性是键值对,提供了关于节点和关系的额外信息。
Neo4j 设计为高度可扩展。它使用灵活的索引系统来高效地查询和操作数据,并支持原子性、一致性、隔离性和持久性事务,以确保数据一致性。它还内置了一种查询语言,称为 Cypher,该语言旨在易于表达且易于使用,用于查询和操作图数据。
使用 Neo4j 的另一个好处是它有两个有用的插件,你将使用它们:
-
Cypher 神奇过程 (APOC) 插件——一个为 Neo4j 提供各种功能的程序、函数和插件库,包括数据导入和导出、数据转换和处理、日期时间区间处理、地理空间处理、文本处理等。
-
图数据科学 (GDS) 插件——一组针对 Neo4j 的图算法和程序,允许用户对其图数据进行高级分析。GDS 提供了常见图算法(如最短路径、PageRank 和社区检测)的高效并行实现。此外,该插件还包括节点嵌入算法和机器学习工作流程,支持节点分类和链接预测工作流程。
A.1 Cypher 查询语言
Cypher 是一种用于图数据库的声明式查询语言,用于检索和操作存储在图数据库中的数据。Cypher 查询使用简单、易于阅读的语法编写。以下列表是一个简单的 Cypher 查询示例,它使用 ASCII 艺术风格的图表来展示所查询的关系。
列表 A.1 一个示例 Cypher 语句
MATCH (a:Person)-[:FOLLOWS]->(b:Person)
WHERE a.name = "Alice"
RETURN b.name
openCypher 创新项目是 Neo4j 与其他几个组织之间的合作,旨在推广 Cypher 查询语言作为处理图数据的标准。该项目的目标是创建一种通用的语言,可以用于查询任何图数据库,无论其底层技术如何。为了实现这一目标,openCypher 创新项目正在将 Cypher 语言规范和相关资源以开源许可证的形式提供,并鼓励各种组织开发 Cypher 实现。到目前为止,Cypher 查询语言已被 Amazon、AgensGraph、Katana Graph、Memgraph、RedisGraph 和 SAP HANA(openCypher Implementers Group,n.d.)采用。
此外,还有一个官方的 ISO 项目提议一个统一的图查询语言(GQL)来与图数据库交互(GQL 标准委员会,n.d.)。GQL 的目标是建立在 SQL 的基础上,并整合现有图查询语言中的成熟想法,包括 Cypher。这使得学习 Cypher 成为与图数据库交互的绝佳起点,因为它已经与许多数据库集成,并将成为官方 ISO 图查询语言的一部分。有关更多信息,请参阅 GQL 的图模式匹配提案(Deutsch 等人,2022)。
A.2 Neo4j 安装
设置您的 Neo4j 环境有几种不同的选项:
-
Neo4j Desktop
-
Neo4j Docker
-
Neo4j Aura
A.2.1 Neo4j Desktop 安装
Neo4j Desktop 是一个本地的 Neo4j 图数据库管理应用程序。它允许您只需几步点击即可创建数据库实例和安装官方插件。如果您决定使用 Neo4j Desktop,请按照以下步骤成功启动一个已安装 APOC 和 GDS 插件的 Neo4j 数据库实例:
- 从官方网站下载 Neo4j 桌面应用程序(
neo4j.com/download;图 A.1)。

图 A.1 下载 Neo4j Desktop。
-
- 在您的计算机上安装 Neo4j Desktop 应用程序,然后打开它。
-
- 完成注册步骤。您可以在下载应用程序时分配的软件密钥输入,或者通过点击“稍后注册”(图 A.2)跳过此步骤。

图 A.2 输入您的个人信息或跳过注册步骤。
-
- 在首次执行 Neo4j Desktop 时,Movies 数据库管理系统(DBMS)会自动启动。如果正在运行,请停止 Movies DBMS(图 A.3)。

图 A.3 停止默认的 Movie DBMS 数据库。
-
- 添加一个新的本地 DBMS(图 A.4)。

图 A.4 添加本地 DBMS。
-
- 为 DBMS 名称和密码输入任何值。请确保选择版本 5.9.0 或更高版本(图 A.5)。

图 A.5 定义 DBMS 密码和版本。
-
- 通过选择 DBMS 安装 APOC 和 GDS 插件,这将打开一个包含详细信息、插件和升级标签的右侧面板。选择插件标签,然后安装 APOC 和 GDS 插件(图 A.6)。

图 A.6 安装 APOC 和 GDS 插件。
-
- 启动数据库(图 A.7)。

图 A.7 启动数据库。
-
- 打开 Neo4j 浏览器(图 A.8)。

图 A.8 打开 Neo4j 浏览器。
-
- 通过在 Cypher 编辑器中键入它们来执行 Cypher 查询。对于较长的 Cypher 语句,你可以使用全屏编辑器选项(图 A.9)。

图 A.9 Neo4j 浏览器中的 Cypher 查询编辑器
A.2.2 Neo4j Docker 安装
如果你选择了 Neo4j Docker 安装,你需要在命令提示符中运行以下列表中的命令。
列表 A.2 启动 Neo4j Docker
docker run \
-p 7474:7474 -p 7687:7687 \
-d \
-v $HOME/neo4j/data:/data \
-e NEO4J_AUTH=neo4j/password \
-e 'NEO4J_PLUGINS=["apoc", "graph-data-science"]' \
neo4j:5.26.0
此命令在后台启动 Docker 化的 Neo4j。通过定义 NEO4J_PLUGINS 环境变量,APOC 和 GDS 插件会自动添加。将 data 卷挂载以持久化数据库文件是一个好习惯。数据库用户名和密码由 NEO4J_AUTH 变量指定。
在执行了列表 A.2 中的命令后,在你的网页浏览器中访问 http://localhost:7474。输入由 NEO4J_AUTH 变量指定的密码。示例中的密码是 password。
A.2.3 Neo4j Aura
Neo4j Aura 是 Neo4j 数据库的托管云实例。你可以用它来学习所有章节,除了第七章,它需要 GDS 库。不幸的是,免费版本不提供 GDS 库。如果你想使用云托管的 Neo4j Aura 来跟随本书中的示例,你需要使用 AuraDS 版本,它提供了对 GDS 算法的支持。你可以在 Neo4j 的官方网站上找到更多信息:neo4j.com/product/auradb/.
A.3 Neo4j 浏览器配置
Neo4j 浏览器有一个面向初学者的功能,可以可视化所有结果节点之间的关系,即使这些关系不是查询结果的一部分。为了避免混淆,请取消选中连接结果节点功能,如图 A.10 所示。

图 A.10 在 Neo4j 浏览器中取消选中连接结果节点。
A.4 电影数据集
在某些章节中,我们使用了电影数据集,这是一个小型且易于加载的示例数据集。以下是如何将电影数据集加载到你的 Neo4j 实例中的说明。
A.4.1 通过 Neo4j 查询指南加载数据
当你在 Neo4j 查询或 Neo4j 浏览器中时,你可以通过在指南侧边栏中找到的“电影图指南”或通过在 Neo4j 浏览器中执行 :play movies 来加载电影数据集。
A.4.2 尝试在线版本
该数据集还有一个在线只读版本,可在demo.neo4jlabs.com:7473/browser/(或者使用 Bolt,demo.neo4jlabs.com:7687)访问。数据库名称、用户名和密码均为“movies”。
A.4.3 通过 Cypher 加载
如果您想直接使用 Cypher 加载数据集,请使用以下列表中的查询。
列表 A.3 通过 Cypher 加载电影数据集
CREATE CONSTRAINT movie_title IF NOT EXISTS FOR (m:Movie)
REQUIRE m.title IS UNIQUE;
CREATE CONSTRAINT person_name IF NOT EXISTS FOR (p:Person)
REQUIRE p.name IS UNIQUE;
MERGE (TheMatrix:Movie {title:'The Matrix'}) ON CREATE SET
TheMatrix.released=1999, TheMatrix.tagline='Welcome to the Real World'
MERGE (Keanu:Person {name:'Keanu Reeves'}) ON CREATE SET Keanu.born=1964
MERGE (Carrie:Person {name:'Carrie-Anne Moss'})
ON CREATE SET Carrie.born=1967
MERGE (Laurence:Person {name:'Laurence Fishburne'})
ON CREATE SET Laurence.born=1961
MERGE (Hugo:Person {name:'Hugo Weaving'}) ON CREATE SET Hugo.born=1960
MERGE (LillyW:Person {name:'Lilly Wachowski'})
ON CREATE SET LillyW.born=1967
MERGE (LanaW:Person {name:'Lana Wachowski'}) ON CREATE SET LanaW.born=1965
MERGE (JoelS:Person {name:'Joel Silver'}) ON CREATE SET JoelS.born=1952
MERGE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix)
MERGE (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrix)
MERGE (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrix)
MERGE (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrix)
MERGE (LillyW)-[:DIRECTED]->(TheMatrix)
MERGE (LanaW)-[:DIRECTED]->(TheMatrix)
MERGE (JoelS)-[:PRODUCED]->(TheMatrix)
MERGE (Emil:Person {name:'Emil Eifrem'}) ON CREATE SET Emil.born=1978
MERGE (Emil)-[:ACTED_IN {roles:["Emil"]}]->(TheMatrix);
MERGE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded'}) ON CREATE SET
TheMatrixReloaded.released=2003, TheMatrixReloaded.tagline='Free your mind'
MERGE (Keanu:Person {name:'Keanu Reeves'}) ON CREATE SET Keanu.born=1964
MERGE (Carrie:Person {name:'Carrie-Anne Moss'})
ON CREATE SET Carrie.born=1967
MERGE (Laurence:Person {name:'Laurence Fishburne'})
ON CREATE SET Laurence.born=1961
MERGE (Hugo:Person {name:'Hugo Weaving'}) ON CREATE SET Hugo.born=1960
MERGE (LillyW:Person {name:'Lilly Wachowski'})
ON CREATE SET LillyW.born=1967
MERGE (LanaW:Person {name:'Lana Wachowski'}) ON CREATE SET LanaW.born=1965
MERGE (JoelS:Person {name:'Joel Silver'}) ON CREATE SET JoelS.born=1952
MERGE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixReloaded)
MERGE (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixReloaded)
MERGE (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixReloaded)
MERGE (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixReloaded)
MERGE (LillyW)-[:DIRECTED]->(TheMatrixReloaded)
MERGE (LanaW)-[:DIRECTED]->(TheMatrixReloaded)
MERGE (JoelS)-[:PRODUCED]->(TheMatrixReloaded);
MERGE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions'})
ON CREATE SET TheMatrixRevolutions.released=2003,
TheMatrixRevolutions.tagline='Everything that has a beginning has an end'
MERGE (Keanu:Person {name:'Keanu Reeves'}) ON CREATE SET Keanu.born=1964
MERGE (Carrie:Person {name:'Carrie-Anne Moss'})
ON CREATE SET Carrie.born=1967
MERGE (Laurence:Person {name:'Laurence Fishburne'})
ON CREATE SET Laurence.born=1961
MERGE (Hugo:Person {name:'Hugo Weaving'}) ON CREATE SET Hugo.born=1960
MERGE (LillyW:Person {name:'Lilly Wachowski'})
ON CREATE SET LillyW.born=1967
MERGE (LanaW:Person {name:'Lana Wachowski'}) ON CREATE SET LanaW.born=1965
MERGE (JoelS:Person {name:'Joel Silver'}) ON CREATE SET JoelS.born=1952
MERGE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixRevolutions)
MERGE (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixRevolutions)
MERGE (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixRevolutions)
MERGE (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixRevolutions)
MERGE (LillyW)-[:DIRECTED]->(TheMatrixRevolutions)
MERGE (LanaW)-[:DIRECTED]->(TheMatrixRevolutions)
MERGE (JoelS)-[:PRODUCED]->(TheMatrixRevolutions);
MERGE (TheDevilsAdvocate:Movie
{
title:"The Devil's Advocate",
released:1997,
tagline:'Evil has its winning ways'
})
MERGE (Keanu:Person {name:'Keanu Reeves'}) ON CREATE SET Keanu.born=1964
MERGE (Charlize:Person {name:'Charlize Theron'})
ON CREATE SET Charlize.born=1975
MERGE (Al:Person {name:'Al Pacino'}) ON CREATE SET Al.born=1940
MERGE (Taylor:Person {name:'Taylor Hackford'})
ON CREATE SET Taylor.born=1944
MERGE (Keanu)-[:ACTED_IN {roles:['Kevin Lomax']}]->(TheDevilsAdvocate)
MERGE (Charlize)-[:ACTED_IN {roles:['Mary Ann Lomax']}]->(TheDevilsAdvocate)
MERGE (Al)-[:ACTED_IN {roles:['John Milton']}]->(TheDevilsAdvocate)
MERGE (Taylor)-[:DIRECTED]->(TheDevilsAdvocate);
MERGE (AFewGoodMen:Movie {title:'A Few Good Men'})
ON CREATE SET
AFewGoodMen.released=1992,
AFewGoodMen.tagline='In the heart of the nation\'s capital,↪
↪ in a courthouse of the U.S. government, one man will stop at nothing to
↪ keep his honor, and one will stop at nothing to find the truth.'
MERGE (TomC:Person {name:'Tom Cruise'}) ON CREATE SET TomC.born=1962
MERGE (JackN:Person {name:'Jack Nicholson'}) ON CREATE SET JackN.born=1937
MERGE (DemiM:Person {name:'Demi Moore'}) ON CREATE SET DemiM.born=1962
MERGE (KevinB:Person {name:'Kevin Bacon'}) ON CREATE SET KevinB.born=1958
MERGE (KieferS:Person {name:'Kiefer Sutherland'})
ON CREATE SET KieferS.born=1966
MERGE (NoahW:Person {name:'Noah Wyle'}) ON CREATE SET NoahW.born=1971
MERGE (CubaG:Person {name:'Cuba Gooding Jr.'}) ON CREATE SET CubaG.born=1968
MERGE (KevinP:Person {name:'Kevin Pollak'}) ON CREATE SET KevinP.born=1957
MERGE (JTW:Person {name:'J.T. Walsh'}) ON CREATE SET JTW.born=1943
MERGE (JamesM:Person {name:'James Marshall'}) ON CREATE SET JamesM.born=1967
MERGE (ChristopherG:Person {name:'Christopher Guest'})
ON CREATE SET ChristopherG.born=1948
MERGE (RobR:Person {name:'Rob Reiner'}) ON CREATE SET RobR.born=1947
MERGE (AaronS:Person {name:'Aaron Sorkin'}) ON CREATE SET AaronS.born=1961
MERGE (TomC)-[:ACTED_IN {roles:['Lt. Daniel Kaffee']}]->(AFewGoodMen)
MERGE (JackN)-[:ACTED_IN {roles:['Col. Nathan R. Jessup']}]->(AFewGoodMen)
MERGE (DemiM)-[:ACTED_IN {
roles:['Lt. Cdr. JoAnne Galloway']
}]->(AFewGoodMen)
MERGE (KevinB)-[:ACTED_IN {
roles:['Capt. Jack Ross']
}]->(AFewGoodMen)
MERGE (KieferS)-[:ACTED_IN {roles:['Lt. Jonathan Kendrick']}]->(AFewGoodMen)
MERGE (NoahW)-[:ACTED_IN {roles:['Cpl. Jeffrey Barnes']}]->(AFewGoodMen)
MERGE (CubaG)-[:ACTED_IN {roles:['Cpl. Carl Hammaker']}]->(AFewGoodMen)
MERGE (KevinP)-[:ACTED_IN {roles:['Lt. Sam Weinberg']}]->(AFewGoodMen)
MERGE (JTW)-[:ACTED_IN {
roles:['Lt. Col. Matthew Andrew Markinson']
}]->(AFewGoodMen)
MERGE (JamesM)-[:ACTED_IN {roles:['Pfc. Louden Downey']}]->(AFewGoodMen)
MERGE (ChristopherG)-[:ACTED_IN {roles:['Dr. Stone']}]->(AFewGoodMen)
MERGE (AaronS)-[:ACTED_IN {roles:['Man in Bar']}]->(AFewGoodMen)
MERGE (RobR)-[:DIRECTED]->(AFewGoodMen)
MERGE (AaronS)-[:WROTE]->(AFewGoodMen);
MERGE (TopGun:Movie {title:'Top Gun'}) ON CREATE SET
TopGun.released=1986, TopGun.tagline='I feel the need, the need for speed.'
MERGE (TomC:Person {name:'Tom Cruise'}) ON CREATE SET TomC.born=1962
MERGE (KellyM:Person {name:'Kelly McGillis'}) ON CREATE SET KellyM.born=1957
MERGE (ValK:Person {name:'Val Kilmer'}) ON CREATE SET ValK.born=1959
MERGE (AnthonyE:Person {name:'Anthony Edwards'})
ON CREATE SET AnthonyE.born=1962
MERGE (TomS:Person {name:'Tom Skerritt'}) ON CREATE SET TomS.born=1933
MERGE (MegR:Person {name:'Meg Ryan'}) ON CREATE SET MegR.born=1961
MERGE (TonyS:Person {name:'Tony Scott'}) ON CREATE SET TonyS.born=1944
MERGE (JimC:Person {name:'Jim Cash'}) ON CREATE SET JimC.born=1941
MERGE (TomC)-[:ACTED_IN {roles:['Maverick']}]->(TopGun)
MERGE (KellyM)-[:ACTED_IN {roles:['Charlie']}]->(TopGun)
MERGE (ValK)-[:ACTED_IN {roles:['Iceman']}]->(TopGun)
MERGE (AnthonyE)-[:ACTED_IN {roles:['Goose']}]->(TopGun)
MERGE (TomS)-[:ACTED_IN {roles:['Viper']}]->(TopGun)
MERGE (MegR)-[:ACTED_IN {roles:['Carole']}]->(TopGun)
MERGE (TonyS)-[:DIRECTED]->(TopGun)
MERGE (JimC)-[:WROTE]->(TopGun);
MERGE (JerryMaguire:Movie {title:'Jerry Maguire'}) ON CREATE SET
JerryMaguire.released=2000,
JerryMaguire.tagline='The rest of his life begins now.'
MERGE (TomC:Person {name:'Tom Cruise'}) ON CREATE SET TomC.born=1962
MERGE (CubaG:Person {name:'Cuba Gooding Jr.'}) ON CREATE SET CubaG.born=1968
MERGE (ReneeZ:Person {name:'Renee Zellweger'})
ON CREATE SET ReneeZ.born=1969
MERGE (KellyP:Person {name:'Kelly Preston'}) ON CREATE SET KellyP.born=1962
MERGE (JerryO:Person {name:'Jerry O\'Connell'})
ON CREATE SET JerryO.born=1974
MERGE (JayM:Person {name:'Jay Mohr'}) ON CREATE SET JayM.born=1970
MERGE (BonnieH:Person {name:'Bonnie Hunt'}) ON CREATE SET BonnieH.born=1961
MERGE (ReginaK:Person {name:'Regina King'}) ON CREATE SET ReginaK.born=1971
MERGE (JonathanL:Person {name:'Jonathan Lipnicki'})
ON CREATE SET JonathanL.born=1996
MERGE (CameronC:Person {name:'Cameron Crowe'})
ON CREATE SET CameronC.born=1957
MERGE (TomC)-[:ACTED_IN {roles:['Jerry Maguire']}]->(JerryMaguire)
MERGE (CubaG)-[:ACTED_IN {roles:['Rod Tidwell']}]->(JerryMaguire)
MERGE (ReneeZ)-[:ACTED_IN {roles:['Dorothy Boyd']}]->(JerryMaguire)
MERGE (KellyP)-[:ACTED_IN {roles:['Avery Bishop']}]->(JerryMaguire)
MERGE (JerryO)-[:ACTED_IN {roles:['Frank Cushman']}]->(JerryMaguire)
MERGE (JayM)-[:ACTED_IN {roles:['Bob Sugar']}]->(JerryMaguire)
MERGE (BonnieH)-[:ACTED_IN {roles:['Laurel Boyd']}]->(JerryMaguire)
MERGE (ReginaK)-[:ACTED_IN {roles:['Marcee Tidwell']}]->(JerryMaguire)
MERGE (JonathanL)-[:ACTED_IN {roles:['Ray Boyd']}]->(JerryMaguire)
MERGE (CameronC)-[:DIRECTED]->(JerryMaguire)
MERGE (CameronC)-[:PRODUCED]->(JerryMaguire)
MERGE (CameronC)-[:WROTE]->(JerryMaguire);
MERGE (StandByMe:Movie {title:'Stand By Me'})
ON CREATE SET StandByMe.released=1986,
StandByMe.tagline='For some, it\'s the last real taste of innocence, and↪
↪ the first real taste of life. But for everyone, it\'s the time that↪
↪ memories are made of.'
MERGE (RiverP:Person {name:'River Phoenix'}) ON CREATE SET RiverP.born=1970
MERGE (CoreyF:Person {name:'Corey Feldman'}) ON CREATE SET CoreyF.born=1971
MERGE (JerryO:Person {name:'Jerry O\'Connell'})
ON CREATE SET JerryO.born=1974
MERGE (WilW:Person {name:'Wil Wheaton'}) ON CREATE SET WilW.born=1972
MERGE (KieferS:Person {name:'Kiefer Sutherland'})
ON CREATE SET KieferS.born=1966
MERGE (JohnC:Person {name:'John Cusack'}) ON CREATE SET JohnC.born=1966
MERGE (MarshallB:Person {name:'Marshall Bell'})
ON CREATE SET MarshallB.born=1942
MERGE (RobR:Person {name:'Rob Reiner'}) ON CREATE SET RobR.born=1947
MERGE (WilW)-[:ACTED_IN {roles:['Gordie Lachance']}]->(StandByMe)
MERGE (RiverP)-[:ACTED_IN {roles:['Chris Chambers']}]->(StandByMe)
MERGE (JerryO)-[:ACTED_IN {roles:['Vern Tessio']}]->(StandByMe)
MERGE (CoreyF)-[:ACTED_IN {roles:['Teddy Duchamp']}]->(StandByMe)
MERGE (JohnC)-[:ACTED_IN {roles:['Denny Lachance']}]->(StandByMe)
MERGE (KieferS)-[:ACTED_IN {roles:['Ace Merrill']}]->(StandByMe)
MERGE (MarshallB)-[:ACTED_IN {roles:['Mr. Lachance']}]->(StandByMe)
MERGE (RobR)-[:DIRECTED]->(StandByMe);
MERGE (AsGoodAsItGets:Movie {title:'As Good as It Gets'})
ON CREATE SET AsGoodAsItGets.released=1997,
AsGoodAsItGets.tagline='A comedy from the heart that goes for the throat.'
MERGE (JackN:Person {name:'Jack Nicholson'}) ON CREATE SET JackN.born=1937
MERGE (HelenH:Person {name:'Helen Hunt'}) ON CREATE SET HelenH.born=1963
MERGE (GregK:Person {name:'Greg Kinnear'}) ON CREATE SET GregK.born=1963
MERGE (JamesB:Person {name:'James L. Brooks'})
ON CREATE SET JamesB.born=1940
MERGE (CubaG:Person {name:'Cuba Gooding Jr.'}) ON CREATE SET CubaG.born=1968
MERGE (JackN)-[:ACTED_IN {roles:['Melvin Udall']}]->(AsGoodAsItGets)
MERGE (HelenH)-[:ACTED_IN {roles:['Carol Connelly']}]->(AsGoodAsItGets)
MERGE (GregK)-[:ACTED_IN {roles:['Simon Bishop']}]->(AsGoodAsItGets)
MERGE (CubaG)-[:ACTED_IN {roles:['Frank Sachs']}]->(AsGoodAsItGets)
MERGE (JamesB)-[:DIRECTED]->(AsGoodAsItGets);
MERGE (WhatDreamsMayCome:Movie {title:'What Dreams May Come'})
ON CREATE SET WhatDreamsMayCome.released=1998,
WhatDreamsMayCome.tagline='After life there is more. The end is just the↪
↪ beginning.'
MERGE (AnnabellaS:Person {name:'Annabella Sciorra'})
ON CREATE SET AnnabellaS.born=1960
MERGE (MaxS:Person {name:'Max von Sydow'}) ON CREATE SET MaxS.born=1929
MERGE (WernerH:Person {name:'Werner Herzog'})
ON CREATE SET WernerH.born=1942
MERGE (Robin:Person {name:'Robin Williams'}) ON CREATE SET Robin.born=1951
MERGE (VincentW:Person {name:'Vincent Ward'})
ON CREATE SET VincentW.born=1956
MERGE (CubaG:Person {name:'Cuba Gooding Jr.'}) ON CREATE SET CubaG.born=1968
MERGE (Robin)-[:ACTED_IN {roles:['Chris Nielsen']}]->(WhatDreamsMayCome)
MERGE (CubaG)-[:ACTED_IN {roles:['Albert Lewis']}]->(WhatDreamsMayCome)
MERGE (AnnabellaS)-[:ACTED_IN {
roles:['Annie Collins-Nielsen']
}]->(WhatDreamsMayCome)
MERGE (MaxS)-[:ACTED_IN {roles:['The Tracker']}]->(WhatDreamsMayCome)
MERGE (WernerH)-[:ACTED_IN {roles:['The Face']}]->(WhatDreamsMayCome)
MERGE (VincentW)-[:DIRECTED]->(WhatDreamsMayCome);
MERGE (SnowFallingonCedars:Movie {title:'Snow Falling on Cedars'})
ON CREATE SET SnowFallingonCedars.released=1999,
SnowFallingonCedars.tagline='First loves last. Forever.'
MERGE (EthanH:Person {name:'Ethan Hawke'}) ON CREATE SET EthanH.born=1970
MERGE (RickY:Person {name:'Rick Yune'}) ON CREATE SET RickY.born=1971
MERGE (JamesC:Person {name:'James Cromwell'}) ON CREATE SET JamesC.born=1940
MERGE (ScottH:Person {name:'Scott Hicks'}) ON CREATE SET ScottH.born=1953
MERGE (MaxS:Person {name:'Max von Sydow'}) ON CREATE SET MaxS.born=1929
MERGE (EthanH)-[:ACTED_IN {
roles:['Ishmael Chambers']
}]->(SnowFallingonCedars)
MERGE (RickY)-[:ACTED_IN {roles:['Kazuo Miyamoto']}]->(SnowFallingonCedars)
MERGE (MaxS)-[:ACTED_IN {roles:['Nels Gudmundsson']}]->(SnowFallingonCedars)
MERGE (JamesC)-[:ACTED_IN {roles:['Judge Fielding']}]->(SnowFallingonCedars)
MERGE (ScottH)-[:DIRECTED]->(SnowFallingonCedars);
MERGE (YouveGotMail:Movie {title:'You\'ve Got Mail'}) ON CREATE SET
YouveGotMail.released=1998,
YouveGotMail.tagline='At odds in life... in love on-line.'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (MegR:Person {name:'Meg Ryan'}) ON CREATE SET MegR.born=1961
MERGE (GregK:Person {name:'Greg Kinnear'}) ON CREATE SET GregK.born=1963
MERGE (ParkerP:Person {name:'Parker Posey'}) ON CREATE SET ParkerP.born=1968
MERGE (DaveC:Person {name:'Dave Chappelle'}) ON CREATE SET DaveC.born=1973
MERGE (SteveZ:Person {name:'Steve Zahn'}) ON CREATE SET SteveZ.born=1967
MERGE (NoraE:Person {name:'Nora Ephron'}) ON CREATE SET NoraE.born=1941
MERGE (TomH)-[:ACTED_IN {roles:['Joe Fox']}]->(YouveGotMail)
MERGE (MegR)-[:ACTED_IN {roles:['Kathleen Kelly']}]->(YouveGotMail)
MERGE (GregK)-[:ACTED_IN {roles:['Frank Navasky']}]->(YouveGotMail)
MERGE (ParkerP)-[:ACTED_IN {roles:['Patricia Eden']}]->(YouveGotMail)
MERGE (DaveC)-[:ACTED_IN {roles:['Kevin Jackson']}]->(YouveGotMail)
MERGE (SteveZ)-[:ACTED_IN {roles:['George Pappas']}]->(YouveGotMail)
MERGE (NoraE)-[:DIRECTED]->(YouveGotMail);
MERGE (SleeplessInSeattle:Movie {title:'Sleepless in Seattle'})
ON CREATE SET SleeplessInSeattle.released=1993,
SleeplessInSeattle.tagline='What if someone you never met, someone you never↪
↪ saw, someone you never knew was the only someone for you?'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (MegR:Person {name:'Meg Ryan'}) ON CREATE SET MegR.born=1961
MERGE (RitaW:Person {name:'Rita Wilson'}) ON CREATE SET RitaW.born=1956
MERGE (BillPull:Person {name:'Bill Pullman'})
ON CREATE SET BillPull.born=1953
MERGE (VictorG:Person {name:'Victor Garber'})
ON CREATE SET VictorG.born=1949
MERGE (RosieO:Person {name:'Rosie O\'Donnell'})
ON CREATE SET RosieO.born=1962
MERGE (NoraE:Person {name:'Nora Ephron'}) ON CREATE SET NoraE.born=1941
MERGE (TomH)-[:ACTED_IN {roles:['Sam Baldwin']}]->(SleeplessInSeattle)
MERGE (MegR)-[:ACTED_IN {roles:['Annie Reed']}]->(SleeplessInSeattle)
MERGE (RitaW)-[:ACTED_IN {roles:['Suzy']}]->(SleeplessInSeattle)
MERGE (BillPull)-[:ACTED_IN {roles:['Walter']}]->(SleeplessInSeattle)
MERGE (VictorG)-[:ACTED_IN {roles:['Greg']}]->(SleeplessInSeattle)
MERGE (RosieO)-[:ACTED_IN {roles:['Becky']}]->(SleeplessInSeattle)
MERGE (NoraE)-[:DIRECTED]->(SleeplessInSeattle);
MERGE (JoeVersustheVolcano:Movie {title:'Joe Versus the Volcano'})
ON CREATE SET JoeVersustheVolcano.released=1990,
JoeVersustheVolcano.tagline='A story of love, lava and burning desire.'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (MegR:Person {name:'Meg Ryan'}) ON CREATE SET MegR.born=1961
MERGE (JohnS:Person {name:'John Patrick Stanley'})
ON CREATE SET JohnS.born=1950
MERGE (Nathan:Person {name:'Nathan Lane'}) ON CREATE SET Nathan.born=1956
MERGE (TomH)-[:ACTED_IN {roles:['Joe Banks']}]->(JoeVersustheVolcano)
MERGE (MegR)-[:ACTED_IN {
roles:['DeDe', 'Angelica Graynamore', 'Patricia Graynamore']
}]->(JoeVersustheVolcano)
MERGE (Nathan)-[:ACTED_IN {roles:['Baw']}]->(JoeVersustheVolcano)
MERGE (JohnS)-[:DIRECTED]->(JoeVersustheVolcano);
MERGE (WhenHarryMetSally:Movie {title:'When Harry Met Sally'}) ON CREATE SET
WhenHarryMetSally.released=1998,
WhenHarryMetSally.tagline='Can two friends sleep together and still love
↪ each other in the morning?'
MERGE (MegR:Person {name:'Meg Ryan'}) ON CREATE SET MegR.born=1961
MERGE (BillyC:Person {name:'Billy Crystal'}) ON CREATE SET BillyC.born=1948
MERGE (CarrieF:Person {name:'Carrie Fisher'})
ON CREATE SET CarrieF.born=1956
MERGE (BrunoK:Person {name:'Bruno Kirby'}) ON CREATE SET BrunoK.born=1949
MERGE (RobR:Person {name:'Rob Reiner'}) ON CREATE SET RobR.born=1947
MERGE (NoraE:Person {name:'Nora Ephron'}) ON CREATE SET NoraE.born=1941
MERGE (BillyC)-[:ACTED_IN {roles:['Harry Burns']}]->(WhenHarryMetSally)
MERGE (MegR)-[:ACTED_IN {roles:['Sally Albright']}]->(WhenHarryMetSally)
MERGE (CarrieF)-[:ACTED_IN {roles:['Marie']}]->(WhenHarryMetSally)
MERGE (BrunoK)-[:ACTED_IN {roles:['Jess']}]->(WhenHarryMetSally)
MERGE (RobR)-[:DIRECTED]->(WhenHarryMetSally)
MERGE (RobR)-[:PRODUCED]->(WhenHarryMetSally)
MERGE (NoraE)-[:PRODUCED]->(WhenHarryMetSally)
MERGE (NoraE)-[:WROTE]->(WhenHarryMetSally);
MERGE (ThatThingYouDo:Movie {title:'That Thing You Do'})
ON CREATE SET ThatThingYouDo.released=1996,
ThatThingYouDo.tagline='In every life there comes a time when that thing you↪
↪ dream becomes that thing you do'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (LivT:Person {name:'Liv Tyler'}) ON CREATE SET LivT.born=1977
MERGE (Charlize:Person {name:'Charlize Theron'})
ON CREATE SET Charlize.born=1975
MERGE (TomH)-[:ACTED_IN {roles:['Mr. White']}]->(ThatThingYouDo)
MERGE (LivT)-[:ACTED_IN {roles:['Faye Dolan']}]->(ThatThingYouDo)
MERGE (Charlize)-[:ACTED_IN {roles:['Tina']}]->(ThatThingYouDo)
MERGE (TomH)-[:DIRECTED]->(ThatThingYouDo);
MERGE (TheReplacements:Movie {title:'The Replacements'}) ON CREATE SET
TheReplacements.released=2000,
TheReplacements.tagline='Pain heals, Chicks dig scars... Glory lasts forever'
MERGE (Keanu:Person {name:'Keanu Reeves'}) ON CREATE SET Keanu.born=1964
MERGE (Brooke:Person {name:'Brooke Langton'}) ON CREATE SET Brooke.born=1970
MERGE (Gene:Person {name:'Gene Hackman'}) ON CREATE SET Gene.born=1930
MERGE (Orlando:Person {name:'Orlando Jones'})
ON CREATE SET Orlando.born=1968
MERGE (Howard:Person {name:'Howard Deutch'}) ON CREATE SET Howard.born=1950
MERGE (Keanu)-[:ACTED_IN {roles:['Shane Falco']}]->(TheReplacements)
MERGE (Brooke)-[:ACTED_IN {roles:['Annabelle Farrell']}]->(TheReplacements)
MERGE (Gene)-[:ACTED_IN {roles:['Jimmy McGinty']}]->(TheReplacements)
MERGE (Orlando)-[:ACTED_IN {roles:['Clifford Franklin']}]->(TheReplacements)
MERGE (Howard)-[:DIRECTED]->(TheReplacements);
MERGE (RescueDawn:Movie {title:'RescueDawn'}) ON CREATE SET
RescueDawn.released=2006,
RescueDawn.tagline='Based on the extraordinary true story of one man\'s↪
↪ fight for freedom'
MERGE (ChristianB:Person {name:'Christian Bale'})
ON CREATE SET ChristianB.born=1974
MERGE (ZachG:Person {name:'Zach Grenier'}) ON CREATE SET ZachG.born=1954
MERGE (MarshallB:Person {name:'Marshall Bell'})
ON CREATE SET MarshallB.born=1942
MERGE (SteveZ:Person {name:'Steve Zahn'}) ON CREATE SET SteveZ.born=1967
MERGE (WernerH:Person {name:'Werner Herzog'})
ON CREATE SET WernerH.born=1942
MERGE (MarshallB)-[:ACTED_IN {roles:['Admiral']}]->(RescueDawn)
MERGE (ChristianB)-[:ACTED_IN {roles:['Dieter Dengler']}]->(RescueDawn)
MERGE (ZachG)-[:ACTED_IN {roles:['Squad Leader']}]->(RescueDawn)
MERGE (SteveZ)-[:ACTED_IN {roles:['Duane']}]->(RescueDawn)
MERGE (WernerH)-[:DIRECTED]->(RescueDawn);
MERGE (TheBirdcage:Movie {title:'The Birdcage'}) ON CREATE SET
TheBirdcage.released=1996, TheBirdcage.tagline='Come as you are'
MERGE (MikeN:Person {name:'Mike Nichols'}) ON CREATE SET MikeN.born=1931
MERGE (Robin:Person {name:'Robin Williams'}) ON CREATE SET Robin.born=1951
MERGE (Nathan:Person {name:'Nathan Lane'}) ON CREATE SET Nathan.born=1956
MERGE (Gene:Person {name:'Gene Hackman'}) ON CREATE SET Gene.born=1930
MERGE (Robin)-[:ACTED_IN {roles:['Armand Goldman']}]->(TheBirdcage)
MERGE (Nathan)-[:ACTED_IN {roles:['Albert Goldman']}]->(TheBirdcage)
MERGE (Gene)-[:ACTED_IN {roles:['Sen. Kevin Keeley']}]->(TheBirdcage)
MERGE (MikeN)-[:DIRECTED]->(TheBirdcage);
MERGE (Unforgiven:Movie {title:'Unforgiven'}) ON CREATE SET
Unforgiven.released=1992,
Unforgiven.tagline='It\'s a hell of a thing, killing a man'
MERGE (Gene:Person {name:'Gene Hackman'}) ON CREATE SET Gene.born=1930
MERGE (RichardH:Person {name:'Richard Harris'})
ON CREATE SET RichardH.born=1930
MERGE (ClintE:Person {name:'Clint Eastwood'}) ON CREATE SET ClintE.born=1930
MERGE (RichardH)-[:ACTED_IN {roles:['English Bob']}]->(Unforgiven)
MERGE (ClintE)-[:ACTED_IN {roles:['Bill Munny']}]->(Unforgiven)
MERGE (Gene)-[:ACTED_IN {roles:['Little Bill Daggett']}]->(Unforgiven)
MERGE (ClintE)-[:DIRECTED]->(Unforgiven);
MERGE (JohnnyMnemonic:Movie {title:'Johnny Mnemonic'}) ON CREATE SET
JohnnyMnemonic.released=1995,
JohnnyMnemonic.tagline='The hottest data on earth. In the coolest head in↪
↪ town'
MERGE (Keanu:Person {name:'Keanu Reeves'}) ON CREATE SET Keanu.born=1964
MERGE (Takeshi:Person {name:'Takeshi Kitano'})
ON CREATE SET Takeshi.born=1947
MERGE (Dina:Person {name:'Dina Meyer'}) ON CREATE SET Dina.born=1968
MERGE (IceT:Person {name:'Ice-T'}) ON CREATE SET IceT.born=1958
MERGE (RobertL:Person {name:'Robert Longo'}) ON CREATE SET RobertL.born=1953
MERGE (Keanu)-[:ACTED_IN {roles:['Johnny Mnemonic']}]->(JohnnyMnemonic)
MERGE (Takeshi)-[:ACTED_IN {roles:['Takahashi']}]->(JohnnyMnemonic)
MERGE (Dina)-[:ACTED_IN {roles:['Jane']}]->(JohnnyMnemonic)
MERGE (IceT)-[:ACTED_IN {roles:['J-Bone']}]->(JohnnyMnemonic)
MERGE (RobertL)-[:DIRECTED]->(JohnnyMnemonic);
MERGE (CloudAtlas:Movie {title:'Cloud Atlas'}) ON CREATE SET
CloudAtlas.released=2012, CloudAtlas.tagline='Everything is connected'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (Hugo:Person {name:'Hugo Weaving'}) ON CREATE SET Hugo.born=1960
MERGE (HalleB:Person {name:'Halle Berry'}) ON CREATE SET HalleB.born=1966
MERGE (JimB:Person {name:'Jim Broadbent'}) ON CREATE SET JimB.born=1949
MERGE (TomT:Person {name:'Tom Tykwer'}) ON CREATE SET TomT.born=1965
MERGE (DavidMitchell:Person {name:'David Mitchell'})
ON CREATE SET DavidMitchell.born=1969
MERGE (StefanArndt:Person {name:'Stefan Arndt'})
ON CREATE SET StefanArndt.born=1961
MERGE (LillyW:Person {name:'Lilly Wachowski'})
ON CREATE SET LillyW.born=1967
MERGE (LanaW:Person {name:'Lana Wachowski'}) ON CREATE SET LanaW.born=1965
MERGE (TomH)-[:ACTED_IN {
roles:['Zachry', 'Dr. Henry Goose', 'Isaac Sachs', 'Dermot Hoggins']
}]->(CloudAtlas)
MERGE (Hugo)-[:ACTED_IN {
roles:[
'Bill Smoke',
'Haskell Moore',
'Tadeusz Kesselring',
'Nurse Noakes',
'Boardman Mephi',
'Old Georgie'
]
}]->(CloudAtlas)
MERGE (HalleB)-[:ACTED_IN {
roles:['Luisa Rey', 'Jocasta Ayrs', 'Ovid', 'Meronym']
}]->(CloudAtlas)
MERGE (JimB)-[:ACTED_IN {
roles:['Vyvyan Ayrs', 'Captain Molyneux', 'Timothy Cavendish']
}]->(CloudAtlas)
MERGE (TomT)-[:DIRECTED]->(CloudAtlas)
MERGE (LillyW)-[:DIRECTED]->(CloudAtlas)
MERGE (LanaW)-[:DIRECTED]->(CloudAtlas)
MERGE (DavidMitchell)-[:WROTE]->(CloudAtlas)
MERGE (StefanArndt)-[:PRODUCED]->(CloudAtlas);
MERGE (TheDaVinciCode:Movie {title:'The Da Vinci Code'}) ON CREATE SET
TheDaVinciCode.released=2006, TheDaVinciCode.tagline='Break The Codes'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (IanM:Person {name:'Ian McKellen'}) ON CREATE SET IanM.born=1939
MERGE (AudreyT:Person {name:'Audrey Tautou'})
ON CREATE SET AudreyT.born=1976
MERGE (PaulB:Person {name:'Paul Bettany'}) ON CREATE SET PaulB.born=1971
MERGE (RonH:Person {name:'Ron Howard'}) ON CREATE SET RonH.born=1954
MERGE (TomH)-[:ACTED_IN {roles:['Dr. Robert Langdon']}]->(TheDaVinciCode)
MERGE (IanM)-[:ACTED_IN {roles:['Sir Leight Teabing']}]->(TheDaVinciCode)
MERGE (AudreyT)-[:ACTED_IN {roles:['Sophie Neveu']}]->(TheDaVinciCode)
MERGE (PaulB)-[:ACTED_IN {roles:['Silas']}]->(TheDaVinciCode)
MERGE (RonH)-[:DIRECTED]->(TheDaVinciCode);
MERGE (VforVendetta:Movie {title:'V for Vendetta'}) ON CREATE SET
VforVendetta.released=2006, VforVendetta.tagline='Freedom! Forever!'
MERGE (Hugo:Person {name:'Hugo Weaving'}) ON CREATE SET Hugo.born=1960
MERGE (NatalieP:Person {name:'Natalie Portman'})
ON CREATE SET NatalieP.born=1981
MERGE (StephenR:Person {name:'Stephen Rea'})
ON CREATE SET StephenR.born=1946
MERGE (JohnH:Person {name:'John Hurt'}) ON CREATE SET JohnH.born=1940
MERGE (BenM:Person {name:'Ben Miles'}) ON CREATE SET BenM.born=1967
MERGE (LillyW:Person {name:'Lilly Wachowski'})
ON CREATE SET LillyW.born=1967
MERGE (LanaW:Person {name:'Lana Wachowski'}) ON CREATE SET LanaW.born=1965
MERGE (JamesM:Person {name:'James Marshall'}) ON CREATE SET JamesM.born=1967
MERGE (JoelS:Person {name:'Joel Silver'}) ON CREATE SET JoelS.born=1952
MERGE (Hugo)-[:ACTED_IN {roles:['V']}]->(VforVendetta)
MERGE (NatalieP)-[:ACTED_IN {roles:['Evey Hammond']}]->(VforVendetta)
MERGE (StephenR)-[:ACTED_IN {roles:['Eric Finch']}]->(VforVendetta)
MERGE (JohnH)-[:ACTED_IN {
roles:['High Chancellor Adam Sutler']
}]->(VforVendetta)
MERGE (BenM)-[:ACTED_IN {roles:['Dascomb']}]->(VforVendetta)
MERGE (JamesM)-[:DIRECTED]->(VforVendetta)
MERGE (LillyW)-[:PRODUCED]->(VforVendetta)
MERGE (LanaW)-[:PRODUCED]->(VforVendetta)
MERGE (JoelS)-[:PRODUCED]->(VforVendetta)
MERGE (LillyW)-[:WROTE]->(VforVendetta)
MERGE (LanaW)-[:WROTE]->(VforVendetta);
MERGE (SpeedRacer:Movie {title:'Speed Racer'}) ON CREATE SET
SpeedRacer.released=2008, SpeedRacer.tagline='Speed has no limits'
MERGE (EmileH:Person {name:'Emile Hirsch'}) ON CREATE SET EmileH.born=1985
MERGE (JohnG:Person {name:'John Goodman'}) ON CREATE SET JohnG.born=1960
MERGE (SusanS:Person {name:'Susan Sarandon'}) ON CREATE SET SusanS.born=1946
MERGE (MatthewF:Person {name:'Matthew Fox'})
ON CREATE SET MatthewF.born=1966
MERGE (ChristinaR:Person {name:'Christina Ricci'})
ON CREATE SET ChristinaR.born=1980
MERGE (Rain:Person {name:'Rain'}) ON CREATE SET Rain.born=1982
MERGE (BenM:Person {name:'Ben Miles'}) ON CREATE SET BenM.born=1967
MERGE (LillyW:Person {name:'Lilly Wachowski'})
ON CREATE SET LillyW.born=1967
MERGE (LanaW:Person {name:'Lana Wachowski'}) ON CREATE SET LanaW.born=1965
MERGE (JoelS:Person {name:'Joel Silver'}) ON CREATE SET JoelS.born=1952
MERGE (EmileH)-[:ACTED_IN {roles:['Speed Racer']}]->(SpeedRacer)
MERGE (JohnG)-[:ACTED_IN {roles:['Pops']}]->(SpeedRacer)
MERGE (SusanS)-[:ACTED_IN {roles:['Mom']}]->(SpeedRacer)
MERGE (MatthewF)-[:ACTED_IN {roles:['Racer X']}]->(SpeedRacer)
MERGE (ChristinaR)-[:ACTED_IN {roles:['Trixie']}]->(SpeedRacer)
MERGE (Rain)-[:ACTED_IN {roles:['Taejo Togokahn']}]->(SpeedRacer)
MERGE (BenM)-[:ACTED_IN {roles:['Cass Jones']}]->(SpeedRacer)
MERGE (LillyW)-[:DIRECTED]->(SpeedRacer)
MERGE (LanaW)-[:DIRECTED]->(SpeedRacer)
MERGE (LillyW)-[:WROTE]->(SpeedRacer)
MERGE (LanaW)-[:WROTE]->(SpeedRacer)
MERGE (JoelS)-[:PRODUCED]->(SpeedRacer);
MERGE (NinjaAssassin:Movie {title:'Ninja Assassin'}) ON CREATE SET
NinjaAssassin.released=2009,
NinjaAssassin.tagline='Prepare to enter a secret world of assassins'
MERGE (NaomieH:Person {name:'Naomie Harris'})
MERGE (Rain:Person {name:'Rain'}) ON CREATE SET Rain.born=1982
MERGE (BenM:Person {name:'Ben Miles'}) ON CREATE SET BenM.born=1967
MERGE (LillyW:Person {name:'Lilly Wachowski'})
ON CREATE SET LillyW.born=1967
MERGE (LanaW:Person {name:'Lana Wachowski'}) ON CREATE SET LanaW.born=1965
MERGE (RickY:Person {name:'Rick Yune'}) ON CREATE SET RickY.born=1971
MERGE (JamesM:Person {name:'James Marshall'}) ON CREATE SET JamesM.born=1967
MERGE (JoelS:Person {name:'Joel Silver'}) ON CREATE SET JoelS.born=1952
MERGE (Rain)-[:ACTED_IN {roles:['Raizo']}]->(NinjaAssassin)
MERGE (NaomieH)-[:ACTED_IN {roles:['Mika Coretti']}]->(NinjaAssassin)
MERGE (RickY)-[:ACTED_IN {roles:['Takeshi']}]->(NinjaAssassin)
MERGE (BenM)-[:ACTED_IN {roles:['Ryan Maslow']}]->(NinjaAssassin)
MERGE (JamesM)-[:DIRECTED]->(NinjaAssassin)
MERGE (LillyW)-[:PRODUCED]->(NinjaAssassin)
MERGE (LanaW)-[:PRODUCED]->(NinjaAssassin)
MERGE (JoelS)-[:PRODUCED]->(NinjaAssassin);
MERGE (TheGreenMile:Movie {title:'The Green Mile'}) ON CREATE SET
TheGreenMile.released=1999,
TheGreenMile.tagline='Walk a mile you\'ll never forget.'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (JamesC:Person {name:'James Cromwell'}) ON CREATE SET JamesC.born=1940
MERGE (BonnieH:Person {name:'Bonnie Hunt'}) ON CREATE SET BonnieH.born=1961
MERGE (MichaelD:Person {name:'Michael Clarke Duncan'})
ON CREATE SET MichaelD.born=1957
MERGE (DavidM:Person {name:'David Morse'}) ON CREATE SET DavidM.born=1953
MERGE (SamR:Person {name:'Sam Rockwell'}) ON CREATE SET SamR.born=1968
MERGE (GaryS:Person {name:'Gary Sinise'}) ON CREATE SET GaryS.born=1955
MERGE (PatriciaC:Person {name:'Patricia Clarkson'})
ON CREATE SET PatriciaC.born=1959
MERGE (FrankD:Person {name:'Frank Darabont'}) ON CREATE SET FrankD.born=1959
MERGE (TomH)-[:ACTED_IN {roles:['Paul Edgecomb']}]->(TheGreenMile)
MERGE (MichaelD)-[:ACTED_IN {roles:['John Coffey']}]->(TheGreenMile)
MERGE (DavidM)-[:ACTED_IN {
roles:['Brutus "Brutal" Howell']
}]->(TheGreenMile)
MERGE (BonnieH)-[:ACTED_IN {roles:['Jan Edgecomb']}]->(TheGreenMile)
MERGE (JamesC)-[:ACTED_IN {roles:['Warden Hal Moores']}]->(TheGreenMile)
MERGE (SamR)-[:ACTED_IN {roles:['"Wild Bill" Wharton']}]->(TheGreenMile)
MERGE (GaryS)-[:ACTED_IN {roles:['Burt Hammersmith']}]->(TheGreenMile)
MERGE (PatriciaC)-[:ACTED_IN {roles:['Melinda Moores']}]->(TheGreenMile)
MERGE (FrankD)-[:DIRECTED]->(TheGreenMile);
MERGE (FrostNixon:Movie {title:'Frost/Nixon'}) ON CREATE SET
FrostNixon.released=2008,
FrostNixon.tagline='400 million people were waiting for the truth.'
MERGE (FrankL:Person {name:'Frank Langella'}) ON CREATE SET FrankL.born=1938
MERGE (MichaelS:Person {name:'Michael Sheen'})
ON CREATE SET MichaelS.born=1969
MERGE (OliverP:Person {name:'Oliver Platt'}) ON CREATE SET OliverP.born=1960
MERGE (KevinB:Person {name:'Kevin Bacon'}) ON CREATE SET KevinB.born=1958
MERGE (SamR:Person {name:'Sam Rockwell'}) ON CREATE SET SamR.born=1968
MERGE (RonH:Person {name:'Ron Howard'}) ON CREATE SET RonH.born=1954
MERGE (FrankL)-[:ACTED_IN {roles:['Richard Nixon']}]->(FrostNixon)
MERGE (MichaelS)-[:ACTED_IN {roles:['David Frost']}]->(FrostNixon)
MERGE (KevinB)-[:ACTED_IN {roles:['Jack Brennan']}]->(FrostNixon)
MERGE (OliverP)-[:ACTED_IN {roles:['Bob Zelnick']}]->(FrostNixon)
MERGE (SamR)-[:ACTED_IN {roles:['James Reston, Jr.']}]->(FrostNixon)
MERGE (RonH)-[:DIRECTED]->(FrostNixon);
MERGE (Hoffa:Movie {title:'Hoffa'}) ON CREATE SET
Hoffa.released=1992, Hoffa.tagline='He didn\'t want law. He wanted justice.'
MERGE (DannyD:Person {name:'Danny DeVito'}) ON CREATE SET DannyD.born=1944
MERGE (JohnR:Person {name:'John C. Reilly'}) ON CREATE SET JohnR.born=1965
MERGE (JackN:Person {name:'Jack Nicholson'}) ON CREATE SET JackN.born=1937
MERGE (JTW:Person {name:'J.T. Walsh'}) ON CREATE SET JTW.born=1943
MERGE (JackN)-[:ACTED_IN {roles:['Hoffa']}]->(Hoffa)
MERGE (DannyD)-[:ACTED_IN {roles:['Robert "Bobby" Ciaro']}]->(Hoffa)
MERGE (JTW)-[:ACTED_IN {roles:['Frank Fitzsimmons']}]->(Hoffa)
MERGE (JohnR)-[:ACTED_IN {roles:['Peter "Pete" Connelly']}]->(Hoffa)
MERGE (DannyD)-[:DIRECTED]->(Hoffa);
MERGE (Apollo13:Movie {title:'Apollo 13'}) ON CREATE SET
Apollo13.released=1995, Apollo13.tagline='Houston, we have a problem.'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (EdH:Person {name:'Ed Harris'}) ON CREATE SET EdH.born=1950
MERGE (BillPax:Person {name:'Bill Paxton'}) ON CREATE SET BillPax.born=1955
MERGE (KevinB:Person {name:'Kevin Bacon'}) ON CREATE SET KevinB.born=1958
MERGE (GaryS:Person {name:'Gary Sinise'}) ON CREATE SET GaryS.born=1955
MERGE (RonH:Person {name:'Ron Howard'}) ON CREATE SET RonH.born=1954
MERGE (TomH)-[:ACTED_IN {roles:['Jim Lovell']}]->(Apollo13)
MERGE (KevinB)-[:ACTED_IN {roles:['Jack Swigert']}]->(Apollo13)
MERGE (EdH)-[:ACTED_IN {roles:['Gene Kranz']}]->(Apollo13)
MERGE (BillPax)-[:ACTED_IN {roles:['Fred Haise']}]->(Apollo13)
MERGE (GaryS)-[:ACTED_IN {roles:['Ken Mattingly']}]->(Apollo13)
MERGE (RonH)-[:DIRECTED]->(Apollo13);
MERGE (Twister:Movie {title:'Twister'}) ON CREATE SET
Twister.released=1996, Twister.tagline='Don\'t Breathe. Don\'t Look Back.'
MERGE (PhilipH:Person {name:'Philip Seymour Hoffman'})
ON CREATE SET PhilipH.born=1967
MERGE (JanB:Person {name:'Jan de Bont'}) ON CREATE SET JanB.born=1943
MERGE (BillPax:Person {name:'Bill Paxton'}) ON CREATE SET BillPax.born=1955
MERGE (HelenH:Person {name:'Helen Hunt'}) ON CREATE SET HelenH.born=1963
MERGE (ZachG:Person {name:'Zach Grenier'}) ON CREATE SET ZachG.born=1954
MERGE (BillPax)-[:ACTED_IN {roles:['Bill Harding']}]->(Twister)
MERGE (HelenH)-[:ACTED_IN {roles:['Dr. Jo Harding']}]->(Twister)
MERGE (ZachG)-[:ACTED_IN {roles:['Eddie']}]->(Twister)
MERGE (PhilipH)-[:ACTED_IN {roles:['Dustin "Dusty" Davis']}]->(Twister)
MERGE (JanB)-[:DIRECTED]->(Twister);
MERGE (CastAway:Movie {title:'Cast Away'}) ON CREATE SET
CastAway.released=2000,
CastAway.tagline='At the edge of the world, his journey begins.'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (HelenH:Person {name:'Helen Hunt'}) ON CREATE SET HelenH.born=1963
MERGE (RobertZ:Person {name:'Robert Zemeckis'})
ON CREATE SET RobertZ.born=1951
MERGE (TomH)-[:ACTED_IN {roles:['Chuck Noland']}]->(CastAway)
MERGE (HelenH)-[:ACTED_IN {roles:['Kelly Frears']}]->(CastAway)
MERGE (RobertZ)-[:DIRECTED]->(CastAway);
MERGE (OneFlewOvertheCuckoosNest:Movie {
title:'One Flew Over the Cuckoo\'s Nest'
}) ON CREATE SET
OneFlewOvertheCuckoosNest.released=1975,
OneFlewOvertheCuckoosNest.tagline='If he\'s crazy, what does that make you?'
MERGE (MilosF:Person {name:'Milos Forman'}) ON CREATE SET MilosF.born=1932
MERGE (JackN:Person {name:'Jack Nicholson'}) ON CREATE SET JackN.born=1937
MERGE (DannyD:Person {name:'Danny DeVito'}) ON CREATE SET DannyD.born=1944
MERGE (JackN)-[:ACTED_IN {
roles:['Randle McMurphy']
}]->(OneFlewOvertheCuckoosNest)
MERGE (DannyD)-[:ACTED_IN {roles:['Martini']}]->(OneFlewOvertheCuckoosNest)
MERGE (MilosF)-[:DIRECTED]->(OneFlewOvertheCuckoosNest);
MERGE (SomethingsGottaGive:Movie {title:'Something\'s Gotta Give'})
ON CREATE SET SomethingsGottaGive.released=2003
MERGE (JackN:Person {name:'Jack Nicholson'}) ON CREATE SET JackN.born=1937
MERGE (DianeK:Person {name:'Diane Keaton'}) ON CREATE SET DianeK.born=1946
MERGE (NancyM:Person {name:'Nancy Meyers'}) ON CREATE SET NancyM.born=1949
MERGE (Keanu:Person {name:'Keanu Reeves'}) ON CREATE SET Keanu.born=1964
MERGE (JackN)-[:ACTED_IN {roles:['Harry Sanborn']}]->(SomethingsGottaGive)
MERGE (DianeK)-[:ACTED_IN {roles:['Erica Barry']}]->(SomethingsGottaGive)
MERGE (Keanu)-[:ACTED_IN {roles:['Julian Mercer']}]->(SomethingsGottaGive)
MERGE (NancyM)-[:DIRECTED]->(SomethingsGottaGive)
MERGE (NancyM)-[:PRODUCED]->(SomethingsGottaGive)
MERGE (NancyM)-[:WROTE]->(SomethingsGottaGive);
MERGE (BicentennialMan:Movie {title:'Bicentennial Man'}) ON CREATE SET
BicentennialMan.released=1999,
BicentennialMan.tagline='One robot\'s 200 year journey to become an ordinary↪
↪ man.'
MERGE (ChrisC:Person {name:'Chris Columbus'}) ON CREATE SET ChrisC.born=1958
MERGE (Robin:Person {name:'Robin Williams'}) ON CREATE SET Robin.born=1951
MERGE (OliverP:Person {name:'Oliver Platt'}) ON CREATE SET OliverP.born=1960
MERGE (Robin)-[:ACTED_IN {roles:['Andrew Marin']}]->(BicentennialMan)
MERGE (OliverP)-[:ACTED_IN {roles:['Rupert Burns']}]->(BicentennialMan)
MERGE (ChrisC)-[:DIRECTED]->(BicentennialMan);
MERGE (CharlieWilsonsWar:Movie {title:'Charlie Wilson\'s War'})
ON CREATE SET CharlieWilsonsWar.released=2007,
CharlieWilsonsWar.tagline='A stiff drink. A little mascara. A lot of nerve.↪
↪ Who said they couldn\'t bring down the Soviet empire.'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (PhilipH:Person {name:'Philip Seymour Hoffman'})
ON CREATE SET PhilipH.born=1967
MERGE (JuliaR:Person {name:'Julia Roberts'}) ON CREATE SET JuliaR.born=1967
MERGE (MikeN:Person {name:'Mike Nichols'}) ON CREATE SET MikeN.born=1931
MERGE (TomH)-[:ACTED_IN {
roles:['Rep. Charlie Wilson']
}]->(CharlieWilsonsWar)
MERGE (JuliaR)-[:ACTED_IN {roles:['Joanne Herring']}]->(CharlieWilsonsWar)
MERGE (PhilipH)-[:ACTED_IN {roles:['Gust Avrakotos']}]->(CharlieWilsonsWar)
MERGE (MikeN)-[:DIRECTED]->(CharlieWilsonsWar);
MERGE (ThePolarExpress:Movie {title:'The Polar Express'}) ON CREATE SET
ThePolarExpress.released=2004,
ThePolarExpress.tagline='This Holiday Season... Believe'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (RobertZ:Person {name:'Robert Zemeckis'})
ON CREATE SET RobertZ.born=1951
MERGE (TomH)-[:ACTED_IN {
roles:[
'Hero Boy',
'Father',
'Conductor',
'Hobo',
'Scrooge',
'Santa Claus'
]
}]->(ThePolarExpress)
MERGE (RobertZ)-[:DIRECTED]->(ThePolarExpress);
MERGE (ALeagueofTheirOwn:Movie {title:'A League of Their Own'})
ON CREATE SET ALeagueofTheirOwn.released=1992,
ALeagueofTheirOwn.tagline='Once in a lifetime you get a chance to do↪
↪ something different.'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (Madonna:Person {name:'Madonna'}) ON CREATE SET Madonna.born=1954
MERGE (GeenaD:Person {name:'Geena Davis'}) ON CREATE SET GeenaD.born=1956
MERGE (LoriP:Person {name:'Lori Petty'}) ON CREATE SET LoriP.born=1963
MERGE (PennyM:Person {name:'Penny Marshall'}) ON CREATE SET PennyM.born=1943
MERGE (RosieO:Person {name:'Rosie O\'Donnell'})
ON CREATE SET RosieO.born=1962
MERGE (BillPax:Person {name:'Bill Paxton'}) ON CREATE SET BillPax.born=1955
MERGE (TomH)-[:ACTED_IN {roles:['Jimmy Dugan']}]->(ALeagueofTheirOwn)
MERGE (GeenaD)-[:ACTED_IN {roles:['Dottie Hinson']}]->(ALeagueofTheirOwn)
MERGE (LoriP)-[:ACTED_IN {roles:['Kit Keller']}]->(ALeagueofTheirOwn)
MERGE (RosieO)-[:ACTED_IN {roles:['Doris Murphy']}]->(ALeagueofTheirOwn)
MERGE (Madonna)-[:ACTED_IN {
roles:['"All the Way" Mae Mordabito']
}]->(ALeagueofTheirOwn)
MERGE (BillPax)-[:ACTED_IN {roles:['Bob Hinson']}]->(ALeagueofTheirOwn)
MERGE (PennyM)-[:DIRECTED]->(ALeagueofTheirOwn);
MATCH (CloudAtlas:Movie {title:'Cloud Atlas'})
MATCH (TheReplacements:Movie {title:'The Replacements'})
MATCH (Unforgiven:Movie {title:'Unforgiven'})
MATCH (TheBirdcage:Movie {title:'The Birdcage'})
MATCH (TheDaVinciCode:Movie {title:'The Da Vinci Code'})
MATCH (JerryMaguire:Movie {title:'Jerry Maguire'})
MERGE (PaulBlythe:Person {name:'Paul Blythe'})
MERGE (AngelaScope:Person {name:'Angela Scope'})
MERGE (JessicaThompson:Person {name:'Jessica Thompson'})
MERGE (JamesThompson:Person {name:'James Thompson'})
MERGE (JamesThompson)-[:FOLLOWS]->(JessicaThompson)
MERGE (AngelaScope)-[:FOLLOWS]->(JessicaThompson)
MERGE (PaulBlythe)-[:FOLLOWS]->(AngelaScope)
MERGE (JessicaThompson)-[:REVIEWED {
summary:'An amazing journey', rating:95
}]->(CloudAtlas)
MERGE (JessicaThompson)-[:REVIEWED {
summary:'Silly, but fun', rating:65
}]->(TheReplacements)
MERGE (JamesThompson)-[:REVIEWED {
summary:'The coolest football movie ever', rating:100
}]->(TheReplacements)
MERGE (AngelaScope)-[:REVIEWED {
summary:'Pretty funny at times', rating:62
}]->(TheReplacements)
MERGE (JessicaThompson)-[:REVIEWED {
summary:'Dark, but compelling', rating:85
}]->(Unforgiven)
MERGE (JessicaThompson)-[:REVIEWED {
summary:"Slapstick redeemed only by the Robin Williams and Gene Hackman's
↪ stellar performances",
rating:45
}]->(TheBirdcage)
MERGE (JessicaThompson)-[:REVIEWED {
summary:'A solid romp', rating:68
}]->(TheDaVinciCode)
MERGE (JamesThompson)-[:REVIEWED {
summary:'Fun, but a little far fetched', rating:65
}]->(TheDaVinciCode)
MERGE (JessicaThompson)-[:REVIEWED {
summary:'You had me at Jerry', rating:9
2}]->(JerryMaguire);
第九章:参考文献
阿奇亚姆, J. 等; OpenAI. (2024). GPT-4 技术报告. arxiv.org/abs/2303.08774.
库达胡里, A. K. (2017). 爱因斯坦的专利和发明. arxiv.org/abs/1709.00666.
丹尼赢得 4-1 (2025). www.nba.com/playoffs/2023/the-finals.
德意志, A. 等. (2022). GQL 和 SQL/PGQ 中的图模式匹配. 在 2022 年国际数据管理会议论文集 (第 2246-2258 页). 计算机协会.
多伊尔, R. (2023, December 28). 出售达拉斯小牛队对马克·库班和球队未来的意义. mng.bz/lZM8.
高露宇, 马学光, 詹姆斯·林, 詹姆斯·卡拉南. (2022). 无相关标签的精确零样本密集检索. arxiv.org/abs/2212.10496.
GQL 标准委员会. (n.d.). Retrieved August 30, 2023, www.gqlstandards.org/home/.
亨德里克斯, 丹, 科林·伯恩斯, 安雅·陈, 和 塞恩·鲍尔. (2021). CUAD:用于法律合同审查的专家注释 NLP 数据集. arxiv.org/abs/2103.06268.
刘易斯, 帕特里克等. (2021). 用于知识密集型 NLP 任务的检索增强生成. arxiv.org/abs/2005.11401.
纽梅斯特, 拉里. (2023, June 8). 律师们指责 ChatGPT 诱使他们引用虚假案例法. mng.bz/Bzd8.
openCypher 实施者小组. (n.d.). Retrieved August 30, 2023, opencypher.org/projects/.
欧维迪亚, O., M. Brief, M. Mishaeli, 和 O. Elisha. (2023). 微调或检索?比较 LLM 中的知识注入. arxiv.org/abs/2312.05934.
田凯, E. Mitchell, 袁浩, C.D. 曼宁, 和 C. Finn. (2023). 微调语言模型以增强事实性. arxiv.org/abs/2311.08401.
瓦斯瓦尼, A. 等. (2017). 注意力即是全部. arxiv.org/abs/1706.03762.
姚思, 等. (2023). ReAct:在语言模型中协同推理和行动 arxiv.org/abs/2210.03629.
郑浩, 等. (2023). 退一步:在大语言模型中通过抽象唤起推理. arxiv.org/abs/2310.06117.
索引
符号
<$nopage>代理式 RAG (检索增强生成)
另见 RAG (检索增强生成)
《奥德赛》 (荷马)
A
APOC (Cypher 上的精彩过程) 插件, 2nd
应用评估, 2nd
答案正确性,第 2 次
基准数据集,测试示例,第 2 次
评估,第 2 次
观察
代理 RAG(检索增强生成),第 2 次
C
上下文回忆,第 2 次
合同类
CUAD(合同理解阿提库斯数据集)
合同节点,第 2 次
合同数据模型
块节点类型
chunk_text 函数
使用 LLMs 进行构建,从文本中提取结构化数据
CUAD 数据集,第 2 次
结构化输出提取请求
结构化输出模型定义,第 2 次
D
文档嵌入策略
数据量
日期属性,第 2 次
具有向量相似度搜索功能的数据库
描述值
E
嵌入模型,第 2 次
嵌入属性
F
忠实度,第 2 次
微调
文本嵌入模型
G
GQL(图查询语言)
生成器
图索引
分块,第 2 次
社区检测和摘要,第 2 次
实体和关系提取,第 2 次
实体和关系摘要,第 2 次
global_retriever 函数
H
假设性问题策略
混合搜索
参考文献
I
使用少量示例进行上下文学习,第 2 次
K
知识截止问题,第 2 次
知识图谱
构建,第 2 次
使用 LLMs 进行构建,第 2 次
L
LLMs(大型语言模型)
使用构建知识图谱
使用, 第 2 次生成答案
限制, 第 2 次
克服限制
概述, 第 2 次
LLM-ready 数据库模式, 第 2 次
M
映射步骤
MS GraphRAG(微软的 GraphRAG)
电影数据集, 第 2 次
通过 Cypher 进行加载
通过 Neo4j 查询指南进行加载
MS GraphRAG(微软的 GraphRAG)
N
从字符串中获取 token 数量函数
Neo4j 环境
Cypher 查询语言
Neo4j 浏览器配置
Neo4j 安装
自然语言问题
O
可选类型
P
父文档-嵌入策略
PdfChunkFulltext 索引
pdf 向量索引
parent_retrieval 函数
PDF 节点
Q
查询重写
R
检索器
嵌入模型
管道
文本分块, 第 2 次
向量相似度搜索函数, 第 2 次
rag_pipeline 函数
RAG(检索增强生成), 第 2 次, 第 3 次
添加全文搜索以启用混合搜索, 第 2 次
应用评估, 第 2 次, 第 3 次, 第 4 次
架构,组件
完整管道, 第 2 次
知识图谱作为数据存储, 第 2 次
管道,查询语言生成适合的位置, 第 2 次
使用向量相似度搜索
报告结构,图检索器
全局搜索, 第 2 次
局部搜索, 第 2 次
S
监督微调,第 2 次
split_text_by_titles 函数
结构化数据
从文本中提取
T
文本属性,第 2 次
文本语料库
术语映射,添加到语义映射用户问题到模式,第 2 次
文本分块
topChunks
tiktoken 包
text2cypher
用于
查询语言生成的基础,第 2 次
从自然语言问题生成查询语言,第 2 次
从,第 2 次
总结,第 2 次
U
非结构化数据
V
向量相似度搜索
以及混合搜索、RAG 架构、组件的
向量检索,第 2 次
父文档检索器
向量索引


浙公网安备 33010602011771号