使用父文档检索器(PDR)构建 RAG 的最佳实践,并使用 PDR 进行元数据过滤。

你是否正在寻找使用 CSV/Excel 文件构建高效的 RAG?那么这篇文章绝对适合你 :)

在本文中,我将谈论构建高效的 RAG 的最佳实践,尤其是当您尝试在 LangChain 中使用父文档检索器,并将 csv/excel 文件与多列作为数据集时。

顺便问一下,为什么特别是父文件检索器?

哈哈哈.... 根据我的经验,运行多次实验后,当您有冗长的文档时,最好使用父文档检索器。

 让我们开始吧!

首先,我们有一个 csv/excel 数据,这意味着虽然一个列可以作为回答问题的来源,但可能还有许多其他的列,比如国家名称、日期和其他列,可以作为元数据来改进答案。

现在让我们首先看看改善答案的不同技巧,然后再谈谈如何使用元数据。

  1. 创建自定义的 CSV 加载器:创建一个类似于 Lang chain 的 CSVLoader 的自定义 CSV 加载器,进行少量定制。这将帮助我们定义哪些列需要被视为“页面内容”,哪些列需要被视为“元数据”。

    您可以使用此自定义的 CSVLoader 来加载您的 csv 文件。让我举个例子:
import csv
from typing import Dict, List, Optional
from langchain.document_loaders.base import BaseLoader
from langchain.docstore.document import Document


class CSVLoader(BaseLoader):
    """Loads a CSV file into a list of documents.

    Each document represents one row of the CSV file. Every row is converted into a
    key/value pair and outputted to a new line in the document's page_content.

    The source for each document loaded from csv is set to the value of the
    `file_path` argument for all doucments by default.
    You can override this by setting the `source_column` argument to the
    name of a column in the CSV file.
    The source of each document will then be set to the value of the column
    with the name specified in `source_column`.

    Output Example:
        .. code-block:: txt

            column1: value1
            column2: value2
            column3: value3
    """

    def __init__(
        self,
        file_path: str,
        source_column: Optional[str] = None,
        metadata_columns: Optional[List[str]] = None,   # < ADDED
        csv_args: Optional[Dict] = None,
        encoding: Optional[str] = None,
    ):
        self.file_path = file_path
        self.source_column = source_column
        self.encoding = encoding
        self.csv_args = csv_args or {}
        self.metadata_columns = metadata_columns        # < ADDED

    def load(self) -> List[Document]:
        """Load data into document objects."""

        docs = []
        with open(self.file_path, newline="", encoding=self.encoding) as csvfile:
            csv_reader = csv.DictReader(csvfile, **self.csv_args)  # type: ignore
            for i, row in enumerate(csv_reader):
                content = "\n".join(f"{k.strip()}: {v.strip()}" for k, v in row.items() if k == "Source column name")
                try:
                    source = (
                        row[self.source_column]
                        if self.source_column is not None
                        else self.file_path
                    )
                except KeyError:
                    raise ValueError(
                        f"Source column '{self.source_column}' not found in CSV file."
                    )
                metadata = {"source": source, "row": i}
                # ADDED TO SAVE METADATA
                if self.metadata_columns:
                    for k, v in row.items():
                        if k in self.metadata_columns:
                            metadata[k] = v
                # END OF ADDED CODE
                doc = Document(page_content=content, metadata=metadata)
                docs.append(doc)

        return docs

2. 两个分割器:通常我们只使用一个文本分割器将长文本分割成多个更小的块,但在父文档检索器的情况下,我们使用两个分割器。一个用于具有更多上下文的较大块(让我们称这些较大块为父级),另一个用于具有更好语义意义的较小块(让我们称这些较小块为子级)。专业提示✨:在创建子文档时,玩转块大小:块大小在确定 RAG 系统如何生成答案方面起着重要作用。

我建议最初尝试不同的块大小,直到您觉得创建的子文档之间有较少的重叠,并且生成的答案符合您的期望。

parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000)

child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)

3. 存储父级和子级块:存储嵌入✨:初始化父文档检索器并将文档添加到检索器是在运行时发生的事情,不仅需要长时间运行,而且每次查询相同的内容都需要花费成本来创建相同的嵌入,您可以使用下面的代码行来存储已创建的嵌入。

vectorstore.persist()

 使用元数据:✨

现在来到有趣的话题。默认情况下,尽管根据文档中的元数据参数应用了元数据,但我们无法使用 LangChain 的父检索器来使用元数据,应用它不会根据提供的过滤器筛选检索到的文档。

因此,我们需要编写一个自定义类,根据元数据过滤相关文档。这有两种可能的方法:

  1. 从向量存储中获取检索到的相关文件,然后在其上应用元数据过滤器。
  2. 在执行向量搜索时应用元数据过滤,并仅返回唯一文档。

第二个选项是解决这个问题的正确方法。因此,始终创建一个自定义类,并使用自定义函数根据元数据过滤器检索最相关的文档。

