没有检索模型构建 RAGs 是一个严重的错误

📅 发布时间:2026/7/12 21:04:48 👁️ 浏览次数:
没有检索模型构建 RAGs 是一个严重的错误
原文towardsdatascience.com/multi-rep-colbert-retrieval-models-for-rags-fe05381b8819我构建 RAG 应用这很有趣但我构建的应用在生产中表现不佳。它们是很有前途的原型但从未真正上线罪魁祸首几乎总是检索。来吧这是 RAGs 的核心。没有这个我们 supposed to build 什么直到我为更快或更好的检索索引文档。索引帮助我们设计出更快的检索数据解决方案。它显著降低了延迟提高了整体应用体验。我们在我们构建的几乎每个应用中都使用索引。这与 LLMs 或 RAGs 无关。几乎所有的数据库都配备了索引支持。例如Postgres可以执行B-Tree, GiST, SP-GiST, BRIN, GIN 和哈希类型的索引。这是一个足够长的列表可以单独写一篇未来的文章。在这篇文章中我将讨论我经常用于更好的文档检索的流行索引策略。然而这些技术是特定于 RAG 应用的。你很快就会明白为什么。我的两个首选索引技术是多重表示和 ColBERT。这些不是我们拥有的唯一方法。而且它是在 RAGs 中的一个不断发展的子领域。如果有很多为什么我更喜欢这两个而不是其他任何东西有两个主要原因。它们既容易理解又容易实现但它们在大多数情况下也表现良好你也会看到与没有索引的系统相比索引提高了速度并有助于检索更多相关文档特别是 ColBERT。快速地说——在继续之前你应该知道如何将大型文档切分成较小的片段以进行嵌入和检索。每当我说文档时我指的是这些块。像往常一样我们有多种切分文档的方法。作为工程师你的任务是选择最适合你情况的方法。我在我的前几篇文章中讨论了这些。花点时间检查一下基于位置的切分与语义块化和代理块化。我甚至尝试了将聚类作为代理块化的廉价且快速的替代方案。多重表示更快且相对准确名字本身就说明了问题。ryte我们存储我们文档的两个版本——一个用于索引。但另一个是我们检索的实际文档。简单一旦我们切分了我们的文档我们就创建了一个优化的检索版本。这通常将是原始块的摘要其中应包含所有关键信息。我们将优化版本存储在向量存储中将原始文档块存储在文档存储中。我们用相同的键将它们绑定在一起以追踪哪个优化版本对应哪个文档。现在我们像往常一样检索。我们要求向量存储从向量空间中检索到与我们的文本输入更接近的文档。但在检索过程中我们使用之前设置的键来检索原始文档而不是优化版本。下面是一个代码示例。# Multi-Representation Indexing Tutorial## 1\. Import required libraries and load environment variablesimportuuidfromdotenvimportload_dotenvfromlangchain_community.document_loadersimportWebBaseLoaderfromlangchain_text_splittersimportRecursiveCharacterTextSplitterfromlangchain_core.promptsimportChatPromptTemplatefromlangchain_openaiimportChatOpenAI,OpenAIEmbeddingsfromlangchain_core.output_parsersimportStrOutputParserfromlangchain.storageimportInMemoryByteStorefromlangchain.vectorstoresimportChromafromlangchain.retrievers.multi_vectorimportMultiVectorRetrieverfromlangchain_core.documentsimportDocument# Load environment variables (e.g., API keys)load_dotenv()## 2\. Load and preprocess the document# Load the document from a web page. We use Djangos security page as an exampleloaderWebBaseLoader(https://docs.djangoproject.com/en/5.1/topics/security/)docsloader.load()# Split the document into smaller chunkstext_splitterRecursiveCharacterTextSplitter(chunk_size500,chunk_overlap100)chunkstext_splitter.split_documents(docs)## 3\. Generate optimized summaries for each chunk# Set up the language model and promptmodelChatOpenAI()template Generate a concise and coherent summary of the following document, ensuring that all key details, important concepts, and relevant information are preserved. Highlight the main points and conclusions while maintaining clarity and context. {document} promptChatPromptTemplate.from_template(template)output_parserStrOutputParser()# Create a chain for summarizationchainprompt|model|output_parser# Generate optimized summaries for each chunkoptimized_chunkschain.batch(chunks)## 4\. Set up the multi-vector retriever# Initialize vector store and document storevectorstoreChroma(embedding_functionOpenAIEmbeddings())documentstoreInMemoryByteStore()# Generate unique keys for each chunkkeyunique_idkeys[str(uuid.uuid4())for_inrange(len(optimized_chunks))]# Create Document objects with metadatadocs[Document(page_contentchunk,metadata{key:keys[ix]})forix,chunkinenumerate(optimized_chunks)]# Set up the multi-vector retrieverretrieverMultiVectorRetriever(vectorstorevectorstore,byte_storedocumentstore,id_keykey,)## 5\. Add documents to the retriever# Add documents to the vector storeretriever.vectorstore.add_documents(docs)# Add documents to the document storeretriever.docstore.mset(list(zip(keys,[Document(page_contentd)fordinoptimized_chunks])))## 6\. Retrieve relevant documents# Example queryqueryHow to restrict access to certain hosts?relevant_docsretriever.get_relevant_documents(query)# Print the retrieved documentsfordocinrelevant_docs:print(fContent:{doc.page_content}n)print(fMetadata:{doc.metadata}n)print(---n)如果我们忽略库加载部分涉及五个关键步骤。让我们详细看看它们。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/b85c5dbbab54c9c783fa56ae83ebae8b.png多表示——图像由作者提供首先我们加载我们的来源。然后我们分割它们。我在这里使用了递归字符分割但我不喜欢基于位置的文档块分割。我之所以在这里使用它是为了简化。然后我们创建一个 LLM 链来总结我们的原始文档块。我们创建的摘要应该包含所有必要细节。这是多表示索引的关键部分。尽管我给出了简短的提示但你应该在这里花大部分时间。更好的提示会产生更好的摘要。但它们也会让你检索到最佳文档。注意我们使用批量函数一次性将所有文档块转换为摘要。这对于大型数据集来说非常有用。在步骤 4 和 5 中我们创建了一个 MultiVectorRetriever。本质上这是一个多表示索引过程。你创建一个了解你的向量存储和文档存储以及优化后的向量版本如何与文档存储中的文档相关联的检索器。这就是文档键。现在我们已经准备好运行查询并检索文档。以下是我们的输出。Content:Django now requires setting ALLOWED_HOSTS explicitly to enhance security,even when using a virtual hostwithServerName configured.This change prevents potential security vulnerabilitiesfromfake Host headersinHTTP requests.Itisrecommended to update Django configurations accordingly to mitigate risks effectively.Source:https://docs.djangoproject.com/en/5.1/topics/security/Metadata:{}---Metadata:{}---这是网站上的原始文档块。然而我们只在向量存储中有用于检索的优化版本。它是如何获取更多相关文档的你可能会问“好吧这可能提高了性能但它如何帮助我们检索到更多相关文档”下面是如何做到这一点。大部分文本主要包含噪声。这些与主要观点的相关性较低。余弦相似度或其他基于距离的相似度搜索在这种噪声中会出错。通过仅在文档块中包含必要的细节我们大大减少了噪声。现在相似度搜索只需要担心“相似度”而不是噪声。ColBERT当准确性比速度更重要ColBERT 背后的想法很复杂——至少对我来说是这样——但并不复杂难以理解。但这里需要不同的方法。在常规检索中文档块和查询被嵌入到向量形式。然后执行相似度搜索以找到最近的邻居。这种方法很简单——但有一个问题。文档通常相对较大。查询则不然。这种比较并不是苹果对苹果。ColBERT 以稍微不同的方式处理它。下面是一个说明它的图表。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/d5de53aa734cb78378c6205bd608a392.pngColBERT 为 RAG 索引 - 图片由作者提供我们逐个处理块并对其进行标记化。我们对搜索查询也做同样的处理。然后我们计算搜索查询中每个标记与块中标记之间的相似度。然后我们取最大值并记录下来。我们为搜索查询中的每个标记执行此过程。现在你得到了一个最大相似度分数数组。这个数组中的每个数字代表一个块中的标记。你计算这个数组的总和。现在你得到了一个块的单个分数。好吧我们需要为向量存储中的所有块执行此操作。现在我们有一个相似度值数组。这次数组中的每个数字代表一个文档。最后你可以比较你为所有块计算出的值以找到最相似的块。这是一项大量工作。我知道你在想什么。如果它很复杂为什么你还要选择它作为你的首选信用归功于一个名为RAGatouille不是Ratatouille 的库这使得 ColBERT 的实现变得轻而易举。下面是如何做的。让我们先使用 Pypi 安装 RAGatouille。!pip install ragatouille如果你是在本地计算机上执行你需要下载许多文件并等待很长时间。如果你是在 Colab 笔记本或云资源上执行你很幸运。我们需要导入 ColBERT 的预训练模型。以下行将完成此操作。fromragatouilleimportRAGPretrainedModel RAGRAGPretrainedModel.from_pretrained(colbert-ir/colbertv2.0)https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/07cf15a8a6c9b186476295381b9a6385.png图片由作者提供我们现在准备好使用 ColBERT 模型进行索引和检索。但让我们先获取并准备我们的文本。我们将使用相同的Django 安全文档来完成此目的。importrequestsfrombs4importBeautifulSoup,SoupStrainer urlhttps://docs.djangoproject.com/en/5.1/topics/security/only_contentSoupStrainer(iddocs-content)soupBeautifulSoup(requests.get(url).content,html.parser,parse_onlyonly_content)full_documentsoup.get_text(separatorn,stripTrue)full_documenthttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/0843bba92e5b75dbe3016faed7e2a391.png图片由作者提供这里是神奇的部分。使用 RAGatouille 进行索引只需一行。检索是另一回事。RAG.index(collection[full_document],index_nameDjagno-security,max_document_length180,split_documentsTrue,)这一步将花费很多时间。ColBERT 在速度上并不出色。我们稍后会谈到这一点。这里要说明的是现在实现起来非常简单。它比其他方法简单。这里是如何进行检索步骤的。resultsRAG.search(queryHow to restrict access based on host names?,k3)resultshttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/635720219378cae2d1a93bdae574042a.png图片由作者提供第一次运行查询时它将花费很多时间。这是因为 RAGatouille 同时检索和索引文档。不要惊慌你的后续查询将花费很少的时间你将获得具有合理低延迟的更准确的文档块。ColBERT 的一个大问题我对 ColBERT 的爱和恨一样深。尽管 ColBERT 在检索相关信息方面非常出色但它具有高计算成本和索引时间。如果你已经阅读了这篇帖子你可能会已经注意到即使是索引一个非常小的网页也需要花费很多时间。想象一下如果你要索引一个存在了几十年的组织的所有文档。你可能需要一个巨大的计算机否则你的假期就会被取消。由于这个原因我没有在任何项目中使用 ColBERT但它仍然存在于我的所有原型中。最后的想法你可以在一夜之间构建一个 RAG 应用第二天就可以上线。然而如果没有做出关键决策应用在生产中可能由于各种原因而无法生存。通常投诉是延迟和不准确。这几乎是 RAG 应用检索或与 LLMs 通信的阶段。在这篇帖子中我分享了一些我学到的非常棒的技术这些技术帮助我减少了检索相关的问题。我希望它也能帮到你。感谢阅读朋友除了*Medium*我还在LinkedIn和X,*上