RAG(Retrieval-Augmented-Generation)
RAG(检索增强生成),顾名思义就是通过检索某个外部知识库来增强生成文本内容的能力;
RAG 的工作原理可以概括为几个步骤。
- 检索:对于给定的输入(问题),模型首先使用检索系统从大型文档集合中查找相关的文档或段落。这个检索系统通常基于密集向量搜索,例如ChromaDB、Faiss这样的向量数据库。
- 上下文编码:找到相关的文档或段落后,模型将它们与原始输入(问题)一起编码。
- 生成:使用编码的上下文信息,模型生成输出(答案)。这通常当然是通过大模型完成的。
RAG的具体过程
文档加载
LangChain 提供了多种类型的文档加载器,以加载各种类型的文档(HTML、PDF、代码)
文本转换
所谓文本转换就是将原来的长文档做一个处理,最常见的就是将这个文档分成一个个的小块以满足大模型提问时窗口大小限制。
文本分割器
理想情况下,我们希望将语义相关的文本块放在一起。
LangChain中,文本分割器的工作原理如下:
- 将文本分成小的、具有语义意义的块(通常是句子)。
- 开始将这些小块组合成一个更大的块,直到达到一定的大小。
- 一旦达到该大小,一个块就形成了,可以开始创建新文本块。这个新文本块和刚刚生成的块要有一些重叠,以保持块之间的上下文。
LangChain提供的文本分割器:
主要有三个参数:文本如何分割、块的大小、块之间重叠文本的长度
实际中的一些考量因素:
-
llm的窗口限制,即上下文所总共能提供的token数量
-
任务类型
- 需要细致查看文本的任务,最好使用较小的分块。例如,拼写检查、语法检查和文本分析可能需要识别文本中的单个单词或字符。垃圾邮件识别、查找剽窃和情感分析类任务,以及搜索引擎优化、主题建模中常用的关键字提取任务也属于这类细致任务。
- 需要全面了解文本的任务,则使用较大的分块。例如,机器翻译、文本摘要和问答任务需要理解文本的整体含义。而自然语言推理、问答和机器翻译需要识别文本中不同部分之间的关系。还有创意写作,都属于这种粗放型的任务。
-
文本的性质
- 如果文本结构很强,如代码或HTML,你可能想使用较大的块,如果文本结构较弱,如小说或新闻文章,你可能想使用较小的块。
其他形式的文本转换
除了对文本的分割外,还有其他处理文档格式或内容的工具:
- 过滤冗余的文档:使用 EmbeddingsRedundantFilter 工具可以识别相似的文档并过滤掉冗余信息。这意味着如果你有多份高度相似或几乎相同的文档,这个功能可以帮助识别并删除这些多余的副本,从而节省存储空间并提高检索效率。
- 翻译文档:通过与工具 doctran 进行集成,可以将文档从一种语言翻译成另一种语言。
- 提取元数据:通过与工具 doctran 进行集成,可以从文档内容中提取关键信息(如日期、作者、关键字等),并将其存储为元数据。元数据是描述文档属性或内容的数据,这有助于更有效地管理、分类和检索文档。
- 转换对话格式:通过与工具 doctran 进行集成,可以将对话式的文档内容转化为问答(Q/A)格式,从而更容易地提取和查询特定的信息或回答。这在处理如访谈、对话或其他交互式内容时非常有用。
文本嵌入
文#本嵌入就是将前面处理好的文本块表示成向量形式,在向量空间中提取出语义信息以便后续更好的去找相似文本。
LangChain中的Embeddings 类是设计用于与文本嵌入模型交互的类。这个类为所有这些提供者提供标准接口。Embeddings_model提供两种方法,一种是文档的嵌入,一种是查询的嵌入。
为什么需要两种方法?虽然看起来这两种方法都是为了文本嵌入,但是LangChain将它们分开了。原因是一些嵌入提供者对于文档和查询使用的是不同的嵌入方法。文档是要被搜索的内容,而查询是实际的搜索请求。这两者可能因为其性质和目的,而需要不同的处理或优化。
embeddings = embeddings_model.embed_documents(
[
"您好,有什么需要帮忙的吗?",
"哦,你好!昨天我订的花几天送达",
"请您提供一些订单号?",
"12345678",
]
)
len(embeddings), len(embeddings[0])
embedded_query = embeddings_model.embed_query("刚才对话中的订单号是多少?")
embedded_query[:3]
存储嵌入
计算嵌入是一个相当费时的过程,为了加快这一过程,我们要提供缓存。
缓存存储
这块没什么好记录的,就是通过一个函数封装,这个函数的参数包括嵌入的计算器、存储的方式、命名空间,通过这样的方式就把这个嵌入存起来了。
不同的缓存策略如下:
- InMemoryStore:在内存中缓存嵌入。主要用于单元测试或原型设计。如果需要长期存储嵌入,请勿使用此缓存。
- LocalFileStore:在本地文件系统中存储嵌入。适用于那些不想依赖外部数据库或存储解决方案的情况。
- RedisStore:在Redis数据库中缓存嵌入。当需要一个高速且可扩展的缓存解决方案时,这是一个很好的选择。
在内存中缓存的示例:
# 导入内存存储库,该库允许我们在RAM中临时存储数据
from langchain.storage import InMemoryStore
# 创建一个InMemoryStore的实例
store = InMemoryStore()
# 导入与嵌入相关的库。OpenAIEmbeddings是用于生成嵌入的工具,而CacheBackedEmbeddings允许我们缓存这些嵌入
from langchain.embeddings import OpenAIEmbeddings, CacheBackedEmbeddings
# 创建一个OpenAIEmbeddings的实例,这将用于实际计算文档的嵌入
underlying_embeddings = OpenAIEmbeddings()
# 创建一个CacheBackedEmbeddings的实例。
# 这将为underlying_embeddings提供缓存功能,嵌入会被存储在上面创建的InMemoryStore中。
# 我们还为缓存指定了一个命名空间,以确保不同的嵌入模型之间不会出现冲突。
embedder = CacheBackedEmbeddings.from_bytes_store(
underlying_embeddings, # 实际生成嵌入的工具
store, # 嵌入的缓存位置
namespace=underlying_embeddings.model # 嵌入缓存的命名空间
)
# 使用embedder为两段文本生成嵌入。
# 结果,即嵌入向量,将被存储在上面定义的内存存储中。
embeddings = embedder.embed_documents(["你好", "智能鲜花客服"])
向量数据库(向量存储)
这种向量数据库有好多,要根据实际情况去选择使用哪种。
- 数据规模和速度需求:考虑你的数据量大小以及查询速度的要求。一些向量数据库在处理大规模数据时更加出色,而另一些在低延迟查询中表现更好。
- 持久性和可靠性:根据你的应用场景,确定你是否需要数据的高可用性、备份和故障转移功能。
- 易用性和社区支持:考虑向量数据库的学习曲线、文档的完整性以及社区的活跃度。
- 成本:考虑总体拥有成本,包括许可、硬件、运营和维护成本。
- 特性:考虑你是否需要特定的功能,例如多模态搜索等。
- 安全性:确保向量数据库符合你的安全和合规要求。
数据检索
向量存储检索器
整个过程也没啥好说的,他都给封装了,整个过程一气呵成,包括文本的分割、存储到的向量数据库、采用的Embedding方式等等,以及内部通过调用QARetrival链配合大模型检索的过程,在外部我们只需调用index.query("玫瑰花的花语是什么?")类似的简单方式,就实现了检索功能。
其他类型的检索器
索引
索引是一种高效地管理和定位文档信息的方法,确保每个文档具有唯一标识并便于检索。索引后的文档具有如下特点:
- 避免重复内容:确保你的向量存储中不会有冗余数据。
- 只更新更改的内容:能检测哪些内容已更新,避免不必要的重写。
- 省时省钱:不对未更改的内容重新计算嵌入,从而减少了计算资源的消耗。
- 优化搜索结果:减少重复和不相关的数据,从而提高搜索的准确性。
LangChain 利用了记录管理器(RecordManager)来跟踪哪些文档已经被写入向量存储。一旦哈希完成,以下信息会被保存在记录管理器中:
- 文档哈希:基于文档内容和元数据计算出的唯一标识。
- 写入时间:记录文档何时被添加到向量存储中。
- 源 ID:这是一个元数据字段,表示文档的原始来源。
这种方法确保了即使文档经历了多次转换或处理,也能够精确地跟踪它的状态和来源,确保文档数据被正确管理和索引。