RAG
概述
RAG全称为Retrieval-Augmented Generation,即检索增强生成,结合了检索和生成的能力,引入外部知识库为文本生成任务提供支持,增强模型生成能力,产生更丰富、准确、有根据的内容。
RAG不仅依赖于训练数据中的信息,还可以利用大型外部知识库进行信息检索,有助于处理训练数据中未出现的问题。
RAG的工作过程:
检索: 从大型文档集合中检索相关文档或段落。
上下文编码: 编码找到的文档或段落以及原始输入。
生成: 利用编码的上下文信息生成输出。
组成
LangChain 提供了RAG应用程序的所有构建模块 - 从简单到复杂。文档的这一部分涵盖了与检索步骤相关的所有内容 - 例如数据的获取。虽然这听起来很简单,但实际上可能非常复杂。这包含几个关键模块。
文档加载器:从许多不同来源加载文档
文档转换器:将大型文档分割(或分块)为较小的块
文本嵌入模型:为文档创建嵌入,嵌入捕捉文本的语义含义,促使能够快速高效地查找其他相似的文本
向量存储:使用嵌入数据库存储文档创建嵌入生成的数据
检索器:通过检索器在数据库中检索相关信息
Document loaders 文档加载
概述
RAG的第一步是文档加载。LangChain的文档加载器可以加载来自多种不同来源的文档,提供了100多种不同的文档加载器选项,并与其他主要提供商(例如AirByte和Unstructed)集成。LangChain的集成功能可以从各种位置(如私有S3存储桶、公共网站)加载各种类型的文档(如HTML、PDF、代码)。
常用文档加载器
加载器 | 描述 |
---|---|
TextLoader | 加载文本文档 |
CSVLoader | 加载CSV文档 |
DirectoryLoader | 加载目录中的所有文档 |
File Directory | 文件目录 |
UnstructuredHTMLLoader | 加载HTML超文本标记语言 |
JSONLoader | 加载JSON文档 |
UnstructuredMarkdownLoader | 加载Markdown文档 |
AzureAIDocumentIntelligenceLoader | 加载 DOCX、XLSX、PPTX |
PyPDFLoader | 加载PDF文档 |
代码示例
简单的加载文件作为文本读入,并将其全部放入一个文档中。
python
from langchain_community.document_loaders import TextLoader
# 导入文档加载器模块,并使用TextLoader来加载文本文件
loader = TextLoader("./index.txt", encoding='utf8')
# 执行加载
res = loader.load()
print(res)
python
[Document(page_content='LangChain的文档加载器可以加载来自多种不同来源的文档,提供了100多种不同的文档加载器选项,并与其他主要提供商集成。', metadata={'source': './index.txt'})]
Text Splitting 文本分割、转换
概述
在加载文档后,下一个步骤是对文本进行转换,LangChain提供了多种内置的文档转换器,最常见的是把长文档分割成适合模型上下文窗口的小块,以便在检索过程中提取相关内容。这些转换器可以轻松拆分、组合、过滤和操作文档,针对不同文档类型(如代码、Markdown等)进行了优化。
文本分割器的工作原理如下:
将文本分成小的、具有语义意义的块(通常是句子)。
开始将这些小块组合成一个更大的块,直到达到一定的大小(通过某些函数测量)。
一旦达到该大小,请将该块设为自己的文本片段,然后开始创建具有一些重叠的新文本块(以保持块之间的上下文)
文本拆分注意事项:
模型Token限制:以GPT-3.5-turbo模型为例,其支持的上下文窗口为4096个令牌,即输入令牌和生成的输出令牌的总和不能超过4096
常用文本分割器
分割器 | 描述 |
---|---|
CharacterTextSplitter | 按字符分割 |
MarkdownHeaderTextSplitter | 标题文本分割器 |
RecursiveCharacterTextSplitter | 按字符递归分割 |
Split by character | 按字符分割 |
代码示例
基于字符(默认为"")进行分割,并通过字符数来测量块长度
python
with open("./index.txt", encoding='utf8') as f:
state_of_the_union = f.read()
from langchain_text_splitters import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
separator=",", # 以,分割
chunk_size=15, # 块大小,字符数
chunk_overlap=5, # 重叠部分,字符数
length_function=len, # 长度计算函数
is_separator_regex=False,
)
texts = text_splitter.create_documents([state_of_the_union])
for text in texts:
print(text)
python
page_content='LangChain的文档加载器可以加载来自多种不同来源的文档'
page_content='提供了100多种不同的文档加载器选项'
page_content='并与其他主要提供商集成。'
Created a chunk of size 30, which is longer than the specified 15
Created a chunk of size 18, which is longer than the specified 15
Text embedding models 文本嵌入模型
概述
为文档创建嵌入是检索的另一个关键部分,嵌入可以快速有效地捕获文本的语义,帮助找到文本的其他相似部分。
文本块形成后,通过LLM进行嵌入(Embeddings)将文本转换为数值表示,让计算机能更轻松处理和比较文本。
LangChain集成了超过25种不同的嵌入提供商和方法,包括开源和专有API,让您选择最适合您需求的方式。LangChain提供标准接口,方便在不同模型之间切换。
OpenAI、Cohere、Hugging Face等平台都提供文本嵌入的模型,通过嵌入可以在向量空间中对文本进行语义搜索,找到最相似的文本片段。
初始化
LangChain中的Embeddings 类是设计用于与文本嵌入模型交互的类。这个类为所有这些提供者提供标准接口。
首先需要安装以下库:
python
pip install langchain-openai
设置OpenAI API密钥
python
import os
os.environ["OPENAI_BASE_URL"] = "https://xx.com/v1"
os.environ["OPENAI_API_KEY"] = "sk-BGFnOL9Q4c99B378B66cT3Bl39b4813bc437B82c2"
初始化Embedding类
python
from langchain_openai import OpenAIEmbeddings
embeddings_model = OpenAIEmbeddings()
提供两个重要方法:
embed_documents :嵌入文本
embed_query :嵌入查询
embed_documents 嵌入文本
为文档创建嵌入,接收多个文本作为输入,可以一次性将多个文档转换为它们的向量表示。
python
embeddings = embeddings_model.embed_documents(
[
"Hi there!",
"Oh, hello!",
"What's your name?",
"My friends call me World",
"Hello World!"
]
)
print(len(embeddings))
print(len(embeddings[0]))
python
5
1536
embed_query 嵌入查询
为查询创建嵌入,只接收一个文本作为输入,通常是用户的搜索查询。
嵌入一段文本是为了与其他嵌入的文本进行比较。
python
embedded_query = embeddings_model.embed_query("What was the name mentioned in the conversation?")
embedded_query[:5]
python
[0.005394653582927107, -0.0006512353067530454, 0.039039471810349266, -0.0029678841334208213, -0.008829414353660532]
缓存存储
概述
在说向量存储之前,先说一下缓存存储。首先,计算嵌入是一个消耗时间大的过程。为了加速嵌入计算过程,可以将计算结果存储或缓存,避免重复计算。
可以使用 CacheBackedEmbeddings 来缓存嵌入。支持缓存的嵌入器是嵌入器的包装器,它将嵌入缓存在键值存储中。对文本进行哈希处理,并将哈希值用作缓存中的密钥。
如何使用
初始化 CacheBackedEmbeddings 的主要支持方式是 from_bytes_store 。它需要以下参数:
arduino
underlying_embedder:用于嵌入的嵌入器
document_embedding_cache:任何用于缓存文档嵌入的 ByteStore
batch_size:(可选,默认为 None )在存储更新之间嵌入的文档数量
namespace:(可选,默认为 "" )用于文档缓存的命名空间。该命名空间用于避免与其他缓存发生冲突。例如,将其设置为所使用的嵌入模型的名称。
不同缓存策略如下:
1.InMemoryStore:在内存中缓存嵌入。主要用于单元测试或原型设计。如果需要长期存储嵌入,请勿使用此缓存
2.LocalFileStore:在本地文件系统中存储嵌入。适用于那些不想依赖外部数据库或存储解决方案的情况
3.RedisStore:在Redis数据库中缓存嵌入。当需要一个高速且可扩展的缓存解决方案时,这是一个很好的选择
本地文件系统存储嵌入
python
from langchain.storage import LocalFileStore
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import CharacterTextSplitter
from langchain.embeddings import CacheBackedEmbeddings
underlying_embeddings = OpenAIEmbeddings()
store = LocalFileStore("./cache/")
cached_embedder = CacheBackedEmbeddings.from_bytes_store(
underlying_embeddings, store, namespace=underlying_embeddings.model
)
# 嵌入之前缓存为空
print(list(store.yield_keys()))
# 加载文档,将其分割成块,嵌入每个块并将其加载到向量存储中。
raw_documents = TextLoader("./index.txt", encoding='utf8').load()
text_splitter = CharacterTextSplitter(chunk_size=15, chunk_overlap=5)
documents = text_splitter.split_documents(raw_documents)
import time
start_time = time.time() # 记录开始时间
# 创建向量存储
db = FAISS.from_documents(documents, cached_embedder)
end_time = time.time() # 记录结束时间
execution_time = end_time - start_time # 计算执行时间
print(f"第一次创建向量存储: {execution_time} 秒")
# 创建向量存储,它会快得多,因为它不需要重新计算任何嵌入。
start_time = time.time() # 记录开始时间
db2 = FAISS.from_documents(documents, cached_embedder)
end_time = time.time() # 记录结束时间
execution_time = end_time - start_time # 计算执行时间
print(f"第二次创建向量存储: {execution_time} 秒")
# 创建的一些嵌入
print(list(store.yield_keys()))
python
[]
第一次创建向量存储: 1.8278753757476807 秒
第二次创建向量存储: 0.0025548934936523438 秒
['text-embedding-ada-0022b03fcec-2e5e-53e7-88ce-eebf249b6d56']
在内存中缓存嵌入
python
# 导入内存存储库,该库允许在RAM中临时存储数据
from langchain.storage import InMemoryStore
# 创建一个InMemoryStore实例
store = InMemoryStore()
# 导入OpenAIEmbeddings,用于生成嵌入的工具
from langchain_openai import OpenAIEmbeddings
# 导入CacheBackedEmbeddings,允许缓存这些嵌入
from langchain.embeddings import CacheBackedEmbeddings
# 创建一个OpenAIEmbeddings的实例,用于实际计算文档的嵌入
underlying_embeddings = OpenAIEmbeddings()
# 创建一个CacheBackedEmbeddings的实例
# 为underlying_embeddings提供缓存功能,嵌入会被存储在InMemoryStore中
# 为缓存指定一个命名空间,以确保不同的嵌入模型之间不会出现冲突
embedder = CacheBackedEmbeddings.from_bytes_store(
underlying_embeddings, # 实际生成嵌入的工具
store, # 嵌入的缓存位置
namespace=underlying_embeddings.model # 嵌入缓存的命名空间
)
# 使用embedder为两段文本生成嵌入
embeddings_result = embedder.embed_documents(
["计算嵌入是一个消耗时间大的过程。", "为了加速嵌入计算过程,可以将计算结果存储或缓存,避免重复计算。"])
# 得到embeddings_result结果,即嵌入向量,将被存储在内存中
# print(embeddings_result)
Vector stores 向量存储
计算嵌入是一个消耗时间大的过程。为了加速嵌入计算过程,可以将计算结果存储或缓存,避免重复计算。
随着嵌入的广泛应用,需要数据库来支持高效存储和检索嵌入。LangChain集成了50多种不同的矢量存储方式,包括开源本地存储和云托管专有存储,方便选择适合需求的方式。LangChain提供标准接口,方便在不同的向量存储之间交换。
存储和搜索非结构化数据的最常见方法之一是嵌入它并存储生成的嵌入向量,然后在查询时嵌入非结构化查询并检索与嵌入查询"最相似"的嵌入向量。矢量存储负责存储嵌入数据并为您执行矢量搜索。
向量数据库(Vector Store)是常见的存储向量的方式,LangChain支持多种向量数据库,包括开源和商业产品,如Elasticsearch、Faiss、Chroma和Qdrant等。
常用向量数据库
向量数据库各有特点,适用于不同的应用场景。选择合适的数据库可以提高向量检索的效率和效果。
数据库名称 | 描述 | 特点 |
---|---|---|
FAISS | Facebook AI Similarity Search,专为高效相似性搜索设计的库。 | 支持大规模向量检索,快速,支持多种索引类型。 |
Annoy | Approximate Nearest Neighbors Oh Yeah,适用于高维数据的近似最近邻搜索。 | 内存友好,支持多种距离度量,适合大规模数据集。 |
Milvus | 开源向量数据库,支持高效的向量检索和管理。 | 支持多种索引类型,易于扩展,适合实时应用。 |
Pinecone | 云原生向量数据库,专注于机器学习和AI应用。 | 高可用性,自动扩展,支持多种数据类型和查询方式。 |
Weaviate | 开源向量搜索引擎,支持图形和向量数据的结合。 | 支持GraphQL查询,易于集成,适合语义搜索。 |
Chroma | 轻量级的向量数据库,专为机器学习应用设计。 | 简单易用,支持多种数据格式,适合小型项目和快速原型开发。 |
Qdrant | 高性能的向量搜索引擎,支持实时数据更新。 | 支持过滤和聚合查询,适合动态数据场景。 |
chroma向量数据库
使用chroma向量数据库,它作为库在本地计算机上运行。
python
pip install langchain-chroma
以下是一个代码示例:
python
from langchain_community.document_loaders import TextLoader
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import CharacterTextSplitter
from langchain_chroma import Chroma
# 加载文档
raw_documents = TextLoader('./index.txt', encoding='utf-8').load()
# 将其拆分为块
text_splitter = CharacterTextSplitter(chunk_size=15, chunk_overlap=0)
documents = text_splitter.split_documents(raw_documents)
# 嵌入每个块并将其加载到矢量存储中
db = Chroma.from_documents(documents, OpenAIEmbeddings())
# 相似性搜索
query = "文档加载器"
docs = db.similarity_search(query)
print(docs[0].page_content)
# 接受嵌入向量作为参数而不是字符串
embedding_vector = OpenAIEmbeddings().embed_query(query)
# 通过向量进行相似性搜索 搜索与给定嵌入向量类似的文档
docs = db.similarity_search_by_vector(embedding_vector)
print(docs[0].page_content)
python
LangChain的文档加载器可以加载来自多种不同来源的文档,提供了100多种不同的文档加载器选项,并与其他主要提供商集成。
LangChain的文档加载器可以加载来自多种不同来源的文档,提供了100多种不同的文档加载器选项,并与其他主要提供商集成。
异步操作
向量存储通常作为一个需要进行IO操作的独立服务来运行,因此它们可能会被异步调用。这样可以提供性能优势,因为不需要等待外部服务的响应。在使用异步框架(如FastAPI)时,这一点也很重要。
LangChain支持向量存储的异步操作。所有方法都可以使用其异步对应方法进行调用,前缀为a ,意思是async 。
Qdrant是一个向量存储,它支持所有异步操作
python
pip install qdrant-client
代码示例:
python
# 导入Qdrant
from langchain.vectorstores import Qdrant
# 异步创建向量存储
db = await Qdrant.afrom_documents(documents, embeddings, "http://localhost:6333")
query = "文档加载器"
# 相似度搜索
docs = await db.asimilarity_search(query)
print(docs[0].page_content)
# 通过向量进行相似度搜索
embedding_vector = embeddings.embed_query(query)
docs = await db.asimilarity_search_by_vector(embedding_vector)
# 最大边际相关搜索 (MMR)
found_docs = await qdrant.amax_marginal_relevance_search(query, k=2, fetch_k=10)
for i, doc in enumerate(found_docs):
print(f"{i + 1}.", doc.page_content, "\n")
检索器
概述
在LangChain中,Retriever(检索器)是数据检索模块的核心入口,通过非结构化查询返回相关文档。
检索器是一个接口,根据非结构化查询返回文档,比矢量存储更通用。检索器不需要存储文档,只需返回(或检索)它们。矢量存储可用作检索器的骨干,但也有其他类型的检索器。
检索器接受字符串查询作为输入,并返回 Document 列表作为输出。
各种类型的检索器
检索器各有特点,适用于不同的应用场景,可以根据具体需求选择合适的检索器。
检索器名称 | 描述 |
---|---|
VectorStores | 向量存储支持的检索器。 |
MultiQueryRetriever | 多查询检索器,支持同时处理多个查询。 |
ContextualCompressionRetriever | 上下文压缩检索器,优化上下文信息以提高检索效率。 |
BaseRetriever | 自定义检索器,允许用户根据需求实现特定的检索逻辑。 |
EnsembleRetriever | 集成检索器,结合多个检索器的结果以提高准确性。 |
Long-Context Reorder | 长上下文重新排序,优化长文本的检索顺序。 |
MultiVectorRetriever | 多向量检索器,支持同时处理多个向量进行检索。 |
ParentDocumentRetriever | 父文档检索器,检索与子文档相关的父文档。 |
SelfQueryRetriever | 自查询检索器,允许系统根据自身信息进行检索。 |
TimeWeightedVectorStoreRetriever | 时间加权向量存储检索器,考虑时间因素进行检索。 |
向量存储检索器
创建一个端到端的数据检索功能,使用VectorstoreIndexCreator来创建索引,然后在索引的query方法中,通过vectorstore类的as_retriever方法,将向量数据库直接作为检索器,完成检索任务。
python
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import CharacterTextSplitter
# 使用TextLoader加载文本文件
loader = TextLoader('./index.txt', encoding='utf8')
documents = loader.load()
# 文档分割
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_documents(documents)
# 存储嵌入
embeddings = OpenAIEmbeddings()
db = FAISS.from_documents(texts, embeddings)
# 创建向量存储检索器
retriever = db.as_retriever()
# 执行检索
result = retriever.get_relevant_documents("文档加载器")
# 打印查询结果
print(result)
Indexing 索引
概述
LangChain中的索引(Index)是一种高效地管理和定位文档信息的方法,确保每个文档具有唯一标识并便于检索。
索引 API 允许将任何来源的文档加载到矢量存储中并保持同步。它有助于:
在向量存储中避免冗余数据,确保数据不重复
只更新已更改的内容,避免不必要的重写
节省时间和金钱,避免重新计算未更改的内容嵌入,减少计算资源消耗
优化搜索结果,降低重复和不相关数据,提高搜索准确性
执行原理
LangChain索引利用记录管理器 ( RecordManager ) 来跟踪文档写入向量存储的情况。
在索引过程中,API对每个文档进行哈希处理,保证每个文档都有唯一标识。哈希值考虑了文档内容和元数据。记录管理器保存以下信息:
文档哈希:基于文档内容和元数据计算的唯一标识
写入时间:记录文档添加到向量存储中的时间
源 ID:表示文档的原始来源的元数据字段
这种方法能够准确追踪文档的状态和来源,保证文档数据正确管理和索引,即使文档经历多次转换或处理。
代码示例
python
from langchain.indexes import SQLRecordManager, index
from langchain_core.documents import Document
from langchain_elasticsearch import ElasticsearchStore
from langchain_openai import OpenAIEmbeddings
# 初始化向量存储并设置嵌入
collection_name = "test_index"
embedding = OpenAIEmbeddings()
vectorstore = ElasticsearchStore(
es_url="http://localhost:9200", index_name="test_index", embedding=embedding
)
# 使用适当的命名空间初始化记录管理器。
namespace = f"elasticsearch/{collection_name}"
record_manager = SQLRecordManager(
namespace, db_url="sqlite:///record_manager_cache.sql"
)
# 在使用记录管理器之前创建架构。
record_manager.create_schema()
# 索引一些测试文档
doc1 = Document(page_content="kitty", metadata={"source": "kitty.txt"})
doc2 = Document(page_content="doggy", metadata={"source": "doggy.txt"})
# 索引到空向量存储中
def _clear():
"""Hacky helper method to clear content. See the `full` mode section to to understand why it works."""
index([], record_manager, vectorstore, cleanup="full", source_id_key="source")