构建你的第一个RAG AI应用

大模型向量数据库机器学习
前言

如何构建你的第一个RAG应用?这篇博客介绍了如何构建一个用于文档问答的RAG(Retrieval-Augmented Generation)应用 ,并以亚马逊2023年第一季度财务报告为示例,演示了从文档中高效提取信息并生成精准回答的流程。RAG通过检索相关上下文,使大语言模型(LLM)更高效地回答问题。

文章涵盖了信息提取、文本分块、上下文存储与检索 等关键环节。作者展示了如何用PyMuDF库 从PDF中提取文本,并将其分块以适应LLM的上下文窗口。文章比较了固定大小分块、递归分块语义分块 等策略,并介绍了关键词匹配(如TF-IDF、BM25)和向量嵌入 用于语义检索。

在示例中,RAG应用根据用户输入检索最相关的文档块,并将其附加到LLM的输入中,以生成精准答案。作者指出了应用的局限性,例如表格数据处理不准确,并提出未来改进方向:通过更高级的检索和性能评估机制优化系统。

该博客为开发者提供了完整的RAG实现流程及代码示例,并鼓励通过迭代优化提升应用效果。

picture.image

典型的RAG应用
    在这篇博客中,您将学习如何创建您的第一个用于文档问答的RAG应用程序。像Adobe这样的公司正在采用这些文档问答和聊天功能作为测试能力。如果做得好,这些功能将非常强大,使读者能够从文档中获得新的见解并节省宝贵的时间。通过构建这个应用程序,您将遇到成功执行文档问答任务所需的多个方面。这些方面包括信息提取、检索相关上下文以及利用这些上下文生成准确的结果。  
    在本博客结束时,您将完成一个基于10-Q财务文件的问题回答系统,使用亚马逊2023年第一季度财务报表作为代表文件,按照上图所示步骤进行。


    首先,我们将讨论如何从该文件中提取信息。其次,我们将讨论如何将文件分解成更小的块,以适应LLM的上下文窗口。第三,我们将讨论保存文档以供将来检索的两种策略。一种是按原样存储文本以进行基于关键词的检索。另一种是将文本转换为向量嵌入,以实现更高效的检索。第四,我们将讨论如何将其保存到相关数据库中。第五,我们将讨论如何根据用户输入获取相关的块。最后,我们将讨论如何将相关的文档块作为LLM上下文的一部分,以生成输出。步骤1到步骤4被称为索引管道,其中文档在用户交互之前离线地索引到数据库中。步骤5和步骤6在用户查询应用程序时实时发生。  
    回答文档问题的第一步是为LLM提取文本信息。根据我的经验,提取步骤通常是最被忽略的因素,但它对于RAG应用程序的成功至关重要。这是因为最终,LLM的回答质量取决于提供的数据上下文。如果这些数据存在准确性或一致性问题,将导致整体结果不佳。本节介绍了为RAG应用程序提取数据的方法,特别是从PDF文档中提取数据。你可以将从提取到最终将数据存储在正确的数据库中的整个过程看作是类似于传统的提取、转换、加载(ETL)过程,在这个过程中,信息从原始数据源中检索,经过一系列修改(包括数据清洗和格式化),然后存储在目标数据存储库中。  
    提取文本的基本方法是将PDF中的所有信息提取为一个大字符串。然后可以将这个字符串分解成更小的块,以适应LLM的上下文窗口。  
    PyMuDF[1] 是一个使从PDF文档中提取文本为字符串变得简单的库。还有其他具有类似功能的文本解析器,如PyPDF和PDFMiner。PyMuDF的优势在于它支持多种格式的解析[2],包括txt、xps、图片等,而这是某些其他软件包无法实现的。下面,你可以看到如何使用PyMuDF从Amazon Q1–2023 PDF文档[3]中提取文本为字符串:

            
import requests  
            
import fitz  
            
import io  
            
url = "https://s2.q4cdn.com/299287126/files/doc_financials/2023/q1/Q1-2023-Amazon-Earnings-Release.pdf"  
            
request = requests.get(url)  
            
filestream = io.BytesIO(request.content)  
            
