LangChain核心模块 Retrieval——Retrievers

Retrievers

  • 检索器

数据进入数据库,仍然需要检索,LangChain支持多种检索算法。LangChain支持易于上手的基本方法——即简单语义搜索。然而,我们还在此基础上添加了一系列算法以提高性能。这些包括:

  • 父文档检索器(Parent Document Retriever):允许为每个父文档创建多个嵌入,从而允许查找较小的块但返回更大的上下文
  • 自查询检索器(Self Query Retriever):用户问题通常包含对某些内容的引用,这些内容不仅是语义的,而且表达了一些可以最好地表示为元数据过滤器的逻辑。自查询允许从查询中存在的其他元数据过滤器解析出查询的语义部分。
  • Ensemble Retriever:可以更容易的从多个不同的来源检索文档或使用多种不同的算法。

检索器是一个接口,它根据非结构化查询返回文档,它比矢量存储更通用。检索器不需要能够存储文档,只需返回(或检索)它们即可。矢量存储可以用作检索器的骨干,但也有其他类型的检索器。

检索器接受字符串查询作为输入,并返回文档列表作为输出。

Advanced Retrieval Types

LangChain提供了多种高级检索类型。下面是完整列表以及以下信息:

Name: 检索算法的名称。

Index Type: 它依赖于哪种索引类型(如果有)

Uses an LLM: 此检索方法是否使用 LLM。

When to Use

Description: Description of what this retrieval algorithm is doing.

NameIndex TypeUses an LLMWhen to UseDescription
VectorstoreVectorstoreNo刚刚开始并正在寻找快速简单的东西最简单的方法,也是最容易上手的方法。它涉及为每段文本创建嵌入。
ParentDocumentVectorstore + Document StoreNo如果页面中有许多较小的不同信息,最好自行索引,但最好一起检索。涉及到为每个文档的多个块建立索引。然后,找到嵌入空间中最相似的块,但检索整个父文档并返回它(而不是单个块)。
Multi VectorVectorstore + Document StoreSometimes during indexing如果能够从文档中提取您认为与索引比文本本身更相关的信息。涉及为每个文档创建多个向量。每个向量都可以通过多种方式创建 - 示例包括文本摘要和假设问题。
Self QueryVectorstoreYes如果用户提出问题,最好通过基于元数据而不是与文本的相似性获取文档来回答。这使用 LLM 将用户输入转换为两件事:(1) 要语义查找的字符串,(2) 与之配套的元数据文件管理器。这很有用,因为问题通常与文档的元数据(而不是内容本身)有关。
Contextual CompressionAnySometimes如果发现检索到的文档包含太多不相关的信息并且分散了LLM的注意力。这将后处理步骤置于另一个检索器之上,并仅从检索到的文档中提取最相关的信息。这可以通过嵌入或LLM来完成。
Time-Weighted VectorstoreVectorstoreNo如果有与文档关联的时间戳,并且想要检索最新的时间戳这基于语义相似性(如普通向量检索)和新近度(查看索引文档的时间戳)的组合来获取文档
Multi-Query RetrieverAnyYes如果用户提出的问题很复杂并且需要多条不同的信息来回答这使用 LLM 从原始查询生成多个查询。当原始查询需要正确回答有关多个主题的信息时,这非常有用。通过生成多个查询,我们可以为每个查询获取文档。
EnsembleAnyNo如果有多种检索方法并想尝试将它们组合起来。这会从多个检索器中获取文档,然后将它们组合起来。
Long-Context ReorderAnyNo如果正在使用长上下文模型并注意到它没有关注检索到的文档中间的信息。这会从底层检索器中获取文档,然后对它们重新排序,以便最相似的文档位于开头和结尾附近。这很有用,因为事实证明,对于较长的上下文模型,它们有时不会注意上下文窗口中间的信息。
Using Retrievers in LCEL
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