通过这种方式,您不仅可以在搜索时应用筛选器,还可以增加检索到的文档数量(默认情况下只能获取 4 个相关文档)。让我举个例子,说明如何做到这一点。

我正在考虑将国家名称和产品名称作为我的数据中的两列,这些列如上所述已加载为元数据(创建自定义 CSVLoader)。

class ParentDocumentRetriever(BaseRetriever):
    vectorstore: VectorStore
    docstore: BaseStore[str, Document]
    id_key: str = "doc_id"
    search_kwargs: dict = Field(default_factory=dict)
    child_splitter: TextSplitter
    parent_splitter: Optional[TextSplitter] = None

    def _get_relevant_documents(
        self,
        query: str,
        *,
        run_manager: CallbackManagerForRetrieverRun,
        metadata_filter: Optional[Dict[str, Any]] = None
    ) -> List[Document]:
        all_results = []
        if metadata_filter:
            # Iterate over each key-value pair in the metadata_filter
            unique_ids = set()

            # Iterate over each key-value pair in the metadata_filter
            for key, value in metadata_filter.items():
                # Perform the similarity search for the current key-value pair
                sub_docs = self.vectorstore.similarity_search(query, k=10, filter={key: value}, **self.search_kwargs)
                ids = [d.metadata[self.id_key] for d in sub_docs]

                # Add unique document IDs to the set
                unique_ids.update(ids)

            # Retrieve documents from the docstore based on the unique IDs
            all_results = self.docstore.mget(list(unique_ids))
            print("Filtering documents with metadata:", metadata_filter)
            filtered_documents = []

            for document in all_results:
                if document is not None:
                    match = all(
                        any(value in document.metadata.get(key, []) for value in values)
                        if isinstance(document.metadata.get(key), list)
                        else document.metadata.get(key) in values
                        for key, values in metadata_filter.items() if values
                        )
                if match:
                    filtered_documents.append(document)

            docs = filtered_documents
        else:
            sub_docs = self.vectorstore.similarity_search(query, k=10, **self.search_kwargs)
            ids = []
            for d in sub_docs:
                if d.metadata[self.id_key] not in ids:
                    ids.append(d.metadata[self.id_key])
            docs = self.docstore.mget(ids)

        return [d for d in docs if d is not None]

    def add_documents(
        self,
        documents: List[Document],
        ids: Optional[List[str]] = None,
        add_to_docstore: bool = True,
    ) -> None:
        if self.parent_splitter is not None:
            documents = self.parent_splitter.split_documents(documents)
        if ids is None:
            doc_ids = [str(uuid.uuid4()) for _ in documents]
            if not add_to_docstore:
                raise ValueError(
                    "If ids are not passed in, `add_to_docstore` MUST be True"
                )
        else:
            if len(documents) != len(ids):
                raise ValueError(
                    "Got uneven list of documents and ids. "
                    "If `ids` is provided, should be same length as `documents`."
                )
            doc_ids = ids

        docs = []
        full_docs = []
        for i, doc in enumerate(documents):
            _id = doc_ids[i]
            sub_docs = self.child_splitter.split_documents([doc])
            for _doc in sub_docs:
                _doc.metadata[self.id_key] = _id
            docs.extend(sub_docs)
            full_docs.append((_id, doc))
        self.vectorstore.add_documents(docs)
        if add_to_docstore:
            self.docstore.mset(full_docs)

让我解释一下当您调用检索器的 get_relavant_documents 函数时会发生什么:

对于您提供的每个元数据筛选器(国家和产品名称)

parent_retriever = ParentDocumentRetriever(vectorstore=vectorstore,
        docstore=store,
        child_splitter=child_splitter,
        parent_splitter=parent_splitter,
    )
parent_retriever.get_relevant_documents(query, metadata_filter={"Country":"Canada","ProductName":"Sample"})

矢量存储将在单独应用过滤器后执行相似性搜索,将它们组合起来,然后过滤出唯一的文档,这些唯一的文档可用于查询,即矢量搜索将发生 n 次,其中 n 是 metadata_filter 字典中键值对的数量,然后过滤出唯一的文档。

一旦您从检索器中获得了最相关的文档,就该是将它们链在一起的时候了:

context = parent_retriever.get_relevant_documents(query, metadata_filter={"Country":"Canada","ProductName":"Sample"})
response = llm_chain({"context": context, "question": query})

生成的响应将根据要求应用正确的元数据过滤器,符合预期。

希望这对你有帮助! :)