with fitz.open(stream=filestream, filetype="pdf") as doc:  
            
    #将文本拼接成一个字符串,并输出前10个字符  
            
    text = ""    for page in doc:        text += page.get_text()print(text[:10])  
        
数据分块
    一个自然的第一个问题是——为什么要做这一切,为什么不直接将所有文本发送给LLM并让它回答问题呢?以亚马逊2023年第一季度的文档为例。整个文本大约有5万个字符。如果你尝试将所有文本作为上下文传递给GPT-3.5,你会因为上下文过长而得到一个错误。


    LLM通常有一个令牌限制(每个令牌大约是3/4个单词)。让我们看看如何通过分块来解决这个问题。分块涉及将冗长的文本分成较小的部分,这样LLM可以更高效地处理。  
    下图概述了如何构建一个基本的RAG,它利用LLM在自定义文档上进行问答。第一部分是将多个文档拆分成可管理的块。相关参数是最大块长度。这些块应该是包含典型问题答案的典型(最小)文本大小。这是因为有时你提的问题可能在文档的多个位置有答案。例如,你可能会问“X公司从2015年到2020年的表现如何?”而你可能有一个大文档(或多个文档),其中在不同部分包含了有关公司多年表现的具体信息。理想情况下,你会希望捕捉到包含这些信息的文档的所有不同部分,将它们链接在一起,并传递给LLM基于这些过滤和连接的文档块进行回答。  

picture.image

RAG 组件
    最大上下文长度基本上是将各种块拼接在一起的最大长度,同时为问题本身和输出答案留出一些空间。请记住,像GPT3.5这样的LLM有严格的长度限制,包括所有内容:问题、上下文和答案。找到合适的分块策略对于构建高质量的RAG应用至关重要。  
    根据具体使用情况,有不同的方法来进行分块。以下是基于复杂性和有效性进行分块的五个层次[4]

固定大小分块: 这是最基本的方法,将文本按指定字符数分割成块,不考虑内容或结构。实现简单,但可能导致块缺乏连贯性或上下文。 •递归分块: 这种方法使用一组分隔符(如换行符或空格)以分层和迭代的方式将文本拆分成较小的块。如果初始分割未能生成所需大小的块,它会使用不同的分隔符递归地对生成的块进行处理。 •基于文档的分块: 在这种方法中,文本根据其固有结构(如markdown格式、代码语法或表格布局)进行分割。这种方法可以保留内容的流动性和上下文,但对于缺乏明确定义结构的文档可能效果不佳。 •语义分块: 这种策略旨在从嵌入中提取语义意义,并评估块之间的语义关系。它通过嵌入相似性自适应地选择句子之间的断点,将语义相关的块保留在一起。 •代理分块: 这种方法探讨了使用语言模型来确定基于上下文的块包含内容和数量的可能性。它通过命题检索生成初始块,然后利用基于LLM的代理来决定某个命题是否应包含在现有块中,还是应创建新的块。

    相似度阈值是一种将问题与文档块进行比较的方法,以找到最有可能包含答案的顶级文档块。余弦相似度是常用的度量标准,但您可能希望权衡不同的度量标准,例如包括关键字度量,以便更重视包含某些关键字的上下文。例如,当您向大型语言模型(LLM)询问总结文档的问题时,您可能希望权重包含“摘要”或“总结”这些词的上下文。  
    让我们在第一个RAG应用程序中使用简单的固定分块,在必要时按句子分割块。为此,我们需要将文本拆分成块,当它们达到提供的最大标记长度时。下面的OpenAI标记器可以用来标记文本,并计算标记的数量。

          
tokenizer = tiktoken.get_encoding("cl100k_base")  
          
df=pd.DataFrame([text]).T  
          
df.columns = ['text']  
          
df['n_tokens'] = df.text.apply(lambda x: len(tokenizer.encode(x)))  
      
    然后,可以根据令牌限制将此文本拆分为多个上下文,以便大语言模型(LLM)理解。为此,文本从句号分隔符处拆分成句子,并将句子附加到一个块中。如果块的长度超过令牌限制,则该块被截断,并开始下一个块。在下图中,您可以看到按句子进行分块的示例,其中三个块显示为三个不同的段落。

picture.image

示例固定分块

