LangChain 六大组件(五)| 豆包MarsCode AI 刷题

80 阅读9分钟

检索

RAG

RAG(Retrieval Augmented Generation)指的是检索增强生成,它是一种结合了检索(Retrieval)和生成(Generation)两种技术的自然语言处理(NLP)方法。RAG通过从大型文档集合中检索与输入查询相关的信息,并将这些信息作为上下文输入到生成模型中,以生成更准确、更丰富的回答或内容。

具体来说,RAG的工作原理分为以下几个步骤:

  1. 检索:使用检索系统(如搜索引擎或数据库)找到与输入查询相关的文档或信息片段。这一步骤的关键在于能够高效地找到与查询最相关的信息。
  2. 编码:将检索到的信息编码成模型可以理解的格式。这通常涉及到将文本信息转换为向量表示,以便在后续的生成过程中使用。
  3. 生成:使用编码后的信息作为生成模型的输入,生成流畅且信息丰富的文本。这一步骤依赖于生成模型(如GPT等)的能力,将检索到的信息融合到生成的文本中。

RAG 的一个关键特点是,它不仅仅依赖于训练数据中的信息,还可以从大型外部知识库中检索信息。这使得RAG模型特别适合处理在训练数据中未出现的问题。

image.png

文档加载

RAG的第一步是文档加载。LangChain 提供了多种类型的文档加载器,以加载各种类型的文档(HTML、PDF、代码),并与该领域的其他主要提供商如 Airbyte 和 Unstructured.IO 进行了集成。

以下是一些常用的文档加载器列表

image.png

文本转换

加载文档后,下一个步骤是对文本进行转换,而最常见的文本转换就是把长文档分割成更小的块(或者是片,或者是节点),以适合模型的上下文窗口。LangChain 有许多内置的文档转换器,可以轻松地拆分、组合、过滤和以其他方式操作文档。

文本分割器

把长文本分割成块听起来很简单,其实也存在一些细节。文本分割的质量会影响检索的结果质量。理想情况下,我们希望将语义相关的文本片段保留在一起。

LangChain中,文本分割器的工作原理如下:

  1. 将文本分成小的、具有语义意义的块(通常是句子)。
  2. 开始将这些小块组合成一个更大的块,直到达到一定的大小。
  3. 一旦达到该大小,一个块就形成了,可以开始创建新文本块。这个新文本块和刚刚生成的块要有一些重叠,以保持块之间的上下文。

下面是一些LangChain的文本分割器的说明和示例:

image.png

其他形式的文本转换

除拆分文本之外,LangChain中还集成了各种工具对文档执行的其他类型的转换。下面让我们对其进行逐点分析。

  1. 过滤冗余的文档:使用 EmbeddingsRedundantFilter 工具可以识别相似的文档并过滤掉冗余信息。
  2. 翻译文档:通过与工具 doctran 进行集成,可以将文档从一种语言翻译成另一种语言。
  3. 提取元数据:通过与工具 doctran 进行集成,可以从文档内容中提取关键信息(如日期、作者、关键字等),并将其存储为元数据。
  4. 转换对话格式:通过与工具 doctran 进行集成,可以将对话式的文档内容转化为问答(Q/A)格式,从而更容易地提取和查询特定的信息或回答。常用于处理如访谈、对话或其他交互式内容。

文本嵌入

生成文本块之后,我们通过LLM来做嵌入(Embedding),将文本转换为数值(n维向量的形式),可以使得计算机更容易处理和比较文本。

image.png

LangChain中的Embeddings 类是设计用于与文本嵌入模型交互的类。这个类为所有这些提供者提供标准接口。

# 初始化Embeddingfrom langchain.embeddings import OpenAIEmbeddings
embeddings_model = OpenAIEmbeddings()

它提供两种方法:

  1. 第一种是 embed_documents 方法,为文档创建嵌入。这个方法接收多个文本作为输入,意味着你可以一次性将多个文档转换为它们的向量表示。
  2. 第二种是 embed_query 方法,为查询创建嵌入。这个方法只接收一个文本作为输入,通常是用户的搜索查询。

为什么需要两种方法?虽然看起来这两种方法都是为了文本嵌入,但是LangChain将它们分开了。原因是一些嵌入提供者对于文档和查询使用的是不同的嵌入方法。文档是要被搜索的内容,而查询是实际的搜索请求。这两者可能因为其性质和目的,而需要不同的处理或优化。

embed_documents 方法的示例代码如下:

embeddings = embeddings_model.embed_documents(
    [
        "您好,有什么需要帮忙的吗?",
        "哦,你好!昨天我订的花几天送达",
        "请您提供一些订单号?",
        "12345678",
    ]
)
len(embeddings), len(embeddings[0])

输出:

(4, 1536)

embed_query 方法的示例代码如下:

embedded_query = embeddings_model.embed_query("刚才对话中的订单号是多少?")
embedded_query[:3]

输出:

[-0.0029746221837547455, -0.007710168602107487, 0.00923260021751183]

存储嵌入

由于文本嵌入将会花费大量的时间,所以为了减少时间的浪费,我们可以将计算出的嵌入存储或临时缓存,这样在下次需要它们时,就可以直接读取,无需重新计算。

缓存存储

CacheBackedEmbeddings是一个支持缓存的嵌入式包装器,它可以将嵌入缓存在键值存储中。具体操作是:对文本进行哈希处理,并将此哈希值用作缓存的键。

要初始化一个CacheBackedEmbeddings,主要的方式是使用from_bytes_store。其需要以下参数:

  • underlying_embedder:实际计算嵌入的嵌入器。
  • document_embedding_cache:用于存储文档嵌入的缓存。
  • namespace(可选):用于文档缓存的命名空间,避免与其他缓存发生冲突。

不同的缓存策略如下:

  1. InMemoryStore:在内存中缓存嵌入。主要用于单元测试或原型设计。如果需要长期存储嵌入,请勿使用此缓存。
  2. LocalFileStore:在本地文件系统中存储嵌入。适用于那些不想依赖外部数据库或存储解决方案的情况。
  3. 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(["你好", "智能鲜花客服"])

其他两种缓存嵌入就不展示了,详情可以看LangChain官网:

LocalFileStore:LocalFileStore — 🦜🔗 LangChain documentation

RedisStore:RedisStore — 🦜🔗 LangChain documentation

向量数据库(向量存储)

向量数据库有这么多,具体选择哪个,应该根据具体需求进行选型

image.png

数据检索

在LangChain中,Retriever,也就是检索器,是数据检索模块的核心入口,它通过非结构化查询返回相关的文档。

向量存储检索器

向量存储检索器是最常见的,它主要支持向量检索。当然LangChain也有支持其他类型存储格式的检索器。

下面实现一个端到端的数据检索功能,我们通过VectorstoreIndexCreator来创建索引,并在索引的query方法中,通过vectorstore类的as_retriever方法,把向量数据库(Vector Store)直接作为检索器,来完成检索任务。

# 设置OpenAIAPI密钥
import os
os.environ["OPENAI_API_KEY"] = 'Your OpenAI Key'

# 导入文档加载器模块,并使用TextLoader来加载文本文件
from langchain.document_loaders import TextLoader
# 当然,这个文档可以根据你自己需要修改。
loader = TextLoader('LangChainSamples/OneFlower/易速鲜花花语大全.txt', encoding='utf8')

# 使用VectorstoreIndexCreator来从加载器创建索引
from langchain.indexes import VectorstoreIndexCreator
index = VectorstoreIndexCreator().from_loaders([loader])

# 定义查询字符串, 使用创建的索引执行查询
query = "玫瑰花的花语是什么?"
result = index.query(query)
print(result) # 打印查询结果

输出:

玫瑰花的花语是爱情、热情、美丽。

各种类型的检索器

除向量存储检索器之外,LangChain中还提供很多种其他的检索工具。

索引

简单的说,索引是一种高效地管理和定位文档信息的方法,确保每个文档具有唯一标识并便于检索。

它的优势有:

  • 避免重复内容:确保你的向量存储中不会有冗余数据。
  • 只更新更改的内容:能检测哪些内容已更新,避免不必要的重写。
  • 省时省钱:不对未更改的内容重新计算嵌入,从而减少了计算资源的消耗。
  • 优化搜索结果:减少重复和不相关的数据,从而提高搜索的准确性。

LangChain 利用了记录管理器(RecordManager)来跟踪哪些文档已经被写入向量存储。

在进行索引时,API 会对每个文档进行哈希处理,确保每个文档都有一个唯一的标识。这个哈希值不仅仅基于文档的内容,还考虑了文档的元数据。

一旦哈希完成,以下信息会被保存在记录管理器中:

  • 文档哈希:基于文档内容和元数据计算出的唯一标识。
  • 写入时间:记录文档何时被添加到向量存储中。
  • 源 ID:这是一个元数据字段,表示文档的原始来源。

这种方法确保了即使文档经历了多次转换或处理,也能够精确地跟踪它的状态和来源,确保文档数据被正确管理和索引。

好了,就学到这里吧。