核心模块(Modules)
数据连接(DataConnection)
检索器(Retrievers)
多向量(MultiVector)

多向量检索工具

在一个文档中存储多个向量通常是有益的。有多种情况下这是有益的。LangChain提供了一个基本的MultiVectorRetriever,使得查询这种类型的设置变得简单。创建多个向量的复杂性主要是在于如何创建每个文档的多个向量。本笔记本介绍了创建这些向量和使用MultiVectorRetriever的一些常见方法。

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

  • 较小的块:将一个文档拆分为较小的块,并对其进行嵌入(这是ParentDocumentRetriever)。
  • 摘要:为每个文档创建摘要,将其与文档一起嵌入(或者替换文档)。
  • 假设性问题:为每个文档创建假设性问题,将其与文档一起嵌入(或者替换文档)。

请注意,这还可以使用另一种向量嵌入的方法 - 手动方式。这非常好,因为您可以明确地添加应该导致恢复文档的问题或查询,从而更好地控制检索过程。

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)

较小的块

通常情况下,检索较大块的信息,但嵌入较小块可能是有用的。这样可以尽可能接近地捕捉语义含义,同时在下游传递尽可能多的上下文。请注意,这就是ParentDocumentRetriever所做的。以下是我们对其内部操作的演示。

# 用于索引子块的向量存储
vectorstore = Chroma(
    collection_name="full_documents", embedding_function=OpenAIEmbeddings()
)
# 父文档的存储层
store = InMemoryByteStore()
id_key = "doc_id"
# 检索器(初始为空)
retriever = MultiVectorRetriever(
    vectorstore=vectorstore,
    byte_store=store,
    id_key=id_key,
)
import uuid
 
doc_ids = [str(uuid.uuid4()) for _ in docs]
# 用于创建较小块的拆分器
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单独检索小块
retriever.vectorstore.similarity_search("justice breyer")[0]
# 检索器返回较大的块
len(retriever.get_relevant_documents("justice breyer")[0].page_content)

检索器在向量数据库上执行的默认搜索类型是相似度搜索。LangChain向量存储还支持通过[最大边际相关度(MMR)]进行搜索(https://api.python.langchain.com/en/latest/vectorstores/langchain_core.vectorstores.VectorStore.html#langchain_core.vectorstores.VectorStore.max_marginal_relevance_search),所以如果您希望使用这种方法,只需将`search_type`属性设置如下: (opens in a new tab)

from langchain.retrievers.multi_vector import SearchType
 
retriever.search_type = SearchType.mmr
 
len(retriever.get_relevant_documents("justice breyer")[0].page_content)

摘要

摘要通常可以更准确地概括块的内容,从而实现更好的检索。这里我们展示如何创建摘要,然后将其嵌入。

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})
# 用于索引子块的向量存储
vectorstore = Chroma(collection_name="summaries", embedding_function=OpenAIEmbeddings())
# 父文档的存储层
store = InMemoryByteStore()
id_key = "doc_id"
# 检索器(初始为空)
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)))
# # We can also add the original chunks to the vectorstore if we so want
# for i, doc in enumerate(docs):
#     doc.metadata[id_key] = doc_ids[i]
# retriever.vectorstore.add_documents(docs)
sub_docs = vectorstore.similarity_search("justice breyer")
sub_docs[0]
retrieved_docs = retriever.get_relevant_documents("justice breyer")
len(retrieved_docs[0].page_content)

我以上对原文内容进行了翻译,请查看替换后的结果,如果有其他问题,请随时告诉我。=======

假设查询

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

functions = [
    {
        "name": "hypothetical_questions",
        "description": "生成假设性问题",
        "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}
    # 只要求3个假设性问题,但可以进行调整
    | ChatPromptTemplate.from_template(
        "生成一个包含3个假设性问题的列表,以下文档可以用来回答这些问题:\n\n{doc}"
    )
    | ChatOpenAI(max_retries=0, model="gpt-4").bind(
        functions=functions, function_call={"name": "hypothetical_questions"}
    )
    | JsonKeyOutputFunctionsParser(key_name="questions")
)
chain.invoke(docs[0])
["作者的编程初体验是什么样的?",
 '为什么作者在研究生阶段将注意力从AI转移到Lisp?',
 '是什么导致作者考虑放弃计算机科学而转向艺术领域?']
hypothetical_questions = chain.batch(docs, {"max_concurrency": 5})
# 用于对子块进行索引的向量存储
vectorstore = Chroma(
    collection_name="假设性问题", embedding_function=OpenAIEmbeddings()
)
# 用于存储父文档的存储层
store = InMemoryByteStore()
id_key = "doc_id"
# 检索器(开始时为空)
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)))
sub_docs = vectorstore.similarity_search("布赖尔法官")
sub_docs
[Document(page_content='谁被提名担任美国最高法院法官?', metadata={'doc_id': '0b3a349e-c936-4e77-9c40-0a39fc3e07f0'}),
 Document(page_content='2010年罗伯特·莫里斯对文档作者的建议的背景和内容是什么?', metadata={'doc_id': 'b2b2cdca-988a-4af1-ba47-46170770bc8c'}),
 Document(page_content='个人情况如何影响放弃领导Y Combinator的决定?', metadata={'doc_id': 'b2b2cdca-988a-4af1-ba47-46170770bc8c'}),
 Document(page_content='作者在1999年夏季离开Yahoo的原因是什么?', metadata={'doc_id': 'ce4f4981-ca60-4f56-86f0-89466de62325'})]
retrieved_docs = retriever.get_relevant_documents("布赖尔法官")
len(retrieved_docs[0].page_content)
9194