继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

LLM学习日记:拆解分析官方MultiVectorRetriever示例代码

RISEBY
关注TA
已关注
手记 493
粉丝 70
获赞 317

在回顾[对LangChain的全面分析]时,我决定更深入地研究Retriever。于是,我决定仔细研究官方文档中的MultiVectorRetriever示例。

多向量检索器 | 🦜️🔗 LangChain | 🦜️🔗 LangChain

通常情况下,为每个文档存储多个向量是有好处的,在这种情况下,有多种应用场景需要这样做……python.langchain.com

粗略一看,我并没有太懂。我有点懵。首先,我搞不懂这段代码最后要干啥。

    # 检索工具返回更大的块
    len(retriever.invoke("justice breyer")[0].page_content) 

思考了一会儿,阅读了代码后,我突然恍然大悟。看这部分代码,哦,我明白了。

运行这行代码来获取布莱尔大法官的信息:retriever.invoke("justice breyer")[0]

我突然明白,在 invoke() 方法中,它不仅使用向量存储进行相似性搜索,而且还在文档中查找具有相同 id 的内容。这时一切都豁然开朗了。

在这份官方文档中间提到:

有时获取较大的信息片段是有用的,但嵌入较小的信息片段。这能让嵌入尽可能精确地捕捉语义,同时也尽可能多地传递上下文信息。

向量搜索,或称为相似度搜索,在使用更小的部分时会更加准确。我了解到这个工具MultiVectorRetriever 对更小的部分执行向量搜索,但以更大的部分检索原始文档数据。这就是它的操作方式!

当我回头查看这段代码,试图理解为什么它会这样工作时,我注意到在创建MultiVectorRetriever对象时,他们设定了id_key="doc_id"参数。这个细节非常微妙,很容易被忽视。

    id_key = "doc_id"  # 文档的ID键
    // 检索器一开始为空
    retriever = MultiVectorRetriever(  
        vectorstore=vectorstore,  
        byte_store=store,  
        id_key=id_key,  
    )

在以下代码中,docs进一步细分为sub_docs,但这些sub_docs使用名为id_key=”doc_id”的元数据被分配了与其父doc相同的id。这种设置允许Retriever使用向量存储在sub_doc级别上执行精确的相似性搜索,然后使用匹配sub_docs的“doc_id”检索包含更广泛上下文信息的docs。

    # 用于将文本分割成更小块的拆分器,chunk_size 为 400
    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)))  # 将文档 ID 和文档列表进行多集合设置,用于更新文档存储

这里,doc.metadata[id_key] = _id 设置了 sub_doc 的元数据,其键名为 id_key(即 “doc_id”)。因为之前在 MultiVectorRetriever 的构造函数中指定了 ‘doc_id’,检索器可以利用它来将 sub_doc 和 doc 联系起来。因此,通过调用检索器,MultiVectorRetriever 可以在向量存储中执行相似度搜索,找到匹配的 sub_doc 后,利用它们的 ‘doc_id’ 在 byte_store 中查找具有相同 id 的 doc 并返回它们。

    // 检索器调用“金斯布雷尔”
    retriever.invoke("金斯布雷尔")

有了这个核心概念后,当我再次查看样本时,一切变得清晰多了。下面从头开始回顾一下当初让我感到困惑的部分。

    加载器们 = [  
        TextLoader("../../paul_graham_essay.txt"),  
        TextLoader("../../state_of_the_union.txt"),  
    ]  
    文档列表 = []  
    for 加载器 in 加载器们:  
        文档列表.extend(加载器.load_docs())

这两个 txt 文件,paul_graham_essay.txt 和 state_of_the_union.txt,通过调用 docs.extend(loader.load()) 被合并到一个单一的 docs 列表中。(它们不是以文件为界限的嵌套列表。)

接下来,使用RecursiveCharacterTextSplitter将这个文档列表进一步分割成最多10000字符的块,并将这些块以列表形式存储在docs中。

    # 文本分割器,用于将长文本分割成较小的块
    text_splitter = RecursiveCharacterTextSplitter(块大小=10000)  
    # 将文档列表中的每个文档分割成多个较小的文档
    文档列表 = text_splitter.split_documents(文档列表)

每个在此文档列表中的项目都是一个doc。对于每个doc,都会为其生成一个UUID,并将其存储在doc_ids列表中。

    doc_ids = [str(uuid.uuid4()) for _ in docs]  
    # 用于将文本分割成更小块的拆分器,块大小为400  
    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)

接下来,每个文档会被细分为最多400个字符的片段。因为每个文档最初已分割成最多10000个字符的部分,这进一步将其分成大约25个部分,生成子文档。在将这些添加到子文档之前,每个子文档(暂时存储在代码中的变量_doc中)会将其元数据名称设置为“doc_id”,包含其原始文档(父文档)的ID。(换句话说,多个子文档可以共享同一个doc_id。)

sub_docs 被添加到向量存储库中,而文档则被添加到文档库中。

    retriever.vectorstore.add_documents(sub_docs)  # 添加子文档到向量存储
    retriever.docstore.mset(list(zip(doc_ids, docs)))  # 设置文档ID和文档列表

文档存储通过在创建检索器对象时传递给byte_store参数的InMemoryByteStore对象来处理。

这设置了MultiVectorRetriever来进行搜索,允许我们在向量数据库中搜索相似的子文档。

    # 向量存储仅检索小段文本:
    retriever.vectorstore.similarity_search("justice breyer")[0]

或者,如前面所说,使用 invoke() 来执行对 sub_docs -> docs 的协调查找:

    # 检索插件返回更大的段落
    len(retriever.invoke("justice breyer")[0].page_content)

虽然LangChain的官方示例因为解释不够详细可能显得不够友好,但代码本身其实很友好,显示出友好之意,透露出一种传达意图的愿望。

待续。

感谢您细读这份关于MultiVector Retriever示例代码的详细分析。希望这份解析能帮助您更好地理解,并能帮助您更清晰地理解LangChain中一些复杂的功能。

虽然这次我们深入探讨了MultiVector Retriever,但人工智能和语言技术的探索是一个持续的探索之旅。在这一快速发展的领域里,总有更多东西等着我们去学习和发现。

如果你对这篇博客有任何疑问,或者想讨论关于OpenAI API、LLM或LangChain的开发项目,欢迎直接联系我。你可以直接联系我:

联系人邮箱:mizutori@goldrushcomputing.com

在 Goldrush Computing,我们自豪于作为一家日本公司并拥有母语为日语的专家。我们擅长开发针对日语和日本文化的提示和RAG系统。如果您需要为日本市场优化AI解决方案或开发日语应用程序,我们能为您提供独特的帮助。对于需要特定日本市场AI优化的合作或项目,欢迎随时联系我们。

敬请关注我即将发布的新博客,关于AI和大型语言模型!

继续阅读这个系列的下一部分:

LLM学习日记:解码LangChain官方的多模态RAG示例 LangChain官方示例里有一个很好的多模态RAG案例,所以我这次决定仔细研究一下,从头到尾……medium.com](https://medium.com/llm-study-diary-a-beginners-path-through-ai/llm-study-diary-decoding-langchains-official-multimodal-rag-sample-b4a645bdabe2?source=post_page-----e6d8360d10e0--------------------------------)
打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP