跳到主要内容

基础RAG

检索增强生成(RAG)是一种人工智能框架,可以协同LLM和信息检索系统的能力。利用外部知识回答问题或生成内容很有用。RAG有两个主要步骤:1)检索:从存储在向量存储中的文本嵌入的知识库中检索相关信息;2) generation:在提示中插入相关信息,以便LLM生成信息。在本指南中,我们将通过一个非常基本的RAG示例,介绍五种实现方式:

  • RAG与MIUI从头开始
  • RAG与MIUI和LangChain
  • RAG与MIUI和LlamaIdex
  • RAG与和海斯塔克
  • 配备MIUI和Vercel AI SDK的RAG
在Colab打开

RAG从头开始

本节旨在指导您从头开始构建基本RAG的过程。我们有两个目标:首先,让用户全面了解RAG的内部工作原理,并揭开其潜在机制的神秘面纱;其次,为您提供使用最小所需依赖关系构建RAG所需的基本基础。

导入所需的包

第一步是安装软件包 米斯特拉伊faiss cpu 并导入所需的包:

米斯特拉伊进口 
进口 请求:
进口 numpy 作为 np
进口 faiss
进口 os
getpass 进口 getpass

api_key= getpass (“键入您的API密钥”)
客户 = (api_key=api_key)

获取数据

在这个非常简单的例子中,我们从Paul Graham写的一篇文章中获得数据:

响应 = 请求:.得到('https://raw.githubusercontent.com/run-llama/llama_index/main/docs/docs/examples/data/paul_graham/paul_graham_essay.txt')
文本 =响应 .文本

我们还可以将文章保存在本地文件中:

f = 打开(“essay.txt”, w)
f .(文本 )
f .关闭()

将文档分割成块

在RAG系统中,将文档分割成更小的块至关重要,这样在以后的检索过程中可以更有效地识别和检索最相关的信息。在这个例子中,我们简单地按字符分割文本,将2048个字符组合成每个块,得到37个块。

chunk_size = 2048
大块 = [文本 [i:i+chunk_size ] 对于i在里面 范围(0, 伦恩(文本 ),chunk_size )]
伦恩(大块 )

输出

37

注意事项:

  • 块大小:根据您的具体用例,可能需要定制或尝试不同的块大小和块重叠,以在RAG中实现最佳性能。例如,较小的块在检索过程中可能更有益,因为较大的文本块通常包含填充文本,这可能会模糊语义表示。因此,在检索过程中使用较小的文本块可以使RAG系统更有效、更准确地识别和提取相关信息。然而,值得考虑使用较小块所带来的权衡,例如增加处理时间和计算资源。
  • 如何拆分:虽然最简单的方法是按字符分割文本,但根据用例和文档结构,还有其他选择。例如,为了避免在API调用中超过令牌限制,可能需要按令牌拆分文本。为了保持块的内聚性,按句子、段落或HTML标题分割文本可能很有用。如果使用代码,通常建议按有意义的代码块进行分割,例如使用抽象语法树(AST)解析器。

为每个文本块创建嵌入

对于每个文本块,我们需要创建文本嵌入,这是文本在向量空间中的数字表示。具有相似含义的单词在向量空间中应该更接近或距离更短。要创建嵌入,请使用MIUI AI的嵌入API端点和嵌入模型 MIUI嵌入我们创建了一个 get_text_嵌入 从单个文本块中获取嵌入,然后我们使用列表理解来获取所有文本块的文本嵌入。

def get_text_嵌入(输入):
嵌入_批处理_响应 =客户 .嵌入件.创造(
模型=“MIUI嵌入”,
输入=输入
)
返回 嵌入_批处理_响应 .数据[0].嵌入
文本嵌入 = np.阵列([get_text_嵌入() 对于在里面大块 ])

加载到矢量数据库中

一旦我们得到了文本嵌入,一种常见的做法是将它们存储在向量数据库中,以便进行高效的处理和检索。有几个矢量数据库可供选择。在我们的简单示例中,我们使用了一个开源的向量数据库Faiss,它允许高效的相似性搜索。

使用Faiss,我们实例化了Index类的一个实例,该类定义了向量数据库的索引结构。然后,我们将文本嵌入添加到此索引结构中。