这里是具有相同功能的split_into_many函数:


          
def split_into_many(text: str, tokenizer: tiktoken.Encoding, max_tokens: int = 1024) -> list:  
          
    """ 将字符串拆分为指定数量的令牌的多个字符串的函数 """  sentences = text.split('. ') #A  n_tokens = [len(tokenizer.encode(" " + sentence))              for sentence in sentences] #B  chunks = []  tokens_so_far = 0  chunk = []  for sentence, token in zip(sentences, n_tokens): #C      if tokens_so_far + token > max_tokens: #D          chunks.append(". ".join(chunk) + ".")          chunk = []          tokens_so_far = 0      if token > max_tokens #E:          continue      chunk.append(sentence) #F      tokens_so_far += token + 1  return chunks  
      

#A 将文本分割成句子
#B 获取每个句子的词元数
#C 循环遍历句子和词元并将它们合并成一个元组
#D 如果到目前为止的词元数加上当前句子的词元数大于最大词元数,则将当前块添加到块列表中并重置
#E 如果当前句子的词元数大于最大词元数,则跳到下一句
#F 否则,将句子添加到块中并将词元数添加到总数中
最后,你可以通过调用tokenize函数对整个文本进行标记,该函数将上述逻辑进行整合:


          
def tokenize(text, max_tokens) -> pd.DataFrame:  
          
    """ 将文本拆分为最大标记数量的块的函数 """  tokenizer = tiktoken.get_encoding("cl100k_base") #A  df=pd.DataFrame(['0',text]).T  df.columns = ['title', 'text']  df['n_tokens'] = df.text.apply(lambda x: len(tokenizer.encode(x))) #B  shortened = []  for row in df.iterrows():      if row[1]['text'] is None: #C          continue      if row[1]['n_tokens'] > max_tokens: #D          shortened += split_into_many(row[1]['text'], tokenizer, max_tokens)      Else: #E          shortened.append(row[1]['text'])  df = pd.DataFrame(shortened, columns=['text'])  df['n_tokens'] = df.text.apply(lambda x: len(tokenizer.encode(x)))  return df  
      

#A 加载cl100k_base分词器,该分词器设计用于与ada-002模型配合使用
#B 对文本进行分词,并将分词数量保存到新列中
#C 如果文本为空,则跳到下一行
#D 如果分词数量超过最大分词数量,则将文本拆分成块
#E 否则,将文本添加到简短文本列表中在下图中,你可以看到运行 tokenize(text,500) 后整个数据框的样子。每个块是一个单独的行,总共有13个块。块文本在‘text’列中,该文本的令牌数量在‘n_tokens’列中。

picture.image

块数据
检索方法
    下一步,在完成文档提取和分片之后,需要将这些文档存储在适当的格式中,以便在响应未来查询时可以轻松检索到相关的文档或段落。在接下来的部分中,你将看到两种检索相关LLM上下文的典型方法:基于关键词的检索和基于向量嵌入的检索。

基于关键词的检索

    排序相关文档的最简单方法是进行关键词匹配,并找到匹配度最高的文档。为此,我们首先需要定义一种基于关键词匹配文档的方法。在信息检索中,两个重要概念构成了许多排序算法的基础:词频(Term Frequency,TF)和逆文档频率(Inverse Document Frequency,IDF)。


    词频(TF)衡量一个词在文档中出现的频率。它基于这样一种假设:一个词在文档中出现的次数越多,该文档与该词的相关性就越高。  

TF(t,d) = 词项 t 在文档 d 中出现的次数 / 文档 d 中词项的总数

    逆文档频率(IDF)衡量词项在整个文档语料库中的重要性。它给在语料库中较为稀有的词项赋予较高的重要性,而对常见的词项赋予较低的重要性。

IDF(t) = 文档总数 / 包含词项 t 的文档数

然后通过将 TF 和 IDF 相乘来计算 TF-IDF 分数:

TF-IDF(t,d) = TF(t,d) * IDF(t)
虽然TF-IDF很有用,但它也有局限性。这时,Okapi BM25算法提供了一种更复杂的文档排序方法。

Okapi BM25[5] 是一种基于关键词匹配文档的常见算法,如下图所示。

picture.image

