Transcription
欢迎大家来到 Takeoff RAG 课程。在本课程中,我们将教你如何使用 AI 模型进行检索技术。让我们开始吧。首先,我想谈谈一些先决条件,以确保你能从本课程中获得最大收益,并跟上我们在这里将要做的所有事情。
所以,第一点是,你需要具备一些基本的编码技能。如果你熟悉该平台,那么你很可能已经准备就绪。如果你到目前为止都能跟上其他所有内容,那么你不需要太多。如果你没有任何编码技能,请务必使用像 Cursor 这样的工具。我们显然有非常受欢迎的 Cursor 课程,所以去看看吧。所有这些 AI 工具的优点是,如果你从未写过代码,或者类似的事情,那么入门非常容易。所以你需要能够写一些代码来跟上这里的进度,但不必太复杂。
你需要一个 OpenAI 账户。在本课程中,我们将经常使用 OpenAI GPT-4o mini。要使用一个非常好的廉价模型,你需要 API 密钥才能访问他们的 API,并使用像他们的嵌入模型这样的东西。所以,如果你还没有 OpenAI 账户,你肯定需要去注册一个。我们还建议注册一个 Superbase 账户。
关于成本,非常快速地说一下,几美元就足够了。如果你在 OpenAI 账户里有 1 或 2 美元,你将有足够的积分。我们不会一下子用完大量的 API 积分。Superbase,你可以注册他们的免费套餐。我们在这里将要做的所有事情,你都可以在向量数据库方面免费使用 Superbase。所以你实际上不需要升级到专业套餐。
所以,你唯一可能需要花钱的地方就是通过 OpenAI 支付一点 API 积分。但是,如果你乐于使用一些现有的免费模型,也请随意使用。但我们大部分时间将使用 OpenAI 模型。如果你想深入研究一些高级内容,我们也会使用一些 Cohere 模型。如果你听说过 Cohere,我们将在课程后期使用他们的一些重新排序 API 来进行一些高级检索技术。如果你真的想深入研究,我也推荐一个 Cohere 账户。但这些是核心的三件事,你真的只需要担心。
话虽如此,让我们开始讨论本课程的目标。好的,到最后,我们希望确保你能够舒适地在一个真实的实际项目中构建一个完整的端到端 RAG 系统。无论你是要为你的公司构建内部工具,以帮助你更好地运营你的业务,还是只是想构建个人项目,因为你已经成为一名 AI 工程师,并且想要更好地了解事物,或者你是否有客户期望你构建项目并实际交付这些真实的检索系统。目标是,到最后,你将了解所有组成部分,了解它们如何协同工作,并且你将能够自信地走向世界并构建这些工具。
那么,你为什么需要学习 RAG 呢?RAG 非常重要,它是构建 AI 工具的基础部分之一。如果你不知道如何构建检索系统,那么构建真正高质量、真正有用的工具将非常困难。所以,如果你不熟悉 RAG,它代表检索增强生成。它基本上是向 LLM(大型语言模型,AI 模型)提供额外信息的过程,无论是来自知识库,还是来自互联网上的实时信息,以便 LLM 能够根据特定的上下文提供答案。
这在各种地方都很有用,对吧?所以,一些常见的 RAG 示例是,你知道,将 LLM 与知识库结合使用。对吧?所以,假设你拥有数百甚至数千个 Google 文档,可能在你自己的个人图书馆里,可能在你整个公司里。然后,有人有一个关于某种 HR 政策的问题,而这可能记录在 Google 文档中。那么,你将希望一个 RAG 系统能够根据用户的查询进行检索,对吧?假设你在某种公司聊天机器人或类似的东西中工作,然后你的一个员工说,“嘿,我们的 PTO 政策是什么?”那么,你希望你的 RAG 系统能够在你所有的文档中找到最相关的文档,以便将其添加到其上下文中。这样,当你调用那个 API,并且你实际上从 LLM,从这些 AI 实验室之一那里获得答案时,它实际上可以拥有回答问题所需的上下文。所以,知识库问答是你会看到的一个非常流行的用例。
你知道,另一个我们经常看到的用例是为内部工具获取关键业务信息。所以,这可能是客户数据、销售数据,对吧?假设你正在构建一个客户支持代理。那么,你需要的一件事是,当客户说,“嘿,我想从月度套餐升级到年度套餐”之类的。那么,你将能够,你需要能够从 Stripe 或类似的东西中获取实时数据,并且你需要一个 RAG 来做到这一点。
另一个流行的用例是访问互联网上最新的实时来源。所以,这对于天气、股票信息等很有用。对吧?如果你问 ChatGPT,例如,苹果的股票价格是多少?它不会知道,它不在其训练数据中。OpenAI 当然,如果你最近使用过 ChatGPT,它有一些网络浏览功能。而 OpenAI 在后台所做的就是,他们有一个检索系统,正在从互联网上获取最新的信息来帮助回答这个问题。所以,获取实时数据是 RAG 的一个很好的用例。
而且,最后一个真正真正核心的常见示例是使用 RAG 作为代理工作流的长期记忆系统。所以,这有点复杂。我们将在我们的代理课程中更侧重于这三个方面。我们可能会在代理的记忆方面更多地涉及。但这基本上是你看到的 RAG 的一些主要用例。
显然,市面上存在的最受欢迎的产品之一,它使用了一个非常复杂的检索系统,就是 Perplexity。所以,Perplexity 就是,你知道,可以说是 AI 版的 Google,如果你愿意的话。你问一个问题,它不会仅仅给你一堆链接,而是使用链接来实际生成 AI,获得一个 AI 生成的答案。所以,在这里你看到,我有一个例子,我们问“英伟达的价格是多少?”你可以看到他们正在从互联网上的信息中抓取实时来源。所以,他们正在做这个,这个事情,他们正在使用它来给你最准确的数据,并用一个 AI 生成的答案来补充它,这个答案就在这个图表下面。我可能应该在这里包含它,但我认为你大概明白了。所以,你有这些价值数十亿美元的大公司正在构建超级高级的检索系统。如果你想构建真正出色的 AI 产品和工具,学习如何构建这些系统非常关键。
所以,让我们在开始学习所有这些东西之前,快速谈谈课程大纲。我们已经将事情分成了三个部分。这些是核心的三部分,然后我们可能会有一个第四部分,包含一些一次性视频,供那些想要一些额外的、分散的资源的人使用。但核心的三部分将是学习基础知识。所以,我们将学习嵌入、向量数据库、相似性搜索。我们还将学习一些基本的提示技术,以帮助你从与 RAG 相关的 AI 查询中获得最大收益。所以,所有这些基础知识,你知道,当你看到像 Perplexity 这样的产品时,所有这些东西都在发挥作用。所以,学习其中每个部分如何融入这个谜题非常重要。
我们在第二部分将要做的是,我们将实际创建一个完整的端到端 RAG 系统。所以,这将更多地是一个后端重点。我们将从零开始编写代码,来尝试实现一个非常基本想法。所以,我们将在这里的第二部分编写大量代码。在第三部分,我们将构建一个完整的真实世界检索项目。所以,我们将把我们在前两个系统中学习到的所有东西结合起来。我们将构建一个带有实际 UI、实际应用程序的基本工具,该应用程序与我们将要构建的 RAG 系统集成。所以,到最后,你应该能够再次做到,你应该能够构建任何类型的 RAG 系统,任何你拥有的工具,无论它是为你自己、为你的企业、为客户,或者你有什么。我们将确保你学到所有你需要的东西,以便能够出去构建所有这些很酷的检索系统。因为一旦你学会了如何构建检索系统,它真的会提升你作为一名 AI 工程师的经验水平,以及作为一个能够出去实现一些产品想法的人的能力。
话虽如此,让我们开始学习基础知识。在本节中,我们将介绍嵌入。我们将介绍向量数据库,这是你如何存储带有嵌入数字的内容。我们如何对向量数据库执行相似性搜索,以便根据给定的查询检索最相关的信息。以及一些你可以使用的提示技巧,以帮助从你的检索系统中获得更多一点。
所以,让我们先对 RAG 进行一个简短的概述。我知道我已经稍微提到了这一点,但我想确保每个人都理解 RAG 是什么,以及为什么检索系统在构建 AI 应用程序方面如此重要。所以,再次,RAG 只是一个花哨的首字母缩略词,代表检索增强生成。在本课程中,你经常会听到我使用 RAG 或检索。我个人更喜欢检索系统而不是 RAG 这个词。我认为 RAG 有点……我认为当你开始引入一堆首字母缩略词时,它只会让人们感到更困惑。你知道,很多研究人员喜欢它,这显然是它的起源。检索,如果我说,“嘿,我们正在进行文档检索”,我认为对普通人来说,这要清楚得多。所以,你会在本课程中经常听到我交替使用检索和 RAG。
而检索系统只是在增强你的大型语言模型。所以,如果你正在调用,你知道,GPT-4,例如,也许你正在使用 Cloud 3.5 Sonnet 作为你的模型。你试图用检索系统做的是,你试图从外部来源访问额外信息,将其注入你的查询中,以便你能够获得一个更好、更详细的答案,该答案由与你的查询相关的任何信息补充。
所以,我们已经在上一节中涵盖了一些实时示例。但是,你知道,RAG 的一个很好的用例是根据实时天气数据回答问题。为什么呢?你知道,如果一个一年前训练的模型被问到今天的天气问题,它显然无法给你一个准确的答案。你需要用额外的信息来补充那个 LLM。所以,这就是检索系统的全部前提。
那么,RAG 解决了什么问题呢?第一点是准确性和可靠性。大型语言模型的一个问题是幻觉。有时它会自信地给你一个明显错误的答案。也许,例如,如果你问一个模型法国的首都是什么,它会,你知道,给出伦敦的答案。显然,那是错的。那就是我们所说的幻觉。模型现在会正确处理如此简单的事情,但重点是,也许你不想让 AI 模型直接给你答案,也许你想用一点额外的信息来补充你给它的提示。也许你检索,你知道,你有一个 RAG 系统,它根据你给它的国家,会实际检索出正确的首都答案。所以,这将是使用 RAG 系统来减少幻觉的一个例子。
所以,你知道,另一个例子,我们提到了股票信息,最新的金融信息,最新的天气数据。使用最新的数据可以真正驱动像 Perplexity、像 GPT 网络搜索这样的令人难以置信的产品。所以,如果你需要访问尽可能最新的信息,对吧?假设你问一个关于,你知道,NBA 比赛的比分是多少?也许你想知道勇士队今晚的比分是多少?你需要一个 RAG 系统来实际获取这些信息。
另一个是事实核查。你知道,所以也许你想,你知道,根据,你知道,一篇科学论文或什么东西来问一个问题。那么,你可以做的是,你可以使用一个检索系统来根据某种参考文档检查模型给出的答案。所以,RAG 系统的好处是,你可以通过用额外的外部信息来补充你的提示,来真正提高你答案的准确性和可靠性。
这基本上是同一枚硬币的两个方面,那就是知识增强。所以,这包括访问你的专有数据。所以,假设你正在为你的公司构建内部工具。你知道,也许你有一些 CRM,对你的所有团队成员来说是内部的。你知道,也许你想构建一个 AI 工具,你可以通过聊天界面快速获取信息。所以,这将是使用检索系统访问专有数据的一个好例子。
你可以结合多个数据源,这真的很酷。也许你保存了很多 PDF 文件,你想将它们与,你知道,一些 Notion 页面结合起来。也许你使用 Notion 中的公司维基或类似的东西。所以,你可以开始混合搭配所有这些不同的来源。
这让我想到第三点,那就是你也可以构建多模态检索系统。所以,也许你不仅有很多文本文件,还有很多图像。你也可以,你知道,也许你有很多图表。你知道,假设你知道,你有一个工具报告你上个季度的销售数字或类似的东西。也许里面有几张图表你想包含进去,而那更多是基于图像而不是文本。所以,你可以做的是,你可以混合搭配所有这些不同的数据片段。最终,我们将进入视频、音频等领域,这实际上已经开始发生了。所以,你可以混合搭配所有这些,不仅仅是数据来源,还有实际的数据类型本身。
而检索系统真正有用的第三件事是成本和效率。所以,LLM 模型正在获得越来越长的上下文窗口。这意味着在提示中,在对话历史中,你能够将更多信息塞进这些请求中。但是,如果你最大化上下文窗口,这些请求会更贵,速度会更慢。所以,通过使用检索机制,你可以做一些事情,比如,你知道,而不是将 50 个文档包含到某个东西中,你可以实际只获取,你知道,最相关的五个文档。将你的成本降低 10 倍,获得更快的响应时间。所以,根据你想要构建的内容以及你想要如何管理你的成本和延迟等内容,RAG 系统确实可以从中受益。
所以,为了更深入地探讨我刚才提到的上下文窗口这一点,让我先解释一下,如果你因为某种原因不熟悉上下文窗口是什么。上下文窗口的概念。所以,这里我们只是在 OpenAI 的模型页面上,我们正在查看他们的 GPT-4o 模型,你可以看到它的上下文窗口是 128,000 个 token。所以,为了简要回顾一下,当 GPT-4 最初发布时,我认为上下文窗口是 4,000 或 8,000 个 token,我不记得是哪个了。但它基本上比今天小得多。所以,现在一年半后,当我们有了 GPT-4o,现在我们有 128,000 个 token 的上下文。所以,这意味着我们可以将更多的文本、更多的图像放入这些对 AI 模型的请求中。这意味着我们有能力,你知道,我相信我可以说,整个哈利波特系列,或者类似的东西,大约是 100,000 个 token,大概在这个范围内。我可能在确切的数字上错了,但重点是,你知道,一年半前你可能只能放一本书,现在你可以放一整个系列。
所以,展望未来,这些上下文窗口将继续增长,增长,增长。所以,你听到人们争论的一个问题是,RAG 是否已经过时?上下文窗口,你知道,当它们达到,例如,Google Gemini 模型已经有百万 token 的上下文窗口时。当这些达到 1000 万、1 亿、10 亿时会发生什么?RAG 还有意义吗?我的答案是,RAG 与上下文窗口协同工作,这不是一个非此即彼的情况。RAG,因为我们在这里列出的所有这些原因,准确性和可靠性,知识增强,成本和效率。RAG 仍然将是未来很重要的事情。
所以,如果你听到人们说,“嘿,上下文窗口正在增长,你不再需要担心 RAG 了。”我的回答是,那根本不是真的。任何在现实世界中实际构建这些系统、构建这些产品的人,RAG 是他们基础设施中非常关键的一部分。所以,上下文窗口让你能够填充,基本上是在 AI 请求中使用更多的内存,这很棒。我们想要更大的上下文窗口,但我们也希望能够继续构建检索系统,以便我们可以利用这些上下文窗口的多少,并拥有最终的灵活性来管理这些上下文窗口。所以,在很多方面,RAG 是一种管理上下文窗口的有用性和效率的方式。
所以,让我们在开始深入研究这些特定部分之前,快速浏览一下整个系统的概述,以便让你对我们将在接下来的课程中探索的过程有一个更好的感觉。所以,检索系统的工作方式是,你想输入一个用户查询。所以,在这种情况下,你知道,想象一下我们有一个聊天机器人,它根据维基百科页面或其他东西回答问题。所以,假设用户问,“哈勃太空望远镜是在哪一年发射的?”所以,在用户这边,他们输入查询。在我们为他们构建的后端会发生很多事情,他们不需要担心。他们的查询会用这个系统的输出进行补充,然后它会使用查询以及相关的文档,在这种情况下,将是相关的维基百科页面,以及可能来自哈勃太空望远镜维基百科页面本身的章节,来从语言模型中获得答案。
所以,这个过程在后端的工作方式,以及这是我们将要学习如何构建的系统,是第一步,它接收用户的查询,并将其发送到嵌入模型。我不会深入探讨每个细节,因为我们显然会有即将到来的课程,会更深入地探讨这些细节。但想法是,查询被发送到嵌入模型,这是一个 AI 模型,它基本上将文本或图像(为了简单起见,我们将只坚持文本)转换成一串巨大的数字。而这些数字的含义,这些数字序列的含义是,它基本上试图编码存储在文本中的信息,并将其转换为我们可以对其进行各种操作的数字格式。这就是查询嵌入将要做的。
所以,你将你的查询发送到嵌入模型,该嵌入模型吐出一个查询嵌入,它只是一系列数字。我们通常会做的是,我们将这个嵌入存储在某种向量数据库中。我们将在详细介绍向量数据库,并向你展示如何设置所有这些。你通常会进行某种检索相似性搜索,将你从嵌入模型获得的嵌入与你存储在向量数据库中的所有嵌入和内容进行比较。所以,输出是你通过这个从查询到相关片段的整个过程,并且你能够将该查询与你存储的最相关信息进行匹配。再次,你将它们组合在一起,发送到,你知道,Cloud 3.5 Sonnet,或者 GPT-4L,或者 Gemini 1.5,无论你偏好的模型是什么,然后你就会得到答案。
所以,这就是我们将在那里探索的旅程。我们将从头开始构建这些步骤中的每一个,向你展示这些拼图的各个部分是如何组合在一起的,解释这些部分是什么,为什么它们很重要,如何充分利用它们,并且我们应该能够为你设置好所有这些。
所以,让我们在这里结束这一课,然后实际设置一些代码,以便我们可以真正深入研究这个问题。好了,让我们设置一些代码。我们将需要你导航到下面的链接。这将是存储库链接。我会为你链接好。在你看到它的时候,它将是公开的。但我们将要做的是,如果你一直在关注我们 Takeoff 的所有其他课程,这是一个非常标准的流程。我们将使用 Git 来克隆我们将要用于编写本课程所有代码的存储库。
所以,你将需要做两件事之一。你可以下载 zip 文件,然后直接下载代码,或者我推荐的方式是直接使用 Git。这只是一个很好的技能,也是我们在这里 Takeoff 通常的业务方式。所以,你将需要点击这个存储库上的大绿色按钮,你将需要将这个 URL 复制到你的剪贴板。我们将需要进入一个终端。所以我现在在 Mac 上。我目前在我的终端里。我们将运行 `git clone`,然后粘贴这个 URL。你将看到我们已经将 Takeoff RAG 课程克隆到了我的电脑上。
所以,我现在要做的是,我将通过更改目录到 `takeoff-rag-course` 来导航。你可以看到我现在在我的终端里打开了它。然后,你将需要用你选择的 IDE 打开它。我将使用 Cursor。这是一个我设置的快捷方式来打开它。你可能需要手动打开它,只需打开 Cursor 应用程序,然后打开一个新文件夹,打开文件夹。你只需要确保你在这里设置好了。再次,我强烈建议你确实使用 Cursor。这就是我们 Takeoff 上所有项目的方式。如果情况发生变化,当然我们会调整。但目前,我认为 Cursor 是迄今为止最好的 AI 编码工具。
所以,一旦你在 Cursor 中设置好了存储库,我们需要做的是,为了确保你已经全部设置好,我们将在这里打开一个终端。如果你更喜欢使用你的自定义终端,那很好。我只是喜欢使用 IDE 中的这个,非常方便。我们将运行 `npm install`。这是 npm install,它将安装我们在这个项目中将要使用的各种包。所以,像 OpenAI SDK、Cohere SDK 之类的东西。
所以,你还需要在这里做的是,对于第一部分,学习基础知识部分,你需要两样东西。所以,如果我们去我们的 `sections` 文件夹,你会看到我们有一个文件夹,它是 `1`,`section one learn the basics`。这将是我们在这里将要使用的所有代码。再次,正如我在本课程一开始提到的,第一部分主要是你只需要看我。我们将讨论概念性问题,然后在第二部分我们将实际从头开始构建一个 RAG 系统。
如果你想自己做,并跟着我一起写代码,同时解释正在发生的事情,这绝对是一个很好的练习。你写的代码越多,就越能巩固概念,就越能让你对正在发生的事情有一个更基本的理解。如果你真的做了,但如果你不想这样做,我们将在第二部分做所有事情。所以,重复次数越多越好,但现在不用担心。
你现在需要担心的是,你会看到在这个 `env.example` 文件中,在 `learn the basics` 部分,这是我们需要两个密钥。所以,我们需要一个 OpenAI API 密钥,然后我们需要一个来自 Superbase 的数据库 URL。如果你只是跟着我,你不需要这些密钥。所以,如果你只想观看这些部分,从概念上理解第一部分正在发生的事情,不用担心。我在这里会涵盖它,只是为了那些确实想运行代码并可能自己写出这些练习的人。
但基本上,你需要做的是,你需要在这里获取一个 OpenAI 密钥。我将在之后做这件事,因为我现在不想暴露我的密钥。你所要做的就是在这里粘贴任何密钥值。然后在 Superbase 方面,再次,我提到你可以使用免费账户,完全没问题。你可以在这里创建一个新项目。在这里填写项目名称。你可以在免费套餐中使用微型大小。然后你只需要在这里输入一个数据库密码并保存。你也可以生成一个,这通常是我做的。
然后,你将需要点击创建新项目。这将带你到一个设置屏幕。通常需要等待一两分钟才能让数据库实际启动。但一旦一切都设置好了,你将需要去项目设置,然后去数据库。你需要做以下事情:你需要复制这个连接字符串 URL。所以,它将是顶部的这个大字符串。你可以点击那个复制按钮。你可以把它粘贴在这里。然后,你将需要输入你输入的或生成的密码,然后你将需要把它放在这里的值中。所以,如果我的密码是“这是我的密码”,那么我的值就是这样。
到目前为止,你将拥有这两者。所以,我在这里要做的是,你会注意到我们实际上是在 `env.example` 文件中。我刚刚注意到,你实际上需要在这个文件夹中创建一个新文件,它叫做 `.env.local`。所以,你应该在 `env.example` 的旁边有一个 `.env.local` 文件,它将是灰色的,因为我们已经将其从 Git 中忽略了,这样你实际上就不会提交它们。而你真正需要做的是,确保你删除 `example` 中的这些值。你不想提交它们,你想把它们放在你的 `.env.local` 文件中。
所以,事情应该看起来像这样。你应该在这里有你的 OpenAI 密钥,你应该在这里有数据库连接字符串,以及你密码的任何值。所以我将把我的放进去,然后我马上回来。
好的,我已经把我的环境变量放到了我的 `.env.local` 文件里了。我们将要做的是,为了测试它是否设置正确,如果你去这个 `one-setup` 文件,我把它编号了,这样它在我们整个部分中可以稍微好一点。你会注意到我们有一个非常基本的函数,那就是我们正在做一个非常基本的调用 OpenAI API,使用 GPT-4o mini。我们使用 40 mini 是因为它是一个非常便宜的模型,你可以用你的 API 积分进行测试。
你现在应该能够复制这个路径。所以,如果我复制路径,顺便说一句,我只是右键单击。所以,我们只是复制路径,然后我们将运行 `tsx`,然后我们将粘贴这个值。我们应该看到,你注意到我实际上收到了一些错误。我怀疑,哦,你知道是什么。所以,我们需要做的是,我们只需要导航到这个部分。所以,你会看到我在我的终端里在 `rag-course`。我们将 `cd` 进入 `sections`。所以,这只是更改目录。然后我们还将进入这个第一个部分。所以,你可以看到我现在在这个部分。
然后,如果我点击我的向上箭头键,这实际上会填充。如果我按三次,我们将再次获得那个命令。我应该能够运行它。现在我们可以看到,我们在终端里打印出“你好,我今天能帮你什么?”所以,我们消除了错误消息,这是由于我们的配置方式。所以,你将需要确保你在终端中导航到这个文件夹,我们将在那里运行本节的代码。然后,如果你使用 `tsx`,也就是 TypeScript execute,然后是文件名,你将能够得到响应。
所以,你可以看到第 19 行,我们正在控制台记录响应。我们正在记录来自 OpenAI API 的消息内容。这意味着你的 OpenAI 密钥正在工作,你应该一切就绪。所以,如果你成功了,这意味着你可能已经准备好继续了。再次,你不需要在这里的第一部分跟随并自己编写代码。你会在第二部分这样做。所以,如果你只是想获得概念上的理解,请随意跳过。但我知道很多人喜欢重复练习,喜欢获得额外的练习。这是我推荐的。如果你有时间的话。
话虽如此,让我们继续讨论嵌入。让我们来谈谈嵌入。嵌入是一种编码数据的方式,可以使它们在各种用例中更容易处理。OpenAI 在其开发者文档网站上有一个关于向量嵌入的页面,这是一个非常好的页面,我推荐阅读。所以,你可以看到这里有很多信息可以阅读。有很多不同的代码示例,很多不同的用例,它涵盖了。我推荐阅读这个页面。我认为它让你对嵌入为何重要以及你通常会看到的一些常见用例有一个非常好的了解。
所以我认为这句话真正地说明了重点,也是我们想在本课程中强调的重点是,OpenAI 的文本和/或嵌入通常会衡量文本字符串的相关性。所以,这里有一个小提示,我们将专注于文本。你可以对图像等事物使用嵌入,但为了简单起见,我们将专注于文本。这里的想法是,我们试图获取一段文本,并试图对其进行编码,并将其转换为数字形式。
所以,让我们来谈谈嵌入的核心。你可以看到我的幻灯片实际上没有更新。我必须确保我可能要为你更新它。但正如我所说,嵌入是一种方式,这很有趣。你知道,我们将保留它。我们在 Takeoff 很有趣。我们必须偶尔给人们一点理由微笑。但尤其是在本课程中,我们将真正专注于嵌入的搜索方面。所以,我们将要做的是,我们将,你知道,获取一堆嵌入文本。所以,假设你有 1000 篇不同的文档。我们将嵌入所有这些不同的文档。然后,我们将能够,我们将能够获取那个嵌入,再次,那一串数字,它现在代表我们已经编码的文本数据,变成了一串数字。我们将把它用于搜索目的。
所以,在本课中,我们想谈谈我们如何实际生成这些嵌入,以及我们可以用这些嵌入做什么。所以,让我们回到幻灯片。这是一个很好的嵌入空间示例。所以,一开始有点棘手的是,你试图理解,好的,我知道嵌入将文本变成一堆数字,但我对嵌入不理解的是,这真的很常见。所以,如果你感到困惑,你并不孤单。人们通常会问,“这些数字如何相互比较?”所以,我们不会深入研究事物的数学原理。我们将尽量简化它。你不需要理解嵌入工作原理的所有数学原理,为什么有这么多数字,以及所有这些不同的浮点数意味着什么。
你需要理解的是,假设我们嵌入了像“花园”这样的东西。所以,我们只是字面意思上取文本,单词“花园”,将其发送到嵌入模型。例如,你知道,OpenAI 的 text-embedding-3-small 模型,这就是我们马上要用的。它会给我们返回一串数字。而这些数字的含义是,你基本上可以在 3D 空间中绘制那个单词的位置。当你开始嵌入所有这些其他单词时,你会得到图表上的所有这些不同的点。
所以,例如,如果我们嵌入单词“软管”,我们将得到一个嵌入,就相似性搜索而言,基于它们的数字,它会非常接近。花园和软管非常接近,这很有道理。花园和软管是相当相似的词。显然,它们不完全一样。但是,你知道,如果有人说“软管”这个词,你可以说,“也许他们的软管在他们的花园里。”但是,例如,如果有人说“花园”和“厨房”这两个词,它们将不那么相似。所以,你会看到,花园和厨房,以及花园和软管之间的距离要远得多。
以“颜色”和“油漆”为例。“颜色”和“油漆”。油漆总是某种颜色。所以,这些将是相当接近的术语。但是,如果我们使用“颜色”和“微波炉”这两个词,它们将远得多。而且,你也可以看到,你正在获得这些分组。花园、软管和洒水器在这里聚集在一起。然后,我们有梳妆台、水槽、浴室、厨房、马桶。这些都聚集在这里。
所以,你会注意到,如果我们进行某种相似性搜索,然后我们说,“嘿,你知道,我们需要问一个关于冰箱之类的问题。”那么,你的相似性搜索将聚集在这里,并获取这个区域的文档。所以,你可以在图表上直观地看到嵌入是如何有意义的。想法是,这是一个很好的心智模型,嵌入是将所有这些不同的单词绘制在我们的情况下,它更像是段落或整个文档,但它们会试图将它们相似地绘制在一起。当我们进行相似性搜索时,你知道,如果我们搜索“混凝土”,我们将获取这个区域的东西。所以,这就是嵌入有用的基础。你可以获取文本,将其嵌入数字,然后进行所有这些不同的操作。我们试图获取文本,我们试图将语义信息编码成数字形式,以便我们可以对其进行检索操作。
所以,让我们更深入地了解如何生成嵌入。所以,我们实际上需要看看,也许我们现在开始在概念上理解它了。如果你仍然感到有点迷茫,请坚持下去。当我们开始实际做这些事情时,事情会变得更清晰。你知道,这通常在不同的阶段,以不同的方式对人们来说变得清晰。所以,对你们中的一些人来说,这可能会在这里变得清晰。
我们想打开这个 `two-generate-embeddings` 文件。我们将要做的是,我们将解释这个文件里发生了什么。所以,在最上面,我们只是导入 OpenAI,我们导入 `dotenv`。我们只需要导入环境变量。我们在这里做的是,我们正在初始化 OpenAI SDK。所以,这将允许我们非常轻松地使用任何 API 端点,通过我们保存为变量的 OpenAI 客户端。
在这种情况下,我们正在获取一个响应,并试图发送一个特定的文本数组。所以,我们将发送很多不同的文档。在这种情况下,你可以看到我们使用的例子是“你好,世界”和“再见,世界”。所以,我们正在获取这两个字符串,我们将它们传递到 OpenAI 的 `create_embeddings` 端点,通过 OpenAI SDK。我们正在使用 OpenAI 的 `text-embedding-3-small` 模型。我们正在使用 256 个维度进行这些嵌入。我将在几节课中更深入地介绍这一点。我们将讨论为什么嵌入维度很重要,以及你如何根据你的用例进行优化。我们甚至会讨论什么时候你可能想使用 `text-embedding-3-small` 而不是 `text-embedding-3-large` 或其他嵌入模型。
但重点是,我们正在获取这两个文本片段,我们将它们发送到 OpenAI 嵌入模型。我选择这些数字的原因是,这是最便宜的方式。顺便说一句,嵌入非常便宜。所以,如果我跳转到 OpenAI 的定价页面,特别是你会看到 `text-embedding-3-small` 大约比 `text-embedding-3-large` 便宜大约六倍。然后我们使用较小的维度来使我们的操作更快,更容易存储,最终当我们设置向量数据库时。所以,这就是我们现在使用这个特定模型和这个维度数量的原因。再次,当我们进入更高级的 RAG 系统时,我们将更深入地讨论这一点。但重点是,我们输入了这两个字符串,我们都得到了嵌入。
所以,为了举例说明,我们正在做的是,我们试图获取那些单词,那些单词集,在我们的用例中不是单个单词,然后将其绘制在这个图上。所以,我实际上要做的是,如果我们复制那个文件路径,然后运行 `tsx` 来运行那个文件,你会看到两个。
所以你会看到我们正在记录我们的响应,以便在某些情况下,我们将记录嵌入,你可以看到我们有所有这些数字,这就是一个嵌入,你也可以看到我们也在记录长度,所以我们在这里得到了两个嵌入,因为我们传入了两个字符串,这很合理。嗯,你知道,如果我在这里做第三个,那就是我的名字是好的,如果我们再运行一次,你会看到我们会得到三个,嗯,如果你在这里向上滚动,你看到所有这些都是黄色的,对你来说,它可能是不同的颜色,这取决于你的编辑器主题,我们会把它做得更大,你在这里看到的是我们得到了那些嵌入,好的,所以这是一个256个数字的长数组,好的,那个256的数字来自维度,所以维度翻译过来就是嵌入中数字的数量,我们将要使用的,好的,所以如果你使用默认数字,我认为默认数字是1536或者其他什么,我们使用256是为了让事情稍微小一点,但是,但是我们在这里得到了所有这些浮点数,它们只是大十进制数,所有这些东西结合起来形成了我们的句子、术语、词语在我们的某种图形图上应该如何放置,好的,所以一旦我们有了这个的嵌入,在接下来的课程中,我们将要弄清楚,好的,我们如何存储这段文本及其完整的嵌入在一个向量数据库中,好的,然后一旦我们将信息存储在向量中,我们如何执行某种相似性搜索,好的,所以所有这些部分都将是我们将在接下来的视频中涵盖的内容,所以你可以看到,你知道,我们将要上传文档,我们将进行检索,好的,所以我们将学习这些嵌入是如何使用的,但总的来说,这就是嵌入,它是一种将文本编码数据的方法,我们希望能够使用输出的数字来衡量这些字符串的相关性,所以我建议你去看看OpenAI的页面,他们的文档在这方面做得很好,如果你想更深入地了解其中一些参数,你也可以查看API参考,我们将在构建这些越来越高级的RAG系统时详细介绍这些,你甚至可以点击嵌入模型,好的,我们将使用三系列,所以我们将在本视频中使用text-embedding-3-small,但他们有一个更强大的模型,那就是大型版本,它只是再次,它只是更贵,而且这里的价格非常便宜,所以对于使用GPT-4o mini和text-embedding-3-small来说,事情会非常便宜且易于处理,好的,所以希望你对嵌入是什么以及我们如何编码信息以及它如何在我们的图形图上发挥作用有了更多的了解,我们在那里只是试图构建某种嵌入空间,我们试图获取所有这些词语、所有这些句子、我们正在处理的所有文本信息,然后得到我们可以使用的数字,然后对其执行操作并存储数据,以便我们可以构建高级检索系统,所以让我们进入实用性,而不是仅仅是什么是嵌入,我们如何使用它,所以现在你对嵌入是什么有了更多的了解,你知道如何使用像OpenAI的text-embedding-3-small或large模型这样的嵌入模型来生成嵌入,让我们来谈谈如何将这些嵌入及其相关文本存储在向量数据库中,好的,所以向量数据库将允许你存储你的嵌入数据,让我们来谈谈向量数据库是什么以及为什么我们需要它们,所以它们的核心是向量数据库或向量DB,它们基本上是专门设计的数据库,用于存储向量数据,所以当我们说高维向量数据时,这里我们再次指的是我们特定嵌入的维度,在这种情况下,在我们的例子中,我们使用256个维度,通过OpenAI API,这是你可以使用的最小维度数量,再次,这些维度对应于你的实际嵌入中你将获得的数字数量,所以如果我们将其设为1000,我们将获得1000个数字,如果1500,我们将获得1500个数字,等等,向量数据库是非常高效的数据库,你想要用于嵌入,所以你不想使用传统数据库,因为问题是存储变得非常昂贵,你的查询非常慢,效率非常低,它很快就会变成一个绝对的噩梦,即使你不需要扩展到数百万用户,你开始扩展到数百个用户,使用传统数据库,你就会开始遇到各种各样的问题,这些数据库中的列类型从技术角度来看根本不是为了处理这些高维向量而设计的,所以我想在这里首先说明一点,嵌入和向量你可以互换使用,虽然有一些细微差别,但你可以几乎用嵌入数据库替换向量数据库,就我们所做的而言,这两个基本上意味着相同的事情,但我们想做的是,我们想谈谈存在的不同类型的向量数据库,为什么我们将使用一个非常基本的Postgres数据库,并带有PGVector扩展作为我们的数据库,我只是想给你一个关于向量数据库格局以及市场格局的概述,所以再次,我们将使用Supabase,它只是一个Postgres提供商,我们使用Supabase的原因是,这是我所有个人项目都使用的,对很多人来说,这是开始使用Postgres数据库最简单的方法,它是最受欢迎的数据库之一,我喜欢为我的向量数据库使用Postgres的原因是,它采用了我已经在使用的现有技术栈,它已经与我的整个技术栈一起工作,它可靠,它对一切都有广泛的支持,我们将简单地将这个名为PGVector的扩展放在Postgres之上,它将为我们提供一个额外的列类型,它将把我们喜欢的所有SQL数据库、关系数据库的东西都带走,它将允许我们现在能够利用这项技术并添加拥有向量列的能力,所以我们将能够实际存储一个特定的向量列类型,它将允许我们以高效的方式存储我们将要生成的所有嵌入,它将允许我们启动一个索引,以便我们对具有向量列类型的表进行的查询将是高效的,它将是可扩展的,所以我喜欢使用这个特定技术栈的原因,你看到很多人甚至公司都在大规模地以各种方式使用它,因为它开箱即用,Postgres和PGVector的开源社区非常强大,它就是这样工作的,也就是说,不同的问题需要不同的工具,这就是为什么我们有所有这些其他不同的数据库,比如Pinecone,比如Weav8,Chroma,这些是更传统的向量数据库,PGVector方法本质上采用了我们喜欢并熟悉的现有技术,并使其易于访问,并使其能够处理你将遇到的向量数据库的典型问题集,Pinecone,Weav8以及所有这些其他的,这些将更倾向于非常独立的向量数据库,你可能不会使用传统数据库,你只会使用你的特定嵌入工作负载,使用这些不同的东西将取决于你试图做什么,你试图解决什么问题,你在什么规模上解决这个问题,你有一个内部工具吗,如果是你自己的公司,这将非常棒,Postgres将非常棒,如果你正在构建一个面向数百万用户的内存代理,你可能需要一种更专门针对实际向量用例的技术,所以这些对话,如果你处于需要进行这些对话的境地,或者如果你真的在那个规模上运作,你可能需要咨询在每个节点上都有专业知识的人,无论是你作为CTO想要选择一个技术栈,还是一个工程团队正在进行这些讨论,你都需要真正地逐个案例地进行评估,但对于绝大多数观看此视频并正在起飞的人来说,你将与我们在这里使用的技术栈一起做得很好,Postgres的优点是,如果你不想使用Supabase,没问题,世界上有很多很多Postgres提供商,你可以使用,再次,我们将在这里使用Supabase与我们的Postgres设置,因为这是我们在起飞时使用的,它非常容易设置,非常熟悉,工具很棒,任何你想要使用的东西,Supabase生态系统之外的东西,你都可以,因为Supabase是开源的,它只是Postgres,所以如果你想在其他地方使用Postgres,完全没问题,所以,这些是一些更受欢迎的Postgres数据库,或者更准确地说,更受欢迎的向量数据库,我们将使用带有PGVector的Postgres,所以我们在这里所做的一切都将与本课程中的内容相匹配,所以让我们开始设置一个向量DB,我们回到我们的存储库,我们将转到第三个文件,这个上传文档,所以我们将把这10个不同的文档上传到一个向量数据库,我将向你展示一些我们将要使用的活动部件,在第二部分,我们将从头开始设置它,所以如果你正在跟随,不知道发生了什么,不用担心,这里的想法是我们只想深入概念,我们只想深入了解设置向量DB的活动部件,在Postgres环境中,以及,你知道,一些你需要注意的小事情,所以再次,我建议如果你有时间,自己从头开始编写这段代码,并获得一些额外的练习,也许运行这些文件,但如果你只是想回顾概念,这不一定需要,我们将使用一个名为Drizzle的工具,我们在这里的所有数据库需求上都使用它,它基本上被称为ORM,它为我们提供了一种非常简单的方式来处理迁移和构建我们的不同表模式等等,所以我们将要做的是,我们将转到这个DB文件夹,我在这里有一个名为schema的文件夹,我们有一个文件,名为documents.schema.ts,这是一个TypeScript文件,它定义了我们想要上传的表,或者我们想要在我们的Supabase数据库中创建的表,所以,如果我们去Supabase,这是我启动的项目,它只是叫做playground,如果我们去侧面的表编辑器,你会看到我们没有表,所以我们需要做的是,我们需要实际生成一个文档,我们需要生成SQL迁移文件,我们可以运行它,以便我们能够实际使用这个表,带有嵌入向量列,正如我在幻灯片中提到的,它通过PGVector启用,我们将要做的是,我们将创建那个表,然后我们将创建一个索引,这将允许我们在这个表上执行诸如相似性搜索之类的操作,这是Drizzle中的所有内容,你需要做的是实际构建这些表,设置方式是,你通常会调用你的列embedding,正如你所看到的,我只是称之为embedding,它被包装在这个Vector助手里,我们所要做的就是定义我们将使用的维度数量,再次,这只是直接对应于我们在嵌入文件中指定的嵌入维度,所以我们将始终使用256个维度,一旦你选择了你的维度,它必须是一致的,所以你不能有1000个维度和500个维度和256个,你必须是标准的,所以请确保你设置了那个数字,并且它在你生成的嵌入和你实际存储在表中的内容之间是相同的,所以这是我们真正需要担心的唯一列类型,以使用PGVector,这很棒,这是PGVector的优点之一,它非常容易设置,我们还需要创建一个索引,以便我们可以执行诸如相似性搜索之类的操作,我们将使用余弦相似度,所以你可以看到这是Drizzle中我们需要使用的语法,如果你不熟悉Drizzle,没问题,我们将实际生成SQL,我将向你展示它是什么,所以如果我们去package.json文件,你会看到我们有两个脚本,对应于我们第一部分需要的操作,所以如果我们使用db:generate:basics和db:migrate:basics,这将负责将这个Drizzle文件变成实际的SQL代码,所以我们可以做npm run db:generate:basics,这将再次,我们只是在这里的one-learn-the-basics文件夹里,你可以看到我们现在有一个migrations文件夹,当你看到这个方式时,我可能实际上会把它上传到git,所以你可能不需要为这一部分生成它,但你可以看到我们现在几乎拥有了实际设置所需的所有SQL代码,这就是Drizzle中的代码的样子,但一旦我们将其从Drizzle语法翻译成SQL语法,这就是你使用的,你只需要添加的是,你必须实际启用向量扩展,所以我们将创建extension if not exists vector,这将是启用PGVector扩展的代码,大多数Postgres提供商目前都与PGVector兼容,我真的很惊讶,如果你使用什么东西不兼容,如果他们不兼容,那可能是一个坏迹象,可能不是一个很好的供应商,但这是你所需的所有SQL代码,所以这里的好处是,无论你使用我们的技术栈,你知道,我们使用Drizzle是因为它很容易使用,但如果你不想使用它,你只需复制所有SQL代码,然后你就在任何Postgres提供商那里拥有你的表,这就是你所需要的,所以再次,它有三个部分,你必须启用向量扩展,这很合理,你有一个Postgres数据库,我们想添加那个扩展,很好,另一件事我们需要做的是,在我们创建的表中,我们需要创建那个向量列,这就是使用PGVector的全部意义,它允许使用PGVector作为列类型,所以我们有这个向量类型,我们称之为embedding,我们指定我们使用256个维度来嵌入,最后但同样重要的是,我们实际上需要创建这个索引,以便我们可以高效地对这个表执行操作,并开始进行实际检索和余弦相似度以及我们将在下一课中介绍的一些内容,所以一旦你设置好了,我们可以做npm run db:migrate:basics,这将,哦,你知道吗,我实际上必须去,我将快速调整一下,伙计们,看起来在Drizzle配置文件中,我可能犯了一个轻微的语法错误,我马上回来,我只是运行,让我们运行npm run db:migrate:basics,这将应用我们的迁移,现在如果我回到Supabase的表编辑器区域,你会看到我们现在有一个表,那个表叫做documents,我们有所有的列类型,包括这个叫做embedding的列,它是Vector类型,再次,这是PGVector启用的,这是使用Postgres和PGVector设置的全部意义,这很棒,我们现在有了具有向量数据库功能的Postgres,喜欢它,现在我们可以实际上传信息,嵌入以及我们想要的文本到我们的表中,所以你会看到在这里的第三个文件,上传文档,我们将要做的是,我们将把所有这些模拟文档,它们只是一堆假的动物事实,我不知道,这是AI为我生成的,但我们将要做的是,我们将把所有这些模拟文档通过这个上传文档函数运行,我们将再次生成一个嵌入,使用与上一课相同的维度和模型,我们得到了那些嵌入,我们将把这些嵌入与内容、文档名称(只是有趣的事实编号)以及嵌入一起插入,我们将把所有这些上传到我们的数据库,在我们运行这个文件之后,如果我们复制这个路径并输入tsx filename,然后回车,我们将得到那10个嵌入,然后我们将把数据插入到我们的数据库中,所以如果我们回到我们的文件,你会注意到这里,这个操作已经完成,这真的很酷,我们现在有了10个有趣的事实,10个动物事实嵌入到我们的向量数据库中,所以你可以看到,如果我稍微展开一点,你会看到我实际上无法点击它,因为它有256个数字长,我们在这里存储了大量数据,但重点是我们嵌入了事实的实际内容,所以你会看到,如果我们回到对OpenAI API的调用,我们嵌入的实际输入是,我们嵌入了文档内容,所以那是实际的事实本身,而不是名字,只是事实本身,所以我们再次做的是,为了回调嵌入部分,我们获取这个内容,将其传递给OpenAI嵌入模型,我们为每个事实得到这个嵌入,所以每一对都匹配,我们所做的是,我们在Postgres表中的向量数据库中设置了一个向量数据库,只是一个标准的SQL表,我们有这个很棒的向量列类型,因为我们启用了PGVector扩展,我们启用了那个扩展,我们为这个向量列定义了256个维度,所以我们的嵌入与我们的向量数据库期望的数据存储方式相匹配,然后我们现在拥有了这个索引,它将允许我们在下一课中实际进行检索,这就是设置向量数据库的基础,非常简单,非常简洁,代码不多,再次,这是所有SQL代码,它需要让一个表运行并支持嵌入,很酷,所以在下一个视频中,我们将进入一些相似性搜索方面的内容,以及我们如何利用这个设置,现在我们已经完成了所有的设置步骤,我们如何实际应用我们学到的知识来做有用的事情,再快速补充一点,我想提到Supabase在他们的文档中有一个AI和向量部分,我认为这是关于向量数据库、向量和嵌入的最好的资源之一,他们有很多非常好的资源,所以我只是把它放在视频的最后,只是为了展示一下,如果你想要更多阅读的东西,他们有很多代码示例,他们有很多关于不同类型索引的深入讨论,我只是使用了最常见的那个,为了保持简单,我们以后可能会讨论,比如生产环境,如果你需要为规模工程化东西,如果你是那种处理数百万,数亿,甚至数十亿向量的人,你可能想更熟悉其中一些其他的东西,然后我唯一想谈的是,我们回顾了一些其他的向量数据库,但我确实想强调,概念上,我们刚刚回顾的所有步骤,无论你使用什么数据库,都将保持不变,所以我们显然正在使用带有PGVector的Postgres,我认为这很棒,我认为这是最容易开始的方式,而且我认为它也非常灵活,因为它很容易过渡到其他东西,如果你需要一些更偏向向量DB的东西,也许你有不同的工作负载,你和你的团队可能需要解决一些不同的权衡,但如果我们快速浏览一下Pinecone数据库的快速入门,Pinecone是更受欢迎的,所谓的纯向量数据库之一,你可以看到事情很简单,你生成你的向量,你创建一个索引,你将这些向量插入数据库,然后你执行某种搜索,某种检索步骤,所以这是相同的步骤,唯一改变的是具体的代码,SQL代码将不同于,你知道,任何Pinecone期望的东西,它不是一个SQL数据库,它有点自己的东西,但概念上,所有步骤都是相同的,你获取文本,你生成文本的嵌入,你将文本及其嵌入上传到某种向量存储,在我们的例子中,它是Postgres和PGVector,在这个特定的例子中,它是Pinecone,用他们自己的方式做事,然后执行某种搜索,所以概念上,整个循序渐进的过程将是相同的,只是中间的一些技术实现会改变,所以一旦你理解了整个系统是如何工作的,很容易在这些提供商之间切换,并找出他们做事的方式,但所有核心步骤都将是相同的,所以我只是想在最后加上这一点,以防对这些其他选项有任何困惑,所以一个很好的练习,例如,将是像我们刚才那样做一个向量数据库,然后选择Pinecone,去尝试用Pinecone做,看看你更喜欢什么,看看成本如何,我会说,这些通常会贵得多,也许效率更高,尤其是在规模上,你将为此付出溢价,再次,所有这些都将是你需要与你的工程团队讨论的,如果你在规模上运作,这显然是一个好问题,恭喜你,这很棒,然后你可以进入这些讨论,也许咨询一些与不同技术栈合作过的人,看看什么适合你,但总的来说,再次,95%以上的人将与我们这里的解决方案做得很好,我只是想在最后加上这一点,到目前为止,我们已经完成了所有工作,设置了我们的基础架构和向量数据库,我们学会了如何嵌入我们的文本,如何将其存储在向量数据库中,所有这些东西,所以让我们开始做一些有用的事情,那就是相似性搜索,所以我们将介绍如何从你的向量存储中检索嵌入数据,所以什么是相似性搜索,基本上,当我们谈论向量数据库中的搜索时,我们实际上是在尝试比较嵌入,比较那些文本嵌入,以找到最语义上相似的项目,所以当我们谈论这个例子,关于守卫和软管,以及,你知道,如何比较电池和钻头与电池和颜色,这就是相似性搜索发生计算的地方,所以我们显然需要做,你知道,在这个例子中,我们可以直观地看到事物之间的接近程度,但当我们实际处理数字形式的嵌入时,我们需要某种操作来执行该计算并执行相似性搜索,以便我们能够获得一个实际的计算数字,说明每个嵌入有多相似,所以这就是相似性搜索发挥作用的地方,我们将要做的是,我们将向你展示如何实际使用它,所以我们将要做的是,我们将获取我们查询的嵌入,所以我们将要做的是,我们将获取查询,我们将将其转换为向量嵌入,我们将要做的是,我们将计算查询与我们表中存储的嵌入的余弦相似度,所以我们将要做的是,你将首先获取那些嵌入,在这种情况下,我们只是嵌入了查询,然后我们正在进行相似性计算,Drizzle的一个很棒的地方是,我们开箱即用地获得了余弦距离,基本上,余弦相似度函数在数学层面上的作用是,你实际上是通过计算它们之间的角度的余弦来计算向量之间的相似度,再次,我们想尽可能远离数学的东西,只关注实际应用,但实际上我们所做的是,我们只是试图衡量我们的查询与我们表中存储的嵌入的相似度,所以我们回到我们的SQL表,你知道,我们将要做的是,如果我说,关于犀牛的事实,我们将要做的是,我们将获取这个嵌入,然后我们将它与我们为“告诉我关于犀牛”的查询得到的嵌入进行比较,这个不会太相似,因为我们谈论的是火烈鸟,但它会有点相似,因为我们谈论的是动物,所以会有一些相似性,但如果我们向下滚动到事实六,你会看到,哦,我们直接有一个关于犀牛的事实,所以那个嵌入会非常接近,所以如果我们回到我们的代码,我们将对它们进行排序并返回所有这些文档,我们将得到一点感觉,如果我们实际记录下来,什么是最相似的文档,以及在RAG系统中会发生什么,你根据相似性分数进行排序,也许你抓取最相似的五个,等等,你可以看到我们正在按降序排序相似性,所以如果我们做一些事情,比如再次,让我们用一个犀牛的例子,告诉我关于犀牛的事实,让我们做一个限制为三个,我们可以实际做的是,如果我们复制文件路径,我们将去tsx,我们在这里运行它,你可以看到正在发生的是,我们正在获取文档,所以,在我向你展示这些结果之前,让我们再回顾一遍,以使其非常清楚,正在发生的事情是,我们正在运行这个检索文档函数,查询是“告诉我关于犀牛”,所以正在发生的是,我们正在获取那个查询,我们正在为它生成嵌入,我们正在做的是,我们正在计算这个嵌入之间的余弦距离,查询的嵌入,以及我们表中的所有嵌入,在这种情况下,我们正在计算这10个事物之间的相似度,所以正在发生的是,我们正在运行那个查询,我们试图找到三个最相关的文档,因为我们使用限制为三个,试图找到三个最相关的文档,基于相似性分数,分数越高,越接近一,我们的相似性分数越接近一,越相关,事实,在我们正在进行的用例的背景下,我们的事实与犀牛越相似,所以我认为我们的犀牛应该排在前面,所以让我们看看我们的结果,让我们回到最上面的一个,你实际上可以看到,这就是一切开始的地方,所有蓝色的部分,我将突出显示它,以便你更容易看到,你可以看到,果然,事实六,犀牛的角是由毛发制成的,是最相关的,是我们表中最相关的文档,我不知道为什么,我以为鼻涕虫有四个鼻子,等一下,第三个是象,所以你可以看到,有时你会得到意想不到的结果,我直观地认为,也许大象的事实更相似,我会把它们两个调换一下,但你知道,有时你永远不知道嵌入,但你可以看到,果然,我们得到的最相似的事实是,犀牛的角是由毛发制成的,所以你可以做的一件事,在RAG系统的背景下,就是你可以获取那个事实,你可以把它注入到一个提示中,将查询与你得到的事实结合起来,在这种情况下是,伙计们,我在这里太激动了,是犀牛的角是由毛发制成的,然后如果我们回到我们的幻灯片,我们将回到很久以前,回到我们几课前做的RAG概述的背景下,你知道,我们终于完成了所有这些步骤,我们给了它“告诉我关于犀牛”的查询,我们得到了它的嵌入,我们将其与我们数据库中的文档进行了比较,我们得到了最相关的那些,我们将查询与片段结合起来,然后我们没有真正调用AI模型,我们将在第二部分进行,但我们已经完成了整个检索过程,我们完成了所有步骤,到目前为止,我们可以调用GPT-4或任何其他模型,以便我们能够得到一个基于我们的小事实列表的答案,这显然是一个非常愚蠢的小例子,但它展示了驱动更强大系统的核心概念,所以一旦你掌握了这个简单的例子,你就可以很快地看到,如果将这个表从愚蠢的、小小的、关于几种不同动物的有趣事实,变成一千个文档,它们都与收益报告有关,如果你想做一个收益报告聊天机器人,而每一个都是收益报告的PDF,你想提取最相关的,比如,谷歌在2024年第三季度的收益是多少,或者类似的东西,现在你拥有了构建该项目所需的所有基础知识,所以显然,我们想开始构建更有用的东西,我们想从头开始构建这些系统,所以这是第一部分的内容,再次,这些都是基础知识,它是嵌入,它是向量数据库,它是相似性搜索,它是学习所有拼图的活动部件如何组合在一起,以便你可以实际构建检索系统,在第二部分,我们将从头开始做这件事,我们将学习如何从头开始构建一个端到端的RAG系统,我们将介绍一个与此类似的用例,但我们将介绍一些更高级的检索技术,你可以用来真正优化你最终得到的结果,在本节的最后,我们将从头开始创建一个完整的端到端RAG系统,所以我们将回顾我们在第一部分所做的所有步骤,但这次你将和我一起从头开始编写代码,以便你可以学习如何从一个想法到实际构建一个RAG系统,显然你们都在这里,因为你们可能有一个想要执行的想法,到我们完成这一部分时,你应该感到非常舒适地执行你的想法并构建一个相当高级的检索系统,我们将介绍很多内容,但首先,我们想做一个快速的介绍性课程,以确保每个人都设置好,所以第一步,我们只是想确保你拥有本节的代码,所以如果我们回到光标,你可能在那里有代码,或者没有,所以如果你回到上一节,我们有代码设置课程,确保你到达拥有RAG课程代码存储库的这一点,所有下载的,我们想做的是,因为你们中的一些人可能,事实上,大多数人已经设置好了,我们想确保你拉取最新的代码,因为我们将不断更新它,以反映最新的变化,所以如果你去你的终端,你会看到我在这里,就在这个项目存储库的最顶层,我们将做一个小git pull origin main,然后回车,你应该会获取最新的代码,如果出于某种原因,它没有获取,你总是可以直接去GitHub存储库,下面会有链接,你可以下载zip文件并获取第二部分的代码,确保你拥有它,你将需要去左边,在你的文件导航器中,点击sections,并确保你拥有reference code文件夹,它包含了我们将要从头编写的所有参考代码,然后你拥有这个your code文件夹,所以你将需要确保你拥有这个create-a-rag-system文件夹,以及所有这些好东西,因为我们将在这里工作,我认为这一部分将有大约10到12节课,所以我们将,我们将做很多事情,所以确保你拥有代码非常重要,以便你可以跟随,所以我们接下来想做的是,我们想谈谈我们在这里将要构建的东西,所以在这个部分,我们将借鉴上一部分的主题,我们将继续使用那个愚蠢的动物事实例子,我知道它很简单,所以为什么我们要使用如此简单的例子,为什么不使用一些更有趣的东西,所以目标是,我们想从头开始介绍所有设置步骤和核心概念,所以这些是学习如何编写生成嵌入的代码,设置你的向量DB,做文档重排序等等,我发现如果你试图将我们将在本节中介绍的所有内容与一个非常高级的概念结合起来,人们可能会有点迷失,第一次这样做时,最好将数据范围缩小,所以我们将继续使用一个非常简单的数据集,它也有帮助,因为这样你就不用处理数据集了,所以我们所做的是,你会看到我们有这个mock-data.ts文件,我们有一个巨大的数组,大约有100个不同的动物事实,我猜我们有大约20种不同的动物,每种有五种事实,你会看到我们有这个大象维基的东西,我们稍后也会用到它,重点是,一旦你学会了所有这些不同的活动部件如何在一个非常简单的数据集上工作,就很容易增加你正在处理的数据的复杂性,所以重点更多地放在实际概念上,而不是具体的数据集,我们将在下一节介绍更复杂的概念,当我们实际构建一个具有更高级数据集的真实世界项目时,但这就是我们为什么将坚持使用那个简单示例的原因,所以我们将在本节学习什么,所以正如我提到的,这将是一个充满课程的部分,我们将学习如何从头开始设置一个向量DB,基本的检索技术,我们将做一些RAG提示,查询优化,文档重排序,元数据过滤,这很有用,然后我们还将简要介绍文本分割,我们将在第三部分更深入地讨论文档分割等内容,但这些都是一些我们将要涵盖的内容,这些都是非常核心的概念,你想了解它们是如何工作的,如何从头开始构建它们,编写代码,并将所有这些部分组合在一起,以便你可以实际构建RAG系统,这些非常普遍,我们已经选择了我们认为你需要知道的主要概念,以便构建最好的检索系统,所以这就是我们将在本节学习的内容,另外,快速说明一下设置,你可能已经有了OpenAI密钥,从上一节开始,但如果你没有,你需要确保你去OpenAI的开发者平台,并从他们那里获取一个OpenAI密钥,因为你需要它,所以我实际上有,在我的代码中,我们需要做什么,实际上这是上一节,我想让你跟着我做的每一件事,所以目标是我们实际上想编写代码并做到这一点,所以你会看到在你的代码文件中,作为快速说明,参考文件夹是为了存放所有参考代码,所以如果我点击这个优化查询文件,它包含了我们将要编写的函数,我们将从头开始做,但我们有每一个都写好了,所以如果你想要完整的代码,你可以参考它,你可以查看,我们甚至有注释来解释一些周围的事情,这就是它,你可以运行任何你想要的东西,这是我们将在本节中做的完整代码,然后your code文件夹是我希望你跟着我做的,所以这就是我们将在本节中进行的方式,我们首先需要做的是创建一个名为.env.local的新文件,那里有.env.local,你想做的是进入.env.example,复制所有这些值,粘贴到.env.local,你应该看到它被灰化了,所以我们不会把我们的API密钥提交到git或其他任何东西,你想做的是在那里放你的OpenAI密钥,你想把你的数据库URL放在这里,你的Cohere API密钥放在这里,我们将在稍后讨论这两个,所以确保你在这里获取你的OpenAI密钥,并粘贴这个值,以便它准备就绪,我们也将使用Supabase,就像我们在上一节中讨论基础设施方面一样,所以我们将使用它。
再次,所以请确保如果您没有跟上该部分的代码,您实际上花时间再次设置一个 Supabase 帐户。这是免费的,它们有一个免费套餐供您使用,这对于我们将在本课程中进行的所有工作来说已经足够了,所以这很棒。当我们实际设置向量数据库时,我们将进入下一个部分来处理实际的密钥。您现在可以跳过此数据库 URL。然后,您需要做的第三件事,这有点可选,但我推荐它,就是注册 Cohere API。所以,如果您注册了 Cohere API,它会带您到一个仪表板,如果您点击那里的 API 密钥,您将能够创建一个 API 密钥。我相信他们允许您免费使用一些,所以我相信您实际上不需要为 Cohere 花钱。如果需要,它将基于使用量,与 OpenAI API 的工作方式非常相似,所以您不会花很多钱。我们在这里做的所有事情都非常便宜。所以,一旦您拥有了它,您就想在这里粘贴您的 API 密钥,无论它是什么。好的,所以我们显然会离屏处理我的,以确保安全,但这些将是我们需要的三个值。我们将进入数据库 URL 和向量数据库课程,所以现在不用担心,只是想确保您拥有它。所以,一旦您拥有了它,您将拥有您的 OpenAI 帐户、您的 Supabase 帐户,以及可选的您的 Cohere API。我们将把它用于文档重新排序部分,它还为您提供了一些额外的灵活性,如果您想尝试另一个嵌入模型,如果您不想使用 OpenAI。所以它只是为您提供了一点更多的灵活性。但是一旦您拥有了它,请回来,因为我们将介绍如何从头开始设置向量数据库。让我们实际设置好我们的向量数据库。所以您应该对为什么我们需要执行此设置步骤有一个基本的了解。所以,再次,设置向量数据库允许我们使用向量嵌入来存储我们从 OpenAI API 生成的所有数字,当我们获取所有这些嵌入时,并允许我们实际对其执行操作。好的,让我们实际设置好这个。所以我们将使用三种技术来构建我们的向量数据库堆栈。所以我们将继续使用 PostgreSQL 和 PGVector。再次,我们只是想在这里使用经典的 PostgreSQL 来处理我们喜欢 SQL 数据库的所有方面,我们将使用 PGVector 扩展,这样我们就可以支持向量嵌入。因为 PostgreSQL 目前不自带此功能。我们将使用 Supabase 来轻松设置 PostgreSQL 数据库。所以这将非常容易使用。再次,这对你们所有人来说都将是免费的,这真的很方便。最后但同样重要的是,我们将在我们的代码中使用一个 ORM,称为 Drizzle。它是一种非常简单的方式,尤其是在处理 TypeScript 时,可以轻松地管理数据模式和迁移文件,并使您的代码保持类型安全。Drizzle 还自带了一些非常好的实用函数,用于计算余弦相似度等。再次,我们在上一节中对此进行了一些介绍,但这将构成我们文本堆栈的核心三个部分。所以,让我们实际开始编写代码。所以我们将需要做很多不同的事情。所以您首先需要确保您在这里做的是,我们将实际导航到我们的终端。所以,无论您使用的是操作系统的原生终端,还是 Cursor 中的终端,我将使用后者。我们需要在这里做的是,我们实际上需要导航到“创建 RAG 系统”中的“部分”,然后进入您的代码。所以,让我们输入 CD 来更改目录,然后我们将进入“部分”,然后进入“创建 RAG 系统”,然后我们将一直进入您的代码。可选地,如果您愿意,您也可以跟上。我将提前向您展示其中每个部分将如何工作。我可能会使用一些参考代码。所以,我将再次点击这个加号。您不一定需要这样做,但我只是这样做,因为它在这里有助于教学。我将直接进入参考代码终端。太棒了。所以,我将点击回到您的代码终端。我们将开始在这里工作。所以,在开始编写代码之前,让我们实际设置好我们的 Supabase 项目,以便我们实际上可以使用我在上一课中提到的数据库 URL。所以,我们将要做的是,我们将转到 Supabase。我将点击我的仪表板。您将需要创建一个新项目。我们来看看,您将需要为它选择一个组织。所以我将选择“Takeoff Projects”。我将称之为“动物事实”,因为这就是我们将要做的。我将生成一个密码,因为 Supabase 有一个很好的算法来安全地生成它们。我将复制该值。我将跳回到这个 Ur 代码中。我将暂时将其保存在 .env.example 中。请稍等片刻,因为我们将一起完成这个过程。再次,您应该能够以微型套餐、免费套餐创建项目。我相信 Supabase 允许您免费创建最多两个项目,我相信是这样。所以,如果您已经有了两个,您可能需要删除一个。但是您将需要创建一个新项目。这需要一两分钟来设置。所以,一旦这个项目设置好了,我就会回来。现在我们的项目已经设置好了,我们将转到下面的项目设置。您需要去左侧边栏的数据库。数据库。您将获取您的连接字符串。所以,这将是我们连接所需的数据库 URL。所以,您将需要复制该值。我们实际上要做的是,我们将在这里粘贴它,然后我们将获取我们的数据库密码。您实际上需要做的是,看到它在这些括号中说“您的密码”。您将需要粘贴您的密码。然后,您需要剪切它,然后您将转到 .env.local。在您的数据库 URL 中,您将需要粘贴完整的该值。我只是做个例子,这样我就不会暴露我其他的密钥了。我现在实际上必须更改我的密码,因为我向你们展示了这一点。但是您只需要确保在您的 .env.example 文件中,在您的实际 .env.local 文件中,您拥有您的 OpenAI 密钥、您的数据库 URL,其中密码已替换为您的 Cohere API。所以,一旦您拥有了这三样东西,您就准备好了。所以我将非常快速地更新,重置我的密码,粘贴它,然后我马上回来。现在我已经成功地将我的所有密钥输入到 .env.local 文件中,我们可以开始工作,编写我们的 Drizzle 代码,以便我们实际上可以编写我们需要的 TypeScript 代码来生成我们的迁移,我们的数据库迁移,并实际迁移它,以便它显示在我们的 Supabase 项目中。所以,您需要做的第一件事,再次,确保您在正确的目录中非常重要。再次,您现在需要导航到您的代码文件夹。否则,您将遇到一些问题,并可能遇到一些令人讨厌的调试错误。但是您首先需要做的是,我已经花时间设置好了我们在此处所需的所有脚本和包。您只需要使用 npm install 来安装它们。那就是 npm install。这将安装我们将在本节中使用的所有不同的 SDK。一旦您完成了,我们实际上要做的第一件事是,在您的代码文件夹中,我们将创建一个新文件夹,我们称之为 DB。然后在 DB 文件夹中,我们将创建另一个名为 schema 的文件夹。这就是我们的模式将要存放的地方。在 schema 文件夹中,我们将创建一个文件,我们称之为 fa-schema。我相信我们在参考代码中就是这样称呼它的,所以我们将保持一致,这样你们就很容易跟上。然后在 DB 文件夹中,您不需要在 schema 文件夹中,您只需要创建另一个名为 index.ts 的文件。所以,这个结构应该是您的数据库文件夹。该文件夹应该有一个名为 index.ts 的文件,一个名为 schema 的文件夹,以及在该 schema 文件夹中,一个名为 fa-schema.ts 的文件。我们需要在您的代码中再多一个文件,而不是在数据库文件夹中。我们将称之为 drizzle.config.ts。我们将设置 Drizzle 所需的配置。再次,我们将使用 Drizzle,因为我们获得了非常好的实用函数集,例如类型安全,因为我们正在使用 TypeScript,非常容易管理迁移,而且我们在 Takeoff 上到处都使用它,所以我们将保持一致。所以,让我们实际设置好它。所以,我们将需要做的第一件事,再次,任何时候您想参考任何这些文件的完整代码,只需点击参考代码文件夹。例如,这是 drizzle.config 文件的参考代码。您实际上可以将其复制并粘贴到此处,您就准备好了。我们将从头开始做,这样我就可以谈论每一行,这样您就能理解发生了什么。再次,我们在上一节中做了无代码的东西。本节的目标是编写一些代码。所以,要在这里设置 Drizzle,首先我们将导入我们的配置。您会看到我得到了一些灰色的填充文本,这是 Cursor 的一个功能。如果您不熟悉 Cursor,我建议您查看我们在 Takeoff 上提供的 Cursor 课程,因为我们将在这里使用 Cursor 的一些功能,我强烈推荐。所以,我将点击 Tab,它将填充。我们还将从 drizzle-kit 导入 DefineConfig。我们将一行一行地进行,我不会对你们太过分。然后,在顶部,我们将在这里设置我们的配置,用于我们的环境变量。我们只需要指定一个名为 path 的参数,它将定义我们正在从我们之前已经设置好的 .env.local 文件中提取我们的环境变量。所以,现在一切都准备好了。我们必须在这里做的是,我们实际上只需要设置我们的 Drizzle 配置。这只会设置一些不同的东西。所以,我们首先要做的就是指定一个方言。在这种情况下,我们使用的是 PostgreSQL 数据库,所以我们将指定 PostgreSQL。如果您想使用 Drizzle 和 MySQL 之类的东西,您也可以这样做。MySQL 是一个很棒的数据库。但是我们将在这个课程中坚持使用 PostgreSQL,因为这是我们在 Takeoff 上最喜欢使用的东西。所以,我们还需要在这里添加数据库凭据。所以,这将是您在 Supabase 上辛苦获取的 URL。所以,我们将在这里导入那个环境变量。太好了。我们必须指定两种文件输出。第一件事是模式。我们的模式文件在哪里?它将在数据库文件夹中,在 schema 文件夹中。所以,这就是我们的模式所在。这很棒。Drizzle 将自动加载该文件夹中我们使用的所有文件。然后,我们还想指定我们希望迁移输出到哪里。所以,我们只是说我们希望它在 DB/migrations 中。所以,您的代码应该看起来完全像这样。再次,这将与您在参考文件中找到的完全匹配。您会看到,哦,这很好,我实际上得到了完全相同的,很满意,不需要编辑。但是,再次,您可以查看其中每一个参考文件,当我们实时进行时。所以,我们有了 Drizzle 文件。让我们实际进入 fact-schema。因为我们现在需要的是,如果我们回到 Supabase 并点击我们的表编辑器,我们没有任何表,这是问题所在。我们需要设置支持向量列的表,以便我们实际上可以存储我们即将处理的所有嵌入。所以,我们将要做的是,我们将导出 const factsTable。所以,我们正在创建一个名为 factsTable 的变量,它将存储我们的表。我们将称之为 facts,保持简单。我们现在需要做的是,我们实际上必须指定我们将使用的所有不同的列。所以,首先,我们将使用一个非常基本的序列化 ID 列作为此表的主键。我们不会深入研究 SQL 的细微差别。如果您想了解更多关于 SQL、PostgreSQL 和后端系统的信息,我们在 Takeoff 上有我们的应用程序构建课程。它提供了更深入的探讨,如果您对此感到困惑。再次,如果您对任何事情感到困惑,请在这里的 Cursor 聊天中输入,输入 ChatGPT,输入 Cloud,无论您最喜欢的工具是什么,然后复制粘贴代码并提问。如果您需要放慢速度,放慢速度,不用着急。按照自己的节奏学习。这是 Takeoff 的一大优点。您可以随时暂停。我们录制视频是有原因的。我们不想实时进行,因为那样,您,您,工作起来更困难,按照自己的速度。但是我们将创建一个 name 列。我们将使用文本数据类型。这应该是一个非空列,我们不希望它是空的。content 在这里也将是一个文本列。我们将称之为 content。这也不应该为空。事情变得有趣了。所以,我们将要做的是,我们将创建一个列,我们称之为 embedding。这将是向量列类型。所以,这是由 PGVector 扩展启用的列类型。我们将称之为 embedding 列,我们只需要指定我们将使用的嵌入的维度。在这种情况下,我们将使用 256。我稍后会更详细地谈论维度大小。最后但同样重要的是,我们将做一些非常基本的元数据列,我几乎总是喜欢添加的,那就是 created_at 和 updated_at。它们将是默认值,这是我们喜欢看到的。所以,我们最后需要做的是,当您处理使用向量列的表时,您将需要在此列上创建索引。所以,我们将要做的是,我们将创建一个 HNSW 索引。这将为您节省一些细节,以便我们能够开始。它只是我们检索系统将使用的操作的最佳实践索引。所以,我们将称之为 embedding_index。index.using,我们将指定我们的索引类型。太棒了。然后我们还需要指定我们希望在此索引上允许的操作,即 vector_cosine_ops。所以,这将允许我们执行所有我们将在检索步骤中需要执行的相似性搜索。所以,这实际上是我们所需的一切。您将看到我导入了一些东西。我使用自动导入系统。所以,我们必须确保所有这些都从 pg_core 导入。所以,再次,如果我们回到参考代码,点击 fact-schema。您会看到它。哦,这是一个很好的提醒,这就是参考代码如此有用的原因。我实际上忘记了在这里进行类型定义。所以,让我们回到文件中。我们还需要导出这两个类型:InsertFacts 和 SelectFacts,因为这将使我们能够使用我们非常喜欢的类型安全应用程序。所以,在这一点上,我们已经构建了 factsTable。我们支持向量嵌入,维度为 256。再次,如果您观看了上一节,否则您将有点迷失,维度只是对应于我们从嵌入模型返回的向量中有多少个数字。所以,最后,让我们实际进入 index.ts 文件。再次,它只是位于 DB 文件夹中。让我们关闭 DB 文件夹中的参考代码。这只是定义了我们实际数据库的一些内容。所以,我们将需要导入,我将稍微加快速度,因为这主要是样板代码,不是您通常需要过多担心的东西。这在您的不同项目之间,大部分将是相同的。所以,我们将要做的是,我们将指定我们的 DB 模式,这样我们就可以使用 Drizzle 查询语言,这有时非常方便。它允许您使用非常好的 Drizzle API 来执行查询、选择和插入等操作,这样您就不必编写原始 SQL,这非常棒,尤其如果您是经常使用 TypeScript 的人,我们肯定在 Takeoff 上一直这样做。然后,我们只需要初始化我们的 DB。使用一些参数。我们需要创建一个客户端。它将是一个 PostgreSQL 客户端,我们从几分钟前安装的 PostgreSQL 包中导入。我们将使用 prepare: false。这与 Supabase 的工作方式有关。然后,我们将用该客户端和我们指定的模式以及我们的表返回 Drizzle 的一个实例。再次,如果您真的想了解更多关于后端内容、Drizzle 的工作原理以及 PostgreSQL 的工作原理,请观看我们的应用程序构建课程。我们在那里有所有关于后端开发的部分,它涵盖了这些内容更深入。但这对现在来说已经足够了。所以,只要您有这个 Drizzle 配置文件,这个 DB 文件夹中的 index 文件,以及您的 fact-schema,这样我们就可以让表支持嵌入。各位,我犯了一个小小的拼写错误,我只想确保您在继续之前进行更改。我不小心写了 consign_ops,所以请确保它只是 cosine_ops。再次,这个文件应该与您在参考文件中找到的文件完全匹配。所以,如果您想确保它正常工作,您甚至可以复制粘贴它。但是一旦您有了 fact-schema、index.ts 文件以及您的 Drizzle 配置,我们应该就能够运行 npm run db:generate。这将生成我们所需的 SQL 迁移文件。您可以看到我正在查看参考代码。要确保的一件事是,不要混合参考代码和您的代码。所以我刚才不小心点击了错误的文件。所以,如果我向下点击,您会看到我们新生成的 SQL 文件。我们需要做的是,Drizzle 的一个很酷之处在于,这就是我们使用它的原因。Drizzle 正在采用我们在这里编写的 TypeScript 代码。它允许在我们的项目中实现类型安全,并生成适当的 SQL。但是,我们实际上需要做的一件事是,因为我们在这里使用 PGVector,我们实际上需要执行 create extension if not exists vector。因为这将实际解锁我们在 PostgreSQL 中使用向量列类型的能力。所以,如果您不将此行添加到您刚刚获得的生成 SQL 文件的顶部,您将无法获得嵌入支持,这将破坏一切。所以,一旦您生成了该 SQL 文件并在顶部进行了此添加,我们就可以运行 npm run db:migrate。当您运行 npm run db:migrate 时,假设您已将连接字符串和密码正确输入到 .env.local 文件中,您将能够看到成功消息。如果我们导航回我们的 Supabase 数据库,您会看到我们现在有一个名为 facts 的表。我们有所有这些列类型,包括我们命名为 embedding 的向量列类型。所以,您现在应该准备就绪了。此时,您的向量数据库已 100% 设置完毕,我们已准备好构建此 RAG 系统。让我们直接开始使用 OpenAI API 生成嵌入。所以,您应该已经设置好了您的 API 密钥,并将其输入到 .env.local 文件中,因为您需要它才能继续。所以,我们将分两步进行。第一件事是,我们将设置一个小的客户端文件,然后我们将进行生成嵌入文件。快速说明一下我如何设置参考代码文件夹的结构:我添加了数字前缀,以便更容易按顺序跟上,我们将在您的代码中也这样做。所以,我们将要做的第一件事是,我们将创建一个名为 0-api-clients.ts 的文件。我们将在这里做的是,我们将使用我们在此节开头安装的相应 SDK 设置 OpenAI 和 Cohere 客户端,以便我们可以在所有不同的文件中使用它们,因为我们将在这里大量使用它们。所以,让我们开始设置。所以,我们将需要设置配置。再次,您会对此非常熟悉,当您编写更多代码时。但基本上,我们将再次,我设置了自动导入,所以您可能需要自己输入它们。您会看到我将继续使用 Cursor 的 Tab 来确保这一点加快速度。但基本上,我们在这里做的是配置我们的环境变量的来源,这很重要,因为我们需要 OpenAI API 和 Cohere API 的 API 密钥,我们将在下面处理。您会看到这实际上有点幻觉。实际上,让我们检查一下参考文件。我不记得那是否是正确的包。所以,看起来是这样。我们只需要确保我们获得了 Cohere 客户端的导入。所以,您将拥有 OpenAI 客户端设置,作为一个变量,您可以在本节中访问。以及我们可以访问的 Cohere 客户端。再次,OpenAI 使用该 API 密钥语法,Cohere 使用 token。所以,我们将确保这些 API 密钥正确地从 .env.local 文件中导入到此文件夹中。一旦您设置好了这些 API 客户端,我们将要做的是,我们将创建另一个文件,我们称之为 1-generate-embeddings.ts。这就是我们将开始编写代码的地方,以便我们可以获取我们的文本,对其进行嵌入,然后在下一节中,我们将能够获得嵌入运行,并且我们可以让数据填充到我们制作的 SQL 表中。所以,我们将使用 OpenAI 的嵌入模型。我们将使用它们的 text-embedding-3 系列模型。所以,OpenAI 有很好的资源。我们已经在上一节中引用了这一点。您可以查看他们所有的出色文档,其中包含所有不同的示例和 API 参考,如果您想直接查看一些代码。但是我们在这里要做的是,我们实际上要编写代码来生成嵌入。所以,我们将要做的是,我们将在这里编写一个名为 generate_embeddings 的函数。我们将经常在所有这些文件中使用它。这个函数将接受一个名为 text 的单个参数。您可以称之为 data,您可以称之为 documents。但这将是我们传递给 API 的字符串数组。您会看到我这里有一个拼写错误。我们将传递给 API。所以,让我删除这段代码。实际上,Cursor 的 Tab 输入了它。好的,所以我们有了这个名为 generate_embeddings 的单个函数。再次,您将需要确保您导出它,以便我们可以在即将到来的所有不同文件中使用它。然后我们有了这个文本参数。所以,在这里,我们将创建一个名为 response 的新变量。我们将将其保存到 OpenAI 嵌入响应中。我们将访问 OpenAI 客户端。您会看到我们在这里获得了自动导入,这很棒。这是 Cursor 的一个功能。再次,Cursor 是最好的,我非常喜欢 Cursor。但是我们将要做的是,我们将使用嵌入端点。我们将使用,让我们看看,create。我在这里有点凭记忆。抱歉,如果我犯了几个拼写错误。但是我们想在这里使用,我稍后会更详细地谈论这些参数。我们将再次使用 text-embedding-3 系列模型。我们将使用小的那个,所以它更便宜。我们将使用 256 维。再次,我们设置了,这就是我们在 SQL 表中设置的。所以,这些值确实需要匹配。您不能在这里改变主意。如果您改变主意,您将不得不重新做。否则,您将遇到很多错误。然后,作为我们输入到模型中的参数,我们想在这里传递文本数组。所以,它将嵌入所有这些,我们给它的各种字符串数组。它将获取嵌入。然后,我们想从响应中获取 response.data。我们将遍历数据。对于返回数组中的每个项,我们将只返回该项及其嵌入。现在,我将再次导入它。您将需要确保您直接从这个 API 客户端文件中导入它。确保您直接从这个文件夹中的那个导入。如果您使用自动导入系统,它可能会意外地从参考代码中导入。所以,请确保,再次,我们只是使用我们的文件输入,或者我们的目录结构,它正在进入上层目录。我们正在重新使用同一目录中的文件。抱歉,各位,我已经录制了几个小时了。我开始有点失去理智了。但是我们正在导入我们在上一步中构建的 OpenAI 客户端。我们在这里使用它。我们正在保存 OpenAI 嵌入 API 的响应。我们正在使用 text-embedding-3-small 模型,使用 256 维。所有这些嵌入。我们正在传递我们的文本。然后我们只是返回嵌入。所以,为什么我们要使用这个模型,为什么我们要使用这个维度大小?所以,让我们最后谈谈这个问题。所以,OpenAI 有两个 text-embedding-3 系列的嵌入模型。它们有 small 和 large。small 是更快、更小的模型,这就是为什么它叫 small。而 large 模型是更大、更慢、更昂贵的模型。所以,两者之间的成本差异是,small 模型的使用成本将比 large 模型便宜六倍多。使用 large 嵌入模型的优点是,当您执行检索步骤时,large 模型实际上会更准确。您基本上会获得更多的数据编码。我正在尝试稍微简化一下,所以一些技术人员会有点生气。但基本上,可以这样想:small 模型不是那么好,但它便宜得多。它仍然非常好,只是为了清楚起见。当我说的不是那么好时,它是比较性的。它仍然是一个非常好的模型。text-embedding-3-large 模型更好,但它会贵得多。所以,如果准确性对您正在做的事情至关重要,您想要绝对最好的,您将需要使用 large 模型。也许您愿意支付差价,那很好,使用 large。我们将在本视频中使用 small,因为我们想确保对您来说尽可能便宜。我们在这里做的事情,我们实际上不需要担心结果。维度也是如此。维度越小,您可以看到 OpenAI 文档中的范围。您会看到,这可能需要您去查看嵌入文档。我不记得维度上限是多少。好的,看起来是这样。您可以在文档中看到。再次,这就是为什么您要阅读文档,它们有很多好信息。但是 small 的默认值是 1536,large 的是 3072。我不记得上限是多少。但是您可以查看我为您提供的链接,如果您想了解更多细节。但是当您处理向量数据库时,更大的维度意味着更多的存储。您正在存储更多数据。所以,您需要,您正在使用数据库上的更多资源,这将更昂贵,它将更慢。所以,如果您使用最小值 256,您基本上是在优化您的数据库,使其成本更低,大规模地显著降低成本。所以,如果您正在构建一个检索系统,它将处理相当大的规模,可能有大量的用户,大量的向量嵌入,您正在处理。只是要知道,维度计数越高,它将真正影响您在向量数据库上的资源。所以,例如,当我们设置我们的 Supabase 项目时,我们有那个微型规模。我们有那种最小的计算机,只是为了简单起见。那个数据库正在运行。如果您使用非常高的维度计数,您通常需要更多的内存。您的数据库以及运行它的任何服务器。所以,您将需要为那种系统支付更多费用。所以,这就是为什么我们使用 256 并使用 small,因为这样我们就不必担心这些问题了。我们仍然可以掌握所有核心概念。我知道这有点技术性,可能比一些人需要的要多。但我只是想解决这个问题。所以,如果您真的想扩展这些系统,显然您需要与人们交谈,并可能就这些决定进行一些咨询。当然,您可以使用很多不同的嵌入模型。我们在这里只使用 OpenAI 的一个,以保持简单。所以,如果我们实际测试一下,我们将要做的是,我们将创建一个小的异步函数。我们将称之为 main。再次,我可能需要休息一下打字了,我遇到了一些麻烦。我们将创建一个名为 main 的函数,我们将运行它,以确保一切都正常。我们将要做的是,我们将生成嵌入。我们将只测试一个嵌入。我们将说“你好,世界”。所以,我们将嵌入这段文本并从中获得结果。我们将记录下来,然后在该文件的最底部调用这个 main 函数。所以,我们可以运行它的方法是,右键点击 generate-embeddings。我们可以复制路径。如果我输入 tsx 空格,然后是文件名,这将运行该文件。假设您已经正确设置了您的 API 密钥,并且编写了所有这些代码,您应该会得到所有这些数字。再次,这是“你好,世界”文本的向量嵌入。所以,这是 generate_embeddings 函数。在这一点上,您已经了解了其中一些,模型和维度是什么,它们为什么重要。我们编写了这个函数,以便我们可以在其他部分使用它,因为我们将生成大量的嵌入。然后我们验证了它有效。所以,我们将在此基础上进行构建。所以,请确保您不要跳过任何一个,因为每个部分都将建立在前一个部分之上,直到我们到达最后。在您继续下一课之前,有一个快速说明:您将需要注释掉它,这样我们导入它时就不会运行这段代码。您会在参考文件中看到,我说“取消注释以测试,然后再次注释”。所以,想法是,如果您想测试文件,您可以像下面使用 tsx 那样运行它。tsx 是 TypeScript 执行。然后,一旦您完成了,您只需要再次注释掉。再次,我们有所有文件的参考文件,这样您就可以确保您的代码与我们为您提供的示例代码匹配。这样您就可以确保事情能够正常运行。我们显然已经验证了所有这些文件都能正常工作。但是让我们继续。我们将开始处理上传和嵌入您的数据,并将其放入您的向量数据库。让我们实际开始处理我们的模拟数据,并嵌入所有这些信息,所有这些小事实,并将它们插入到我们的向量数据库中。所以,我们现在需要做的是,我们实际上需要构建一个新的文件,我们将在这里重用 generate_embeddings 函数。所以,我们将创建一个新文件。我们称之为 upload-data.ts。我将继续用数字前缀,以保持整洁和有条理。所以,我们需要做的是,我们需要获取所有数据。所以,这些事实中的每一个。您可以看到这是一个包含 100 个对象的数组。这些对象中的每一个都有两个属性。我们有一个 content 属性,这是我们要嵌入的事实。然后我们还有一个 name,这是它对应的动物的名称。当我们将进行元数据过滤部分时,我们将使用这个 name。所以,让我们回到我们的 upload-data 文件。所以,我们将继续。所有这些函数都非常相似。我们将创建许多异步函数,我们将导出它们。再次,我们将在此基础上进行构建,并在未来的课程中使用我们编写的每个函数。所以,再次,我们可以称这个函数为任何我们想要的。让我们称之为 docs,我在参考文件中就是这样称呼它的,所以我将保持一致。docs 将是一个对象,它有一个字符串和一个 name,它也是一个字符串。这将是一个数组。太棒了。所以,这个 upload_data 函数接受一个参数,它只是一个文档数组,匹配我们在这个 mock_data 数组中使用的。太好了。所以,我们最终将调用这个函数。所以,我们将要做的是,另一个 async main。再次,您将在运行后注释掉它,但我们将在本课结束时进行。最终,我们将用这个 mock_data 调用 upload_data 函数。您只需要导入 mock_data 数组。所以,一旦我们为这个函数编写了代码,我们将运行这个文件,这将把所有数据插入到我们的数据库中,并带有嵌入。所以,话虽如此,我们实际上需要生成嵌入。所以,我们将创建一个名为 embeddings 的新变量。我们将将其保存到我们在这里使用的 generate_embeddings 函数的结果中。我们需要遍历我们的 docs。我们需要,让我们看看,doc.content。我们将实际上将它从 docs 改为 doc。再次,我们这样做的原因是,在我们的 generate_embeddings 函数中,请记住我们接受一个名为 text 的字符串数组。所以,在这里,我们只是保持数据类型与我们拥有的 mock_data 一致。但我们实际上只在 generate_embeddings 函数中使用 content 属性,因为事实本身是我们真正想要嵌入的。嵌入名称对我们没有多大好处。所有这些嵌入都会相同。所以,我们想嵌入事实,它在 content 属性中。所以,我们将运行 upload_data。它将生成所有这些事实的嵌入。再次,我们只是将它传递给下面的函数。它将把嵌入保存到这个 embeddings 变量中。然后,我们需要将所有这些数据插入。所以,我们将要做的是,我们将 db.insert。我们将插入到 facts 表中。values。所以,我们将进行 values。这将允许我们定义值。我们将想要做的是,我们将想要获取从 OpenAI 响应中获得的对象。再次,它将包含我们所需的所有嵌入。我们将想要做两件事。我们将想要获取每一个嵌入,然后我们还想要获取索引,因为我们需要使用索引来获取这些 docs 的信息。我们将要做的是,我们将返回一个对象,其中 embedding 是一个属性。它来自 OpenAI API。这就是发送到该模型的主要目的。然后,我们将获取每个索引处的 doc 及其名称。然后,我们将获取每个索引处的 doc 及其 content。所以,我们在这里要做的步骤是,当然,我们需要从我们的数据库设置中导入 DB。它来自 index.ts 文件。记住,我们在上一课中导出了那个 DB 和我们所做的那个小设置。所以,让我们回顾一下即将发生的事情,然后我们将实际运行代码。所以,我们将调用 upload_data 函数。我们将传递所有 mock_data。再次,它们只是那些带有 content 和 name 的事实。有 100 个。我们将生成 generate_embeddings 函数。这是我们在上一课中做的那个。我们将上传数据,将其插入到数据库中,以及它的名称和 content。所以,我们将
正在添加这三个不同的属性并将它们插入到我们的数据库中。所以我们可以再次复制这里的路径 TSX 粘贴文件路径,你应该会得到,当然我们得等一秒钟,因为这是一个异步操作,它将上传我们的数据,你在这里可以做的是,我们去我们的数据库,点击刷新,这里你会注意到,砰,我们现在在我们的数据库中有 100 条记录。你会注意到几件事,我们有自动生成的序列 ID,我们有名字,哦,你知道吗,我实际上在这里犯了一个错误,嗯,让我们回去看看,你会看到在内容上我做了名字而不是内容,所以也许这实际上是有帮助的,以防有人这样做错了,嗯,所以让我们来做这个改变,然后我们在这里保存它,你实际上可以做的是,如果你点击这个按钮,这是 subase 的一个很棒的地方,它们不仅让设置 postrest 数据库变得非常容易,而且它们还有各种非常好的 goys,各种用于处理你的数据的 UI。所以我们可以做的是,选择所有这些行,嗯,我们将删除它们,嗯,希望你注意到了拼写错误,或者我之前运行这个时遇到的那个小小的嗯 bug,但如果我在 Mac 上按 Ctrl C,我们可以再次运行它,这次我们将真正上传我们的嗯内容,所以我们应该希望,嗯,再次,我将在这里刷新它,你会看到,因为嗯嗯,我们的序列 ID 从 100 开始而不是从 101 开始而不是一个,它们会有点不同,但你可以看到我们有名字,我们有内容,我们有那个嵌入存储在我们从 PG Vector 获得的那个漂亮的 Vector 列类型中,然后当然,因为我们之前设置了我们的模式,我们自动获得了创建时间和更新时间。好的,你可以看到,嗯,我们得到了默认值,所以当我们插入时,它将默认为“现在”值。在这一点上,我们嵌入了我们所有的事实,所有 100 个。你可以看到我们有 100 条记录,我们已经正确地完成了嵌入,只是为了验证,我在这里使用了内容而不是名字,就像我在这里犯的错误一样,一切看起来都准备就绪了。我们最后要做的是注释掉它,这样它就不会在将来运行。所以我们现在已经上传了我们的数据,我们已经生成了所有这些嵌入,我们现在可以开始进行下一步,这将是进行一个非常基本的检索。所以让我们继续进行下一步,我们将处理基本检索。好的,所以我们将进行最基本、最常见的检索类型,你将使用嵌入进行检索,那就是非常基本的相似性搜索。所以让我们直接进入代码。这次我们将创建一个文件三,我们将称之为 retrieve-das-dat。好的,现在我们有了 retriev-dash-data。在这个文件中,我们将进行另一个名为 retrieve data 的函数。这次我们将接受两个参数,而不是一个。第一个参数将是我们的输入,它将是一个字符串。所以输入将等同于我们正在使用的用户查询。所以,例如,在这种情况下,我们可以问一个关于,我想主题是关于大象的问题。所以,你知道,如果我们说大象吃什么?这将是我们的输入。而我们需要它的原因是,我们最终需要为输入生成嵌入,并将其与我们数据库中的记录进行比较。我们需要对该输入与我们 Vector DB 中所有嵌入值之间的相似性进行搜索。第二个参数我们将使用 here 是 options。这些将是一些可选参数,我们可以用来帮助我们的查询。我们将进行一个 limit,它将限制我们从搜索中返回的结果数量。我们将进行一个 min_similarity。这将允许我们选择性地,我需要在这里添加一个问号来使其可选,所以我们可以选择性地按最小相似度分数进行过滤,这可能很有用。也许你只想要与查询极其相似的文档。所以这是过滤掉其中一些的好方法。我们还将进行 name,它可以是字符串或 null。或者 null,我们将 options 默认为一个空对象。当我们进行元数据过滤时,我们将重新审视 name。所以我们可能要过一会儿才会回来。但是我们想做的是我们称之为解构。所以我们将从那个对象中获取这些值并设置一些默认值。所以默认情况下,我们将接受 10 的 limit。所以我们每次至少会得到 10 个事实,除非我们另有说明。对于我们的最小相似度,让我们设置 0.3。所以我们只接受相似度分数计算高于 0.3 的记录。我们将 name 默认为 null。当然,我们必须从 options 对象中获取它。所以现在我们可以开始第一步了,那就是我们需要为我们的查询生成嵌入。所以再次,我们将在这里使用我们老朋友,generate_embeddings 函数。它将是一个数组,其中只有一个值,那就是那个输入。所以我们只会生成一个输入,而不是我们用所有这些上传生成的所有输入。再次,你将继续从我们一直在使用的文件中导入这些。所以一旦你有了那个嵌入,你就可以计算相似度。所以我们将在这里创建一个新的变量,称为 similarity。我们将在这里使用 SQL 类型。我们需要一些反引号。这将是 1 减去。所以如果你看了上一节,你会记得我们现在正在尝试计算输入与数据库中的记录之间的余弦相似度。为了做到这一点,我们将使用 cosine_distance 函数。记住,我们从 drizzle ORM 库中免费获得它,这非常方便。它需要两个参数。我们将采用列。在这种情况下,它是那个 facts_table.embeddings。再次,facts_table 将从我们的 DB schema 中导入。然后当然,我们需要将其与实际的 embedding 进行比较。在这种情况下,它将是那个 index 中的第一个项。这就是它抱怨的原因,那里有一个额外的 s。记住,代码语法很重要,拼写很重要,你必须正确地写出这些字符。但是,这将计算那个向量列的情况。它将把我们的嵌入查询与例如第一个事实进行比较,与第一个嵌入进行比较,并将计算其中一个记录的相似度分数。然后我们可以用它来进行相似性搜索。所以我们现在要做的是,我们将获取我们的文档。所以我们将在这里进行一些查询。所以这里会有几件事。首先,我们需要一个 select。这是一个 SQL select 操作。我们想选择 name,facts_table.name。我们将选择 content,facts_table.content。这只是指定内容类型,或者更确切地说,是列类型。我们还想获取相似度分数。当然,这是 drizzle 语法,带有 SQL 查询。如果你不熟悉的话。所以,如果你熟悉 SQL,你知道其中一些听起来会非常熟悉。我们将从 select from facts_table where。所以这是 where 子句,这里会有点棘手。所以我们将根据 name 是否为 null 来有条件地执行 where 子句。再次,当我们进行元数据过滤时,我们将回到这里。但现在,如果 name 有值。所以,如果 name 被识别。如果我们进行元数据过滤,我们将以最小相似度计算相似度。然后我们将进行一些花哨的语法。这只是为了帮助我们处理大小写敏感的值进行比较。一旦我们进入元数据过滤部分,这就会有意义。再次,你将需要确保它写得和我一样,因为语法在这方面很重要。所以,如果你需要检查那些参考文件,以确保你都准备好了。所以,这个问号的意思基本上是,如果 name 有值,如果它不是 null,我们希望这是我们的 where 子句,否则。所以这就是冒号代表的。否则,我们只想大于 GT。GT 代表大于,如果你想知道那是什么意思。所以你可以看到大于。你可以看到,悬停在这里。它只是测试第一个表达式是否大于第二个。所以我们给它相似度是因为那是相似度分数,然后我们只是确保它大于最小相似度。再一次,我在这里犯了一个 s。必须停止这样做。所以我们有了 where 子句。让我们进行一个基本的 order by。只是因为我们想按相似度分数对结果进行排序。所以我们将从最高相似度分数到最低排序,然后我们将根据任何限制值进行限制。再次,记住默认值是 10。默认情况下,最小相似度分数是 0.3。然后一旦我们有了所有这些文档,我们就可以返回它。所以,再次,我必须确保我拼写这些东西正确。所以,在我们实际运行代码之前,让我们快速回顾一下。我们创建了一个 retrieve_data 函数,它接受两个输入,或者说接受两个参数进入函数。它将接受用户输入,它将接受我们的 options,这是一个对象。有几个不同的值,我们将获取这些值或设置默认值。我们将为我们的输入生成嵌入。我们将把这个输入与我们表中的所有记录进行比较,计算相似度分数。再次,我们建立的索引,当我们构建 SQL 迁移时,允许我们大规模高效地做到这一点,这很棒。喜欢余弦相似度与 PG Vector,非常非常有用,非常棒。然后我们只是获取文档。我们从 facts_table 中获取 name,content 和 similarity。其中条件满足。按相似度从高到低排序,然后限制为我们指定的数字,默认是 10 或我们指定的数字。我们可以这样做,我们可以做另一个异步函数 main。在这种情况下,我们将做 constant documents = await,然后我们将调用 retrieve_data。我将只说告诉我大象。告诉我大象。我们将为这个计算嵌入,与我们所有 100 条记录进行比较,并找出最相似的 10 条,假设它们都高于至少 0.3。然后我们将在这里记录下来,这样我们就可以看到我们将调用该函数。现在我们可以复制文件路径,然后 TSX 空间文件路径,然后回车并运行它。如果我把它放大,你会注意到我们现在有 10 条记录。1、2、3、4、5、6、7、8、9、10。所以你可以看到最低的相似度分数是 0.315。所以这里所有 10 条都高于 0.3。你可以看到我们正在计算相似度分数。所以这就是我们非常基本的检索步骤所做的。它接受了我们的查询,即告诉我大象,然后它抓取了它计算出的我们数据库中最相关的 10 个事实。我们的大象系统开始工作的迹象之一是,我看看最相似的五个事实是什么。它们都是关于大象的事实。而且你不仅看到它们都是关于大象的事实,而且你还可以看到这里的相似度分数实际上比其他分数要高得多。所以你刚刚完成了基本检索,这很酷。再次,这就是我在本节开头提到的,使用非常基本的数据集。它很容易说明我们正在做什么。你显然可以处理不同的数据,你可以处理巨大的 PDF 文件,并且可以把很多内容塞进这些值中,等等。但很容易理解,当有一个相对简单的数据集时,相似性搜索实际上正在起作用。所以,这是非常基本检索。很多人会在这里停止,但我们不会在这里停止。我们将教你很多不同的东西,你可以添加到你的 rag 系统中。我们希望确保你拥有最常见、最受欢迎的工具。这样当你走向现实世界并构建各种 rag 项目、构建这些 AI 项目和工具以及所有这些不同的东西时,你就可以真正地采用像这样的基本 rag 系统并以更高级的方式构建它,这样你就会觉得你在构建非常有竞争力的工具和非常有竞争力的产品。所以让我们开始进入一些更高级的概念。最后但同样重要的是,请确保你再次注释掉 main 函数。我正在插入这个,我不得不编辑它,因为我忘了做。所以这会很快。请确保你注释掉那个函数,伙计们。我们将从查询优化开始。查询优化是我们将在那里讨论的第一个高级检索技术。所以我们将介绍,我想是五种不同的技术。这是最容易实现的。这是一个非常简单的过程。成本非常低,无论是延迟还是实际的美元成本。通过进行查询优化,你可以获得相当多的性能提升。所以这是我在 rag 系统中用于我的实际工具集的技术之一。所以让我们回到代码库。所以我们要做的是,当然,我们将创建一个新文件。这是第四个。我们将进行优化查询。优化。让我们把它拼写对。好的,所以我们将拥有这个优化查询文件。我们将创建一个函数,并称之为 g_optimized_query。这个函数将非常简单。这将是一个相当快的。所以,我将接受一个参数,即 query。因为目标是优化该查询。我们将通过直接使用 OpenAI API 来做到这一点。所以,我实际上会帮忙。让我们先写代码。所以我将创建一个新的 response 变量。我们将使用 openai chat completions API。chat completions。让我们看看。它创建了。现在让我们输入这些参数。让我们实际获取那个导入。再次,当你处理这些文件时,要注意的一件事是,如果你像我一样使用自动导入系统,它可能会直接导入 OpenAI SDK。我们实际上只想做我们在这个 CL 文件中初始化的那个。这将使我们的生活更轻松。你只需要注意导入。我们将使用 40 mini。我将稍微谈谈一些区别,以及何时你可能需要一个更强大的模型。但让我们实际传入我们的消息。所以我们将需要一个系统提示。所以我们将需要一个带有系统角色的对象。对于这个内容,我将直接从屏幕上复制粘贴。我将确保在下面包含提示。让我记下这一点,在下面包含提示,以确保我为你做到这一点。这显然可以在参考文件中找到,但它也会在课程笔记中。我们将使用用户消息。你只会看到它是一个用户角色,内容只是我们的查询。所以这就是我们将输入到模型的内容。然后让我们处理 return 语句。也就是说,我们想从消息响应中返回内容。如果出于某种原因,它返回为空,我们将返回查询,这样我们就不会返回 null 值。我们需要确保我们实际上返回一个字符串,即使它是没有被优化的查询。所以,让我们来谈谈这个提示,然后我们将谈谈模型以及这个过程中发生了什么。所以你可以看到,我不会全部读出来。让你坐在这里听我读文本,你可以自己读。但基本上,我们在这里给出五个简单的指导方针,告诉模型我们想要什么作为响应。所以这里的查询优化的想法是,如果你知道,如果我们问一个非常复杂的问题,比如让我们实际打开我们的模拟数据并思考一下。所以,其中一个事实是,章鱼可以再生失去的肢体。所以,也许我们问一个非常复杂的问题,我们正在进行关于章鱼的研究问题,需要写一篇论文,我不知道,我只是即兴发挥。但是,可能发生的一件事是,突然,你知道,你的查询是一整段话。我敢肯定,如果你使用 ChatGPT,你使用 Cloud,任何 AI 应用,你知道,有时你可以输入非常非常长的查询。有时,你知道,以代码为例。很多时候,如果我们使用光标,我们复制粘贴很多代码。发生的事情是,这些查询变得非常非常长。当你实际进入生成嵌入的步骤时,信息变得不那么密集了。很多额外的东西被添加进去了。嵌入变得更难用于检索目的。所以,你可以用查询优化做什么,你几乎输入了我们正在使用的完整查询,并且你几乎试图得到一个看起来类似于搜索引擎请求的结果,其中 AI,当然,如果你通读这个提示,例如,你知道,删除不必要的词。确保查询清晰简洁。保持原始意图。识别关键概念。所以,这里会发生什么,我们将在接下来的课程中看到一些例子。但是,它将尝试压缩我们非常非常长的查询,如果我们有一个非常长的查询,它将把它压缩成类似关键词的东西。所以,你从这些关键词获得的嵌入,而不是完整的文本,将信息密度更高,更准确。它将更好地代表你实际上想从检索系统中获得的东西。所以,通过做这样的事情,通过使用像 GPT-40 mini 这样便宜的模型,并基本上提取几个关键词,就可以极大地改善你从相似性搜索中获得的结果。这基本上就是查询优化。它试图将一个非常长的查询减少到几个关键词,几个关键短语,这样你就可以获得更好的检索结果。所以,便宜的模型在这里非常好,因为它会非常快,而且会非常便宜,如果你试图获取大量文本并将其压缩成几个词或几句话,这会非常划算。而且延迟也很重要,因为当你堆叠一堆对不同模型的检索调用时,时间会累加。所以,有时用户体验可能很糟糕,如果你将太多慢的东西串联起来。所以,这就是为什么我们使用 GPT-4 mini。我们不需要太多的推理能力。有时,如果你使用更好的模型,我们只是使用 GPT-40 vanilla。可能会得到一些,你知道,关键词可能更好,特别是对于非常长的结果。但通常 mini 是一个很好的模型。就像 Claude,Hiu,那将是一个很好的模型。Google 的 Gemini Flash 系列,那将是一个很好的模型。所以,我们将在这里停止。实际上,让我们先运行一次,这样你就可以大致了解发生了什么。所以我们将做我们典型的做法。我们将创建一个异步 main 函数。我们将在这里创建一个名为 optimized_query 的变量。我们将继续使用这个告诉我大象的例子。然后,如果我们获取文件路径来运行它。让我们稍微放大一下终端。你会看到它接受告诉我大象,然后优化它为关于大象的信息。所以,我可能更喜欢,老实说,这应该是大象。所以,如果我们去,你知道,有一些东西可以玩。比如,让我们尝试一个更强大的模型。看看我们会得到什么。所以,这实际上更长。所以,让我们回到 mini。也许如果我们调整一下。让我们调整一下温度参数,将其设置为零,这样它就不会那么有创造性。你再次看到关于大象的信息。所以,也许你想做一些事情,比如提供例子。也许你想倾向于几个词。你知道,就像使用一到五个词。也许添加一条新规则。倾向于更短的查询。也许我们再试一次,看看是否能得到。好的,现在我们只得到大象信息。所以,我们稍微减少了一点。所以,你可以看到如何玩查询优化。这取决于你如何去做。你将需要在你的完整 rag 系统的上下文中评估它。但我认为这可能是这个视频的一个很好的停止点。重点是,当你将一个非常长而复杂的查询减少到几个关键词时,你可以获得更好的检索结果。而这正是实现这一目标所需要的。所以,我们将在未来的课程中使用它,但这正是我们开始工作所需要的。当然,让我们确保我们注释掉这段代码,这样我们就可以继续进行下一个高级技术。我们将使用 coher API 进行文档重新排序,这是一个很酷的。本课我们将学习如何使用文档重新排序器来对你的检索系统进行排序,以便为你的 rag 系统添加高级检索技术。我们只是在这个阶段堆叠性能增益。所以我们将使用 coher API。所以希望在本节的早期课程中,你已经获得了你的 coher API 密钥。如果你没有,请确保去 coher dashboard。一旦你注册了,去 API 密钥,并确保它在你的 .env 文件中。你就可以开始了。所以我们要做的就是,我们将创建一个新文件。我们现在在文件五。我们将称之为 rerank_documents。rerank_documents.ts。我们将在该文件中利用 coher 的 rerank API。所以,这将做什么呢?我们将采用我们的基本检索过程,并通过使用另一个 API 端点来进一步优化这些结果。所以,我们将进行相似性搜索,然后我们将进行文档重新排序。有不同的方法可以做到这一点。有些人喜欢以不同的顺序进行。我真的很喜欢进行检索,基本检索,然后进行重新排序。这可能是更常见的场景。但基本上,这个端点将做什么。所以,我们只是使用 coher 的 V2 API。我们将进行查询,它将需要一个文本列表。所以,在这种情况下,它只是我们从相似性搜索中获得的那些记录。我们将传递这个查询和文本列表。coher 将为我们做的是,它将对我们提供的文档进行排序。所以,在这种情况下,我们通常会使用我们从相似性搜索步骤中获得的 10 条记录的限制。然后它将对这 10 个文档进行排序。所以,也许我们想,你知道,通过这两个步骤运行,然后只产生,我不知道,三到五篇最相似的。所以,只是另一种获得更好排序的方法。所以,让我们开始编写这个东西的代码。编写代码。我们将开始创建一个 rank_documents 函数。为我们编写代码的好处是,它将更清晰地说明实际发生的事情。所以,如果你感到有点迷失,没关系。代码将使这个的有用性非常清晰。所以,这个函数将接受两个参数,与我们的 retrieve_data 函数非常相似。我们将接受一个 query,我们将接受一些 documents。我们将使这些 documents 的数据类型等同于我们的 facts,带有 content 和 name。它将是一个数组。我们还将有一个第二个参数,它将是一个 limit。默认是 3。所以,我提到过,文档重新排序器的一个很棒的地方,说十遍。是你可以先通过余弦相似性搜索,然后进行第二次通过来真正优化文档。所以你可以把它缩减到最相关的文档。并将限制从,我们将使用从 10 到 3 的例子。所以,我们有这三个参数:query,documents 和 limit。所以,让我们开始使用 coher API 编写 rerank 代码。我们将使用 rerank 端点。我们将再次传递,我们需要传递 texts,然后我们需要传递 query。因为你需要有东西可以相互比较。所以,我们将 doc.content。当然,你需要从 API 客户端导入 coher API。你还需要拼写正确。raw_ring,因为它不会是正确的。但请确保导入你在 API 客户端中设置的 coher 客户端。所以,我们将最终使用它。我们需要将 query 作为第二个参数传递。然后我们还有另外两个参数。我们将进行 top_n。这只是记录的数量。在这种情况下,将是我们的 limit。所以,在这种情况下,我们将默认使用 3。然后我们还需要指定模型。我们将使用 rerank-english-v3.0。这就是从 coher API 获取重新排序结果所需的一切,基于我们的查询和文本列表,它将来自我们余弦相似性步骤中获得的记录。当然,我们只想返回它们。所以,我将快速过一遍。我们将使用光标标签。所以,我们想获取 name。我们想获取 content。然后我们还想获取 relevant_score,这是 coher API 如何为我们重新排序的。那就是我们将从该 API 获得的分数。为了测试这一点,我们需要做一个小的 main 函数。当然,我们将在下面调用它。让我们创建一个名为 retrieved_docs 的变量。在这种情况下,我们将进行第一步。我们将只进行熟悉的相似性搜索。我们将使用我们已经花时间编写的 retrieve_data 函数。当然,我们需要 await 它。这将是 retrieve_data。我们将继续使用 tell me about elephants。当然,你需要从那个特定的文件中导入它。然后一旦我们有了 retrieved_docs 变量。记住,我们只是将其作为返回的文档。现在我们将做什么?我们将创建一个名为 documents 的变量。这次我们将把 retrieved_docs 传递给 rank_documents。首先,我们需要 tell me about elephants。再次,我们正在使用相同的查询。然后我们需要传递 docs,我们从检索步骤中获得的 docs。然后我们将做的是,当然,我们将 console.log 这些 documents。这样你就可以看到这个 API 结果是什么样的。所以,让我们复制文件路径并开始运行它。我将进行一次 clear,以清除它。我们应该能够找到的是,我们得到一个包含三条记录的数组。所以,你可以看到我们正在获取 name。我们正在获取 fact。然后我们正在获取从 coher API 返回的 relevant_score。所以,基本上发生了什么。你知道吗,实际上,我也会记录 retrieved_docs。这样你就可以看到它是如何被缩减的。让我们做一点 Ctrl C,再次运行它。让我们把它放大,这样我就可以真正强调这一点。所以,我们得到的第一个结果,第一个这里,将是仅从相似性搜索中获得的原始 10 条记录。所以,我们已经在之前的课程中看到过这个结果,当我们第一次测试 retrieve_data 函数时。我们得到了所有五个关于大象的事实,然后我们得到了其他五个。你可以看到我们得到了长颈鹿,犀牛,甚至海豚也混进来了。你可以看到它们带有相似度分数。所以,我们用 coher reranking API 做的是,我们传递我们的查询,即告诉我大象,我们传递所有文档列表。除了我们只获取 content 属性,以简化事情。而 coher 正在做的是,它正在根据它们计算的相关分数将所有这些从 1 到 10 进行排序。并返回最相似的三个事实。所以,你可以看到,如果我们比较这两个列表,你会发现第一个结果,大象是唯一不能跳跃的动物。在我们的相似度分数中,它实际上排名第四。当我们添加一个排序器时,它就跳到了第一位。所以,你可以看到,reranker 一定做得很好,因为它保留了所有关于大象的事实。这绝对是你所期望的。而且这个查询不是,我可能应该做一个更好的例子来真正强调这一点。比如,让我们看看我们的模拟数据。让我们再做一个小例子。我们可以用企鹅做些什么吗?好的,有一些关于睡眠的问题。让我们尝试一些关于睡眠的事情。让我们实际上改变这个查询,变成我想了解动物睡眠。让我们再运行一次。我将 Ctrl C,再次运行它。我们可以再看一个例子。所以,有趣的是,我们实际上没有得到 10 条记录。这告诉我,如果你还记得我们编写的 retrieve_data 函数,我们有一个默认的 0.3 相似度分数。所以,通过使用最小相似度,我们可能已经删除了几条记录,这实际上对检索系统来说非常好。你基本上设定了一个硬性界限,我们只想要与 x 相关的。所以我们得到了这六条,然后我们把它缩减到这三条。所以,你可以看到,订单又有点变化了。所以,你会看到,当我们进入更多的例子时,特别是在下一节,当我们做一个生产级的系统,处理一些更复杂的数据集时,重新排序器可以带来更好的结果。coher 的重新排序 API 非常便宜。所以,你可以获得很多性能,而且成本很低。所以,这就是重新排序器的工作方式。我们将像往常一样注释掉这段代码。继续让它变得更好。但这将在以后变得更加明显,为什么它有用。所以,请继续关注。接下来将是我们的下一个高级技术,它将是元数据过滤。所以,我们将终于谈谈为什么我们一直使用 name。是时候终于进入元数据过滤了。所以,我们将在本课中要做的是,我们将使用 OpenAI 函数调用。所以,我们将使用 LLM 加函数调用来为我们的查询添加额外的查询或额外的 where 子句。所以,演示它的最好方法就是直接开始。所以,我将要做的是,我将清除我之前的工作。让我们创建一个新文件。我们将称之为 filter-das-metadata。metadata.ts。这里将发生的是,我们将构建一个小的 name 提取器,带有一个函数调用。所以,如果你不熟悉函数调用,在 AI 世界里,我们将边做边介绍。所以,我们需要做的是,我们将创建一个新的异步函数。我们将称之为 extract_name。它将接受一个查询参数,这就是这个函数所需要的。所以,我们将做几件事。我们将实际调用 OpenAI API。所以,我们将使用我们老朋友的 chat completions 端点来做到这一点。我们需要做几件事。像往常一样,我们将使用 GPT-40 mini 进行消息传递。我们实际上不会使用系统消息。我们将只使用一个用户消息,带有查询。这将是一个非常非常基本的调用。我们还需要添加 tools 参数。它将是一个工具数组。我将直接复制粘贴我在这里的工具。这样我们就不必输入所有这些。再次,我可能在上一课或之前提到过,但你要确保你正确地导入了。你想导入你制作的客户端,而不是直接从其库中导入 OpenAI 包。所以,我们将要做的是,实际上,让我们再做一件事。作为工具调用的一部分,你实际上需要解析你的工具调用。我将在这里自动完成。然后我们想返回解析的工具调用或 null 值。以防我们实际上没有 name 实体。所以,让我们退一步。现在我们有了所有这些代码。让我解释一下发生了什么。所以,我们使用 GPT-40 mini。我们将一个消息发送到 OpenAI chat completions API,带有那个查询。这个工具是做什么的?所以,正如你所看到的,我们必须定义一个函数调用。函数调用基本上是 AI 模型识别我可以使用很多这些工具的能力。何时使用它们。所以,你会在 AI 世界中看到的最常见的工具之一是 get_weather 函数。我们将,你知道,函数调用不是本视频的重点。所以我只是会快速过一下。但想法是,假设你在这里有另一个工具,get_weather。你问了一个关于天气的问题。那么模型很可能会知道,嘿,我应该调用 get_weather 函数。而你在这里定义的 schema 将定义模型作为工具调用的一部分实际返回什么。所以,在这种情况下,我们有一个 filter_by_name 函数。你可以看到我们有一个 description。它被粘贴到 GPT-40 mini 中。所以,它就像这个特定函数调用的提示。正如你所看到的,我们试图从查询中提取动物或实体的名称。它必须是动物的完整名称,并且是复数。所以,如果我使用我们的,你知道,我想了解动物睡眠。可能不会得到一个特定的动物,因为我们没有问任何东西。但如果我们使用,比如 tell me about elephants,我们之前用过的。这个函数调用将返回,因为你可以看到我们只有一个属性,name。动物或实体的名称,并且它是必需的。它将返回该动物的名称。LLM 将会推理出来。所以,给定一个像 tell me about elephants 这样的查询,它很可能会返回 elephants。为了证明这一点,让我们测试一下。所以,我们将创建一个查询。我们将只说 tell me about elephants。我们将继续使用它。我们想做的是,我们实际上想创建一个名为 name 的新变量。我们将用这个查询调用 extract_name 函数。所以,希望我们应该能够记录这个 name。如果我们得到 elephants。所以,如果我们实际运行它。让我们复制文件路径,在我们的终端中运行它。希望我们应该得到 elephants 的记录。太棒了,果然我们得到了。所以,这就是 name 提取器的作用。如果我这样做,你知道,告诉我关于长颈鹿的事情。如果我们运行它,我们应该得到长颈鹿。所以,你大概明白了。它基本上是在尝试提取哪种动物。所以,我们可以做的是,因为我们在 where 子句中内置了可选的 name 过滤。作为 retrieve_data 函数的可选参数。我们将能够传递动物的名称,以确保我们只获取与该特定动物相关的记录。所以,让我们实际测试一下。所以,我们将创建一个名为 documents 的新变量。我们将调用 retrieve_data。这次我们将使用可选的第二个参数,带有 name。我们将直接传递我们。
从我们的函数调用中提取。好的,那么如果我实际打印出这个,然后我们再运行一次这个函数,我们在这里应该看到的是,我们应该看到 um,我们应该看到 uh,关于 um 大象的记录。好的,所以你可以在这里看到 um,这是可能的。好的,有趣的是,模型实际上给了我们一个空值,这有点有趣。让我们再运行一次,看看这次我们是否能得到正确答案。好的,所以我们一遍又一遍地得到空值,这告诉我我们可能可以把它做得更健壮一些。所以,例如,我们可以做的一件事是尝试使用一个更强大的模型。让我们尝试使用 GPT 40 而不是 Mini,看看这是否会对任何事情产生影响。好的,看起来这个模型可能好一点。好的,你可以看到我们得到了 um,这是可能的。我真的犯了一个小错误吗?哦,我确实犯了。让我们修复它。我将保存它。我认为我不需要更改任何东西。我想在我操作检索数据文件时,我不小心按到了我的按键。让我们再运行一次。好的,很棒,这就是我想要的。好的,嗯,我不知道为什么 GPT 40 Mini 几次都返回空值,这有点奇怪。可能是因为我只需要构建一个更好的描述。也许是一个更好的属性描述。在提示方面有很多事情可以做,比如也许让我们再用 Mini 试一次。我只是,我的意思是,这是一个很好的练习,只是看看底层模型本身如何实际地极大地影响你的查询。好的,嗯,无论出于何种原因,Mini 现在都不想工作了,而且我们也不想让 um 提示成为这个视频的重点。我只是想向你展示元数据过滤是如何工作的。所以你可以在这个例子中看到。好的,我们进行大象扫描,然后你就可以看到,我们只从我们的检索系统,从我们的 rag 系统中获取与大象有关的事实,因为正在发生什么?好的,如果我回到代码,我们正在从这个查询中提取名称,将该名称保存到一个方便地称为 name 的变量中,然后在 retrieve R data 函数中,我们正在做的是,我们正在传入那个可选的 name 参数,并且正在发生什么?在 retrieve data 中,好的,回到几个课程之前,对吧?我们在这里提取了 name,然后如果 name 存在,所以你会记得我们有一个所谓的三元运算符,那就是如果这个 name 有一个值。好的,所以以前,以前我们只是运行这个,这个大于号和相似度分数,但现在我们运行大于相似度分数,它被包装在这个 and 函数中,这个 and 助手在这里,我们正在做的是,我们正在检查 if fax table.name,所以如果 fax table 中的值,或者列中的值,或者更确切地说,name 列的值。好的,所以我们看看我们的数据库,只是为了让这更清楚一点,对吧?我们有这个 name 列,里面有动物的名字,对吧?所以在这个例子中,它对应于大象,所以它只是获取这些记录,它实际上根本不看任何这些,它只是抓取大象的记录,这真的很方便,对吧?因为以前发生的是,我们运行了一些查询,你知道可能只是针对特定的动物,对吧?在这种情况下,获取关于考拉的事实对我们来说完全没有意义,对吧?所以,你可以用元数据过滤做的一件事是,为了一个真实的例子,也许让它更具体一点,就是说你在这里有很多,你知道,收益报告,而不是 name,也许它是公司名称,对吧?也许你有谷歌,你有英伟达,你有特斯拉,你知道,你有苹果,你知道,你有所有这些不同的公司,你知道,然后说,说你问了一个关于微软的问题,例如,对吧?就像,嘿,给我一份微软收益报告的摘要。那么,你可以做的一件事是,你可以构建一个公司名称提取器,而不是一个动物名称提取器。然后你可以做的是,它会在这里提取微软,你就可以根据与微软作为公司名称相对应的记录进行过滤。这样,你实际上就不会意外地从,你知道,比如说,苹果那里拉取记录到你的检索系统中,对吧?所以元数据过滤允许你做各种有趣的事情来减少不相关的记录,对吧?你可以编写更精细的查询,当你将它与函数调用结合起来时,它是一种构建这些提取器的好方法,这些提取器可以接受你的查询,提取相关的元数据,然后你可以使用你提取的元数据来缩小你的查询范围。所以这是一个非常强大的概念,你可以在你的 rag 系统中使用它来帮助缩小结果范围并获得更准确的信息。所以这就是元数据过滤。我们将再次注释掉 main 函数,然后我们将继续进行下一个高级技术,即文本分割。我们将把这个部分,或者说这个课程,保持得相对简短。但我确实想谈谈文本分割,文档分割器之类的东西。对于我们正在处理的数据集来说,这并不是一个大问题,显然我们所有的模拟数据事实都相当短,对吧?所以文本分割并不是我们真正需要做的,我们将会在接下来的部分,当我们处理更长的文档时,我们会更多地关注这一点,而且我们必须,你知道,分割东西,因为 token 大小是我们想要管理的。关于这些记录,但我确实想稍微提一下文本过滤。所以让我们开始吧。我们将要做的是,我们将去你的代码,你的代码,我们将创建一个新文件。让我们看看,这是文件 7,文件 7,我想我得记下我在哪里。好的,所以 text spitting dots。我们将要做的是,我们将做一个非常基本的 split text 函数。我们实际上不会使用它。我确实在这一部分的开头提到过,如果我们再次打开那个模拟数据,你会看到我们有这个 elephant Wiki。所以这是我们模拟数据文件中的第二个值。这是我让 AI 生成的维基百科条目,有点长。你可以看到它有几百行文本。我们将使用那个值来测试文本分割器是如何工作的。好的,所以我们将要做的是,我们将在这里导出异步函数来创建一个 split text 函数。让我们确保我们在这里使用了正确的的大小写。这将接受一个单一的参数,它将是一个对象数组,匹配我们一直在处理的数据结构。所以对象中有两个字符串,content 和 name。而我们在这里需要做的是,我们实际上需要分割这个文本。所以在我们实际编写代码之前,我将稍微谈谈为什么文本分割器,为什么文档分割器很重要。所以当你处理像 elephant Wiki 变量这样的东西时,会发生什么,所有这些文本,对吧?这更具代表性,你知道,你在真实世界环境中可能会处理的文档,对吧?你知道,假设你在抓取维基百科或者其他什么东西,你想在所有那些维基百科信息上构建一个问答机器人,问答 AI 机器人,对吧?你可能需要把一些更长的文件分割开。所以这就是文本分割器发挥作用的地方,你试图做某种系统性的分割,以防你试图分割或将特定的文档分割成许多更小的文档,以便更容易地处理 rag 系统。好的,就像我提到的,我们不会在这里深入细节,因为你可以,这是一个兔子洞。所以如果你想更深入地研究文档分割器,处理更多真实世界的数据集,你知道,你想办法把它们分割成更小的值。你知道,例如,一个常见的文档分割器是,如果你处理一个 PDF,你可能会根据页面来分割 PDF,你可能只为那个特定页面创建一个嵌入。所以你知道,如果你有一个 100 页的 PDF,你把它分割成 100 个不同的文档,然后你嵌入每一个。打破事情会更顺畅一些。所以如果你想进入那个兔子洞,就去吧。我们只会展示一个非常基本的,你知道,你可以根据字符数来分割的方式。所以让我们开始吧。实际上,让我们做单词计数。所以我们将要做的是,我们将在这里定义 content 数组,或者 chunks 数组,因为我们将基本上尝试获取那个 elephant Wiki 并将其分割开。所以我们要做的其中一件事是,我们将决定我们想要一个 500 个单词的块大小。所以我们将要做的是,我们将把这个维基百科条目输入到我们的函数中,我们将尝试将其分割成尽可能多的 500 个单词的块。所以让我们开始吧。所以我们可以做的是,我们可以创建一个 for 循环。好的,所以我们将遍历我们在这里接收到的数据,遍历我们的数据数组。在这种情况下,当我们实际运行函数时,它将只是数组中的一个项目,对吧?因为我们只处理一个数据项。但我们在这里想做的是,我们实际上想跟踪单词。我们将要做的是,我们将做一个正则表达式,一个 Rex,这就是这行代码。我现在把它去掉。我在这里标签打得有点太多了。而这只是分割内容。所以那个 content 属性,它会按单词分割。所以如果我们回到我们的数据,它会,我的意思是,我不需要解释,你们都明白分割成单词是什么意思。哦,天哪,很有趣。但是我们将要做的是,我们将做一个 for 循环。我们将遍历那些单词。所以我们将遍历 words.length。好的,然后我们将按块大小进行迭代。在这里,我们将要做的是,我们将创建一个名为 chunk words 的变量。我们将切片那些单词,并将我们切片后的块推入 chunks 数组。所以你可以看到 chunk words.join,我们是在那些单词上连接。所以我们做的是,我们基本上一次抓取 500 个单词,一旦达到 500 个,就将其推入那个数组,然后继续下一个。所以我们在这里最后想做的是,我们想返回 chunks。我可能犯了一个轻微的语法错误,那就是我需要把它变成一个数组。好了,我们已经消除了那个错误。现在我们要做的就是创建一个名为 main 的新函数,当然我们想调用它,以便我们可以测试这个东西。现在让我们来处理这个第一个 bin。所以我们将创建所有那些块。所以让我们实际运行那个 split text 函数。我们将要做的是,我们将传入一个包含单个对象和 content 的数组。我们将导入 elephant Wiki。我们将导入那个文本,它将是 content。然后,你知道,name 将会,就像我们把整个维基百科添加到我们的数据库一样。我们可以做的是,我们可以做 console.log。我们可以记录那些块,我们也可以记录块的长度,这样我们就可以看到我们得到了多少个 500 词的块。让我们运行这个东西。让我们看看我们得到了多少个不同的块。好的,所以我将向下滚动,做一点 contrl C,这样我们就可以重置那个终端,清理一下。然后我将粘贴我们的文件名,带有 TSX。好的,所以看起来我们得到了三个不同的块。所以看起来我们填满了,让我们实际全屏显示,这样你们更容易看到。好的,所以我们得到了第一个完整的块,我们得到了第二个完整的块。所以这些每个都将是 500 个单词,对吧?这就是按 5 个单词分割的意义。然后最后一个将是剩余的单词。所以看起来我们还没有完全结束,对吧?所以这不一定必须是 500 个单词,对吧?假设维基百科只在这里的这个句号结束。那么这个块就会很小。碰巧的是,这看起来大约是 500 个。所以这些看起来都差不多。但重点是,我们将这段非常长的文本分割成了许多更小的文本。然后我们将嵌入每一个这样的小块,并将其上传到我们的向量数据库。所以这就是我们目前要讲的文本分割。我们实际上不会在我们的 rag 系统中使用它,在这个特定的例子中。但它非常普遍,尤其是在你处理更高级的 rag 系统时。我只是想让每个人都知道。我们可能会在接下来的部分,在 take off 和 rag 相关项目中更多地讨论这一点。你知道,就像我之前提到的,文档分割器是一个兔子洞。但这就是你现在需要知道的全部。让我们确保我们注释掉这个函数,然后我们可以继续开始构建一个完整的、高级的 rag 系统。我们将把我们刚刚学到的所有东西都放在一起,这样我们就可以优化一个非常好的、生产级的 rag 系统。所以我们将把我们学到的所有 rag 技术都放在一起。所以我们将要做的是,我们将最终创建文件 8,也就是 all together。好的,我们将把每一个技术都放在里面。查询优化,元数据过滤。我们将使用我们的基本检索,我们将使用我们的文档重排序器。所有这四种技术都将在一个管道中。我们不会使用文本分割,但我们会使用其他四种。所以让我们开始吧,看看我们如何获得更好的结果。好的,这将是一个非常好的 rag 系统,我们已经构建好了。所以这将是一个名为 process query 的函数。它将接受一个单一的参数,即你的原始查询。仅此而已。这就是它工作所需要的一切。所以第一步将是使用我们的查询优化器。所以我们将它保存到一个名为 optimized query 的变量中。我们将使用那个 get optimized query 函数。你会记得它只需要一个参数,query。然后我们得到那个优化的查询。我们也会把它打印出来,这样我们就可以在运行函数时看到所有这些步骤。所以第一步是获得优化的查询。这将影响从这一点开始的所有下游。接下来我们要做的就是使用我们的元数据过滤器来获取那个实体名称。所以我们将要做的是,我们将再次使用 extract name 函数,它只需要一个参数 query。所以我们已经导入了这两个。所以我们将优化查询,从 um 查询中获取实体名称。所以现在我们可以做的是,我们可以运行我们的基本检索。我们可以运行 retrieved docs。我们将它保存到那个变量中。我们将调用 retrieve data。我们将传入的不是我们的常规查询,而是我们的优化查询。我们将传入 name,我们将传入那个实体名称,以便我们可以使用元数据过滤来进一步限制我们的结果。我们将结果限制为 10 个。我们将 um,我们将只是,我们将实际上删除它,我们将只使用默认值。然后我们将它们打印出来。最后我们要做的就是获取 retrieve docs。我们将把它们限制在我们的排序结果中。所以我们将使用那个 coher here ranker。再次,我们有所有这些函数,用于我们 rag 过程的所有这些不同的点,这很好。我们将传入优化的查询作为搜索查询。我们将传入我们从基本检索步骤中获得的那些检索到的文档。我们将继续,我们将把它缩小到三个最相关的imerick。然后我们将它们打印出来。最后,当然,我们要返回那些排序的结果。所以实际上不需要做任何新的事情。我们只是在不同的点使用每一个现有的步骤。然后我们可以做的是,我们可以实际测试它。所以我们基本上堆叠了一堆非常有用的检索步骤,试图在一个优化的 um 优化的 rag 管道中工作。这更具代表性,是一个真实世界的用例。这就是你知道的公司在构建 rag 系统和规模化时,他们正在使用所有这些不同的步骤。而且有很多小的事情,你知道,你可以做,以确保用户,或者你的团队成员,或者你自己能够获得最佳的检索结果。所以,让我们说我想再次了解动物的睡眠模式。数据集中有很多睡眠事实。我们正在处理的数据集。我们将要做的是,我们将调用这个 process query 函数,用我们的查询。查询不是这里的查询。然后,在这里调用 main。而这将使我们能够测试我们的 rag 管道。所以让我们开始吧,清除我们的终端。我们将把它稍微向上移一点。我们将复制那个 all together 文件。然后 go TSX 空间文件名 回车。我们将看到我们的 rag 系统在工作。所以你可以看到我们得到了优化的查询。所以它把这个小句子优化成了动物睡眠模式。你注意到我们实际上得到了一个空值,因为我们没有指定一个特定的动物。我们将在下一个例子中尝试另一个查询。所以我们实际上没有按动物过滤。这有点像我们在例子中过滤的元数据。这里的元数据值是 name 列的值。在这种情况下,它只是空值。然后我们对其进行了基本检索。你可以看到它检索了九个文档。也许实际上在这里获取所有这些文档会很有帮助。我们将在下一个例子中尝试。然后将这九个文档与优化的查询一起输入到 coheres ranker 中。它将这些排序为三个最相关的imerick。事实证明,我们实际上得到了非常好的结果。因为如果你实际上检查一下事实是什么,每一个都与动物睡眠有关。所以而不是得到一堆不相关的信息。我们基本上能够从我们这里有 100 个事实的数据库中,将它们缩小到三个与你的查询高度相关的结果。这正是你在一个高性能、可靠的检索系统中想要的。所以这是一个很好的结果。让我们注释掉它,然后让我们尝试一个不同的查询。让我们使用一个我们没有处理过的动物。还有什么我们在这里有的?我们可以做海獭。让我们试试海獭。有什么事实?让我们试试海獭能用工具吗?有一个事实。让我们试试海獭能用工具吗?海獭,海獭,海獭在海里。海獭能用工具吗?问号。我们可以做的是,我们可以按 Ctrl C 退出。我们再运行一次。这次我们应该能提取到一个实体。看起来我们没有。这有点有趣。这可能就是我们想要检查的东西。所以我们去 extract name。你知道,我们又一次遇到了这个问题,在那个特定的课程中。所以我想我们需要做的就是也许添加一个系统提示。改进那个提示,这样你知道,总是有事情可以做的。这就是为什么你测试你的 rag 系统。并且你知道,记录下来。这样你就可以看到薄弱点在哪里,然后你可以努力改进它。为了保持相对简单,我们不会担心它。但我认为你可以看到,当我处理它时,那可能是我们 rag 系统的一个薄弱环节,我们可能想去改进它。但看起来我们得到了很多检索到的文档。让我们把它稍微向上移一点。所以这是数据库返回的内容。你可以看到我们的。好的,所以这是一个有趣的结果。你可以在这里看到,它实际上把一些章鱼的事实排得相当高。所以这证明了为什么重排序器如此重要。因为你可以看到这个章鱼事实的相似度分数甚至比海獭的还要高。这告诉我,无论我们从 doc ERS Ed tools 查询中得到什么嵌入,看起来你知道,也许因为章鱼非常聪明,能够解决谜题,也许那个嵌入与工具有点关系。所以它混入其中了。所以如果我们不使用文档重排序器,你知道,如果我们把它限制在三个事实,其中一个将是章鱼,这完全与我们的用户查询无关。但是当我们实际上将其输入到 rear aner 中,输入到文档重排序器中时,你可以看到这些被重新排序了。现在我们只有海獭。所以这两个可能不一定与工具有关。就像这个一样,但至少与正确的动物有关。所以通过添加这个额外的检索步骤,使用那个高级的重排序技术,我们实际上得到了更好的结果。所以我很高兴那个例子按照它的方式进行了,因为你可以真正看到将这些步骤链接在一起如何能够将你的基本检索,通过应用所有这些高级技术,你实际上可以获得好得多的结果。这比我们仅从基本检索中获得的要好得多。所以这就是如何将所有这些高级技术结合起来构建一个更高级的检索系统。我们将要做最后一件事,那就是我们将把这些例子输入到 chat completions prompt 中,或者 chat completions 调用到 open ai 的 API,实际上让 llm 根据我们的来源给出答案,并谈谈 rag prompting 以及如何有效地使用 prompts 进行 rag。最后但同样重要的是,这可以说是你想要达到的最终目标,这样你就可以让 llm 给你一个答案。我们将提示 open AI。我们将提示 GPT 40 mini 根据我们从 rag 系统中获得的结果给出一个答案。所以让我们直接进入代码。我们将再次进入文件 9。这是最后一课。所以吸收你刚刚构建的 rag 系统。这很酷。你学到了很多。所以非常令人兴奋。我们显然在这个简单的例子中,但一切都会从这里开始。我们将开始处理复杂的数据集等等。但让我们先让它回答一些问题。所以我们将立即开始编写一个函数,我们将在每次运行这个文件时调用它。而我们想做的是,我们想来一个查询。让我们用那个睡眠的。我想了解动物的睡眠模式。我们将要做的是,而不是做每一个单独的步骤。现在我们可以做的是,我们可以直接导入我们的 process query 函数,并将其直接传入这里。而当我们运行这个函数时会发生什么?让我们实际注释掉这个。我上个视频忘了做。现在这将要做的是,它将运行我们刚刚在一个函数中 put together 的所有四个步骤。所以它将大大简化我们将要做的事情。我们不必做所有那些函数调用。我们现在只需要做一个,这很好。并且让我们实际将其保存为 relevant docs 而不是 results,以便更清楚地了解发生了什么。然后我们可以做的是,我们可以创建一个 context 变量。我们将要做的是,我们将实际映射这些 docs。我们将要做的是,我们将使用这个 join 助手。这个 join 方法在我们创建的数组上,它将填充所有这些事实。记住,每一个事实都存储在文档的 content 属性上。我们将用两个换行符连接它们,以便稍微分开它们。而我们将要做的是,我们将尝试两件事。我们将从一个非常基本的 prompt 开始。我也会把这两个 prompt 都放在下面。所以我们将有 basic prompt 和 advanced prompt。我将对此做一个快速的笔记,以确保我能拿到这些资源。所以首先,我们将看看 basic prompt。所以我只是复制粘贴它。让我们来看看。所以这基本上是你所能得到的尽可能简单的 prompt。你可以说你是一个乐于助人的助手。你回答关于动物事实的问题,基于动物事实。使用以下上下文来回答问题,然后我们包含我们刚刚用这个 context 变量构建的上下文。所以我们可以做的是,我们只是使用一个,一个新的变量叫做 prompt,以便更容易处理。我们将使用那个 basic prompt。我们将从 open AI API 获取一个 completion。所以我们将使用那个 chat completions API。很棒。我们将使用 GPD 40 mini。我们将有一个 messages 参数。所以我们需要一个系统角色,带有我们的系统 prompt。然后我们需要第二个消息,它将是用户角色。而它只会包含我们原始查询的内容。在这种情况下,它是这个句子。然后我们将流式传输答案,这将非常有趣。要流式传输,我们需要做的是,我们实际上只需要迭代从我们这里流回来的块。所以它将被保存在 completion response 上。然后我们将直接将其写入我们的终端。我们将保持事情非常简单。看看它是如何工作的。所以让我们实际测试一下,然后再深入更高级的提示,只是为了确保它有效。所以我需要实际需要按 contrl C 退出那个函数。清理一下。让我们在运行它的时候把它弄大一点。所以让我们复制那个文件路径。我们将使用我们最好的朋友 TSX 来运行这个文件。你可以看到我们得到了优化的查询,动物睡眠模式。很棒。所以现在后台发生的是,它正在运行那个 process query 函数,它再次,它只需要一点时间。你可以看到我们正在获得一个流式传输的答案。非常酷。所以让我们来看看发生了什么。所以看起来我们得到了,优化的查询。再次,它不会提取名称,因为它只是动物,而不是特定的动物。我们在上一个课程中看到了这一点。我们得到了所有这些检索到的文档,基于余弦相似度分数。所以记住,我们将这个优化查询的嵌入与我们数据库中的记录的嵌入进行比较。在这种情况下,我们更喜欢那些相似度搜索。然后一旦我们有了它们,我们就将这些检索到的文档输入到 coher API 的文档重排序器中,然后它检索出这些结果。所以显然我们之前运行过这个例子,但你可以看到它又在工作了。但这次发生的是,它被应用于 llm 的响应,这有点酷,对吧?你可以在答案本身中看到,它实际上引用了每一个事实。考拉,长颈鹿和海獭的事实,这三者都被引用了。所以这实际上相当,你知道,看到它发生是相当酷的。但是我们要做的其中一件事是,我们想向你展示如何进行一些轻微的提示调整,以获得更好的答案。而且这可能不会像一个简单的数据集,一个简单的检索那样重要。你知道,这些事实真的很短。我们只有那么多。但是学习如何为 rag 提示可以极大地提高你的结果,当你处理更强大的系统时。所以我想向你展示一个我非常喜欢的工具。我们将引入一个新的 API,那就是我们将使用 anthropic API。这是 CLA 的制造商,非常流行的 CLA。显然,使用 cursor,我们正在使用 CLA 3.5 sonnet。截至我录制此视频时,Cloud 3.5 Sonnet 是新更新的版本,大约在十一月初。这是我目前最喜欢的模型。你可以做的是,anthropic,如果你真的创建一个账户,然后去仪表板,你会看到我们有一个生成 prompt 按钮。所以如果你点击它,你就可以把你的 basic prompt 粘贴进去,然后点击生成。这实际上将要做的是,它将使用 anthropics 提示模型生成一个更好的 prompt。所以你可以看到你可以做什么。你会看到我们得到了一些值。所以你只需要稍微编辑一下,但我们得到了一个更好、更健壮的 prompt。我有一个我们要使用的。我也会把它放在下面,这样你就可以复制粘贴。但我现在就做。然后我们来谈谈。所以基本上我在这里的工作流程是,我把 basic prompt 粘贴到那个 anthropic 工具中。我得到了一个结果,然后我自己迭代了一下,稍微清理了一下,去掉了不需要的东西。你可以看到现在我们有一个更健壮的 prompt。我们将要做的是,我们将实际设置 prompt 来使用 advanced prompt。然后我们可以再次运行它。你会注意到我们得到了一个略有不同的答案,因为我们的 prompt 略有调整。所以让我们让它运行。然后 boom,答案来了。所以它更简洁。再次,你不会在这个特定的例子中注意到巨大的差异,因为这是一个相当简单的数据集。但是如果你在做,比如,也许你有一个公司的知识库,你想构建一个工具,你的团队成员可以询问公司的知识库。那么你将需要一个像这样的高级 prompt。所以我们可以尝试另一个查询。请原谅我。所以我们可以做的一件事是,比如,你知道,让我们做,让我们做,你知道,长颈鹿吃什么?长颈鹿吃什么?让我们试试那个。所以我们可以再次运行它。这次我们应该能提取到一个名字,希望如此。所以让我们看看我们的 rag 系统做得怎么样。它优化了查询。老实说,结果不太好。这可能就是我们想要标记的。确保我们再次更新。我们的实体提取器又出问题了。这可能就是我们想去解决的。因为这应该给我们长颈鹿。而不知何故它没有。我们已经讲过几次了。所以我不会,我不会深入研究。但我们得到了一些检索到的结果。然后你会注意到我们得到了最终的重排序。我怀疑的是,如果我们去 mock data。让我们快速搜索一下长颈鹿。看起来我们拥有的五个长颈鹿事实中,没有一个与食物有关。但是很酷的是,你实际上可以看到模型的回应。它实际上,你知道,能够注入那些事实,并且能够识别,嘿,这些事实实际上与你的查询无关。它在这里的第一句话就解决了这个问题。然后给出一点答案。这实际上是 rag 系统在工作。你知道,我们谈论过,在之前的介绍部分。我们要减少幻觉。所以如果你的 rag 系统没有产生文档,没有产生回答问题所需的内容文本,那么它应该像这样回答。这实际上是一个很好的答案。所以我们回到这里,看看像北极熊。我们可以说,北极熊能游多久?让我们也许把我们的查询改成那个。你知道,让我们试试。北极熊能游多远?让我们运行它,看看我们是否能得到一个好的事实。我们应该得到。你可以看到我们最终的重排序结果实际上是北极熊游泳的事实。你可以看到相关的分数非常非常高。再次,这是你能得到的最高分数,1。所以这几乎是你能得到的。你会注意到,我们的模型用那个高级 prompt 给出了我们想要的答案。我们可以做的一件事是,也许让我们进行基准测试。把高级 prompt 切换回 basic prompt。让我们再运行一次,只是为了好玩,看看用 basic prompt 是否会得到更差的答案。我猜它可能会很好。再次,我们正在处理的数据的简单性不会让这里的提示差异显得过于强大。但这是一个需要了解的好事。因为你确实想写更多的 prompt。如果你使用 anthropic prompt enhancer,这是一个很好的技巧,可以写出更好的检索 prompt。显然,你会想根据你的用例来调整它们。但是,你知道,你的 prompt 中的指令越清晰,你的模型就越能更好地处理基于你提供给它的上下文回答问题。所以这是第二部分。这是创建完整的端到端 rag 系统。你显然学会了如何从基本检索中极大地改进事情。你甚至可以看到,你知道,正如我们在过去一两个课程中一直在谈论的,这里有一些我们可以做得更好的地方。我会在你自己的时间让你去做,因为我们想继续前进。但这就是端到端 rag 系统。我希望你觉得你学到了很多。所有这些概念都是非常真实的。你知道,当你提高数据集的复杂性,并从动物事实转向更长的数据源,更多样化的数据源。你知道,对你来说很重要的事情,对你实际工作很重要的事情,在你的日常工作中,在你的日常生活中。你知道,也许你有你的个人日记。也许你有很多笔记,你想为它们创建一个 rag 系统。现在你知道如何做。所以我们将在这里结束这个部分。在第三部分,我们将把我们在第一部分和第二部分学到的所有东西都应用到构建一个真实世界的项目。所以让我们谈谈我们要构建什么。为什么我们要构建它,以及在我们开始编写代码之前需要做的一些事情。所以从一开始,我们将在这个部分编写大量的代码。所以这将是相当技术性的。如果你只想快速浏览一下我正在做什么,然后也许将这些技术应用到你自己的项目中,那也很棒。我确实强烈建议你和我一起编写代码,因为我们将构建这个很酷的小工具。所以你的 UI 在这个过程结束时看起来会和你我的有点不同,因为在我们构建这个东西的过程中,我们将使用一些东西,比如 cursor 和 v0 来教授一些 AI 工作流程,当我们做一些事情,比如在前端构建实际的应用 UI 时。但我们将做两件事。一个是我们将构建一个完整的检索管道。与我们在第二部分所做的非常相似,完整的生产级端到端 rag 管道。我们想做的事情,比如,你知道,基本检索,查询优化,文档重排序等。我们将把它应用于一个文档上传器。我们在这里输入的任何文本都将被处理到我们的 rag 管道中,上传到我们的向量数据库。而我们将在我们的 AI 聊天中,在右边,我们可以输入一条消息,问任何关于我们上传的文档的问题。所以无论你上传一个文档,还是你知道,你输入了一千个不同的东西。这里的想法是,这是一个关于知识库的 rag 管道。所以你看到这个。这是一个非常典型的流程,非常典型的工具。你知道,你可以想象,你知道,如果你有很多公司文档,或者其他什么东西。你知道,也许你是一个金融分析师,或者其他什么,你有很多文档。你知道,你可以把它们都放在这里,上传它们,然后问任何关于你上传的文档的问题。检索管道将根据你的查询,获取最相关的信息。然后它将使用一个 AI 模型。我们将使用 open AI 的一个 llm 来回答这些问题。然后我们将看到我们的检索结果。所以我们实际上将能够看到 llm 在其上下文中引用的文档,以便给你一个关于你上传的任何文档的准确答案。所以这是一个非常常见的工具。你会看到这个模式到处都是。你知道,如果我们以 perplexity 为例。你知道,我们谈论过它。我认为在第一部分我们谈论过 perplexity。他们所做的事情基本上是他们的文档上传器就是互联网。然后想法是,你进入一个 UI,问一个问题,执行整个检索过程,在上下文的末尾添加,然后回答它。所以如果你学会了如何做这件事,你就会拥有构建各种 rag 应用和工具的基础。所以这个项目最终相当不错,因为你可以看到我们所做的所有 rag 管道的东西,与第二部分非常相似,但也看到了它如何被实现到一个实际的用户界面,一个实际的应用程序,你可以使用。所以让我们谈谈你需要开始做些什么。所以显然你需要在一个代码库中工作。我们一直在其他部分工作的那个。所以你会注意到,再次,我们会把 GitHub 链接放在下面,如果你还没有跟上。但是你想要做的是,我实际上要停止我的开发服务器。你要确保你运行 get pole origin main,或者从 G 获取代码的 zip 文件。因为我们更新了这一部分,自从你学习了第二部分以来。所以你想要确保你获取了更新的代码。你会注意到,如果你这样做了,你将会有 section three,并且你将有一个 reference app 在里面。所以 reference code 将包含你实际需要确保你的代码正确的所有东西。所以项目,我实际上正在运行的那个。完全由这个 reference code 表示。所以如果你想。
举个最终产品的例子,那些文件中的代码将会在那里,我们将在同一个文件夹里工作,旁边有一个项目,那是你的代码。所以,这将是我们在这个部分编写的项目代码。如果你学习了第二部分,这将会与它的格式非常相似。我们有你的代码,然后我们有参考代码。所以,你将需要确保你获取最新的代码,然后你将再次需要三样东西。这些和我们为第一部分和第二部分使用的三样东西是一样的。所以,你需要一个 OpenAI 密钥。所以,你需要去 platform.openai.com 获取你的 API 密钥,因为你需要它。然后,如果你想使用文档重排序,我强烈推荐你使用,这是非常好的生产级实践,而且是学习的好东西。但你也需要一个 Cohere API 密钥,这样你就可以使用他们的文档重排序 API。所以,这很重要。然后,当然,最后但并非最不重要的是,你需要去 Superbase。我们将在几秒钟内在这里进行设置。你需要一个项目。你可以使用免费的 Superbase 账户。如果你用完了免费项目,你可能需要删除一个旧项目,这没什么大不了的。我们在学习,所以你可能可以轻松地丢弃旧的东西。但是,让我们来看一个实际使用应用程序的快速小例子,这样我们就可以给你一个我们实际要做什么的预览。
所以,像这样的 RAG 系统可以非常有用的一个方面是,你知道,博客文章、论文之类的内容,可能没有直接包含在训练数据中,或者你只是想问一些关于它的非常具体的问题。你知道,如果一个模型是在六个月前训练的,它可能不会有新的数据。所以,例如,这是一篇 Paul Graham 的文章,写于 2024 年 10 月。这篇文章没有包含在 GPT-4o 的训练数据中。所以,如果你问关于这篇文章的问题,它将无法得到答案,因为它没有任何可以参考的东西。所以,如果我运行我的本地服务器,并在参考应用程序上运行 `npm run dev`,以确保一切正常运行,并向你展示正在发生的事情。所以我可以做的一件事是,我实际上已经嵌入了这篇文章。让我们来处理之前的那个。所以,这是一篇写于 2024 年 9 月的文章。我可以直接复制粘贴整个内容。我甚至会包含那个。我们将实际参考这个。这可能是一个很酷的例子。所以,我将在这里粘贴它。你可以看到我们有所有的文本。我将点击上传文档。这将把所有这些文本作为一个新的数据条目上传到我的向量数据库的文档表中。你会看到,这是文章的数据。所以,我们可以看看这篇文章。让我们试着找一些可以提问的问题。比如,在不确定面前我们得到了什么?让我们来问这个问题。实际上,因为我们将复制完全相同的查询,这可能就像这个嵌入基本上是相同的。所以,让我们来做一些事情,比如,我应该等到大学毕业后再决定做什么工作?让我们看看它怎么说。好的,我们将发送出去。我们将在这里得到我们的查询,用绿色显示。你可以看到,如果我们向下滚动,让我们暂时隐藏我们的来源。它说,不,你不应该等到大学毕业后再决定做什么。你可以看到它从这三个来源提取信息。所以,你可以看到这些是我们检索过程结束时返回的前三个排名最高的文档。所以,你可以看到文章的这个片段被分割成块。我们将在这里使用一种简单的分块技术。有很多更高级的方法,我将在最后作为奖励提供给你,并展示你可以从哪里开始。将来,如果需求足够,我们可能会制作一些关于特定文档加载器的视频。但这涵盖了要点。如果你掌握了这些,那么转向其他那些就很容易了。但是,话虽如此,让我们开始设置代码,这样我们就可以从头开始构建这个东西。让我们开始初始化我们的代码。这会很简单。我将快速导航回父目录,这样我就可以和你一起导航,就像你跟着我一样。好的,你需要做的是,你需要回到代码仓库,我们将要做的是,我目前可以看到我的终端已经打开。我们将导航到我们要构建项目的仓库位置。我们将进入 sections 目录,然后进入 section three。在这里,我们将运行我们的初始设置脚本。它将位于 reference app 项目旁边。我们将在这里创建一个新的应用程序。我们将使用 Shad CN CLI,这是一个非常棒的项目设置方式。这是我们在 takeoff 中设置 next.js 全栈应用程序的方式。所以,你可能已经熟悉了。你可能需要运行某种安装命令。你的终端可能会告诉你有一个命令需要运行,然后你必须再次运行它。但是,我们将要做的是,我们将运行 `npx shadcn-ui@latest init`。这将在这里初始化一个新项目。看起来,我是否拼错了什么?是的,我拼错了。是 Shad CN。我们再运行一次。它将识别出我们目前没有 next.js。所以,你可以看到 Y 是大写的。我将输入,我将把它命名为,你知道吗,我们把它命名为 my rag app,怎么样?my rag app。所以,这将在这里设置一个新的 next.js 项目。正如你所看到的,现在我们有 reference app,这是完成的代码,然后是我们现在将要工作的 my rag app 文件夹。所以,你必须在这里运行一点设置步骤。我们将使用 default 作为我们的风格。我们将使用 neutral 颜色作为基础。点击是。这将安装我们基本的依赖项,并让我们的应用程序运行起来。一旦项目初始化完成,我们将进入 rag app 文件夹。如果你运行 `npm run dev`,你应该可以看到你的应用程序在 localhost 3000 上运行。所以,如果我刷新一下,我们现在有了我们的入门应用程序。这是一个使用 Shad CN CLI初始化的 starter next.js 应用程序,这样我们就可以开箱即用地获得许多额外的功能。这是一个非常好的设置步骤,如果你不熟悉的话。但是,接下来我们要做的就是,我们将添加一些 Shad CN 组件。所以,如果我拉入 Shad CN 文档,我会确保链接到这些文档。如果你正在构建很多自己的全栈项目,你可能会想熟悉这些。所以,我们实际上正在遵循这个 next.js 指南。你可以看到这非常熟悉。我们要利用这个组件库。所以,例如,如果我点击这个按钮组件,一个很酷的事情是,我们可以直接安装这个按钮组件,然后我们就可以开箱即用地获得一个非常漂亮的按钮。我们将对三个不同的组件做同样的事情。我们将回到这里。你应该有一个终端正在运行你的 localhost 应用程序,这很好。我将创建一个分割终端。你可能只想创建一个新的。我喜欢分割版本,这样我的应用程序可以在这里运行,脚本可以在这里运行。我们将要做的是,我们将运行 `npx shadcn-ui@latest add button input textarea`。所以,我们想要这三个组件。如果我在这里输入,你会看到我们在文件浏览器中有一个新文件夹。我们有按钮组件,输入组件和文本区域组件。所以,我们不必自己构建这些组件。我们开箱即用地获得了它们。Shad CN 有所有这些不同的组件供你使用。强烈推荐。它是目前世界上最受欢迎的库之一。它是热门的 UI 库,它会让你的生活变得容易得多。所以,习惯使用它。一旦你安装了这些组件,我们将安装一些我们将要在项目中使用到的软件库。这些将非常熟悉。我将复制粘贴,因为它有很多。我将把脚本放在下面,你也可以复制粘贴。我们将运行 `npm install` 来安装这些库。这将是像 OpenAI SDK。我们想使用 Cohere 的 SDK。我们将使用 Vercel AI SDK 来实现聊天功能。我们将使用 tiktoken 来计数。在第二部分,我们按单词计数分割了我们的文档。这次我们将按 token 计数。所以,我们将提高一个级别。所以,我们需要所有这些不同的东西。然后,你还需要为你的开发依赖项运行以下脚本。这些只是我们需要的一些开发工具方面的东西,以便获得像我们的迁移脚本,用 Drizzle 编写。
好的,这是我们的一些代码设置。我们确实需要设置我们的环境变量。所以,如果我快速看一下 reference app,你会看到这个 `.env.example` 文件。如果你一直在关注课程的其他部分,它会非常熟悉。所以,你想要做的是,你实际上想回到 my rag app。你想创建一个名为 `.env.local` 的新文件。然后,我们将获取那个示例文件。我们将把这些值复制粘贴到 `.env.local` 文件中。我将在屏幕外进行,这样我就不会暴露我的 API 密钥。但是,你需要输入你的 OpenAI 密钥。你需要输入你的 Cohere API 密钥。然后,我们将开始我们的数据库设置步骤。所以,确保你到达了这个点。获取 OpenAI 密钥,获取 Cohere API 密钥。确保你的应用程序在 localhost 上运行。确保你安装了那三个组件,这样你就可以使用它们了。然后,让我们继续下一课。
既然我们已经有了应用程序的基础代码,这要归功于 Shad CN 的初始化步骤,我们可以设置我们的向量数据库。这与我们在第二部分所做的非常相似,实际上是相同的。所以,在这个阶段,按照我们在这里教授的方式设置向量数据库,使用 Postgres 和 PG Vector,这应该感觉很标准。你知道,任何时候你设置一个检索项目,你都需要做类似的事情。即使你使用不同的向量数据库,比如 Pinecone 或其他什么,同样的步骤也会适用。你需要去设置数据库,你需要弄清楚你想要用于检索的表的模式,然后你需要去设置它。我们将按照我们在 Takeoff 的方式来做,使用 Postgres PG Vector。所以,让我们直接开始。你会看到,我在这里是 superb.com,并且在创建新项目屏幕上。所以,我们将在这里做的就是,我们将把它命名为,我将把它命名为 takeoff rag live,因为我正在直播。然后,我们将生成一个密码。当然,你可以选择自己的。我将使用生成的密码。提醒你,你需要保密。所以,就像对待密码一样对待这个值。它说数据库密码,所以确保你保持私密。我显然只向你展示这些,以便我能教东西。所以,我将复制粘贴这个值到 `.env.local` 文件中。我知道现在它不在正确的位置,因为我们需要更改数据库 URL。如果你看了第二部分,你可能已经熟悉了。但是,我们将启动这个项目。这通常需要一两分钟来初始化数据库。所以我将剪辑到它完成的时候。项目设置好了。让我们获取数据库 URL 来连接到它。你将去侧边栏,进入项目设置,然后在项目设置中,你想进入数据库页面。在这里,你将找到你的 Postgres 数据库的连接字符串。所以,复制那个,然后回到你的代码。你将把它粘贴到你的数据库 URL 的值中。一旦你有了那个值,现在我们将剪辑我们保存的值,并将替换这个括号里的密码值。好了,我们现在有了应用程序所需的连接字符串。在关闭这个文件之前,我想提醒你几件事,因为环境变量非常重要。你需要把它放在 `.env.local` 中。你需要更改任何,我这里只有一个测试值。我将在之后做这个。我将在这里输入我的 OpenAI 密钥。我将在这里输入我的 Cohere API 密钥。然后,我将在这里输入我的值,这没关系。不要与他人分享这些值。把它们当作密码。因为如果人们获得了这些值,他们就可以访问你的账户积分,进入你的数据库等等。所以,确保这些东西是安全的。我将在这里输入我的值,然后关闭这个文件。
在输入我的密钥后关闭了文件。现在我们需要获取我们的 Drizzle 文件。我们在第二部分做了所有这些步骤,但我们将为这个项目再次做。这些重复很有帮助。你需要习惯做这些事情。你可以使用你自己的技术栈。如果你想使用不同的向量数据库,那完全没问题。有很多选择。这是我使用的方法。我觉得它非常可靠,而且它是一种相当方便的技术。使用我们在 Takeoff 中使用的技术栈的一个优点是,AI 模型对它有很好的上下文。所以,如果你使用像 Cursor、Vercel Chat GPT Cloud 这样的工具,它们将能够给你很好的答案,因为库在训练数据中。所以,这就是我们使用我们所使用的技术栈的一个重要原因,如果你好奇的话。但是,我们将要做的是,再次确保你在 my rag app 中进行编码,而不是在 reference app 中。reference app 是完成的代码。再次,在任何时候,如果你想参考我们要处理的任何文件,你可以进入 reference app。所以,例如,我们要处理的是我们的 `drizzle.config`。我将要做的是,因为这些都是相当样板化的设置步骤,我将利用这一点,我们将进行一些复制粘贴。所以,在我的 my rag app 中,我们要做的第一件事是创建一个顶级的 `drizzle.config.ts` 文件。接下来,我们需要在 my rag app 文件夹中创建一个新文件夹。它将被称为 `db`。在 `db` 中,我们需要创建一个名为 `db.ts` 的新文件。实际上,我认为 reference app 有时我做 `db.ts`,有时我做 `index`。所以,我们将保持与 reference app 一致。我们将从 reference app 获取代码。这只是非常样板化的东西。如果你是第一次设置项目,你可以复制粘贴。然后,我们将要做的是,我们将在 `db` 文件夹中创建一个名为 `schema` 的新文件夹。我们在这里创建一个名为 `documents-schema.ts` 的新文件。如果你看,我实际上不知何故称之为 `documents_data`。我们将坚持 `document_schema`,这是一个更好的命名约定。但是,我们将复制粘贴这个表,并将其粘贴到我们的文件中。所以,在这个阶段,你应该已经添加了 `drizzle.config` 文件,它位于项目的顶层。然后在 `db` 文件夹中,我们有 `index.ts` 文件。你实际上需要修改那个命名。我可能会在 repo 中更新它,使其一致。但是,如果你遇到那个小错误,请确保你更改了文件名以进行导入,否则你会遇到一些错误。然后,我们将拥有我们模式文件中定义的文档表。所以,我们再次快速地过一遍,因为我们在第二部分做了完全相同的事情,并且逐行解释了。但是,我们将再次快速地过一遍。我将稍微谈一下这个表,因为我认为这很重要。所以,我们有一个 ID 列,这些都是列类型。我们将有一个 ID 列。我们将有我们的内容列。所以,这是我们文本实际所在的地方。在我们的应用程序中,我们有那个大的文本区域,我们上传我们的文本,对吧?这就是文本。我们需要使用向量列。再次,这是我们从 PG Vector 获得的。我们在第二部分已经涵盖了所有这些。所以,我们将快速过一遍。我们将再次使用 256 维,因为我们想保持快速、灵活和便宜。如果你正在做一个生产数据库,我建议你增加维度。我们有 OpenAI 嵌入文档的链接。所以,如果你想了解一个好的维度计数可能是什么,如果你想获得更好的检索结果,并且你对运行数据库的服务器端要求感到满意,那就去看看吧。但是,我们将保持简单。然后,当然,我们需要我们的嵌入索引。我实际上将把它从 `data` 改为 `documents`。所以,这将是 `documents`。我们将要做的是,我们需要从这个 Drizzle 文件生成 SQL 迁移文件。所以,我们基本上在这里用 TypeScript 编写所有的数据库模式。它将使我们的数据库在整个应用程序中保持类型安全,这对于我们构建全栈应用程序来说将非常有帮助。当你做一些后端工具时,比如我们在第二部分做的,它并没有那么明显为什么这很好。但当我们处理前端时,它会变得更加明显,我们需要到处插入和选择文档类型。所以,我们将要做的是,我们需要对我们的 `package.json` 文件进行一点调整。我们需要创建两个脚本。我们需要做 `db:generate`,它将运行 `drizzle-kit generate`。实际上,我们可以这样做,我们将再次参考我们的 reference app,因为我们想复制这两个脚本。生成脚本和迁移脚本。这将在你做的所有应用程序中保持一致。所以,这只是一个非常样板化的步骤。一旦我们有了它,我们可以回到我们的终端。所以,我有一个终端正在运行 `npm run dev`。再次,你可以运行 `npm run dev`,它将运行这个 dev 脚本并启动你的应用程序。然后,你可以在 localhost 3000 上访问它。在右边,我有一个另一个终端,我们可以运行一些脚本。我将运行 `npm run db:generate`。它将运行这个脚本。所以,我们将输入。你会看到我们现在有一个新文件夹。你会看到,在我们的数据库文件夹中,我们现在有一个 migrations 文件夹,里面有一些东西。最主要的是我们的 SQL 迁移文件。再次,这是你需要针对你的 Superbase 项目运行的,以创建那个表。再次提醒,就像我们在第二部分做的那样,我们必须创建 `CREATE EXTENSION IF NOT EXISTS vector;`。这是我们将 PG Vector 扩展添加到我们的 Postgres 数据库的方法,这样我们就可以利用向量嵌入类型。这就是我们进行基本检索的余弦相似度搜索。所以,你将需要确保你将这一行添加到生成的 SQL 文件中。然后,如果你运行 `npm run db:migrate`,它将应用你的迁移到你的数据库。所以,如果我们回到我们的 Superbase 项目,进入我的表编辑器,你应该看到这个文档表。所以,在左侧边栏的表编辑器中,你会看到这个新的文档表。你可以看到它有我们在模式文件中定义的所有列类型。所以,如果你遇到这个步骤的错误,很可能是因为你在 `.env.local` 文件中输入了错误的数据库 URL。也许你没有替换密码。所以,如果你遇到错误,通常 99% 的时间就是这个原因。但是,话虽如此,我们现在拥有构建 RAG 管道所需的一切。我们在 Superbase 中设置了向量数据库。我们只是使用了标准的 Postgres 和 PG Vector 设置。这是我用于所有项目的方法,包括个人项目、工具和实验,以及生产级的东西。我有很多生产级应用程序,有真正的用户,它们使用相同的设置。这是一种非常标准的东西。我们已经涵盖了一些细节。所以,我们将继续,主要专注于构建。但是,你应该确保你到达了这个阶段。然后,我们将继续下一课,也就是实际构建 RAG 管道。
我们在这一部分所做的将与我们在第二部分所做的非常相似。所以,我们将实际构建我们的 RAG 管道。构建 RAG 管道的好处之一是,一旦你设置好一次,再次设置就非常快了,因为你可以复制粘贴以前使用过的代码,你可以调整一些东西来更好地适应你的用例。但是,好处是,每一个 RAG 管道在大多数项目上都会有相同的基本框架,你可以一遍又一遍地重用。所以,让我们进入我们的项目,看看我的意思。我们将大量参考我们在“创建 RAG 系统”中编写的代码。如果你没有跟上第二部分,那没关系。我们将在这一部分稍微快一点地讨论它。而且,因为我们有所有的参考代码,你仍然可以很好地跟上。所以,我们要做的就是,我们要进入 my rag app 中的这个 lib 文件夹。我们将创建一个名为 `rag` 的文件夹。所以,这就是我们将要构建系统的所有实用函数。我们将把它组织成三个部分。我们将有一个 `generate` 文件夹。所以,我们的一些生成函数,比如我们的嵌入生成和我们的上下文完成,最终的 API 调用到 OpenAI,它将为我们提供最终答案。我们需要两者。我们将需要一个 `retrieval` 文件夹。所以,这里将有一些更偏向检索的函数,比如我们的查询优化等等。最后,我们将有一个 `processing` 文件夹。所以,这里将有一些我们的分块、文本分割器。所以,这里我们将实际使用我们安装的 tiktoken 库来标记我们上传的文本,这样我们就可以将它们分割成 500 个 token 的块。所以,对于非常长的条目,它不会被嵌入。你不想有一个 15,000 个 token 的文本文件被嵌入。你想要把它分成几块。我们已经在第二部分涵盖了这些技术。我们将把它们串联起来,稍微讨论一下,这样我们就可以构建一个完整的端到端 RAG 管道,我们可以在下一课中使用 UI。所以,让我们开始进行完成生成。所以,我们将要做的是,正如我所说的,我们将首先创建文件。所以,让我们创建一个 `generate-completion.ts` 文件。我们还需要,这不应该是 processing,所以我将把它拖到 generate。我们将有一个 `generate-embeddings.ts` 文件。所以,让我把它放大一点,这样你就可以看到完整的文件路径。所以,在你的 generate 文件夹中,你应该有一个 `generate-completion` 和一个 `generate-embeddings` 文件。所以,让我们开始处理嵌入。所以,如果我们去第二部分的 reference code,在这个 repository 中,你将有这个第一个文件,`generate-embeddings`。我们将复制这个函数。如果你想保留注释,以便在回顾代码时有一些注释,那也没关系。所以,我们将这样做,然后回到我们的新文件,我们将粘贴它。我们将在文件顶部用一个指令来标记它。这被称为 `use server` 指令。这只是让我们的应用程序知道我们想在服务器端运行这段代码,而不是客户端。客户端在浏览器上,服务器端在服务器上。你可以看到 Cursor 正在准确地预测我们需要什么。所以我将按 Tab 键。这只是导入了 OpenAI SDK,然后我们正在设置它。在后台发生的事情是,OpenAI 使用我们 `.env.local` 文件中的环境变量。好处是,它会自动拉取,假设你将那个值命名为 `OPENAI_API_KEY` 等于你的密钥,这正是我们设置项目的方式。所以,你将没问题。你实际上不需要在那里输入 API 密钥。你会看到,我们将使用与第二部分完全相同的函数。我们将继续使用小模型,再次,这只是为了保持便宜。我们的维度需要与我们在模式文件中设置的匹配。所以,我们参考模式文件。256 维,嵌入函数中 256 维,好了,我们都设置好了。我们将如何调用这个函数呢?我们将提供文本字符串数组,并将其传递给 API。OpenAI 使用他们的 `text-embedding-3-small` 模型来编码数据,以 256 个浮点维度来获取向量嵌入,然后我们返回这些值。所以,这很简单。`generate-completion` 文件将非常简单。所以,如果你进入 `-rag-prompting`,这实际上将与我们将要使用的非常相似。所以,我们将要做的是,我将复制那段代码,然后我将去这里。我们将复制这个 `async main` 函数。实际上,我们需要修改比嵌入函数更多的东西。所以,让我们这样做。首先,我们需要做的是,你可以看到 Cursor 正在准确地使用 Tab 补全。所以,我们需要 `use server` 指令。我们需要导入 OpenAI,并再次初始化客户端。我们将在每个文件中初始化客户端,以保持简单。我们要做的就是导出这个。导出 `async function`,我们将把它命名为 `generateCompletionWithContext`。它将接受两个参数。它将接受 `context`,这是一个字符串。它将接受 `input`,也就是用户的输入,作为一个字符串。然后,它将是所有的代码。你可以删除所有到这个 `completions` 代码。所以,你可以删除到那里。我们可以做的是,我们将更改一些东西。我们将保留模型 `gpt-4o-mini`。你可以使用任何模型。我们将使用 `mini`,因为它最便宜。这对于测试总是很好。我们将温度设置为零,这样我们的答案会更确定。我们将最大 token 设置为 1000,所以模型可以输出的最大 token 是 1000 个。我们将稍微更改系统提示。再次,如果你想构建一个更高级的工具,这是一个很好的扩展目标。让这个提示更复杂,针对特定用例进行定制。你可以尽情发挥。我们将保持简单。我们将说“根据提供的上下文回答”。然后,我们将换行,然后是 `context:`,然后在下一行,我们将放入上下文。所以,我们将有一个满足该标准的系统提示。然后,对于用户消息,我们将使用输入值。我们实际上不会流式传输它。我们将返回完成。你需要返回 `completion.choices[0].message.content`。然后,我们还需要删除 `stream: true` 来消除那个小错误。所以,这将使我们能够调用这个函数,并提供我们获得的任何上下文。再次,我们获得的上下文将是我们的检索管道的最终结果。再加上我们的用户查询,它将命中 OpenAI completions API,命中 GPT-4o mini,它将构建这些消息,然后我们将得到一个响应。我们将得到消息的文本。所以,这就是生成函数的作用。所以,这是第一步。第二步是,我们实际上需要做几个处理函数。所以,我们将做两件事。第一个函数是,我们需要一个 `splitText` 函数。所以,让我们回到我们的文件夹。我们将做 `split-text.ts`。我们还需要一个 `process-document` 文件。所以,又是两个文件,在 processing 文件夹中。所以,如果我们回到 `split-text` 文件,我们将在这里做的是,我们将用 `use server` 来标记它。我们希望它在服务器端运行。这是 tiktoken 工作方式的要求。我们将导入一个名为 `getEncoding` 的辅助函数,来自 tiktoken。我们将把它用在我们的文本分割器函数中。所以,让我们初始化一个名为 `splitText` 的函数,它接受一个名为 `text` 的字符串参数。在这个函数中,我们将做的是,而不是在第二部分中按单词计数分割文本,我们将按 token 计数分割。这很酷。所以,在这个例子中,我们将把所有块分割成 500 个 token 的块。人们有很多不同的方法来分割文档。所以,我们将在最后更详细地讨论。但是,我想在这里教你的是如何按 token 分割,因为这是生产级 RAG 系统中最流行的分割技术之一。所以,我们将创建一个名为 `chunks` 的字符串数组,并将其设置为空数组。接下来,我们将定义一个 `chunkSize`。我建议在 500 到 1000 之间。这可能是最常见的两个值。如果你设置了一个特定的限制,很多人会以不同的方式分割,比如基于语义、标题或换行符。但是,为了简单起见,我们将只做 500 个 token 的块。你需要做的是,你需要初始化这个 `encoding` 变量。这将定义我们将使用的编码器。在这种情况下,我们将使用 `cl100k_base`。这是一个你可以使用的分词器,与 OpenAI 模型相关。我们将要做的是,我们实际上需要用 `try-catch` 来包装我们的代码。因为我们将要做的是,我们将运行一些代码,然后在最后,我们将这样做 `finally`。在最后,我们需要运行 `encoding.free()`,它基本上结束了编码序列。在 `try` 块中,我们将创建一个名为 `tokens` 的变量。我们将获取 `encoding.encode(text)`。这将对我们给这个函数的任何文本进行编码。所以,基本上是分词。我们可以这样做,我们可以计数。我们将创建一个 for 循环。我有一个非常基本的 for 循环,它只是迭代每个 token。我们将调用一个名为 `chunkTokens` 的变量。我们将对 `tokens` 调用 `slice`。这基本上就是 `slice`。它将每 500 个 token 块分割编码值。我们将在这里做的就是,每 500 个 token,我们将向 `chunks` 数组推送一个新的块。所以,这只是在做这件事。它遍历我们的编码值,它计算,我这里有 500 个 token 吗?如果没有,继续。否则,我们将推送那个值。所以,我们将 `chunks.push()`。我们不需要这个返回值。你可以看到,我得到一个错误。啊,我称之为 `chunk`,这应该是 `chunks`。所以,我们将每 500 个 token 循环一次,我们将推送一个新的块。所以,这将把文本变成 500 个 token 的块,而不是一个大块。这对于学习 RAG 系统非常重要。因为,比如说,你有一个 100,000 个 token 的 PDF。你需要把它分成很多块。所以,分块、分割文本,我们在第二部分已经讲过了。我们只是把单词换成了 token。我知道这里有很多技术性的东西。但希望你能理解。如果你在 Cursor 中工作,可以问几个问题。但是,我们将继续。对于 `processDocument` 函数,这里的想法是,我们想构建一个函数,它将接受我们输入到 UI 中的文本,用我们刚刚编写的文本分割器函数来分割它,嵌入所有这些不同的块。
然后,它将把这个上传到我们的数据库,好的,所以我们将继续,我们将再次使用服务器,我们希望它在服务器端运行,我们将在这里做一个异步函数,处理文档,再次,这将接受一个名为文本的单个参数,这里是标准的,嗯,我们需要做三个步骤,好的,所以让我们从分块开始,好的,所以我们需要做的第一件事是创建一个名为块的变量,我们将等待,我们将在这里获取那个split text函数,好的,所以我们将导入我们刚刚在这里编写的这个函数,我们将最终使用它,好的,我们将传入我们的文本,所以这将做的是,它会将这个字符串分割成每块 500 个 token,当然除了最后一个,因为你知道,你不会在最后有正好 500 个,嗯,你接下来需要做的是,你实际上需要嵌入这些块,嗯,为了得到这些嵌入,我们可以创建一个嵌入变量,现在我们可以终于使用我们的嵌入生成嵌入函数了,我们可以直接传入块,我们从split text那里得到的块,所以你可以看到这些东西是如何开始一起工作的,这有点不错,嗯,看起来,嗯,我可能在这里犯了一个类型错误,让我快速找出这个错误是什么,我马上回来,好的,我意外犯的错误是,我需要保留那个return chunks,我不小心删掉了它,所以请确保你回到你的split text文件,嗯,并且在那个try块的底部,你想返回chunks,然后如果你回到process documents,你会看到那里的波浪线消失了,嗯,如果你正在使用,嗯,eslm,嗯,我认为这个设置自动为每个人都这样做了,但是,嗯,是的,嗯,所以让我们实际上继续进行第三步,也就是将它插入到我们的数据库中,好的,所以我们将使用db.insert,我们想插入到我们构建的那个documents表,我们将使用的值是,我们将映射这些块,所以我们实际上想获取块和它们的索引,在这种情况下,这就是I代表索引,然后对于每个记录,嗯,将发生的是,content将等于那个块的值,然后embedding,embedding将是那个块的特定索引处的嵌入,所以这在这里相当直接,好的,我们将获取这些的导入,好的,我们将在这里导入document table,所以再次,DB将来自,我们之前做的你的DB设置,嗯,在这一点上,你有一个process document函数,所以这是一个有点长的课程,因为我们在这里做很多不同的事情,我们最后要做的是,我们将在这里构建我们需要用于检索文件夹的辅助函数,我们将需要四个,让我们看看,三个或四个,我们将做四个,我们将在这里做四个文件,我们将需要optimize query.ts,再次,这些将与我们在第二部分所做的非常相似,因为正如我所提到的,我们需要检索documents.ts,rerank documents.ts,以及run rag pipeline.ts,所以正如我在本课开头提到的,我们将使用大量我们在检索中做过的相同的东西,一旦我们设置了我们的rag系统,一旦你可以重用很多那些,所以这就是我们将要做的,好的,所以让我们实际上从optimize query开始,我们将跳回第二部分,回到参考代码,你可以看到文件四,我们实际上将基本上复制同样的东西,所以我会复制,我们将复制注释,给你们一些注释,我们将把它粘贴在这里,再次,你将,嗯,我们实际上将,嗯,导入open app from AI,我们实际上也将在这一点上使用use server,我们希望它在这里服务器端运行,好的,再次,这将做我们的查询优化,我不会在这里详细介绍,因为我们在第二部分已经做过了,但是,这里的想法是,我们正在接受我们的输入,我们的用户输入,我们正在尝试将该输入优化为某种我们可以嵌入的查询,以获得更好的可检索结果,所以再次,我们涵盖了背后的原因和方法,所以我们将继续前进,再次,我们只想专注于实现,我将添加的一件事是我们没有在第二部分添加的,是以下内容,我将在这里放几个例子,因为正如我在第二部分提到的,我们看到的一个事情是,我们从模型获得的输出并不总是最好的,所以你在生产环境中可以做的一件事,好的,当你试图让rag系统真正好起来时,你可以做所谓的few shot prompting,这基本上就是我们给模型一些例子,在这种情况下,我们正在做六个例子,你可以看到一些非常基本的用户输入,然后是一个箭头,以及我们希望它们成为的结果,所以你可以看到我们如何处理“给我看看关于iPhone的最新消息”,我们只是将其缩小,我们使用像GPT 4L mini这样便宜的模型来优化查询,以获得更好的结果,我们可以实际上传递给嵌入模型,这使得检索步骤更加准确,如果你这样做的话,好的,这就是optimize query,让我们继续进行下一个函数,retrieve document函数,好的,所以我们将做retrieve documents,我们想在这里做什么,我们将做一些与我们在第二部分使用我们的检索函数所做的非常相似的事情,但实际上我们会稍微简化它,因为这次我们实际上不会做元数据过滤,只是为了去掉一个不适用于我们用例的步骤,如果你想把它作为一个延伸目标,那当然可以做到,但是我们将重用输入参数和min similarity参数,所以如果我们实际上跳回到,嗯,让我们看看,我们在哪里实际做到这一点,从第二部分检索数据函数,在参考代码文件夹中,这将是第三个文件,我们实际上将从这个const similarity thing here一直到return documents here,我们将回到我们的文件并粘贴到函数中,需要做一些调整,好的,所以我们将删除name,就像我说的,我们不做元数据过滤,我们需要调整我们的where子句,我们正在做一个条件where子句,我们可以实际上删除这个最后的similarity check之前的所有内容,然后当然,我们需要做,快速更改,将fact table更改为documents table,我们显然在使用不同的documents table,我们不再使用name值,所以我们可以删除那个,然后你的文件在那时应该看起来像这样,所以这是你的函数应该看起来的样子,从高层次来看,我们正在传入我们的用户输入,我们正在传入一些可选参数,这是一个文档限制,默认为10,以及一个可选的最小相似度分数,默认为3,再次,你可以根据你的用例,根据你喜欢你的系统设置的方式来调整这些值,但是我们将保持这些值与我们在第二部分所做的相同,以保持一致性,然后我们正在计算输入的嵌入,在这种情况下,这将是优化后的查询,好的,它将被传入这个函数,我们将为其生成一个嵌入,然后我们将生成相似度分数,将嵌入的优化查询与我们向量数据库中所有内容的嵌入进行比较,然后我们将检索10个,在这种情况下,使用10个默认值,10个最相似的文档,然后我们将返回它们,所以再次,我们在上一节中已经讲过了,所以我们将继续进行下一步,这是我们完整的rag pipeline的第三步,所以它将是optimize query,然后retrieve documents,然后rerank documents,所以在这种情况下,我们可以直接回到第二部分的参考代码,我们只需要获取文件五,rerank documents,我们只需要获取rank documents函数,整个函数,好的,这段代码到这里,我们将回到我们的文件,我们将把它粘贴在这里,我们需要做几件事,所以这将是另一个use server函数,所以我们将运行coher API,这实际上是错误地导入了coher,我们终于从cursor那里得到了一个幻觉,这实际上是coher AI的coher客户端,然后当然,我们需要初始化东西,不知何故,你实际上必须在这里放入token,当你初始化coher客户端时,使用环境变量,我不知道这是否是我的设置方式的错误,但是请注意,对于open AI,你不需要这样做,但对于coher,你需要这样做,所以请确保你在这里添加它,然后这将与我们之前使用的几乎相同,唯一的例外是,我们实际上将删除那个name参数,所以我们实际上可以删除那里的name,以及上面的name,因为我们只使用content,在第二部分,我们使用name和content,在第三部分,我们只使用content字段,这将运行我们从retrieve documents函数返回的文档,然后将运行coher reranking API,以进一步优化我们的检索器结果,从最相似到最不相似,最后,我知道这变得越来越长,我们快要结束了,再次,有些人可能正在快速浏览这个,因为我们在第二部分做了完全相同的事情,但是显然我们需要重复它,以确保每个人都跟上我们,然后在run rag pipeline函数中,再次,这将是服务器函数,use server,我们将做export async function run rag Pipeline,这只会接受用户查询,他们将接受那个单一的用户查询,它将运行我们整个rag pipeline,所以我们需要的第一件事是optimize query,我们终于将利用那个get optimized query函数,当然,我们将查询作为单个参数传递给该函数,让我们实际记录下来,这样我们就可以,嗯,当我们运行我们的应用程序时,我们可以实时看到那些结果,然后我们需要获取第二步,这是我们将在我们的向量数据库上运行的基本相似性搜索,我们将传入两个参数,我们将传入那个优化后的查询,然后我们将做一个限制,我们实际上将做一个25的限制,所以我们将实际覆盖那个10的默认值,对于这个例子,我们将获取25个,我们可以甚至做,让我们做min similarity,让我们变得更慷慨一点,让我们使用0.2,这样它们可以稍微不那么相似,因为我们正在获取更多的记录,让我们记录下这两件事,我们将记录下那些文档,然后只记录它们的长度,这样我们就可以看到我们实际上得到了多少,记住,仅仅因为我们在这里定义了25个,并不意味着我们一定会得到25个,我们可能在表中没有25条记录,或者25条可能不符合我们设定的标准,那个最小相似度分数,所以我们可能会得到更少,我们将看看,最后,我们需要运行那些结果,那些文档,我们将称之为ranked results,我们实际上需要将所有那些文档和那个优化后的查询通过ranker,我们将在这里获取五个,我们将覆盖它并运行,使用五个,我们将记录下最终结果,然后当然,我们想返回ranked results,所以我们刚刚完成了所有这些,我们实际上做了四个,我们做了八个不同的文件,所以工作量相当大,但是这是一个完整的rag pipeline,它将接受用户查询,它将优化它,它将通过你的向量数据库中的余弦相似度进行基本相似性搜索,它将检索那些文档,基于我们设定的这些参数,然后它将把那些文档放入一个ranker,再次,我们使用coheres rank API,然后在最后,我们将得到那些ranked results,我们将得到我们数据库中的ranked records,希望是那五个最相似的,假设我们得到了一个完整的fetch结果,我们可以将那些结果馈送到completions函数中,以实际获得答案,所以再次,我知道这很多,你可能想参考第二部分,以获得更深入的讨论,对一些步骤进行更深入的解释,但是那就是我们的rag pipeline,那就是我们将在下一课中使用的,所以让我们实际上开始构建它的UI,这通常是大家最喜欢的部分,所以我们将终于连接一个rag pipeline,我们将连接这个你构建的东西到一个实际工作的前端,所以这里的想法是,我们需要我一开始提到的两个基本部分,我们将需要左边的文档上传器,然后我们将使用右边的AI聊天,所以我们需要那两个不同的东西,所以我们将在这里进入,进入我们的项目,在我的rag app中,在这个app文件夹中,这就是你的客户端应用代码将要存放的地方,所以如果你实际上打开layout,打开page,我们将在这些文件中工作,我们需要两个自定义组件,我们自己制作,所以我们在components文件夹中,你需要制作两个文件,你需要做ai chat.tsx,你还需要做一个document uploader.tsx,所以我们需要制作这两个组件,以便我们可以将它们导入到我们的页面中,并实际拥有一个像样的应用程序来实际工作,所以我们将在这里做两件事,我们将把它分成两部分,我将向你展示一种使用AI工具来原型化这种情况下UI的非常好的方法,这是一个非常有价值的工具,叫做v0,你可以用得非常好,这是由Vercel制作的,所以就像我们使用他们的AI SDK一样,如果你学过我们的app课程,这就是我们用于部署的东西,等等,而Vercel,他们有一个类似ChatGPT的应用程序,专门用于开发问题,所以如果你熟悉类似Clerk Artifact的东西,或者GPT,我相信他们称之为canvas或类似的东西,我们在这里所做的就是,我们可以说,我需要一个文本区域来上传文档,我需要将一些文本复制粘贴到那个巨大的文本区域中,我需要能够点击一个上传按钮,我们可以发送这个,而v0将要做的是,我相信你应该能够免费尝试所有这些,我相信他们有一个消息限制,所以这是一个我实际上付费的专业工具,我从中获得了巨大的价值,你可以在右边看到,我们正在实时获得一个可以在我们的项目中使用的组件,而酷的是,它不仅生成UI,而且如果我们说,好的,把它放在屏幕的左边,右边,我们需要一个基本的AI聊天界面,你会在这里看到,然后v0,我们可以实际上迭代这个我们得到的预览,而v0将为我们做的是,基本上将编写我们所需的所有代码,一旦它完成,我们就可以将该代码复制到我们的应用程序中,我将向你展示如何做到这一点,这是一个非常棒的工具,我强烈推荐它,如果你是专业人士,或者正在大量构建这些工具,我相信vzer pro每月大约20美元,这也不是赞助,它只是我个人使用的工具之一,我认为它已经变得如此之好,我认为它是那种,如果你以专业身份做这个,或者你与之相关,我认为它值得付费,我认为你将获得巨大的价值,你可以看到,我们基本上有了两个组件,我们需要在我们的应用程序中使用,所以像我想要的一件事是,我希望它占据与聊天相同的高度,所以我只需要在v0中做的是提问,对吧,我可以说,请让document upload占据页面的全部高度,就像聊天一样,我们希望它一直向下延伸,如果你看到了示例应用程序,你知道,参考应用程序,你也可以查看代码,两者都占据了全部高度,所以我们可以继续迭代,如果我们想手动迭代,当然,我们可以直接复制代码,但是我要做的是,我将直接复制它,这样我就可以展示如何在应用程序中使用它,对吧,所以我们可以做的就是,当预览加载时,好的,现在这是整页,我们可以转到上面的代码选项卡,看起来它为我们制作了两个,我猜我们要用的是这个,这很有趣,这可能是一个小错误,但是你可以下载文件,你可以复制它,在这种情况下,我们将复制这段代码,然后我们将把它粘贴到我们的document uploader组件中,然后当然,要在这里使用上传器,我们需要进入我们的page.tsx,我们将删除所有那些样板代码,再次,那是页面代码,它渲染这个启动模板代码,我们需要做的是,我们将把它包装在一个div中,我们将使用document upload document upload,然后如果我们回到localhost 3000并刷新,我们应该会看到document upload组件,你可能会想,为什么是这个布局,我们可以回到v0,转到那个页面,我们将复制这段代码,我们将用这段代码替换我们的页面代码,当然,导入会略有不同,在我们的应用程序中,所以我们需要做的是,我们还需要确保获取那个AI chat代码,所以让我们复制它,把它放在AI chat中,这样我们也有那段代码,让我们获取我们的UI,然后如果我们回到localhost 3000,你会看到我们有v0生成的这个漂亮的 कोड,我们可以直接在聊天界面中工作,并获得这些相当不错的起始组件,当然,如果你想进入这段代码,好的,好处是你拥有代码,在这个时候,所以你可以进去,你可以调整样式等等,但是这里的重点是,这是一个非常好的构建UI的方法,我们只是想偷偷地加入这个小技巧,我知道这个课程的重点是Rag和检索和构建它,但是,你经常需要做的一件事是,当你构建rag工具,检索工具时,你需要实际构建UI,vzer是一个非常有用的方法,如果你不是,你知道,如果你不是前端开发人员,等等,你只是,你知道,也许你是一个业务分析师,试图使用cursor来快速启动一个rag工具,或者类似的东西,而所有这些AI工具都允许你第一次这样做,那么,vzer在很多方面都是你的前端开发伴侣,所以,我们要做的其中一件事,只是为了保持参考应用程序和我们的应用程序之间的一致性,我们实际上现在已经向你展示了如何自己做,我们将直接复制我们现有的代码,以保持简单,所以我们将进入那个参考应用程序,我们将首先获取页面代码,所以我将把参考应用程序的页面代码复制到我的rag app page,你可以看到它很相似,唯一改变的是一些样式,然后我们将关闭它,然后我们将转到参考应用程序的两个组件,AI chat和document uploader,所以让我们复制它们,放到我们的文件中,所以我们只是复制粘贴整个文件,我们将用这里的内容替换所有内容,所以我将获取参考document uploader,并将其替换到我们的app document uploader中,你应该看到,看起来我有一个轻微的导入错误,我们是否将它命名为其他东西了,让我们检查一下,所以我们在lib文件夹中,在Rag and generate lib rag generate,所以我们称之为generate而不是generation,好了,消除了那个错误,现在如果我们回到localhost,你会看到或多或少看起来一样,但是我们现在有了我们的rag系统连接到这个UI,所以我们现在需要做的是实际上解释它是如何连接起来的,这样你实际上就有了一些关于正在发生的事情的背景,所以我们将这样做,让我关闭几个文件,只是为了简化事情,所以从高层次来看,我们有我们的页面,这是我们顶层的页面,左边是文档上传器,右边是AI聊天组件,很简单,然后接下来在左边,我们有我们的文档上传器,所以我们这里有哪些部分,嗯,我们有一个巨大的文本区域,再次,你记得我们之前从Shaden导入或安装了按钮、输入和文本区域组件,所以我们的组件在这里正在做的是,它只是导入了我们从Shaden库获得的那些组件,再次,一个非常好的方法来获得免费的现成代码,这就是它如此受欢迎的原因之一,所以你有一个小标题,我们有那个文本区域,所以当我们处理文本时,如果你想,你知道,说你有一个巨大的Word文档或其他什么,你把它粘贴在这里,点击上传按钮,你会看到这个上传按钮调用这个handle upload函数,那么handle upload函数做什么呢,它获取文本区域中的任何文本,将其运行到我们的process document函数中,所以你记得这是将我们输入的文本嵌入、分块并插入数据库的管道,所以我们将在下一课中测试它,但我只是想提前谈谈正在发生的事情,所以document uploader,你在这里粘贴你的文本,所以我们甚至可以这样做,我可以输入“嗨,我的名字是McKay”,所以这将把这个文本,当我们点击上传文档时,它将运行,它将分块,在这种情况下,这显然少于500个token,所以它不需要分块,它将是一个单一的块,它将嵌入它,然后它将上传到我们的数据库,你可以测试它是否有效的方法是,如果你去你的数据库,我实际上只需要刷新它,你会看到在我点击那个按钮之后,我们现在有了“嗨,我的名字是McKay”,我们有了嵌入,一切都为检索过程设置好了,而检索过程由右边处理,由AI聊天区域处理,所以让我们进入那个组件,谈谈正在发生的事情,所以我们正在做的一件事,我们正在跟踪我们的消息,我们的输入,这都是前端状态的东西,如果你想更详细地了解它,我们有我们的app building课程,你可以去学,它教你所有这些东西的来龙去脉,但是因为我们有那些参考资料,我不会把它变成一个完整的关于前端开发的讲座,我们将保持,我们将保持相当轻松,但是我们可以做的是,我们可以发送,我们可以发送消息,所以这里发生了什么,如果我说我的名字是什么,我的名字是什么,这里发生的是,那个值被跟踪在这个输入上,当我们实际上点击这个发送按钮时,如果你回到下面,你会看到我们在底部有一个按钮,你可以看到那个发送文本,你可以看到当这个按钮被点击时,它正在运行这个handle send message函数,那么handle send message函数做什么呢,从高层次来看,我们要做的第一件事是,我们将根据我们的输入运行我们的rag pipeline,所以我们将把这个“我的名字是什么”我甚至会加一个问号“我的名字是什么”,然后我们点击发送,它将被传递到这个handle send message函数,并将运行我们的rag pipeline,正如你所记得的,我们的rag pipeline包括三个步骤,首先,我们将把这个文本,我们将优化那个查询,所以它将运行查询优化,下一步,我们将把优化后的查询传递给我们的retrieve documents函数,我们将获取25个最相似的记录,最小相似度得分为0.2,在这种情况下,我们数据库中目前只有一个记录,所以它只会获取一个,但是,接下来发生的是,我们将把那些检索到的文档和优化后的查询,我们将把我们的文档通过coheres rerank API,然后我们将得到从最相似到最不相似的五个最相似的文档,然后我们将返回它们,如果我们回到我们的组件,你会看到它被设置为relevant docs,一旦我们有了relevant docs,relevant docs只是我们rag pipeline的最终输出,我们可以将其作为上下文传递给我们在几课前编写的generate completion with context函数,所以发生的是,来自那五个来源的上下文将被插入到这里,所以发生的是,AI模型,在这种情况下是GPT 40 mini,它得到一个系统提示,它说根据提供的上下文回答,以及上下文是什么,来自那五个最相关的文档,以及我们的输入,在这种情况下,它将是“我的名字是什么”,然后如果我们发送它,你会看到我们得到一个漂亮的UI,显示那些来源,所以如果我实际上点击那个按钮并打开它,你会看到第一号,再次,我们现在只有一个来源,“嗨,我的名字是M”,然后你可以在蓝色中看到AI回答“你的名字是McKay”,所以这就是我们的rag pipeline与这两个组件一起工作,完全构建出来,所以我们将继续,实际上看看一个更高级的例子,所以为了证明这一点,我将在这里做一件事,我们可以获取,这是2024年美国大选的维基百科条目,我使用这个例子的唯一原因是因为这是一个相当近期发生的事件,它不会在LLM的训练数据中,模型可能在几个月前就训练好了,所以它不会知道谁赢得了选举,所以,我们可以复制粘贴整个维基百科条目,你可以看到我选择这个是因为它是一个当前事件,有一个非常长的维基百科条目,我们当然不会在这里涉及政治,但它只是rag的一个很好的例子,当然,我们可以粘贴进去,你会看到我们有一个巨大的段落,这是整个维基百科页面,在我们的文本框里,显然这是一个非常长的文本文档,这个东西需要分块,你可以使用的很酷的工具之一是这个OpenAI tokenizer,他们在OpenAI网站上有一个非常酷的例子,所以如果我们实际上也把文本粘贴到tokenizer里,你会看到这个文本是37514个token,这意味着它将被分成很多块,所以我们可以做的是,如果我们点击上传文档,它将运行那个document uploader函数,它将处理文档,如果我们去我们的rag数据库,我们将刷新它,你会看到我们现在有了所有这些块,所以发生的是,因为那个维基百科页面有那么多token,它不得不运行我们的文本分割器,不得不把东西分成很多500个token的块,来分割那个东西,你可以看到我们总共有76条记录,加上我们从例子中得到的一条,而酷的是,它为每个记录生成了一个嵌入,现在我们可以,你知道,如果我们说,嗯,让我们看看,让我们看看我们能不能找到一些非常小众的东西,比如,埃里克·奎萨说了什么,让我们实际重置一下,这样我们就有了一个重置按钮来重置聊天,这个个人说了什么,让我们看看我们的rag pipeline有多好,让我们发送,你可以看到我们有五个来源,我们可以隐藏它们,你可以看到这基本上是从维基百科页面上提取的精确段落,所以它能够运行我们的查询优化,然后是基本检索,然后是文档重排,给我们这五个来源,记住,由于我们分块的方式,每个都将是500个token,所以我们将这五个来源作为上下文,将其发送给AI模型,它正在生成答案,正如你所看到的,我们使用了非常非常细微的东西,你知道,里面的一个小细节,它实际上做对了,这很令人印象深刻,如果我们尝试另一件事,我们可以说,让我们去这里,让我们做一些非常基本的事情,比如,选举计数是多少,选举计数是多少,这是一个相当标准的查询,人们会想问,你可以看到我没有使用选举,我没有引用文档,它只是继续查找结果,你可以在这里看到,这是最相关的,它似乎正在提取选举票数,你可以看到,它分别是312和226,我猜这会排在前面,312到226,非常有趣,所以你可以看到我们的rag pipeline正在按我们想要的方式工作,我们正在处理一个非常长的文本文档,37000个token,我们正在上传它,我们正在处理它,以便将其嵌入到我们的向量数据库中,然后我们能够成功地对最相关的文档进行排序,并实际获取我们想要的信息,所以,让我们远离政治,怎么样,再次,我们只是用它是因为它是一个当前事件,有一个非常长的维基百科条目,让我们做一些像,让我们把火星的维基百科页面放进来,然后稍微,嗯,不那么爆炸性的东西,我们可以复制这个,我只是想再做一个例子,因为它展示了你如何真正开始分离事物,所以让我们获取整个条目,让我们上传它,这需要一点时间来运行,如果我们再次进入我们的数据库,刷新它,我们应该会看到更多记录,所以看起来我们得到了大约30条左右的记录,我们可以做的是,好的,让我们在这里找到一个小事实,比如,火星上的环境条件是什么,比如,火星表面有多少热量传递,让我们重置一下,火星表面有多少热量传递,所以我们可能会期望答案得到一些与这段文字有关的东西,所以让我们看看它是否能做到,你可以看到火星表面由于其稀薄的大气层而几乎没有热量传递,你可以看到,行星表面几乎没有热量传递,它在谈论大气压,所以它基本上能够通过,我们有多少条记录,104条记录,再次,我们有整个选举的东西,我们有这个测试记录,我放进去的,我们有火星数据库,它仍然能够获取最相关的结果,并给我们想要的答案,所以这是一个真实的rag pipeline的绝佳例子,任何时候你构建检索工具,这都是你将要使用的绝对基础,再次,会有一些不同的事情,你知道,你想尝试的一件事,我把它留给你作为延伸目标,因为我认为这就是你真正学习的方式,对于很多文档上传器来说,你不想复制粘贴文本,你可能想,你知道,获取一个,你知道,Google文档或其他什么,下载它,然后也许上传到某种文件上传器,而不是复制粘贴,所以学习如何做到这一点,并采用我们已经讨论过的所有原则,并学习如何使用不同的文档加载器,也许你想按语义而不是按token计数来分割,这些是你想要习惯的用例相关的变量,但是我们之所以这样构建项目,是因为我们在这里这样做的方式将是第一层,基础层,你可以带到每个项目中,并在任何地方使用,你知道,所有其他的小东西,那将是与用例相关的,所以我们处理了检索工具的绝对核心,它将是某种上传文档的方式,将其放入向量数据库,某种使用AI聊天界面或某种查询机制的方式,以便你可以实际运行rag pipeline,获取最相关的结果,将其发送给AI模型,并获得你想要的结果,所以希望你能利用你在本节中学到的一切,来构建你自己的应用程序,你自己的工具,以便你可以以各种有用的方式使用检索和rag,希望你觉得这很有帮助,这是完成的项目,再次,我建议如果你有时间,尝试一些延伸目标,尝试PDF,尝试,而不是按token计数分块,尝试按换行符,段落等进行分块,希望每个人都觉得这很有帮助