基础RAG
检索增强生成(RAG)是一种人工智能框架,可以协同LLM和信息检索系统的能力。利用外部知识回答问题或生成内容很有用。RAG有两个主要步骤:1)检索:从存储在向量存储中的文本嵌入的知识库中检索相关信息;2) generation:在提示中插入相关信息,以便LLM生成信息。在本指南中,我们将通过一个非常基本的RAG示例,介绍五种实现方式:
- RAG与MIUI从头开始
- RAG与MIUI和LangChain
- RAG与MIUI和LlamaIdex
- RAG与和海斯塔克
- 配备MIUI和Vercel AI SDK的RAG
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”),
促使 : `仅根据提供的上下文回答以下问题:
${ 上下文 }
问题: ${输入}`,
});
慰问.日志(文本 );
}
主要的().抓住(慰问.错误);
输出:
作者在上大学之前主要从事的两件事是写作和编程。