Okapi BM25 算法 [6]
给定包含关键词 q1、q2、… 的查询 Q,文档 D 的 BM25 得分如上所示。函数 f(qi, D) 表示 qi 在 D 中出现的次数,k1 和 b 是常数。IDF 表示词语 qi 的逆文档频率。IDF 衡量一个词在整个语料库中的重要性。它为语料库中罕见的词分配较高的重要性,而为常见的词分配较低的重要性。这用于规范化常见词如 “The” 或 “and” 在搜索结果中的贡献。avgdl 是从中提取文档的文本集合中的平均文档长度。

BM25公式可以理解为TF-IDF的扩展:

1.它使用IDF来衡量术语在整个语料库中的重要性,类似于TF-IDF。 2.术语频率组件(f(qi,D))使用饱和函数进行归一化,这防止了分数随着术语频率线性增加。这解决了基本TF-IDF的一个限制。 3.它包含文档长度归一化(|D| / avgdl),调整了由于文档长度较长而更可能具有较高术语频率的事实。
通过考虑这些额外的因素,BM25通常比简单的TF-IDF方法提供更准确和细致的文档排序,使其成为信息检索系统中的热门选择。

    BM25算法返回一个介于0(查询和文档之间没有关键词重叠)到1(文档包含查询中的所有关键词)之间的值。例如,如果用户输入是“windy day”,而文档是“It's quite windy”——BM25算法将会产生一个非零结果。以下是BM25的Python实现代码片段:

          
from rank_bm25 import BM25Okapi  
          

          
语料库 = ["你好,你怎么样!",            "波士顿的风很大",            "明天天气怎么样?"            ]  
          

          
分词语料库 = [文档.split(" ") for 文档 in 语料库]  
          
bm25 = BM25Okapi(分词语料库)  
          

          
查询 = "大风天"  
          
分词查询 = 查询.split(" ")  
          

          
文档分数 = bm25.get_scores(分词查询)  
          
与查询有关键词重叠的文档。  
          

          
输出:  
          
array([0. , 0.48362189, 0. ]) #A  
          

          
#A 只有第二篇文档与查询有重叠,其他文档没有。  
      
    用户输入的是“有风的日子”。如你所见,语料库中的第二个文档(“波士顿的风很大”)与输入内容有重叠,这反映在第二个得分最高(0.48)。  
    然而,你也会看到第三个文档(“明天天气怎么样?”)与输入内容的相关性(因为两者都讨论了天气)。我们希望第三个文档能有一个非零的评分。这就是语义相似性和向量嵌入的概念所在。一个经典的例子是用户搜索“狂野西部”并期望获得关于牛仔的信息。语义搜索意味着算法足够智能,能知道牛仔和狂野西部是相似的概念(虽然使用了不同的词语)。这对于RAG来说非常重要,因为用户很可能输入一个查询,该查询在文档中并没有完全匹配的内容,因此我们需要一个良好的语义相似性度量来根据用户的意图找到相关的文档。

向量嵌入

当你拥有大量数据,包括数百份或更多文件时,向量搜索可以帮助选择相关的上下文。向量搜索是一种信息检索和机器学习技术,它使用数据点的向量表示来高效地在大型数据集中找到相似的项目。它涉及将数据编码为高维向量,并使用距离度量来衡量这些向量之间的相似性。

在下图中,你可以看到一个简化的二维向量空间:

X轴:大小(小 = 0,大 = 1)
Y轴: 类型 (树 = 0, 动物 = 1)

这个例子展示了方向和大小:

一棵小树可以表示为 (0, 0)

一棵大树可以表示为 (1, 0)

一只小动物可以表示为 (0, 1)

一只大动物可以表示为 (1, 1)

向量的方向表示特征的组合,而向量的大小(长度)代表这些特征的强度或显著性。
这是一个概念性的例子,可以扩展到成百上千个维度,每个维度代表数据的不同属性。在实际应用中,这些向量通常具有更高的维度,允许对复杂数据进行更细致的表示。同样的操作也可以对文本进行,如下所示,相较于关键词搜索,这种方法能产生更好的语义相似性。一个合适的嵌入算法能够判断哪些上下文与用户输入最相关,哪些上下文不太相关,这对于RAG应用中的检索步骤至关重要。一旦找到相关的上下文,可以将其添加到用户输入中,并传递给大型语言模型(LLM)以生成适当的输出,然后发送回用户。