<think>嗯,用户现在想了解如何使用RAG模型对CSV文件进行检索,或者RAG检索CSV数据的实现方式。我需要先回忆一下RAG的基本概念和流程,然后看看如何将CSV文件整合进去。 首先,根据引用[2],RAG的数据提取阶段需要从各种来源加载文档数据,包括CSV。CSV文件通常以表格形式存储数据,所以可能需要将表格内容转换成文本格式,以便后续处理。比如,可以把每一行数据转换为自然语言描述的句子,或者保留表头作为元数据。 接下来,数据预处理部分,可能需要分块。CSV的行可能比较短,所以是否需要合多个行为一个块?或者按列来分块?但一般来说,分块策略需要考虑上下文的相关性。可能每个块包含若干行,或者根据列的重要性来处理。此外,引用[3]提到了文档检索器PDR),可能涉及元数据过滤,比如在CSV中,可以添加列名作为元数据,方便后续检索过滤。 然后是向量化处理。需要将CSV中的每个数据块转换为向量,存储到向量数据库中。这里可能需要用到像Sentence Transformers这样的模型生成嵌入向量。在检索时,用户的查询也会被向量化,然后与数据库中的向量进行相似度匹配,找到最相关的数据块。 最后,生成部分需要将检索到的CSV数据整合到生成模型中。比如,在问答系统中,将检索到的表格行作为上下文输入给LLM,生成答案。需要注意的是,表格数据可能结构复杂,转换成自然语言描述会更适合模型处理,比如“产品名称:X,销量:Y”。 另外,用户可能需要具体的实现步骤或代码示例。比如使用Python的pandas库读取CSV,用LangChain框架处理分块和向量化,再结合类似FAISS的向量数据库进行检索。最后用Hugging Face的模型生成回答。 还要考虑性能优化,比如如何处理大型CSV文件,分块的大小设置,以及元数据的合理使用来提高检索准确性。例如,在引用[3]中提到的元数据过滤,可以针对CSV中的特定列进行过滤,比如国家或产品名称,这样能更精确地定位相关信息。 用户可能还关心实际应用场景,比如用RAG处理销售数据CSV,进行问答或生成报告。需要确保生成的回答不仅正确,还能有效利用表格中的结构化数据。可能需要测试不同的分块策略和提示词设计,以优化生成结果的质量。 总之,步骤大致分为数据加载与解析、预处理与分块、向量化与存储、检索与生成四个部分,每个部分都需要根据CSV的特性进行调整,确保RAG系统能有效处理和利用表格数据。</think>### 分步解答:如何用RAG模型检索CSV文件数据? #### **1. 数据加载与解析** - **CSV文件加载**:使用工具(如Python的`pandas`)读取CSV文件,提取结构化数据。例如: ```python import pandas as pd data = pd.read_csv("data.csv") ``` - **数据规范化**:将表格数据转换为自然语言描述或键值对格式,便于后续处理。例如将一行数据转换为: $$文本 = "产品名称:{ProductName}, 销量:{Sales}, 国家:{Country}"$$ #### **2. 预处理与分块** - **分块策略**: - **按行分块**:每行作为独立文本块,适合字段独立的场景[^2]。 - **按逻辑组合**:合相关行(如同一产品的多个月份数据),增强上下文关联性。 - **添加元数据**:为每个块附加列名(如`ProductName`、`Country`)作为元数据,用于后续过滤[^3]。 #### **3. 向量化与存储** - **向量生成**:使用嵌入模型(如`Sentence-BERT`)将文本块转换为向量: ```python from sentence_transformers import SentenceTransformer model = SentenceTransformer('all-MiniLM-L6-v2') embeddings = model.encode(text_chunks) ``` - **向量数据库存储**:将向量存入数据库(如`FAISS`或`Chroma`),关联元数据: ```python import faiss index = faiss.IndexFlatL2(embedding_dim) index.add(embeddings) ``` #### **4. 检索与生成** - **检索过程**: - 将用户查询(如“加拿大的产品销量”)转换为向量。 - 通过向量相似度匹配,检索相关数据块,可结合元数据过滤(如`Country=Canada`)提升精度[^3]。 - **生成答案**:将检索结果输入生成模型(如GPT-3),生成结构化回答: ```python context = "产品A销量:1000;产品B销量:2000" prompt = f"根据以下数据回答问题:{context}\n问题:加拿大销量最高的产品是什么?" response = llm.generate(prompt) ``` --- ### **示例代码框架** ```python from langchain.document_loaders import CSVLoader from langchain.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import FAISS # 1. 加载CSV分块 loader = CSVLoader(file_path="data.csv", metadata_columns=["Country", "ProductName"]) documents = loader.load() # 2. 向量化存储 embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2") db = FAISS.from_documents(documents, embeddings) # 3. 检索与生成 query = "加拿大的产品销量" docs = db.similarity_search(query, filter={"Country": "Canada"}) context = "\n".join([doc.page_content for doc in docs]) answer = llm(f"根据上下文回答:{context}\n问题:{query}") ``` --- ### **优化建议** 1. **元数据设计**:为CSV列添加清晰的元数据标签(如`数据类型`、`时间范围`),提升检索过滤效率[^3]。 2. **分块粒度**:根据字段关联性调整分块大小(如单行 vs 多行聚合)。 3. **混合检索**:结合关键词匹配(如精确列名)与向量检索,平衡精度与语义理解。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值