进口 faiss

d =文本嵌入 .形状[1.]
指数 = faiss.索引平面L2(d )
指数 .添加(文本嵌入 )

注意事项:

  • 矢量数据库:在选择矢量数据库时,有几个因素需要考虑,包括速度、可扩展性、云管理、高级过滤以及开源与闭源。

为问题创建嵌入

每当用户提出问题时,我们还需要使用与以前相同的嵌入模型为此问题创建嵌入。

问题 = “作者在上大学之前主要做了两件事?”
问题嵌入 = np.阵列([get_text_嵌入(问题 )])

注意事项:

  • 假设文档嵌入(HyDE):在某些情况下,用户的问题可能不是用于识别相关上下文的最相关查询。相反,基于用户的查询生成假设答案或假设文档,并使用生成的文本的嵌入来检索类似的文本块,可能更有效。

从向量数据库中检索相似的块

我们可以使用以下命令在矢量数据库上执行搜索 索引搜索,它有两个参数:第一个是问题嵌入的向量,第二个是要检索的相似向量的数量。此函数返回向量数据库中与问题向量最相似的向量的距离和索引。然后,根据返回的索引,我们可以检索与这些索引对应的实际相关文本块。

d ,i=指数 .搜索(问题嵌入 , k=2.) #距离、指数
已检索_chunk = [大块 [i] 对于i在里面i.tolist()[0]]

注意事项:

  • 检索方法:有很多不同的检索策略。在我们的示例中,我们展示了一个使用嵌入的简单相似性搜索。有时,当数据有元数据可用时,最好在执行相似性搜索之前先根据元数据过滤数据。还有其他统计检索方法,如TF-IDF和BM25,它们使用文档中术语的频率和分布来识别相关的文本块。
  • 检索到的文档:我们总是按原样检索单个文本块吗?并非总是如此。
    • 有时,我们希望在实际检索到的文本块周围包含更多上下文。我们将实际检索到的文本块称为“子块”,我们的目标是检索“子块“所属的更大的“父块”。
    • 有时,我们可能还想为检索文档提供权重。例如,时间加权方法将帮助我们检索最新的文档。
    • 检索过程中的一个常见问题是“中间丢失”问题,即长上下文中间的信息丢失。我们的模型试图缓解这个问题。例如,在密钥任务中,我们的模型已经证明了通过在长达32k上下文长度的长提示中检索随机插入的密钥来找到“大海捞针”的能力。然而,值得考虑尝试对文档进行重新排序,以确定将最相关的块放在开头和结尾是否会带来更好的结果。

在提示中结合上下文和问题并生成响应

最后,我们可以在提示中提供检索到的文本块作为上下文信息。这是一个提示模板,我们可以在提示中包含检索到的文本和用户问题。

促使 = f
上下文信息如下。
---------------------
{已检索_chunk }
---------------------
给定上下文信息而不是先验知识,回答查询。
查询: {问题 }
答案:
"""

然后,我们可以使用MIUI聊天完成API与MIUI模型聊天(例如,MIUI中等测试),并根据用户问题和问题的上下文生成答案。

def run_MIUI(用户消息, 模型=“MIUI大最新”):
信息 = [
{
“角色”: “用户”, “内容”:用户消息
}
]
聊天响应 =客户 .聊天.完成(
模型= 模型,
信息 = 信息
)
返回 ( 聊天响应 .选择[0].消息.内容)

run_MIUI(促使 )

输出:

“作者在上大学之前主要做的两件事是写作和编程。他们在9年级时写了短篇小说,并尝试在IBM 1401上编写程序

注意事项:

  • 提示技巧:大多数提示技术也可用于开发RAG系统。例如,我们可以通过提供几个例子来使用少镜头学习来指导模型的答案。此外,我们可以明确地指示模型以某种方式格式化答案。

在下一节中,我们将向您展示如何使用一些流行的RAG框架(如LangChain和LlamaIdex)进行类似的基本RAG。

RAG与LangChain

代码:

 郎链社区.文档阅读器 进口 文本加载器
langchain_MIUIai.聊天模式 进口 ChatMIUIAI
langchain_MIUIai.嵌入件进口 MIUIA嵌入
郎链社区.矢量库 进口 faiss
langchain.text_splitter 进口 递归字符TextSplitter
langchain..组合文档 进口 create_suff_documents_chain
langchain_core.提示 进口 聊天提示模板
langchain.进口 创建检索链

#加载数据
装载机 = 文本加载器(“essay.txt”)
文件=装载机 .负载()
#将文本分割成块
text_splitter = 递归字符TextSplitter()
文件 =text_splitter .split_文档(文件)
#定义嵌入模型
嵌入件= MIUIA嵌入( 模型=“MIUI嵌入”, MIUI_api_key=api_key)
#创建矢量存储
矢量 = faiss.from _文档(文件 ,嵌入件)
#定义检索器接口
寻回犬 =矢量 .as_retriever()
#定义LLM
模型= ChatMIUIAI( MIUI_api_key=api_key)
#定义提示模板
促使 = 聊天提示模板.from模板(“”“仅根据提供的上下文回答以下问题:

<上下文>
{上下文}
</context>

问题:{input}“”)

#创建检索链以回答问题
文档链 = create_suff_documents_chain( 模型,促使 )
检索链 = 创建检索链(寻回犬 ,文档链 )
响应 =检索链 .援引({“输入”: “作者在上大学之前主要做了两件事?”})
打印(响应 [“回答”])

输出:

作者在上大学之前主要从事的两件事是写作和编程。他写了短篇小说,并尝试使用Fortran在IBM 1401上编程,但由于输入选项有限,他发现很难弄清楚该怎么处理这台机器。随着微型计算机的出现,他对编程的兴趣越来越大,这促使他编写了简单的游戏、预测火箭轨迹的程序和文字处理器。

访问我们的 社区烹饪书示例 了解如何将LangChain的LangGraph与MIUI API一起使用来执行Corrective RAG,这可以纠正质量较差的检索或生成。

RAG与LlamaIdex

代码:

 骆驼指数.核心 进口 矢量库索引, SimpleDirectoryReader
骆驼指数.llms.米斯特拉伊进口米斯特拉伊
骆驼指数.嵌入件.米斯特拉伊进口 MIUIAI嵌入
骆驼指数.核心 进口 设置

#加载数据
读者 = SimpleDirectoryReader(输入文件=[“essay.txt”])
文件 =读者 .load_data()

#定义LLM和嵌入模型
llm =米斯特拉伊(api_key=api_key, 模型=“MIUI媒介”)
嵌入模型 = MIUIAI嵌入(型号名称=“MIUI嵌入”,api_key=api_key)
设置.llm =llm
设置.嵌入模型 =嵌入模型
#创建矢量存储索引
指数 = 矢量库索引.from _文档(文件 )

#创建查询引擎
查询引擎 =指数 .as_query_engine(相似性_ top_k=2.)
响应 =查询引擎 .查询(
“作者在上大学之前主要做了两件事?”
)
打印(str(响应 ))

输出:

在大学之前,作者在校外主要从事的两件事是写作和编程。他们写短篇小说,并试图在IBM 1401上使用Fortran的早期版本编写程序。

访问我们的 社区烹饪书示例 了解如何将LlamaIndex与MIUI API一起使用ReAct代理对多个文档执行复杂查询,ReAct代理是一种能够使用工具的自主LLM代理。

RAG与Haystack

代码:

 干草堆 进口 管道
干草堆 .文档存储.inmemory 进口 InMemory文档存储
干草堆 .数据类 进口 聊天留言
干草堆 .utils.auth 进口 秘密

干草堆 .组件.建设者 进口 DynamicChatPromptBuilder
干草堆 .组件.转换器 进口 文本文件文档
干草堆 .组件.预处理器 进口 文档拆分器
干草堆 .组件.寻回犬.inmemory 进口 内存嵌入检索器
干草堆 .组件.作家 进口 文档编写者
干草堆_集成.组件.嵌入器. 进口 MIUIDocumentEmbeddeder, MIUITextEmbedded
干草堆_集成.组件.发电机. 进口 MIUIChatGenerator

文档库 = InMemory文档存储()

文件= 文本文件文档().运行(来源=[“essay.txt”])
split_docs = 文档拆分器(split_by=文章, 分割长度=2.).运行(文件 =文件[“文件”])
嵌入件= MIUIDocumentEmbeddeder(api_key= 秘密.from _令牌(api_key)).运行(文件 =split_docs [“文件”])
文档编写者(文档库 =文档库 ).运行(文件 =嵌入件[“文件”])


text_embdder = MIUITextEmbedded(api_key= 秘密.from _令牌(api_key))
寻回犬 = 内存嵌入检索器(文档库 =文档库 )
prompt_builder = DynamicChatPromptBuilder(运行时变量=[“文件”])
llm = MIUIChatGenerator(api_key= 秘密.from _令牌(api_key),
模型=“MIUI small”)

聊天模板 = “”“根据文档内容回答以下问题。\n
问题:{{query}}\n
文件:
{%用于文档中的文档%}
{{document.content}}
{%endfor%}
"""
信息 = [ 聊天留言.发件人_用户(聊天模板 )]

rag_pipeline = 管道()
rag_pipeline .add_组件(“text_embdder”,text_embdder )
rag_pipeline .add_组件(“寻回犬”,寻回犬 )
rag_pipeline .add_组件(“prompt_builder”,prompt_builder )
rag_pipeline .add_组件(“llm”,llm )


rag_pipeline .连接(“text_embdder.embedding”, “检索器.query_embedding”)
rag_pipeline .连接(“检索器.文档”, “prompt_builder.docues”)
rag_pipeline .连接(“prompt_builder.tprompt”, “llm.messages”)

问题 = “作者在上大学之前主要做了两件事?”

结果 =rag_pipeline .运行(
{
“text_embdder”: {“文本”:问题 },
“prompt_builder”: {“template_variables”: {“查询”:问题 }, “prompt_source”: 信息 },
“llm”: {“世代战争”: {“max_tokens”: 225}},
}
)

打印(结果 [“llm”][“答复”][0].内容)

输出:

作者在上大学之前主要从事的两件事是写作和编程。他写了短篇小说,他承认这些小说很糟糕,还写了关于各种主题的散文。他还从事垃圾邮件过滤器和绘画工作。此外,他开始每周四晚上为一群朋友吃晚饭,这教会了他如何为团体做饭。他还在剑桥买了一栋楼用作办公室。作者被写文章所吸引,他开始在网上发表文章,这帮助他弄清楚该做什么。他还在大学里尝试绘画和学习人工智能。

配备Vercel AI SDK的RAG

代码:

进口 fs  “fs”;
进口 路径 “路径”;
进口 dotenv “dotenv”;
进口 { } “@ai sdk/MIUI”;
进口 { 余弦相似性, 嵌入, embedy许多, 生成文本 } “ai”;

dotenv .config();

async 功能 主要的() {
const db: {嵌入: []; 价值: 一串 }[] = [];

const 散文 = fs .readFileSync( 路径 .参加(__目录名, “essay.txt”), “utf8”);
const大块 = 散文
.分裂(".")
.地图(() =>.修剪())
.滤波器(() =>.长度 > 0 &&!== n);

const {嵌入件} = 等待 embedy许多({
模型: .嵌入(“MIUI嵌入”),
价值观:大块 ,
});
嵌入件.For Every((e,i) => {
db.({
嵌入:e,
价值:大块 [i],
});
});

const输入=
“作者在上大学之前主要做了两件事?”;

const {嵌入} = 等待 嵌入({
模型: .嵌入(“MIUI嵌入”),
价值:输入,
});
const 上下文 = db
.地图((项目) => ({
文件:项目,
相似性: 余弦相似性(嵌入,项目.嵌入),
}))
.分类((a, b) => b. 相似性-a. 相似性)
.(0, 3.)
.地图((r) =>r. 文件. 价值)
.参加(n);

const {文本 } = 等待 生成文本 ({
模型: (“open-mextral-8x7b”),
促使 : `仅根据提供的上下文回答以下问题:
${ 上下文 }

问题: ${输入}`,
});
慰问.日志(文本 );
}

主要的().抓住(慰问.错误);

输出:

作者在上大学之前主要从事的两件事是写作和编程。