picture.image

向量搜索 101 注意下面的图中,向量化能够捕捉语义表示(即,它知道谈论一只鸟俯冲下来抓住一只小花栗鼠的句子应该在(小,动物)象限,而谈论昨天的暴风雨导致一棵大树倒在路上的句子应该在(大,树)象限)。实际上,维度不止两个。例如,OpenAI的嵌入模型有1536个维度。

picture.image

使用单词进行向量搜索101
从OpenAI的嵌入模型中获取嵌入是非常容易的。在这篇博客中,我们将使用OpenAI的嵌入模型和大语言模型(LLM)。OpenAI的嵌入模型费用是$0.10每百万个标记,每个标记大约是3/4个单词。一个标记是一个单词或子单词。当文本通过分词器时,它根据特定的方案对输入进行编码,并发出LLM可以理解的专门向量。这个成本非常低——大约是每3000页10美分,但随着文档和用户数量的增加,成本也会逐渐增加。


          
import openai  
          
from getpass import getpass  
          
api_key = getpass('请输入OpenAI API Key: ')  
          
client = openai.OpenAI(api_key=api_key)  
          
openai.api_key = api_key  
          

          
def get_embedding(text, model="text-embedding-ada-002"):  
          
    return client.embeddings.create(input=[text], model=model).data[0].embedding  
          
#A  
          
e1 = get_embedding('那个男孩去参加了一个派对')  
          
e2 = get_embedding('那个男孩去参加了一个派对')  
          
e3 = get_embedding("""我们通过运行SEAT(May等,2019)和Winogender(Rudinger等,2018)基准测试,发现了我们模型中的偏见证据。这些基准测试共包括7项测试,测量模型在应用于性别化名字、地区名字和某些刻板印象时是否包含隐性偏见。  
          
例如,我们发现我们的模型在与非裔美国人名字相比时,更强烈地将(a)欧洲裔美国人名字与正面情感联系在一起,以及(b)将负面刻板印象与黑人女性联系在一起。""")  
          

          
#A 现在让我们获取下面几个示例文本的嵌入。  
      
    前两个文本(对应于嵌入向量 e1 和 e2)是相同的,因此我们期望它们的嵌入向量也是相同的,而第三个文本则完全不同。为了找到嵌入向量之间的相似性,我们使用余弦相似度。余弦相似度通过两个向量之间夹角的余弦值来衡量两个向量的相似性。余弦相似度为 0 意味着这些文本完全不同,而余弦相似度为 1 则表示文本完全相同或几乎相同。我们使用下面的查询来计算余弦相似度:  

1-spatial.distance.cosine(e1,e2)
输出:
1
1-spatial.distance.cosine(e1,e3)
输出:
0.69

正如你所看到的,余弦相似度(1-余弦距离)为1

用向量嵌入寻找相关上下文

    现在让我们看看向量嵌入在选择正确的上下文来回答问题时的表现。假设我们想问一个问题,对应于2023年第一季度的亚马逊,并提出以下问题:

          
prompt="""亚马逊第一季度的销售增长是多少?"""  
          
我们可以从GPT3.5 (ChatGPT) API中获取答案,如下所示:  
          
def get_completion(prompt, model="gpt-3.5-turbo"):  
          
response = openai.chat.completions.create(  
          
model="gpt-3.5-turbo",  
          
temperature=0,  
          
messages=[{"role": "user", "content": prompt}]  
          
) #A  
          
return response.choices[0].message.content  
          
答案:  
          
亚马逊第一季度的销售增长为9%,净销售额增加到1274亿美元,相比2022年第一季度的1164亿美元。除去外汇汇率的影响,净销售额相比2022年第一季度增长了11%。  
          
#A 调用OpenAI Completions端点  
      
    如你所见,尽管上面的回答没有错,但这不是我们要找的答案(我们要找的是2023年第一季度的销售增长,而不是2022年第一季度的)。因此,向LLM提供正确的上下文非常重要——在这种情况下,这将是与2023年第一季度销售业绩相关的上下文。假设我们有以下三个上下文可供选择以附加到LLM:

#A 以下是三个与用户输入(2023年第一季度销售业绩)相关的上下文,需要从中选择相关的内容


          
context1="""净销售额在第一季度增长了9%,达到1274亿美元,而2022年第一季度为1164亿美元。若排除整个季度内外汇汇率变化所带来的24亿美元的不利影响,净销售额相比2022年第一季度增长了11%。北美地区的销售额同比增长了11%,达到769亿美元。国际地区的销售额同比增长了1%,达到291亿美元,若排除汇率变化的影响,则增长了9%。AWS部门的销售额同比增长了16%,达到214亿美元。"""  
          
context2="""营业收入在第一季度增加到48亿美元,而2022年第一季度为37亿美元。2023年第一季度的营业收入包括大约5亿美元的预计遣散费用。北美地区的营业收入为9亿美元,而2022年第一季度为16亿美元的营业亏损。国际地区的营业亏损为12亿美元,而2022年第一季度为13亿美元的营业亏损。AWS部门的营业收入为51亿美元,而2022年第一季度为65亿美元的营业收入。"""  
          
context3="""第一季度的净收入为32亿美元,即每股摊薄收益为0.31美元,而2022年第一季度的净亏损为38亿美元,即每股摊薄亏损为0.38美元。为了便于比较,所有有关前期的每股和每股信息均已根据2022年5月27日生效的20比1股票拆分进行了追溯调整。  
          
• 2023年第一季度的净收入包括在非营业费用中的来自Rivian Automotive, Inc.普通股投资的5亿美元税前估值损失,而2022年第一季度则为该投资的76亿美元税前估值损失。"""  
          
测量查询嵌入和三个上下文嵌入之间的余弦相似度,结果显示上下文1与查询嵌入的余弦相似度最高。因此,将此上下文附加到用户输入并发送给LLM,更有可能给出与用户输入相关的答案。我们可以将这个相关的上下文输入到提示中,如下所示:  
          

          
prompt=“””根据以下上下文,亚马逊在第一季度的销售增长是多少?  
          
上下文:  
          
{context1}  
          

          
print(get_completion(prompt))  
      

LLM现在给出的答案是我们想要的,如下所示,因为这是2023年第一季度的销售增长:


        
            

          根据报告的净销售额,亚马逊第一季度的销售额增长了9%,从上一年第一季度的1164亿美元增长到1274亿美元。 
        
      
增强生成
    上面讨论的步骤是为用户通过提出查询与RAG应用程序交互时准备文档的步骤。  
    在本节中,我们将探讨如何在用户查询应用程序时使用分块的嵌入信息作为相关上下文。此步骤是根据用户输入实时检索上下文,并使用检索到的上下文生成LLM输出。让我们以用户输入的问题“亚马逊第一季度的销售增长是多少?”为例,该问题基于亚马逊2023年第一季度的10-Q文件。要回答这个问题,我们首先需要从上面创建的文档分块中找到正确的上下文。  
   让我们为此定义一个`create_context`函数。如你所见,下面的`create_context`函数需要三个输入——需要嵌入的用户输入查询、包含文档的数据框以找到与用户输入相关的上下文子集,以及最大上下文长度,如下图所示。

picture.image

检索和生成
逻辑在于获取问题的嵌入,如上图所示,计算输入查询嵌入和上下文嵌入之间的成对距离(步骤2),并根据相似度对这些上下文进行排序并附加(步骤3)。如果运行的上下文长度超过最大上下文长度,则截断上下文。最后,将用户查询和相关上下文一起发送到LLM,以生成输出。


          
def create_context(question: str, df: pd.DataFrame, max_len: int = 1800) -> str:  
          
    """    通过从数据框中找到最相似的上下文来为问题创建上下文  
          
    """    q_embeddings = get_embedding(question)  #A    df['distances'] = df['embeddings'].apply(lambda x: spatial.distance.cosine(q_embeddings, x))  #B    returns = []    cur_len = 0    for i, row in df.sort_values('distances', ascending=True).iterrows():  #C        cur_len += row['n_tokens']  #D        if cur_len > max_len:  #E            break        returns.append(row["text"])  #F    return "\n\n###\n\n".join(returns)  #G  
          
