Neo4j-驱动的大语言模型应用构建指南-全-

Neo4j 驱动的大语言模型应用构建指南(全)

原文:zh.annas-archive.org/md5/41171770dbb6778039be2460bd7f9dba

译者:飞龙

协议:CC BY-NC-SA 4.0

第一部分

介绍 RAG 和知识图谱用于 LLM 基础知识

本书的第一部分为构建基于事实、具有上下文感知的 AI 应用程序奠定了基础。我们将从介绍大型语言模型LLMs)的基本知识开始,探讨它们在事实性方面面临的挑战,以及检索增强生成RAG)如何帮助解决这些限制。接下来,我们将通过实际见解和实施指南来分解 RAG 架构。最后,我们将建立对知识图谱的基础理解——强调 Neo4j 如何通过结构化、语义丰富的表示来增强 LLMs 的基础和推理能力。

本部分包括以下章节:

  • 第一章**,介绍 LLMs、RAGs 和 Neo4j 知识图谱

  • 第二章**,揭开 RAG 的神秘面纱

  • 第三章**,构建智能应用的知识图谱基础知识*

请保持关注

为了跟上生成式 AI 和 LLM 领域的最新发展,请订阅我们的每周通讯,AI_Distilled,链接为 packt.link/Q5UyU

第一章:介绍 LLM、RAGs 和 Neo4j 知识图谱

人工智能AI)正在从利基和专业化领域演变,变得更加易于访问,并能协助日常任务。最好的例子之一是生成式人工智能GenAI)的爆炸性出现。在过去的几年里,GenAI 凭借其易用性和理解并回答问题的能力,在技术构建者和普通用户中引起了极大的兴奋。大型语言模型LLMs)的突破推动了 GenAI 的发展,这为企业在如何与客户互动方面开辟了许多机会。客户可以用自然语言提问,并获得答案,而无需有人类存在来理解问题或从数据中提取智能。尽管 GenAI 在文本、音频和视频等不同模态的不同领域取得了重大进展,但本书的重点始终是 LLMs 及其在商业和工业用例中的应用。

在本章中,我们将通过 LLM 的视角来审视 GenAI,探讨其影响、陷阱和伦理问题。为了为本书奠定基础,我们将简要介绍可以增强 LLM 以使其更有效的技术。

在本章中,我们将涵盖以下主要主题:

  • 通过 LLM 的视角概述 GenAI 的演变

  • 理解 RAGs 和知识图谱在 LLMs 中的重要性

  • 介绍 Neo4j 知识图谱

通过 LLM 的视角概述 GenAI 的演变

到 2022 年底,OpenAI 通过发布一个名为 ChatGPT 的人工智能引擎而震惊世界,该引擎能够像人类一样理解语言,并以自然语言与用户互动。这是很长时间以来 GenAI 的最佳代表。AI 概念最初是规则系统,90 年代演变为机器学习算法。随着深度学习和 LLMs 的兴起,GenAI 的概念变得更加流行。这些 AI 系统在经过现有内容的训练后可以生成新的内容。OpenAI 的 GPT-3 LLM 模型是第一批引起大众兴趣的 LLM 之一。GenAI 可以以类似人类互动的方式获取答案,也可以通过提供文本描述来生成图像,将图像描述为文本内容,使用文本内容生成视频,以及许多其他事情。它可以增强创造力,加速研发,使复杂概念易于理解,并提高个性化。

LLM 的演变是 GenAI 受欢迎的核心。让我们来看看 LLM 以及它们是如何推动 GenAI 发展的。

介绍 LLM

LLM 是一种为自然语言处理而构建的机器学习模型,它可以理解语言结构,并根据训练在该语言中生成内容。

在 GPT-3 流行之前,关于 LLMs 的研究已经进行了几年。一些开创性工作包括谷歌的双向编码器表示从 TransformerBERT (https://github.com/google-research/bert))和 OpenAI 的生成预训练 TransformerGPT)。LLM 的训练需要大量的参数和计算能力。

在本质上,LLMs 是一种循环神经网络RNN)架构。传统的 RNN 在处理序列数据中的长期依赖关系时存在困难。为了解决这个问题,LLMs 通常利用如长短期记忆LSTM)网络或 Transformer 等架构。这些架构允许模型学习单词之间的复杂关系,即使这些单词在训练文本中相隔很远。

这里是基本 LLM 架构的一个简单示意图:

图 1.1 — 解释基本 LLM 架构的流程图

图 1.1 — 解释基本 LLM 架构的流程图

让我们剖析这个架构

  • 输入层:这一层接收初始文本提示或序列

  • 嵌入层:输入序列中的单词被转换为数值向量,捕捉其语义意义

  • 编码器:这是一个多层 RNN(例如,LSTM)或 Transformer,它处理嵌入单词的序列,捕捉上下文信息

  • 解码器:解码器利用编码表示逐词生成输出序列

你可以在这篇论文中了解更多关于 LLMs 的信息:https://arxiv.org/pdf/2307.06435。

构建一个 LLM 需要大量的努力和资源。让我们看看 OpenAI 为训练每个 GPT 模型所使用的参数数量:

  • GPT-1:这是第一个模型,使用了 1.17 亿个参数。

  • GPT-2:该模型使用了 15 亿个参数进行训练。

  • GPT-3:这是第一个发布的通用模型。该模型使用了 1750 亿个参数进行训练。

  • GPT-4 系列:这是 OpenAI 发布的最新模型。该模型使用了 170 万亿个参数进行训练。

这些训练数据表明,随着每个新版本的发布,参数数量增加了几个数量级。这意味着训练这些模型需要越来越多的计算能力。其他 LLM 模型也有类似的训练数据。

虽然生成人工智能是一项伟大的技术,但其应用也存在陷阱以及法律和伦理问题。我们将在下一节探讨这些问题。

理解生成人工智能的陷阱和伦理问题

虽然 LLM 在总结、生成上下文和其他用例方面很出色,但它们本身并不理解语言。它们根据训练文本识别模式来生成新文本。它们也不理解事实,不理解情感或伦理。它们只是预测下一个标记并生成文本。正因为这些缺陷,由 GenAI 生成的内容可能产生巨大的后果。

为了理解和解决这些方面,我们首先需要识别正在生成的任何有害或不准确的内容,并通过重新训练模型或添加单独的检查和平衡来处理这些问题,以确保这些内容不会被用作输出。

例如,最近有关于使用 LLM 生成法律摘要的案例,其中 LLM 创建了不存在的案例并基于这些案例生成了法律摘要。虽然技术上可能生成了所需解决方案,但这在法律上是不正确的。也有案例表明 LLM 被用来生成冒犯性的图像和视频并在互联网上分享。由于难以识别 AI 生成的内容,很容易被这种内容欺骗。这在社会、法律和伦理上都是不可接受的。有很多例子表明 LLM 只是编造事实。

微软网站上的这个教程(https://learn.microsoft.com/en-us/training/modules/responsible-ai-studio/)提供了这些担忧的详细解释以及我们如何识别它们。

检索增强生成RAG)和知识图谱结合可以帮助解决这些问题,我们将在下一节讨论。

理解 RAG(检索增强生成)和知识图谱在 LLM(大型语言模型)中的重要性

为了解决 GenAI 的缺陷,我们可以通过微调模型或使用其他来源来使响应归因。

微调涉及使用额外信息训练现有模型,这可能导致高质量的响应。但这个过程可能既复杂又耗时。

RAG 方法涉及在我们向 LLM 提问时提供额外信息。

采用这种方法,可以将知识库集成到生成过程中。在这种情况下,LLM 可以利用从其他来源检索到的额外信息,调整响应以匹配提供的信息,从而使结果归因。

这些存储库和来源可以包括以下内容:

  • 公开可用的结构化数据集(例如,如 PubMed 这样的科学数据库或如维基百科这样的公开可访问的百科全书资源)

  • 企业知识库(例如,内部公司文档、产品目录或具有严格隐私和安全要求的合规相关内容)

  • 特定领域的来源(例如,法律案例记录、医疗指南或针对特定行业的定制技术手册)

通过整合这些存储库和来源的相关信息,RAG 赋予 LLMs 生成既符合事实又与当前任务上下文一致输出的能力。与 LLM 训练数据中编码的静态知识不同,这些额外的数据源允许实时检索最新和专业的信息,解决数据新鲜度、准确性和特定性等挑战。我们将在第二章中详细讨论 RAG。

另一个信息来源,使 RAG 成为可能的是知识图谱。让我们简要谈谈它们及其在 LLM 领域的角色。

知识图谱在 LLMs 中的作用

知识图谱在为 LLMs 生成富有创造性和上下文丰富的内容方面发挥着巨大作用。它们提供了一个结构化、相互关联的基础,使信息检索更加相关。

通过在复杂和多层次的数据理解中定位 AI 结果,使其相关且富有洞察力。

将数据表示为图,为理解数据开辟了更多途径。同时,知识图谱不能是一个静态实体,只能在一个固定维度上表示数据。它的真正力量在于其动态和多维的能力。它可以通过实时数据流实时捕捉时间、空间或上下文信息。

除了作为存储信息的重要工具外,知识图谱还是智能、上下文感知 AI 的骨架。

有几个原因说明知识图谱对 GenAI 至关重要:

  • 增强的上下文理解:知识图谱允许 GenAI 系统根据关系而非孤立的事实检索相关信息。例如,在医疗保健领域,知识图谱可以将症状、疾病和治疗联系起来,使 GenAI 能够基于相互关联的医学知识提出更准确的诊断见解。

  • 高效的数据检索:与传统数据库不同,知识图谱允许多跳推理,这使得 GenAI 可以从几个分离度中提取见解。这在金融等领域非常有价值,GenAI 可以使用知识图谱揭示客户、交易和市场趋势等实体之间的隐藏关系。

  • 向量嵌入的集成:当与向量嵌入结合时,知识图谱使 GenAI 能够理解和回应更细微的查询。向量嵌入捕捉数据点之间的语义相似性,知识图谱随后将其语境化,在响应中创造准确性和相关性相结合的强大组合。

  • 现实世界的影响:主要组织已经开始利用知识图谱的力量来增强 GenAI 应用。例如,电子商务公司使用知识图谱提供不仅相关而且上下文丰富的产品推荐,这些推荐来自客户评价、购买历史和产品特性等多样化的数据源。

通过整合知识图谱,GenAI 模型超越了传统数据限制,有助于在不同领域创建更智能、更可靠的应用。

让我们谈谈Neo4j 知识图谱

介绍 Neo4j 知识图谱

知识图谱是动态的,并随着数据及其关系随时间演变而持续发展。

Neo4j 是一个擅长以图形式存储数据的数据库。例如,在存储中,大多数产品都按照一定的分组排列并保持在那些组中。但这种情况有一个例外。当商店想要推广某些产品时,它们会被放置在商店的前面。这种灵活的思维方式应该适用于我们的知识图谱实现。随着数据语义的演变,知识图谱应该能够捕捉这种变化。

Neo4j,凭借其节点上的多个标签和可选的架构方法,通过帮助我们以额外的标签或提供更多相关上下文的具体关系来持久化(保留)我们对数据的理解,使得保持我们的图相关变得容易。我们将在接下来的章节中深入探讨如何从头开始构建 Neo4j 知识图谱。

现在,让我们看看 Neo4j 知识图谱是如何增强 LLM 的响应的。

使用 Neo4j 知识图谱与 LLMs

假设有一个基于 LLM 的聊天机器人与 Neo4j 知识图谱集成。这个 GenAI 聊天机器人旨在回答医疗查询。图 1.2展示了 Neo4j 知识图谱如何通过将结构化的患者症状记录与医学研究论文和临床试验中的非结构化见解联系起来,增强这个聊天机器人的医疗推理能力。

非结构化文本经过基于嵌入的模型处理,这些模型由OllamaOpenAIHugging Face等提供商提供,然后进行命名实体识别(NER),以提取关键实体,如症状和治疗。这些数据被集成到一个 Neo4j 知识图谱中,其中文档提到症状和治疗,患者表现出症状,症状与潜在的治疗方法相联系。这使多跳推理成为可能,允许聊天机器人有效地回答如下复杂查询:

哪些患者表现出与流感相似的症状,并且在过去也表现出 COVID-19 的症状?

图 1.2 — 由 Neo4j 知识图谱驱动的医疗保健 Gen-AI

图 1.2 — 由 Neo4j 知识图谱驱动的医疗保健 Gen-AI

为了检索此查询的结果,将按照以下顺序遵循多跳知识图谱查询路径图 1.2):

  1. 从研究文档中检索与流感相关的症状。

  2. 识别目前表现出这些症状的患者。

  3. 对比过去患者的 COVID-19 症状记录。

  4. 返回同时符合两种条件并具有支持性文档来源的患者。

采用这种方法,LLM 的响应可以基于事实正确、相关且最新的结果来支持医疗决策。

类似的方法也可以用来增强支持其他应用的 LLMs。

我们现在已经看到了知识图谱如何增强 GenAI 提供上下文丰富、准确洞察的能力。但是,这种变革力量如何转化为现实生活中的具体好处呢?我们将在本书的剩余部分继续这一旅程。

摘要

在本章中,我们讨论了在 LLMs 背景下 GenAI 的演变。我们还探讨了 RAG 和知识图谱是如何成为这一变革的关键推动者,并有助于提供结构和上下文,从而提高 LLM 的准确性和推理能力。

展望未来,下一章将深入探讨 RAG——一种通过在检索到的、验证过的信息中定位响应来显著提高 GenAI 准确性的技术。

第二章:揭秘 RAG

在上一章中,我们探讨了 LLM 的演变以及它们如何改变 GenAI 的格局。我们还讨论了一些它们的陷阱。在本章中,我们将探讨如何使用 Retrieval-Augmented GenerationRAG)来避免这些陷阱。我们将探讨 RAG 的含义、其架构以及它在构建改进的智能应用程序的 LLM 工作流程中的位置。

在本章中,我们将涵盖以下主要主题:

  • 理解 RAG 的力量

  • 解构 RAG 流程

  • 为您的 RAG 检索外部信息

  • 构建端到端 RAG 流程

技术要求

本章需要熟悉 Python 编程语言(建议使用版本 3.6 或更高版本)以及深度学习的基本概念。

我们将利用流行的 AI 工具包,如 Hugging Face 的 Transformers 库 (huggingface.co/docs/transformers/en/index) 来构建和实验 RAG。虽然不是强制性的,但具备 Git 版本控制的基本理解可能会有所帮助。

Git 允许您轻松地克隆本章的代码存储库并跟踪您所做的任何更改。无需担心自己寻找或输入代码!我们已经在 GitHub 上创建了一个专门的公共存储库,github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/tree/main/ch2,您可以通过它轻松地克隆并跟随本章的动手练习。

本存储库包含实现 RAG 模型以及将 Neo4j 与高级知识图谱功能集成的所有必要脚本、文件和配置。

为了跟上进度,请确保您在环境中安装了以下 Python 库:

  • Transformers:安装 Hugging Face Transformers 库以处理模型相关功能:pip install transformers

  • PyTorch:将 PyTorch 作为计算的后端进行安装。按照 pytorch.org/get-started/locally/ 上的说明安装适合您系统的相应版本。

  • scikit-learn:对于相似度计算,使用 pip install scikit-learn 命令安装 scikit-learn

  • NumPy:安装 NumPy 以进行数值运算:pip install numpy

  • SentencePiece:某些模型需要 SentencePiece 进行文本分词。您可以使用官方 GitHub 存储库中提供的说明进行安装:github.com/google/sentencepiece#installation。对于大多数 Python 环境,您可以通过 pip 安装它:pip install sentencepiece

  • rank_bm25:实现基于关键字的检索的 BM25 算法需要 rank_bm25 库。您可以使用 pip 安装它:pip install rank_bm25

  • 数据集:Hugging Face 的datasets库提供了高效的工具,用于加载数据集、处理和转换数据集。它支持使用最小内存使用量处理大规模数据集。您可以使用pip install datasets进行安装。

  • pandaspandas是 Python 中一个强大的数据分析库,用于操作表格数据。在这个例子中,它通过将其转换为 DataFrame 来帮助预处理数据集,以便更容易地进行操作。使用pip install pandas进行安装。

  • faiss-CPUfaiss-cpu是一个用于高效搜索和聚类密集向量的库。在这个例子中,它用于构建在推理期间检索相关段落的检索器。访问 Faiss 的 GitHub 仓库(github.com/facebookresearch/faiss)以获取文档和示例。使用pip进行安装:pip install faiss-cpu

  • 加速:加速是 Hugging Face 的一个库,它简化了分布式训练和推理。它确保了在 CPU、GPU 和多节点设置中硬件利用的最优化。使用pip install accelerate进行安装。

通过确保您的环境配置了这些工具,您可以无缝探索本章提供的动手练习。

注意

本章的所有部分都专注于相关的代码片段。

完整代码,请参阅本书的 GitHub 仓库:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/tree/main/ch2

理解 RAG 的力量

RAG 是由 Meta 研究人员在 2020 年引入的(arxiv.org/abs/2005.11401v4),作为一个框架,允许 GenAI 模型利用模型训练之外的外部数据来增强输出。

众所周知,大型语言模型(LLMs)容易产生幻觉。一个经典的 LLMs 产生幻觉的真实世界例子是 Levidow, Levidow & Oberman 律师事务所,该律所在与哥伦比亚航空公司 Avianca 的案件中提交了一份包含由 OpenAI 的 ChatGPT 生成的虚假引用的法律简报,因此被罚款。他们随后被罚款数千美元,并且可能因声誉受损而损失更多。更多关于此事的信息,请参阅此处:news.sky.com/story/lawyers-fined-after-citing-bogus-cases-from-chatgpt-research-12908318

LLM 的幻觉可能由以下几个因素引起:

  • 过度拟合训练数据:在训练过程中,LLM 可能会过度拟合到训练数据中的统计模式。这可能导致模型优先复制这些模式,而不是生成事实准确的内容。

  • 缺乏因果推理能力:大型语言模型在识别词语之间的统计关系方面表现出色,但可能难以理解因果关系。这可能导致输出在语法上正确但在事实上不可信。

  • 温度配置:大型语言模型可以通过一个名为温度的参数进行配置,这是一个介于01之间的数字,它控制文本生成的随机性。较高的温度增加了创造力,但也增加了模型偏离预期响应并产生幻觉的可能性。

  • 缺失信息:如果生成准确响应所需的信息未包含在训练数据中,模型可能会生成听起来合理但实际上错误的答案。

  • 有缺陷或存在偏差的训练数据:训练过程的质量起着重要作用。如果数据集包含偏差或不准确性,模型可能会持续这些问题,导致幻觉。

虽然幻觉是一个重大挑战,但几种方法可以在一定程度上帮助减轻它们:

  • 提示工程:这涉及精心设计和迭代优化提供给大型语言模型的指令或查询,以产生一致和准确的响应。例如,向一个大型语言模型提问

     List five key benefits of Neo4j for knowledge graphs 
    

相比于像“提示工程”这样的宽泛查询,它提供了更多的结构和精确性:

Tell me about Neo4j 

前者查询指定了预期的输出,引导模型关注一个简洁且相关的利益列表,而后者可能产生冗长或离题的回答。提示工程有助于引导模型保持在所需的信息范围内,并减少其产生不相关或虚构输出的可能性。有关提示工程技术和最佳实践的详细探讨,请参阅此指南:cloud.google.com/discover/what-is-prompt-engineering

  • 情境学习少样本提示):在此方法中,示例包含在提示中,以引导大型语言模型向准确、特定任务的响应。例如,当要求产品比较时,在提示中提供几个结构良好的比较示例有助于模型模仿该模式。这种方法利用了模型推断情境并根据给定示例调整其响应的能力,使其在特定领域任务中非常有效。

  • 微调:这涉及在特定数据集上进一步训练已经预训练的 LLM,以适应特定领域或任务。这个过程增强了模型生成特定领域、相关和准确响应的能力。微调的一种流行方法是强化学习与人反馈(RLHF),其中人类评估者通过评分模型的输出来引导模型。这些评分用于调整模型的行为,使其与人类期望保持一致。例如,在公司的内部文档上微调 LLM 可以确保它产生准确且相关的输出,以满足组织的特定需求。如果提示:

    Explain the onboarding process for new hires 
    

一个微调后的模型可能会提供一个与公司政策一致详细解释,而一个通用模型可能会提供模糊或不相关的响应。让我们再举一个例子场景,以了解如何使用 RLHF 来改进响应。

假设最初 LLM 被询问:

What are the benefits of using XYZ software? 

响应可能包括与软件独特功能不匹配的通用好处。使用 RLHF,人类评估者根据准确性、相关性和完整性评分响应。例如,初始响应可能是:

XYZ software improves productivity, enhances collaboration, and reduces costs. 

反馈可能包括:

Too generic; lacks specifics about XYZ software. 

在经过人类反馈的微调后,结果可能是一个更准确和定制的响应,如下所示:

XYZ software offers real-time data synchronization, customizable workflows, and advanced security features, making it ideal for enterprise resource planning. 

RLHF 在减少幻觉方面特别有价值,因为它强调从人类编辑的反馈中学习。

尽管这些方法提供了显著的改进,但它们在关键领域仍有所不足:使组织能够利用特定领域的知识快速构建准确、上下文相关且可解释的通用人工智能应用。解决方案在于扎根——一个将模型的响应与真实世界的事实或数据联系起来的概念。这种方法构成了文本生成新范式的基础,称为 RAG(Retrieval-Augmented Generation,检索增强生成)。通过从可靠的知识源动态检索事实信息,RAG 确保输出既准确又与上下文一致。RAG 通过结合来自事实知识库的相关信息来尝试解决大型语言模型(LLM)的幻觉问题。

术语检索增强生成(Retrieval-Augmented Generation),简称 RAG,首次由Facebook AI Research(FAIR)的研究人员在 2020 年 5 月提交的一篇题为《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》的论文中提出:arxiv.org/abs/2005.11401

论文提出了 RAG(检索增强生成)作为一种混合架构(参见图 2.1),它结合了一个神经检索器和一个序列到序列生成器。检索器从外部知识库中检索相关文档,然后这些文档被用作生成器的上下文,以产生基于事实数据的输出。这种方法已被证明可以显著提高知识密集型 NLP 任务(如开放域问答和对话系统)的性能,通过减少对模型内部知识的依赖并提高事实准确性。RAG 通过引入一个关键元素来解决之前提到的 LLM 的不足:从补充或特定领域的数据源中检索相关知识的能力。

图 2.1 — FAIR 在“检索增强生成用于知识密集型 NLP 任务”研究论文中提出的 RAG 架构

图 2.1 — FAIR 在“检索增强生成用于知识密集型 NLP 任务”研究论文中提出的 RAG 架构

此外,RAG 管道提供了在保持准确性的同时减少模型大小的潜力。而不是将所有知识嵌入到模型的参数中——这将需要大量资源——RAG 允许模型动态检索信息,保持其轻量级和可扩展性。

本章下一节将深入探讨 RAG 的内部工作原理,探讨它是如何弥合原始生成和基于知识文本生产之间的差距。

解构 RAG 流程

让我们现在解构 RAG 模型的构建块,并帮助您了解它是如何工作的。

首先,我们将查看常规 LLM 应用流程。图 2.2展示了这个基本流程。

图 2.2 — 带有 LLM 的聊天应用中的信息基本流程

图 2.2 — 带有 LLM 的聊天应用中的信息基本流程

当用户向 LLM(大型语言模型)提出请求时,会发生什么情况。

  1. 用户发送提示:这个过程从用户向 LLM 聊天 API 发送提示开始。这个提示可能是一个问题、一个指令或任何其他请求信息或内容生成的请求。

  2. LLM API 处理提示:LLM 聊天 API 接收用户的提示并将其传输给 LLM。LLM 是经过大量文本数据训练的 AI 模型,允许它们对广泛的提示和问题进行沟通并生成类似人类的文本。

  3. LLM 生成响应:然后 LLM 处理提示并制定一个响应。这个响应被发送回 LLM 聊天 API,然后将其传输给用户。

从这个流程中,我们可以看到 LLM 负责提供答案,中间没有其他过程。这是没有 RAG 的请求-响应流程中最常见的用法。

现在我们来看看 RAG 在这个工作流程中是如何定位的。

图 2.3 — 带有 RAG 模型的聊天应用中的信息流程

图 2.3 — 带有 RAG 模型的聊天应用中的信息流

我们可以从图 2.3中看到,在调用实际的 LLM 服务之前,我们有一个中间数据源,它可以提供 LLM 请求的上下文:

  1. 用户发送提示:该过程从用户通过聊天界面发送提示或问题开始。这个提示可能是用户想要了解的任何信息或需要帮助的内容。

  2. RAG 模型处理提示:提示被聊天 API 接收,然后将其转发给 RAG 模型。RAG 模型有两个主要组件协同工作:检索器(在第3步中讨论)和编码器-解码器(在第4步中讨论)。

  3. 检索器:该组件在知识库中搜索,可能包括非结构化文档、段落或如表格或知识图谱之类的结构化数据。其作用是定位处理用户提示所需的最相关信息。

我们将涵盖检索器组件的一个简单示例。你可以在github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch2/dpr.py查看完整的代码。

context encoder model and *tokenizer* from Hugging Face’s Transformers library:
  1. 让我们定义一组我们想要存储在文档存储中的文档。这里我们使用一些预定义的句子来演示:

    documents = [
        "The IPL 2024 was a thrilling season with unexpected results.",
    .....
        "Dense Passage Retrieval (') is a state-of-the-art technique for information retrieval."
    ] 
    
  2. 接下来,我们将之前定义的内容存储在内容存储中。然后我们将为每个文档生成一个嵌入并将它们存储在内容存储中:

    def encode_documents(documents):
        inputs = tokenizer(
            documents, return_tensors='pt', 
            padding=True, truncation=True)
        with torch.no_grad():
            outputs = model(**inputs)
        return outputs.pooler_output.numpy()
    
    document_embeddings = encode_documents(documents) 
    
  3. 现在,让我们定义一种方法,根据查询输入从文档存储中检索内容。我们将生成请求的嵌入并查询内容存储以检索相关结果。我们在这里利用向量搜索来获取相关结果:

    def retrieve_documents(query, num_results=3):
        inputs = tokenizer(query, return_tensors='pt', 
            padding=True, truncation=True)
        with torch.no_grad():
            query_embedding = model(**inputs).pooler_output
                                             .numpy()
        similarity_scores = cosine_similarity(
            query_embedding, document_embeddings).flatten()
        top_indices = similarity_scores.argsort()[-num_results:]
            [::-1]
        top_docs = [
            (documents[i], similarity_scores[i]) 
            for i in top_indices]
        return top_doc 
    

我们可以看到对于给定的查询,我们会收到什么样的输出作为示例。

以下是一个示例输入:

Query: What is Dense Passage Retrieval? 

下面是示例输出:

Top Results:
Score: 0.7777, Document: Dense Passage Retrieval (') is a state-of-the-art technique for information retrieval.
... 

注意

检索器实现可能相当复杂。它们可能涉及

使用高效的搜索算法,如 BM25、TF-IDF 或神经检索器

例如密集段落检索。你可以在github.com/facebookresearch/了解更多信息。

  1. 编码器-解码器/增强生成:该组件的编码器部分处理提示以及检索到的信息——无论是结构化还是非结构化——以创建一个全面的表示。然后解码器使用这个表示来生成一个准确、语境丰富且针对用户提示的响应。

这涉及到使用输入查询和上下文信息调用 LLM API。让我们看看一个示例,看看它是如何工作的。以下示例展示了如何使用上下文信息调用查询。这个示例展示了 T5Tokenizer 模型的使用:

  1. 让我们先定义一个 LLM。我们将使用 Hugging Face 的 T5 模型:

    tokenizer = T5Tokenizer.from_pretrained('t5-small', 
        legacy=False)
    model = T5ForConditionalGeneration.from_pretrained(
        't5-small') 
    
  2. 定义 RAG 流程的查询和文档。通常,我们利用检索器进行 RAG 流程。在这里,为了演示目的,我们将使用硬编码的值:

    query = "What are the benefits of solar energy?"
    retrieved_passages = """
    Solar energy is a renewable resource and reduces electricity bills.
    ......
    """ 
    
  3. 我们将定义一个方法,它接受输入查询和检索到的段落,以使用 LLM API 来演示 RAG 方法:

    def generate_response(query, retrieved_passages):
            input_text = f"Answer this question based on the provided context: {query} Context: {retrieved_passages}" 
        inputs = tokenizer(input_text, return_tensors='pt', 
            padding=True, 
            truncation=True, max_length=512
        ).to(device)
        with torch.no_grad():
            outputs = model.generate(
                **inputs,
                max_length=300,  # Allow longer responses
                num_beams=3,     # Use beam search for better results
                early_stopping=True
            )
        return tokenizer.decode(outputs[0], 
            skip_special_tokens=True) 
    

注意

我们正在使用T5 模型的束搜索解码来生成

准确且与上下文相关的响应。束搜索解码是一种在文本生成过程中寻找最可能序列(单词)的搜索算法。与贪婪解码不同,贪婪解码选择最

在每个步骤中,束搜索维护多个潜在的

序列(称为)并同时探索它们。此

增加了找到高质量结果的机会,因为它避免了在生成过程中过早地做出次优选择。你可以在本文中了解更多关于 Transformers 中束搜索的信息:huggingface.co/blog/constrained-beam-search

现在,让我们调用此方法并审查响应。

  1. 聊天 API 提供响应:以下代码将调用generate_response方法,并为输入查询提供聊天响应:

    response = generate_response(query, retrieved_passages) 
    print("Query:", query) 
    print("Retrieved Passages:", retrieved_passages) 
    print("Generated Response:", response) 
    

当我们运行这个示例时,结果如下。

以下是一个示例输入:

Query: What are the benefits of solar energy? 

检索到的段落如下:

Solar energy is a renewable resource and reduces electricity bills.
...... 

以下是一个示例输出:

Generated Response: it is environmentally friendly and helps combat climate change 

你可以在github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch2/augmented_generation.py找到这个示例的完整代码。

  1. 集成和微调:现在让我们看看一个代码片段,它将检索器和 LLM 调用结合起来,作为完整的 RAG 流程。以下代码展示了这一点:

    def rag_pipeline(query):
        retrieved_docs = retrieve_documents(query)
        response = generate_response(query, retrieved_docs)
        return response
    
    query = "How does climate change affect biodiversity?"
    generated_text = rag_pipeline(query)
    print("Final Generated Text:", generated_text) 
    

从代码中,我们可以看到流程很简单。我们使用检索器检索利用 RAG 流程所需的文档,并将输入查询和检索到的文档传递给 LLM API 调用。

在这次对 RAG 架构的深入研究中,我们关注了其机制,并展示了其核心组件的功能。通过结合高效的信息检索和高级语言生成模型,RAG 产生了上下文相关且知识丰富的响应。随着我们过渡到下一节,我们将讨论检索过程

为你的 RAG 检索外部信息

理解 RAG 如何利用外部知识对于欣赏其生成事实准确和富有信息性的响应的能力至关重要。本节讨论了各种检索技术、整合检索信息的策略,以及说明这些概念的实例。

理解检索技术和策略

RAG 模型的成功取决于其使用常用检索技术之一从庞大的外部知识库中检索相关信息的能力。这些检索方法对于从大型数据集中获取相关信息至关重要。常见的技术包括传统的 BM25 方法以及现代的 DPR 神经网络方法。总的来说,这些技术可以分为三类:向量相似度搜索关键词匹配段落检索。我们将在以下小节中讨论每个技术。

向量相似度搜索

您传递给 LLM 的文本或查询被转换成一个称为嵌入的向量表示。向量相似度搜索通过比较向量嵌入来检索最接近的匹配项。其基本思想是相关和相似文本将具有相似的嵌入。该技术的工作原理如下:

  1. 构建输入查询的嵌入。我们对输入查询进行分词,并生成其向量嵌入表示:

    query_inputs = question_tokenizer(query, return_tensors="pt")
    with torch.no_grad():
      query_embeddings = question_encoder(
            **query_inputs
        ).pooler_output 
    
  2. 构建文档的嵌入。我们使用分词器为每个文档生成一个嵌入,并将每个嵌入与其对应的文档关联:

    for doc in documents:
        doc_inputs = context_tokenizer(doc, return_tensors="pt")
        with torch.no_grad():
            doc_embeddings.append(
                context_encoder(**doc_inputs).pooler_output)
    doc_embeddings = torch.cat(doc_embeddings) 
    
  3. 使用点积计算查找相似文档。此步骤使用输入查询嵌入并在文档嵌入中搜索与输入查询相似的文档:

    scores = torch.matmul(query_embeddings, doc_embeddings.T).squeeze() 
    
  4. 按相关性分数对文档进行排序并返回结果。结果包含匹配的文档以及一个表示其与输入查询相似度的分数。我们将按照所需的顺序对结果进行排序,从最相似到最不相似:

    ranked_docs = sorted(
        zip(documents, scores), key=lambda x: x[1], reverse=True) 
    

让我们运行这个示例,看看结果会是什么样子。

以下是一个示例输入查询:

What are the benefits of solar energy? 

以下为示例输出(按相关性排序的文档):

Document: Solar energy is a renewable source of power., Score: 80.8264
....
Document: Graph databases like Neo4j are used to model complex relationships., Score: 52.8945 

上述代码演示了如何使用 DPR 将查询和一组文档编码成高维向量表示。通过计算相似度分数,例如查询向量与文档向量之间的点积,模型评估每个文档与查询的相关性。然后根据相似度分数对文档进行排序,最相关的文档将出现在顶部。这个过程突出了基于向量的检索在有效识别来自各种文档的上下文相关信息方面的强大功能,即使这些文档包含相关和不相关的混合内容。

本例的完整版本可在 GitHub 仓库中找到:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch2/vector_similarity_search.py

关键词匹配

关键词匹配是一种更简单的方法,它识别包含用户提示中关键词的文档。虽然效率高,但可能容易受到噪声的影响,并错过包含相关同义词的文档。BM25 是一种基于关键词的概率检索函数,它根据每个文档中出现的查询词对文档进行评分,考虑词频和文档长度。这种方法的基本流程如下:

  1. 使用文档构建 BM25 语料库。我们将对文档进行分词并从这些文档中构建语料库。我们将构建 BM25 语料库:

    tokenized_corpus = [doc.split() for doc in corpus]
    # Initialize BM25 with the tokenized corpus
    bm25 = BM25Okapi(tokenized_corpus, k1=1.5, b=0.75) 
    
  2. 将查询分词以使用它进行搜索:

    tokenized_query = query.split() 
    
  3. 使用分词查询查询 BM25 语料库。这将返回匹配文档的分数:

    scores = bm25.get_scores(tokenized_query) 
    
  4. 我们将使用这些分数,按所需顺序排列文档,并返回它们:

    ranked_docs = sorted(zip(corpus, scores), key=lambda x: x[1], 
        reverse=True) 
    

当我们运行此示例时,对于给定的输入,结果将如下所示。

以下是一个示例输入查询:

quick fox 

以下是一个示例输出:

Ranked Documents:
Document: The quick brown fox jumps over the lazy dog., Score: 0.6049
.....
Document: Artificial intelligence is transforming the world., Score: 0.0000 

BM25 算法根据文档与查询的相关性对文档进行排名。它依赖于词频(关键词在文档中出现的频率)和文档长度,应用概率评分函数来评估相关性。与将查询和文档都表示为高维空间中密集数值向量的向量相似度搜索不同,它使用如点积等数学函数来衡量相似度,BM25 直接在离散单词匹配上操作。这意味着 BM25 效率高且可解释,但可能在处理语义关系方面遇到困难,因为它无法识别同义词或上下文含义。相比之下,向量相似度搜索,如 DPR,在识别即使精确关键词不同时也能识别概念相似性方面表现出色,这使得它更适合需要深度语义理解的任务。此代码片段说明了 BM25 在简单关键词匹配任务中的实用性,其中效率和可解释性至关重要。

完整示例可在 GitHub 仓库中找到:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch2/keyword_matching.py

段落检索

与检索整个文档不同,RAG 可以专注于文档中直接针对用户查询的具体段落。这允许进行更精确的信息提取。这种方法的基本流程与向量搜索方法非常相似。我们使用向量搜索中显示的方法获取排名靠前的文档,然后如以下代码片段所示提取相关段落:

# Extract passages for the reader
passages = [doc for doc, score in ranked_docs]

# Prepare inputs for the reader
inputs = reader_tokenizer(
    questions=query,
    titles=["Passage"] * len(passages),
    texts=passages,
    return_tensors="pt",
    padding=True,
    truncation=True
)
# Use the reader to extract the most relevant passage
with torch.no_grad():
    outputs = reader(**inputs)
# Extract the passage with the highest score
max_score_index = torch.argmax(outputs.relevance_logits)
most_relevant_passage = passages[max_score_index] 

当我们针对给定的输入查询运行此示例时,结果如下所示。

以下是一个示例输入查询:

What are the benefits of solar energy? 

以下是一个示例输出:

Ranked Documents:
Document: Solar energy is a renewable source of power., Score: 80.8264
.....
Document: It has low maintenance costs., Score: 57.9905

Most Relevant Passage: Solar panels help combat climate change and reduce carbon footprint. 

上述示例说明了段落检索方法,它比文档级检索更细粒度,专注于提取直接针对用户查询的特定段落。通过结合使用读者模型检索器,这种方法增强了相关性和特异性,因为它不仅确定了最相关的文档,还确定了其中最佳回答查询的确切段落。

即使一个段落的检索器分数略低,读者也可能优先考虑它,因为它在词和跨度级别上更精确地评估相关性,考虑了上下文细微差别。检索器通常使用查询和段落嵌入的点积来计算相似度分数:

这里,𝑞是查询嵌入,段落的嵌入,𝑑是嵌入的维度。

然而,读者通过分析每个段落的文本内容进一步细化这一过程。它根据给定段落包含答案的可能性分配一个相关性分数logit(也称为置信度分数)。这个相关性分数是从读者模型的原始输出(logits)中计算出来的,该模型考虑了查询与段落之间的词级和跨度级交互。相关性分数的公式可以表示如下:

这里,我们有以下内容:

通过结合两个阶段,系统可以识别出不仅语义相似(检索器阶段),而且与查询意图上下文对齐的段落(读者阶段)。

这个双阶段过程突出了段落检索在信息检索管道中生成高度针对性的响应的优势。

完整示例可在 GitHub 仓库中找到:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch2/passage_retrieval.py

集成检索信息

在 RAG 流程的最后一步,让我们看看我们如何以综合上下文相关且连贯的响应的方式将检索器信息与生成模型相结合。与早期示例不同,这种方法明确地将多个检索到的段落与查询相结合。通过这样做,它为生成模型创建了一个单一输入。这使得模型能够综合出一个统一且丰富的响应,而不仅仅是选择或排序段落:

def integrate_and_generate(query, retrieved_docs):
    # Combine query and retrieved documents into a single input
    input_text = f"Answer this question based on the following context: {query} Context: {' '.join(retrieved_docs)}"

    # Tokenize input for T5
    inputs = t5_tokenizer(input_text, return_tensors="pt", 
        padding=True, truncation=True, max_length=512)

    # Generate a response
    with torch.no_grad():
        outputs = t5_model.generate(**inputs, max_length=100)

    # Decode and return the generated response
    return t5_tokenizer.decode(outputs[0], skip_special_tokens=True) 

以下是一个示例输入查询:

What are the benefits of solar energy? 

以下为示例输出:

Ranked Documents:
Document: Solar energy is a renewable source of power., Score: 80.8264
....
Document: It has low maintenance costs., Score: 57.9905

Most Relevant Passage: Solar panels help combat climate change and reduce carbon footprint. 
sized response. The generate() function processes the combined input (query and passages) through the encoder to produce contextual embeddings, *ℎ*. These embeddings are then used by the decoder, which generates each token sequentially based on probabilities:

这里, 是位置 的标记, 是隐藏状态,而 是模型的权重矩阵。Beam 搜索通过最大化跨标记的整体概率来确保选择最可能的序列。与前面示例中单独选择或排序段落不同,此代码明确地将多个检索到的文档与查询组合成一个单一输入。这使得 T5 模型能够全面处理组合上下文,并产生一个包含来自多个来源信息的连贯响应,这使得它在需要跨多个段落综合或总结查询时特别有效。

要参考此代码的完整版本,请参阅:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch2/integrate_and_generate.py

通过探索各种检索技术和它们与生成模型的集成,我们看到了 RAG 架构如何利用外部知识来产生准确和有信息量的响应。

在下一节中,让我们看看从源读取输入文档并利用这些文档进行检索流程的整体流程,而不是本节示例中查看的简单硬编码句子。

构建端到端 RAG 流程

在前面的章节中,我们通过简单的数据深入探讨了 RAG 流程中的各个步骤以展示用法。退一步使用一个真实世界的数据集(尽管很简单)来完成整个流程是个不错的主意。为此,我们将使用 GitHub 的问题数据集(huggingface.co/datasets/lewtun/github-issues)。我们将探讨如何读取这些数据并在 RAG 流程中使用它们。这将为后续章节中完整端到端 RAG 流程的实现奠定基础。

在本例中,我们将加载 GitHub 注释以回答诸如如何离线加载数据等问题。我们需要遵循以下步骤来加载数据并设置检索器:

  1. 准备数据:首先,我们需要准备我们的数据集。我们将使用 Hugging Face datasets 库:

    # Load the GitHub issues dataset
    issues_dataset = load_dataset("lewtun/github-issues", split="train")
    
    # Filter out pull requests and keep only issues with comments
    issues_dataset = issues_dataset.filter(
        lambda x: not x["is_pull_request"] and len(x["comments"]) > 0) 
    
  2. 选择相关列:仅保留分析所需列:

    # Define columns to keep
    columns_to_keep = ["title", "body", "html_url", "comments"]
    columns_to_remove = set(issues_dataset.column_names) - \ 
                        set(columns_to_keep)
    # Remove unnecessary columns
    issues_dataset = issues_dataset.remove_columns(columns_to_remove) 
    
  3. 将数据集转换为 pandas DataFrame:将数据集转换为 pandas DataFrame 以便于操作:

    # Set format to pandas and convert the dataset
    issues_dataset.set_format("pandas")
    df = issues_dataset[:] 
    
  4. 爆炸注释,将它们转换回数据集,并处理:将注释展开成单独的行,将 DataFrame 转换回数据集,并计算每条注释的长度。这一步使得数据更适合与检索流程一起使用:

    # Explode comments into separate rows
    comments_df = df.explode("comments", ignore_index=True) 
    # Convert the DataFrame back to a Dataset
    comments_dataset = Dataset.from_pandas(comments_df)
    
    # Compute the length of each comment
    comments_dataset = comments_dataset.map(
        lambda x: {"comment_length": len(x["comments"].split())}, 
        num_proc=1)
    # Filter out short comments
    comments_dataset = comments_dataset.filter(
        lambda x: x["comment_length"] > 15) 
    
  5. 拼接文本以生成嵌入:让我们通过拼接相关文本字段来准备文档文本。我们将从每一行中提取单个字段,并准备代表该行文档文本的文本。这些文档存储在嵌入存储中,用于检索器使用:

    # Function to concatenate text fields
    def concatenate_text(examples):
        return {
           "text": examples["title"] + " \n " + 
                   examples["body"] + " \n " + 
                   examples["comments"]
        }
    # Apply the function to create a text field
    comments_dataset = comments_dataset.map(concatenate_text, 
        num_proc=1) 
    
  6. 加载模型和分词器:让我们加载 LLM,我们将使用它将文档转换为嵌入并将它们存储在嵌入存储中以用于检索器流程:

    # Load pre-trained model and tokenizer
    model_ckpt = "sentence-transformers/all-MiniLM-L6-v2"
    tokenizer = AutoTokenizer.from_pretrained(model_ckpt)
    model = AutoModel.from_pretrained(model_ckpt).to("cpu") 
    
  7. 定义嵌入函数:定义一个嵌入函数,该函数利用我们之前定义的模型来生成嵌入。我们可以迭代地调用此方法,一次生成一个文档的所有嵌入:

    # Function to get embeddings for a list of texts
    def get_embeddings(text_list):
        encoded_input = tokenizer(text_list, padding=True, 
            truncation=True, return_tensors="pt").to("cpu")
        with torch.no_grad():
            model_output = model(**encoded_input)
        return cls_pooling(model_output).numpy() 
    
  8. 计算嵌入:为数据集计算嵌入。现在我们已经定义了嵌入函数,让我们为我们的评论数据集中的所有文档调用它。请注意,我们正在将嵌入存储在同一数据集的新列embedding中:

    # Compute embeddings for the dataset
    comments_dataset = comments_dataset.map(
        lambda batch: {"embeddings": [get_embeddings([text])[0] 
            for text in batch["text"]]},
        batched=True,
        batch_size=100,
        num_proc=1
    ) 
    
  9. 执行语义搜索:让我们为问题执行检索器流程。这将检索与我们所提问题相关的所有问题。我们可以使用这些文档根据需要改进响应:

    # Define a query
    question = "How can I load a dataset offline?"
    # Compute the embedding for the query
    query_embedding = get_embeddings([question]).reshape(1, -1) 
    # Find the nearest examples
    embeddings = np.vstack(comments_dataset["embeddings"])
    similarities = cosine_similarity(
        query_embedding, embeddings
    ).flatten()
    # Display the results
    top_indices = np.argsort(similarities)[::-1][:5]
    for idx in top_indices:
        result = comments_dataset[int(idx)]  # Convert NumPy integer to native Python integer
        print(f"COMMENT: {result['comments']}")
        print(f"SCORE: {similarities[idx]}")
        print(f"TITLE: {result['title']}")
        print(f"URL: {result['html_url']}")
        print("=" * 50) 
    

前面的代码展示了完整的流程,从我们将数据加载到数据存储中,这可以成为检索器的基础,到检索文档,这些文档可以用于在 LLM 生成答案时提供更多上下文。

现在,让我们看看运行此应用程序时的输出看起来如何。示例代码中硬编码了问题,它是:

How can I load a dataset offline?. 

以下是一个示例输出:

COMMENT: Yes currently you need an internet connection because the lib tries to check for the etag of the dataset script ...
SCORE: 0.9054292969045314
TITLE: Downloaded datasets are not usable offline
URL: https://github.com/huggingface/datasets/issues/761
==================================================
COMMENT: Requiring online connection is a deal breaker in some cases ...
SCORE: 0.9052456782359709
TITLE: Discussion using datasets in offline mode
URL: https://github.com/huggingface/datasets/issues/824
================================================== 

这个动手实验展示了端到端 RAG 架构的实际应用,利用强大的检索技术来增强语言生成。前面的代码是从 Hugging Face NLP 课程中改编的,可在huggingface.co/learn/nlp-course/chapter5/6?fw=tf找到。

完整的 Python 文件以及如何运行的详细说明可在github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch2/full_rag_pipeline.py找到。

摘要

在本章中,我们深入探讨了 RAG 模型的世界。我们首先理解了 RAG 的核心原则以及它们与传统生成式 AI 模型的不同之处。这种基础性知识至关重要,因为它为欣赏 RAG 带来的增强功能奠定了基础。

接下来,我们更详细地研究了 RAG 模型的架构,通过详细的代码示例来分解其组件。通过检查编码器、检索器和解码器,你了解了这些模型的内部工作原理以及它们如何整合检索信息以产生更具有上下文相关性和连贯性的输出。

我们随后探讨了 RAG 如何利用信息检索的力量。这些技术帮助 RAG 有效地利用外部知识源来提高生成文本的质量。这对于需要高精度和上下文感知的应用尤其有用。你还学习了如何使用像 Transformers 和 Hugging Face 这样的流行库构建一个简单的 RAG 模型。

随着我们迈向下一章节第三章,我们将在此基础上继续前进。你将了解图数据建模以及如何使用 Neo4j 创建知识图谱。

第三章:为智能应用建立知识图谱的基础理解

在上一章中,我们探讨了什么是 RAG 以及如何结合 LLMs 实现 RAG 流程的一些简单示例。在本章中,我们将探讨知识图谱是什么以及图如何使检索增强生成RAG)更加有效。我们将探讨如何建模知识图谱以及 Neo4j 如何用于此目的。我们将探讨使用 Neo4j 数据持久化方法进行数据建模如何帮助构建更强大的知识图谱。我们还将探讨从关系数据库管理系统RDBMSs)到 Neo4j 知识图谱的数据存储持久化方法,以更好地理解使用各种数据模型的数据。

我们将踏上激动人心的旅程,了解 RAG 模型与 Neo4j 强大的图数据库功能的融合如何使创建利用结构化知识库以增强性能和结果的应用程序成为可能。

在本章中,我们将涵盖以下主要内容:

  • 理解图数据建模的重要性

  • 结合 RAG 和 Neo4j 知识图谱的力量,使用 GraphRAG

  • 增强知识图谱

技术要求

在我们深入探讨如何为与 Neo4j 集成构建知识图谱的实际方面之前,设置必要的工具和环境是至关重要的。以下是本章的技术要求:

  • Neo4j 数据库:您可以使用 Neo4j Desktop 进行本地设置或使用 Neo4j Aura 进行基于云的解决方案。从 Neo4j 下载中心下载 Neo4j Desktop:neo4j.com/download/。对于 Neo4j Aura,请访问 Neo4j Aura:neo4j.com/product/neo4j-graph-database/。Neo4j 提供两种主要的基于云的服务——AuraDB 和 AuraDS:

    • AuraDB是一个为构建智能应用程序的开发者量身定制的完全托管的图数据库服务。它支持灵活的模式、关系的原生存储以及使用 Cypher 语言的高效查询。AuraDB 提供免费层,使用户能够在不产生成本的情况下探索图数据。在neo4j.com/product/auradb/了解更多关于 AuraDB 的信息。

    • AuraDS是一个完全托管的 Neo4j 图数据科学实例,可用于构建数据科学应用程序。您可以在neo4j.com/docs/aura/graph-analytics/了解更多信息。

  • DB Browser for SQLite:此工具用于轻松查询 SQLite 数据库。sqlitebrowser.org/

  • Cypher 查询语言: 在开始本章之前,您需要熟悉 Cypher,Neo4j 的查询语言。Neo4j 提供了优秀的 Cypher 教程。如果您不熟悉 Cypher,Neo4j 提供了优秀的教程和 GraphAcademy 的基本课程 (graphacademy.neo4j.com/),以帮助您入门。您还可以阅读这本书来详细了解 Cypher:Graph Data Processing with Cypher (www.packtpub.com/en-us/product/graph-data-processing-with-cypher-9781804611074).

  • Python 环境: 推荐使用 Python 3.8 或更高版本。请确保您已安装它。您可以从官方 Python 网站下载它 www.python.org/downloads/.

  • Neo4j Python 驱动程序: 这允许您从 Python 与 Neo4j 数据库交互。使用pip安装它:

    pip install neo4j-driver 
    
  • GitHub 仓库: 本章的所有代码和资源都可在以下 GitHub 仓库中找到:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs。导航到ch3文件夹以获取与本章相关的具体内容。

在继续之前,请确保您已安装并配置了所有这些工具和库。此设置将使您能够无缝地跟随示例和练习。

理解图数据建模的重要性

在我们继续查看 GraphRAG 流程如何与 Neo4j 协同工作之前,让我们退一步,了解我们如何建模知识图。我们将使用一些简单的数据,并尝试查看我们在 RDBMS 和图形中如何建模这些数据。我们还将看到这种建模如何根据我们看待数据的方式而有所不同。

图形迫使我们以不同的方式思考,并从不同的角度看待数据,这取决于我们试图解决的问题。虽然这看起来可能像是一个问题,但实际上它实际上打开了许多大门。长期以来,我们一直被教导用实体-关系ER)图来思考 RDBMS 存储方法。当技术有限制且存储成本非常高时,这种方法对于表示/持久化数据是好的。随着技术的进步和硬件的降价,新的途径已经打开,新的数据建模方法成为可能。图形非常适合利用这一点。

要考虑新的数据建模方式,我们可能不得不放弃一些我们习惯于使用 ER 图表示数据的方法。虽然这看起来很简单,但在现实中可能有点困难。学习和放弃的过程类似于以下图中描述的神经可塑性棱镜眼镜实验。

图 3.1 — 神经可塑性棱镜眼镜实验

图 3.1 — 神经可塑性棱镜眼镜实验

这个实验涉及佩戴棱镜眼镜执行一个简单的任务。大脑需要一段时间来调整视觉变化以正确完成任务。当参与者取下眼镜时,需要一段时间才能再次执行相同的任务。数据建模也是如此。我们可能需要放弃一些我们之前依赖的方法,才能构建更好的图数据模型。您可以在sfa.cems.umn.edu/neural-plasticity-prism-goggle-experiment上了解更多关于这个实验的信息。

我们将研究我们在现实生活中如何消费数据,以了解是否有其他方法可以帮助我们构建一个好的图数据模型。

例如,让我们考虑一个图书馆或书店来了解我们的数据或信息消费是如何驱动书籍布局的。在图书馆中,书籍是按照类别和作者姓氏排列的。这与我们利用索引查找数据的方式相似。但图书馆入口处可能还有其他区域突出显示新书和热门书籍。这样做是为了确保人们可以快速找到这些书籍。在关系型数据库管理系统(RDBMS)中尝试模拟这些方面是困难的。但 Neo4j 中的图数据库方法通过利用多个标签使这变得相当容易。这使得图数据库能够帮助我们构建一个有助于轻松高效消费数据的数据模型。使用图,我们可能需要尝试改变我们的思维方式,并尝试几种不同的数据建模方法。我们的初始方法可能并不完全正确,但我们需要不断调整数据模型,以达到一个对我们来说可接受的数据模型。在 RDBMS 和其他技术中,数据模型是固定的,如果做得不对,可能会产生巨大的影响。这正是 Neo4j 脱颖而出的地方。其可选的灵活模式方法帮助我们从一个可能最初并不理想的数据模型开始,但我们可以在不从头开始的情况下逐步调整它。

我们将使用一些小型、简单的数据,并查看使用 RDBMS 和图进行的数据建模。我们将尝试建模的数据如下所示:

  • 一个具有以下必要细节的人:

    firstName
    lastName 
    
  • 以下格式中的人居住过的五个租赁地址:

    Address line 1
    City
    State
    zipCode
    fromTime
    tillTime 
    

虽然这看起来很简单,但它足以让我们理解如何在关系型数据库管理系统(RDBMS)和图中表示这些数据的细微差别。

这些是我们希望通过这些数据来回答的问题:

  • 约翰·多伊(John Doe)目前居住的最新地址是哪里?

  • 约翰·多伊(John Doe)最初居住的地址是哪里?

  • 约翰·多伊(John Doe)居住的第三个地址是哪里?

让我们看看这些数据如何在关系型数据库管理系统(RDBMS)中建模。

关系型数据库管理系统(RDBMS)数据建模

在本节中,我们将查看先前定义的样本数据的 RDBMS 数据建模方面。以下图表示数据模型作为一个实体关系图:

图 3.2 — 实体关系图

图 3.2 — 实体关系图

在这个数据模型中有三个表。Person 表包含个人详细信息。Address 表包含地址详细信息。Person_Address 表包含租赁详情,以及 PersonAddress 表的引用。我们使用这个连接表来表示租赁详情,以避免重复 PersonAddress 实体的数据。在构建这些数据模型时,我们需要格外注意细节,因为更改它们可能相当耗时,具体取决于我们更改了多少。如果我们把一个表拆分成多个表,那么数据迁移可能是一项相当大的任务。

您可以使用此教程创建 SQLite 数据库:datacarpentry.org/sql-socialsci/02-db-browser.html。我们将使用该 SQLite 数据库来加载数据并验证查询以回答我们之前定义的问题。

以下 SQL 脚本创建表:

-- Person Table definition
`CREATE TABLE` IF NOT EXISTS `person` (
    `id` INTEGER PRIMARY KEY,
    `name` varchar(100) NOT NULL,
    `gender` varchar(20) ,
    UNIQUE(`id`)
) ;
-- Address table definition
`CREATE TABLE` IF NOT EXISTS `address` (
    `id` INTEGER PRIMARY KEY,
   `line1` varchar(100) NOT NULL,
    `city` varchar(20) NOT NULL,
    `state` varchar(20) NOT NULL,
    `zip` varchar(20) NOT NULL,
    UNIQUE(`id`)
) ;
-- Person Address table definition
`CREATE TABLE` IF NOT EXISTS `person_address` (
    `person_id` INTEGER NOT NULL,
    `address_id` INTEGER NOT NULL,
    `start` varchar(20) NOT NULL,
    `end` varchar(20) ,
    `FOREIGN KEY` (`person_id`) REFERENCES person (`id`)
        ON DELETE CASCADE ON UPDATE NO ACTION,
    `FOREIGN KEY` (`address_id`) REFERENCES address (`id`)
        ON DELETE CASCADE ON UPDATE NO ACTION
) ; 

以下 SQL 脚本将数据插入到表中:

-- Insert Person Record
INSERT INTO person (id, name, gender) values `(1, 'John Doe', 'Male') ;`
-- Insert Address Records
INSERT INTO address (id, line1, city, state, zip) values `(1, '1 first ln', 'Edison', 'NJ', '11111') ;`
INSERT INTO address (id, line1, city, state, zip) values `(2, '13 second ln', 'Edison', 'NJ', '11111') ;`
INSERT INTO address (id, line1, city, state, zip) values `(3, '13 third ln', 'Edison', 'NJ', '11111') ;`
INSERT INTO address (id, line1, city, state, zip) values `(4, '1 fourth ln', 'Edison', 'NJ', '11111') ;`
INSERT INTO address (id, line1, city, state, zip) values `(5, '5 other ln', 'Edison', 'NJ', '11111') ;`
-- Insert Person Address (Rental) Records
INSERT INTO person_address `(person_id, address_id, start, end) values (1,1,'2001-01-01', '2003-12-31') ;`
INSERT INTO person_address `(person_id, address_id, start, end) values (1,2,'2004-01-01', '2008-12-31') ;`
INSERT INTO person_address `(person_id, address_id, start, end) values (1,3,'2009-01-01', '2015-12-31') ;`
INSERT INTO person_address `(person_id, address_id, start, end) values (1,4,'2016-01-01', '2020-12-31') ;`
INSERT INTO person_address `(person_id, address_id, start, end) values (1,5,'2021-01-01', null) ;` 

一旦我们加载了数据,它看起来会是这样。

图 3.3 — RDBMS 中存储的数据

图 3.3 — RDBMS 中存储的数据

现在,我们将看看如何从 RDBMS 中查询数据:

  • 查询 1 – 获取最新地址

让我们查看以下 SQL 查询以回答第一个问题:

SELECT line1, city, state, zip from
person p, person_address pa, address a
WHERE p.name = `'John Doe'`
    and pa.person_id = p.id
    and pa.address_id = a.id
    and pa.end is `null` 

从查询中,我们可以看到我们依赖于末列值为空来确定哪个是最新的地址。这是在 SQL 查询中确定最后一个地址的逻辑。

  • 查询 2 – 获取第一个地址

我们将查看 SQL 查询以回答第二个问题:

SELECT line1, city, state, zip from
person p, person_address pa, address a
WHERE p.name = `'``John Doe'`
    and pa.person_id = p.id
    and pa.address_id = a.id
`ORDER BY pa.start ASC`
`LIMIT 1` 

从查询中,我们可以看到我们依赖于搜索-排序-过滤模式来获取我们想要的数据,SQL 查询中的逻辑。

  • 查询 3 – 获取第三个地址

我们将查看 SQL 查询以回答第三个问题:

SELECT line1, city, state, zip from
person p, person_address pa, address a
WHERE p.name = 'John Doe'
    and pa.person_id = p.id
    and pa.address_id = a.id
`ORDER BY pa.start ASC`
`LIMIT 2, 1` 

同样,在这个查询中我们也可以看到我们依赖于搜索-排序-过滤模式来获取我们想要的数据。

现在,我们将看看如何使用图来建模这些数据。

图数据建模:基本方法

为了说明目的,我们将使用在图中建模此数据最常见和最简单的方法。

图 3.4 — 基本图数据模型

图 3.4 — 基本图数据模型

这与我们通常用英语表达信息的方式一致:

Person 居住在 Address

在这个句子中,名词被表示为节点,而动词被表示为关系。这种数据模型方法相当简单,几乎类似于 RDBMS 数据模型的 ER 图。这里唯一的区别是,表示租赁的连接表被建模为关系。这种类型的数据持久化的优点是它减少了索引查找成本。在 RDBMS 中,数据检索方面最大的成本是连接表的索引查找成本。随着数据量的增加,这种查找成本会持续增加。我们可以通过这种方法来减少这种成本。

注意

如果你使用 Neo4j,你可以使用这个教程来创建 Neo4j 数据库。

桌面:neo4j.com/docs/desktop-manual/current/operations/create-dbms/.

或者,你可以使用这个教程在云中创建数据库:neo4j.com/docs/aura/auradb/getting-started/create-database/。这里有一个免费选项可用。这对于可能不想或不需要在本地安装 Neo4j Desktop 的人来说可能是最佳选择。Neo4j Aura 是一个完全管理的图数据库即服务解决方案。

让我们看看以下图查询来理解这一点。

以下 Cypher 脚本设置了索引以加快数据加载和检索。这可以被视为一个模式:

CREATE CONSTRAINT person_id_idx FOR (n:Person) REQUIRE n.id IS UNIQUE ;
CREATE CONSTRAINT address_id_idx FOR (n:Address) REQUIRE n.id IS UNIQUE ;
CREATE INDEX person_name_idx FOR (n:Person) ON n.name ; 

这个 Cypher 脚本创建了两个唯一约束,以确保我们不会有两个重复的PersonAddress节点。我们还添加了一个索引,以加快使用名称查找人员的速度。

一旦设置好模式,我们就可以使用这个 Cypher 脚本来将数据加载到 Neo4j 中:

CREATE (p:Person {`id:1, name:'John Doe', gender:'Male'`})
CREATE (a1:Address {`id:1, line1:'1 first ln', city:'Edison', state:'NJ', zip:'11111'`})
CREATE (a2:Address {`id:2, line1:'13 second ln', city:'Edison', state:'NJ',` `zip:'11111'`})
CREATE (a3:Address {`id:3, line1:'13 third ln', city:'Edison', state:'NJ', zip:'11111'`})
CREATE (a4:Address {`id:4, line1:'1 fourth ln', city:'Edison', state:'NJ', zip:'11111'`})
CREATE (a5:Address {`id:5, line1:'5 other ln', city:'Edison', state:'NJ', zip:'11111'`})
CREATE (p)-[:HAS_ADDRESS {`start:'2001-01-01', end:'2003-12-31'`}]->(a1)
CREATE (p)-[:HAS_ADDRESS {`start:'2004-01-01', end:'2008-12-31'`}]->(a2)
CREATE (p)-[:HAS_ADDRESS {`start:'2009-01-01', end:'2015-12-31'`}]->(a3)
CREATE (p)-[:HAS_ADDRESS {`start:'2016-01-01', end:'2020-12-31'`}]->(a4)
CREATE (p)-[:HAS_ADDRESS {`start:'2021-01-01'`}]->(a5) 

一旦我们加载了数据,在图中看起来就像这样。

图 3.5 — 使用图数据建模表示人员租赁的基本方法

图 3.5 — 使用图数据建模表示人员租赁的基本方法

现在,我们将创建与上一节中使用的 RDBMS 查询类似的 Cypher 查询:

  • 查询 1 – 获取最新地址

以下 Cypher 查询获取我们最新的地址:

MATCH (p:Person {name:`'John Doe'`})-[r:HAS_ADDRESS]->(a)
WHERE r.end is `null`
RETURN a 

如果我们查看这个查询,它比我们之前看到的 SQL 查询要简单得多。然而,结果取决于我们如何标记最后一个地址,即不设置关系的end属性。因此,确定最后一个地址的逻辑仍然是查询的一部分,就像在 SQL 查询中一样。我们可以看到,我们在检查关系中的值,并尝试在连接表上使用索引,如下面的代码所示:

and pa.person_id = p.id
and pa.address_id = a.id 

仅避免这些索引本身就可以获得更好的性能。

  • 查询 2 – 获取第一个地址

这个 Cypher 查询为我们获取了第一个地址:

MATCH (p:Person {`name:'John Doe'`})-[r:HAS_ADDRESS]->(a)
WITH r, a
`ORDER BY r.start ASC`
WITH r,a
RETURN a
`LIMIT 1` 

从查询中,我们可以看到我们依赖于搜索-排序-过滤模式来获取我们想要的数据,类似于 SQL 查询。确定第一个地址的逻辑是 Cypher 查询的一部分。

  • 查询 3 – 获取第三个地址

这个 Cypher 查询为我们获取第三个地址:

MATCH (p:Person {`name:'John Doe'`})-[r:HAS_ADDRESS]->(a)
WITH r, a
`ORDER BY r.start ASC`
WITH r,a
RETURN a
`SKIP 2`
`LIMIT 1` 

与之前的查询类似,我们必须依赖搜索排序过滤来获取我们想要的数据。确定第三个地址的逻辑是 Cypher 查询的一部分。

接下来,我们将深入探讨图形数据建模的更细致的方法。

图形数据建模:高级方法

我们将用不同的方式查看这些数据并构建一个数据模型。这个模型受我们如何消费数据的影响。

图 3.6 — 使用图形数据建模表示 Person Rentals 的消费方法

图 3.6 — 使用图形数据建模表示 Person Rentals,消费方法

初看之下,这似乎更接近 RDBMS ER 图。我们有 PersonAddressRental 节点。但相似之处到此为止。我们可以看到 Person 通过一个 FIRSTLATEST 关系与 Rental 节点连接。Rental 可能与另一个 Rental 节点有一个 NEXT 关系。Rental 节点也连接到一个 Address。模型可能看起来有点复杂。一旦我们加载数据并看到它是如何连接的,它就更有意义了。

这个 Cypher 脚本设置了索引以加快数据加载和检索:

CREATE CONSTRAINT person_id_idx FOR (n:Person) REQUIRE n.id IS UNIQUE ;
CREATE CONSTRAINT address_id_idx FOR (n:Address) REQUIRE n.id IS UNIQUE ;
CREATE INDEX person_name_idx FOR (n:Person) ON n.name ; 

我们可以看到索引与之前的模型相同。我们没有为 Rental 节点添加任何索引或约束。

这个 Cypher 脚本将数据加载到 Neo4j 中:

CREATE (p:Person {`id:1, name:'John Doe', gender:'Male'`})
CREATE (a1:Address {`id:1, line1:'1 first ln', city:'Edison', state:'NJ', zip:'11111'`})
CREATE (a2:Address {`id:2, line1:'13 second ln', city:'Edison', state:'NJ', zip:'11111'`})
CREATE (a3:Address {`id:3, line1:'13 third ln', city:'Edison', state:'NJ', zip:'11111'`})
CREATE (a4:Address {`id:4, line1:'1 fourth ln', city:'Edison', state:'NJ', zip:'11111'`})
CREATE (a5:Address {`id:5, line1:'5 other ln', city:'Edison', state:'NJ', zip:'11111'`})
CREATE (p)-[`:FIRST`]->(r1:Rental {`start:'2001-01-01', end:'2003-12-31'`})-[:HAS_ADDRESS]->(a1)
CREATE (r1)-[`:NEXT`]->(r2:Rental {`start:'2004-01-01', end:'2008-12-31'`})-[:HAS_ADDRESS]->(a2)
CREATE (r2)-[`:NEXT`]->(r3:Rental {`start:'2009-01-01', end:'2015-12-31'`})-[:HAS_ADDRESS]->(a3)
CREATE (r3)-[`:NEXT`]->(r4:Rental {`start:'2016-01-01', end:'2020-12-31'`})-[:HAS_ADDRESS]->(a4)
CREATE (r4)-[`:NEXT`]->(r5:Rental {`start:'2021-01-01'`})-[:HAS_ADDRESS]->(a5)
CREATE (p)-[`:LATEST`]->(r5) 

数据加载后,在图中将看起来像这样(图 3.7)。

图 3.7 — 使用图形数据建模表示 Person Rentals 的租赁序列图

图 3.7 — 使用图形数据建模表示 Person Rentals 的租赁序列图

我们可以看到,存储在图中的数据与之前大不相同。Person 只与第一个和最后一个租赁相关联。从第一个到最后的每个租赁都通过一个 NEXT 关系连接:

  • 查询 1 – 获取最新地址

这个 Cypher 查询为我们获取最新的地址:

MATCH (p:Person {name:`'John Doe'`})-[:`LATEST`]->()-[:HAS_ADDRESS]->(a)
RETURN a 

我们可以看到,这个查询与之前的图形和 SQL 查询非常不同。在之前的图形模型中,Cypher 查询在确定最后一个地址方面与 SQL 查询相似。在这里,查询看起来类似于一句英文(Person’s latest address)。

虽然查询看起来对大多数人来说更简单、更容易理解,但这种表示数据的方式值得吗?在这种情况下,我们将使用更多的存储空间来以更详细的方式表示数据。让我们分析从初始图形数据模型到这个数据模型的查询,看看是否有任何优势。

图 3.8 — 基本图形模型与高级图形模型 – 查询 1 分析

图 3.8 — 基本图形模型与高级图形模型 – 查询 1 分析

查询配置文件 中,我们可以看到初始图数据模型执行操作需要 18 数据库访问(访问)和 312 字节的内存。当前图数据模型执行操作需要 12 次数据库访问和 312 字节的内存。我们可以看到,新的数据模型能够更优化地执行这个查询。随着数据量的增长,之前的图数据模型将需要更多的时间来执行操作,数据库访问次数将与该人拥有的关系数量线性增长。使用当前数据模型,它将保持相对稳定。

现在我们来看查询 2。

  • 查询 2 – 获取第一个地址

这个 Cypher 查询获取我们到第一个地址:

MATCH (p:Person {name:`'John Doe'`})-[:`FIRST`]->()-[:HAS_ADDRESS]->(a)
RETURN a 

我们可以看到,这个查询与上一个查询几乎完全相同,只是我们遍历的关系不同。我们不再使用 search-sort-filter 模式。这是这种数据模型的最大优势。这种模型还使我们能够轻松地将图用作结构来检索数据。这也意味着确定我们正在查看的数据的逻辑并没有以某些属性比较的形式编码到查询中。让我们比较查询配置文件,看看这是否给我们带来任何优势。

图 3.9 — 基本图模型与高级图模型对比 – 查询 2 配置文件

图 3.9 — 基本图模型与高级图模型对比 – 查询 2 配置文件

我们可以看到,对于初始图数据模型,查询执行计划比当前数据模型更大、更复杂。使用初始图数据模型,执行操作需要 19 次数据库访问和 1,020 字节的内存。使用当前数据模型,计划几乎与查询 1 相似。执行操作需要 12 次数据库访问和 312 字节的内存。我们可以看到,排序导致我们使用了更多的内存,并将消耗更多的 CPU 周期。随着Person与更多地址的连接,初始图数据模型将需要更多的内存和数据库访问,性能将逐渐下降。使用当前数据模型,性能将保持相对稳定。

  • 查询 3 – 获取第三个地址

这个 Cypher 查询获取第三个地址:

MATCH (p:Person {name:`'John Doe'`})-[:`FIRST`]->()-[`:NEXT*2..2`]->()-[:HAS_ADDRESS]->(a)
RETURN a 

我们可以从查询中看到,它的编写方式是遍历到第一个租赁,跳过下一个租赁以到达第三个租赁。这是我们通常查看数据的方式,并以这种方式检索数据感觉自然。同样,我们不再依赖于 search-sort-filter 模式。让我们比较查询配置文件,看看这是否给我们带来任何优势。

图 3.10 — 基本图模型与高级图模型对比 – 查询 3 配置文件

图 3.10 — 基本图模型与高级图模型对比 – 查询 3 配置文件

从这些配置文件中,我们可以看到当前数据模型查询配置文件比之前的查询要复杂一些。初始图数据模型执行操作需要 19 次数据库访问和 1,028 字节。当前图数据模型执行操作需要 16 次数据库访问和 336 字节。

注意

查询分析是了解查询工作原理的最佳方式。如果我们对查询性能不满意,分析可以帮助我们了解哪些查询执行区域需要改进或更改以提高性能。你可以在 neo4j.com/docs/cypher-manual/current/planning-and-tuning/ 上了解更多相关信息。

通过分析查询和数据模型,我们可以看到,重新审视数据模型的定义可以在性能和执行相同操作的成本方面产生巨大影响。

当前数据模型的另一个优点是,如果我们确实想从地址的角度跟踪租赁情况,我们可以在同一地址的租赁之间添加另一个关系,比如说 NEXT_RENTAL。这将为我们提供同一数据的另一种视角。在 RDBMS 或其他数据持久化层中尝试以这种方式表示数据将是困难的。这正是 Neo4j 的优势所在,它具有灵活性,能够持久化关系以避免连接索引成本和可选模式,这使得它更适合构建知识图谱。

一个好的图数据模型可以使 RAG 流中的 检索器 更有效。它使得检索相关数据更快、更简单,正如我们在这里所探讨的。

我们将探讨如何将知识图谱作为 RAG 流的一部分来使用。

将 RAG 和 Neo4j 知识图谱的强大功能结合到 GraphRAG 中

在上一章中,我们讨论了 检索器,它是 RAG 流的核心。检索器利用数据存储来检索相关信息,以提供给 LLM 以获得对我们问题的最佳回答。检索器可以根据需要与各种数据存储一起工作。数据存储功能可以极大地决定检索到的信息的有用性、速度和有效性。这正是图发挥重要作用的地方。这就是 GraphRAG 产生的原因。

注意

你可以在 www.microsoft.com/en-us/research/blog/graphrag-unlocking-llm-discovery-on-narrative-private-data/microsoft.github.io/graphrag/ 上了解更多关于 GraphRAG 及其如何有效利用的信息。为了全面了解 GraphRAG,你可以参考微软的研究论文《从局部到全局:基于图 RAG 的查询聚焦摘要方法》(arxiv.org/abs/2404.16130)。此外,微软已在 GitHub 上发布了 GraphRAG 项目(github.com/microsoft/graphrag),提供了实施此方法所需资源和工具。

Neo4j 图数据库擅长以属性图的形式持久化数据,其中包含节点和关系。这使得以直观的方式存储和检索数据变得容易,并为 RAG 检索器提供数据存储。这种方法允许实现更准确、上下文感知和可靠的 AI 驱动应用程序。

我们现在将构建一个 GraphRAG 流程,结合 RAG 和知识图谱的力量以改善 LLM 响应。

GraphRAG:通过 Neo4j 增强 RAG 模型

在上一章中,我们讨论了具有 RAG 模型的聊天应用中的信息流(参见图 3.5)。

现在,我们将看到如何增强此工作流程以生成改进的聊天应用响应。图 3.11 展示了 GraphRAG 的工作流程,其中用户的提示通过 LLM API 处理,从 Neo4j 中检索相关信息,然后与提示结合在一起,再发送到 LLM API。

图 3.11 — GraphRAG 的工作流程

图 3.11 — GraphRAG 的工作流程

LLM API 使用提示和来自 Neo4j 知识图谱的相关信息生成响应,为用户提供准确且上下文丰富的结果。通过结合 Neo4j 和 RAG 模型的功能,GraphRAG 通过更多的领域上下文增强了相关性。

让我们构建一个简单的图来展示这个 GraphRAG 流程。

为 RAG 集成构建知识图谱

对于此示例,我们将使用有限的数据进行演示,以构建图,重点关注电影及其剧情。

Python 代码示例:在 Neo4j 中设置知识图谱

通过跟随提供的代码示例,您将学习如何设置 Neo4j 数据库,定义节点和关系,并使用 Cypher 执行基本查询:

  1. 设置 Neo4j 数据库:在运行代码之前,请确保您有权访问 Neo4j 数据库。您可以使用以下任一选项:

  2. 启动您的数据库实例并记录连接凭据(例如,URI、用户名和密码)。

  3. 安装必要的 Python 库:您需要以下 Python 库:

    • Neo4j Python 驱动程序:用于与数据库交互

    • Pandas:用于处理数据结构和分析

    • 使用以下命令安装这些库:

      pip install neo4j pandas 
      
  4. 连接到数据库并设置知识图谱:一旦您的 Neo4j 数据库运行并且已安装所需的 Python 库,您可以使用以下 Python 脚本设置一个简单的知识图谱。在此示例中,我们将创建一个包含电影及其剧情的图,节点代表电影和剧情,关系表示哪个剧情属于哪部电影。

注意

我们将不会使用任何外部数据集,而是将使用硬编码的数据集来展示图模型和 GraphRAG 流程。我们将在第四章和第五章中探索完整的数据加载和 GraphRAG 流程。此示例仅用于展示 GraphRAG 流程的方面。

我们首先构建一个简单的图。我们将使用这个简单的图来展示 Neo4j 在 GraphRAG 流程中的位置和作用:

  1. 导入GraphDatabase库并定义 Neo4j 连接性和凭证:

    from neo4j import GraphDatabase
    uri = "bolt://localhost:7687"  # Replace with your Neo4j URI
    username = "neo4j"             # Replace with your Neo4j username
    password = "password"          # Replace with your Neo4j password 
    
  2. 让我们创建一些节点:

    def create_graph(tx):
        tx.run("CREATE (m:Movie {title: 'The Matrix', year: 1999})")
        ....
        # Create plot nodes
        tx.run("CREATE (p:Plot {description: 'A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.'})") 
    
  3. 下一步是创建关系:

    tx.run("""
        MATCH (m:Movie {title: 'The Matrix'}),
              (p:Plot {description: 'A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.'})
        CREATE (m)-[:HAS_PLOT]->(p)
        """) 
    
  4. 如果我们可视化我们创建的数据,它将看起来如图 3.12 所示:

    MATCH p=(:Movie)-[:HAS_PLOT]->()
    RETURN p
    LIMIT 5 
    

图 3.12 — 显示电影和剧情的示例图

图 3.12 — 显示电影和剧情的示例图

  1. 我们现在将使用 Cypher 查询检索数据:
def query_graph(tx):
    # Query to retrieve movies and their plots
    result = tx.run("""
    MATCH (m:Movie)-[:HAS_PLOT]->(p:Plot)
    RETURN m.title AS movie, m.year AS year, p.description AS plot
    """)
    # Print the results
    for record in result:
        print(f"Movie: {record['movie']} ({record['year']}) - Plot: {record['plot']}") 
  1. 如果我们运行它,我们可以看到如下所示的输出:
Movie: The Matrix (1999) - Plot: A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers. 

你可以在github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch3/imdb_kg.py找到完整的代码。

现在我们已经构建了基本的图,让我们在 GraphRAG 流程中使用它。

将 RAG 与 Neo4j 知识图谱集成

要将 RAG 模型与 Neo4j 集成,您需要配置模型以查询图数据库。这通常涉及设置一个 API 或中间件层,以促进 RAG 模型和 Neo4j 之间的通信。

这里提供了一个示例集成工作流程:

  1. 用户输入:用户提供一个提示。在以下代码示例中,提示信息在脚本中预定义作为示例("The Matrix")。用户可以修改它以测试其他电影或提示:

    prompt = "The Matrix" 
    
  2. 查询生成:提示信息被处理,并生成一个 Cypher 查询以从 Neo4j 中检索相关信息。例如,查询可能会获取提示中提到的电影的剧情:

    """
        Fetch relevant data (plots) for movies that match the user's prompt.
        """
        query = f"""
        MATCH (m:Movie)-[:HAS_PLOT]->(p:Plot)
        WHERE m.title CONTAINS '{prompt}'
        RETURN m.title AS title, m.year AS year, p.description AS plot
        """ 
    
  3. 数据检索:执行 Cypher 查询,并从知识图谱中获取相关数据(例如,《黑客帝国》的剧情):

    with driver.session() as session:
            result = session.run(query)
            records = [
                {
                    "title": record["title"],
                    "year": record["year"],
                    "plot": record["plot"],
                }
                for record in result if record["plot"] is not None
            ]
            print(f"Retrieved Records: {records}")  # Debugging line
            return records 
    
  4. RAG 模型处理:检索到的数据与原始提示信息结合,并传递给 RAG 模型进行进一步处理,允许模型生成更丰富和上下文感知的响应:

    """
        Combine the user's prompt with relevant data from the graph
        and generate a focused, non-repetitive response using the RAG model.
        """
        relevant_data = get_relevant_data(prompt)
        if not relevant_data:
            return "No relevant data found for the given prompt."
        # Combine dictionaries in relevant_data into a single string
        combined_input = (
          f"Provide detailed information about: {prompt}. " + 
          " ".join([
              f"{data['title']} ({data['year']}): {data['plot']}" 
              for data in relevant_data
        ])
        print(f"Combined Input: {combined_input}")
        if not combined_input.strip():
            return "No relevant data to process for this prompt."
        # Tokenize the combined input with truncation
        max_input_length = 512 - 50  # Leave space for output
        tokenized_input = tokenizer(combined_input, truncation=True, 
            max_length=max_input_length, return_tensors="pt") 
    
  5. 响应生成:RAG 模型使用增强的提示信息(例如,“《黑客帝国》的剧情是:‘一个计算机黑客从神秘的叛军那里了解到他现实世界的真实性质以及他在对抗其控制者的战争中的角色。’”)生成响应:

    # Generate response with tuned parameters
        outputs = model.generate(
            **tokenized_input,
            max_length=150,
            temperature=0.7,
            top_k=50,
            top_p=0.9,
            num_beams=5,
            no_repeat_ngram_size=3,
            early_stopping=True
        )
        # Decode the response with improved formatting
        response = tokenizer.decode(outputs[0], 
            skip_special_tokens=True, 
            clean_up_tokenization_spaces=True)
        return response 
    

下面的示例输出:

Prompt: The Matrix
Response: : the matrix ( 1999 ) : a computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers. 

本章中代码的完整版本放置在:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch3/neo4j_rag.py

numpy versions < 2. If you are running numpy versions > 2, use the following commands on the terminal to create a clean virtual environment to isolate the issue:
python3 -m venv my_env
source my_env/bin/activate
pip install numpy==1.26.4 neo4j transformers torch faiss-cpu datasets 

通过理解如何构建和查询基本知识图,以及如何将 RAG 模型与 Neo4j 集成,你现在已经具备了创建智能、上下文感知应用程序所需的基础技能。接下来,我们将探讨一些增强知识图的方法。我们在这里只是介绍这些概念,将在接下来的章节中更详细地探讨它们,以构建智能应用程序。

增强知识图

在上一节中,我们探讨了构建图和 GraphRAG 流程。我们所探讨的是一个简单的图。我们可以遵循一些方法来使知识图更有效。让我们来看看这些方法。我们将在接下来的章节中使用这些方法来增强我们的知识图:

  • 本体开发:本体可以定义图的结构和内容。通过将本体持久化在图中,我们可能能够以更直观的方式解释数据和其连通性。这确保了图遵循最佳实践并与您的特定领域需求保持一致。本体还有助于在不同数据集之间保持一致性,并在随时间扩展图。在第五章中,我们将增强本章中创建的简单电影知识图。如果您想了解更多关于本体,可以查看neo4j.com/blog/ontologies-in-neo4j-semantics-and-knowledge-graphs/

  • 图数据科学(GDS):虽然作为图加载数据可以作为一个有效的知识图,但还有一些其他方法可以使这个图更加有效。例如,我们可以执行一些链接预测或进行社区检测,以在图中基于现有数据推断出节点之间的额外关系。这可以帮助我们增强图中存储的智能,以便在查询时提供更好的答案。我们将在第十章中利用 KNN 相似性和社区检测算法来增强图,以获得更多的智能。

我们探讨了增强知识图的一些方法。现在,让我们总结一下我们对所探讨的概念的理解。

概述

在本章中,我们探讨了使用 Neo4j 构建知识图以实现 RAG 集成的基本方面。我们首先理解了 Neo4j 知识图的重要性及其在 GraphRAG 中的作用。我们还设置了 Neo4j 数据库,创建了节点和关系,并执行查询以检索相关信息。

我们还介绍了 RAG 模型与 Neo4j 的集成工作流程。你现在可以继续进入 第二部分将 Haystack 与 Neo4j 集成:构建 AI 驱动搜索的实用指南。在下一部分,我们将在此基础上构建,探讨如何将 Haystack 与 Neo4j 集成以创建强大、AI 驱动的搜索功能。这一步将自然而然地扩展你的知识和技能,使你能够开发利用 Haystack 和 Neo4j 双方优势的复杂搜索应用。

第二部分

将 Haystack 与 Neo4j 集成:构建 AI 驱动搜索的实用指南

在本书的第二部分,我们从概念转向实际操作。我们首先将一个现实世界的数据集——电影——建模为一个结构良好的 Neo4j 知识图谱,为智能查询做准备。然后,我们将 Haystack 框架与 Neo4j 集成,以实现结合语义理解和基于图谱的上下文的强大、混合搜索体验。最后一章进一步探讨了高级功能,如多跳推理和上下文感知搜索,展示了如何从您的知识图谱中解锁更深入的见解。

本部分包括以下章节:

  • 第四章**,使用电影数据集构建您的 Neo4j 图谱

  • 第五章**,使用 Neo4j 和 Haystack 实现强大的搜索功能

  • 第六章**,探索高级知识图谱功能

请保持关注

为了跟上生成式 AI 和 LLMs 领域的最新发展,请订阅我们的每周通讯,AI_Distilled,packt.link/Q5UyU

第四章:使用电影数据集构建您的 Neo4j 图

在前面的章节中,我们学习了知识图谱如何成为一项变革性工具,它提供了一种结构化的方式来连接不同的数据点,使各种领域中的智能搜索、推荐和推理能力成为可能。

知识图谱擅长捕捉实体之间的复杂关系,对于需要深度上下文理解的应用程序来说,它们是不可或缺的。

基于其最先进的图数据库技术,Neo4j 在构建和管理知识图谱方面脱颖而出,成为领先的平台。正如我们在上一章中看到的,与传统的关系数据库不同,Neo4j 被设计用来轻松处理高度连接的数据,这使得查询更加直观,并能够更快地检索洞察。这使得它成为希望将原始的非结构化数据转化为有意义洞察的开发人员和数据科学家的理想选择。

在本章中,我们将涵盖以下主要主题:

  • 为高效搜索设计的 Neo4j 图设计考虑

  • 利用电影数据集

  • 使用代码示例构建您的电影知识图谱

  • 超越基础:用于复杂图结构的先进 Cypher 技术

技术要求

要成功完成本章的练习,您需要以下工具:

  • Neo4j AuraDB:您可以使用 Neo4j AuraDB,这是 Neo4j 的云版本,可在neo4j.com/aura找到。

  • Cypher 查询语言:熟悉 Cypher 查询语言是必要的,因为我们将广泛使用 Cypher 来创建和查询图。您可以在 Cypher 查询语言文档中找到有关 Cypher 语法的更多信息:neo4j.com/docs/cypher/

  • Python:您需要在系统上安装 Python 3.x。Python 用于脚本编写和与 Neo4j 数据库交互。您可以从官方 Python 网站下载 Python:www.python.org/downloads/

  • Python 库

    • Python 的 Neo4j 驱动程序:使用 Python 连接到 Neo4j 数据库,请安装 Neo4j Python 驱动程序。您可以通过pip安装它:

      pip install neo4j 
      
    • pandas:此库将用于数据处理和分析。您可以通过pip安装它:

      pip install pandas 
      
  • 集成开发环境(IDE):推荐使用 PyCharm、VS Code 或 Jupyter Notebook 等 IDE 来高效地编写和管理您的 Python 代码。

  • Git 和 GitHub:需要基本的 Git 知识来进行版本控制。您还需要一个 GitHub 账户来访问本章的代码仓库。

  • 电影数据集The Movie DatabaseTMDb)是必需的,可在 Kaggle 上找到:www.kaggle.com/datasets/rounakbanik/the-movies-dataset/

  • 此数据集是Movie Lens Datasets(F. Maxwell Harper 和 Joseph A. Konstan. 2015. The MovieLens Datasets: History and Context. ACM Transactions on Interactive Intelligent Systems (TiiS) 5, 4: 19:1–19:19. doi.org/10.1145/2827872)的衍生品。

  • 由于存储限制,某些数据文件,如credits.csvratings.csv,可能不在 GitHub 上可用。但是,您可以从 GCS 存储桶中访问所有原始数据文件。

本章的所有代码均可在以下 GitHub 仓库中找到:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/tree/main/ch4

该文件夹包含所有必要的文件和脚本,以帮助您使用电影数据集和 Cypher 代码构建 Neo4j 图。

确保克隆或下载该仓库,以跟随本章提供的代码示例。

GitHub 仓库包含访问原始数据文件的 GCS 路径。

为高效搜索设计的 Neo4j 图设计考虑

一个设计良好的 Neo4j 图确保您的搜索功能不仅准确,而且高效,能够快速检索相关信息。数据在图中的组织方式直接影响搜索结果的表现力和相关性,因此理解有效图建模的原则至关重要。

本节将深入探讨正确结构化您的 Neo4j 图的重要性,它如何影响搜索过程,以及在设计图模型时您需要牢记的关键考虑因素。

定义节点和关系类型时的注意事项

回想一下第三章,任何 Neo4j 图的基础都是建立在节点关系之上的。节点代表实体,如电影或人物(例如,演员或导演),而关系定义了这些实体如何连接。您选择的节点和关系类型在确定搜索查询的有效性方面起着至关重要的作用。

在电影数据集中,节点可以传统地表示不同的实体,如MoviesActorsDirectorsGenres。关系随后定义这些节点如何交互,如ACTED_INDIRECTEDBELONGS_TO。然而,有一种替代方法,通常更有效——将相似实体合并为单个节点类型。

你不需要为ActorsDirectors创建单独的节点,你可以创建一个单一的Person节点。每个Person节点的特征——无论是演员、导演还是两者都是——由它与Movie节点的关系类型定义。例如,通过ACTED_IN关系连接到Movie节点的Person节点表示该人是该电影中的演员。同样,DIRECTED关系表示该人执导了该电影。我们将在接下来的章节中创建完整的图。

但首先,让我们谈谈为什么这种方法更好。正如我们在第三章中展示的那样,这种方法导致以下结果:

  • 简化的数据模型: 通过使用单个Person节点来表示演员和导演,你的数据模型变得更加精简。这降低了图的复杂性,使其更容易理解和维护。

  • 增强查询性能: 由于节点类型较少,图数据库在查询期间可以更有效地遍历关系。这是因为数据库引擎有更少的独特实体需要区分,从而缩短查询执行时间。

  • 减少冗余: 通过统一的Person节点,消除了信息重复的需求。在一个人既是演员又是导演的情况下,你可以避免创建两个具有重叠数据的独立节点,从而最小化冗余,节省存储空间。

  • 灵活的关系定义: 这种方法允许更灵活和细粒度的关系定义。如果一个人在多部电影中扮演多个角色(例如,在一部电影中担任演员,在另一部电影中担任导演),关系可以清楚地区分这些角色,而无需创建多个节点。

  • 易于维护和扩展: 随着你的数据集增长,维护更简单的节点结构变得越来越重要。当你使用统一的节点类型工作时,添加新的角色或关系变得更加直接。

通过仔细选择和定义这些类型和关系,你创建了一个反映现实世界联系的图结构。这使得你的搜索查询更加直观,结果更有意义,整个系统更加高效。

应用索引和约束对搜索性能的影响

随着你的 Neo4j 图数据库增长,索引约束的应用变得至关重要。索引允许 Neo4j 快速定位查询的起点,极大地提高了搜索性能,尤其是在大型数据集中。然而,约束通过防止创建重复节点或无效关系来确保数据完整性。

在我们的电影数据集的背景下,我们使用一个统一的Person节点来表示演员和导演,索引变得尤为重要。你可以根据诸如person_namerole等属性来索引节点,确保对特定人物或他们在电影中的角色的搜索能够迅速返回结果。例如,你可以对关系(例如,ACTED_INDIRECTED)上的角色属性进行索引,以便快速过滤参与特定电影的人物。

约束对于维护图形的完整性也是必不可少的。让我们看看这些约束中的一些。这些约束应根据数据集的性质和应用需求仔细设计——它们不是一刀切解决方案。

以下是一些示例语句,展示了如何为电影数据集创建定制的约束和索引。这些示例包括确保人员标识符的唯一性和优化节点和关系属性上的搜索性能的常见场景。根据你的具体用例和数据质量,你可以调整这些模式以强制执行数据完整性和提高查询速度:

  • person_name的唯一约束(对于简化用例)。在许多情况下——例如我们的电影数据集,我们假设每个人都有一个独特的名字——你可能会对person_name属性施加唯一约束,以确保即使他们在不同的电影中扮演多个角色(例如,演员和导演),每个个体也只由一个节点表示。以下是你可以这样做的示例:

    CREATE CONSTRAINT unique_person_name IF NOT EXISTS
    FOR (p:Person)
    REQUIRE p.person_name IS UNIQUE; 
    

这有助于防止意外创建重复节点,并保持你的图形干净高效。

  • 对更可靠的 ID(例如,person_id)的唯一约束。在前面的场景中,唯一约束是基于对数据的假设。在现实世界的场景中,遇到具有相同名字的不同个体是很常见的。

在这种情况下,你应该使用更可靠的标识符,例如来自外部源(例如,互联网电影数据库IMDb)或TMDb)的person_id值,以确保唯一性。以下 Cypher 代码展示了如何实现这一点:

CREATE CONSTRAINT unique_person_id IF NOT EXISTS
FOR (p:Person)
REQUIRE p.person_id IS UNIQUE; 
  • person_name上建立索引(如果未强制执行唯一性,则用于更快地查找)。如果你没有强制执行唯一性,但仍然经常按名称搜索人物,对person_name属性建立索引可以显著提高查询性能。这允许 Neo4j 根据其名称快速定位Person节点:

    CREATE INDEX person_name_index IF NOT EXISTS
    FOR (p:Person)
    ON (p.person_name); 
    
  • Movietitle属性上建立索引。电影通常按标题查询——特别是在推荐系统或搜索功能中。对title属性建立索引确保当用户搜索特定电影时能够快速查找:

    CREATE INDEX movie_title_index IF NOT EXISTS
    FOR (m:Movie)
    ON (m.title); 
    
  • ACTED_IN关系中的role属性上建立索引。如果你的应用程序需要通过电影中演员的具体角色进行过滤(例如,主角或客串),在ACTED_IN关系上对role属性建立索引可以帮助加快这些查询,避免对所有关系进行全扫描:

    CREATE INDEX acted_in_role_index IF NOT EXISTS
    FOR ()-[r:ACTED_IN]-()
    ON (r.role); 
    

    注意

    Neo4j 仅支持版本5.x及以上版本的关系属性索引。

正确实现的索引和约束使你的图更加健壮,搜索过程更快、更可靠。这不仅提升了用户体验,还减少了系统上的计算负载,允许实现更可扩展的解决方案。

在下一节中,我们将探讨如何通过利用电影数据集来构建你的图来发挥开放数据的力量。

利用电影数据集

在本节中,我们将专注于利用TMDb,这是一个在 Kaggle 上提供的综合元数据集合:www.kaggle.com/datasets/rounakbanik/the-movies-dataset/。这个数据集包含了关于电影的各种信息,如标题、类型、演员阵容、制作团队、上映日期和评分。这个数据集包含超过 45,000 部电影及其制作人员的详细信息,为构建一个能够捕捉电影行业复杂关系的 Neo4j 图提供了一个坚实的基础。

你将使用这个数据集来将数据建模为知识图谱,在一个实际的应用场景中学习数据集成。你将学习如何获取、准备并将这些数据导入 Neo4j。

当处理像 TMDb 这样的大型数据集时,在将其集成到你的 Neo4j 图之前确保数据是清洁的、一致的并且结构良好至关重要。原始数据虽然信息丰富,但往往包含不一致性、冗余和复杂的结构,这些可能会阻碍知识图谱的性能和准确性。这就是数据规范化和清理发挥作用的地方。

为什么需要规范化和清理数据?

在构建 Neo4j 图时,保持数据集的清洁和规范化至关重要,因为它直接影响到应用程序的质量和性能。通过规范化和清理数据,你确保了数据的一致性,提高了效率,并为分析创建了一个可扩展的基础。以下是为什么每个步骤都很重要的原因:

  • 一致性:原始数据可能存在记录相似信息的方式上的变化。例如,电影类型可能以不同的格式列出或包含重复项。规范化数据确保相似的数据点以一致格式记录,这使得查询和分析更加容易。然而,在现实世界的数据集中处理这些问题可能具有挑战性。Neo4j 通过强大的功能如 Cypher 模式匹配、用于合并节点和清理重复项的 APOC 过程以及包含节点相似度算法以识别和合并相关实体的 Graph Data Science 库,帮助解决实体链接和去重等问题。这些功能使您能够构建一个干净、可靠的图,反映数据的真实结构。

  • 效率:规范化数据减少了冗余,这可以提高 Neo4j 图的效率。通过将数据组织成标准化的格式,您可以最小化存储需求并优化查询性能。

  • 准确性:清理数据涉及删除或纠正不准确记录。这一步骤对于确保从您的图中得出的见解基于准确和可靠的数据至关重要。

  • 可扩展性:一个干净且规范化的数据集更容易进行扩展。随着数据集的增长,保持标准化的结构可以确保图在不断增加的负载下保持可管理并表现良好。

让我们继续清理和规范化 CSV 文件。

清理和规范化 CSV 文件

现在,我们将清理和规范化 TMDb 中包含的每个 CSV 文件。我们数据集中的可用 CSV 文件如下:

  • credits.csv:此文件包含关于我们数据集中每部电影演员和制作团队的详细信息,以字符串化的 JSON 对象形式呈现。就我们的目的而言,我们将专注于提取与角色、演员、导演和制片人相关的相关细节:

    # Load the CSV file
    df = pd.read_csv('./raw_data/credits.csv')
    # Function to extract relevant cast information
    def extract_cast(cast_str):
        cast_list = ast.literal_eval(cast_str)
        return [
            {
                'actor_id': c['id'],
                'name': c['name'],
                'character': c['character'],
                'cast_id': c['cast_id']
            }
            for c in cast_list
        ]
    # Function to extract relevant crew information
    def extract_crew(crew_str):
        crew_list = ast.literal_eval(crew_str)
        relevant_jobs = ['Director', 'Producer']
        return [
            {
                'crew_id': c['id'],
                'name': c['name'],
                'job': c['job']
            }
            for c in crew_list if c['job'] in relevant_jobs
        ]
    # Apply the extraction functions to each row
    df['cast'] = df['cast'].apply(extract_cast)
    df['crew'] = df['crew'].apply(extract_crew)
    # Explode the lists into separate rows
    df_cast = df.explode('cast').dropna(subset=['cast'])
    df_crew = df.explode('crew').dropna(subset=['crew'])
    # Normalize the exploded data
    df_cast_normalized = pd.json_normalize(df_cast['cast'])
    df_crew_normalized = pd.json_normalize(df_crew['crew'])
    # Reset index to avoid duplicate indices
    df_cast_normalized = df_cast_normalized.reset_index(drop=True)
    df_crew_normalized = df_crew_normalized.reset_index(drop=True)
    # Drop duplicate rows if any
    df_cast_normalized = df_cast_normalized.drop_duplicates()
    df_crew_normalized = df_crew_normalized.drop_duplicates()
    # Add the movie ID back to the normalized DataFrames
    df_cast_normalized['tmdbId'] = df_cast.reset_index(drop=True)['id']
    df_crew_normalized['tmdbId'] = df_crew.reset_index(drop=True)['id']
    # Save the normalized data with the updated column names
    df_cast_normalized.to_csv(
        os.path.join(output_dir, 'normalized_cast.csv'),
        index=False
    )
    df_crew_normalized.to_csv(
        os.path.join(output_dir, 'normalized_crew.csv'),
        index=False
    )
    # Display a sample of the output for verification
    print("Sample of normalized cast data:")
    print(df_cast_normalized.head())
    print("Sample of normalized crew data:")
    print(df_crew_normalized.head()) 
    
  • keywords.csv:此文件包含数据集中每部电影的剧情关键词。这些关键词对于对电影中的主题元素进行分类和识别至关重要,可用于各种目的,例如搜索、推荐和内容分析:

    # Load the CSV file
    df = pd.read_csv('./raw_data/keywords.csv')  # Update the path as necessary
    # Function to extract and normalize keywords
    def normalize_keywords(keyword_str):
        if pd.isna(keyword_str) or not isinstance(keyword_str, str):  # Check if the value is NaN or not a string
            return []
        # Convert the stringified JSON object into a list of dictionaries
        keyword_list = ast.literal_eval(keyword_str)
        # Extract the 'name' of each keyword and return them as a list
        return [kw['name'] for kw in keyword_list]
    # Apply the normalization function to the 'keywords' column
    df['keywords'] = df['keywords'].apply(normalize_keywords)
    # Combine all keywords for each tmdbId into a single row
    df_keywords_aggregated = df.groupby('id', as_index=False).agg({
        'keywords': lambda x: ', '.join(sum(x, []))
    })
    # Rename the 'id' column to 'tmdbId'
    df_keywords_aggregated.rename(
        columns={'id': 'tmdbId'}, inplace=True
    )
    # Save the aggregated DataFrame to a new CSV file
    df_keywords_aggregated.to_csv(
        os.path.join(output_dir, 'normalized_keywords.csv'),
        index=False
    )
    # Display the first few rows of the aggregated DataFrame for verification
    print(df_keywords_aggregated.head()) 
    
  • links.csv:此文件包含将全MovieLens 数据集中的每部电影与其在 TMDb 和 IMDB 中的对应条目链接的必要元数据。此文件作为连接 MovieLens 数据集与外部电影数据库的关键桥梁,实现了数据集成和进一步分析的丰富化。然而,对于此用例,我们跳过了处理links.csv文件,因为它对我们当前的分析不是必需的。我们的重点将保持在其他与我们的项目目标更直接相关的 CSV 文件上。links.csv中的数据对于需要与外部数据库集成的未来项目仍然可能是有用的,但在此实例中不会使用。

  • links_small.csv: 这个文件包含来自完整 MovieLens 数据集的 9,000 部电影的 TMDb 和 IMDb IDs 的子集。虽然这个文件为较小电影集合提供了简化的链接版本,但我们不会使用这个文件,因为我们已经使用了 Kaggle 提供的完整数据集,其中包含所有可用的电影。这个文件通常在需要更易于管理的较小数据集的场景中很有用,但出于我们的目的,完整的数据库更适合进行综合分析和集成。

  • movies_metadata.csv: 这个文件是一个包含 45,000 部电影详细信息的全面数据集,这些电影出现在完整的 MovieLens 数据集中。该文件包括各种功能,如海报、背景、预算、收入、上映日期、语言、制作国家和公司等。为了有效地组织和分析这些数据,我们将 movies_metadata.csv 文件归一化成多个 CSV 文件,每个文件代表数据集中一个相关的节点。这些节点包括流派、制作公司、制作国家和配音语言。通过将这些数据分解成单独的文件,我们可以更轻松地管理和利用数据集中包含的丰富信息。让我们看看如何操作。

    1. 开始必要的导入。

      import pandas as pd
      import ast
      # Load the CSV file
      df = pd.read_csv('./raw_data/movies_metadata.csv')  # Update the path as necessary 
      
    2. 提取并归一化流派、制作公司、国家和配音语言。我们将为流派和制作公司演示这一步骤。其余的代码可在 github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/tree/main/ch4 上找到。

      # Function to extract and normalize genres
      def extract_genres(genres_str):
          if pd.isna(genres_str) or not isinstance(
              genres_str, str
          ):
              return []
          genres_list = ast.literal_eval(genres_str)
          return [
              {'genre_id': int(g['id']), 'genre_name': g['name']}
              for g in genres_list
          ]
      # Function to extract and normalize production companies
      def extract_production_companies(companies_str):
          if pd.isna(companies_str) or not isinstance(
              companies_str, str
          ):
              return []
          companies_list = ast.literal_eval(companies_str)
          if isinstance(companies_list, list):
              return [
                  {'company_id': int(c['id']),
                      'company_name': c['name']
                  }
                  for c in companies_list
              ]
          return [] 
      
    3. 应用提取函数。

      df['genres'] = df['genres'].apply(extract_genres)
      df['production_companies'] = \
          df['production_companies'].apply(
              extract_production_companies
          )
      df['production_countries'] = \
          df['production_countries'].apply(
              extract_production_countries
          )
      df['spoken_languages'] = df['spoken_languages'].apply(
          extract_spoken_languages
      )
      # Explode lists into rows
      df_genres = df.explode('genres').dropna(subset=['genres'])
      df_companies = df.explode('production_companies').dropna(
          subset=['production_companies']
      )
      df_countries = df.explode('production_countries').dropna(
          subset=['production_countries']
      )
      df_languages = df.explode('spoken_languages').dropna(
          subset=['spoken_languages']
      ) 
      
    4. 归一化展开后的数据。让我们先对流派进行操作。

      df_genres_normalized = pd.json_normalize(df_genres['genres'])
      # Reset index to avoid duplicate indices
      df_genres_normalized = \
          df_genres_normalized.reset_index(drop=True)
      # Add the movie ID back to the normalized DataFrames as 'tmdbId'
      df_genres_normalized['tmdbId'] = df_genres.reset_index(
          drop=True
      )['id']
      # Ensure that 'company_id' and similar fields are treated as integers
      df_genres_normalized['genre_id'] = \
          df_genres_normalized['genre_id'].astype(int)
      # Save the normalized data with the updated column names
      df_genres_normalized.to_csv(
          os.path.join(output_dir, 'normalized_genres.csv'),
          index=False
      ) 
      
    5. 接下来,提取集合名称。

      # For the movies, including "Belongs to Collection" within the same CSV
      # Extract only the "name" from "belongs_to_collection" and include additional fields
      def extract_collection_name(collection_str):
          if isinstance(collection_str, str):
              try:
                  collection_dict = \
                      ast.literal_eval(collection_str)
                  if isinstance(collection_dict, dict):
                      return collection_dict.get('name', "None")
              except (ValueError, SyntaxError):  # Handle cases where string parsing fails
                  return "None"
          return "None"
      df_movies = df[
          [
              'id', 'original_title', 'adult', 'budget', 'imdb_id',
              'original_language', 'revenue', 'tagline', 'title',
              'release_date', 'runtime', 'overview',
              'belongs_to_collection'
          ]
      ].copy()
      df_movies['belongs_to_collection'] = \
          df_movies['belongs_to_collection'].apply(
              extract_collection_name
          )
      df_movies['adult'] = df_movies['adult'].apply(
          lambda x: 1 if x == 'TRUE' else 0
      )  # Convert 'adult' to integer
      # Rename 'id' to 'tmdbId'
      df_movies.rename(columns={'id': 'tmdbId'}, inplace=True)  # Rename 'id' to 'tmdbId'
      # Save the movies to a separate CSV, including the extracted fields
      df_movies.to_csv(
          './normalized_data/normalized_movies.csv', index=False
      ) 
      
  • ratings.csv: 这个文件是完整的 MovieLens 数据集,包含 2600 万条评分和 75 万个标签应用,来自 27 万名用户对数据集中所有 45,000 部电影的评分。这个全面的数据集提供了详细的用户交互数据,我们将直接使用这些数据,无需进行归一化处理。然而,对于这个用例,我们决定跳过处理 ratings.csv 文件。虽然它提供了广泛的用户交互数据,但对于我们当前的分析和目标来说并非必需。我们正在关注其他与我们的项目更直接相关的 CSV 文件。ratings.csv 中的数据对于未来需要深入挖掘用户评分和交互的项目仍然有价值,但在这个实例中不会使用。

  • ratings_small.csv: 这个文件是 ratings.csv 文件的较小子集,包含 700 名用户对 9,000 部电影的 10 万条评分。我们将使用 ratings_small.csv 而不是关注 ratings.csv 中提供的完整数据集。

通过这个过程,我们学习了如何将原始的非结构化数据转换为干净、规范化的数据集,这些数据集现在已准备好集成到您的 Neo4j 图中。这种准备为构建一个强大、高效和有效的 AI 驱动的搜索和推荐系统铺平了道路。在下一节中,我们将使用这些规范化的 CSV 文件,并通过 Cypher 代码构建知识图谱,释放我们数据集的全部潜力。

使用代码示例构建您的电影知识图谱

在本节中,我们将导入您的标准化数据集到 Neo4j,并将它们转换成完全功能的知识图谱。

设置您的 AuraDB 免费实例

要开始使用 Neo4j 构建您的知识图谱,您首先需要设置一个 AuraDB Free 实例。AuraDB Free 是一个云托管的 Neo4j 数据库,它允许您快速开始,无需担心本地安装或基础设施管理。

按照以下步骤创建您的实例:

  1. 访问 console.neo4j.io

  2. 使用您的 Google 账户或电子邮件登录。

  3. 点击创建免费实例

  4. 在实例配置过程中,将出现一个弹出窗口,显示您的数据库连接凭据。

确保从弹出窗口中下载并安全保存以下详细信息——这些信息对于将您的应用程序连接到 Neo4j 是必不可少的:

NEO4J_URI=neo4j+s://<your-instance-id>.databases.neo4j.io
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=<your-generated-password>
AURA_INSTANCEID=<your-instance-id>
AURA_INSTANCENAME=<your-instance-name> 

在您的 AuraDB Free 实例设置完成后,您现在可以导入您的标准化数据集,并开始使用 Cypher 代码构建您的知识图谱。在下一节中,我们将指导您导入数据并在您的图中构建关系。

将数据导入 AuraDB

现在,您的 AuraDB Free 实例已经启动并运行,是时候导入您的标准化数据集并构建您的知识图谱了。在本节中,我们将通过一个 Python 脚本指导您准备 CSV 文件、设置索引和约束、导入数据以及创建关系。

  1. 准备您的 CSV 文件以供导入。

  2. 确保您生成的 CSV 文件(例如,normalized_movies.csvnormalized_genres.csv 等)已准备好导入。这些文件应该是干净的、结构良好的,并且托管在可访问的 URL 上。在这种情况下,graph_build.py 脚本从公共云存储(例如,storage.googleapis.com/movies-packt/normalized_movies.csv)获取文件,因此您不需要手动上传它们到任何地方。

  3. 添加索引和约束以优化图查询检索。

在加载数据之前,创建唯一约束和索引对于确保完整性和优化查询性能至关重要。该脚本包括用于以下操作的 Cypher 命令:

  • 确保 tmdbIdmovieIdcompany_id 等 ID 的唯一性

  • actor_idcrew_iduser_id 等属性上创建索引

下面是如何创建索引和约束的说明:

"CREATE CONSTRAINT unique_tmdb_id IF NOT EXISTS FOR (m:Movie) REQUIRE m.tmdbId IS UNIQUE;",
"CREATE CONSTRAINT unique_movie_id IF NOT EXISTS FOR (m:Movie) REQUIRE m.movieId IS UNIQUE;",
"CREATE CONSTRAINT unique_prod_id IF NOT EXISTS FOR (p:ProductionCompany) REQUIRE p.company_id IS UNIQUE;",
"CREATE CONSTRAINT unique_genre_id IF NOT EXISTS FOR (g:Genre) REQUIRE g.genre_id IS UNIQUE;",
"CREATE CONSTRAINT unique_lang_id IF NOT EXISTS FOR (l:SpokenLanguage) REQUIRE l.language_code IS UNIQUE;",
"CREATE CONSTRAINT unique_country_id IF NOT EXISTS FOR (c:Country) REQUIRE c.country_code IS UNIQUE;",
"CREATE INDEX actor_id IF NOT EXISTS FOR (p:Person) ON (p.actor_id);",
"CREATE INDEX crew_id IF NOT EXISTS FOR (p:Person) ON (p.crew_id);",
"CREATE INDEX movieId IF NOT EXISTS FOR (m:Movie) ON (m.movieId);",
"CREATE INDEX user_id IF NOT EXISTS FOR (p:Person) ON (p.user_id);" 
  1. 导入数据并创建节点。

在添加约束和索引后,脚本从各自的 CSV 文件中加载节点:

  • load_movies() 添加所有电影元数据。

  • load_genres()load_production_companies()load_countries() 等创建相关的节点,例如 GenreProductionCompanyCountrySpokenLanguage

  • 使用 load_person_actors()load_person_crew() 添加与人物相关的数据。

通过 load_links()load_keywords()load_ratings() 添加额外的属性。

以下是一个示例:

graph.load_movies('https://storage.googleapis.com/movies-packt/normalized_movies.csv', movie_limit) 
  1. 创建关系。

随着每个加载函数的运行,它不仅创建节点,还建立有意义的关联:

  • HAS_GENREMovieGenre 之间。

  • PRODUCED_BYMovieProductionCompany 之间。

  • HAS_LANGUAGEMovieSpokenLanguage 之间,PRODUCED_INMovieCountry 之间,ACTED_INDIRECTEDPRODUCEDMoviePerson 之间,以及 RATEDMovieUser 之间,等等。

  1. 运行完整脚本。

在运行脚本之前,请确保您已安装 Neo4j Python 驱动程序。您可以使用 pip 安装它。

 pip install neo4j 

要运行整个图构建过程,只需执行以下操作:

python graph_build.py 

此脚本按以下顺序执行以下操作:

  • 使用 .env 文件中的凭据连接到您的 AuraDB 实例。

  • 清理数据库。

  • 添加索引和约束。

  • 使用托管 CSV 文件批量加载所有节点数据和关系。

请参阅此处提供的完整脚本:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch4/graph_build.py

完成后,使用 Neo4j 浏览器验证您的导入:

MATCH (m:Movie)-[:HAS_GENRE]->(g:Genre)
RETURN m.title, g.genre_name
LIMIT 10; 

图 4.1 展示了一个包含超过 90K 个节点和 320K+ 关联的连接电影图。例如 MovieGenrePersonProductionCompany 这样的节点用不同的颜色表示,而例如 ACTED_INHAS_GENREPRODUCED_BY 这样的关系展示了相互关联的元数据网络。

图 4.1 — 电影数据集的 Neo4j 图

图 4.1 — 电影数据集的 Neo4j 图。

使用 Python 和 Cypher 成功导入数据并构建完知识图谱后,您现在可以开始构建一个由 GenAI 驱动的搜索应用了。在下一章中,我们将深入探讨高级 Cypher 技术,这些技术能帮助您处理复杂的关系并从数据中获得更深入的见解。

除此之外:复杂图结构的 Cypher 高级技术。

随着您的知识图谱在规模和复杂性上的增长,对您的查询和数据管理能力的需求也在增加。Cypher,Neo4j 强大的查询语言,提供了一系列高级功能,旨在处理复杂的图结构并实现更复杂的数据分析。在本节中,我们将探索这些高级 Cypher 技术,包括路径模式可变长度关系、子查询和图算法。理解这些技术将帮助您有效地管理复杂的关系,进行更深入的分析,并释放您的知识图谱在高级用例中的全部潜力。

让我们探索这些关键的 Cypher 高级技术:

  • 可变长度的关系:Cypher 中的可变长度关系允许您在节点之间匹配不同长度的路径。这在探索层次结构或具有多个分离度的网络时特别有用。例如,找到与特定演员在三个分离度范围内的所有电影:

    MATCH (a:Actor {name: 'Tom Hanks'})-[:ACTED_IN*1..3]-(m:Movie)
    RETURN DISTINCT m.title; 
    
    • 在这里,*1..3指定了关系路径的长度可以在 1 到 3 步之间。

    • 用例:可变长度关系非常适合社交网络分析等场景,您想要找到在特定连接度内的人,或者在具有多级父子关系的层次数据集中探索父子关系。

  • 使用路径模式进行模式匹配:您可以在 Neo4j 中创建命名路径模式以及链式路径。

    • 定义路径模式:Cypher 允许您定义可重用的命名路径模式,这些模式可以在查询中多次使用。这使得您的查询更易于阅读,并允许您将复杂的关系封装在单个模式中。以下是一个示例:

      MATCH path = (a:Actor)-[:ACTED_IN]->(m:Movie)
      RETURN path; 
      

    在这里,path是一个命名路径模式,可以在后续操作或子查询中重复使用。

    • 链式路径模式:Cypher 允许您组合多个路径模式,在图内执行复杂的遍历。这在试图揭示间接关系或发现满足特定标准的多条路径时特别有用。

    一个例子是探索电影数据集中的合作情况。

    假设我们想要找到演员与导演合作过的电影,而这些导演之前可能通过另一部电影与他们合作过。这涉及到从演员到电影,再到导演的路径链,并查看是否存在另一部电影连接相同的演员-导演对:

    MATCH (a:Actor {name: "Tom Hanks"})-[:ACTED_IN]->(m1:Movie)<-[:DIRECTED_BY]-(d:Director) MATCH (a)-[:ACTED_IN]->(m2:Movie)<-[:DIRECTED_BY]-(d)
    WHERE m1 <> m2
    RETURN a.name AS actor, d.name AS director, collect(DISTINCT m1.title) + collect(DISTINCT m2.title) AS movies 
    

    这种模式链在识别专业关系、重复合作或分析网络中的间接影响方面非常有帮助。

  • 子查询过程逻辑:您可以使用子查询和过程来处理复杂查询。以下是操作方法:

    • 使用子查询进行模块化查询:Cypher 中的子查询允许您将复杂的查询分解成模块化、可重用的组件。这在处理大型图或需要对同一数据集执行多个操作时特别有用。以下是一个示例:

      CALL {
        MATCH (m:Movie)-[:HAS_GENRE]->(g:Genre {name: 'Action'})
        RETURN m
      }
      MATCH (m)-[:DIRECTED_BY]->(d:Director)
      RETURN d.name, COUNT(m) AS action_movies_directed; 
      

    在这里,子查询检索所有动作电影,而外部查询将这些电影与它们的导演匹配。

    • 使用 CALL 执行过程逻辑:Cypher 中的 CALL 子句允许您调用过程并在后续查询中使用这些结果。这对于高级数据处理至关重要,例如运行图算法或调用自定义过程。

    我们已经在 graph_build.py 文件中的实现中应用了这种方法,特别是在 load_ratings() 函数中。在这里,我们使用 CALL { ... } IN TRANSACTIONS 模式,通过以 50,000 行的块来处理数据,有效地加载大量数据集:

    LOAD CSV WITH HEADERS FROM $csvFile AS row
    CALL (row) {
      MATCH (m:Movie {movieId: toInteger(row.movieId)})
      WITH m, row
      MERGE (p:Person {user_id: toInteger(row.userId)})
      ON CREATE SET p.role = 'user'
      MERGE (p)-[r:RATED]->(m)
      ON CREATE SET r.rating = toFloat(row.rating), r.timestamp = toInteger(row.timestamp)
    } IN TRANSACTIONS OF 50000 ROWS; 
    

    这种方法使我们能够在保持性能和事务完整性的同时处理大量的 CSV 导入——这是 CALL 在现实世界图应用中的许多强大用例之一。

  • 处理嵌套查询:在复杂的图结构中,您可能需要组合多个查询的结果。Cypher 允许您嵌套查询,将一个查询的结果传递给另一个查询,这对于基于多个标准过滤或细化结果非常有用。以下是一个示例:

    MATCH (m:Movie)
    WHERE m.revenue > 100000000
    CALL {
      WITH m
      MATCH (m)-[:HAS_GENRE]->(g:Genre)
      RETURN g.name AS genre
    }
    RETURN m.title, genre; 
    

在这里,嵌套查询通过根据收入过滤电影来细化结果,然后找到它们相关的类型。

这些 Cypher 技巧使您能够应对复杂的图结构,实现更深入的洞察和更复杂的分析。您可以参考neo4j.com/docs/cypher-manual/current/appendix/tutorials/advanced-query-tuning/以进一步探索这些技巧。

摘要

在本章中,我们致力于将原始的半结构化数据转换为干净、规范化的数据集,以便将其集成到我们的知识图谱中。然后,我们探讨了图建模的最佳实践,重点关注如何构建节点和关系以增强搜索效率,并确保您的图保持可扩展和高效。在此之后,我们探讨了其他 Cypher 技巧,为您提供处理可变长度关系、模式匹配、子查询和图算法的技能。您现在已准备好构建一个由知识图谱驱动的搜索,它可以处理甚至最复杂的数据关系。

在下一章中,我们将进一步探索如何将 Haystack 集成到 Neo4j 中。这本实用指南将向您展示如何在您的知识图谱中构建强大的搜索功能,让您能够充分利用 Neo4j 和 Haystack 的全部潜力,以实现智能搜索解决方案。

第五章:使用 Neo4j 和 Haystack 实现强大的搜索功能

在本章中,我们开始将 Haystack 与 Neo4j 集成,结合 LLM 和图数据库的能力来构建一个由 AI 驱动的搜索系统。Haystack 是一个开源框架,使开发者能够通过利用现代 NLP 技术、机器学习模型和基于图的数据来创建 AI 驱动的应用程序。对于我们的智能搜索,Haystack 将作为一个统一的平台来协调 LLM、搜索引擎和数据库,提供高度上下文化和相关的搜索结果。

在前一章的工作基础上——我们清理并结构化 Neo4j 数据——我们将首先使用 OpenAI 的 GPT 模型生成嵌入。这些嵌入将丰富图结构,使其更强大,并能够处理细微的、上下文感知的搜索查询。Haystack 将作为 OpenAI 模型和 Neo4j 图数据库之间的桥梁,使我们能够结合两者的优势。

在本章中,您将学习如何设置和配置 Haystack 以实现与 Neo4j 的无缝集成。我们将引导您构建强大的搜索功能,并最终使用 Hugging Face Spaces 上的 Gradio 部署这个完全功能化的搜索系统。

在本章中,我们将涵盖以下主要主题:

  • 使用 Haystack 生成嵌入以增强您的 Neo4j 图

  • 将 Haystack 连接到 Neo4j 进行高级向量搜索

  • 构建强大的搜索体验

  • 微调您的 Haystack 集成

技术要求

要成功实现 Haystack 和 Neo4j 的集成,并构建一个由 AI 驱动的搜索系统,您需要确保您的环境已正确设置。以下是本章的技术要求列表:

  • Python:您需要在您的系统上安装 Python 3.11。Python 用于脚本编写和与 Neo4j 数据库交互。您可以从官方 Python 网站下载 Python:www.python.org/downloads/

  • Neo4j AuraDB 或本地 Neo4j 实例:您需要访问一个 Neo4j 数据库来存储和查询您的图数据。这可以是本地安装的 Neo4j 实例或云托管的 Neo4j AuraDB 实例。如果您正在跟随前一章的内容,其中我们讨论了 graph_build.py 脚本 (github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch4/graph_build.py),您可以继续使用已设置并填充数据的相同 Neo4j 实例。这确保了连续性,并允许您在已导入的结构化数据之上构建。

  • Cypher 查询语言:熟悉 Cypher 查询语言是必要的,因为我们将在创建和查询图时广泛使用 Cypher。你可以在 Cypher 查询语言文档中了解更多关于 Cypher 语法的细节:neo4j.com/docs/cypher/

  • Neo4j Python 驱动程序:安装 Neo4j Python 驱动程序以使用 Python 连接到 Neo4j 数据库。你可以通过pip安装它:

    pip install neo4j 
    
  • Haystack:我们将使用 Haystack v2.5.0。

使用 pip 安装 Haystack:

pip install haystack-ai 
  • OpenAI API 密钥:要成功使用基于 GPT 的模型生成嵌入,你需要一个 OpenAI API 密钥。

如果你还没有账户,请在 OpenAI(platform.openai.com/signup)注册以获取 API 密钥。

注意

免费层 API 密钥在本项目的多数用例中都不会工作。你需要一个活跃的付费 OpenAI 订阅才能访问必要的端点和使用限制。

登录后,导航到你的 OpenAI 仪表板中的API 密钥部分(platform.openai.com/api-keys)并生成一个新的 API 密钥。

你还需要使用 pip 安装 OpenAI 包。在你的终端中运行以下命令:

pip install openai 
  • Gradio:我们将使用 Gradio 创建一个用户友好的聊天机器人界面。使用 pip 安装 Gradio:

    pip install gradio 
    
  • Hugging Face 账户:要将你的聊天机器人托管在 Hugging Face Spaces 上,你需要一个 Hugging Face 账户。如果你还没有账户,请在 Hugging Face 网站上注册:huggingface.co/

  • Google Cloud Storage(可选):如果你将 CSV 文件存储在 Google Cloud Storage 上,请确保在脚本中正确配置了文件路径。

  • python-dotenv 包:确保安装python-dotenv包以管理项目中的环境变量:

    pip install python-dotenv 
    

本章的所有代码都可在以下 GitHub 仓库中找到:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs

在此仓库中,导航到名为ch6的文件夹以访问与本章相关的代码示例和资源。此文件夹包含实现 Neo4j 和 Haystack 集成以及使用电影数据集构建 AI 驱动的搜索系统所需的所有必要脚本、文件和配置。

确保克隆或下载仓库,这样你就可以跟随本章中的代码示例进行操作。

使用 Haystack 生成嵌入以增强你的 Neo4j 图

在本节中,我们将专注于生成上一章中添加到我们的 Neo4j 图中的电影剧情的嵌入。嵌入是现代搜索系统的一个关键部分,因为它们将文本转换为高维向量,从而实现相似度搜索。这使得搜索引擎能够理解单词和短语之间的上下文关系,提高搜索结果的准确性和相关性。

我们将集成 Haystack 与 OpenAI 的基于 GPT 的模型以生成这些嵌入,并将它们存储在你的 Neo4j 图中。这将启用更准确和上下文感知的搜索功能。

初始化 Haystack 和 OpenAI 以生成嵌入

在生成嵌入之前,你需要确保 Haystack 已设置并集成到 OpenAI 的 API 中,以从其基于 GPT 的模型中检索嵌入。按照以下步骤设置 Haystack:

  1. 通过以下命令安装所需的库(如果你还没有安装):

    pip install haystack haystack-ai openai neo4j-haystack 
    
  2. 接下来,配置你的 OpenAI API 密钥,并确保它在你的.env文件中设置:

    makefile
    OPENAI_API_KEY=your_openai_api_key_here 
    
  3. 通过创建一个初始化 Haystack 并连接到 OpenAI 以生成嵌入的 Python 脚本来初始化 Haystack 使用 OpenAI 嵌入:

    # Initialize Haystack with OpenAI for text embeddings
    def initialize_haystack():
        # Initialize document store (In-memory for now, but you can configure other stores)
        document_store = InMemoryDocumentStore()
        # Initialize OpenAITextEmbedder to generate text embeddings
        embedder = OpenAITextEmbedder(
            api_key=Secret.from_env_var("OPENAI_API_KEY"),
            model="text-embedding-ada-002"
        )
        return embedder 
    

此配置初始化 Haystack 使用内存中的文档存储,并使用 OpenAI 嵌入设置检索器。

为电影剧情生成嵌入

接下来,我们将为存储在 Neo4j 图中的电影剧情生成嵌入。目标是检索剧情描述,为它们生成嵌入,并将这些嵌入链接回相应的电影节点:

  1. 从 Neo4j 查询电影剧情:首先,你需要从 Neo4j 查询电影剧情。使用以下 Cypher 查询检索电影标题和剧情摘要:

    # Retrieve movie plots and titles from Neo4j
    def retrieve_movie_plots():
        # The query retrieves the "title", "overview", and "tmdbId" properties of each Movie node
        query = """
        MATCH (m:Movie)
        WHERE m.embedding IS NULL
        RETURN m.tmdbId AS tmdbId, m.title AS title, m.overview AS overview
        """
        with driver.session() as session:
            results = session.run(query)
            # Each movie's title, plot (overview), and ID are retrieved and stored in the movies list
            movies = [
                {
                    "tmdbId": row["tmdbId"],
                    "title": row["title"],
                    "overview": row["overview"]
                }
                for row in results
            ]
        return movies 
    

这将返回图中的每个电影的tmdbId值和概述(即剧情摘要)。

  1. 使用 OpenAI 和 Haystack 生成嵌入:一旦检索到剧情摘要,就可以使用 Haystack 的OpenAITextEmbedder生成嵌入:

    #Parallel embedding generation with ThreadPoolExecutor
    def generate_and_store_embeddings(embedder, movies, max_workers=10): 
        results_to_store = []
        def process_movie(movie):
            title = movie.get("title", "Unknown Title")
            overview = str(movie.get("overview", "")).strip()
            tmdbId = movie.get("tmdbId")
            if not overview:
                print(f"Skipping {title} — No overview available.")
                return None
            try:
                print(f"Generating embedding for: {title}")
                embedding_result = embedder.run(overview)
                embedding = embedding_result.get("embedding")
                if embedding:
                    return (tmdbId, embedding)
                else:
                    print(f"No embedding generated for: {title}")
            except Exception as e:
                print(f"Error processing {title}: {e}")
            return None 
    
  2. 在 Neo4j 中存储嵌入:生成嵌入后,下一步是将它们存储在你的 Neo4j 图中。每个电影节点都将更新一个属性,以存储其嵌入:

    # Store the embeddings back in Neo4j
    def store_embedding_in_neo4j(tmdbId, embedding):
        query = """
        MATCH (m:Movie {tmdbId: $tmdbId})
        SET m.embedding = $embedding
        """
        with driver.session() as session:
            session.run(query, tmdbId=tmdbId, embedding=embedding)
        print(f"Stored embedding for TMDB ID: {tmdbId}") 
    

这将在 Neo4j 图中的每个Movie节点中存储名为embedding的属性。

  1. 验证 Neo4j 中的嵌入存储:一旦嵌入存储,你可以通过查询几个节点来检查embedding属性以验证它们的存在:

    # Verify embeddings stored in Neo4j
    def verify_embeddings():
        query = """
        MATCH (m:Movie)
        WHERE exists(m.embedding)
        RETURN m.title, m.embedding
        LIMIT 10
        """
        with driver.session() as session:
            results = session.run(query)
            for record in results:
                title = record["title"]
                embedding = np.array(record["embedding"])[:5]
                print(f" {title}: {embedding}...") 
    

此查询将返回一些电影的标题和嵌入,以便你可以验证嵌入是否已成功存储。

注意

这些只是代码片段。完整版本可在 GitHub 仓库中找到:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch5/generate_embeddings.py

我们现在已经用这些嵌入丰富了我们的图,从而添加了相似性搜索,这将使我们能够执行更具有上下文意识和智能的查询。这一步对于增强搜索体验和基于文本意义的先进检索操作至关重要,而不是简单的关键词匹配。

现在我们已经用向量嵌入丰富了我们的 Neo4j 图,下一步是将 Haystack 连接到 Neo4j 以进行高级向量搜索。在接下来的章节中,我们将重点介绍如何使用这些嵌入在 Neo4j 中执行高效且准确的向量搜索,使我们能够根据它们的向量相似性检索电影或节点。

将 Haystack 连接到 Neo4j 以进行高级向量搜索

现在电影嵌入已存储在 Neo4j 中,我们需要在 embedding 属性上配置一个向量索引,这将使我们能够根据它们的向量相似性高效地搜索电影。通过在 Neo4j 中创建向量索引,我们能够快速检索在高维嵌入空间中彼此接近的节点,这使得执行复杂的查询成为可能,例如找到具有相似剧情摘要的电影。

一旦创建了向量索引,它将与 Haystack 集成以从 Neo4j 执行基于向量的检索。此搜索将基于向量相似性机制,如余弦相似性。

在 Neo4j 中创建向量搜索索引

您首先需要删除嵌入属性上的任何现有向量索引(如果存在),然后创建一个新的索引以执行向量搜索。这是您如何在 Python 脚本中使用 Cypher 查询来完成此操作的示例:

def create_or_reset_vector_index():
    with driver.session() as session:
        try:
            # Drop the existing vector index if it exists
            session.run("DROP INDEX overview_embeddings IF EXISTS ")
            print("Old index dropped")
        except:
            print("No index to drop")
        # Create a new vector index on the embedding property
        print("Creating new vector index")
        query_index = """
        CREATE VECTOR INDEX overview_embeddings IF NOT EXISTS
        FOR (m:Movie) ON (m.embedding)
        OPTIONS {indexConfig: {
            `vector.dimensions`: 1536,
            `vector.similarity_function`: 'cosine'}}
        """
        session.run(query_index)
        print("Vector index created successfully") 

使用 Haystack 和 Neo4j 向量索引执行相似性搜索

在 Neo4j 图上创建向量索引后,您可以利用 Haystack 执行基于电影剧情嵌入的相似性搜索查询。这种方法允许您比较给定电影剧情或任何文本查询与现有电影概述之间的相似性,根据它们的嵌入返回最相关的结果。在这个示例中,我们使用 Haystack 库中的 OpenAITextEmbedder 模型将文本查询转换为嵌入,然后使用它来搜索具有相似剧情的 Neo4j 图中的电影。

这就是您生成查询嵌入并执行相似性搜索的方法:

text_embedder = OpenAITextEmbedder(
        api_key=Secret.from_env_var("OPENAI_API_KEY"),
        model="text-embedding-ada-002"
    )
    # Step 1: Create embedding for the query
    query_embedding = text_embedder.run(query).get("embedding")

    if query_embedding is None:
        print("Query embedding not created successfully.")
        return

    print("Query embedding created successfully.") 

使用 Haystack 和 Neo4j 运行向量搜索查询

一旦创建了向量索引并将嵌入存储在 Neo4j 中,您就可以通过传递查询或样本电影剧情来执行基于向量的搜索。系统将为查询生成一个嵌入,将其与存储在 Neo4j 中的嵌入进行比较,并返回最相关的结果。

这里是一个使用 Haystack 进行向量搜索的示例,它显示了最相似的电影剧情,而不使用 Cypher:

# Step 2: Search for similar documents using the query embedding
    similar_documents = document_store.query_by_embedding(
        query_embedding, top_k=3
    )
    if not similar_documents:
        print("No similar documents found.")
        return
    print(f"Found {len(similar_documents)} similar documents.")
    print("\n\n")
    # Step 3: Displaying results
    for doc in similar_documents:
        title = doc.meta.get("title", "N/A")
        overview = doc.meta.get("overview", "N/A")
        score = doc.score
        print(
             f"Title: {title}\nOverview: {overview}\n"
             f"Score: {score:.2f}\n{'-'*40}"
        )
    print("\n\n") 

现在,我们将集成 Neo4j Cypher 查询与 Haystack 以运行向量搜索,从而实现类似剧情的检索。

使用 Cypher 和 Haystack 运行向量搜索查询

要运行向量搜索,我们将使用 Cypher 的图查询功能,同时使用由OpenAITextEmbedder生成的向量嵌入进行相似度搜索。

与直接使用 Haystack 查询向量索引不同,这种方法结合了 Cypher 的灵活性,可以返回更复杂的数据,例如电影元数据(例如,演员和类型),同时仍然保持向量相似度搜索的效率。

这里涉及到的步骤如下:

  1. 使用 OpenAITextEmbedder 嵌入查询:将用户的文本查询(例如,电影剧情)转换为高维向量嵌入。

  2. 使用 Neo4j 和 Cypher 进行搜索:使用 Cypher 通过比较查询嵌入与存储在 Neo4j 向量索引中的电影剧情嵌入来检索相似电影。

  3. 返回丰富数据:为每个结果检索额外的电影信息,例如标题、概述、演员、类型和评分(相似度)。

这就是实现向量搜索的方法:

  1. 定义 Cypher 查询:我们首先定义一个 Cypher 查询,该查询搜索 Neo4j 向量索引(overview_embeddings),以检索基于查询嵌入和电影嵌入之间的余弦相似度的top_k最相似的电影:

    cypher_query = """
        CALL db.index.vector.queryNodes("overview_embeddings", $top_k, $query_embedding)
        YIELD node AS movie, score
        MATCH (movie:Movie)
        RETURN movie.title AS title, movie.overview AS overview, score
    """ 
    
  2. 生成查询嵌入:使用OpenAITextEmbedder,我们将用户的输入查询(例如,电影剧情)转换为嵌入。此嵌入将被传递到 Neo4j 向量索引,以便与存储的电影嵌入进行比较:

    text_embedder = OpenAITextEmbedder(
        api_key= Secret.from_env_var("OPENAI_API_KEY"),
        model="text-embedding-ada-002"
    ) 
    
  3. 使用 Haystack 管道运行向量搜索:我们设置 Haystack 管道来管理 Haystack 组件:

    • query_embedder从用户查询生成嵌入

    • retriever在 Neo4j 上使用查询嵌入运行 Cypher 查询,并返回最相似的电影:

      retriever = Neo4jDynamicDocumentRetriever(
          client_config=client_config,
          runtime_parameters=["query_embedding"],
          compose_doc_from_result=True,
          verify_connectivity=True,
      )
      pipeline = Pipeline()
      pipeline.add_component("query_embedder", text_embedder)
      pipeline.add_component("retriever", retriever)
      pipeline.connect(
          "query_embedder.embedding", "retriever.query_embedding"
      )
      result = pipeline.run(
          {
              "query_embedder": {"text": query},
              "retriever": {
                  "query": cypher_query,
                  "parameters": {
                      "index": "overview_embeddings", "top_k": 3
                  },
              },
          }
      ) 
      
  4. 显示结果:一旦搜索完成,我们从 Neo4j 图中提取结果,并显示电影标题、概述和相似度分数:

    # Extracting documents from the retriever results
    documents = result["retriever"]["documents"]
    for doc in documents:
        # Extract title and overview from document metadata
        title = doc.meta.get("title", "N/A")
        overview = doc.meta.get("overview", "N/A")
        # Extract score from the document
        score = getattr(doc, "score", None)
        score_display = f"{score:.2f}" if score is not None else "N/A"
        # Print the title, overview, and score (or N/A for missing score)
        print(
             f"Title: {title}\nOverview: {overview}\n"
             f"Score: {score_display}\n{'-'*40}\n"
        ) 
    

使用 Cypher 和 Haystack 提供了以下好处:

  • Cypher 的灵活性:通过结合 Cypher 和 Haystack,我们不仅可以查询嵌入,还可以检索基于图的其他信息,例如演员、类型和实体之间的关系。

  • 丰富结果:除了检索最相似的电影外,您还可以轻松扩展查询以检索相关元数据(例如,演员、类型、评分)或使用额外的过滤条件(例如,上映年份、类型)来细化搜索。

  • 针对大型图优化:Neo4j 的向量索引允许高效查询具有复杂关系的大型数据集,而 Haystack 的嵌入模型提供了对电影剧情的准确理解。

让我们看看下一个示例用例。

示例用例

考虑寻找剧情类似于一个英雄必须拯救世界免于毁灭的电影。通过使用我们刚刚创建的管道,您可以检索相关结果:

Title: The Matrix
Overview: A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.
Score: 0.98
----------------------------------------
Title: Inception
Overview: A thief who steals corporate secrets through dream-sharing technology is given the inverse task of planting an idea into the mind of a CEO.
Score: 0.96
----------------------------------------
Title: The Dark Knight
Overview: Batman raises the stakes in his war on crime, with the help of Lieutenant Jim Gordon and District Attorney Harvey Dent.
Score: 0.94
---------------------------------------- 

此管道结合了两者之长——通过向量嵌入进行相似度搜索和通过 Cypher 进行图查询的丰富数据功能——允许在大型数据集(如电影)上进行强大且灵活的搜索。

注意

这些只是代码片段。完整版本可在 GitHub 仓库中找到:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch5/vector_search.py

我们现在已经将 Haystack 连接到 Neo4j 并启用了高级向量搜索功能。有了向量索引,Neo4j 现在可以高效地根据嵌入相似度搜索类似的电影节点。Haystack 的集成允许您无缝地使用 Neo4jDynamicDocumentRetriever 执行这些搜索。此检索器通过利用向量嵌入和 Neo4j 的图功能在您的图中搜索类似项。

在下一节中,我们将探讨如何构建一个利用 Haystack 和 Neo4j 的强大功能来提供丰富、上下文感知响应的搜索驱动聊天机器人。使用 Gradio,我们将创建一个直观的聊天机器人界面,可以与用户交互并通过自然语言查询执行高级搜索。这将结合 LLMs、向量搜索和 Neo4j 的优势,创建一个用户友好、AI 驱动的搜索体验。

使用 Gradio 和 Haystack 构建 search-driven 聊天机器人

在本节中,我们将集成 Gradio 来构建一个由 Haystack 和 Neo4j 驱动的交互式聊天机器人界面。Gradio 使得创建一个用于与聊天机器人交互的基于网页的界面变得简单。聊天机器人将允许用户输入查询,然后触发对存储在 Neo4j 中的电影嵌入的基于向量的搜索。聊天机器人将返回详细的响应,包括电影标题、概述和相似度分数,提供信息丰富且用户友好的体验。

设置 Gradio 界面

如果您尚未安装 Gradio,请通过运行以下命令进行安装:

pip install gradio 

注意

本章中的脚本与 Gradio v 5.23.1 兼容。

接下来,我们将设置一个基本的 Gradio 界面,该界面触发我们的搜索管道并显示结果:

import gradio as gr
# Define the Gradio chatbot interface
def chatbot(user_input):
    return perform_vector_search_cypher(user_input)
# Create Gradio interface
chat_interface = gr.Interface(
    fn=chatbot,
    inputs=gr.Textbox(
        placeholder="What kind of movie would you like to watch?",
        lines=3,
        label="Your movie preference"
    ),
    outputs=gr.Textbox(
        label="Recommendations",
        lines=12
    ),
    title="AI Movie Recommendation System",
    description="Ask me about movies! I can recommend movies based on your preferences.",
    examples=[
        ["I want to watch a sci-fi movie with time travel"],
        ["Recommend me a romantic comedy with a happy ending"],
        ["I'm in the mood for something with superheroes but not too serious"],
        ["I want a thriller that keeps me on the edge of my seat"],
        ["Show me movies about artificial intelligence taking over the world"]
    ],
    flagging_mode="never" 

此界面允许用户输入文本查询,聊天机器人将使用 perform_vector_search_cypher() 函数搜索最相关的电影。

与 Haystack 和 Neo4j 集成

为了为聊天机器人提供动力,我们将将其连接到 Haystack 的嵌入生成和 Neo4j 的向量搜索功能。我们将使用 OpenAITextEmbedder 为查询和存储在 Neo4j 中的电影情节生成嵌入。电影嵌入存储在 Neo4j 内部的向量索引中,我们将查询最相似的电影。

这就是如何将我们的聊天机器人与之前的 Haystack 设置集成:

# Conversational chatbot handler using Cypher-powered search and Haystack
def perform_vector_search(query):
    print("MESSAGES RECEIVED:", user_input)
    cypher_query = """
        CALL db.index.vector.queryNodes("overview_embeddings", $top_k, $query_embedding)
        YIELD node AS movie, score
        MATCH (movie:Movie)
        RETURN movie.title AS title, movie.overview AS overview, score
    """
    # Embedder
    embedder = OpenAITextEmbedder(
        api_key=Secret.from_env_var("OPENAI_API_KEY"),
        model="text-embedding-ada-002"
    )
    # Retriever
    retriever = Neo4jDynamicDocumentRetriever(
        client_config=client_config,
        runtime_parameters=["query_embedding"],
        compose_doc_from_result=True,
        verify_connectivity=True,
    )
    # Pipeline
    pipeline = Pipeline()
    pipeline.add_component("query_embedder", embedder)
    pipeline.add_component("retriever", retriever)
    pipeline.connect(
        "query_embedder.embedding", "retriever.query_embedding"
    ) 

将 Gradio 连接到完整管道

现在,将这个 Gradio 聊天机器人连接到您已经设置的 Haystack 和 Neo4j 管道。Gradio 接口将调用 perform_vector_search_cypher() 函数,该函数反过来利用 Neo4jDynamicDocumentRetriever 根据用户的查询搜索类似的电影。

更新 main() 函数以初始化聊天机器人:

# Main function to orchestrate the entire process
def main():
    # Step 1: Create or reset vector index in Neo4j AuraDB
    create_or_reset_vector_index()
    # Step 2: Launch Gradio chatbot interface
    chat_interface.launch()
if __name__ == "__main__":
    main() 

运行聊天机器人

要运行聊天机器人,只需执行您的 Python 脚本。Gradio 接口将在您的浏览器中启动,让您能够实时与聊天机器人互动:

python search_chatbot.py 

在您的浏览器中将会启动一个 Gradio 接口,让您能够实时与聊天机器人互动。您可以输入如下查询:

"Tell me about a hero who saves the world." 

聊天机器人将根据向量搜索返回与该查询相似的剧情。

注意

这些只是代码片段。完整版本可在 GitHub 仓库中找到:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch5/search_chatbot.py

当我们接近本节的结尾时,我们已经使用 Gradio、Haystack 和 Neo4j 构建了一个功能齐全的搜索驱动聊天机器人。该聊天机器人利用存储在 Neo4j 中的嵌入来执行高级基于向量的搜索,通过从 Neo4j 中检索有意义的电影标题和演员来以用户查询的形式向用户返回上下文相关的结果。

然而,这仅仅是开始。在下一节中,我们将更深入地探讨如何微调您的 Haystack 集成,并探索高级技术,例如优化搜索性能、调整检索模型以及改进聊天机器人的响应,以创建一个更加无缝和高效的搜索驱动体验。

微调您的 Haystack 集成

现在是时候探索如何微调此集成以提升性能和用户体验了。虽然当前的设置提供了丰富且上下文感知的响应,但您还可以实施一些高级技术来优化搜索过程、提高检索准确性,并使聊天机器人的交互更加流畅。

在本节中,我们将专注于调整 Haystack 的关键组件,包括尝试不同的嵌入模型、优化 Neo4j 查询以获得更快的速度,以及改进聊天机器人显示其响应的方式。这些改进将帮助您扩展聊天机器人以处理更复杂的查询,提高响应时间,并呈现更加相关的搜索结果。

尝试不同的嵌入模型

目前,我们正在使用 OpenAI 的 text-embedding-ada-002 模型来生成嵌入。虽然这个模型自发布以来一直作为各种任务的可靠和高效选择,但值得注意的是,OpenAI 最近推出了新的模型——例如 text-embedding-3-smalltext-embedding-3-large——它们在性能和成本效益方面都取得了显著改进。例如,text-embedding-3-small 在多语言和英语任务中实现了更好的结果,同时比 text-embedding-ada-002 至少节省五倍的成本。尽管我们在这个项目中没有切换模型以保持一致性,但正在实施类似管道的读者可以考虑使用 text-embedding-3-small 来提高效率,同时不牺牲性能——特别是如果嵌入生成是频繁或大规模操作的话。

然而,Haystack 支持各种其他模型,并且你可以尝试不同的模型以查看哪个为你特定的用例提供了最准确或最相关的结果。例如,你可以切换到一个更复杂的 OpenAI 模型,具有更高的维度,或者尝试 Haystack 支持的另一个嵌入服务。

这就是你可以轻松切换到不同模型的方法:

embedder = OpenAITextEmbedder(
    api_key=Secret.from_env_var("OPENAI_API_KEY"),
    model="text-embedding-babbage-001"  # Experiment with different models
) 

你还可以探索 OpenAI 的其他模型,甚至集成不同的嵌入服务,以查看哪个对你的电影聊天机器人表现最佳。

优化 Neo4j 以实现更快的查询

虽然 Neo4j 已经在处理基于图查询方面非常高效,但你还可以应用一些优化,特别是对于大型数据集。你可以索引额外的属性以提高查询性能。

索引额外属性

除了嵌入属性上的向量索引之外,你还可以索引其他频繁查询的属性,例如 titletmdbId,以加快检索速度。这将确保每次你根据这些属性过滤或检索电影时,搜索都更快、更高效:

def create_additional_indexes():
    with driver.session() as session:
        session.run("CREATE INDEX IF NOT EXISTS movie_title_index FOR (m:Movie) ON (m.title)")
        session.run("CREATE INDEX IF NOT EXISTS movie_tmdbId_index FOR (m:Movie) ON (m.tmdbId)")
        print("Additional indexes created successfully") 

通过索引这些属性,你可以在搜索不仅基于嵌入时优化查找,例如在按标题过滤或检索特定电影时。

为了持续改进聊天机器人的搜索体验,你可以记录用户查询并随着时间的推移进行分析。让我们详细谈谈这一点。

记录和分析查询

记录可以帮助你跟踪最常见的搜索模式。基于用户查询的日志及其分析,你可以调整索引策略,优化检索器,或调整嵌入模型以获得更好的准确性。

这就是实现简单日志记录机制的方法:

import logging
logging.basicConfig(filename='chatbot_queries.log', level=logging.INFO)
def log_query(query):
    logging.info(f"User query: {query}") 

每当用户输入一个查询时,它将被记录以供将来分析。然后你可以分析这些日志,对系统进行有根据的调整,确保它随着时间的推移变得更加响应和准确。

这些技术可以帮助您显著提升搜索驱动的聊天机器人的性能、准确性和用户体验。无论是尝试不同的嵌入模型、优化 Neo4j 查询,还是改进结果格式,每一次调整都让您更接近无缝且强大的用户交互。

这些高级技术使您的聊天机器人能够有效扩展,处理更复杂的查询,并返回更加相关和吸引人的结果。

摘要

在本章中,我们通过整合 Gradio、Haystack 和 Neo4j 成功构建了一个功能齐全的搜索驱动的聊天机器人。我们首先通过 OpenAI 的模型生成的电影嵌入丰富了我们的 Neo4j 图,从而实现了高级的基于向量的搜索功能。从那里,我们将 Haystack 连接到 Neo4j,使我们能够在图中存储的嵌入上执行相似度搜索。最后,我们通过创建一个用户友好的聊天机器人界面(使用 Gradio),根据用户查询动态检索电影详情,如标题和演员,来完成整个构建过程。

在下一章中,我们将重点关注 Haystack 的高级搜索能力和搜索优化。我们还将讨论大型图的查询优化。

第六章:使用 Neo4j 探索高级知识图谱功能

通过在前一章建立的基础知识,其中我们介绍了基本的搜索功能,我们现在将探索更复杂的知识探索、图推理和性能优化技术。在本章中,我们将利用 Neo4j 的高级功能,重点关注将这些功能与 Haystack 集成,以创建一个更智能、AI 驱动的搜索系统。

到本章结束时,您将能够从您的知识图谱中解锁更深入的见解,利用高级搜索功能,并确保您的 AI 驱动的搜索系统既高效又可持续。

在本章中,我们将涵盖以下主要主题:

  • 探索高级 Haystack 功能以进行知识探索

  • 使用 Haystack 进行图推理

  • 扩展您的 Haystack 和 Neo4j 集成

  • 维护和监控您的 AI 驱动的搜索系统的最佳实践

技术要求

在深入本章内容之前,请确保您的开发环境已设置好必要的技术和工具。此外,您的 Neo4j 实例应加载了来自 Ch4 的数据和来自 Ch5 的嵌入。以下是本章的技术要求:

  • Neo4j (v5.x 或更高版本): 您需要在您的本地机器或服务器上安装并运行 Neo4j。您可以从 neo4j.com/download/ 下载它。

  • Haystack (v1.x): 我们将使用 Haystack 框架来集成 AI 驱动的搜索功能。请确保按照 docs.haystack.deepset.ai/docs/installation 中的说明安装 Haystack。

  • Python (v3.8 或更高版本): 确保您已安装 Python。您可以从 www.python.org/downloads/ 下载它。

  • OpenAI API 密钥: 要成功使用基于 GPT 的模型生成嵌入,您需要一个 OpenAI API 密钥:

注意

免费层 API 密钥在本项目的多数用例中都不会工作。您需要一个有效的付费 OpenAI 订阅来访问必要的端点和使用限制。

如果您已经遵循了前几章的设置,您可以跳过这些要求,因为它们已经安装好了。

本章的所有代码都可在以下 GitHub 仓库中找到:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/tree/main/ch6

此文件夹包含实现 Neo4j 和 Haystack 集成以及高级知识图谱功能所需的所有必要脚本、文件和配置。

确保克隆或下载存储库,以便您可以在本章中跟随代码示例。

探索高级 Haystack 功能以进行知识探索

在本节中,我们将深入探讨使用 Haystack 的更高级搜索功能。您在第五章中集成了嵌入到 Neo4j 图中。现在是时候探索如何超越基本的相似度匹配来增强搜索了。这里的目的是从简单的基于检索的嵌入转向对图中知识的更细致、多层次的探索。

我们将探讨诸如基于上下文的推理和针对特定用例优化搜索功能以提供高度相关和智能结果的技术。

让我们先谈谈基于上下文的推理。

基于上下文的搜索

现在,我们将在前一章的将 Haystack 连接到 Neo4j 进行高级向量搜索部分的基础上构建基于嵌入的方法,通过将多跳推理集成到 Neo4j 图和 Haystack 的相似度搜索功能中。这种方法允许搜索引擎在利用基于 AI 的高级检索方法的同时遍历节点之间的多个关系。我们不会仅仅根据直接匹配检索节点或文档,而是利用 Haystack 探索相关节点之间的路径,增加层次化的上下文并揭示更深入的见解。这种基于图推理和相似度理解的结合使得搜索结果更加智能和相关性更强。

Inception.

注意

标题作为title变量在主程序中的值传递。

在检索到这些相关电影后,Haystack 用于根据相似查询分析和排名结果,展示了结合基于图的关系和高级相似度检索的多跳搜索:

def fetch_multi_hop_related_movies(title):
    query = """
    MATCH (m:Movie {title: $title})<-[:DIRECTED]-(d:Director)-[:DIRECTED]->(related:Movie)
    RETURN related.title AS related_movie, related.overview AS overview
    """
    with driver.session() as session:
        result = session.run(query, title=title)
        documents = [
            {
                "content": record["overview"],
                "meta": {"title": record["related_movie"]}
            }
            for record in result
        ]
    return documents
def perform_similarity_search_with_multi_hop(query, movie_title):
    # Fetch multi-hop related movies from Neo4j
    multi_hop_docs = fetch_multi_hop_related_movies(movie_title)
    if not multi_hop_docs:
        print(f"No related movies found for {movie_title}")
        return
    # Write these documents to the document store
    document_store.write_documents(multi_hop_docs)
    # Generate embedding for the search query (e.g., "time travel")
    query_embedding = text_embedder.run(query).get("embedding")
    if query_embedding is None:
        print("Query embedding not created successfully.")
        return
    # Perform vector search only on the multi-hop related movies
    similar_docs = document_store.query_by_embedding(
        query_embedding, top_k=3
    )
    if not similar_docs:
        print("No similar documents found.")
        return
    for doc in similar_docs:
        title = doc.meta.get("title", "N/A")
        overview = doc.meta.get("overview", "N/A")
        score = doc.score
        print(
            f"Title: {title}\nOverview: {overview}\n"
            f"Score: {score:.2f}\n{'-'*40}"
        )
    print("\n\n") 

然而,由于我们只导入了一小部分原始数据集,它没有一对一的关系。当一个导演指导了多部电影时,搜索可能会产生类似Inception 没有找到相关电影的输出。

您可以尝试更新脚本以导入整个数据集(在从 AuraDB Free 升级到 AuraDB Professional 或 AuraDB Business Critical 或在 Neo4j Desktop 版本中)并查看多跳推理是如何执行的。

动态搜索查询与灵活的搜索过滤器

知识图谱的一个优势是在搜索查询期间动态应用过滤器。

在以下代码片段中,我们将演示如何将过滤器和约束条件纳入您的 Haystack 查询中,使用户能够根据特定参数(例如时间范围、类别或实体之间的关系)细化搜索结果。这种灵活性对于构建更互动和上下文丰富的搜索系统至关重要:

def perform_filtered_search(query):
    pipeline = Pipeline()
    pipeline.add_component("query_embedder", text_embedder)
    # pipeline.add_component("retriever", retriever)
    pipeline.add_component(
        "retriever", 
        Neo4jEmbeddingRetriever(document_store=document_store)
    )
    pipeline.connect(
        "query_embedder.embedding", "retriever.query_embedding"
    )
    result = pipeline.run(
        data={
            "query_embedder": {"text": query},
            "retriever": {
                "top_k": 5,
                "filters": {
                    "field": "release_date", "operator": ">=", 
                    "value": "1995-11-17"
                },
            },
        }
    )
    # Extracting documents from the retriever results
    documents = result["retriever"]["documents"]
    for doc in documents:
        # Extract title and overview from document metadata
        title = doc.meta.get("title", "N/A")
        overview = doc.meta.get("overview", "N/A")
        # Extract score from the document (not from meta)
        score = getattr(doc, "score", None)
        # Format score if it exists, else show "N/A"
        score_display = f"{score:.2f}" if score is not None else "N/A"
        # Print the title, overview, and score (or N/A for missing score)
        print(
            f"Title: {title}\nOverview: {overview}\n"
            f"Score: {score_display}\n{'-'*40}\n"
        ) 
demonstrates how to apply dynamic filters, such as release_date, to refine search results. By incorporating these filters, you can add constraints on specific fields—for instance, showing only documents from a certain date onward or filtering by specific attributes such as category or rating. This capability allows you to narrow down results to what is most relevant to them, effectively enhancing the search functionality. Using this approach, you can easily extend or modify filters to suit different needs, offering a flexible and powerful way to interact with data in the knowledge graph.

搜索优化:针对特定用例定制搜索

并非所有搜索系统都是相同的。无论您是在构建推荐引擎还是特定领域的搜索工具,都需要不同的优化。在本节中,我们将探讨如何根据您的独特用例定制 Haystack 的搜索配置,确保针对您特定数据的最优性能和相关性。我们还将讨论调整模型和索引以适应高规模环境的重要性。

请查看以下代码块:

def perform_optimized_search(query, top_k):
       optimized_results = document_store.query_by_embedding(
            query_embedding=text_embedder.run(query).get("embedding"), 
            top_k=top_k
        )
    for doc in optimized_results:
        title = doc.meta["title"]
        overview = doc.meta.get("overview", "N/A")
        print(f"Title: {title}\nOverview: {overview}\n{'-'*40}") 

此代码展示了如何调整参数,如top_k,以微调搜索查询返回的前 N 个结果的数量——而不是模型本身。top_k参数决定了基于向量相似度检索多少个前 N 个结果。

注意

这些只是代码片段。完整版本可在 GitHub 仓库中找到:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch6/beyond_basic_search.py

利用 Haystack 的相似度检索能力(如上下文感知搜索方法和动态过滤),您现在可以创建更精确的 AI 驱动搜索系统,并实现更好的搜索优化。然而,搜索只是开始。

在下一节中,我们将利用 Haystack 的推理能力和 Neo4j 知识图中的关系,将推理扩展到基于图的方法。

基于 Haystack 的图推理

在本节中,我们将探讨如何通过将 Haystack 与 Neo4j 强大的图推理功能集成,扩展 Haystack 的能力,使其超越基本搜索。虽然传统搜索方法基于文本相似度检索结果,但图推理允许您通过利用知识图中实体之间丰富的关联来揭示更深入的洞察。通过结合 Haystack 的相似度理解和 Neo4j 中的结构化数据,您可以执行更复杂的查询,遍历多个连接,揭示隐藏的模式,并解锁上下文丰富的洞察。

本节将指导您构建这些高级推理能力,将您的搜索系统转变为一个智能的、知识驱动的工具。

通过遍历多个关系来揭示隐藏的洞察

当前的图遍历有助于发现实体之间的联系,但跨多个关系和不同类型的关系遍历可以揭示你的知识图谱中的隐藏模式。通过在 Neo4j 中跨越各种路径——无论是电影、演员、导演还是类型之间——你可以生成超越直接关系的更深入见解。这种多步遍历允许你以基本搜索无法实现的方式探索数据,揭示可能被忽视的联系。

我们现在将探讨如何使用多种关系类型和多跳查询来检索更复杂的结果。然后我们将结合 Haystack 的相似度搜索功能进行精炼和排序。

这里有一个例子;你想找到既有与《侏罗纪公园》相同的演员又有相同导演的电影,这样你不仅可以发现直接合作,还可以发现间接联系:

def fetch_multi_hop_related_movies(title):
    query = """
    MATCH (m:Movie {title: $title})<-[:ACTED_IN|DIRECTED]-(p)-
        [:ACTED_IN|DIRECTED]->(related:Movie)
    WITH related.title AS related_movie, p.name AS person,
         CASE
            WHEN (p)-[:ACTED_IN]->(m) AND (p)-[:ACTED_IN]->(related) THEN 'Actor'
            WHEN (p)-[:DIRECTED]->(m) AND (p)-[:DIRECTED]->(related) THEN 'Director'
            ELSE 'Unknown Role'
         END AS role,
         related.overview AS overview, related.embedding AS embedding
     RETURN related_movie, person, role, overview, embedding
    """
    with driver.session() as session:
        result = session.run(query, title=title)
        documents = []
        for record in result:
            documents.append(
                Document(
                    content=record.get("overview", "No overview available"),  # Store overview in content
                    meta={
                        "title": record.get("related_movie", "Unknown Movie"),  # Movie title
                        "person": record.get("person", "Unknown Person"),       # Actor/Director's name
                        "role": record.get("role", "Unknown Role"),              # Actor or Director
                        "embedding": record.get("embedding", "No embedding available")  # Retrieve the precomputed embedding
                    },
                )
            )
    return documents 

通过路径查询解锁见解

图推理的另一个强大功能是能够查询节点之间的特定路径。例如,通过一系列合作找出两部电影是如何连接的,可以揭示令人惊讶的见解。

看看下面的查询:

MATCH path = (m1:Movie {title: "Inception"})-[:ACTED_IN*3]-(m2:Movie)
RETURN m1.title, m2.title, path 

这个查询找到了《盗梦空间》和另一部电影通过共享演员连接,跨越了三个层次的关系。

图 6.1 — 电影图中三跳路径遍历的插图

图 6.1 — 电影图中三跳路径遍历的插图

本图中的插图显示了一个电影图中的三跳路径遍历,从电影《盗梦空间》开始,通过一系列演员合作链达到《电影 C》。这条路径是使用重复三次的ACTED_IN关系探索电影之间连接的 Cypher 查询的结果。在所描述的示例中,《盗梦空间》通过演员 A 与《电影 B》相连,而《电影 B》又通过演员 B 进一步与《电影 C》相连。每跳代表从电影到演员或反之的转换,形成一个三跳的无向遍历。这种可视化突出了 Neo4j 中的多跳推理如何揭示更深层次的间接关系——这对于内容发现、推荐系统和协作网络分析等应用非常有价值。

注意

这些只是代码片段。完整版本可在 GitHub 仓库中找到:github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/blob/main/ch6/graph_reasoning.py

通过结合 Neo4j 的图推理和 Haystack 的相似度理解,我们已经能够捕捉到数据中的有意义联系,例如电影和演员之间的关系、理解多跳导演合作,以及揭示实体之间的复杂路径。

接下来,我们将探讨如何优化这些过程,以确保随着图在复杂性和规模上的增长,保持高性能。

扩展您的 Haystack 和 Neo4j 集成

随着您的系统扩展,对 Haystack 和 Neo4j 的需求也会增加。优化性能变得至关重要,尤其是在处理大型数据集、更复杂的图结构和高级搜索功能时。

在本节中,我们将重点关注最佳实践和技术,以确保您的 Haystack 和 Neo4j 集成能够高效地处理增加的负载。我们将探讨查询优化、缓存策略、索引改进以及扩展基础设施的技术,以满足性能需求,同时不牺牲速度或准确性,以下各小节将进行详细说明。

优化大型图上的 Neo4j 查询

随着您的 Neo4j 图的大小和复杂性增加,查询性能可能会下降,尤其是在遍历多个关系或处理大型数据集时。以下是一些提高 Neo4j 查询性能的技术:

  • 使用索引和约束:确保经常查询的属性,如titlename,被索引。索引可以加快节点查找速度,并使遍历更高效:

    CREATE INDEX FOR (m:Movie) ON (m.title);
    CREATE INDEX FOR (p:Person) ON (p.name); 
    
  • 配置和优化查询:使用 Neo4j 的PROFILEEXPLAIN关键字来分析查询的性能。这有助于您了解查询的哪些部分正在减慢速度,以及您可以在哪里进行优化:

    PROFILE MATCH (m:Movie {title: "Inception"}) RETURN m; 
    
  • 尽早限制结果数量:如果您正在处理大型结果集,请在查询的早期阶段限制返回的节点数量,以避免过度获取数据:

    MATCH (m:Movie)-[:ACTED_IN]->(a:Actor) RETURN m.title LIMIT 10; 
    

缓存嵌入和查询结果

当扩展 Haystack 和 Neo4j 时,缓存可以帮助减少冗余计算和网络调用,显著提高性能。通过缓存嵌入和查询结果,您可以提高搜索系统的效率,尤其是在处理大量查询时。以下是这些缓存策略如何产生差异的示例:

  • 缓存嵌入:将 Haystack 生成的嵌入存储在 Neo4j 或单独的缓存层(如 Redis)中。通过缓存嵌入,您可以避免为频繁询问的查询重新计算它们:

    # Example of caching embeddings
    embedding_cache = {}  # Simple in-memory cache, replace with Redis for larger setups
    def get_cached_embedding(query):
        if query in embedding_cache:
            return embedding_cache[query]
        else:
            embedding = text_embedder.run(query).get("embedding")
            embedding_cache[query] = embedding
            return embedding 
    
  • 缓存查询结果:对于频繁执行的 Neo4j 查询,考虑在内存中缓存查询结果或使用缓存(如 Redis 或 Memcached)。这通过为常用查询返回缓存结果来减少对 Neo4j 的负载:

    # Example using a Redis cache for Neo4j query results
    import redis
    cache = redis.Redis()
    def get_cached_query_result(query):
        cached_result = cache.get(query)
        if cached_result:
            return cached_result
        else:
            # Run the query against Neo4j
            result = run_neo4j_query(query)
            cache.set(query, result)
            return result 
    

高效使用向量索引

随着基于向量的搜索能力扩展,优化 Neo4j 中的向量索引对于保持性能至关重要。您可以这样做:

  • 配置向量索引以实现高性能:确保您的 Neo4j 中的向量索引根据嵌入维度和搜索需求进行优化配置:

    CREATE VECTOR INDEX overview_embeddings IF NOT EXISTS
    FOR (m:Movie) ON (m.embedding)
    OPTIONS {
        indexConfig: {
            `vector.dimensions`: 1536, 
            `vector.similarity_function`: 'cosine'
        }
    } 
    
  • 批量写入操作:当将许多嵌入写入 Neo4j 时,使用批量操作以减少单个写入的开销:

    document_store.write_documents(embeddings_list, batch_size=100)  
    # Batch size optimized for performance 
    

负载均衡和水平扩展

为了处理 Haystack 和 Neo4j 上的增加的交通和负载,水平扩展和负载均衡是必不可少的。通过实施负载均衡和水平扩展,你可以确保在大量交通下,你的系统保持响应性和弹性。以下是每种方法如何贡献于可扩展性的说明:

  • 扩展 Neo4j:利用 Neo4j AuraDB 或 Neo4j 集群,你可以将你的数据库工作负载分配到多个实例,增强读写能力。这对于需要快速数据检索和大规模处理的应用程序特别有益。

  • 负载均衡 Haystack:通过负载均衡器将传入的搜索查询分配到多个 Haystack 实例,可以防止任何单个实例过载。这种方法保持了一致的性能,并确保了高可用性,即使需求增长也是如此。

  • 使用 Kubernetes:通过容器化实例在 Kubernetes 上部署 Haystack 允许你根据流量调整副本数量,轻松地进行扩展。Kubernetes 动态编排这些副本,确保资源与需求相匹配,并且你的系统可以有效地处理使用高峰。以下是一个 Kubernetes 部署配置示例,用于扩展 Haystack,其中创建了多个副本以有效地处理增加的交通:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: haystack-deployment
    spec:
      replicas: 3  # Number of replicas to scale based on traffic
      selector:
        matchLabels:
          app: haystack
      template:
        metadata:
          labels:
            app: haystack
        spec:
          containers:
          - name: haystack
            image: haystack:latest 
    

通过实施这些优化策略,你可以确保随着你的数据和查询复杂性的增长,Haystack 和 Neo4j 的集成保持高性能和可扩展性。无论是通过缓存、高效的索引还是水平扩展基础设施,这些技术都将帮助你保持速度和准确性,即使在不断增长的负载下。随着你的系统增长,优化性能是至关重要的,但维护和监控你的 AI 驱动的搜索系统的健康状况同样关键。

注意

想了解 Neo4j 如何实现行业领先的速度和可扩展性,尤其是在你的数据和查询复杂性增长时?请探索这篇博客文章:使用 Neo4j 实现无与伦比的速度和可扩展性 (neo4j.com/blog/machine-learning/achieve-unrivaled-speed-and-scalability-neo4j/)。

在下一节中,我们将探讨保持你的系统长期平稳运行的最佳实践,重点关注如何监控性能、设置警报,并确保代码之外的长远稳定性和可靠性。

维护和监控你的 AI 驱动的搜索系统的最佳实践

构建一个强大的 AI 驱动的搜索系统只是开始。为了确保其长期成功,你需要超越初始设置,并专注于随着时间的推移维护和监控你的系统。定期的性能检查、主动监控和坚实的日志策略对于识别瓶颈、防止系统故障和优化资源使用至关重要。

现在我们来谈谈保持 Haystack 和 Neo4j 集成平稳运行的最佳实践,包括监控关键性能指标、设置关键问题的警报以及实施可持续的维护流程,以确保您的搜索系统即使在扩展时也能保持可靠和高效。

性能优化不是一个一次性活动。我们需要持续监控和收集指标,以识别瓶颈和改进区域。让我们看看我们如何实现这一点。

监控 Neo4j 和 Haystack 性能

定期跟踪查询响应时间、数据库性能和整体系统健康对于维护 AI 驱动的搜索系统至关重要。为 Neo4j 和 Haystack 设置监控以跟踪关键指标、识别瓶颈并确保平稳运行:

  • Neo4j 监控:利用 Neo4j 内置的指标以及与 Prometheus 和 Grafana 等工具的集成,可视化查询性能并监控系统负载。

  • Haystack 监控:使用 Grafana 和 Prometheus 监控 Haystack 中的查询吞吐量、延迟和响应时间。

这里是一个监控查询响应时间的示例:

# Example: Monitor response time of a query in Haystack
import time
start_time = time.time()
result = retriever.retrieve(query)
end_time = time.time()
response_time = end_time - start_time
print(f"Query response time: {response_time} seconds") 

设置关键问题的警报

设置自动警报可以确保在性能或系统故障发生时您会收到通知。通过使用 Prometheus 与 Alertmanager 或 Grafana,您可以设置基于阈值的警报,用于慢查询、失败的搜索或增加的负载。

例如,您可以为当 Neo4j 查询响应时间超过某个阈值或当 Haystack 的搜索延迟超出可接受范围时触发的警报。

您可以在 neo4j.com/docs/operations-manual/current/monitoring/ 上了解更多关于 Neo4j 监控和警报的信息。

实施日志策略

详细日志有助于排查问题并了解失败或性能下降的根本原因。在 Haystack 和 Neo4j 中实施日志记录,包括记录查询执行时间、失败和系统资源使用情况。

neo4j.com/docs/operations-manual/current/logging/ 上了解更多关于 Neo4j 日志的信息。有关 Haystack 日志和调试的更多信息,请访问 docs.haystack.deepset.ai/docs/debug

建立定期的维护流程

定期安排的维护确保您的 AI 驱动的搜索系统随着时间的推移继续以最佳性能运行。这包括以下内容:

通过实施这些最佳实践,你确保你的 AI 驱动搜索系统保持稳健、可靠,并能适应不断变化的需求。主动监控、有效的日志记录和定期的维护使你能够在问题影响性能之前发现它们,并确保随着数据和查询负载的增长,系统运行平稳。这些策略不仅防止了停机和不效率,还使你的系统能够无缝地发展和扩展。随着你继续构建和改进你的 AI 驱动搜索,对监控和维护的持续关注将是维持其长期成功的关键。

摘要

在本章中,我们探讨了如何优化你的 Haystack 和 Neo4j 集成,并确立了维护和监控你的 AI 驱动搜索系统的最佳实践。你学习了关于缓存、高效索引、查询优化以及扩展你的基础设施以处理增长的数据和查询负载的关键策略。我们还强调了监控系统性能、设置警报以及实施坚实的日志策略以保持系统长期平稳运行的重要性。随着数据和复杂性的增加,这些知识是创建快速、可靠和可扩展的搜索系统的关键第一步。

随着我们结束 Haystack 的这部分旅程,本书的下一部分将转向将 Spring AI 框架和 LangChain4j 与 Neo4j 集成。在接下来的章节中,你将探索这些技术如何结合在一起来构建复杂的推荐系统,进一步增强你的 AI 驱动应用程序的功能。

第三部分

使用 Neo4j、Spring AI 和 LangChain4j 构建智能推荐系统

在本书的第三部分,我们将探讨如何使用 Spring AI 和 LangChain4j 框架构建推荐应用。我们将探讨利用 LLMs 和 GraphRAG 来增强图,为构建更好的推荐应用奠定基础。我们将通过利用图数据科学算法,如 KNN 相似性和社区检测,来进一步增强图,以提供更好的推荐。我们还将探讨使用这些算法相对于基本向量搜索的优势。本书的这一部分包括以下章节:

本书这一部分包括以下章节:

  • 第七章**,介绍用于构建推荐系统的 Neo4j Spring AI 和 LangChain4j 框架

  • 第八章**,使用 H&M 个性化数据集构建推荐图

  • 第九章**,将 LangChain4j 和 Spring AI 与 Neo4j 集成

  • 第十章**,创建一个智能推荐系统

请保持关注

为了跟上生成式 AI 和 LLMs 领域的最新发展,请订阅我们的每周通讯,AI_Distilled,请访问packt.link/Q5UyU

第七章:介绍用于构建推荐系统的 Neo4j Spring AI 和 LangChain4j 框架

在前面的章节中,我们探讨了基于 Haystack 和 Python 的智能应用。虽然 Python 是数据科学家偏爱的语言框架,但在某些场景中,我们可能需要其他框架来构建解决方案。另一个值得考虑的流行语言框架是 Java。Java 比 Python 运行速度快,能够以无缝的方式集成到各种数据源中,并且是构建基于 Web 的应用程序以及 Spring 框架中最常用的语言。为此,在接下来的几章中,我们将探讨如何基于大型语言模型LLMs)和 Neo4j 构建智能应用。

此外,我们一直专注于利用 LLMs 的能力来构建智能搜索应用。尽管这只是其中一个方面;LLMs 在构建和使用知识图谱以增强推荐系统方面也可以是伟大的工具。在本章中,我们将了解推荐系统以及为什么个性化推荐很重要。我们将简要介绍推荐系统的传统基于规则的途径,并讨论其一些不足之处。然后,我们将向您介绍 LangChain4j 和 Spring AI 框架,以及它们如何支持您构建智能推荐系统。

在本章中,我们将涵盖以下主要主题:

  • 理解扩展的 Neo4j 能力以构建智能应用

  • 个性化推荐

  • 介绍 Neo4j 的 LangChain4j 和 Spring AI 框架

  • Neo4j GenAI 生态系统中智能推荐系统的概述

技术要求

虽然本章重点在于个性化推荐并介绍了 LangChain4j 和 Spring AI 框架,但本节没有特定的技术要求。

然而,如果您对 Spring 应用还不熟悉,可以参考spring.io/guides/gs/spring-boot上的文档来熟悉 Spring Boot。在接下来的章节中,我们将使用一个带有内置 Web 框架的 Spring Boot 应用程序。您还需要在系统上安装 Java。推荐使用 Java 17 或 19。

理解扩展的 Neo4j 能力以构建智能应用

在前面的章节中,我们探讨了如何使用 LLMs 和 Neo4j 构建优秀的搜索应用。虽然知识图谱为构建智能搜索应用提供了很好的上下文,但它们也可以是构建个性化推荐应用的坚实基础。

为了从数据中提取智能并构建超越基本流程分析更好、更智能的应用,我们需要比图数据库功能更多的能力。这正是 Neo4j 作为数据库的能力可以帮助构建更好应用的地方。

其中一些能力如下所示:

  • 可扩展性:Neo4j 使我们能够构建大型图,使用分片构建联邦图以处理大型数据集。它能够扩展以满足数据增长和业务需求,同时最小化成本。您可以在neo4j.com/docs/operations-manual/current/database-administration/composite-databases/concepts/了解更多关于这些功能的信息。

  • 安全性:通过利用角色,Neo4j 实现了数据安全。存在一些角色可以提供高级别的安全性,例如谁可以读取或写入数据库。它还提供了更细粒度的安全控制,根据角色定义可以读取哪些数据。采用这种方法,一个用户可能正在查看图的一部分,而另一个用户根据分配的角色查看图的不同部分。您可以在neo4j.com/docs/operations-manual/current/authentication-authorization/了解更多关于这些功能的信息。

  • 灵活的部署架构:Neo4j 的集群架构提供了多种部署选项,可以水平扩展以处理更高的读取量,并将读取本地化到不同的服务器,以最小化数据增长时的拥有成本。您可以在neo4j.com/docs/operations-manual/current/clustering/introduction/了解更多关于 Neo4j 集群功能的信息。

  • 图数据科学算法:Neo4j 图数据科学算法能够从连接数据中解锁隐藏的洞察。这些算法涵盖了路径查找、节点相似度、中心性和社区检测,以及机器学习方面的链接预测和节点分类。您可以在neo4j.com/docs/graph-data-science/current/了解更多关于 Neo4j 图数据科学功能的信息。

  • 向量索引:Neo4j 提供了向量索引功能,以索引嵌入,以便能够查找相似的节点,然后利用图遍历提供更准确的结果。您可以在neo4j.com/docs/cypher-manual/current/indexes/semantic-indexes/vector-indexes/了解更多关于其向量索引功能的信息。

作为图数据库的 Neo4j 使得轻松处理连接数据变得容易,上述功能超越了连接数据,帮助我们构建可扩展且复杂的智能应用。

注意

如果您想了解更多关于搜索和推荐系统

这些文章可能有所帮助:

搜索和推荐有何区别:medium.com/understanding-recommenders/whats-the-difference-between-search-and-recommendation-c32937506a29

搜索和推荐有何相同之处,有何不同之处?gist.github.com/veekaybee/2cf54ebcbd72aa73bfe482f20866c6ef

我们将在接下来的章节中利用 Neo4j 的能力来构建智能推荐系统。在此之前,让我们讨论一下推荐引擎是什么,以及个性化如何帮助创建智能推荐系统。

个性化推荐

推荐系统是一个基于用户的购买和搜索偏好向用户推荐产品的应用程序。这一方面不仅限于产品定位,还用于医疗诊断和治疗。例如,推荐可以帮助理解患者对药物的反应以及哪种治疗顺序更有效。

随着数据的增长和可用产品的增加,理解用户行为并提供最个性化的推荐变得越来越重要。

这些策略可以用来构建个性化体验。以下是一些提到的策略:

  • 构建用户档案:我们可以通过理解用户行为来构建自定义用户档案。行为模式可以包括用户在特定时间段内进行的交易顺序或事件的结果,以及其他属性,如年龄、种族和性别。我们可以使用这些方面将用户分成不同的组,并为每个组创建档案。

  • 提供上下文支持:一旦有了用户档案,我们就应该能够提供更有意义和上下文相关的支持给用户。这可能包括基于最后一次购买的产品推荐购买产品,或者基于当前治疗水平和当前症状的下一剂药物。这些推荐不仅考虑了最近发生的事件,还可以考虑其他用户属性,以提供更直接的支持。

  • 提供自助体验:除了根据需要提供上下文支持外,还可以使用推荐来提供更令人满意的自我服务体验。用户应该能够更改用于推荐的特性,从而提供一个能够根据用户事件调整其响应方式的系统。

  • 整合反馈:使用所有上述策略,可以整合正面和负面反馈,以便系统能够根据需要适应个别用户的要求。

个性化推荐提供了许多优势,包括基于当前视图建议下一个产品、根据用户行为提供激励、提升品牌声誉、优化患者治疗方案、更有效地推广新药、改进供应链流程以及确定最佳配送路线。这些定制化建议使企业能够向客户提供更相关和有影响力的体验。

这些是一些推荐可以使用的方法。推荐系统的一些其他有趣的用例可能包括提升销售额(neo4j.com/developer-blog/graphs-acceleration-frameworks-recommendations/)、管理供应链(neo4j.com/developer-blog/supply-chain-neo4j-gds-bloom/)以及执行患者旅程映射(www.graphable.ai/blog/patient-journey-mapping/)。

让我们来看看传统的基于规则的推荐系统方法以及为什么这种方法对于构建智能和个性化的推荐系统来说是不够的。

传统方法的局限性

传统上,推荐系统使用基于规则的系统。基于规则的系统是指决策是通过执行基于提供的数据输入的一组规则来做出的。逻辑可以是简单的,也可以根据需要非常复杂。例如,在特定地区,任何超过 1000 美元的信用卡交易都会自动拒绝。一个稍微复杂一些的规则可能是,当一个小交易成功执行后,然后尝试进行大交易时,会拒绝该交易。

基于规则的系统通常应用两种类型的规则:

  • 静态规则:在这里,规则是手动配置的。一旦这些规则被设置好,它们可以非常高效地工作,系统可以忠实执行它们。当您需要快速响应且资源消耗最少时,它们是好的。它们可以像基于输入返回值的 case 语句一样简单。

  • 动态规则:这些是复杂的规则引擎。在这些情况下,下一个决策可以依赖于当前决策树所处的状态和下一个数据输入。

使用基于规则的系统的一些好处如下:

  • 一致性:它们的行为是一致的,并保证对于给定的输入或输入集,输出是相同的。

  • 可扩展性:这些系统可以很好地扩展以轻松处理数据和复杂性。

  • 高效:在资源消耗和系统成本方面,它们非常高效。

  • 维护和管理:这些规则更容易构建和维护。这反过来使得管理这些系统变得容易。

通常,这些系统的用例是欺诈预防和网络安全。虽然这些系统简单且易于构建,但它们存在局限性。以下是一些例子:

  • 复杂性:如果处理不当,随着业务需求的增加,它们可能会变得相当复杂。随着复杂性的增加,大多数好处将逐渐开始消失。

  • 僵化性:系统过于僵化,难以适应新的数据类型和场景。即使我们识别出新的场景,编码和配置它们可能也需要太长时间才能有效。

  • 业务需求适应性:适应不断增长的业务需求和要求的这些系统可能需要太多的努力。

正如我们所见,随着业务需求的发展,当我们依赖于基于规则的系统时,我们会陷入有限的选项。构建一个能够适应新的数据点和数据复杂性,以提供更好的上下文并给出良好建议的智能应用变得越来越重要。这些系统应该能够快速适应不断变化的环境、数据和新的要求。

正是这里,Neo4j 作为图数据库及其周围的技术堆栈帮助我们构建智能推荐系统。让我们来看看如何实现。

介绍 Neo4j 的 LangChain4j 和 Spring AI 框架

要构建智能应用,我们可以利用围绕 Neo4j 可用的多个框架。对于智能推荐系统的特定用例,我们将探讨 Java 框架 Spring AI 和 LangChain4j。

LangChain4j

LangChain4j (github.com/langchain4j/) 是一个受流行的 Python LangChain 框架启发的 Java 框架,用于在 Java 中构建 LLM 应用程序。其目标是简化将 LLM API 集成到 Java 应用程序中。为此,它构建了一个结合了 LangChain、Haystack、LlamaIndex 和其他概念的 API,并为构建复杂应用增添了独特的风味。这就是它实现这些目标的方式。

以下列表帮助我们了解它是如何实现这些目标的:

  • 统一 API:所有大型语言模型(LLM)提供商,如 Open AI 和 Google Gemini,都有自己的专有 API 来构建应用程序。像 Neo4j、Pinecone 和 Milvus 这样的向量存储也提供自己的 API 来存储和检索嵌入。LangChain4j 提供了一个统一的 API,以隐藏所有这些 API 的复杂性,使开发更加容易。

  • 全面的工具箱:LangChain 社区已经识别出各种模式、抽象和技术,以构建大量的 LLM 应用程序和示例,并提供现成的包以加速开发。其工具箱包括低级提示模板、聊天内存管理、AI 服务和 RAG 的示例。其中大部分示例都易于集成到其他应用程序中。

LangChain4j 提供以下功能,帮助我们构建智能应用:

  • 超过 15 个 LLM 提供商:LangChain4j 提供了一个简单的 API,将 LLM 提供商集成到应用中并轻松使用。您可以在docs.langchain4j.dev/category/language-models上了解更多关于语言模型集成的内容。

  • 超过 20 个向量存储:向量存储 API 允许存储生成的嵌入并查询它们。以下是您要查看的向量存储 API:docs.langchain4j.dev/tutorials/embedding-stores

  • AI 服务:LangChain4j 提供了低级 API,例如直接与 LLM 提供商和向量存储交互的 API。但对于某些场景来说,这可能太底层了。为了简化操作,它还提供了更高级的 API 流程来集成 LLM、向量存储、嵌入模型和 RAG 作为管道。这些被称为 AI 服务(docs.langchain4j.dev/tutorials/ai-services)。我们将在接下来的章节中使用 AI 服务。

  • RAG:LangChain4j 提供了对 RAG 索引和检索阶段的支持。它有一个简单的Easy RAG功能,使得开始使用 RAG 功能变得容易。您可以在docs.langchain4j.dev/tutorials/rag上了解更多关于 LangChain4j 提供的 RAG 能力。

LangChain4j 与 Spring 框架有良好的集成。但 Apache Spring 框架也构建了一个类似于 LangChain4j 的独立 AI 集成框架,称为 Spring AI。我们将在下一节中查看这个框架。

Spring AI

Spring AI受到 LangChain4j 和 LlamaIndex 的启发。虽然 LangChain4j 支持简单的 Java 应用以及 Spring 应用,但 Spring AI 针对与 Spring 框架协同工作进行了优化。这意味着熟悉 Spring 框架的开发者可以更快、更轻松地开发 LLM 应用。

由于 Spring 框架提供了多个模块来连接各种数据库和许多开发者定义和使用的良好编码模式,这个新特性使得开发者能够非常容易地采用并快速构建 AI 应用。以下是一些 Spring AI 能力,它们可以帮助我们构建智能应用:

  • LLM 提示模板:LLM 提示模板提供了一个简单的 API,以便轻松集成 LLM。

  • 嵌入模型:Spring AI 可以通过配置集成各种嵌入模型引擎,以生成向量嵌入。

  • 向量存储:Spring AI 还提供了简单的 API 来存储和查询向量存储。它提供了基于配置的简单集成,以便连接到各种向量存储,如 Neo4j、Pinecone 和 Milvus。

  • RAG:您还可以使用 Spring AI 将 LLM 提示模板、嵌入模型和向量存储链接起来,构建有效的 RAG 应用。

LangChain4j 和 Spring AI 框架都提供了核心 API,用于与 LLM 聊天模型、提示模板、嵌入模型和向量存储集成。除了提供与这些系统通信的低级 API 外,它们还使使用高级 API(如 RAG 框架 API)构建更复杂的应用程序变得容易。

为什么选择基于 Java 的框架?

在 Python 中有很多框架可以与 Neo4j 协同工作。但是,有很多应用程序使用 Java 框架。这些框架提供了一种连接到各种数据源的方式,利用各种可用的包来构建复杂的应用程序。

这些框架支持各种向量存储,如 Neo4j,以及多个 LLM 提供商,如 Amazon Bedrock、Azure OpenAI、Google Gemini、Hugging Face 和 OpenAI。它们提供高级 AI 功能,从简单的任务,如为 LLM 格式化输入和解析输出,到更复杂的功能,如聊天记忆、工具和 RAG。

通过将这些能力与 Neo4j 结合,这些框架使构建更复杂的应用程序变得更容易,例如使用 LLM 生成图特征(路径等)的嵌入,这可以作为使用相似性和社区检测算法将节点分组到段的基础,以增强图谱。这种分段可以提供下一级推荐和其他方面的基础。您可以在neo4j.com/labs/genai-ecosystem/了解更多关于 Neo4j 的 GenAI 生态系统信息。

Neo4j GenAI 生态系统中的智能推荐系统概述

让我们看看基于 LLM/RAG 原则构建的推荐系统在 Neo4j GenAI 生态系统中的运作方式(图 7.1)。

图 7.1 — Neo4j RAG 推荐架构

图 7.1 — Neo4j RAG 推荐架构

我们可以利用这些框架的特性来构建基于知识图谱的 RAG 应用程序。在这个架构中,我们利用 Spring AI 应用程序来增强图谱,以便能够提供更多个性化的推荐。

此外,对于 RAG,这个架构可以利用向量索引以及图遍历来增强响应,以获得两者的最佳效果,从而获得更准确的响应。这个概念被称为G****raph RAG。知识图谱可以为 AI 模型交互带来更准确的响应、丰富的上下文和可解释性。Neo4j 可以集成到 LangChain4j 和 Spring AI 中,作为向量存储以及图数据库,以增强 LLM 的响应。

摘要

在本章中,我们探讨了 Neo4j 帮助我们构建智能应用程序的能力,为什么这些应用程序可以提供的个性化是有用的,以及它们与现有的基于规则的应用程序有何不同。我们探讨了 Spring AI 和 LangChain4j 是什么,以及它们构建智能应用程序的能力。

在下一章,第八章,我们将使用 H&M 数据集构建一个图数据模型,以支持智能和个性化的推荐,并了解如何将此类数据加载到图数据模型中,目的是提供推荐。本书的第九章将使您能够将这个智能推荐系统集成到 Spring AI 和 LangChain4j 框架中。

第八章:使用 H&M 个性化数据集构建推荐图

虽然 Neo4j 非常适合构建知识图谱,但审慎地考虑我们如何建模数据是很重要的。一个好的数据持久化模型可以使数据检索最优化并更好地处理大量负载。在本章中,我们将回顾一下构成知识图谱的内容,以及使用 Neo4j 数据持久化方法对数据建模的不同视角如何帮助构建更强大的知识图谱。您可能需要重新阅读第三章中定义的方法,这将使您能够使用个性化时尚推荐(H&M 个性化)数据构建知识图谱。

我们将在解决数据建模演变问题时涵盖这些主题:

  • 使用 H&M 个性化数据集建模推荐图

  • 优化推荐:图建模的最佳实践

技术要求

您需要熟悉 SQL 和 Cypher。我们将使用 SQLite 和 Neo4j 来理解数据建模的各个方面。在本章中,我们将使用以下工具:

记住从第三章中提到的,一个好的图数据模型可以使 RAG 流程中的检索部分更加有效。它使得检索相关数据更快、更简单。您可以重新阅读第三章以快速回顾图数据建模。在本章中,我们将时间作为一个维度来建模数据。以时间为维度的交易链使得数据检索非常高效和性能优良。

使用 H&M 个性化数据集建模推荐图

在本节中,我们将使用现实生活中的大规模 H&M 个性化数据集创建一个图数据模型。这个图数据模型将使我们能够增强我们在后续章节中创建的推荐引擎。

在 2022 年,H&M 发布了客户交易数据以及其他与客户、产品等相关联的元数据,作为构建推荐引擎竞赛的一部分。此数据集包含以前交易的数据以及客户和产品元数据。可用的元数据范围从简单的数据,如服装类型和客户年龄,到产品描述中的文本数据,再到服装图像中的图像数据。

我们将讨论数据集的特征,并逐步将数据加载到知识图谱中。

我们将查看此数据集中可用的数据:

  • images/: 这包含与给定article_id.关联的图片。数据集中并非所有文章都有与之关联的图片。我们不会使用这些数据来构建图。在图中存储图片不仅效率低下,而且对于我们要构建的图流程来说也不是必要的。

  • articles.csv: 该文件包含可供购买的商品的元数据。每一行代表一个独特的商品,包括产品系列、颜色、风格、商品所属的章节和部门。

  • customers.csv: 该文件包含数据集中每个客户的元数据,包括客户 ID、年龄、时尚新闻频率、活动标志、H&M 俱乐部会员状态和邮政编码。

  • transactions_train.csv: 该文件包含客户进行的交易。如果客户购买了同一件商品多次,数据可能以多行形式出现——每行代表一件购买的物品,包括交易日期、商品 ID、客户 ID、价格和销售渠道。

我们将在下一节查看这些数据的图数据模型,并加载数据以支持该模型。当我们为 H&M 个性化数据集构建推荐知识图谱时,我们将有一个客户进行的交易列表,将这些交易表示为按时间维度的交易链可能对我们非常有效。通过将我们对数据的理解添加到图数据模型中,可以使我们的推荐更有价值。例如,交易是一系列事件;因此,将它们建模为序列更有意义。与传统数据库不同,Neo4j 使得将这些交易作为通过关系顺序连接的图存储成为可能。

我们可以说,我们将对数据的了解持久化到图中,从而创建一个知识图谱。

构建您的推荐图

要构建推荐模型图,我们将查看数据集中每个文件中的数据以及它们如何贡献到图中。我们将应用之前讨论过的过程,在第三章**, 来构建图。在加载数据之前,我们需要使用 Neo4j Desktop 并执行以下步骤:

  1. 创建本地数据库。您可以按照neo4j.com/docs/desktop-manual/current/operations/create-dbms/中的说明执行此操作。

  2. 将 H&M 推荐数据集的 CSV 文件复制到该数据库的import目录。如果您不确定如何操作,请访问community.neo4j.com/t/where-is-neo4j-home/6488/5以获取参考。

现在,让我们将数据加载到图数据库中。

加载客户数据

客户数据包含以下元素:客户 ID、年龄、时尚新闻频率、活跃标志、H&M 俱乐部会员状态和邮政编码。

客户 ID 是客户的唯一 ID。为了确保我们有代表客户的唯一节点,我们需要有一个 UNIQUE 约束。此外,我们将邮政编码作为一个节点,因为我们可能想要轻松地按邮政编码隔离客户。

在加载这些数据之前,我们需要通过连接到我们创建的 Neo4j 数据库来创建这些唯一约束:

CREATE CONSTRAINT customer_id_idx FOR (n:Customer) REQUIRE n.id IS UNIQUE ;
CREATE CONSTRAINT postal_code_idx FOR (n:PostalCode) REQUIRE n.code IS UNIQUE ; 

一旦创建了唯一约束,我们就可以使用这个 Cypher 将数据加载到数据库中:

注意

对于 LOAD CSV 查询,我们需要在它们前面加上 auto 前缀,以便能够在 Neo4j 浏览器中运行它们。

LOAD CSV WITH HEADERS FROM "file:///customers.csv" as row
WITH row
CALL {
    WITH row
    MERGE (c:Customer {id:row.customer_id})
    SET c.age = row.age
    FOREACH( ignoreME in CASE WHEN row.fashion_news_frequency = 'Regularly'  THEN [1] ELSE [] END |
        SET c:FN_REGULAR
    )
    FOREACH( ignoreME in CASE WHEN row.club_member_status = 'ACTIVE'  THEN [1] ELSE [] END |
        SET c:CLUB_ACTIVE
    )
    FOREACH( ignoreME in CASE WHEN row.club_member_status = 'PRE-CREATE'  THEN [1] ELSE [] END |
        SET c:CLUB_PRE_CREATE
    )
    FOREACH( ignoreME in CASE WHEN row.Active <> 'ACTIVE'  THEN [1] ELSE [] END |
        SET c:INACTIVE
    )
    MERGE(p:PostalCode {code:row.postal_code})
    MERGE(c)-[:LIVES_IN]->(p)
} IN TRANSACTIONS OF 1000 ROWS 

这个脚本使用 1000 行作为一个批次提交,将客户数据加载到数据库中。在这个脚本中,我们可以注意到几个问题:

  • 除了唯一的 ID customer_id 之外,Customer 节点上只有一个名为 age 的属性

  • 我们将客户数据的其他属性映射到 Customer 节点的标签上

这种方法遵循我们之前讨论过的基于消费的数据建模方法。比如说,我们想要了解那些定期订阅时尚新闻的顾客的行为——这为我们提供了一个简单的方式来检索这些信息。Neo4j 通过基于标签的方法优化这种检索。我们可以将顾客行为(时尚新闻订阅)作为一个属性,并创建一个索引来检索这些数据,但这将需要更多的存储空间,以及索引查找的成本。如果我们想要使用那些活跃的俱乐部会员和定期消费时尚新闻的顾客——这种基于标签的方法与将它们作为属性存储相比,能更有效地检索这些信息。此外,当我们以图形的形式展示这些信息时,用户可以很容易地看到标签中的信息,而不是寻找属性。以这种方式消费数据感觉更自然,查询也更自然。

接下来,我们将加载文章数据。

加载文章数据

文章数据包含除了唯一的文章 ID 和描述之外的其他描述文章的类别。我们将创建其他描述文章本身的属性。

为了这个目的,我们需要创建这些唯一约束:

CREATE CONSTRAINT product_code_idx FOR (n:Product) REQUIRE n.code IS UNIQUE ;
CREATE CONSTRAINT article_id_idx FOR (n:Article) REQUIRE n.id IS UNIQUE ;
CREATE CONSTRAINT product_type_id_idx FOR (n:ProductType) REQUIRE n.id IS UNIQUE ;
CREATE CONSTRAINT colour_group_idx FOR (n:ColorGroup) REQUIRE n.id IS UNIQUE ;
CREATE CONSTRAINT product_group_name_idx FOR (n:ProductGroup) REQUIRE n.name IS UNIQUE ;
CREATE CONSTRAINT graphical_appearance_id_idx FOR (n:GraphicalAppearance) REQUIRE n.id IS UNIQUE ;
CREATE CONSTRAINT perceived_colour_id_idx FOR (n:PerceivedColor) REQUIRE n.id IS UNIQUE ;
CREATE CONSTRAINT department_id_idx FOR (n:Department) REQUIRE n.id IS UNIQUE ;
CREATE CONSTRAINT section_id_idx FOR (n:Section) REQUIRE n.id IS UNIQUE ;
CREATE CONSTRAINT garment_group_id_idx FOR (n:GarmentGroup) REQUIRE n.id IS UNIQUE ;
CREATE CONSTRAINT article_index_id_idx FOR (n:Index) REQUIRE n.id IS UNIQUE ;
CREATE CONSTRAINT article_index_group_id_idx FOR (n:IndexGroup) REQUIRE n.id IS UNIQUE ; 

我们可以看到,我们已经将文章的大多数属性转换成了节点。这种类型的数据规范化在图中表示。这个 Cypher 将数据加载到图中:

LOAD CSV WITH HEADERS FROM "file:///articles.csv" as row
WITH row
CALL {
    WITH row 

对于每一行,创建一个文章、产品以及产品组,并将它们关联起来:

MERGE(a:Article {id:row.article_id})
    SET a.desc = row.detail_desc
    MERGE(p:Product {code:row.product_code})
    SET p.name = row.prod_name
    MERGE(a)-[:OF_PRODUCT]->(p)
    MERGE(pt:ProductType {id:row.product_type_no})
    SET pt.name = row.product_type_name
    MERGE(p)-[:HAS_TYPE]->(pt)
    WITH row, a, p
    MERGE(pg:ProductGroup {name:row.product_group_name})
    MERGE(p)-[:HAS_GROUP]->(pg) 

现在添加与文章相关的图形外观和颜色:

WITH row, a
    MERGE(g:GraphicalAppearance {id:row.graphical_appearance_no})
    SET g.name = row.graphical_appearance_name
    MERGE (a)-[:HAS_GRAPHICAL_APPEARANCE]->(g)
    WITH row, a
    MERGE (c:ColorGroup {id: row.colour_group_code})
    SET c.name = row.colour_group_name
    MERGE (a)-[:HAS_COLOR]->(c)
    WITH row, a
    MERGE (pc:PerceivedColor {id: row.perceived_colour_value_id})
    SET pc.name = row.perceived_colour_value_name
    MERGE (a)-[:HAS_PERCEIVED_COLOR]->(pc)
    MERGE (pcm:PerceivedColor {id: row.perceived_colour_master_id})
    SET pcm.name = row.perceived_colour_master_name
    MERGE (pc)-[:HAS_MASTER]->(pcm) 

现在,让我们连接与之相关的部门:

WITH row, a
    MERGE (d:Department {id:row.department_no})
    SET d.name = row.department_name
    MERGE (a)-[:HAS_DEPARTMENT]->(d)
    WITH row, a
    MERGE (i:Index {id: row.index_code})
    SET i.name = row.index_name
    MERGE (a)-[:HAS_INDEX]->(i)
    MERGE (ig:IndexGroup {id: row.index_group_no})
    SET ig.name = row.index_group_name
    MERGE (i)-[:HAS_GROUP]->(ig) 

最后,让我们将文章所属的章节和服装组连接起来:

WITH row, a
    MERGE (s:Section {id: row.section_no})
    SET s.name = row.section_name
    MERGE (a)-[:HAS_SECTION]->(s)
    WITH row, a
    MERGE (gg:GarmentGroup {id: row.garment_group_no})
    SET gg.name = row.garment_group_name
    MERGE (a)-[:HAS_GARMENT_GROUP]->(gg)
} IN TRANSACTIONS OF 1000 ROWS 

从 Cypher 查询中,我们可以看到,在图中,我们正在持久化规范化数据,而没有对描述文章的各个方面重复值。

接下来我们将加载数据。

加载交易数据

transaction_train.csv 数据按照交易发生的顺序排列。这使得我们可以轻松地加载数据并保留图中数据的顺序。对于每一笔交易,我们都有以下数据:交易日期、商品 ID、客户 ID、价格和销售渠道。

注意

我们没有为每一笔交易分配一个唯一的 ID。

我们可以使用以下 Cypher 加载数据:

LOAD CSV WITH HEADERS FROM "file:///transactions_train.csv" as row WITH row
CALL {
    WITH row
    MATCH (c:Customer {id:row.customer_id})
    MATCH (a:Article {id:row.article_id})
    WITH a, c, row
    CREATE (t:Transaction {date: row.t_dat, price: row.price, salesChannel: row.sales_channel_id})
    CREATE (t)-[:HAS_ARTICLE]->(a)
    WITH c, t
    CALL {
        WITH c, t
        WITH c, t
        WHERE exists((c)-[:START_TRANSACTION]->()) OR exists((c)-[:LATEST]->())
        MATCH (c)-[r:LATEST]->(lt)
        DELETE r
        CREATE (lt)-[:NEXT]->(t)
        CREATE (c)-[:LATEST]->(t)
        UNION
        WITH c, t
        WITH c,t
        WHERE NOT ( exists((c)-[:START_TRANSACTION]->()) OR exists((c)-[:LATEST]->()) )
        CREATE (c)-[:START_TRANSACTION]->(t)
        CREATE (c)-[:LATEST]->(t)
    }
} IN TRANSACTIONS OF 1000 ROWS 

从这个 Cypher 查询中,我们可以看到我们取给定客户的第一个交易,并使用一个 START_TRANSACTION 关系将其连接到客户。我们使用一个 LATEST 关系来跟踪客户所做的最后交易。随着我们为客户获取更多交易,我们将 LATEST 关系移动到最新的交易。我们使用 NEXT 关系将之前通过 LATEST 关系连接的交易和新的交易连接起来。因此,在这个图中,我们代表客户所做的交易就像 transaction_train.csv 数据集文件名所暗示的那样,是一个交易列车。

最终图

加载所有数据后,我们的图模型将如图 图 8.1 所示。

图 8.1 — 加载 H&M 数据后的图数据模型

图 8.1 — 加载 H&M 数据后的图数据模型

从这个图中我们可以看到,商品属性已经分散到各个单独的节点中。客户 节点连接到邮政编码和第一笔和最后交易。交易商品 关联。交易 节点还连接到给定客户可用的任何后续交易。

现在我们已经加载数据,让我们探索如何从这些数据中进一步增强图,将我们对数据的理解和想法添加到图中。

优化推荐:图建模的最佳实践

我们现在有一个图,数据以我们想要消费的方式加载,并代表数据的上下文。尽管如此,图仍然只代表提供的原始上下文。如果我们想按季节和年份消费数据,我们仍然需要构建查询来检索它。由于 Neo4j 是可选模式的,我们可能可以进行一些后处理并添加额外的关联,以便以这种方式消费数据。

在这个 Cypher 脚本中,我们正在创建季节性关系:

  1. 对于每个客户,遍历交易并根据月份和年份分配一个季节值:

    MATCH (c:Customer)
    WITH c
    CALL {
        WITH c
        MATCH (c)-[:START_TRANSACTION]->(s)
        MATCH (c)-[:LATEST]->(e)
        WITH c,s,e
        MATCH p=(s)-[:NEXT*]->(e)
        WITH c, nodes(p) as nodes
        UNWIND nodes as node 
    
  2. 例如,如果月份是 1 且年份是 2020,我们将 WINTER_2019 作为该交易的季节名称作为上下文:

    WITH c, node, node.date as d
        WITH c, node, toInteger(substring(d, 0,4)) as year, substring(d, 5,2) as month
        WITH c, node,
            CASE WHEN month="12" THEN year
                 WHEN month="01" OR month="02" THEN year-1
                ELSE
                    year
            END as year, 
    
  3. 收集每个季节值对应的交易:

    CASE WHEN month="12" OR month="01" OR month="02" THEN "WINTER"
                 WHEN month="03" OR month="04" OR month="05" THEN "SPRING"
                 WHEN month="06" OR month="07" OR month="08" THEN "SUMMER"
                 WHEN month="09" OR month="10" OR month="11" THEN "FALL"
            END as season
        WITH c, node, season+'_'+year as relName 
    
  4. 获取每个季节值集合的第一个记录:

    WITH c, relName, head(collect(node)) as start
        WHERE relName is not null 
    
  5. 使用季节值作为关系名称,在客户和该交易之间创建一个关系。我们使用 apoc 方法创建关系,因为关系名称是动态的:

    CALL apoc.create.relationship(c, relName, {}, start) YIELD rel
        WITH 1 as out
        return DISTINCT out
    } IN TRANSACTIONS OF 1000 ROWS
    WITH 1 as r
    RETURN DISTINCT r 
    

请注意,这是一个非常基础的途径。这表明我们可以根据我们对数据的理解在图中创建额外的上下文。这些方法使 Neo4j 非常适合构建知识图谱。当我们以这种方式轻松访问数据时,它可以开启更多关于如何以不同方式观察相同数据的想法,以简单、可追踪和可理解的方式提取更多智能

如果您不想手动加载数据,您可以从以下网址下载数据库快照:  packt-neo4j-powered-applications.s3.us-east-1.amazonaws.com/Building+Neo4j-Powered+Applications+with+LLMs+Database+Dump+files.zip

我们现在已经为数据添加了更多上下文。接下来,让我们看看图数据模型。

图 8.2 — 增强季节关系后的 H&M 图数据模型

图 8.2 — 增强季节关系后的 H&M 图数据模型

让我们利用我们对数据的理解来编写一个查询,以获取 2019 年夏季随机客户购买的文章:

MATCH (c:Customer)-[:SUMMER_2019]->(start), (c)-[:FALL_2019]->()<-[:NEXT]-(end)
WITH c, start, end SKIP 100 LIMIT 1
MATCH p=(start)-[:NEXT*]->(end)
WITH nodes(p) as nodes, relationships(p) as rels
UNWIND nodes as node
MATCH p=(node)-[:HAS_ARTICLE]->(a)
RETURN a.desc as article 

使用这个查询,我们找到在 2019 年夏季和秋季都购买过物品的客户,从那个列表中选取一个客户,并检索文章描述。

查询的输出将如下所示:

图 8.3 — 查询 SUMMER_2019 客户购买的 Cypher 查询

图 8.3 — 查询 SUMMER_2019 客户购买的 Cypher 查询

通过查看查询,可以很容易地理解查询正在做什么。我们使用 SUMMER_2019 作为起点,以及一个在 FALL_2019 关系之前的交易作为终点,从起点遍历到终点,并检索那些交易的物品。

我们可以看到,我们完全依赖于图遍历而不是基于属性的过滤器,这使得执行此查询非常高效。Neo4j 就是构建来高效执行这类查询的。

摘要

在本章中,我们探讨了如何观察图数据模型,以及如何根据我们消费数据的方式构建模型,使其更易于高效地检索数据。我们研究了 H&M 推荐数据集,并使用这些原则加载了它,还利用属性和我们对这些数据的理解对其进行了增强。这为图添加了更多上下文,并使得查询数据变得简单——查询更易于阅读和以更简单的方式向他人解释。

在下一章中,我们将在此基础上构建数据,使用 LLM 进一步增强它,并看看 LLM 如何为我们提供更强大的知识图谱。

第九章:将 LangChain4j 和 Spring AI 与 Neo4j 集成

现在我们已经将数据加载到图中,在本章中,我们将探讨如何使用 LangChain4j 或 Spring AI 来增强图的功能并构建一个知识图谱。我们将研究如何将图与 LLMs 集成以生成客户购买的摘要,并创建该摘要的嵌入来表示客户购买历史。这些嵌入对于使机器学习和图算法能够理解和处理图数据至关重要。这些嵌入可以帮助我们构建知识图谱,通过理解购买行为为客户提供更个性化的推荐。我们还将探讨如何创建数据集中每个文章详细描述的嵌入。

在本章中,我们将涵盖以下主要主题:

  • 设置 LangChain4j 和 Spring AI

  • 使用 LangChain4j 构建你的推荐引擎

  • 使用 Spring AI 构建你的推荐引擎

  • 微调你的推荐系统

技术要求

我们将使用 Java IDE 环境来处理 LangChain4j 和 Spring AI 项目。你需要安装这些并了解如何使用它们。开始之前,你需要以下内容:

  • 将使用 Maven 来构建项目和管理工作依赖。如果你打算使用 IntelliJ IDE(或 IntelliJ IDEA),那么 Maven 将与其一起安装,你无需单独安装。如果你是 Maven 的新手,你可以在maven.apache.org/了解更多相关信息。

  • Java 17。

  • IntelliJ – 这些示例是用 IntelliJ IDE 构建和测试的。你可以使用你喜欢的 IDE,但我们将使用 IntelliJ IDEA 工具来构建和运行我们的项目。你可以从www.jetbrains.com/idea/下载该工具。你可以下载社区版本来运行本章的示例。你可以在www.jetbrains.com/idea/spring/了解更多关于如何使用此 IDE 构建 Spring 应用程序的信息。

  • Spring Boot – 如果你是对 Spring Boot 新手,你可以访问spring.io/projects/spring-boot来了解更多。

  • 安装了以下插件的 Neo4j Desktop。我们将从上一章构建的图数据库开始。你可以从neo4j.com/download/下载 Neo4j Desktop。如果你是 Neo4j Desktop 的新手,你可以在neo4j.com/docs/desktop-manual/current/了解更多。代码与数据库的 5.21.2 版本进行了测试。以下是需要安装的插件:

    • APOC 插件 – 5.21.2

    • 图形数据科学库 – 2.9.0

下图展示了如何为数据库管理系统安装这些插件。

图 9.1 — 在 Neo4j Desktop 上安装插件

图 9.1 — 在 Neo4j Desktop 上安装插件

当你在 Neo4j Desktop 中选择 DBMS 时,在右侧会显示其详细信息。点击插件选项卡并选择所需的插件。在详细信息面板中,点击安装并重启按钮。

注意

您可以在github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/tree/main/ch9找到您需要的所有代码。这些是完整的项目,可以在 IDE 中运行。在本章中,我们只展示代码片段以展示其用法。因此,下载代码以遵循本章中的步骤可能是个好主意。

我们将从设置 LangChain4j 和 Spring AI 项目开始。

设置 LangChain4j 和 Spring AI

我们将查看如何使用spring initializr网站(start.spring.io/)设置 Spring AI 和 LangChain4j 项目。

我们将独立查看这些技术。LangChain4j 和 Spring AI 都是执行相同任务的选项。我们只需要其中一个框架来构建 GenAI 项目。LangChain4j 比 Spring AI 更早一些时间可用。在 API 和集成方面,它们的工作方式相当相似。我们将使用这两个框架构建相同的应用程序,并查看它们有多么相似。我们还将确定它们之间的差异。

我们需要遵循以下步骤来创建启动项目:

  1. 设置 LangChain4j 项目:

    1. 访问网站,start.spring.io/

    2. 项目部分下选择Maven

    3. 语言部分下选择Java

    4. 项目元数据部分,填写以下值:

      • : com.packt.genai.hnm.springai

      • 工件: springai_graphaugment

      • 名称: springai_graphaugment

      • 描述: 使用 Spring AI 进行图增强

      • 包名: com.packt.genai.hnm.springai.graphaugment

      • 打包: Jar

      • Java: 17

    5. 依赖项部分,点击添加依赖项按钮并选择Spring Web依赖项。

      • 当前初始化器没有列出其他依赖项需要添加到项目中。我们将手动将 LangChain4j 依赖项添加到项目中。
    6. 下载并保存生成的 ZIP 文件。

  2. 设置 Spring AI 项目:

    1. 访问网站,start.spring.io/

    2. 项目部分下选择Maven

    3. 语言部分下选择Java

    4. 项目元数据部分,填写以下值:

      • : com.packt.genai.hnm.langchain

      • 工件: langchain_graphaugment

      • 名称: langchain_graphaugment

      • 描述: 使用 Langchain4J 进行图增强

      • 包名: com.packt.genai.hnm.langchain.graphaugment

      • 打包: Jar

      • Java: 17

    5. 依赖项部分,点击添加依赖项按钮以选择以下依赖项:

      • Spring Web

      • OpenAI

      • Neo4j 向量数据库

    6. 下载并保存生成的 ZIP 文件。

这只会给我们一个基本的项目框架,我们将在此基础上添加更多逻辑来构建应用程序。

在我们继续构建应用程序之前,让我们看看我们希望从应用程序中得到什么。在前一章中,我们将 H&M 的交易数据加载到了图数据库中。目前,它包含了客户、商品和交易,以及一些辅助关系,这些关系标记了给定季节和年份中的第一次交易。由于我们想要构建一个个性化推荐系统,我们希望增强图数据库以理解客户行为并提供推荐。为此,我们将采取以下方法:

  1. 选择一个季节来了解购买行为。例如,假设我们想要找到在 2019 年夏天和秋天进行购买的客户,并使用这些季节之间的交易来理解客户行为。请注意,可能有一些客户在 2019 年的秋天没有进行任何交易,尽管他们可能在夏天进行了交易。为了使事情更简单,我们将忽略这些客户。

  2. 获取这些交易中购买的商品。这些商品应满足条件(在 2019 年的夏天和秋天购买),并按照购买顺序排列。然后我们将使用 LLM 来总结这些购买。这种总结保留了购买商品的顺序。

  3. 使用 LLM 为这篇总结文本生成嵌入。我们将利用 OpenAI 的 LLM 来完成这一部分。

  4. 存储这些嵌入。我们将将这些嵌入存储在生成这些嵌入的季节关系上。例如,如果我们正在为 2019 年的夏天生成总结,我们将把生成的嵌入存储在SUMMER_2019关系上。我们将使用 OpenAI 的 LLM 来生成嵌入。

在下一节中,我们将探讨如何使用 LangChain4j 构建一个执行我们之前描述的功能的应用程序。

使用 LangChain4j 构建你的推荐引擎

在本节中,我们将探讨构建一个利用 LangChain4j 的图增强应用程序。在这个项目中,我们将使用 GraphRAG 方法为满足我们要求的交易链生成嵌入。我们将使用 Neo4j 图检索器检索满足我们要求的交易链,以及一个 LLM 生成这些交易的摘要以描述客户购买行为并生成嵌入。生成的嵌入将是一个向量表示,以机器学习或图数据科学算法可以利用的方式描述文本摘要。它也可以用于向量搜索。这篇文章很好地解释了 LLM 上下文中的嵌入:ml-digest.com/architecture-training-of-the-embedding-layer-of-llms/。我们将从上一节下载的 ZIP 文件开始。我们需要解压我们下载的文件。一旦解压,我们将使用以下步骤将这些项目加载到 IntelliJ 平台:

  1. 启动 IntelliJ IDE。

  2. 点击 文件 | 新建 | 从现有源创建项目…

图 9.2 — 创建新项目

图 9.2 — 创建新项目

  1. 从我们解压的目录中选择 pom.xml 文件。

图 9.3 — 选择 pom.xml

图 9.3 — 选择 pom.xml

  1. 点击 信任项目 以加载项目。

图 9.4 —信任项目

图 9.4 — 信任项目

  1. 当提示时,选择 新窗口

图 9.5 — 选择新窗口

图 9.5 — 选择新窗口

  1. 一旦项目加载完成,您可以继续到下一节。

在下一节中,我们将更新项目依赖项。

LangChain4j:更新项目依赖项

当我们使用 Spring 启动器准备启动项目时,我们只能添加由该工具识别的依赖项。我们需要编辑 pom.xml 文件来添加依赖项。

以下是我们需要添加到项目中的依赖项:

  • LangChain4j Spring Boot 启动器 – 这个依赖提供了 LangChain4j 的 Spring Boot 集成:

    <!-- Langchain Springboot integration -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-spring-boot-starter</artifactId>
        <version>0.36.0</version>
    </dependency> 
    
  • LangChain4j OpenAI 集成 – 这个依赖提供了 OpenAI 集成:

    <!-- Open AI integration -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
        <version>0.36.0</version>
    </dependency> 
    
  • LangChain4j Neo4j 集成 – 这个依赖提供了 Neo4j 集成:

    <!-- Neo4j Vector Store integration -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-neo4j</artifactId>
        <version>0.35.0</version>
    </dependency> 
    
  • LangChain4j LLM 嵌入集成 – 这个依赖提供了 LLM 嵌入 API:

    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId>
        <version>0.35.0</version>
    </dependency> 
    

最新集成选项和详细信息可以在 docs.langchain4j.dev/category/integrations 找到。

现在我们已经添加了项目依赖项,我们需要更新应用程序所需的配置属性。在下一节中,我们将探讨更新应用程序属性。

注意

当您对 pom.xml 文件进行了更改,可能需要重新加载

为 IDE 更新所有依赖项的项目。您可以在www.jetbrains.com/help/idea/delegate-build-and-run-actions-to-maven.html#maven_reimport了解更多关于如何在 IntelliJ IDEA 中与 Maven 项目一起工作的信息。

LangChain4j:更新应用程序属性

在本节中,我们需要更新应用程序属性,以便利用我们上一节中添加的依赖项的 API。我们需要将此配置添加到项目的application.properties文件中。由于我们将使用 OpenAI LLM 作为聊天模型和嵌入,因此我们需要为此目的获取一个 API 密钥。我们需要访问以下网站以获取此目的的 API 密钥:platform.openai.com/docs/overview

这些是我们需要添加的配置属性:

  • OpenAI 聊天模型集成 – 我们需要将此配置添加到application.properties

    # Open AI LLM Integration for Generating Summary using Chat Model.
    langchain4j.open-ai.chat-model.api-key=<`OPEN_AI_KEY`>
    langchain4j.open-ai.chat-model.model-name=gpt-4o-mini
    langchain4j.open-ai.chat-model.log-requests=true
    langchain4j.open-ai.chat-model.log-responses=true 
    
  • OpenAI 嵌入集成 – 我们需要将此配置添加到application.properties

    # Open AI LLM Integration for Generating Embeddings
    langchain4j.open-ai.embedding-model.api-key=<`OPEN_AI_KEY`>
    langchain4j.open-ai.embedding-model.model-name=text-embedding-3-large 
    
  • Neo4j 集成 – 这次我们将添加基本的 Neo4j 集成,而不是与 Neo4j 向量数据库相关的集成:

    # Neo4j Integration
    neo4j.uri=bolt://localhost:7687
    neo4j.user=neo4j
    neo4j.password=test1234
    neo4j.database=hmreco
    config.batchSize=5 
    

现在我们已经查看完配置属性,让我们开始构建应用程序。我们将从 Neo4j 数据库集成开始,然后添加聊天模型集成来总结交易并生成摘要的嵌入。最后,我们将查看如何构建一个 REST 端点以按需调用这些请求。

LangChain4j:Neo4j 集成

我们首先将查看 Neo4j 集成。我们将首先查看这一点,因为我们需要一种与数据库集成的手段来执行这些任务:

  1. 设置连接以能够执行读取和写入事务。

  2. 读取指定季节发生的交易的文章。

  3. 一旦生成嵌入,就将其持久化(保存)。

在我们构建这个逻辑之前,我们需要为 Neo4j 连接创建一个配置 Bean。我们可以这样定义这个 Bean 来从application.properties中读取:

@ConfigurationProperties(prefix = "neo4j")
public class Neo4jConfiguration { 
    private String uri; 
    private String user ; 
    private String password ; 
    private String database ; 
   */** Getter/Setters **/*
} 

类定义上方的ConfigurationProperties注解将读取application.properties并初始化 Bean 中的属性。prefix选项告诉我们只读取以该前缀开始的属性。例如,如果我们想填充uri字段,那么我们需要将neo4j.uri属性添加到配置中。我们没有在这里包含从该 Bean 读取属性所需的所有 getter 和 setter 代码。

现在,我们将定义一个服务,以提供与 Neo4j 数据库的集成,读取文章和客户交易数据,并根据需要更新嵌入:

  1. 使用@Service注解定义服务类。我们还需要在这里注入Neo4JConfiguration

    @Service
    @Configuration
    @EnableConfigurationProperties(Neo4jConfiguration.class)
    public class Neo4jService {
        @Autowired
        private Neo4jConfiguration configuration ;
        private Driver driver ; 
    
  2. setup方法添加到初始化与 Neo4j 数据库的连接:

    public synchronized void setup() {
            if( driver == null ) {
                driver = GraphDatabase.driver(
                        configuration.getUri(),
                        AuthTokens.basic(
                                configuration.getUser(),
                                configuration.getPassword()));
                driver.verifyConnectivity();
            }
        } 
    
  3. 添加一个方法来获取给定季节开始和结束值客户交易数据。根据提供的季节开始和结束值,它检索开始季节值的elementId值和购买顺序中的文章描述。我们需要这个elementId值来稍后保存嵌入。我们可以看到,我们正在尝试从文章属性中获取更多相关数据,而不仅仅是描述。这样,我们可以包括更多属性,如颜色,作为摘要的一部分,以便我们可以更准确地表示它们作为嵌入:

    public List<EncodeRequest> getDataFromDB(String startSeason, String endSeason) {
        setup();
        String cypherTemplate = """
            --- Cypher query to get the transactions
        """;
        String cypher = String.format(cypherTemplate, startSeason, endSeason);
        SessionConfig config = SessionConfig.builder()
            .withDatabase(configuration.getDatabase())
            .build();
        try (Session session = driver.session(config)) {
            List<EncodeRequest> data = session.executeRead(tx -> {
                List<EncodeRequest> out = new ArrayList<>();
                var records = tx.run(cypher);
                while (records.hasNext()) {
                    var record = records.next();
                    String id = record.get("elementId").asString();
                    String articles = record.get("articles").asString();
                    out.add(new EncodeRequest(articles, id));
                }
                return out;
            });
            return data;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    } 
    
  4. 将获取数据库中文章的方法添加:

    public List<EncodeRequest> getArticlesFromDB() {
        setup();
        String cypherTemplate = """
            -- Cypher query to get the articles.
        """;
        SessionConfig config = SessionConfig.builder()
            .withDatabase(configuration.getDatabase())
            .build();
        try (Session session = driver.session(config)) {
            List<EncodeRequest> data = session.executeRead(tx -> {
                List<EncodeRequest> out = new ArrayList<>();
                var records = tx.run(cypherTemplate);
                while (records.hasNext()) {
                    var record = records.next();
                    String id = record.get("elementId").asString();
                    String article = record.get("article").asString();
                    out.add(new EncodeRequest(article, id));
                }
                return out;
            });
            return data;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    } 
    
  5. 添加一个方法来保存客户选定季节的嵌入。我们将摘要保留在图中,以了解嵌入表示什么。一旦我们理解了这个方面,我们就不需要将摘要存储在数据库中:

    public void saveEmbeddings(List<Map<String, Object>> embeddings) {
        setup();
        String cypher = """
            UNWIND $data as row
            WITH row
            MATCH ()-[r]->()
            WHERE elementId(r) = row.id
            SET r.summary = row.summary
            WITH row, r
            CALL db.create.setRelationshipVectorProperty(r, 'embedding', row.embedding)
        """;
        SessionConfig config = SessionConfig.builder()
            .withDatabase(configuration.getDatabase())
            .build();
        try (Session session = driver.session(config)) {
            session.executeWriteWithoutResult(tx -> {
                tx.run(cypher, Map.of("data", embeddings));
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    } 
    
  6. 添加一个方法来保存Article节点上Article文本的嵌入:

    public void saveArticleEmbeddings(List<Map<String, Object>> embeddings) {
        setup();
        String cypher = """
            UNWIND $data as row
            WITH row
            MATCH (a:Article)
            WHERE elementId(a) = row.id
            CALL db.create.setNodeVectorProperty(a, 'embedding', row.embedding)
        """;
        SessionConfig config = SessionConfig.builder()
            .withDatabase(configuration.getDatabase())
            .build();
        try (Session session = driver.session(config)) {
            session.executeWriteWithoutResult(tx -> {
                tx.run(cypher, Map.of("data", embeddings));
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    } 
    

从代码中我们可以看到,这个服务依赖于Neo4jConfiguration并提供了这些方法。

这里的代码流程简单,提供了与 Neo4j 数据库交互的实用方法。获取和保存数据的方法在这里嵌入 Cypher 查询。

接下来,我们将查看一个 OpenAI 聊天模型集成,它可以生成文章列表的摘要。

LangChain4j:OpenAI 聊天集成

要集成聊天,我们需要定义AiService。这是 Langchain4J 暴露的 API,用于构建 Java 应用程序。

让我们看看我们如何做到这一点:

  1. 当我们定义AiService时,LangChain4j Spring 框架在幕后提供了实现,使得调用聊天服务变得非常简单。让我们看看如何定义它:

    @AiService
    public interface ChatAssistant {
        @SystemMessage(""" 
    
  2. 我们为 LLM 聊天引擎设置了一个角色。这为引擎设置了处理数据的上下文,即使用什么指南来处理数据:

    ---Role---
    
                 You are an helpful assistant with expertise in fashion for a clothing company. 
    
  3. 我们在这里为 LLM 引擎设置了一个目标,说明它应该如何处理数据。这描述了输入数据是什么以及它的结构:

    ---Goal---
    
                Your goal is to generate a summary of the products purchased by the customers and descriptions of each of the products.\s
                Your summary should contain two sections -\s
                Section 1 - Overall summary outlining the fashion preferences of the customer based on the purchases. Limit the summary to 3 sentences
                Section 2 - highlight 3-5 individual purchases.
    
                You should use the data provided in the section below as the primary context for generating the response.\s
                If you don't know the answer or if the input data tables do not contain sufficient information to provide an answer, just say so.\s
                Do not make anything up.
    
                Data Description:
                - Each Customer has an ID. Customer ID is a numeric value.
                - Each Customer has purchased more than one clothing articles (products). Products have descriptions.
                - The order of the purchases is very important. You should take into account the order when generating the summary. 
    
  4. LLM 的响应指令规定了响应应该如何结构化:

    Response:
                ---
                # Overall Fashion Summary:
    
                \\n\\n
    
                # Individual Purchase Details:
    
                -- 
    
  5. Data部分定义了{text}变量,该变量用方法接收到的输入进行替换:

    Data:
                {text}
        """)
        String chat(String text);
    } 
    

在这里,我们定义了一个带有@AiService注解的接口。在这个服务中,我们需要定义一个聊天方法。在这里,我们将使用一个简单的带有System Message选项的 AI 聊天服务。要了解AIServices提供的常见操作和高级操作,请阅读docs.langchain4j.dev/tutorials/ai-services/中的文档。在这里,我们要求 LLM 扮演时尚专家的角色,并给出客户时尚偏好的摘要,并突出显示顶级购买,同时考虑到购买顺序。文本中的input参数被用作聊天助手的输入数据。

现在我们将看看如何调用这个聊天请求:

@Service
public class OpenAIChatService
    private ChatAssistant assistant ;
    public OpenAIChatService(ChatAssistant assistant) {
        this.assistant = assistant;
    }
    public String getSummaryText(String input) {
        String out = assistant.chat(input) ;
        return out ;
    }
} 
getSummaryText method invokes the chat request. It is as simple as that to integrate the chat services into the application.

我们将接下来看看嵌入模型集成。

LangChain4j:OpenAI 嵌入模型集成

嵌入模型集成相当简单,因为我们已经为聊天服务启用了 AiService。嵌入模型的使用方式如下所示:

@Service
public class OpenAIEmbeddingModelService {
    EmbeddingModel embeddingModel ;
    public OpenAIEmbeddingModelService(EmbeddingModel embeddingModel) {
        this.embeddingModel = embeddingModel;
    }
    Embedding generateEmbedding(String text) {
        Response<Embedding> response = embeddingModel.embed(text) ;
        return  response.content() ;
    }
} 

从代码中我们可以看到,这就像是将 EmbeddingModel 添加到类中并使用构造函数初始化它一样简单。当 Spring Boot 应用程序启动时,根据属性实例化并分配给这个变量的适当嵌入模型实现。此服务提供了一个为给定文本生成嵌入的方法。

现在我们已经查看所有定义的服务,让我们看看我们如何使用所有这些来构建增强客户交易图的程序。

LangChain4j:最终应用

对于最终应用,我们将构建一个 REST 端点以发出执行增强的请求。由于该过程本身可能需要时间,它被分为两部分:

  1. 发出请求以启动增强过程。这将返回一个请求 ID。

  2. 我们可以使用步骤 1 中返回的请求 ID 来检查请求的进度。

让我们先看看 REST 控制器以发出请求:

  1. 我们需要创建一个 REST 控制器来处理 HTTP 请求:

    @Configuration
    @EnableConfigurationProperties(RunConfiguration.class)
    @RestController
    public class LangchainGraphAugmentController { 
    
  2. 使用 Autowired 指令注入定义的各个服务:

    @Autowired
        private OpenAIEmbeddingModelService embeddingModelService ;
        @Autowired
        private Neo4jService neo4jService ;
        @Autowired
        private OpenAIChatService chatService ;
        @Autowired
        private RunConfiguration configuration ; 
    
  3. 定义全局变量以保存当前处理请求:

    private HashMap<String, IRequest> currentRequests = new HashMap<>() ; 
    
  4. 添加启动客户交易增强过程的方法。此方法接受季节的开始和结束值,并创建一个 ProcessRequest 对象。它启动一个请求并返回此请求的 UUID 的进程线程。我们保留 UUIDProcessRequest 映射,以便在请求时提供状态:

    @GetMapping("/augment/{startSeason}/{endSeason}")
        public String processAugment(
                @PathVariable (value="startSeason") String startSeason,
                @PathVariable (value="endSeason") String endSeason
        ) {
            String uuid = UUID.randomUUID().toString() ;
            ProcessRequest request = new ProcessRequest(
                    chatService,
                    embeddingModelService,
                    neo4jService,
                    configuration,
                    startSeason,
                    endSeason
            ) ;
            currentRequests.put(uuid, request) ;
            Thread t = new Thread(request) ;
            t.start();
            return uuid ;
        } 
    
  5. 添加启动文章文本增强过程的方法:

    @GetMapping("/augmentArticles")
        public String processAugmentArticles() {
            String uuid = UUID.randomUUID().toString() ;
            ProcessArticles request = new ProcessArticles(
                    embeddingModelService,
                    neo4jService,
                    configuration
            ) ;
            currentRequests.put(uuid, request) ;
            Thread t = new Thread(request) ;
            t.start();
            return uuid ;
        } 
    
  6. 添加一个获取指定请求 ID 状态的方法:

    @GetMapping("/augment/status/{requestId}")
        public String getStatus(
                @PathVariable (value="requestId") String requestId) {
            IRequest request = currentRequests.get(requestId) ;
            if( request != null ) {
                if( request.isComplete() ) {
                    currentRequests.remove(requestId) ;
                }
                return request.getCurStatus() ;
            } else {
                return "Request Not Found." ;
            }
        }
    } 
    

    注意

    图增强过程可能需要很长时间。特别是,使用 LLM 聊天 API 生成的摘要部分可能很耗时,并且增强所有符合要求的客户,比如 2019 年夏天的购买,可能需要相当多的时间。因此,包含完整增强的数据库转储仅覆盖大约 10,000 名客户。

现在,让我们看看处理请求的实现。这是我们将所有各种 API 绑定在一起以执行所需过程的地方:

  1. 我们需要定义一个实现 Runnable 接口的 ProcessRequest 类。由于这些请求是长时间运行的,我们将启动一个线程。当创建此请求时,聊天服务、嵌入模型服务、Neo4j 服务和其他参数作为输入传递。此类跟踪当前处理状态:

    public class ProcessRequest implements Runnable, IRequest {
        private OpenAIChatService chatService ;
        private OpenAIEmbeddingModelService embeddingModelService ;
        private Neo4jService neo4jService ;
        private RunConfiguration configuration ;
        private String startSeson ;
        private String endSeason ;
        private String curStatus = "0 %" ;
        private boolean isComplete = false ;
        public ProcessRequest(
                OpenAIChatService chatService,
                OpenAIEmbeddingModelService embeddingModelService,
                Neo4jService neo4jService,
                RunConfiguration configuration,
                String startSeson,
                String endSeason) {
            this.chatService = chatService;
            this.embeddingModelService = embeddingModelService;
            this.neo4jService = neo4jService;
            this.configuration = configuration ;
            this.startSeson = startSeson ;
            this.endSeason = endSeason ;
        }
        public String getCurStatus() {
            return curStatus ;
        }
        public boolean isComplete() {
            return isComplete;
        } 
    

run 方法实现了实际过程:

@Override
    public void run() {
        try { 
  1. 从 Neo4j 数据库中检索客户交易数据。输出是一个列表,其中每个记录包含起始季度的关系 ID 作为上下文,以及按购买顺序排列的文章描述:

    System.out.println("Retrieving Data from Graph");
                List<EncodeRequest> dbData = neo4jService.getDataFromDB(startSeson, endSeason);
                System.out.println("Retrieved Data from Graph");
                int i = 0;
                int processingSize = dbData.size();
                List<Map<String, Object>> embeddings = new ArrayList<>();
                for( EncodeRequest request: dbData ) { 
    

一旦收集到所需的结果批次大小,将数据保存到 Neo4j 数据库中:

if (i > 0 && i % configuration.getBatchSize() == 0) {
                    System.out.println("Saving Embeddings to Graph : " + i);
                    neo4jService.saveEmbeddings(embeddings);
                    embeddings.clear();
                    curStatus = ( ( i * 100.0 ) / processingSize ) + " %";
                }
                i++;
                Map<String, Object> embedMap = new HashMap<>(); 
  1. 通过传递从图中检索到的交易列表,从 LLM 聊天服务中检索客户购买摘要:

    String id = request.getId();
                    System.out.println("Retrieving Summary");
                    String summary = chatService.getSummaryText(request.getText());
                    System.out.println("Retrieving embedding"); 
    
  2. 对于从 LLM 聊天服务获得的摘要,利用嵌入服务创建嵌入:

    Embedding embedding = embeddingModelService.generateEmbedding(summary); 
    
  3. 将摘要和嵌入以及关系上下文 ID 保存到记录中,然后将其保存到批次中:

    embedMap.put("id", id);
                    embedMap.put("embedding", embedding.vector());
                    embedMap.put("summary", summary);
                    embeddings.add(embedMap);
                } 
    
  4. 如果批次中还有任何数据,将其保存到 Neo4j 数据库中:

    if( embeddings.size() > 0 ) {
                    System.out.println("Saving Embeddings to Graph");
                    neo4jService.saveEmbeddings(embeddings);
                    embeddings.clear();
                }
                curStatus = "100 %";
            }catch (Exception e) {
                e.printStackTrace();
            }
            isComplete = true;
        }
    } 
    

使用这种方法,我们可以增强图以执行理解客户购买行为的下一步,以便为他们提供更好的推荐。

以下代码可以处理文章增强。代码与ProcessRequest类非常相似。我们在这里只看差异:

public class ProcessArticles implements Runnable, IRequest { 

run方法从 Neo4j 读取数据,并在调用批嵌入请求之前将其分割成批次:

@Override
    public void run() {

            List<EncodeRequest> dbData = neo4jService.getArticlesFromDB();

            for( EncodeRequest request: dbData ) {
                if (i > 0 && i % batchSize == 0) { 

一旦收集到文章文本的批次,我们将将其传递给嵌入服务以获取嵌入。我们将生成的嵌入保存到 Neo4j 数据库中:

List<Embedding> embedList = embeddingModelService.generateEmbeddingBatch(inputData);
                                                            neo4jService.saveArticleEmbeddings(embeddings);
                                    }
                                i++;
            } 

为任何剩余的文章文本生成嵌入,并将其保存到 Neo4j 数据库中:

if( inputData.size() > 0 ) {
                                List<Embedding> embedList = embeddingModelService.generateEmbeddingBatch(inputData);
                 neo4jService.saveArticleEmbeddings(embeddings);
                            }
            curStatus = "100 %";
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
} 

操作流程与ProcessRequest类中的类似。虽然我们使用了单一请求模式进行季节购买嵌入,但对于文章嵌入,我们使用的是批处理模式。使用单一请求模式(使用 API),我们一次只能生成一个摘要。然而,使用批处理模式,生成嵌入要快得多。

如果你想要尝试该项目,可以从github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/tree/main/ch9/langchain_graphaugment下载最新项目,而不是从头开始构建。

要运行项目,你可以右键单击LangchainGraphaugmentApplication.java文件,并选择Run菜单选项。

注意

如果你对自定义运行选项和其他方面感兴趣,则可以使用 IDE 提供的Run/Debug配置。要了解更多关于这些方面的信息,请访问www.jetbrains.com/help/idea/run-debug-configuration-java-application.html

在下一节中,我们将探讨如何使用 Spring AI 构建相同的推荐引擎。

使用 Spring AI 构建你的推荐引擎

在本节中,我们将探讨利用 Spring AI 构建图增强应用程序。这种方法与我们使用 LangChain4j 构建的项目方法类似。我们将利用 GraphRAG 方法为满足我们要求的交易链生成嵌入。我们将从上一节下载的 ZIP 文件开始。我们需要解压我们下载的文件。一旦解压,我们将使用以下步骤将项目加载到 IntelliJ 平台。这与我们在上一节中做的是一样的。请按照使用 LangChain4j 构建推荐引擎部分开头列出的步骤导入项目。

与 LangChain4j 相比,更新 Spring AI 项目依赖项没有显著的步骤。让我们看看原因。

Spring AI:更新项目依赖项

与 LangChain4j 项目不同,我们不需要更新任何依赖项。我们能够从 Spring 启动器项目中添加所有必需的依赖项。接下来,我们将查看更新应用程序属性。

Spring AI:更新应用程序属性

在本节中,我们需要更新应用程序属性以利用 API。我们需要将此配置添加到项目中的application.properties文件。由于我们将使用 OpenAI LLM 作为聊天模型和嵌入,我们需要为此目的获取一个 API 密钥,这可以通过访问platform.openai.com/docs/overview来完成。

这些是我们需要添加的配置属性:

  • OpenAI 聊天模型集成 – 我们需要将此配置添加到application.properties。我们只需要添加 OpenAI API 密钥:

    # Open AI LLM Integration for Generating Summary using Chat Model.
    spring.ai.openai.api-key=`<OPEN_AI_KEY>` 
    
  • OpenAI 嵌入集成 – 我们需要将此配置添加到application.properties。我们不需要再次添加 OpenAI API 密钥,因为它使用与 LLM 聊天配置相同的配置:

    # Open AI LLM Integration for Generating Embeddings
    spring.ai.openai.embedding.options.model=text-embedding-3-large 
    
  • Neo4j 集成 – 我们将添加基本的 Neo4j 集成,而不是与 Neo4j 向量数据库相关的集成:

    # Neo4j Integration
    neo4j.uri=bolt://localhost:7687
    neo4j.user=neo4j
    neo4j.password=test1234
    neo4j.database=hmreco
    config.batchSize=5 
    

现在我们已经查看配置属性,让我们开始构建应用程序。我们将首先进行 Neo4j 数据库集成,然后添加聊天模型集成以总结交易并生成摘要的嵌入。最后,我们将查看构建 REST 端点以按需调用这些请求。

Spring AI:Neo4j 集成

我们首先考虑 Neo4j 集成,因为我们需要一种与数据库集成的手段来执行以下任务:

  1. 设置连接以能够执行读取和写入事务。

  2. 查看指定期间发生的交易文章。

  3. 一旦生成嵌入,就将其持久化。

这里的实现与上一节Langchain4J – Neo4j 集成部分中讨论的 LangChain4j 项目完全相同。我们将查看一个 OpenAI 聊天模型集成,可以为文章列表生成摘要。

Spring AI:OpenAI 聊天集成

要集成聊天功能,与 LangChain4j 略有不同。我们需要定义Service并初始化ChatClient。我们需要利用这个客户端并使用聊天 API 来发送请求。它没有像 LangChain4j 那样抽象。让我们看看这个服务:

@Service
public class OpenAIChatService {
    private final ChatClient chatClient; 

现在让我们看看集成 OpenAI 聊天步骤:

  1. 在 Spring AI 框架中,我们必须以不同的方式为 LLM 提供提示。在 LangChain4j 框架中,我们有一个单一的系统消息,定义了 LLM 扮演的角色、响应的目标以及作为消息参数的数据。在这里,我们必须将角色和目标拆分为系统提示模板,而data参数则传递到用户消息中。两种情况下结果相同:

    private final String SYSTEM_PROMPT_TEMPLATE = """ 
    
  2. 我们正在为 LLM 聊天引擎设置一个角色。这为引擎设置了处理数据的指导方针:

    ---Role---
    
                 You are an helpful assistant with expertise in fashion for a clothing company. 
    
  3. 我们在这里为 LLM 引擎设置一个目标,说明它应该如何处理数据。这描述了输入数据是什么以及它的结构:

    ---Goal---
    
                Your goal is to generate a summary of the products purchased by the customers and descriptions of each of the products.\s
                Your summary should contain two sections -\s
                Section 1 - Overall summary outlining the fashion preferences of the customer based on the purchases. Limit the summary to 3 sentences
                Section 2 - highlight 3-5 individual purchases.
    
                You should use the data provided in the section below as the primary context for generating the response.\s
                If you don't know the answer or if the input data tables do not contain sufficient information to provide an answer, just say so.\s
                Do not make anything up.
    
                 Data Description:
                - Each Customer has an ID. Customer ID is a numeric value.
                - Each Customer has purchased more than one clothing articles (products). Products have descriptions.
                - The order of the purchases is very important. You should take into account the order when generating the summary. 
    
  4. LLM 的响应指令给出了关于响应应该如何构建的指导:

    Response:
                ---
                # Overall Fashion Summary:
    
                \\n\\n
    
                # Individual Purchase Details:
        --
        """ ; 
    
  5. 数据作为用户消息传递。它定义了{text}变量,这是用方法接收的输入替换的属性:

    private final String userMessage = """
                Data:
                {text}
                """ ; 
    
  6. 我们需要使用ChatClient.Builder初始化聊天客户端,该客户端由 Spring 框架注入到构造函数中:

    public OpenAIChatService(ChatClient.Builder chatClientBuilder) {
            this.chatClient = chatClientBuilder.build();
        }
        public String getSummaryText(String input) 
    
  7. 我们可以看到,使用方法与 LangChain4j 框架不同。在这里,我们需要使用系统模板创建一个提示,用数据替换传递用户消息,并调用chatResponse方法:

    ChatResponse response = chatClient
                    .prompt()
                    .system(SYSTEM_PROMPT_TEMPLATE)
                    .user(p -> p.text(userMessage).param("data", input))
                    .call()
                    .chatResponse() ;
            return response.getResult().getOutput().getContent() ;
        }
    } 
    

我们将接下来看看嵌入模型集成。

Spring AI:OpenAI 嵌入模型集成

嵌入模型集成相当简单。我们可以使用Autowired来初始化嵌入模型实例。嵌入模型的使用方法如下所示:

@Service
public class OpenAIEmbeddingModelService {
    private EmbeddingModel embeddingModel ;
    @Autowired
    public OpenAIEmbeddingModelService(EmbeddingModel embeddingModel) {
        this.embeddingModel = embeddingModel;
    }
    float[] generateEmbedding(String text) {
        float[] response = embeddingModel.embed(text) ;
        return  response ;
    }
    List<float[]> generateEmbeddingBatch(List<String> textList) {
        List<float[]> responses = embeddingModel.embed(textList) ;
        return responses ;
    }
} 

从代码中我们可以看到,它就像将EmbeddingModel添加到类中并使用构造函数初始化它一样简单。当 Spring Boot 应用启动时,根据属性实例化并分配给这个变量的适当的嵌入模型实现。此服务提供了一个方法来为给定的文本生成嵌入。

现在我们已经查看所有定义的服务,让我们看看如何使用所有这些来构建一个图增强应用。

Spring AI:最终应用

应用流程基本上与我们在LangChain4j – 最终应用部分讨论的 LangChain4j 应用相同。代码相似,所以我们不会在这里添加代码。唯一的区别将是 Java 包名。为了未来的参考,让我们看看应用流程。

构建的 REST 端点用于发出执行增强的请求。由于这个过程本身可能需要时间,这个过程被分为两部分:

  1. 发起一个请求以启动增强过程。这将返回一个请求 ID。

  2. 使用步骤 1 中返回的请求 ID 来检查请求的进度。

第一步启动一个线程并开始处理整个数据。请求过程遵循以下步骤:

  1. 以起始季度的关系 ID 作为上下文,按购买顺序检索文章的描述。我们以记录列表的形式返回响应。

  2. 对于我们从 Neo4j 检索的每个记录,我们执行以下步骤:

    1. 执行聊天请求以生成摘要。

    2. 对于聊天请求返回的摘要,使用 LLM 嵌入 API 生成嵌入。

    3. 将关系 ID、摘要和嵌入保存到映射中,以构建一个批次。

    4. 一旦批次大小达到配置中指定的尺寸,就将摘要和嵌入写入由关系 ID 标识的关系中。

使用这种方法,我们可以增强图以执行下一步来理解客户购买行为,以便为客户提供更好的推荐。

如果你想玩这个项目,你可以从github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/tree/main/ch9/springai_graphaugment下载最新项目,而不是从头开始构建。

要运行项目,你可以右键单击SpringaiGraphAugmentApplication.java文件,并选择运行菜单选项。

注意

如果你感兴趣于自定义运行选项和其他方面,那么你可以使用 IDE 提供的运行/调试配置。要了解更多关于这些方面的信息,请访问www.jetbrains.com/help/idea/run-debug-configuration-java-application.html

现在我们来看看我们如何使用我们构建的这个应用程序来增强图,并看看我们如何从中提供推荐。

微调你的推荐系统

现在项目已经准备好了,我们既可以直接在 IDE 中运行应用程序,也可以构建一个可运行的 JAR 文件。在这里,我们将直接从 IDE 运行它。我们将使用 LangChain4j 应用程序进行测试。Spring AI 应用程序将遵循相同的原则。我们将从上一章创建的数据库开始。如果你不想从头开始,你可以从packt-neo4j-powered-applications.s3.us-east-1.amazonaws.com/Building+Neo4j-Powered+Applications+with+LLMs+Database+Dump+files.zip下载数据库转储文件,并从中创建一个数据库。

你可以双击 LangchainGraphaugmentApplication.j``ava 文件将其加载到 IDE 中。一旦加载,你可以在类名上右键单击以运行应用程序。图 9.6 展示了如何进行此操作。

图 9.6 — 从 IDE 运行应用程序

图 9.6 — 从 IDE 运行应用程序

一旦你右键单击了类名,点击 运行 菜单项以启动应用程序。一旦应用程序准备就绪,你应该在 IDE 控制台中看到以下内容:

2024-12-12T14:52:30.075+05:30  INFO 5296 --- [langchain_graphaugment] [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1271 ms
2024-12-12T14:52:31.347+05:30  INFO 5296 --- [langchain_graphaugment] [           main] o.neo4j.driver.internal.DriverFactory    : Direct driver instance 1567253519 created for server address localhost:7687
2024-12-12T14:52:31.388+05:30  INFO 5296 --- [langchain_graphaugment] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2024-12-12T14:52:31.398+05:30  INFO 5296 --- [langchain_graphaugment] [           main] g.h.l.g.LangchainGraphaugmentApplication : Started LangchainGraphaugmentApplication in 3.146 seconds (process running for 3.746) 

一旦应用程序启动并运行,我们就可以打开浏览器并输入 URL http://localhost:8080/augment/SUMMER_2019/FALL_2019 以开始对客户的 SUMMER_2019 购买进行增强过程。当我们发出此请求时,我们会得到一个 UUID,例如 aff867bd-08fb-42fb-8a27-3917e0ce83d1 作为响应。在过程运行期间,我们可以在浏览器中输入 URL http://localhost:8080/augment/status/aff867bd-08fb-42fb-8a27-3917e0ce83d1 来查询当前的完成百分比。

注意

注意,之前提到的 aff867bd-08fb-42fb-8a27-3917e0ce83d1UUID 值是动态的。不能保证你会得到与前面文本中显示的相同的 UUID。这个 UUID 是针对本例中的运行特定的。你需要查看你运行请求返回的 UUID 并使用它来检查状态。

生成摘要和嵌入将需要时间。一旦这个过程完成,我们应该在文章上创建嵌入。过程与上一步类似。我们需要在浏览器中输入 http://localhost:8080/augmentArticles。它也会提供一个 UUID 作为响应。我们需要持续检查完成百分比,直到完成。

如果你不想等待整个过程的完成,你可以从 packt-neo4j-powered-applications.s3.us-east-1.amazonaws.com/Building+Neo4j-Powered+Applications+with+LLMs+Database+Dump+files.zip 下载数据库。

现在,我们已经完成了增强,让我们看看这些嵌入的效果以及它们如何帮助我们提供推荐。为此,我们可以通过为创建的嵌入创建向量索引来进一步增强图。

你可以执行这个 Cypher 查询来为文章上的嵌入创建向量索引:

CREATE VECTOR INDEX `article-embeddings` IF NOT EXISTS
FOR (a:Article)
ON a.embedding
OPTIONS { indexConfig: {
 `vector.dimensions`: 3072,
 `vector.similarity_function`: 'cosine'
}} 

这将在 Article 节点上创建一个名为 article-embeddings 的向量索引。

以下 Cypher 代码可以用来在 2019 年夏季购买的嵌入上创建向量索引:

CREATE VECTOR INDEX `summer-2019-embeddings` IF NOT EXISTS
FOR ()-[r:SUMMER_2019]->() ON (r.embedding)
OPTIONS { indexConfig: {
 `vector.dimensions`: 3072,
 `vector.similarity_function`: 'cosine'
}} 

这将在 SUMMER_2019 关系上创建一个名为 summer-2019-embeddings 的向量索引。

让我们先看看如何使用 Article 向量索引。这个 Cypher 试图找到 ID 为 0748579001 的文章的前五条匹配项:

MATCH (a:Article {id:'0748579001'})
WITH a
CALL db.index.vector.queryNodes('article-embeddings', 5, a.embedding)
YIELD node, score
RETURN score, node.id as id, node.desc as desc 

从结果(图 9.7)中,我们可以看到第一个匹配项是最好的匹配,正是我们寻找的Article节点:

评分 ID 描述
1.0 “0748579001” “踝部长款 A 字沙滩裙,采用轻盈的图案编织,深 V 领,金色珠片装饰,长灯笼袖,袖口有弹性装饰和装饰性的抽绳。无衬里。”
0.882 “0748033001” “长款沙滩裙,采用轻盈的皱褶雪纺,窄肩带,颈部后面有开口。无衬里。”
0.873 “0748582008” “短款沙滩裙,采用轻盈的棉质编织,带有法式刺绣。前后 V 领,带有钩针蕾丝装饰,水平,颈部后面有流苏带,落肩,短袖。腰部有缝线,裙摆微微张开。无衬里。”
0.866 “0748025004” “前襟有纽扣,侧面有高开叉的沙笼。”
0.866 “0747737004” “轻盈编织的沙笼,尺寸 130x150 厘米。”

图 9.7 — 给定文章的相似文章

此外,我们还可以看到彼此不太相似的物品具有较低的评分值。从这个结果来看,让我们选取 ID 为0748582008Article,看看我们能找到什么:

MATCH (a:Article {id:'0748582008'})
WITH a
CALL db.index.vector.queryNodes('article-embeddings', 5, a.embedding)
YIELD node, score
RETURN score, node.id as id, node.desc as desc 

当我们运行 Cypher 查询时,我们可以看到以下结果:

评分 ID 描述
1.0 “0748582008” “短款沙滩裙,采用轻盈的棉质编织,带有法式刺绣。前后 V 领,带有钩针蕾丝装饰,水平,颈部后面有流苏带,落肩,短袖。腰部有缝线,裙摆微微张开。无衬里。”
0.969 “0748582001” “短款沙滩裙,采用轻盈的棉质编织,带有法式刺绣。前后 V 领,带有钩针蕾丝装饰,水平,颈部后面有流苏带,落肩,短袖。腰部有缝线,裙摆微微张开。无衬里。”
0.893 “0848082001” “短款沙滩开衫,采用轻盈的蕾丝装饰编织。短袖,宽袖,腰部有抽绳。”
0.884 “0854784001” “短款沙滩裙,采用轻盈的棉质编织,含有闪亮线。圆领,V 领开口,前面有窄带,落肩,长灯笼袖,袖口有窄扣。下摆有褶皱层,增加宽度。”
0.884 “0850893001” “开襟长款蕾丝开衫,开口处有钩针装饰,腰部有抽绳和扭曲的腰带,长袖。袖口和下摆有波浪形装饰。”

图 9.8 — 给定文章的相似文章

从结果来看,当评分接近0.9时,文章非常相似。我们可以利用这些信息,根据客户已购买的物品提供相似文章作为推荐。

现在,让我们看看 ID 以92f0结尾的客户的夏季购买行为。我们可以称这位客户为 A:

MATCH (c:Customer)-[r:SUMMER_2019]->() WHERE c.id='0002b7a7ab270a638fcb2eb5899c58696db24d9d954ddb43683dd6b0ffa292f0'
WITH r
CALL db.index.vector.queryRelationships('summer-2019-embeddings', 5, r.embedding)
YIELD relationship, score
MATCH (oc)-[relationship]->()
WITH oc, score, relationship
WITH oc, score, split(relationship.summary, '\n') as s
WITH oc, score, CASE when s[2] <> '' THEN s[2] ELSE s[3] end as desc
WITH score, oc.id as id, desc
RETURN round(score,3) as score, substring(id,0,4)+".."+substring(id,size(id)-4) as id, desc 

当我们运行这个 Cypher 查询时,我们可以看到以下结果:

Score Id Desc
1.0 “0002..92f0” “客户表现出对鲜艳色彩和舒适休闲风格的强烈偏好,尤其是在泳装和牛仔布方面。他们的购买表明对既有趣又实用的服装的喜爱,适合海滩出行和日常穿着。泳装、短裤和休闲上衣的混合表明一个注重风格和舒适的多功能衣橱。”
0.968 “044d..d47e” “客户表现出对泳装的强烈偏好,尤其是在浅橙色和深红色等鲜艳色彩中,表明一种有趣且活泼的风格。他们的选择也反映了倾向于高腰设计和支撑性上衣,表明对舒适和时尚的渴望。此外,购买多功能连衣裙和定制夹克表明对时尚且实用的日常穿着的欣赏。”
0.967 “07fe..a87f” “客户表现出对泳装的强烈偏好,尤其是在橙色和黑色等鲜艳色彩中,表明对海滩和泳池边活动的喜爱。同时,对基本服装必备品,如无袖上衣和短裤的明显倾向,表明对舒适且时尚休闲装的渴望。泳装和基本服装的混合反映了适合休闲和日常穿着的多功能时尚感。”
0.966 “0247..74b3” “客户表现出对鲜艳色彩和适合各种场合的服装的偏好,包括休闲装和泳装。泳装的重复购买表明对海滩或泳池边活动的浓厚兴趣。此外,包括连衣裙和配饰表明对时尚且舒适的服装组合的渴望。”
0.965 “0686..5220” “客户表现出对鲜艳色彩的强烈偏好,尤其是橙色和白色,这在他们的泳装和休闲装选择中可见。他们的购买表明舒适和风格的结合,注重适合各种场合的多功能单品。包括泳装和日常服装表明一种既欣赏休闲又欣赏时尚的生活方式。”

图 9.9 — 与给定客户相似的客户的购买摘要

从基本总结中,我们可以看到客户的购买行为相当相似。让我们从这个列表中挑选另一个客户(比如客户 B)来查看当我们运行相同的查询时是否返回了相同的客户。我们将选择以74b3结尾的客户 ID:

MATCH (c:Customer)-[r:SUMMER_2019]->() WHERE c.id=' 0247b7b564909181b2e552fe3d5cec01056ebc1b3d61d38f1ff0658db69174b3'
WITH r
CALL db.index.vector.queryRelationships('summer-2019-embeddings', 5, r.embedding)
YIELD relationship, score
MATCH (oc)-[relationship]->()
WITH oc, score, relationship
WITH oc, score, split(relationship.summary, '\n') as s
WITH oc, score, CASE when s[2] <> '' THEN s[2] ELSE s[3] end as desc
WITH score, oc.id as id, desc
RETURN round(score,3) as score, substring(id,0,4)+".."+substring(id,size(id)-4) as id, desc 

让我们看看运行此查询的结果:

Score Id Desc
1.0 “0247..74b3” “客户表现出对鲜艳色彩和适合各种场合的服装的偏好,包括休闲装和泳装。泳装的重复购买表明对海滩或泳池边活动的浓厚兴趣。此外,包括连衣裙和配饰表明对时尚且舒适的服装组合的渴望。”
0.968 “05de..29df” “客户的时尚偏好表明他们强烈倾向于泳装和连衣裙,尤其是在鲜艳且活泼的颜色如粉色、橙色和蓝色。泳装和连衣裙的选择暗示了一种多变的风格,既包括休闲沙滩装也包括时尚的日常装。此外,反复购买高腰比基尼短裤展示了他们对于既美观又实用的泳装选择的偏好。”
0.967 “0322..3e92” “客户对泳装表现出强烈的偏好,这从他们多次购买比基尼上衣和短裤中可以看出,展示了他们对于时尚沙滩装的渴望。此外,连衣裙和衬衫的选择反映了他们倾向于时尚且舒适的日常装。鲜艳的颜色和独特的设计元素表明他们喜欢现代且引人注目的单品。”
0.966 “0002..92f0” “客户对鲜艳的颜色和舒适、休闲的风格表现出强烈的偏好,尤其是在泳装和牛仔装上。他们的购买表明他们喜欢既有趣又实用的服装,适合海滩出行和日常穿着。泳装、短裤和休闲上衣的混合选择表明他们有一个注重风格和舒适的多变衣橱。”
0.965 “0863..c454” “客户对泳装表现出强烈的偏好,尤其是在鲜艳的颜色如深红和橙色,这表明他们喜欢沙滩装和夏季风格。此外,他们在日常服装,如宽松连衣裙和牛仔裙的选择,表明他们倾向于舒适且时尚的休闲装。重复购买特定商品也反映了他们在时尚选择上对一致性和可靠性的渴望。”

图 9.10 — 与给定客户相似的其它客户的购买摘要

我们可以看到,客户 B 的前五名匹配与客户 A 的非常不同,尽管客户 B 的购买摘要出现在客户 A 的前五名相似客户购买中。

我们可以使用这种方法根据客户购买行为推荐商品。我们正在捕捉购买顺序,但如何通过嵌入来捕捉这些购买的摘要定义了谁将被视为相似客户。让我们看看这个查询会是什么样子:

MATCH (c:Customer)-[r:SUMMER_2019]->() WHERE c.id='0247b7b564909181b2e552fe3d5cec01056ebc1b3d61d38f1ff0658db69174b3'
WITH c, r 

我们希望根据购买行为找到与这位客户相似的其它客户。我们将使用向量索引来获取前五名相似客户:

CALL db.index.vector.queryRelationships('summer-2019-embeddings', 5, r.embedding)
YIELD relationship, score
MATCH (oc)-[relationship]->()
WITH c, collect(oc) as others
CALL { 

收集客户的购买商品:

WITH c
    MATCH (c)-[:SUMMER_2019]->(start)
    MATCH (c)-[:FALL_2019]->(end)
    WITH start, end
    MATCH p=(start)-[:NEXT*]->(end)
    WITH p
    WITH nodes(p) as txns
    UNWIND txns as tx
    MATCH (tx)-[:HAS_ARTICLE]->(a)
    RETURN collect(a) as customerPurchases
}
WITH others, customerPurchases
CALL { 

收集与第一位客户相似的其它客户的购买商品:

WITH others
    UNWIND others as a
    MATCH (a:Customer)-[:SUMMER_2019]->(start)
    MATCH (a)-[:FALL_2019]->(end)
    WITH start, end
    MATCH p=(start)-[:NEXT*]->(end)
    WITH nodes(p) as txns
    UNWIND txns as tx
    MATCH (tx)-[:HAS_ARTICLE]->(a)
    WITH DISTINCT a
    RETURN collect(a) as otherPurchases
}
WITH customerPurchases, otherPurchases 

从相似客户的购买商品中移除原始客户的购买商品:

WITH apoc.coll.subtract(otherPurchases, customerPurchases) as others
UNWIND others as other
RETURN other.id as id, other.desc as desc
LIMIT 10 

此 Cypher 首先收集客户的购买,找到具有相似购买行为的其他客户,检索那些客户的购买,并向原始客户推荐 10 件他们之前未购买的商品。此查询的输出如下:

Id Desc
“0471714036” “棉质编织的膝盖长度短裤,有纽扣飞翼,侧口袋和带纽扣的翻盖后口袋。”
“0699923078” “柔软的印花棉质 T 恤。”
“0786663001” “短款无肩带连衣裙,采用轻盈的缎子编织,顶部有弹性和小褶边装饰。长袖,袖口有弹性,腰部有弹性接缝,下摆有荷叶边。 jersey 衬里。”
“0728473001” “三角形比基尼上衣,有激光切割的波浪边缘和轻微填充的罩杯,可拆卸的填充物。窄的可调节肩带,可以以不同的方式固定,背部有可调节的金属钩扣。”
“0689040001” “全内衬比基尼下装,中腰,两侧宽,一侧宽腰带,后部中等覆盖。”
“0736046001” “不同尺寸的金属圈耳环,三个带有各种设计的吊坠。直径 1-2 厘米。”
“0713200006” “全内衬,腰高比基尼下装,两侧宽,后部中等覆盖。”
“0674606026” “短款 A 字裙,高腰,前面有纽扣。”
“0562245064” “五口袋牛仔裤,采用水洗超弹牛仔布,常规腰围,拉链和纽扣,细腿。”
“0557247005” “宽松的上衣,采用耐用的运动衫面料,肩部下坠,领口、袖口和下摆有罗纹。内部柔软刷毛。”

图 9.11 — 基于相似客户购买行为推荐的客户推荐

通过遵循解释的步骤微调您的图,我们现在可以通过找到相似客户及其购买或基于客户购买的商品的相似文章来根据客户购买行为提供推荐。这种方法简单且效果良好。但我们在确定相似客户是谁等问题上。我们可能想使用图数据科学算法或机器学习来更好地分组客户,以便我们可以提供更好的推荐。我们将在下一章中探讨这一方面。

摘要

在本章中,我们探讨了如何通过利用 LangChain4j 和 Spring AI 构建智能应用。我们使用这些应用来增强上一章中加载的 H&M 交易图,通过利用 LLM 聊天和嵌入功能。一旦图被增强,我们进一步通过利用向量索引增强了图,并看到了这些索引如何帮助我们根据客户的购买行为找到相似的文章或客户。

在下一章中,我们将步入图数据科学算法,看看我们如何进一步优化这些推荐。

第十章:创建智能推荐系统

现在我们已经将数据加载到图中,并查看如何使用 Langchain4j 和 Spring AI 以及生成推荐来增强图,我们将探讨如何进一步利用图形数据科学GDS算法和机器学习来改进推荐。我们将回顾 Neo4j 提供的 GDS 算法,以超越我们在上一章中创建的推荐系统。我们还将学习如何使用 GDS 算法构建协同过滤以及基于内容的推荐方法。我们还将查看算法运行后的结果,以审查我们的方法是否有效,以及我们是否走上了构建更好推荐系统的正确道路。我们将试图理解为什么这些算法比我们在上一章中实现的方法更好。

在本章中,我们将涵盖以下主要主题:

  • 使用 GDS 算法改进推荐

  • 理解社区的力量

  • 结合协同过滤和基于内容的策略

技术要求

我们将使用 Java IDE 环境来与 Langchain4j 和 Spring AI 项目一起工作。您需要安装这些项目并了解如何使用它们。

设置环境

我们将从上一章中构建的图数据库开始。代码与 Neo4j 5.21.2版本的数据库进行了测试。

要设置环境,您需要

  • 安装了以下插件的 Neo4j Desktop:

    • APOC 插件 – 5.21.2

    • 图形数据科学库 – 2.9.0

图 10.1 展示了如何为数据库管理系统安装这些插件。

图 10.1 — 在 Neo4j Desktop 中安装插件

图 10.1 — 在 Neo4j Desktop 中安装插件

当您在 Neo4j Desktop 中选择数据库管理系统时,在右侧,它会显示其详细信息。单击插件选项卡并选择插件。一旦展开,单击安装和重启按钮。

接下来,我们将查看本章所需的数据库转储。

准备数据库

在开始之前,您需要创建社区。相似性和社区检测算法的完成需要一些时间。因此,建议从packt-neo4j-powered-applications.s3.us-east-1.amazonaws.com/hmreco_post_augment_with_summary_communities.dump 下载数据库转储,并使用它来在 Neo4j Desktop 中创建数据库。您可以使用以下说明将此转储加载到 Neo4j Desktop 中:neo4j.com/docs/desktop-manual/current/operations/create-from-dump/

此数据库转储已创建所有 SUMMER_2019_SIMILAR 关系,并已识别社区。

让我们从使用 GDS 算法来增强我们的知识图,以改善推荐开始。

使用 GDS 算法改进推荐

在本节中,我们将探讨如何进一步优化图,以获得更多关于图的洞察,从而构建一个更好的推荐系统。我们将从上一章中创建的图数据库开始。为了参考,您可以从packt-neo4j-powered-applications.s3.us-east-1.amazonaws.com/Building+Neo4j-Powered+Applications+with+LLMs+Database+Dump+files.zip下载它。

Neo4j GDS 算法(neo4j.com/docs/graph-data-science/current/)将帮助我们增强图。此过程包括以下步骤:

  1. 根据我们创建的嵌入计算客户之间的相似性,并在这些客户之间创建相似关系。为此,我们将利用K-Nearest NeighborsKNN)算法(neo4j.com/docs/graph-data-science/current/algorithms/knn/)。

  2. 运行社区检测算法,根据相似关系将客户分组。为此,我们将利用Louvain 社区检测算法(neo4j.com/docs/graph-data-science/current/algorithms/louvain/)。

首先,我们将利用 KNN 算法来增强图。

使用 KNN 算法计算相似性

K-Nearest NeighborsKNN)算法收集节点对,计算节点与其邻居之间的距离值,并在节点与其前 K 个邻居之间创建关系。距离是根据节点属性计算的。我们需要提供一个同质图来使用此算法。当所有节点和关系都相同时,它被称为同质图。我们提供给 KNN 算法的节点对不需要任何节点标签或关系类型。KNN 算法只需要连接的节点对,以及一个可选的属性,该属性可以用作它们之间关系的上下文。您可以在neo4j.com/docs/graph-data-science/current/algorithms/knn上了解更多信息。

要使用此算法,我们需要遵循以下过程:

  1. 将感兴趣的图项目应用于算法。

  2. 使用适当的配置调用算法。此算法有三种模式:

    • Stream:此模式将算法应用于内存中的图,并流式传输结果。您可以使用流模式来检查结果,看看是否符合我们的期望。

    • 变异:这将在内存图中应用算法并将数据写回内存图。实际数据库不会改变。当我们要更新内存图并希望以后用于不同目的时,使用变异方法。

    • 写入:这将在内存图中应用算法并将关系写回实际数据库。当我们对过程有信心并希望立即将结果写回图时,使用此模式。

我们将从图投影开始。由于我们在 SUMMER_2019 关系中写入了嵌入,我们将使用这些嵌入进行处理。

此 Cypher 将内存中的图投影以调用此算法:

MATCH (c:Customer)-[sr:SUMMER_2019]->()
  WHERE sr.embedding is not null
  RETURN gds.graph.project(
    'myGraph',
    c,
    null,
    {
      sourceNodeProperties: sr { .embedding },
      targetNodeProperties: {}
    }
  ) 

通常,我们将使用节点上的属性来构建投影。这里,我们将嵌入值写入关系,因为此嵌入是为了表示 2019 年夏季的购买而创建的。如果我们将其写入 Customer 节点,那么如果我们想了解各种场景下的客户购买行为,我们就需要使用一些巧妙的命名来只将这些写入 Customer 节点。通过将嵌入写入关系,我们在图中保留了嵌入的上下文。

从前面的 Cypher 可以看出,我们正在从关系中检索嵌入并将其作为投影中的源节点属性添加。现在,让我们调用算法将类似的关系写回图中。

此 Cypher 调用算法将结果写回:

CALL gds.knn.write('myGraph', {
    writeRelationshipType: 'SUMMER_2019_SIMILAR',
    writeProperty: 'score',
    topK: 5,
    nodeProperties: ['embedding'],
    similarityCutoff: 0.9
})
YIELD nodesCompared, relationshipsWritten 

从 Cypher 中,我们可以看到我们正在调用算法的 write 模式。该算法将通过在嵌入上使用 余弦相似度 来计算客户之间的相似度,阈值分数为 0.9,按相似度分数的顺序选择前 5 个邻居,并在这些客户之间写入一个名为 SUMMER_2019_SIMILAR 的关系。

注意

余弦相似度计算两个向量之间的角度。因此,如果向量彼此更远,则相似度值将接近 0。如果它们彼此更近,则相似度值将接近 1。如果您想了解更多信息,可以阅读en.wikipedia.org/wiki/Cosine_similarity

相似度分数可以在 0 和 1 之间。如果两个实体之间的分数接近 0,则它们彼此不相似。如果接近 1,则它们更相似。我们使用 0.9 作为相似度阈值,因为我们基于生成的摘要文本生成嵌入 - 客户可能具有更高的相似度分数。我们不希望客户之间存在相似关系,因为有一些关键词是相似的。我们将在后续步骤中验证此假设。我们将限制自己只考虑前五个(k =5)相似客户行为,以获得更接近的推荐。

注意

请注意,KNN 算法默认是一个非确定性算法。这意味着不同的运行可能会得到不同的结果。您可以在neo4j.com/docs/graph-data-science/2.14/algorithms/knn/了解更多信息。如果您想要确定性结果,则必须确保并发参数设置为 1,并且显式设置randomSeed参数。

一旦调用算法,我们需要删除图投影。否则,它将继续使用数据库服务器中的内存:

CALL gds.graph.drop('myGraph') 

此 Cypher 将删除图并清除图投影使用的内存。

我们将基于SUMMER_2019_SIMILAR关系查看社区检测。

为了做到这一点,我们将利用 Louvain 社区检测算法,这是最受欢迎的社区检测算法。

使用 Louvain 算法检测社区

Louvain 算法依赖于实体和组之间的相似度分数,并将它们分组到社区中。它通过查看邻居及其关系,将大型、网络化数据分组到更小、更紧密的社区中。这个层次聚类算法递归地将社区合并成一个节点,并在压缩图上执行模块度聚类。它为每个社区最大化模块度分数,评估社区内节点之间的连接密度与它们在随机网络中连接密度的差异。我们希望通过更自动化的方式将客户分组到更紧密的群体中,以提供更广泛的推荐。您可以在neo4j.com/docs/graph-data-science/current/algorithms/louvain/了解更多信息。

这种方法与我们调用 KNN 算法的方式非常相似。要使用此算法,我们需要遵循以下过程。

  1. 将感兴趣的图项目应用于算法。

  2. 使用适当的配置调用算法。此算法有三种模式。

    • 流式处理:将算法应用于内存中的图,并将结果流式传输。您可以使用流式处理模式来检查结果,看看是否符合我们的预期。

    • 变异:将算法应用于内存中的图,并将数据写回内存中的图。实际数据库没有改变。当我们要更新内存中的图并希望以后用于不同目的时,使用mutate方法。

    • 写入:将算法应用于内存中的图,并将关系写回实际数据库。当我们对过程有信心并希望立即将结果写回图时,使用此模式。

让我们从图投影开始。我们将使用SUMMER_2019_SIMILAR关系及其上保存的score值来执行社区检测:

MATCH (source:Customer)-[r:SUMMER_2019_SIMILAR]->(target)
RETURN gds.graph.project(
  'communityGraph',
  source,
  target,
  {
    relationshipProperties: r { .score }
  },
  { undirectedRelationshipTypes: ['*'] }
) 

之前的 Cypher 创建了一个名为communityGraph的内存投影。它使用源节点、目标节点和SUMMER_2019_SIMILAR关系上的分数来构建投影。

一旦构建了投影,我们就可以使用此 Cypher 来执行社区检测:

CALL gds.louvain.write('communityGraph', { writeProperty: 'summer_2019_community' })
YIELD communityCount, modularity, modularities 

此 Cypher 执行社区检测并将社区 ID 作为名为summer_2019_community的属性写回Customer节点。

一旦社区检测完成,我们需要删除图投影。

CALL gds.graph.drop('communityGraph') 

此 Cypher 将删除图并清除图投影使用的内存。

我们可以使用此 Cypher 来检查创建了多少社区:

MATCH (c:Customer) WHERE c.summer_2019_community IS NOT NULL
RETURN c.summer_2019_community, COUNT(c) as count
ORDER BY count DESC 

此 Cypher 按社区中客户的数量顺序显示所有社区。响应将如图图 10.2所示。

图 10.2 — 客户数量社区

图 10.2 — 客户数量社区

注意

请注意,Louvain 社区检测算法默认是非确定性算法。这意味着不同的运行可能会得到不同的结果。您可以在neo4j.com/docs/graph-data-science/2.14/algorithms/louvain/了解更多信息。

现在我们已经建立了社区,让我们来看看生成的社区。在下一节中,我们将检查其中的一些社区,以观察它们是否根据客户的购买行为对客户进行分组。

理解社区的力量

在之前的第九章中,我们探讨了使用向量相似性查找相似客户以及如何为客户提供推荐。让我们回顾图 9.10图 9.11图 9.10显示了与特定客户相似的客户的购买历史。图 9.11显示了基于相似客户购买的客户推荐。购买历史和客户推荐是我们之前在微调您的推荐部分中进行的 Cypher 查询的结果,以了解向量相似性的使用。

在本节中,我们将更深入地了解社区,并探讨为什么它们可能比利用简单的向量相似性来查找相似客户更有优势。

注意

以下 Cypher 与技术部分中共享的数据库相关。

要求部分。

从我们在上一节中运行的 Cypher,使用 Louvain 算法检测社区,让我们选择一个包含大量客户的社区。我们将查看 ID 为133的社区,其中大约有1,242名客户。

以下 Cypher 显示前五个客户的客户购买摘要,不包括文章详情:

MATCH (c:Customer)-[r:SUMMER_2019]->()
WHERE c.summer_2019_community=133
WITH split(r.summary, '\n') AS s
WITH CASE WHEN s[2] <> '' THEN s[2] ELSE s[3] END AS d
return d LIMIT 5 

当我们运行社区检测时,每次运行生成的社区 ID 可能都不同。所以,如果你已经运行了自己的 Cypher 脚本来创建客户社区,你需要查看这些社区并使用那些 ID 来验证数据。

当我们运行前面的 Cypher 脚本时,输出看起来像这样:

The customer demonstrates a preference for stylish and modern pieces, particularly favoring dresses and lingerie that offer both comfort and elegance. The consistent choice of midi and short dresses paired with a variety of non-wired bras suggests a desire for chic yet relaxed fashion options. Additionally, the inclusion of tailored blouses and fashionable outerwear indicates an appreciation for versatile styles suitable for various occasions.
The customer exhibits a preference for comfortable yet stylish clothing, favoring light and soft colors such as light pink and light blue. Their purchases reflect a blend of casual and lingerie items, indicating a focus on both everyday wear and intimate apparel. The selection features a mix of high-waisted denim and lace detailing, suggesting an appreciation for modern, flattering silhouettes.
The customer demonstrates a strong preference for versatile and stylish pieces, favoring bold colors like pink and orange while incorporating comfortable fabrics such as jersey and cotton. Their purchases include a mix of casual wear, activewear, and lingerie, suggesting a balanced lifestyle that values both comfort and aesthetics. The frequent selection of shorts and dresses indicates a preference for easy-to-wear, fashionable items suitable for various occasions.
The customer demonstrates a preference for comfortable and stylish lingerie, favoring soft materials with unique design details such as lace trims and laser-cut edges. Additionally, their choice of everyday wear leans towards light and airy fabrics, showcasing a blend of casual and chic styles suitable for various occasions. The color palette reflects a soft and neutral aesthetic, with light pinks, beiges, and whites dominating their selections.
The customer exhibits a preference for comfortable and functional clothing, particularly in the realm of casual and lingerie wear. There is a clear inclination towards basic styles in neutral colors such as black and white, complemented by playful accents in perceived colors like orange and pink. The focus on versatile pieces suggests a desire for practicality combined with style. 

从摘要描述中,我们可以看出这个社区的顾客更喜欢一起购买休闲和内衣服装。

让我们看看这些客户购买的文章:

MATCH (c:Customer)
WHERE c.summer_2019_community=133
WITH c LIMIT 10
MATCH (c)-[:SUMMER_2019]->(start)
MATCH (c)-[:FALL_2019]->(end)
WITH c, start, end
CALL {
    WITH start, end
    MATCH p=(start)-[:NEXT*]->(end)
    WITH nodes(p) as nodes
    UNWIND nodes as n
    MATCH (n)-[:HAS_ARTICLE]->(a)
    WITH a LIMIT 3
    RETURN collect(a.desc) as articles
}
WITH c, articles
RETURN articles 

这个 Cypher 语句给出了以下输出:

["Calf-length dress in a crinkled weave with a V-neck, wrapover front with ties at the waist and short sleeves with a slit and ties. Unlined.", "Calf-length dress in a crinkled weave with a V-neck, wrapover front with ties at the waist and short sleeves with a slit and ties. Unlined.", "Soft, non-wired bras in cotton jersey with moulded, padded triangular cups for a larger bust and fuller cleavage. Adjustable shoulder straps that cross at the back and lace at the hem. No fasteners."]
["High-waisted jeans in washed superstretch denim with hard-worn details, a zip fly and button, back pockets and skinny legs.", "Lace push-up bra with underwired, moulded, padded cups for a larger bust and fuller cleavage. Adjustable shoulder straps and a hook-and-eye fastening at the back.", "Lace push-up bra with underwired, moulded, padded cups for a larger bust and fuller cleavage. Adjustable shoulder straps and a hook-and-eye fastening at the back."]
["Vest top in cotton jersey with a print motif.", "Soft, non-wired bras in microfibre with padded cups that shape the bust and provide good support. Adjustable shoulder straps and a hook-and-eye fastening at the back.", "Chino shorts in washed cotton poplin with a zip fly, side pockets, welt back pockets with a button and legs with creases."]
["Microfibre Brazilian briefs with laser-cut edges, a low waist, lined gusset, wide sides and half-string back.", "Hipster briefs in microfibre with lace trims, a low waist, lined gusset and cutaway coverage at the back.", "Blouse in an airy weave with a V-neck, covered buttons down the front, short dolman sleeves and a tie detail at the hem."]
["Round-necked T-shirt in soft cotton jersey.", "Round-necked T-shirt in soft cotton jersey.", "Thong briefs in cotton jersey and lace with a low waist, lined gusset, wide sides and string back."] 

我们限制自己只查看前三篇文章,这样我们不会在这里查看大量数据。我们可以从购买的文章中看到,摘要很好地总结了客户的购买行为。

让我们看看客户年龄组与社区之间的相关性是如何存在的。

以下 Cypher 语句为我们提供了社区中年龄组最频繁出现的年龄段:

MATCH (c:Customer) where c.summer_2019_community is not null
WITH  c.summer_2019_community as community, toInteger(c.age) as age,  c
WITH community,
    CASE WHEN age < 10 THEN "Young"
         WHEN 10 < age < 20 THEN "Teen"
         WHEN 20 < age < 30 THEN "Youth"
         WHEN 30 < age < 50 THEN "Adult"
         ELSE "Old"
    END as ageGroup,
    C
WITH community, ageGroup, count(*) as count
CALL {
    WITH community
    MATCH (c:Customer) where c.summer_2019_community=community
    RETURN count(*) as totalCommunity
}
WITH community, ageGroup,count, totalCommunity
WITH community, ageGroup, round(count*100.0/totalCommunity, 2) as ratio
WITH community, collect({ageGroup: ageGroup, ratio:ratio}) as data
CALL {
    WITH community, data
    UNWIND data as d
    WITH community, d
    ORDER BY d.ratio DESC
    RETURN community as c, d.ageGroup as a, d.ratio as r
    LIMIT 1
}
RETURN c as community, a as ageGroup, r as ratio
ORDER BY r DESC 

结果将如图图 10.3所示。

社区 年龄组 比率
1899 “青年” 60.87
5823 “成人” 56.68
770 “青年” 47.9
1729 “青年” 46.92
4602 “青年” 45.71
133 “青年” 44.61
921 “青年” 44.17
3444 “青年” 41.73
649 “青年” 41.62
1881 “青年” 41.26
1696 “老年” 41.06
2381 “老年” 40.94
6010 “老年” 37.67
713 “青年” 37.64
760 “青年” 36.09
1875 “老年” 35.09
2778 “青年” 34.47

图 10.3 — 每个社区中最频繁出现的年龄组和其比率

我们可以看到大多数社区都由年龄组青年主导,他们的年龄在 20 到 30 岁之间。让我们看看一个青年年龄组不是主导的社区。让我们看看社区5823

这个 Cypher 语句给出了社区5823前五个客户的购买摘要:

MATCH (c:Customer)-[r:SUMMER_2019]->()
WHERE c.summer_2019_community=5823
WITH c, split(r.summary, '\n') AS s
WITH c, CASE WHEN s[2] <> '' THEN s[2] ELSE s[3] END AS d
RETURN d LIMIT 5 

当你使用特定客户的向量嵌入进行相似度搜索时,结果将主要是其他与目标客户向量表示接近的个体客户。你可能观察到一定程度的不一致性。一个重要的注意事项是,当我们仅仅基于向量距离寻找与目标客户相似的客户时,我们可能会错过潜在的相关推荐。

看看以下结果:

The customer demonstrates a strong preference for `versatile and stylish pieces, with a notable inclination towards swimwear and casual skirts, reflecting an active and chic lifestyle`. The selection features a mix of practical and trendy items, highlighting an appreciation for both comfort and aesthetics. The color palette leans towards soft tones and earthy shades, suggesting a preference for understated elegance.
The customer exhibits a preference for stylish yet `comfortable footwear and swimwear, favoring pieces that blend functionality with trendy elements. The consistent use of white and orange in swimwear suggests a bold and lively aesthetic, while the choice of soft organic cotton for kids'` basics indicates an appreciation for quality and sustainability. Overall, there is a clear inclination towards versatile and fashionable pieces suitable for both leisure and casual settings.
The customer's fashion preferences indicate a strong inclination towards `relaxed and comfortable styles, particularly in children's denim wear. The consistent choice of blue tones across multiple purchases suggests a preference for classic and versatile colors.` Additionally, the inclusion of a dress with a structured yet casual design highlights an appreciation for both practicality and style in their wardrobe choices.
The customer exhibits a preference for `versatile and comfortable clothing, with a notable inclination towards knitwear and soft fabrics. Their choices reflect a balance of casual and practical styles suitable for everyday wear`, particularly in hues of black, dark orange, and grey, complemented by accents of pink. The selected items also indicate a focus on functionality, especially with the inclusion of nursing bras.
The customer demonstrates a preference for `versatile and stylish pieces that blend comfort with contemporary design. They appreciate a mix of youthful and sophisticated styles, as seen in their selection of both kids' dresses and women's wear.` The choice of colors suggests a fondness for neutral tones with pops of color, reflecting both playful and elegant aesthetics. 

这些摘要显示这个社区倾向于有孩子的家庭。在观察了这些社区和其中的一些客户摘要后,我们能够比仅仅根据向量相似度更好地理解购买行为。

我们下一步是将协同过滤和基于内容的途径结合起来,以提供更好的推荐。

结合协同过滤和基于内容的途径

协同过滤涉及根据客户的购买相似性提供推荐,我们据此构建了客户社区,或者根据他们的特征进行文章相似性过滤。基于内容的过滤允许根据文章属性或特征提供推荐。我们将探讨如何结合这两种方法以提供更好的推荐。

我们将尝试以下场景:

  • 场景 1:过滤属于其他社区的文章

  • 场景 2:根据特征过滤并属于其他社区的文章

让我们先讨论场景 1。

场景 1:过滤属于其他社区的文章

在此场景中,我们首先找到所有同一社区内所有客户购买的商品。接下来,我们将找到属于其他社区的客户购买的商品。然后,我们将移除属于其他社区的商品。随后,我们将过滤掉(移除)这些属于其他社区的商品。

对于这个场景,我们将从社区1696中选取由000ae8a03447710b4de81d85698dfc0559258c93136650efc2429fcca80d699a标识的客户。

  1. 让我们看看这位客户的购买摘要:

    MATCH (c:Customer)-[r:SUMMER_2019]->()
    WHERE
    .id='000ae8a03447710b4de81d85698dfc0559258c93136650efc2429fcca80d699
    '
    WITH c, split(r.summary, '\n') AS s
    WITH c, CASE WHEN s[2] <> '' THEN s[2] ELSE s[3] END AS d
    RETURN d 
    

这个 Cypher 给出了以下输出:

"The customer's fashion preferences indicate a strong inclination towards comfortable yet stylish pieces, often favoring soft fabrics and relaxed fits. The choices reflect a taste for versatile items that can be dressed up or down, particularly in a palette that leans towards darker shades with hints of pink. Overall, there is a notable emphasis on casual wear that combines simplicity with a touch of elegance." 
  1. 现在让我们为这位客户获取推荐——他们之前未购买的商品,使用以下 Cypher:

    MATCH (c:Customer {id:'000ae8a03447710b4de81d85698dfc0559258c93136650efc2429fcca80d699a'})
    WITH c
    CALL { 
    
  2. 获取这位客户购买的商品:

    WITH c
        MATCH (c)-[:SUMMER_2019]->(start)
        MATCH (c)-[:FALL_2019]->(end)
        MATCH p=(start)-[:NEXT*]->(end)
        WITH nodes(p) as txns
        UNWIND txns as txn
        MATCH (txn)-[:HAS_ARTICLE]->(a)
        WITH DISTINCT a
        RETURN collect(a) as articles
    }
    WITH c, articles, c.summer_2019_community as community
    CALL { 
    
  3. 获取与原始客户属于同一社区的客户购买的商品:

    WITH community
        MATCH (inc:Customer) WHERE inc.summer_2019_community = community
        MATCH (inc)-[:SUMMER_2019]->(start)
        MATCH (inc)-[:FALL_2019]->(end)
        MATCH p=(start)-[:NEXT*]->(end)
        WITH nodes(p) as txns
        UNWIND txns as txn
        MATCH (txn)-[:HAS_ARTICLE]->(a)
        WITH DISTINCT a
        RETURN collect(a) as inCommunityArticles
    }
    WITH c, articles,  community, inCommunityArticles
    CALL { 
    
  4. 获取不属于与原始客户同一社区的客户购买的商品:

    WITH community
        MATCH (outc:Customer) WHERE outc.summer_2019_community is not
    ull and outc.summer_2019_community <> community
        MATCH (outc)-[:SUMMER_2019]->(start)
        MATCH (outc)-[:FALL_2019]->(end)
        MATCH p=(start)-[:NEXT*]->(end)
        WITH nodes(p) as txns
        UNWIND txns as txn
        MATCH (txn)-[:HAS_ARTICLE]->(a)
        WITH DISTINCT a
        RETURN collect(a) as outCommunityArticles
    }
    WITH c, articles,  community, inCommunityArticles,
    utCommunityArticles 
    
  5. 移除原始客户所属社区外的客户购买的商品:

    WITH c, articles, apoc.coll.subtract(inCommunityArticles, outCommunityArticles) as onlyInCommunity 
    
  6. onlyInCommunityArticles中移除原始客户购买的商品:

    WITH c, apoc.coll.subtract(onlyInCommunity, articles) as notPurchasedButInCommunity 
    
  7. 从剩余列表中提供 10 篇推荐文章。为了简单和演示目的,我们限制为 10 篇文章。我们可以查看所有文章,并可能根据其他方面进行分组,提供不同的推荐:

    UNWIND notPurchasedButInCommunity as article
    RETURN article.id as id, article.desc as desc
    LIMIT 10 
    

在这里,我们首先获取客户购买的商品。然后,我们检索客户所属社区的所有商品。之后,我们获取属于其他社区的客户购买的所有商品。我们从这些商品中获取仅由社区内客户购买的商品子集。从这个集合中,我们移除客户购买的商品,并将这些商品作为推荐提供。

此查询的输出将如图 10.4 所示。

Id Desc
0708679001 紧身,脚踝长牛仔裤,水洗,超弹力牛仔布,高腰,拉链前开,假前口袋和真后口袋。
0834749001 超大号柔软罗纹针织衫,含有部分羊毛,圆领,低落肩,长袖,袖口和下摆宽罗纹。该运动衫的聚酯含量为回收材料。
0513701002 有机棉针织圆领 T 恤。
0522374003 一件柔软、精细针织的落肩长袖毛衣,下摆圆滑。
0522374001 一件柔软、精细针织的落肩长袖毛衣,下摆圆滑。
0687041002 长袖合身上衣,柔软的有机棉针织,深领口,顶部有纽扣和圆下摆。
0724567004 带有细肩带和短裤的睡衣,柔软缎面,有蕾丝装饰。上衣有 V 领和窄可调节肩带。短裤腰部有窄弹性。
0785086001 V 领缎面睡衣,顶部和下摆有蕾丝装饰,可调节的细肩带。
0725353002 高腰、包臀的针织裙,侧面有隐藏的拉链和钩扣闭合。有衬里。
0604655007 印花棉针织睡衣。圆领短袖上衣。底部有弹性腰围和宽大的、略微收窄的腿,边缘有罗纹。

图 10.4  — 通过过滤客户购买的文章和社区外客户购买的文章进行推荐

这些推荐似乎符合客户的购买摘要。

现在,让我们看看场景 2。

场景 2:通过特征和属于其他社区过滤文章

在此场景中,我们希望将文章特征添加到查询中。这意味着,对于一个客户,我们首先找到社区中以特定特征购买的 所有文章。然后我们将找到这些文章所属的社区,并移除属于其他社区的文章。

为此,我们将选择社区 5823 和 ID 为 00281c683a8eb0942e22d88275ad756309895813e0648d4b97c7bc8178502b33 的客户。让我们看看这位客户的购买情况。

  1. 这个 Cypher 给我们以下信息:

    MATCH (c:Customer) where c.id='00281c683a8eb0942e22d88275ad756309895813e0648d4b97c7bc8178502b33'
    WITH c
    CALL {
        WITH c
        MATCH (c)-[:SUMMER_2019]->(start)
        MATCH (c)-[:FALL_2019]->(end)
        MATCH p=(start)-[:NEXT*]->(end)
        WITH nodes(p) as txns
        UNWIND txns as txn
        MATCH (txn)-[:HAS_ARTICLE]->(a)-[:HAS_SECTION]->(s)
        WITH DISTINCT a,s
        RETURN collect({section:s.name, article:a.desc}) as articles
    }
    return articles 
    

根据前面的 Cypher 我们得到以下输出:

{
  "article": "5-pocket jeans in washed stretch denim in a relaxed fit with an adjustable elasticated waist, zip fly and press-stud and tapered legs.",
  "section": "Kids Boy"}
,
{
  "article": "5-pocket jeans in washed stretch denim with hard-worn details in a relaxed fit with an adjustable elasticated waist, zip fly and press-stud, and tapered legs.",
  "section": "Kids Boy"
}
,
{
  "article": "Short dress in woven fabric with a collar, buttons down the front and a yoke at the back. Narrow, detachable belt at the waist and long sleeves with buttoned cuffs. Unlined.",
  "section": "Divided Collection"
}
,
{
  "article": "Dungarees in washed stretch denim with a three-part chest pocket, adjustable straps with metal fasteners, and front and back pockets. Fake fly, press-studs at the sides, jersey-lined legs and a lining at the hems in a patterned weave.",
  "section": "Kids Boy"
} 

由于这位客户正在购买 "Kids Boy" 部分的服装,让我们检索属于此部分的建议。

  1. 这个 Cypher 通过将此部分的详细信息添加到早期查询中,给出推荐。让我们获取 ID 为:00281c683a8eb0942e22d88275ad756309895813e0648d4b97c7bc8178502b33 的客户和名为 Kids Boy 的部分:

    MATCH (c:Customer {id:'00281c683a8eb0942e22d88275ad756309895813e0648d4b97c7bc8178502b33'})
    MATCH (s:Section) WHERE s.name='Kids Boy'
    WITH c,s
    CALL { 
    
  2. 获取该客户购买且属于重要部分的文章:

    WITH c,s
        MATCH (c)-[:SUMMER_2019]->(start)
        MATCH (c)-[:FALL_2019]->(end)
        MATCH p=(start)-[:NEXT*]->(end)
        WITH nodes(p) as txns
        UNWIND txns as txn
        MATCH (txn)-[:HAS_ARTICLE]->(a)-[:HAS_SECTION]->(s)
        WITH DISTINCT a
        RETURN collect(a) as articles
    }
    WITH c, articles,s, c.summer_2019_community as community
    CALL { 
    
  3. 获取与原始客户属于同一社区的客户购买的重要部分的文章:

    WITH community, s
        MATCH (inc:Customer) WHERE inc.summer_2019_community = community
        MATCH (inc)-[:SUMMER_2019]->(start)
        MATCH (inc)-[:FALL_2019]->(end)
        MATCH p=(start)-[:NEXT*]->(end)
        WITH nodes(p) as txns, s
        UNWIND txns as txn
        MATCH (txn)-[:HAS_ARTICLE]->(a)-[:HAS_SECTION]->(s)
        WITH DISTINCT a
        RETURN collect(a) as inCommunityArticles
    }
    WITH c, articles,  community, inCommunityArticles, s
    CALL { 
    
  4. 获取原始客户所属社区之外的其他社区的客户购买的重要部分的文章:

    WITH community, s
        MATCH (outc:Customer) WHERE outc.summer_2019_community is not
    ull and outc.summer_2019_community <> community
        MATCH (outc)-[:SUMMER_2019]->(start)
        MATCH (outc)-[:FALL_2019]->(end)
        MATCH p=(start)-[:NEXT*]->(end)
        WITH nodes(p) as txns, s
        UNWIND txns as txn
        MATCH (txn)-[:HAS_ARTICLE]->(a)-[:HAS_SECTION]->(s)
        WITH DISTINCT a
        RETURN collect(a) as outCommunityArticles
    }
    WITH c, articles,  community, inCommunityArticles,
    utCommunityArticles 
    
  5. 从原始客户所属社区的客户购买的文章中移除社区外客户购买的文章:

    WITH c, articles, apoc.coll.subtract(inCommunityArticles, outCommunityArticles) as onlyInCommunity 
    
  6. 从上一步得到的文章列表中移除原始客户购买的文章:

    WITH c, apoc.coll.subtract(onlyInCommunity, articles) as notPurchasedButInCommunity 
    
  7. 提供以下 10 篇文章作为推荐:

    UNWIND notPurchasedButInCommunity as article
    RETURN article.id as id, article.desc as desc
    LIMIT 10 
    

当我们运行这个查询时,我们将看到图 10.5所示的输出:

Id Desc
0505507003 洗涤弹力牛仔布 5 口袋修身牛仔裤,可调节弹性腰带和拉链式前门襟。
0704150011 前面有图案、领口、袖口和下摆处有罗纹的套头衫长袖上衣。
0701969005 软质、图案棉斜纹布短裤,弹性抽绳腰带,假拉链和侧口袋。
0595548001 软质洗涤牛仔布短裤,弹性抽绳腰带和后口袋。
0704150006 前面有图案、领口和袖口处有罗纹的套头衫长袖上衣。
0626380001 长袖套头衫,柔软的图案棉质圆领 T 恤,胸前开口口袋和下摆开叉。后身略长。
0701972005 织物短裤,可调节弹性腰带和装饰性抽绳。拉链式前门襟和纽扣,斜侧口袋和贴袋。
0771489001 空气棉质圆领 T 恤,胸前口袋和侧面短开叉。后身略长。
0705911001 棉质圆领 T 恤,带有印花图案和领口及袖口罗纹装饰。
0666327011 前面有图案的柔软棉质圆领 T 恤。

图 10.5 — 通过过滤掉客户和社区外客户的购买文章,考虑购买和文章属性进行推荐的示例

本章的演示展示了如何通过使用这些方法,根据其他客户的相似购买提供不同类型的推荐。

摘要

在本章中,我们探讨了如何超越基本的推荐应用程序,利用图算法来增强图并提供更合适的推荐。我们探讨了如何使用 KNN 相似度算法和社区检测来从数据中获得隐藏的洞察。

在接下来的章节中,我们将探讨如何将这些应用程序部署到云中,以及我们可以在部署过程中遵循的最佳实践。

第四部分

在云中部署您的 GenAI 应用程序

在本书的最后一部分,我们专注于将您的 GenAI 应用程序从开发阶段过渡到生产阶段。我们首先评估选择合适的云平台来部署 GenAI 工作负载的关键因素,包括可扩展性、成本和服务集成。然后,我们详细介绍在 Google Cloud 上部署您的应用程序的实际步骤,涵盖关键服务和最佳实践,以确保平稳可靠地启动。无论您是开发者、架构师还是 AI 从业者,本节都为您提供了在现实世界的云环境中实施 GenAI 解决方案的知识。

本书本部分包括以下章节:

  • 第十一章**,为 GenAI 应用程序选择合适的云平台

  • 第十二章**,在 Google Cloud 上部署您的应用程序

  • 第十三章**,结语

请保持关注

为了跟上生成式 AI 和 LLMs 领域的最新发展,请订阅我们的每周通讯,AI_Distilled,链接为packt.link/Q5UyU

第十一章:为 GenAI 应用选择正确的云平台

当您开始部署您的 GenAI 应用时,您将做出的最关键决策之一是选择正确的云平台。云领域广阔且多样化,提供了一系列针对不同需求、预算和技术要求量身定制的选项。然而,在没有明确的框架来指导您的决策时,选择最适合您特定 GenAI 用例的最佳平台可能会令人不知所措。虽然本书专注于智能 LLM 应用,但本章的学习将使您能够为任何 GenAI 用例选择云平台。

本章提供了评估云平台时需要考虑的关键因素的全面概述。我们将探讨领先提供商的独特功能、优势和定价模式,使您能够根据您的需求做出明智的决定——无论是可扩展性、专业 AI 服务还是成本效益。

在本章中,我们将涵盖以下主要内容:

  • 理解 GenAI 应用的云计算选项

  • 选择 GenAI 应用的云平台:关键考虑因素

  • 做出正确的选择:选择云平台的决定框架

理解 GenAI 应用的云计算选项

云已经成为现代通用人工智能(GenAI)应用的骨干,提供了处理其苛刻计算需求所需的基础设施和工具。虽然关于云的传统讨论通常集中在服务模式(IaaS、PaaS 和 SaaS)和部署类型(公有云、私有云和混合云)上,但本节将重点转向云提供商在支持专业 AI 服务中扮演的独特角色,以及为什么云对于 GenAI 来说是不可或缺的。

云:GenAI 不可或缺的基础

GenAI 应用资源密集,需要大量的计算能力、存储和可扩展性。云提供了几个独特的优势,使其对于部署 GenAI 解决方案至关重要:

  • 可扩展性以应对增长的工作负载:GenAI 模型可能需要数千个 GPU 或 TPU 进行训练和部署。云平台允许您动态扩展资源以满足这些需求,而无需进行前期基础设施投资。

  • 成本效益:采用按使用付费的定价模式,您只需为使用的资源付费,这使得云与维护本地基础设施相比成为一种成本效益的选择。

  • 全球可访问性:云平台提供地理分布式的数据中心,确保对 AI 服务和跨区域部署的低延迟访问。

  • 可靠性和安全性:云提供商提供企业级的安全性和高可用性,确保您的 GenAI 应用既安全又具有弹性。

  • 协作与集成:GenAI 工作流程通常涉及跨职能团队。云提供了协作环境和与流行开发工具的集成,简化了流程。

通过云服务,企业可以克服高基础设施成本和复杂性的障碍,转而专注于构建创新、AI 驱动的解决方案,以产生实际影响。

让我们讨论一下领先的云服务提供商为支持 GenAI 应用提供的某些专业 AI 服务。

不同云提供商的专业 AI 服务

领先的云提供商不仅提供计算能力;他们提供专门针对 GenAI 独特需求的 AI 和机器学习服务生态系统。这些服务通过以下方式提供以下服务,以实现 AI 应用的更快、更高效的开发:

  • 预训练模型和 API:访问现成的 AI 能力,如文本生成、语音识别和图像分析,无需从头开始训练模型

  • 定制 AI 训练服务:用于在定制数据集上训练和微调模型的工具,使构建特定领域的 AI 应用更加容易

  • 托管 AI 工作流:自动化工具,用于数据预处理、模型训练、评估和部署,简化开发过程

  • 与数据服务的集成:与存储和数据库解决方案的无缝连接,用于管理 GenAI 所需的庞大数据集

GenAI 应用得益于云平台提供的丰富生态系统,包括预训练模型、训练服务、托管 AI 工作流和数据集成工具。以下图表概述了主要提供商如何支持这些功能,帮助开发者加速 AI 开发和部署。

能力 Google Cloud (Vertex AI) Amazon Web Services (AWS SageMaker) Microsoft Azure (Azure AI and ML)
预训练模型和 API Gemini 模型和模型花园(开源和专有基础模型);用于 NLP、语音、视觉和结构化数据任务的 AI API AWS Bedrock(支持多个基础模型,如 Anthropic Claude、Meta Llama 和 AI21);用于文本、图像、视频和语音的 AI API Azure AI 模型目录,包含多种开源和专有模型(例如 OpenAI GPT-4 和 Meta Llama);认知服务 API 用于 NLP、视觉和语音
定制 AI 训练服务 Vertex AI 使用 AutoML 和微调进行定制模型训练;支持 PyTorch、TensorFlow 和 JAX SageMaker AI 使用预训练模型以及定制训练 Docker 镜像 Azure ML 使用 AutoML、微调功能和内置 ML 管道
托管 AI 工作流 Vertex AI 管道用于 MLOps 自动化(预处理、训练、评估和部署) SageMaker 管道用于完整 ML 生命周期自动化;Amazon Step Functions 用于工作流编排 Azure ML 管道用于自动化 AI 工作流和数据工程工具的集成
与数据服务的集成 BigQuery ML 用于数据库内 ML、Cloud Storage 和 Dataflow 用于可扩展的 AI 数据管道 AWS S3、Redshift ML 和 Glue 用于 ETL 和 AI 驱动的分析 Azure Synapse、Data Lake 和 Databricks 用于 AI 驱动的数据分析和处理

图 11.1 — 各云提供商 AI 能力的比较

利用这些能力,企业可以简化 AI 模型开发,从使用预训练模型到定制微调和大规模 AI 部署。

在下一节中,我们将探讨可以帮助您将平台选择与项目的技术和业务需求相匹配的关键考虑因素。

为 GenAI 应用选择云平台:关键考虑因素

选择适合您的 GenAI 应用的正确云平台不仅仅是比较功能和定价。每个平台都有其独特的优势和权衡,理解这些是确保平台与您的项目目标、预算和运营需求相匹配的关键。

选择 GenAI 部署的云平台涉及权衡多个技术和业务因素。在此,我们探讨指导您决策过程的最关键方面。

可扩展性和性能

GenAI 应用本质上资源密集型,需要大量的计算能力和处理各种工作负载的灵活性。无论您是在训练大规模语言模型,在大数据集上进行推理,还是提供实时预测,您选择的云平台的可扩展性和性能对于您应用的成功至关重要。

动态扩展资源的能力确保您的系统可以处理需求高峰,例如在模型训练或高流量期间,而不会过度配置并产生不必要的成本。同时,高性能基础设施,包括访问如 GPU 和 TPU 这样的专用硬件,对于加快模型训练和优化推理时间至关重要。除此之外,低延迟数据处理能力对于实时应用至关重要,例如聊天机器人或推荐引擎,在这些应用中,即使是轻微的延迟也可能对用户体验产生负面影响。

因此,通过关注这些因素,您可以确保您的 GenAI 应用即使在需求随时间演变的情况下也能高效且可靠地运行。

可扩展性和性能的一些重要考虑因素如下:

  • 弹性扩展:寻找支持自动扩展的平台,根据工作负载强度自动调整资源。这确保了您在低活动期间不会为未使用的资源付费,同时仍然满足高峰需求。

  • 高性能硬件:评估可用的 GPU、TPU 或 FPGA,这些硬件可以显著加速训练和推理任务。例如,Google Cloud、AWS 和 Azure 提供了对这种硬件的广泛支持。

  • 区域可用性和延迟:对于面向全球用户的应用程序,请确保平台拥有广泛的数据中心网络,以最小化延迟并在各个区域提供一致的性能。

  • 批量和实时处理:考虑平台是否能够高效地处理批量处理任务(例如,批量训练作业)和实时需求(例如,生成聊天机器人响应)。

下图突出了主要云服务提供商的关键可扩展性和性能能力,以及他们在处理 GenAI 工作负载方面的独特优势概述。

因素 Google Cloud (Vertex AI) Amazon Web Services (AWS SageMaker) Microsoft Azure (Azure AI and ML)
弹性扩展 AutoML 和 AI 平台根据工作负载自动扩展,并与 Kubernetes (GKE) 集成以实现容器化扩展。 自动扩展组 (AutoScaling Groups) 和 SageMaker 自动化训练和推理工作负载的扩展。 虚拟机规模集 (Virtual Machine Scale Sets) 和 Azure Machine Learning Autoscale 用于 AI/ML 工作负载。
高性能硬件 TPUs、NVIDIA GPUs(A100、H100)、定制 AI 芯片(Axion) AWS Inferentia、NVIDIA GPUs、Trainium 用于深度学习加速 NVIDIA GPUs、FPGAs、基于 AMD 的虚拟机
延迟和区域可用性 35+ 个区域的数据中心;在北美、欧洲和亚太地区有强大影响力 最广泛的全球足迹(32+ 个区域);高速跨区域网络 60+ 个区域的数据中心;强大的混合云选项
批量处理 支持通过 Vertex AI 管道和 AI 平台作业进行批量推理 管理批量转换作业以进行大规模机器学习推理 Azure ML 管道支持批量预测和大数据集处理。
实时处理 通过 Vertex AI Endpoints 进行低延迟预测 使用自动扩展的 SageMaker 实时推理 Azure ML Endpoints 用于低延迟模型服务

图 11.2 — 各云平台 GenAI 工作负载处理能力

选择云服务提供商时,您应考虑可扩展性和性能,但也要确保您考虑到效率、成本效益和对用户需求的响应性。

成本和定价模型

成本往往是选择 GenAI 应用程序的云平台时的决定性因素,因为 AI 工作负载资源密集型的特性,如果管理不当,可能会迅速增加费用。云服务提供商提供各种定价模型和成本管理工具,但理解和选择适合您预算的正确服务组合至关重要。

从在 GPU 和 TPU 上训练大型模型到管理大量数据集和提供实时推理,GenAI 应用在多个维度上产生成本。这些包括计算能力、存储、数据传输以及针对专用 AI 服务的额外费用。选择合适的定价模型并利用成本节省策略也可以提供竞争优势,确保您在扩展 AI 能力的同时保持在预算范围内。

影响您选择云服务提供商的成本和定价因素有很多:

  • 按量付费定价:云平台通常按使用量计费,您只需为使用的资源付费。这对于资源需求波动的动态工作负载来说非常理想,因为它消除了前期基础设施投资的必要性。例如,AWS On-Demand InstancesGoogle Cloud Compute Engine提供计算和存储服务的按量付费定价。考虑一家使用 GenAI 生成个性化推荐的电子商务公司;在假日和促销活动期间,它可能会遇到更高的流量。使用按量付费定价,它们可以在高峰期(例如黑色星期五)增加计算资源,在流量较低的月份减少资源,从而优化成本。
提供商 定价详情 定价链接
Google Cloud 计算引擎和 AI 服务按秒/分钟/小时计费。支持自动扩展。 cloud.google.com/pricing/
AWS EC2 按需实例按秒/分钟计费。支持动态扩展。 aws.amazon.com/pricing/
Azure 虚拟机和 AI 服务按每秒使用量计费。 azure.microsoft.com/en-in/pricing

图 11.3 — 按量付费定价选项

  • 预留实例和折扣:许多平台如果您承诺长期使用计划(例如 1 年或 3 年),将提供显著的折扣(高达 75%)。如果您有可预测的工作负载,例如托管具有稳定流量的推理模型,预留实例是一个不错的选择。例如,Azure 预留 VM实例为稳定状态使用提供了可预测的成本节省。考虑一个全年需求稳定的医院 AI 系统,该系统用于处理诊断用的放射学图像。对于 GPU/TPU 工作负载,承诺预留实例与按需定价相比可以显著降低成本。

  • 针对成本敏感型工作负载的 Spot 实例:Spot 实例以显著低于市场价的价格提供未使用的计算能力,但如果平台需要为更高优先级任务分配资源时,可能会被中断。这些实例非常适合非关键或批量处理过程,例如可以容忍中断的模型训练作业。考虑使用AWS Spot InstancesGoogle Preemptible VMs,与按需定价相比,它们可以提供高达 90%的节省。

  • 数据存储成本:GenAI 应用程序通常依赖于大型数据集进行训练和推理。了解可用的存储选项,包括热(频繁访问)、温(中等访问)和冷(存档)层。评估存储和检索数据的成本,因为它们可能会根据使用模式显著变化。例如,Amazon S3Google Cloud Storage 提供分层定价,根据访问频率优化成本。例如,对于使用人工智能推荐内容的视频流媒体服务,它需要频繁访问热门视频(热存储),同时将旧视频存档在冷存储中。选择正确的存储层可以确保成本效益的同时保持性能。

  • 数据传输成本:在不同区域、服务或平台之间移动数据可能会产生显著费用。确保您了解这些费用,尤其是对于分布式应用程序。例如,出口费用(数据从云中传输出去)对于向全球最终用户提供大量数据的应用程序来说可能会迅速增加。考虑一家分析全球交易的金融服务公司,它需要由人工智能驱动的实时欺诈检测。在欧洲和美国之间频繁的跨区域数据传输可能会很昂贵,而且了解出口费用有助于优化流量路由并降低费用。

提供商 定价详情 定价链接
Google Cloud 按区域收费,区域内传输免费 cloud.google.com/storage-transfer/pricing
AWS 区域间和互联网数据传输费用 aws.amazon.com/ec2/pricing/on-demand/#Data_Transfer
Azure 超出免费限制后适用出口费用 azure.microsoft.com/en-us/pricing/details/bandwidth/

图 11.4 — 数据传输成本

  • 成本管理和监控工具:云平台提供成本计算器和监控工具,帮助您有效地估算和管理费用:

    • AWS 成本探索器:跟踪使用模式并识别节省成本的机会。

    • Google Cloud 账单报告:提供对项目间支出的实时洞察。

    • Azure 成本管理:允许进行预测和预算分配。

    • 免费层和试用信用额:许多平台提供免费层服务或试用信用额,让您可以在不产生即时成本的情况下尝试功能。例如,Google Cloud 免费层AWS 免费层 为新用户提供有限的资源来探索他们的服务。

虽然了解定价模型是必要的,但积极优化成本同样重要,以确保可持续的云支出。如果管理不善,云资源可能会迅速变得昂贵,尤其是对于需要大量计算能力、存储和数据处理的 GenAI 工作负载。

因此,为了管理和优化成本,您应该考虑以下方面:

  • 资源合理配置:持续监控您的使用情况,并适当调整资源,以避免为闲置容量付费。这可以通过使用云原生监控工具如谷歌云推荐器、AWS 计算优化器和 Azure 顾问来实现,这些工具分析使用模式并提出优化建议。实施自动扩展策略(例如,AWS 自动扩展、谷歌云托管实例组、Azure 规模集)确保资源能够动态调整以满足工作负载需求。根据性能需求选择合适的实例类型可以防止过度配置,而无服务器计算(AWS Lambda、谷歌云函数、Azure 函数)和容器化工作负载(Kubernetes)通过仅在需要时分配资源进一步优化成本。

  • 数据生命周期管理:使用适当的存储层来优化不常访问数据的成本。

  • 集中计费:将多个账户或项目合并到一个计费系统中,以简化跟踪和优化批量定价。

通过理解成本结构和利用定价策略,您可以显著降低在云中部署和运行 GenAI 应用程序的相关费用。这将帮助您在性能和成本效益之间实现平衡。

随着对基于云的 GenAI 应用的依赖性日益增加,我们必须确保强大的安全和监管合规机制。在下一节中,我们将探讨关键安全考虑因素、身份和访问管理策略以及监管合规要求,以确保您的 GenAI 部署保持安全、弹性且合法合规。

安全和合规性

安全和合规性是部署 GenAI 应用程序到云中的关键考虑因素。这些应用程序通常处理敏感数据,如专有数据集、用户信息或行业特定记录,使安全成为一项基本功能。此外,遵守法律和监管标准是从事受监管行业(如医疗保健或金融)和不同地缘政治地区的企业的不可协商的要求。

了解云平台的安全性和合规性功能,可以帮助您保护数据,与用户建立信任,并避免昂贵的法律或声誉风险。虽然领先的云服务提供商——谷歌云、AWS 和 Azure——提供了强大的安全功能,但根据其用例正确配置这些功能是用户的责任。

一些安全功能,如基本的静态存储加密和身份管理,默认启用,而其他功能,如自定义 基于角色的访问控制RBAC)和合规性配置,则需要手动设置。为确保正确配置,云服务提供商提供详细的文档和最佳实践:

通常,您需要关注一些关键的安全考虑和配置:

  • 数据加密:确保敏感信息在传输和静态存储中都得到保护。云服务提供商提供内置的加密功能,但根据您的安全和合规性需求,可能需要额外的配置:

    • 传输中:系统或服务之间移动的数据应使用安全的协议,如 TLS/SSL。在某些情况下,例如强制服务之间数据移动使用 TLS/SSL 连接,这需要手动设置。

    • 静态存储:存储在数据库、文件系统或对象存储中的数据应使用如 AES-256 这样的强大算法进行加密,这对于大多数云存储服务默认启用。然而,用户可以配置自定义加密密钥以增强安全性。

此表详细说明了在 Google Cloud、AWS 和 Azure 中配置加密:

云服务提供商 静态存储加密 传输中加密 文档
Google Cloud 默认使用由 Google 管理的密钥进行加密;用户可以通过 Cloud KMS 配置 客户管理的加密密钥CMEKs 默认使用 TLS/SSL;API 和数据库提供额外的加密选项 cloud.google.com/docs/security/overview/whitepaper
AWS AWS S3、RDS 和 EBS 默认启用加密;AWS KMS 允许客户管理密钥加密 通过 AWS 证书管理器和 TLS 设置强制执行 docs.aws.amazon.com/whitepapers/latest/logical-separation/encrypting-data-at-rest-and--in-transit.html
Azure Blob 存储和 SQL 以及磁盘的静态存储加密已启用;通过 Azure Key Vault 可用 客户管理的密钥CMKs 使用 TLS/SSL 进行传输加密;提供额外的网络加密设置 learn.microsoft.com/en-us/azure/security/fundamentals/encryption-overview

图 11.5 — 数据安全选项

  • 身份和访问管理(IAM):IAM 对于控制谁可以访问云资源以及确保只有授权用户或服务拥有所需的权限至关重要。云服务提供商提供内置的 IAM 框架,允许组织安全地管理访问:

    • 基于角色的访问控制(RBAC):IAM 通过为用户和服务分配角色(例如,管理员、开发人员和查看者)来实现细粒度的权限,确保仅按需授予访问权限。

    • 多因素身份验证(MFA):通过要求用户通过密码之外的额外身份验证因素(例如,短信或身份验证应用程序)验证其身份来增强安全性。

    • 与企业身份提供者的集成单点登录SSO)和利用 Azure Active DirectoryAAD)、AWS IAM Identity Center 和 Google Cloud IAM 的联合身份验证。

    • 例如:AAD 和 AWS IAM 提供对用户角色和权限的细粒度控制,允许组织实施最小权限访问和安全的身份验证方法,以保护敏感的云工作负载。

  • 网络安全:使云工作负载免受未经授权的访问、数据泄露和网络威胁的保护。云服务提供商提供一系列网络安全工具,以创建安全的边界,限制访问并监控流量:

    • 虚拟专用云(VPCs):允许用户在云基础设施内创建隔离的网络环境,确保对敏感资源的受控访问。

    • 防火墙和私有端点:云原生防火墙和私有连接选项(例如,AWS PrivateLink、Azure Private Link 和 Google Private Service Connect)保护内部流量,同时限制对公共网络的暴露。

    • 日志记录和监控:安全工具,如 Google Cloud VPC Service Controls、AWS VPC Flow Logs 和 Azure Network Watcher,可以实时检测并响应可疑活动。

    • 例如,Google Cloud VPC Service Controls 允许组织在敏感工作负载周围定义安全的边界,限制跨云服务的数据移动,同时防止未经授权的访问。

  • 威胁检测和事件响应:对于实时识别安全威胁和减轻风险在风险升级之前至关重要。云服务提供商提供基于 AI 的安全工具,以检测异常、防止入侵并自动化安全响应。

    • 异常检测和入侵预防:内置的安全工具使用机器学习来识别可疑活动、未经授权的访问和潜在的威胁,在它们影响工作负载之前。

    • 自动事件响应:云服务提供商提供自动修复服务,以遏制安全威胁,减少手动干预的需求。

    • 安全事件记录:威胁情报服务收集、分析和响应安全事件,与 安全信息和事件管理SIEM)平台集成,以实现高级监控。

    • 例如,AWS GuardDutyAzure Security Center 使用基于 AI 的威胁检测来识别安全风险、分析异常活动并触发自动警报到安全团队,从而实现对潜在入侵的快速响应。

在云中部署 GenAI 应用时,确保符合行业法规和数据保护法至关重要。虽然主要的云服务提供商——谷歌云、AWS 和 Azure——提供内置的合规框架,但大多数合规设置需要根据业务需求、行业标准和数据驻留要求进行用户配置。

  • 一些合规措施,如默认加密和安全日志,是预先配置的。

  • 其他设置,例如数据驻留控制、审计工具和合规性(如 HIPAA、GDPR 和 SOC 2),在云资源配置期间需要手动设置。

  • 云服务提供商通常在设置期间提示用户配置合规设置,但企业必须确保它们与特定的法律和运营要求一致。

一些关键合规和法规要求如下:

  • 法规标准:云服务提供商符合多个行业标准,但需要手动设置以确保符合特定业务需求。

    • 健康保险可携带性和问责制法案 (HIPAA):用于处理受保护的健康信息 (PHI) 的医疗保健相关应用

    • 通用数据保护条例 (GDPR):规范企业如何处理欧盟公民的个人数据,并要求明确的用户同意机制

    • 服务组织控制 2 (SOC 2):确保云服务遵循严格的数据安全和隐私控制,以处理客户数据。

    • 例如,Azure Compliance Manager 提供预配置模板,以帮助将工作负载与法规标准对齐。

  • 数据驻留和主权:数据主权法律要求企业将数据存储和处理在特定的地理区域内,以符合当地法规。这不是预先配置的;云服务提供商提供区域数据存储选项,但企业必须手动选择数据驻留设置。例如,Google Cloud 区域服务AWS 区域 允许用户选择数据存储位置以满足法规要求。

  • 可审计性:审计日志默认部分启用,但需要手动配置以满足定制合规需求:

    • 确保平台提供审计访问和敏感数据上采取的行动的工具。

    • 维护所有数据交互的日志和记录,以在审计期间证明合规性。

    • 例如,AWS CloudTrailAzure Monitor Logs 提供全面的审计功能。

  • 认证和信任计划:云服务提供商参与外部认证和信任计划,以验证其符合行业安全和监管标准。这些认证有助于企业满足数据保护、隐私和治理的法律和安全要求。所有三大主要云服务提供商——谷歌云、AWS 和 Azure——都符合全球认证,但组织必须确保其云配置与其特定的用例的合规性需求相一致。

认证 谷歌云 AWS Azure
ISO 27001(信息安全管理体系) ✅ 符合规范 ✅ 符合规范 ✅ 符合规范
支付卡行业数据安全标准 (PCI DSS) ✅ 符合规范 ✅ 符合规范 ✅ 符合规范
联邦风险和授权管理计划 (FedRAMP)(美国政府安全合规) ✅ 符合规范(针对选定服务 FedRAMP 高级) ✅ 符合规范(针对 GovCloud FedRAMP 高级) ✅ 符合规范(针对政府服务 FedRAMP 高级)
SOC 2(客户数据安全处理) ✅ 符合规范 ✅ 符合规范 ✅ 符合规范
健康保险可携带性和责任法案 (HIPAA)(医疗数据保护) ✅ 符合规范(需要手动设置) ✅ 符合规范(需要手动设置) ✅ 符合规范(需要手动设置)
欧盟数据的一般数据保护条例 (GDPR) ✅ 符合规范 ✅ 符合规范 ✅ 符合规范

图 11.6 — 可用的合规认证

实施安全和合规措施只是第一步——有效地维护它们同样关键。随着网络安全威胁的不断演变和监管环境的不断变化,采取积极主动的安全和合规方法对于保护您的生成式人工智能应用免受漏洞的影响是必要的。

现在让我们探讨一些有助于维护安全且合法的生成式人工智能部署的最佳实践。

生成式人工智能部署中的安全和合规最佳实践

确保强大的安全和合规性对于成功部署生成式人工智能应用至关重要,尤其是在处理敏感数据或在受监管的行业中运营时。采用最佳实践有助于降低风险、保持与监管的合规性,并赢得用户的信任,确保您的应用程序既安全又可靠:

  • 定期安全审计:定期审查和更新您的安全配置以应对新的威胁。

  • 自动合规性检查:使用云原生工具持续监控和执行合规性要求。

  • 共享责任模型:了解您与云服务提供商之间的责任划分——虽然提供商负责保护基础设施,但您负责保护应用程序和数据。

  • 数据最小化:仅存储应用程序运行所需的数据,以减少对安全风险的暴露。

虽然最佳实践提供了基础,但选择正确的云服务提供商需要了解哪些平台最能支持安全和合规性需求。以下图示概述了 Google Cloud、AWS 和 Azure 如何解决这些关键领域:

最佳实践 Google Cloud AWS Azure
安全审计 安全指挥中心,云审计日志 AWS 安全中心,AWS Inspector 微软云防御,Azure 安全中心
自动化合规性检查 确保的工作负载,策略智能 AWS Config,审计经理,AWS Artifact Azure Policy,合规性经理
共享责任模型 共享安全最佳实践文档 清晰的 AWS 共享安全模型 Azure 的共享安全责任指南
数据最小化 数据丢失预防DLP)API,云存储生命周期策略 亚马逊 Macie(S3 的 DLP),IAM 数据访问策略 Azure Purview(数据治理),DLP
合规框架 HIPAA,GDPR,FedRAMP,ISO 27001,PCI DSS(认证) HIPAA,GDPR,FedRAMP,ISO 27001,PCI DSS(认证) HIPAA,GDPR,FedRAMP,ISO 27001,PCI DSS(认证)

图 11.7 — 安全合规性可用性

为了进一步评估,您可以探索每个云服务提供商提供的官方安全和合规性文档:

如果您正在寻找云服务提供商安全框架的详细比较,云安全联盟CSA)维护了一个公开可访问的安全评估数据库,网址为 cloudsecurityalliance.org/star

此资源列出了云服务提供商的安全控制、合规认证和第三方风险评估,帮助您评估哪个平台最能满足您的需求。

优先考虑安全和合规性非常重要,这有助于保护敏感数据,遵守监管标准,并构建一个值得信赖的 GenAI 应用程序。这不仅保护您的组织免受潜在威胁和法律问题,而且有助于增强用户对您解决方案可靠性和安全性的信心。

通过仔细评估这些因素,您可以选择一个云平台,它不仅满足您的技术和运营需求,而且确保您的 GenAI 项目长期的可扩展性、成本效益和合规性。虽然实施最佳实践的安全和合规性为您的 GenAI 部署奠定了坚实的基础,但了解主要云服务提供商的提供内容同样至关重要。

关键要点

总结主要云提供商的分析,以下关键要点突出了每个平台的独特优势和理想用例。以下是一个快速参考,帮助您将 GenAI 部署需求与最合适的云解决方案相匹配。

  • Google Cloud在 AI 分析和端到端机器学习工作流程方面表现出色,具有强大的数据集成能力。

  • AWS提供无与伦比的可扩展性和成本节约选项,非常适合高容量和灵活的工作负载。

  • Microsoft Azure以其混合云支持和企业级 AI 服务而突出,满足具有多样化基础设施需求的企业。

随着您处理和理解每个平台独特的优势,您可以将选择与您项目的需求相匹配,确保您的 GenAI 部署成功。这包括仔细分析主要云提供商的功能和定价,并确保它们与您的目标和限制相一致。

在下一节中,我们将介绍一个决策框架,帮助您选择最适合您的 GenAI 应用程序需求的云平台。

做出正确的选择:选择云平台的决策框架

为您的 GenAI 应用程序选择正确的云平台是一个多方面的决策,这取决于您项目的独特需求、优先级和限制。一个结构化的框架通过提供基于可扩展性、成本、专用功能和合规性等因素评估平台的方法,简化了这一过程。

让我们逐步分析决策过程,以帮助您有效地平衡技术和业务考虑因素。无论您是专注于优化性能、控制成本还是利用专门的 AI 工具,这个框架都能确保您的选择与短期目标和长期可扩展性相一致。

  • 定义您的需求和优先级:首先确定您的 GenAI 应用程序的具体需求。这一步骤有助于您关注与您的优先级相一致的平台,并忽略不必要的功能。

    • 性能需求:评估您的计算和存储需求。您是否需要访问 GPU、TPU 或高性能存储?

    • 可扩展性需求:考虑您的负载是稳定还是波动较大,因为这会影响您对动态扩展的需求。

    • AI 专用功能:确定必要的 AI 工具或服务,例如预训练模型、API 或自定义模型训练功能。

    • 预算限制:设定明确的预算并确定您在意外成本方面的灵活性。

    • 合规性和安全性:确保您选择的平台符合行业法规(例如,HIPAA 和 GDPR)并提供强大的安全措施。

  • 缩小潜在提供商的短名单:根据您定义的要求,将您的选项缩小到几个与您的需求一致的云提供商。使用以下标准创建您的短名单:

    • AI 服务和工具:评估 Google Cloud(Vertex AI)、AWS(SageMaker)和 Azure(认知服务)等平台在通用人工智能(GenAI)方面的提供。

    • 成本模型:比较定价结构,包括按使用付费、预留实例和按需实例。

    • 集成和生态系统:考虑与您现有的工具、框架和工作流程(例如 TensorFlow、PyTorch 或数据服务)的兼容性。

    • 区域可用性:确保提供商在其应用程序性能和合规性至关重要的地区拥有数据中心。

  • 评估权衡:通过分析关键领域的权衡来比较候选提供商:

    • 性能 versus 成本:提供商是否以您预算范围内的价格提供您所需性能?

    • 灵活性 versus 专业化:您是否优先考虑在一系列任务中的灵活性,还是需要为通用人工智能(GenAI)提供高度专业化的工具?

    • 支持和可靠性:评估提供商的支持服务和可靠性记录,特别是对于关键工作负载。

  • 利用决策工具:许多云提供商提供工具以协助选择和规划:

    • 成本计算器:使用 AWS 定价计算器、Google Cloud 定价估算器或 Azure 成本管理工具来预测费用。

    • 试用和免费层:利用免费层提供或试用信用额度在做出承诺之前测试服务。

    • 性能基准:审查公开可用的基准或在自己的模型训练、推理和数据传输任务上运行测试。

  • 确保决策具有前瞻性:超越您的即时需求,以确保未来增长的可扩展性和适应性:

    • 长期成本效率:评估预留实例或混合模型等选项以控制长期成本。

    • 不断发展的 AI 需求:考虑平台是否能够支持随着项目增长而出现的 AI 技术或扩展需求。

    • 供应商锁定风险:评估应用程序的可移植性以及将来更换提供商是否可行。

  • 做出数据驱动的决策:将您的发现汇总成比较矩阵,对每个提供商在成本、性能、可扩展性、合规性和功能支持等关键标准上进行评分。根据其对项目的重要性为每个标准分配权重,然后使用评分来识别最佳匹配的平台。

此决策框架确保您的云平台选择是战略性的、数据驱动的,并与您的通用人工智能(GenAI)项目的独特需求保持一致。通过仔细定义您的优先事项、评估权衡并规划未来,您可以选择一个最大化您的通用人工智能部署成功的平台。

摘要

在本章中,我们探讨了在云中部署 GenAI 应用程序的基本考虑因素,为您提供做出明智决策的知识。我们首先强调了专用 AI 服务的重要性以及云在支持 GenAI 的可扩展性和性能需求中的作用。然后,我们讨论了评估云平台的关键因素,例如成本、安全性、合规性和功能提供。对领先的云服务提供商——谷歌云、亚马逊 Web 服务和微软 Azure 的详细分析有助于突出它们的独特优势、定价模式和适用于不同用例的适用性。最后,我们介绍了一个结构化的决策框架,以帮助您选择最适合您特定需求的最佳平台。

在下一章中,我们将采取动手实践的方法来部署您的 GenAI 应用程序。基于本章的基础知识,我们将指导您在谷歌云上实施一个实用的部署工作流程。

第十二章:在 Google Cloud 上部署您的应用程序

您在设计和发展您的 GenAI 应用程序方面已经取得了长足的进步。现在,是时候迈出下一个关键步骤——部署。虽然真正的生产级部署涉及各种复杂性,如 CI/CD 管道、可扩展性考虑、可观察性、成本优化和安全加固,但本章旨在为您提供一个基于 Google Cloud Run 的云部署基础、动手实践介绍。Cloud Run 由 Google Cloud 提供,它提供了一种强大且对开发者友好的方式来部署容器化应用程序,无需管理基础设施,非常适合快速原型设计和小规模生产用例。

部署步骤和服务可能在不同云平台(如 AWS 或 Microsoft Azure)上略有不同,但我们将专注于 Google Cloud 以简化说明。然而,一旦您熟悉了这里的核心概念,我们鼓励您在其他提供商上尝试类似的流程,以拓宽您的云部署专业知识。

我们将逐步介绍如何将您在第五章中构建的 Haystack 聊天机器人作为无服务器应用程序部署到 Google Cloud。关于使用 Spring AI 部署智能推荐系统的步骤将在本章后面提及,并附有资源链接以便您跟随步骤进行。到本章结束时,您将拥有一个在云上运行的聊天机器人,并对此基础上的更高级部署充满信心。

在本章中,我们将涵盖以下主要主题:

  • 使用 Haystack 准备您的搜索聊天机器人以进行部署

  • 使用 Docker 容器化应用程序

  • 设置 Google Cloud 项目和服务

  • 部署到 Google Cloud Run

  • 测试和验证部署

技术要求

要使用 Google Cloud Run 部署您的 Haystack 聊天机器人,您需要以下条件:

  • 拥有一个已启用计费的活跃 Google Cloud 账户。如果您是 Google Cloud 的新用户,可以从console.cloud.google.com/创建一个账户,并利用为新用户提供免费层和信用额度。

  • 访问 Neo4j 数据库:

    • 如果您使用的是本地 Neo4j 实例,您必须使用 ngrok 或类似工具将其公开,以便部署的聊天机器人可以连接到它。以下是一个公开 Neo4j 的 bolt 端口的示例:

      ngrok tcp 7687 
      
    • 使用 ngrok 公共 URL 更新您的 .env 文件:

      NEO4J_URI=bolt://0.tcp.ngrok.io:XXXXX 
      
  • 如果您使用 AuraDB Free,可以忽略前面的步骤。

准备您的 Haystack 聊天机器人以进行部署

我们将在 Google Cloud 上部署我们的应用程序。我们将讨论的部署方法在所有流行的云环境中从部署角度来看是相似的。我们将查看构建docker compose并在云中运行它。选择 Google Cloud 是因为它方便,而不是因为它有任何技术优势。一旦我们在 Google Cloud 上部署并运行了应用程序,将提供其他云部署相同docker compose的官方文档链接。仅仅重复书中的那些步骤并不会带来太大的不同。

在进行容器化和部署之前,确保您的 Haystack 聊天机器人代码以与无服务器部署兼容的方式组织非常重要。在本节中,您将适当地结构化代码库,并准备部署到 Google Cloud Run 所需的必要配置文件。

我们将通过创建主脚本(search_chatbot.py)的副本,将其重命名为app.py(因为这是许多云服务在提供 Python 网络应用程序时期望的默认入口点),并将其放置在一个简化文件夹中以准备容器化,来重用工作搜索聊天机器人。由于聊天机器人逻辑已经功能正常,我们将跳过本地测试,直接进行打包并将其部署到云端。

注意

这些步骤将帮助您将 Haystack 聊天机器人容器化和部署到 Google Cloud Run,这些步骤也在本书 GitHub 仓库的README.md文件中展示,网址为github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs/tree/main/ch12

接下来,准备一个requirements.txt文件,列出聊天机器人运行所需的全部 Python 依赖项。此文件允许容器在构建过程中安装所需的包。此文件的内容看起来可能如下所示:

haystack-ai==2.5.0
openai==1.67.0
gradio==4.44.1
python-dotenv>=1.0.0
neo4j==5.25.0
neo4j-haystack==2.0.3 

为了安全地管理敏感凭证和环境特定的配置,建议使用.env文件。在 GitHub 仓库的ch12目录中,您将找到一个名为example.env的文件,它作为一个模板。此文件包括用于您的 OpenAI API 密钥和 Neo4j 数据库凭证等关键变量的占位符。要使用它,只需创建此文件的副本,将其重命名为.env,并用您的实际值填充。应用程序利用python-dotenv库在运行时加载这些变量,从而将秘密从代码库中排除,同时仍然使它们对应用程序可访问:

OPENAI_API_KEY=<insert-your-openai-api-key>
NEO4J_URI=<insert-your-neo4j-uri>
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=<insert-your-neo4j-password> 

到目前为止,您已经设置了部署所需的核心组件——您的应用程序脚本、依赖项和环境变量。为了确保一切组织正确,以便进行容器化和部署,您的项目目录现在应遵循以下结构:

haystack-cloud-app/
├── app.py                  # Renamed chatbot server file (originally search_chatbot.py)
├── requirements.txt        # Python dependencies
├── Dockerfile              # Will be created in the next step
├── .env                    # For storing configuration variables
├── example.env             # Template file for environment variables 

example.env 文件作为用户创建自己的 .env 文件的参考,其中包含有效的凭证和配置值。现在,所有核心组件都已就位——包括你的应用程序脚本、依赖项和环境设置——你现在可以为部署容器化你的应用程序。

让我们继续进行下一步,即使用 Docker 容器化你的应用程序。

使用 Docker 容器化应用程序

在将你的 Haystack 聊天机器人部署到 Google Cloud Run 之前,应用程序必须被打包到 Docker 容器中。容器化允许你将你的代码、依赖项和环境捆绑成一个单一、可移植的单位,该单位可以在不同的系统上(包括云端)一致地运行。

在本节中,你将创建一个 Dockerfile,它定义了构建你的聊天机器人 Docker 镜像所需的步骤。然后,这个镜像将被部署到 Cloud Run 作为无服务器网络服务。

这里是用于容器化你的 Haystack 聊天机器人的 Dockerfile:

FROM python:3.11
EXPOSE 8080
WORKDIR /app
COPY . ./
RUN pip install -r requirements.txt
CMD ["python", "app.py"] 

让我们分解每一行的作用:

  • FROM python:3.11: 这将基础镜像设置为 Python 3.11,它包括运行 Python 应用程序所需的一切。

  • EXPOSE 8080: Cloud Run 预期应用程序在端口 8080 上监听。此行记录了容器在运行时将公开的端口。

  • WORKDIR /app: 这将容器内的工作目录设置为 /app。所有后续命令都将从这个目录运行。

  • COPY . ./: 这将你的本地项目目录的全部内容复制到容器的 /app 目录中。

  • RUN pip install -r requirements.txt: 这安装了你在 requirements.txt 文件中列出的所有 Python 依赖项。

  • CMD ["python", "app.py"]: 这指定了容器启动时要运行的命令——在这种情况下,它使用 app.py 运行你的聊天机器人应用程序。

一旦你的 Dockerfile 就位,你现在就拥有了一个完全容器化的 Haystack 聊天机器人版本,准备好部署到云端。下一步是配置你的 Google Cloud 环境,以便你可以推送你的容器并使用 Cloud Run 运行它。

让我们继续设置你的 Google Cloud 项目和服务。

设置 Google Cloud 项目和服务

Google Cloud 为部署现代应用程序(包括由 GenAI 驱动的解决方案)提供了一个强大且面向开发者的平台。借助 Cloud Run、Artifact Registry 和 Cloud Build 等工具,Google Cloud 使你能够从代码到可扩展的无服务器部署,同时最小化运营开销。

尽管你的 Haystack 聊天机器人使用 OpenAI 进行语言处理,但 Google Cloud 在托管应用程序、管理容器构建和安全性存储 Docker 镜像方面发挥着关键作用。在本节中,你将配置你的 Google Cloud 项目,仅启用必要的服务(如 Cloud Run、Cloud Build 和 Artifact Registry),并为部署准备你的环境。

到本节结束时,你的项目将准备好云部署,所有服务和权限都已就绪,以便使用 Google Cloud 的无服务器基础设施部署你的聊天机器人。

让我们开始设置你的项目并启用所需的 API。

创建一个项目

在 Google Cloud 控制台(console.cloud.google.com/)的项目选择器页面,选择或创建一个 Google Cloud 项目(cloud.google.com/resource-manager/docs/creating-managing-projects)。

确保你的 Cloud 项目已启用计费。了解如何在项目中检查计费是否已启用,请参阅cloud.google.com/billing/docs/how-to/verify-billing-enabled

启动 Google Cloud Shell

为了简化设置并避免在本地安装任何工具,我们将使用预装了 Docker、gcloud CLI 和 Git 的 Google Cloud Shell。以下是开始的方法:

  1. 访问 Google Cloud 控制台(console.cloud.google.com/)。

  2. 点击导航栏右上角的 Cloud Shell 图标(终端图标)。

屏幕底部将打开一个终端窗口。这是一个功能齐全的 shell,可以访问你的 Google Cloud 项目和服务。

注意

Cloud Shell 提供了一个具有 5 GB 持久存储的临时 VM——对于这个教程来说已经足够了。

设置你的活动项目

确保你正在操作正确的 Google Cloud 项目。你可以创建一个新的项目或者使用现有的一个。使用以下步骤设置:

gcloud config set project YOUR_PROJECT_ID 

你可以使用以下命令验证活动项目:

gcloud config list project 

启用所需的服务

现在,启用部署你的容器所需的 Google Cloud 服务:

gcloud services enable cloudresourcemanager.googleapis.com \
                       servicenetworking.googleapis.com \
                       run.googleapis.com \
                       cloudbuild.googleapis.com \
                       cloudfunctions.googleapis.com 

命令执行成功后,你应该会看到类似于以下的消息:

Operation "operations/..." finished successfully. 

上述 gcloud 命令的替代方法是通过对控制台中的每个产品进行搜索。如果遗漏了任何 API,你可以在实施过程中启用它。请参阅 gcloud 命令和用法文档:cloud.google.com/sdk/gcloud/reference/config/list

将你的项目文件添加到 Cloud Shell

在继续部署步骤之前,请确保你的 Haystack 聊天机器人文件已存在于你的 Google Cloud Shell 环境中。

你有两个选择来完成这个操作:

  1. 上传现有文件:如果你已经在本地开发项目(例如,作为早期章节的一部分),你可以使用 Cloud Shell 编辑器的 上传 选项将工作目录上传到 Cloud Shell。只需点击 打开编辑器 按钮(铅笔图标),然后使用 文件 | 上传文件 或直接将文件夹拖放到编辑器中。

  2. 从 GitHub 克隆(推荐用于干净设置):或者,你可以使用以下命令直接从官方书籍仓库克隆 第十二章 的代码:

    git clone [`github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs.git`](https://github.com/PacktPublishing/Building-Neo4j-Powered-Applications-with-LLMs.git)
    cd Building-Neo4j-Powered-Applications-with-LLMs/ch12 
    

一旦你进入 ch12 文件夹,你将找到所有必要的文件——app.pyrequirements.txtDockerfileexample.env

如果你决定克隆仓库,请确保遵循前面提到的步骤来生成 .env 文件。现在你的项目文件已经放置在 Cloud Shell 内并准备就绪,是时候进入最终阶段——将你的 Haystack 聊天机器人部署到 Google Cloud Run。

部署到 Google Cloud Run

在本节中,你将了解完整的部署工作流程——从设置环境变量和配置 Artifact Registry 到构建你的容器并使用 Cloud Run 部署它。让我们一步一步来分解:

  1. 设置环境变量。首先,导出你的 Google Cloud 项目和部署区域的关键环境变量。用你的实际值替换占位符:

    # Set your Google Cloud project ID
    export GCP_PROJECT='your-project-id'  # Replace with your actual
    roject ID
    # Set your preferred deployment region
    export GCP_REGION='us-central1'       # You can choose a different supported region 
    
  2. 创建一个 Artifact Registry 实例并构建容器。配置你的 Artifact Registry 仓库并使用 Cloud Build 构建你的容器镜像:

    # Set Artifact Registry repo name and Cloud Run service name
    export AR_REPO='your-repo-name'       # Choose a name like 'genai-chatbot'
    export SERVICE_NAME='movies-chatbot'  # Or any descriptive name 
    
    1. 创建 Docker 仓库:

       gcloud artifacts repositories create "$AR_REPO" \
        --location="$GCP_REGION" \
        --repository-format=Docker 
      
    2. 使用 Artifact Registry 验证 Docker:

      gcloud auth configure-docker "$GCP_REGION-docker.pkg.dev" 
      
    3. 然后,构建并推送你的容器镜像:

      gcloud builds submit \
        --tag "$GCP_REGION-docker.pkg.dev/$GCP_PROJECT/$AR_REPO/$SERVICE_NAME" 
      

此命令使用你的 Dockerfile 打包应用程序,并将生成的容器镜像推送到 Artifact Registry。

  1. 部署到 Cloud Run。在部署之前,请确保你的 .env 文件包含所有必需的环境变量,例如 OPENAI_API_KEYNEO4J_URI 以及任何特定于项目的配置。

为了在部署期间传递这些变量,将你的 .env 文件转换为与 --set-env-vars 标志兼容的格式:

ENV_VARS=$(grep -v '^#' .env | sed 's/ *= */=/g' | xargs -I{} echo -n "{},")
ENV_VARS=${ENV_VARS%,} 
  • 现在,将你的应用程序部署到 Cloud Run:

    gcloud run deploy "$SERVICE_NAME" \
      --port=8080 \
      --image="$GCP_REGION-
    docker.pkg.dev/$GCP_PROJECT/$AR_REPO/$SERVICE_NAME" \
      --allow-unauthenticated \
      --region=$GCP_REGION \
      --platform=managed \
      --project=$GCP_PROJECT \
      --set-env-vars="GCP_PROJECT=$GCP_PROJECT,GCP_REGION=$GCP_REGION,$ENV_VARS" 
    

一旦完成,Google Cloud Run 将返回一个 URL,你的聊天机器人可以通过网页实时访问。

恭喜——你的 Haystack 聊天机器人现在已成功部署,并在 Google Cloud 上作为一个无服务器应用程序运行!

让我们继续到最后一步:测试和验证部署,以确保一切按预期工作。

在 Google Cloud 上测试和验证部署

一旦你的部署完成,Google Cloud Run 将返回一个公共服务 URL,通常格式如下:

https://movies-chatbot-[UNIQUE_ID].${GCP_REGION}.run.app 

在你的浏览器中打开此 URL。你应该能看到你的 Gradio 驱动的聊天机器人界面在网页上实时运行——其功能与本地版本完全相同。你现在可以与聊天机器人互动,提交查询,并像以前一样接收电影推荐,但这次它是完全在云端运行的。

如果某些事情没有按预期工作,请准备好以下清单进行故障排除:

  • 依赖性检查:确保你的 Dockerfile 使用 pip install -r requirements.txt 正确安装所有依赖项。缺少依赖项可能导致 Cloud Run 上的构建或运行时错误。

  • Cloud Shell 与本地环境:如果您没有使用 Google Cloud Shell,请确保您的本地环境通过具有适当权限的 Google Cloud 服务账户进行认证,以便于 Cloud Run、Artifact Registry 以及(如果适用)Vertex AI。

  • 监控日志和指标:您可以直接从 Google Cloud 控制台下的 Cloud Run 中监控您服务的日志、请求历史和性能指标。这对于调试和性能调整特别有用。

  • Cloud Run 服务管理:在云控制台中导航到 Cloud Run,您将看到已部署服务的列表。您的聊天机器人(例如,movies-chatbot)应该会出现在这里。点击服务名称将为您提供以下访问权限:

    • 公共服务 URL

    • 部署历史

    • 容器配置

    • 环境变量

    • 日志和错误报告

这种可见性使得跟踪和管理您的应用程序在部署后变得容易。

现在您的聊天机器人已经上线,部署在可扩展的无服务器平台上,并且公开可访问,您已经成功完成了部署之旅。您的基于 GenAI 的电影推荐聊天机器人现在可以使用了,可以分享,并且可以进一步改进。

将聊天机器人部署到其他云平台

如初始部分所述,一旦您准备好了 docker compose,您就可以遵循提供的说明将相同的应用程序部署到其他云平台:

当您遵循这些链接中的说明时,您会发现这与我们选择将应用程序容器化的部署方法有相似之处。

每个云平台的官方文档中都有大量信息,可以帮助您将 Spring Boot 应用程序部署到云平台。例如,如果您想运行我们在第 9 章和 10 章中创建的应用程序,您可以遵循各个云服务提供商提供的文档中的步骤:

这些文章足以指导你部署我们的 GenAI 应用程序,因为它的核心是一个简单的应用程序,不需要这样的扩展,因为它只执行增强——而且,那也是一个批量过程。

这些应用程序的生产部署是一个更复杂的流程,需要你关注各种支持元素,如数据库、监控等。讨论整个部署超出了本书的范围,但我们将突出部署架构和关键考虑因素,以便在下一节中部署你的应用程序。

准备在生产环境中部署:关键考虑因素

在本节中,我们将查看智能应用程序的典型架构部署。当我们迁移到生产环境时,还有很多其他方面我们需要考虑。为了简单起见,我们将参考我们在第九章第十章中构建的增强应用程序。

让我们看看从加载数据到审查结果的所有任务:

  1. 我们将数据加载到图中。

  2. 该图通过季节性关系进行了增强。我们使用增强应用程序——包括文章以及客户行为方面——来增强该图。

  3. 我们还利用了 KNN 相似性和社区检测算法来增强图,并回顾了这种方法如何给我们带来更好的结果。

在生产部署中,所有这些方面可能都需要自动化并作为独立的应用程序部署。让我们简要地看看所有这些我们需要注意的方面。

当我们将智能应用程序部署到生产环境时,我们需要确保我们关注数据摄取、数据消费、用于增强图的 LLM 或 ML 管道,以及用于扩展的图数据库部署架构。

让我们先看看图 12.1中的部署架构。

图 12.1 — Neo4j 智能应用的部署架构

图 12.1 — Neo4j 智能应用的部署架构

我们可以看到这里显示了两个不同的 Neo4j 数据库。在 Neo4j 的常规交互中,我们将拥有Primary,它可以执行READWRITE功能。对于分析、图数据科学GDS)和其他用途,我们将使用Secondary,它只提供READ功能。我们可以拥有多个Primary数据库以提供高可用性,以及多个Secondary数据库以提供横向扩展性。我们可以从图中看到,所有常规交互,包括数据摄取和数据消费,都由Primary处理,而增强图的负载分析工作由Secondary处理。

这种部署架构也使得维护和监控系统变得容易。Neo4j 数据库附带Neo4j Ops Manager (neo4j.com/docs/ops-manager/current/),用于部署和监控 Neo4j 服务器。它提供仪表板来监控系统的当前健康状况,并在出现错误时设置警报以通知用户。

对于其他应用程序,我们需要有类似的监控,特别是对于数据摄取和增强应用程序。当它们在中间失败时,我们应该能够从失败的地方重新启动。增强应用程序就是为此目的而构建的。

当我们构建数据摄取管道时,我们需要牢记这些方面:

  • 我们的初始数据量是多少?

  • 我们日常的更改(增量数据更改)及其大小是什么?

  • 增量数据是如何来的?它是以接近实时的方式,定期作为一个批次到来,还是在一天结束时作为一个大批次到来,还是由最终用户通过用户界面进行的交互式更改?

我们将探讨这些场景的最佳实践。

初始数据加载

如果我们从其他数据源或数据库迁移,我们可能需要第一次将数据移动到 Neo4j。根据数据量,我们必须决定我们是否可以采取事务方法来加载数据,或者利用名为neo4j-admin的离线数据导入方法。

第九章中,我们使用了事务方法来加载数据。如果我们加载的数据量不超过几百万条记录,比如说一亿条,我们可以在合理的时间内加载这些数据。当我们以事务方式加载数据时,Neo4j 数据库需要更新索引并保持事务日志分开,将数据提交到数据库。这给过程增加了相当多的开销。但这种方法给我们提供了更多的灵活性和可重用的代码,可以用于增量数据加载。如果我们正在将数据加载到集群中,我们可以使用这种方法来确保数据在集群中可用,因为 Neo4j 数据库服务器确保更改在集群中复制。我们使用了LOAD CSV选项来加载数据。您可以在neo4j.com/docs/cypher-manual/current/clauses/load-csv/了解更多信息。

这种方法对于概念验证和临时数据加载目的来说很好,但对于生产系统,数据摄取应该通过连接到数据库的客户端使用 Neo4j 协议来执行。虽然LOAD CSV选项简单且吸引人,但它使用数据库堆来加载数据和执行数据摄取,这可能不是所希望的。您可以在github.com/neo4j-field/pyingest找到一个基本的 Python 客户端应用程序,它可以向图中摄取数据。请注意,这是一个示例客户端,您需要构建一个适合您生产需求的客户端。

如果数据量更大,那么使用Neo4j Admin Import过程会更好。为此,我们需要以特定格式准备节点和关系的 CSV 文件,并使用neo4j-admin工具准备数据库。您可以在neo4j.com/docs/operations-manual/current/tools/neo4j-admin/neo4j-admin-import/了解更多关于 CSV 文件格式和示例的信息。

增量数据加载

增量数据加载的方法取决于我们将用于加载数据的框架。如果有很多流数据,那么利用像 Apache Kafka 这样的框架可能是个好主意。此外,使用 Java、JavaScript、.NET 或 Python 等语言框架构建与数据库交互以摄取数据的应用程序也很容易。

您可以在neo4j.com/docs/create-applications/了解如何为 Neo4j 构建客户端应用程序。我们需要注意的一点是利用管理事务函数,以便在集群因网络故障或服务器故障导致集群拓扑变化时,驱动程序可以在集群中根据需要重试事务。您可以在neo4j.com/docs/java-manual/current/transactions/了解更多关于此的信息。此链接指向 Java 使用,但相同的功能适用于所有受支持的语言框架。

图增强

虽然我们将文章增强和客户增强作为 Spring Boot 应用程序构建,但我们还没有调查自动化其他方面。在为特定季节的客户生成嵌入后,我们运行了如 KNN 相似性和社区检测等独立的 ML 方面。我们可能也需要自动化这些方面。每当新数据被摄入到图中时,我们可能需要触发增强应用程序以生成嵌入以适应我们加载的新数据,然后在该完成后触发 GDS 算法。请注意,我们查看的 ML 管道是 KNN 相似性和社区检测的简单链式连接;根据需要,它们可以更加复杂。

如果您想了解更多关于 Neo4j 的 ML 管道,您可以访问neo4j.com/docs/graph-data-science/current/machine-learning/machine-learning/

我们探讨了如何将推荐作为分析验证的一部分作为 Cypher 查询来查看。一旦我们对查询感到满意,我们可能需要构建一个应用程序,以便根据需要提供推荐。

我们的应用程序是基于 Spring 框架构建的,它提供了各种功能和选项来构建一个生产级的应用程序,这使得部署和监控应用程序变得更加容易。您可以在docs.spring.io/spring-boot/reference/packaging/index.html了解更多关于我们如何为生产部署打包 Spring 应用程序的信息。您可以在docs.spring.io/spring-boot/reference/actuator/index.html了解更多关于生产级特性,这些特性帮助我们监控应用程序。

这些是部署的关键原则和考虑因素;对于生产部署,需要考虑多个方面,包括适当的部署架构以及应用程序开发。需要部署多个流程以进行监控和性能评估,以确保应用程序随着数据和流量的增长而扩展。

摘要

在这一章的结尾,您学习了如何将 Haystack 驱动的 GenAI 聊天机器人从本地开发过渡到完全部署的、云托管的 Google Cloud Run 应用程序。我们一步步介绍了准备项目结构、使用 Docker 容器化应用程序、配置必要的 Google Cloud 服务,以及在可扩展和无服务器环境中部署聊天机器人。您还学习了如何验证部署、监控性能和解决常见问题——这些实用技能远超这个项目本身。

更重要的是,这一章节将所有内容串联起来。从理解知识图谱和向量搜索,到使用 Haystack 和 Neo4j 整合 GenAI 工作流程,最后将您的应用程序部署到云端——您现在拥有了构建智能、可扩展和现成生产环境的 GenAI 应用程序的完整端到端蓝图。

我们现在已经完成了使用 LLMs 构建 Neo4j 应用程序的旅程。接下来,我们将快速回顾这一旅程的关键收获。

第十三章:结语

我们已经探讨了相当多的主题,并研究了如何构建智能应用。现在,我们将回顾在前几章中学到的内容,并查看我们旅程的下一步。我们将回顾 Neo4j 如何最适合构建知识图谱,以及它是如何与 GenAI 生态系统集成,以轻松构建智能搜索和推荐应用。我们将涵盖以下主题:

  • GenAI 和 Neo4j 的联合力量

  • 超越书籍:探索持续学习的先进技术和资源

  • 结语

GenAI 和 Neo4j 的联合力量

第 1-3 章中,我们探讨了 GenAI 和 LLMs 的演变是如何进入画面,以及它们如何启动技术,使自然交互和易用性成为可能。我们还探讨了这些相同的由于工作方式而产生的功能,可以提供可信但可能实际上错误的信息,以及可能不存在的信息,这被称为幻觉。我们探讨了 RAG 如何帮助减少这些幻觉。

我们探讨了知识图谱以及我们如何有效地对它们进行建模以参与 RAG 流程,称为 GraphRAG 方法。通过集成不断发展的知识图谱,GraphRAG 可以比传统的 RAG 更有效、更准确,因为它可以轻松地将 LLMs 与易用性相结合。

GraphRAG 用于搜索应用

第 4-6 章中,我们开始了一段动手之旅,使用电影知识图谱构建强大的智能搜索体验。从结构化数据开始,我们使用 Python 建模和构建了图谱,并通过 Haystack 框架生成的向量嵌入来丰富它。这些嵌入直接存储在图中,帮助我们实现了 GraphRAG 管道的相似性搜索。

通过使用知识图谱的力量进行向量搜索,我们创建了一个混合检索系统,不仅能够回答简单的关键词查询,还能够回答可能需要多跳遍历以检索结果的复杂问题。通过 LLMs 和 GraphRAG 的集成,我们进入了智能搜索,它能够进行文档检索和上下文感知理解。

这些章节展示了将图中的结构化知识与 LLMs 的表达能力相结合的真正潜力。

GraphRAG 用于推荐

第 7-10 章 中,我们探讨了通过利用 GraphRAG 来构建推荐应用。我们利用 Langchain4J 和 Spring AI 来理解如何使用 Java 和 Spring 框架构建 GraphRAG 应用。我们遵循了众多步骤来构建一个智能推荐应用。第一步是通过加载 H&M 客户交易数据来构建知识图谱。接下来,一个重要的步骤是通过添加季节性关系来增强图谱,这有助于更细致地处理数据。然后,我们使用这些季节性关系作为 GraphRAG 流的一部分来生成特定季节的客户购买摘要,以及嵌入以丰富图谱并增加更多上下文。这些嵌入和图数据科学算法通过 KNN 算法捕捉了相似客户之间的关系。在创建相似性关系后,我们使用了社区检测算法将客户分组到社区中,以提供更好的推荐。我们还探讨了为什么这个流程比仅仅依赖简单的向量搜索本身能给出更好的推荐。

当我们构建这个应用时,我们使用 GraphRAG 不是为了生成供最终用户消费的文本,而是为了丰富知识图谱,以提供更好的推荐。这展示了 LLMs 不仅可以用作聊天机器人,还可以在丰富数据以更好地理解和以新的视角理解数据方面发挥作用。

选择您的云平台

第 11-12 章 专注于各种可用的云服务。我们进行了详细的比较,以帮助您为您的 GenAI 应用选择云平台。在最后一章中,我们演示了如何将您的应用部署到 Google Cloud。

接下来,我们将讨论如何超越本书中讨论的内容。

超越书籍:探索持续学习的资源

虽然我们已经通过简单的例子讨论了图数据建模的重要性,并探讨了两个特定的图数据模型示例,但了解 Neo4j 如何帮助构建更好的图谱以及其架构如何帮助以可扩展的方式构建知识图谱是明智的。Neo4j 提供以下资源,以了解更多关于 Neo4j、知识图谱和 GraphRAG 实现的信息:

让我们转到结束语。

结束语

我们已经探讨了新兴的 LLM 框架的各个方面以及如何使用这些框架构建更好的解决方案。虽然这项技术令人兴奋,并为我们的问题和解决方案提供了新的视角,但它仍处于起步阶段。它需要大量的处理能力,可能无法达到我们期望的服务级别协议(SLA)响应时间。虽然我们可能会为使用这种方法解决所有问题而感到兴奋,但评估尝试解决特定问题是否真的需要利用这些技术,或者我们是否可以更合理、更有效地解决它们,这一点很重要。例如,许多人对于 LLM 的能力感到兴奋,并试图通过描述问题来生成数据模型(无论是图、SQL 还是其他)。这把双刃剑可能会带来问题。如果我们对我们正在处理的数据不熟悉,我们可能无法验证生成的模型。如果我们非常熟悉数据,我们可能对数据的上下文和细微差别有更好的理解,而这些可能是 LLM 所缺少的。因此,当我们尝试以这种方式解决问题时,我们可能不会从中获得太多好处。所以,请记住,这可能是一把用于有效解决问题的手术刀,而不是一把斧头,所以请有效地使用它。

虽然这本书在这里结束,但您的旅程不必结束。凭借基础知识和工具以及工作示例,您现在可以开始实验、扩展并提升您自己的 GenAI 解决方案。无论您是构建智能助手、上下文搜索引擎还是个性化推荐系统,智能应用的未来现在就在您的手中。

继续构建。继续探索。最重要的是,通过知识图谱和生成式 AI 的力量,继续将想法、数据和人们联系起来。

保持关注

要了解生成式 AI 和 LLM 领域的最新发展,请订阅我们的每周通讯 AI_Distilled,packt.link/Q5UyU

对本书有任何疑问或想参与关于生成式 AI 和 LLM 的讨论?

加入我们的 Discord 服务器packt.link/4Bbd9和 Reddit 频道packt.link/wcYOQ,与志同道合的爱好者建立联系、分享和协作。

packtpub.com

订阅我们的在线数字图书馆,全面访问超过 7,000 本书和视频,以及领先的行业工具,帮助您规划个人发展并推进职业生涯。更多信息,请访问我们的网站。

为什么订阅?

通过来自 4,000 多位行业专业人士的实用电子书和视频,节省学习时间,增加编码时间

  • 通过为你量身定制的技能计划提高学习效果

  • 每月免费获得一本电子书或视频

  • 全文搜索,便于快速获取关键信息

  • 复制粘贴、打印和收藏内容

你知道 Packt 为每本书都提供电子书版本,包括 PDF 和 ePub 文件吗?你可以在 packtpub.com 升级到电子书版本,作为印刷书客户,你有权获得电子书副本的折扣。如需了解更多详情,请联系我们 customercare@packtpub.com

www.packtpub.com,你还可以阅读一系列免费的技术文章,注册各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。

你可能还会喜欢的其他书籍

如果你喜欢这本书,你可能对 Packt 的以下其他书籍也感兴趣:

包含网站的图片

描述自动生成](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/bd-neo4j-pwrd-app-llm/img/9781835884447.png)](https://packt.link/1835884458)

使用 Python 和 PyTorch 进行生成式 AI

约瑟夫·巴布科克,拉加夫·巴利

ISBN: 978-1-83588-444-7

  • 掌握 LLM 的核心概念和能力

  • 使用思维链、ReAct 和提示查询语言来构建有效的提示,引导 LLM 生成你期望的输出

  • 了解注意力和 Transformer 如何改变了 NLP

  • 通过结合 VAEs 优化你的扩散模型

  • 基于 LSTM 和 LLM 构建文本生成管道

  • 利用开源 LLM 的力量,如 Llama 和 Mistral,应用于各种

    应用

包含网站的图片

描述自动生成](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/bd-neo4j-pwrd-app-llm/img/9781836200079.png)](https://packt.link/1836200072)

LLM 工程师手册

保罗·尤斯廷,马克西姆·拉邦

ISBN: 9781836200079

  • 实现稳健的数据管道并管理 LLM 训练周期

  • 通过实际示例创建自己的 LLM 并进行优化

  • 通过深入了解核心 MLOps 原则,如编排器和提示监控,开始 LLMOps 的学习

    和提示监控

  • 进行监督微调和 LLM 评估

  • 使用 AWS 和其他工具部署端到端 LLM 解决方案

  • 设计可扩展和模块化的 LLM 系统

  • 通过构建特征和推理管道来了解 RAG 应用

Packt 正在寻找像你这样的作者

如果你对成为 Packt 的作者感兴趣,请访问 authors.packtpub.com 并今天申请。我们已与成千上万的开发者和技术专业人士合作,就像你一样,帮助他们将见解分享给全球技术社区。你可以提交一个一般性申请,申请我们正在招募作者的特定热门话题,或者提交你自己的想法。

分享你的想法

现在你已经完成了 《使用 LLMs 构建 Neo4j 应用程序》,我们非常想听听你的想法!如果你在亚马逊购买了这本书,请点击此处直接跳转到该书的亚马逊评论页面并分享你的反馈或在该购买网站上留下评论。

你的评价对我们和整个技术社区都至关重要,并将帮助我们确保我们提供高质量的内容。

第十四章:索引

A

加速 13

高级 Cypher 技术,复杂图结构

嵌套查询 93

模式匹配,使用路径模式 91

程序逻辑 92

子查询 92

可变长度关系 90

高级向量搜索

Haystack,连接到 Neo4j 102

AI 驱动的搜索系统

警报,为关键问题设置 132

最佳实践,用于维护和监控 131

日志策略,实施 132

Neo4j 和 Haystack 性能,监控 131

定期维护流程,

建立 132

AI 服务

由云服务提供商 233,234

参考链接 143

应用程序

使用 Docker 容器化 254,255

文章

通过特征进行过滤 223-227

过滤,属于其他

社区 219-227

AuraDB

参考链接 38

AuraDB Free 86

数据,导入到 87-89

实例,设置 86

AuraDS

参考链接 38

AWS

参考链接,为了安全和

合规性 246

参考链接,为了最佳安全性

实践 240

AWS 区域 243

Azure

参考链接,为了安全和

合规性 246

参考链接,为了最佳安全性

实践 240

Azure Active Directory (AAD) 241

B

光束搜索解码 22

来自 Transformer 的双向编码器表示 (BERT)

参考链接 4

C

缓存嵌入 128

缓存查询结果 128

链接路径模式 91

聊天机器人

部署到其他云 261

ChatGPT 4

部署 GenAI 的优势

解决方案 232

云计算选项

用于 GenAI 应用 232

云平台

决策框架,用于

选择 247-249

选择 269

云平台,用于 GenAI 应用

成本 236-239

关键考虑因素 234

性能 234-236

定价模型 236-239

可扩展性 234-236

安全和合规 239-244

云服务提供商

专用 AI 服务 233, 234

云安全联盟 (CSA)

参考链接 246

协同过滤 219

社区 213-219

使用 Louvain 算法检测 210-212

约束 74, 75

容器

部署到 Google Cloud Run 258, 259

容器化 254

基于内容的过滤 219

上下文感知搜索 119-121

余弦相似度 209

参考链接 209

CSV 文件

清洗 78-85

归一化 78-85

客户管理加密密钥

(CMEKs) 240

客户管理密钥 (CMKs) 241

Cypher

参考链接 38

D

数据清洗

需要 77, 78

数据加密 240

静态存储 240

在传输中 240

数据丢失预防 (DLP) API 245

数据归一化

需要 77, 78

数据集库 12

密集段落检索 20

部署

Haystack 聊天机器人,准备 252-254

部署,在生产环境中

图增强 264, 265

增量数据加载 264

初始数据加载 263, 264

关键考虑因素 262, 263

Docker

应用,容器化 254, 255

动态搜索查询

带有灵活的搜索过滤器 121, 122

E

Easy RAG 功能 143

嵌入模型 113

嵌入 24, 98

生成,用于电影情节 99-101

嵌入,在 LLM 的上下文中

参考链接 166

嵌入存储库

参考链接 143

端到端 RAG 流程

构建 30-34

实体-关系 (ER) 图 39

扩展的 Neo4j 功能

参考链接 138, 139

用于构建智能

应用 138, 139

F

Facebook AI Research (FAIR) 16

faiss-cpu 12

参考链接 12

微调 6, 15

G

生成式 AI

与 Neo4j 结合 267

潜在问题和伦理关注 6

生成式 AI 应用

云计算选项 232

生成式 AI 部署

最佳实践,用于安全和

合规性 245, 246

生成式 AI 生态系统

参考链接 144

一般数据保护条例

(GDPR) 243

生成式预训练 Transformer (GPT) 4

Google Cloud

活动项目,设置 256

部署,测试 260

部署,验证 260

项目,创建 256

参考链接,用于安全和

合规性 246

参考链接,用于最佳安全

实践 240

服务,启用 257

Google Cloud 区域服务 243

Google Cloud Run

容器,部署到 258, 259

Google Cloud Shell

启动 256

项目文件,添加 257

Google Cloud VPC 服务控制 242

GPT 模型

训练,使用 OpenAI 参数 5

GraphAcademy

参考链接 38

图数据模型 149

图数据建模

高级方法 49-57

基本方法 45-48

需要 39-41

RDBMS 数据建模 41-45

图数据科学 (GDS) 66

图数据科学 (GDS) 算法 205

推荐,改进 207

图建模

最佳实践 156-160

Graph RAG 58, 59, 145

流 39

用于推荐 268

用于搜索应用 268

图推理,Haystack 124

通过路径解锁见解

查询 125, 126

多重关系,遍历以揭示隐藏的见解 124

基础 16

H

堆栈

初始化 98, 99

Haystack 聊天机器人

准备,用于部署 252-254

Haystack 集成

微调 112

Haystack 日志记录和调试

参考链接 132

Haystack 优化和维护

参考链接 132

健康保险可携带性和问责制法案 (HIPAA) 243

H&M 个性化数据集

推荐图,建模

与 148-156

同质图 208

水平扩展 129,130

I

身份和访问管理(IAM) 241

事件响应 242

在上下文中学习 14

索引 74-76

J

基于 Java 的框架 144,145

K

关键词匹配 23,25,26

K-最近邻(KNN)算法 207

参考链接 207

相似性,计算 208-210

知识图谱 8,147

构建,用于 RAG 集成 59

增强 66

图数据科学(GDS) 66

在 LLMs 中的重要性 6,7

本体开发 66

角色,在 LLMs 中 7,8

设置,在 Neo4j 中 60-62

使用,与 LLMs 一起 9,10

L

LangChain4j 142

综合工具箱 143

特性 143

参考链接 142

设置 164-166

统一 APIs 142

语言模型

参考链接 143

大型语言模型(LLMs) 4-6,137

GenAI 进化,通过 4 概述

幻觉,因素 13,14

知识图谱的重要性 6,7

RAGs 的重要性 6,7

知识图谱,角色 7,8

使用知识图谱 9,10

负载均衡 129,130

logit 28

长短期记忆 (LSTM) 4

Louvain 算法 210

社区,检测 210-212

参考链接 210

M

电影知识图谱

构建 86

电影数据集

利用 77

多因素认证 (MFA) 241

多跳知识图谱查询路径 10

多跳推理 9

N

命名实体识别 (NER) 9

命名路径模式 91

Neo4j

知识图谱,设置 60-62

用于增强 RAG 模型 58, 59

向量搜索索引,创建 102

Neo4j Aura

参考链接 38

Neo4j AuraDB

参考链接 60

Neo4j 桌面

参考链接 38

Neo4j GDS 算法

参考链接 207

Neo4j 图

设计考虑,以提高效率

搜索 73-76

Neo4j 知识图谱

RAG,与 63-65 集成

使用 GraphRAG 57, 58

Neo4j 记录

参考链接 132

Neo4j 维护

参考链接 132

Neo4j 监控和警报

参考链接 132

Neo4j,优化

额外属性,索引 113, 114

查询,分析 114

查询,记录 114

Neo4j Python 驱动 38

Neo4j 查询

优化,用于大型图 127

嵌套查询 93

网络安全 241

节点 73

NumPy 12

O

实体集

参考链接 66

实体集开发 66

OpenAI

初始化 98, 99

P

pandas 12

文档检索 23, 27, 28

路径模式 90

个性化体验

构建,策略 140

程序逻辑 92

提示工程 14

参考链接 14

受保护的健康信息(PHI) 243

Python

参考链接 38

PyTorch

参考链接 12

Q

查询分析 57

R

RAG 方法 7

RAG 功能

参考链接 143

RAG 集成

用于构建知识图谱 59

RAG 模型

增强,使用 Neo4j 58, 59

rank_bm25 库 12

RDBMS 数据建模 41-45

推荐引擎,LangChain4j

应用属性,更新 169, 170

构建 166, 167

最终应用 179-186

Neo4j 集成 170-175

OpenAI 聊天集成 175-178

OpenAI 嵌入模型

集成 178, 179

项目依赖,更新 168, 169

推荐引擎,Spring AI

应用属性,更新 187

构建 186, 187

最终应用 192, 193

Neo4j 集成 188

OpenAI 聊天集成 188-191

OpenAI 嵌入模型

集成 191, 192

项目依赖,更新 187

推荐图

文章数据,加载 151-154

构建 149

客户数据,加载 150, 151

最终图 155, 156

模型,使用 H&M 个性化

数据集 148-156

事务数据,加载 154,155

建议

改进,使用 GDS 算法 207

推荐系统 140

微调 193-203

参考文献列表 141

循环神经网络 (RNN) 4

带有人类反馈的强化学习 (RLHF) 15

关系型数据库管理系统 (RDBMSs) 37

关系 73

相关性得分 28

检索增强生成

(RAG) 11,13,37

架构 16

流程,分解 17-23

重要性,在大型语言模型中 6,7

集成,使用 Neo4j 知识库

图 63-65

检索过程 23

检索技术和策略 23

关键词匹配 25,26

文档检索 27,28

向量相似度搜索 24,25

检索到的信息

集成 29,30

检索器 16

基于角色的访问控制 (RBAC) 240,241

基于规则的系统 141

优点 141

动态规则 141

局限性 142

静态规则 141

S

scikit-learn 12

搜索和推荐系统,差异

参考链接 139

搜索驱动的聊天机器人

构建,使用 Gradio 和 Haystack 109

Gradio,连接到完整流程 111

Gradio 接口,设置 109,110

集成,使用 Haystack 和 Neo4j 110

运行 112

搜索优化 123

安全信息和事件管理 (SIEM) 242

SentencePiece

参考链接 12

服务组织控制 2 (SOC 2) 243

相似度

使用 K-Nearest Neighbors 进行计算

(KNN) 208-210

相似度搜索

使用 Haystack 和 Neo4j 向量索引进行执行 103

单点登录 (SSO) 241

Spring AI 143

功能 144

设置 164-166

Spring Initializr

参考链接 164

SQLite 数据库

参考链接 38

子查询 92

T

威胁检测 242

Top K 邻居 207

Transformers 库 12

参考链接 11

V

可变长度关系 90

向量索引 129

向量搜索索引

在 Neo4j 中创建 102

向量搜索查询

使用 Cypher 和 Haystack 运行 105-107

使用 Haystack 和 Neo4j 运行 104

向量相似度搜索 23-25

虚拟私有云 (VPCs) 241

下载此书的免费 PDF 版本

感谢您购买此书!

你喜欢在移动中阅读,但无法携带你的印刷书籍到处走吗?

您的电子书购买是否与您选择的设备不兼容?

不要担心,现在每购买一本 Packt 书,您都可以免费获得该书的 DRM-free PDF 版本。

在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠远不止这些,您还可以获得独家折扣、时事通讯和每日收件箱中的精彩免费内容。

按照以下简单步骤获取优惠:

  1. 扫描下面的二维码或访问以下链接:

packt.link/free-ebook/9781836206231

  1. 提交您的购买证明。

  2. 就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件。

posted @ 2026-03-25 10:28  布客飞龙II  阅读(3)  评论(0)    收藏  举报