template = """Answer the question based only on the following context:

{context}

Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
model = ChatOpenAI()

def format_docs(docs):
    return "\n\n".join([d.page_content for d in docs])

chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)

chain.invoke("What did the president say about technology?")

Custom Retriever

由于检索器接口非常简单,因此编写自定义接口非常容易。

from langchain_core.retrievers import BaseRetriever
from langchain_core.callbacks import CallbackManagerForRetrieverRun
from langchain_core.documents import Document
from typing import List


class CustomRetriever(BaseRetriever):
    
    def _get_relevant_documents(
        self, query: str, *, run_manager: CallbackManagerForRetrieverRun
    ) -> List[Document]:
        return [Document(page_content=query)]

retriever = CustomRetriever()

retriever.get_relevant_documents("bar")

Vector store-backed retriever

  • 矢量存储支持的检索器

​ 矢量存储检索器是使用矢量存储来检索文档的检索器。它是矢量存储类的轻量级包装器,使其符合检索器接口。它使用向量存储实现的搜索方法(例如相似性搜索和 MMR)来查询向量存储中的文本。

一旦构建了向量存储,构建检索器就变得非常容易。

from langchain_community.document_loaders import TextLoader

loader = TextLoader("../../state_of_the_union.txt")
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import CharacterTextSplitter

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()
docs = retriever.get_relevant_documents("what did he say about ketanji brown jackson")
Maximum marginal relevance retrieval
  • 最大边际相关检索

默认情况下,向量存储检索器使用相似性搜索。如果底层向量存储支持最大边际相关性搜索,可以将其指定为搜索类型。

retriever = db.as_retriever(search_type="mmr")
docs = retriever.get_relevant_documents("what did he say about ketanji brown jackson")
Similarity score threshold retrieval
  • 相似度阈值检索

还可以设置一种检索方法,该方法设置相似度分数阈值,并且仅返回分数高于该阈值的文档。

retriever = db.as_retriever(
    search_type="similarity_score_threshold", search_kwargs={"score_threshold": 0.5}
)
docs = retriever.get_relevant_documents("what did he say about ketanji brown jackson")
Specifying top k
  • 指定前 k 个

还可以指定在检索时使用的搜索 kwargs,例如 k。

retriever = db.as_retriever(search_kwargs={"k": 1})
docs = retriever.get_relevant_documents("what did he say about ketanji brown jackson")
len(docs)
1

MultiQueryRetriever

基于距离的向量数据库检索将查询嵌入(表示)高维空间,并根据“距离”查找相似的嵌入文档。但是,如果查询措辞发生细微变化,或者嵌入不能很好地捕获数据的语义,检索可能会产生不同的结果。有时会进行及时的工程/调整(engineering / tuning)来手动解决这些问题,但这可能很乏味。

MultiQueryRetriever 通过使用 LLM 从不同角度为给定的用户输入查询生成多个查询,从而自动执行提示调整过程。对于每个查询,它都会检索一组相关文档,并采用所有查询之间的唯一并集来获取更大的一组潜在相关文档。通过对同一问题生成多个视角,MultiQueryRetriever 或许能够克服基于距离的检索的一些限制,并获得更丰富的结果集。

# Build a sample vectorDB
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

# Load blog post
loader = WebBaseLoader("https://lilianweng.github.io/posts/2023-06-23-agent/")
data = loader.load()

# Split
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=0)
splits = text_splitter.split_documents(data)

# VectorDB
embedding = OpenAIEmbeddings()
vectordb = Chroma.from_documents(documents=splits, embedding=embedding)
  1. Simple usage

    指定用于查询生成的 LLM,检索器将完成其余的工作。

    from langchain.retrievers.multi_query import MultiQueryRetriever
    from langchain_openai import ChatOpenAI
    
    question = "What are the approaches to Task Decomposition?"
    llm = ChatOpenAI(temperature=0)
    retriever_from_llm = MultiQueryRetriever.from_llm(
        retriever=vectordb.as_retriever(), llm=llm
    )
    
    # Set logging for the queries
    import logging
    
    logging.basicConfig()
    logging.getLogger("langchain.retrievers.multi_query").setLevel(logging.INFO)
    
    unique_docs = retriever_from_llm.get_relevant_documents(query=question)
    len(unique_docs)
    
  2. Supplying your own prompt

    还可以提供提示和输出解析器,以将结果拆分为查询列表。

    from typing import List
    
    from langchain.chains import LLMChain
    from langchain.output_parsers import PydanticOutputParser
    from langchain.prompts import PromptTemplate
    from pydantic import BaseModel, Field
    
    
    # Output parser will split the LLM result into a list of queries
    class LineList(BaseModel):
        # "lines" is the key (attribute name) of the parsed output
        lines: List[str] = Field(description="Lines of text")
    
    
    class LineListOutputParser(PydanticOutputParser):
        def __init__(self) -> None:
            super().__init__(pydantic_object=LineList)
    
        def parse(self, text: str) -> LineList:
            lines = text.strip().split("\n")
            return LineList(lines=lines)
    
    
    output_parser = LineListOutputParser()
    
    QUERY_PROMPT = PromptTemplate(
        input_variables=["question"],
        template="""You are an AI language model assistant. Your task is to generate five 
        different versions of the given user question to retrieve relevant documents from a vector 
        database. By generating multiple perspectives on the user question, your goal is to help
        the user overcome some of the limitations of the distance-based similarity search. 
        Provide these alternative questions separated by newlines.
        Original question: {question}""",
    )
    llm = ChatOpenAI(temperature=0)
    
    # Chain
    llm_chain = LLMChain(llm=llm, prompt=QUERY_PROMPT, output_parser=output_parser)
    
    # Other inputs
    question = "What are the approaches to Task Decomposition?"
    
    # Run
    retriever = MultiQueryRetriever(
        retriever=vectordb.as_retriever(), llm_chain=llm_chain, parser_key="lines"
    )  # "lines" is the key (attribute name) of the parsed output
    
    # Results
    unique_docs = retriever.get_relevant_documents(
        query="What does the course say about regression?"
    )
    len(unique_docs)
    

Contextual compression

  • 上下文压缩

检索的一个挑战是,通常不知道将数据引入系统时,文档存储系统将面临哪些特定查询。这意味着与查询最相关的信息可能被隐藏在包含大量不相关文本的文档中,但传递完整的文件可能会导致更昂贵的LLM会话和更差的响应。

上下文压缩旨在解决这个问题。这个想法很简单:可以使用给定查询的上下文来压缩它们,以便只返回相关信息,而不是立即按原样返回检索到的文档。这里的“压缩”既指压缩单个文档的内容,也指批量过滤文档。

要使用上下文压缩检索器,需要:

  • a base retriever
  • a Document Compressor

上下文压缩检索器将查询传递给基本检索器,获取初始文档并将它们传递给文档压缩器。文档压缩器获取文档列表并通过减少文档内容或完全删除文档来缩短它。

# Helper function for printing docs

def pretty_print_docs(docs):
    print(
        f"\n{'-' * 100}\n".join(
            [f"Document {i+1}:\n\n" + d.page_content for i, d in enumerate(docs)]
        )
    )
Adding contextual compression with an LLMChainExtractor

ContextualCompressionRetriever包装我们的基本检索器。添加一个 LLMChainExtractor,它将迭代最初返回的文档,并从每个文档中仅提取与查询相关的内容。

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain_openai import OpenAI

llm = OpenAI(temperature=0)
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor, base_retriever=retriever
)

compressed_docs = compression_retriever.get_relevant_documents(
    "What did the president say about Ketanji Jackson Brown"
)
pretty_print_docs(compressed_docs)
More built-in compressors: filters
  • 更多内置压缩机:过滤器
  1. LLMChainFilter

    LLMChainFilter 是稍微简单但更强大的压缩器,它使用 LLM 链来决定过滤掉最初检索到的文档中的哪些以及返回哪些文档,而无需操作文档内容。

    from langchain.retrievers.document_compressors import LLMChainFilter
    
    _filter = LLMChainFilter.from_llm(llm)
    compression_retriever = ContextualCompressionRetriever(
        base_compressor=_filter, base_retriever=retriever
    )
    
    compressed_docs = compression_retriever.get_relevant_documents(
        "What did the president say about Ketanji Jackson Brown"
    )
    pretty_print_docs(compressed_docs)
    
  2. EmbeddingsFilter

    EmbeddingsFilter 通过嵌入文档和查询并仅返回那些与查询具有足够相似嵌入的文档

    from langchain.retrievers.document_compressors import EmbeddingsFilter
    from langchain_openai import OpenAIEmbeddings
    
    embeddings = OpenAIEmbeddings()
    embeddings_filter = EmbeddingsFilter(embeddings=embeddings, similarity_threshold=0.76)
    compression_retriever = ContextualCompressionRetriever(
        base_compressor=embeddings_filter, base_retriever=retriever
    )
    
    compressed_docs = compression_retriever.get_relevant_documents(
        "What did the president say about Ketanji Jackson Brown"
    )
    pretty_print_docs(compressed_docs)
    
Stringing compressors and document transformers together
  • 将压缩器和文档转换器串在一起

使用 DocumentCompressorPipeline 可以轻松地按顺序组合多个压缩器。除了压缩器之外,我们还可以将 BaseDocumentTransformers 添加到管道中,它不执行任何上下文压缩,而只是对一组文档执行一些转换。

例如,TextSplitters 可以用作文档转换器,将文档分割成更小的部分,而 EmbeddingsRedundantFilter 可以用于根据文档之间嵌入的相似性来过滤掉冗余文档。

下例中创建一个压缩器管道,首先将文档分割成更小的块,然后删除冗余文档,然后根据与查询的相关性进行过滤。

from langchain.retrievers.document_compressors import DocumentCompressorPipeline
from langchain_community.document_transformers import EmbeddingsRedundantFilter
from langchain_text_splitters import CharacterTextSplitter

splitter = CharacterTextSplitter(chunk_size=300, chunk_overlap=0, separator=". ")
redundant_filter = EmbeddingsRedundantFilter(embeddings=embeddings)
relevant_filter = EmbeddingsFilter(embeddings=embeddings, similarity_threshold=0.76)
pipeline_compressor = DocumentCompressorPipeline(
    transformers=[splitter, redundant_filter, relevant_filter]
)
compression_retriever = ContextualCompressionRetriever(
    base_compressor=pipeline_compressor, base_retriever=retriever
)

compressed_docs = compression_retriever.get_relevant_documents(
    "What did the president say about Ketanji Jackson Brown"
)
pretty_print_docs(compressed_docs)

Ensemble Retriever

EnsembleRetriever 将检索器列表作为输入,并集成其 get_relevant_documents() 方法的结果,并根据倒数排名融合算法对结果进行重新排名。

通过利用不同算法的优势,EnsembleRetriever 可以获得比任何单一算法更好的性能。

最常见的模式是将稀疏检索器(如 BM25)与密集检索器(如嵌入相似性)相结合,因为它们的优势是互补的。它也被称为“混合搜索”。

稀疏检索器擅长根据关键词查找相关文档,而密集检索器擅长根据语义相似度查找相关文档。

from langchain.retrievers import BM25Retriever, EnsembleRetriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
doc_list_1 = [
    "I like apples",
    "I like oranges",
    "Apples and oranges are fruits",
]

# initialize the bm25 retriever and faiss retriever
bm25_retriever = BM25Retriever.from_texts(
    doc_list_1, metadatas=[{"source": 1}] * len(doc_list_1)
)
bm25_retriever.k = 2

doc_list_2 = [
    "You like apples",
    "You like oranges",
]

embedding = OpenAIEmbeddings()
faiss_vectorstore = FAISS.from_texts(
    doc_list_2, embedding, metadatas=[{"source": 2}] * len(doc_list_2)
)
faiss_retriever = faiss_vectorstore.as_retriever(search_kwargs={"k": 2})

# initialize the ensemble retriever
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, faiss_retriever], weights=[0.5, 0.5]
)
docs = ensemble_retriever.invoke("apples")
docs
Runtime Configuration
  • 运行时配置

可以在运行时配置检索器。为此,我们需要将字段标记为可配置

from langchain_core.runnables import ConfigurableField
faiss_retriever = faiss_vectorstore.as_retriever(
    search_kwargs={"k": 2}
).configurable_fields(
    search_kwargs=ConfigurableField(
        id="search_kwargs_faiss",
        name="Search Kwargs",
        description="The search kwargs to use",
    )
)
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, faiss_retriever], weights=[0.5, 0.5]
)
config = {"configurable": {"search_kwargs_faiss": {"k": 1}}}
docs = ensemble_retriever.invoke("apples", config=config)
docs

注意,这仅从 FAISS 检索器返回一个源,因为我们在运行时传入了相关配置

Long-Context Reorder

  • 长上下文重新排序

无论模型的架构如何,当包含 10 多个检索到的文档时,性能都会大幅下降。简而言之:当模型必须在长上下文中访问相关信息时,它们往往会忽略所提供的文档。

为了避免此问题,可以在检索后对文档重新排序,以避免性能下降。

MultiVector Retriever

  • 多向量检索器

LangChain 有一个基础 MultiVectorRetriever,可以轻松查询此类设置。很多复杂性在于如何为每个文档创建多个向量。本节内容涵盖了创建这些向量和使用 MultiVectorRetriever 的一些常见方法。

为每个文档创建多个向量的方法包括:

  • Smaller chunks: 将文档分割成更小的块,然后嵌入这些块(这是 ParentDocumentRetriever)。
  • Summary: 为每个文档创建一个摘要,将其与文档一起嵌入(或代替)。
  • Hypothetical questions: 创建每个文档都适合回答的假设问题,将这些问题与文档一起嵌入(或代替)。

注意,这还启用了另一种添加嵌入的方法—manually。这样就可以显式添加导致文档恢复的问题或查询,从而为您提供更多控制权。

from langchain.retrievers.multi_vector import MultiVectorRetriever
from langchain.storage import InMemoryByteStore
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
loaders = [
    TextLoader("../../paul_graham_essay.txt"),
    TextLoader("../../state_of_the_union.txt"),
]
docs = []
for loader in loaders:
    docs.extend(loader.load())
text_splitter = RecursiveCharacterTextSplitter(chunk_size=10000)
docs = text_splitter.split_documents(docs)
Smaller chunks

通常,检索较大的信息块可能很有用,但嵌入较小的信息块。这允许嵌入尽可能接近地捕获语义,但可以将尽可能多的上下文传递到下游。

注意,这就是 ParentDocumentRetriever 的作用。

# The vectorstore to use to index the child chunks
vectorstore = Chroma(
    collection_name="full_documents", embedding_function=OpenAIEmbeddings()
)
# The storage layer for the parent documents
store = InMemoryByteStore()
id_key = "doc_id"
# The retriever (empty to start)
retriever = MultiVectorRetriever(
    vectorstore=vectorstore,
    byte_store=store,
    id_key=id_key,
)
import uuid

doc_ids = [str(uuid.uuid4()) for _ in docs]
# The splitter to use to create smaller chunks
child_text_splitter = RecursiveCharacterTextSplitter(chunk_size=400)
sub_docs = []
for i, doc in enumerate(docs):
    _id = doc_ids[i]
    _sub_docs = child_text_splitter.split_documents([doc])
    for _doc in _sub_docs:
        _doc.metadata[id_key] = _id
    sub_docs.extend(_sub_docs)
retriever.vectorstore.add_documents(sub_docs)
retriever.docstore.mset(list(zip(doc_ids, docs)))
# Vectorstore alone retrieves the small chunks
retriever.vectorstore.similarity_search("justice breyer")[0]
# Retriever returns larger chunks
len(retriever.get_relevant_documents("justice breyer")[0].page_content)

检索器对矢量数据库执行的默认搜索类型是相似性搜索。

LangChain Vector Stores 还支持通过最大边际相关性进行搜索,因此如果想要这样,只需设置 search_type 属性。

from langchain.retrievers.multi_vector import SearchType

retriever.search_type = SearchType.mmr

len(retriever.get_relevant_documents("justice breyer")[0].page_content)
Summary

通常,摘要可能能够更准确地提炼出某个块的内容,从而实现更好的检索。下面,我们展示如何创建摘要,然后嵌入它们。

import uuid

from langchain_core.documents import Document
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
chain = (
    {"doc": lambda x: x.page_content}
    | ChatPromptTemplate.from_template("Summarize the following document:\n\n{doc}")
    | ChatOpenAI(max_retries=0)
    | StrOutputParser()
)
summaries = chain.batch(docs, {"max_concurrency": 5})
# The vectorstore to use to index the child chunks
vectorstore = Chroma(collection_name="summaries", embedding_function=OpenAIEmbeddings())
# The storage layer for the parent documents
store = InMemoryByteStore()
id_key = "doc_id"
# The retriever (empty to start)
retriever = MultiVectorRetriever(
    vectorstore=vectorstore,
    byte_store=store,
    id_key=id_key,
)
doc_ids = [str(uuid.uuid4()) for _ in docs]
summary_docs = [
    Document(page_content=s, metadata={id_key: doc_ids[i]})
    for i, s in enumerate(summaries)
]
retriever.vectorstore.add_documents(summary_docs)
retriever.docstore.mset(list(zip(doc_ids, docs)))
Hypothetical Queries

LLM还可以用于生成针对特定文档可能提出的假设问题列表。然后可以嵌入这些问题

functions = [
    {
        "name": "hypothetical_questions",
        "description": "Generate hypothetical questions",
        "parameters": {
            "type": "object",
            "properties": {
                "questions": {
                    "type": "array",
                    "items": {"type": "string"},
                },
            },
            "required": ["questions"],
        },
    }
]
from langchain.output_parsers.openai_functions import JsonKeyOutputFunctionsParser

chain = (
    {"doc": lambda x: x.page_content}
    # Only asking for 3 hypothetical questions, but this could be adjusted
    | ChatPromptTemplate.from_template(
        "Generate a list of exactly 3 hypothetical questions that the below document could be used to answer:\n\n{doc}"
    )
    | ChatOpenAI(max_retries=0, model="gpt-4").bind(
        functions=functions, function_call={"name": "hypothetical_questions"}
    )
    | JsonKeyOutputFunctionsParser(key_name="questions")
)
hypothetical_questions = chain.batch(docs, {"max_concurrency": 5})
# The vectorstore to use to index the child chunks
vectorstore = Chroma(
    collection_name="hypo-questions", embedding_function=OpenAIEmbeddings()
)
# The storage layer for the parent documents
store = InMemoryByteStore()
id_key = "doc_id"
# The retriever (empty to start)
retriever = MultiVectorRetriever(
    vectorstore=vectorstore,
    byte_store=store,
    id_key=id_key,
)
doc_ids = [str(uuid.uuid4()) for _ in docs]
question_docs = []
for i, question_list in enumerate(hypothetical_questions):
    question_docs.extend(
        [Document(page_content=s, metadata={id_key: doc_ids[i]}) for s in question_list]
    )
retriever.vectorstore.add_documents(question_docs)
retriever.docstore.mset(list(zip(doc_ids, docs)))

Parent Document Retriever

  • 父文档检索器

在拆分文档进行检索时,经常会出现相互冲突的需求:

  1. 可能想要小文档,以便它们的嵌入能够最准确地反映它们的含义。如果太长,那么嵌入可能会失去意义。
  2. 希望拥有足够长的文档以保留每个块的上下文。

ParentDocumentRetriever 通过分割和存储小块数据来实现这种平衡。在检索过程中,它首先获取小块,然后查找这些块的父 ID,并返回那些较大的文档。

注意,“父文档”是指小块源自的文档。这可以是整个原始文档或更大的块。

from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
loaders = [
    TextLoader("../../paul_graham_essay.txt"),
    TextLoader("../../state_of_the_union.txt"),
]
docs = []
for loader in loaders:
    docs.extend(loader.load())
Retrieving full documents
  • 检索完整文档

在这种模式下,我们想要检索完整的文档。因此,我们只指定一个子分割器。

# This text splitter is used to create the child documents
child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)
# The vectorstore to use to index the child chunks
vectorstore = Chroma(
    collection_name="full_documents", embedding_function=OpenAIEmbeddings()
)
# The storage layer for the parent documents
store = InMemoryStore()
retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=store,
    child_splitter=child_splitter,
)
retriever.add_documents(docs, ids=None)

这应该会产生两个键,因为我们添加了两个文档。

list(store.yield_keys())

现在调用向量存储搜索功能 - 应该看到它返回小块(因为我们正在存储小块)。

sub_docs = vectorstore.similarity_search("justice breyer")

现在从整体检索器中检索。这应该返回大文档 - 因为它返回较小块所在的文档。

retrieved_docs = retriever.get_relevant_documents("justice breyer")
Retrieving larger chunks
  • 检索更大的块

有时,完整文档可能太大而无法按原样检索它们。在这种情况下,我们真正想做的是首先将原始文档分割成更大的块,然后将其分割成更小的块。然后我们索引较小的块,但在检索时我们检索较大的块(但仍然不是完整的文档)。

# This text splitter is used to create the parent documents
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000)
# This text splitter is used to create the child documents
# It should create documents smaller than the parent
child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)
# The vectorstore to use to index the child chunks
vectorstore = Chroma(
    collection_name="split_parents", embedding_function=OpenAIEmbeddings()
)
# The storage layer for the parent documents
store = InMemoryStore()
retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=store,
    child_splitter=child_splitter,
    parent_splitter=parent_splitter,
)
retriever.add_documents(docs)

底层向量存储仍然检索小块。

Self-querying

自查询检索器,顾名思义,是一种能够查询自身的检索器。具体来说,给定任何自然语言查询,检索器使用查询构造 LLM 链来编写结构化查询,然后将该结构化查询应用于其底层 VectorStore。

这允许检索器不仅使用用户输入的查询与存储的文档的内容进行语义相似性比较,而且还从对存储的文档的元数据的用户查询中提取过滤器并执行这些过滤器。

在这里插入图片描述

Constructing from scratch with LCEL
  • 使用 LCEL 从头开始构建

首先,创建一个查询构造链。该链将接受用户查询并生成一个 StructuredQuery 对象,该对象捕获用户指定的过滤器。我们提供了一些帮助函数来创建提示和输出解析器。它们有许多可调参数,为简单起见,我们在这里忽略它们。

from langchain.chains.query_constructor.base import (
    StructuredQueryOutputParser,
    get_query_constructor_prompt,
)

prompt = get_query_constructor_prompt(
    document_content_description,
    metadata_field_info,
)
output_parser = StructuredQueryOutputParser.from_components()
query_constructor = prompt | llm | output_parser

查询构造函数是自查询检索器的关键元素。为了构建一个出色的检索系统,需要确保查询构造函数运行良好。通常这需要调整提示、提示中的示例、属性描述等。

下一个关键要素是结构化查询翻译器。该对象负责将通用 StructuredQuery 对象转换为您正在使用的向量存储语法中的元数据过滤器。LangChain内置了多个翻译器。

from langchain.retrievers.self_query.chroma import ChromaTranslator

retriever = SelfQueryRetriever(
    query_constructor=query_constructor,
    vectorstore=vectorstore,
    structured_query_translator=ChromaTranslator(),
)

Time-weighted vector store retriever

  • 时间加权向量存储检索器

该检索器使用语义相似性和时间衰减的组合。

对它们进行评分的算法是:

semantic_similarity + (1.0 - decay_rate) ^ hours_passed

注意,hours_passed 指的是自上次访问检索器中的对象以来经过的小时数,而不是自创建以来经过的小时数。这意味着经常访问的对象保持“新鲜”。

from datetime import datetime, timedelta

import faiss
from langchain.docstore import InMemoryDocstore
from langchain.retrievers import TimeWeightedVectorStoreRetriever
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings
Low decay rate

低衰减率(极端地说,我们将其设置为接近 0)意味着记忆将被“记住”更长时间。衰减率为 0 意味着记忆永远不会被遗忘,使得该检索器相当于向量查找。

# Define your embedding model
embeddings_model = OpenAIEmbeddings()
# Initialize the vectorstore as empty
embedding_size = 1536
index = faiss.IndexFlatL2(embedding_size)
vectorstore = FAISS(embeddings_model, index, InMemoryDocstore({}), {})
retriever = TimeWeightedVectorStoreRetriever(
    vectorstore=vectorstore, decay_rate=0.0000000000000000000000001, k=1
)
yesterday = datetime.now() - timedelta(days=1)
retriever.add_documents(
    [Document(page_content="hello world", metadata={"last_accessed_at": yesterday})]
)
retriever.add_documents([Document(page_content="hello foo")])
High decay rate

如果衰减率较高, recency score很快就会变为 0!如果将其一直设置为 1,则所有对象的recency均为 0,这再次相当于向量查找。

# Define your embedding model
embeddings_model = OpenAIEmbeddings()
# Initialize the vectorstore as empty
embedding_size = 1536
index = faiss.IndexFlatL2(embedding_size)
vectorstore = FAISS(embeddings_model, index, InMemoryDocstore({}), {})
retriever = TimeWeightedVectorStoreRetriever(
    vectorstore=vectorstore, decay_rate=0.999, k=1
)
yesterday = datetime.now() - timedelta(days=1)
retriever.add_documents(
    [Document(page_content="hello world", metadata={"last_accessed_at": yesterday})]
)
retriever.add_documents([Document(page_content="hello foo")])
Virtual time

使用LangChain中的一些实用程序,可以模拟时间组件。

import datetime

from langchain.utils import mock_now
# Notice the last access time is that date time
with mock_now(datetime.datetime(2024, 2, 3, 10, 11)):
    print(retriever.get_relevant_documents("hello world"))
  • 31
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值