#A 获取问题的嵌入  
          
#B 获取嵌入之间的距离  
          
#C 按距离排序并将文本添加到上下文,直到上下文过长  
          
#D 将文本的长度添加到当前长度  
          
#E 如果上下文过长,则中断  
          
#F 否则将其添加到返回的文本中  
          
#G 返回上下文  
          

          
# 以下是运行此行后生成的查询及相应的部分上下文:  
          
create_context("亚马逊第一季度的销售增长是多少", df)  
          
# 亚马逊公司公布第一季度业绩  
          
西雅图 — (商业资讯)2023427日 — 亚马逊公司(NASDAQ: AMZN)今天宣布了截至2023331日的第一季度财务业绩。  
          
- 净销售额在第一季度增长了9%,达到1274亿美元,而2022年第一季度为1164亿美元。  - 如果不包括整个季度因外汇汇率年度变化带来的24亿美元不利影响,净销售额相比2022年第一季度增长了11%。  
          
- 北美部门销售额同比增长了11%,达到769亿美元……  
          
如你所见,上下文是相当相关的。然而,这并没有很好地格式化。这正是LLM大显身手的地方,LLM可以根据创建的上下文回答如下问题:  
          

          
def answer_question(  
          
    df: pd.DataFrame,    question: str):  
          
    """    根据数据框文本中最相似的上下文回答问题  
          
    """    context = create_context(        question,        df    )    prompt=f"""根据提供的上下文回答问题。  
          
    问题:"""  
          
    {question}.    上下文:  
          
        {context}    response = openai.chat.completions.create(  
          
model="gpt-3.5-turbo",  
          
temperature=0,  
          
messages=[{"role": "user", "content": prompt}]  
          
)  
          
return response.choices[0].message.content  
      

最后,这里是从查询和数据框生成的相应答案:


          
answer_question(df, question=”亚马逊第一季度的销售增长是多少”)  
          

          
**亚马逊第一季度的销售增长为9%,达到1274亿美元,相比2022年第一季度的1164亿美元。**  
      
    恭喜,你现在已经构建了你的第一个RAG应用程序。虽然它在回答文本上下文中明确的问题时效果很好,但从表格中检索到的答案并不总是准确的。让我们问一个问题:“截至2022年3月31日,亚马逊的综合收入(损失)是多少?”——答案在表格中显示为$ 4,833百万,如下图所示:

picture.image

表格中的答案:

应用程序给出的答案是:
截至2022年3月31日的三个月期间,亚马逊的综合收益(亏损)为净亏损38亿美元。

    正如你所看到的,它给出了净收益(亏损),而不是综合收益(亏损)。这说明了我们构建的基本RAG架构的局限性。  
    在下一篇博客中,我们将学习高级文档提取、分块和检索机制,这些机制是建立在我们在这里学到的概念之上的。我们将学习如何使用各种指标来评估我们RAG应用的响应质量。我们将学习如何使用不同的技术,在评估结果的指导下,进行迭代改进以提升性能。

你可以在这里找到代码:https://github.com/skandavivek/RAG-From-Scratch/blob/main/Ch2/Ch02\_walkthrough.ipynb

声明

本文由山行翻译整理自:https://medium.com/emalpha/your-first-rag-5844527aab4a,如果对您有帮助,请帮忙点赞、关注、收藏,谢谢~

References

[1] PyMuDF: https://pymupdf.readthedocs.io/en/latest/
[2] 支持多种格式的解析: https://pymupdf.readthedocs.io/en/latest/about.html
[3] Amazon Q1–2023 PDF文档: https://s2.q4cdn.com/299287126/files/doc\_financials/2023/q1/Q1-2023-Amazon-Earnings-Release.pdf
[4] 五个层次: https://medium.com/@anuragmishra\_27746/five-levels-of-chunking-strategies-in-rag-notes-from-gregs-video-7b735895694d#b123
[5] Okapi BM25: https://www.elastic.co/blog/practical-bm25-part-1-how-shards-affect-relevance-scoring-in-elasticsearch
[6] Okapi BM25 算法 : https://en.wikipedia.org/wiki/Okapi\_BM25

0
0
0
0
关于作者
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论