1. 拆分文檔與檢索的沖突
在 RAG 應用開發中,文檔拆分 和 文檔檢索 通常存在相互沖突的愿望,例如:
- 我們可能希望擁有小型文檔,以便它們的嵌入可以最準確地反映它們的含義,如果太長,嵌入/向量沒法記錄太多文本特征。
- 但是又希望文檔足夠長,這樣能保留每個塊的上下文。
這個時候就可以考慮通過拆分子文檔塊
,檢索 父文檔塊
的策略來實現這種平衡,即在檢索中,首先獲取小塊,然后再根據小塊元數據中存儲的 id,使用 id 來查找這些塊的父文檔,并返回那些更大的文檔,該策略適合一些不是特別能拆分的文檔,或者是文檔上下文關聯性很強的場景
。
請注意,這里的“父文檔”指的是小塊來源的文檔,可以是整個原始文檔,也可以是切割后比較大的文檔塊
。
子文檔->父文檔
的運行流程也非常簡單,其實和 多向量檢索器 一模一樣,如下:
除了使用MultiVectorRetriever
來實現該運行流程,在 LangChain 中,還封裝了ParentDocumentRetriever
,可以更加便捷地完成該功能,使用技巧也非常簡單,傳遞 向量數據庫、文檔數據庫 和 子文檔分割器 即可。
代碼示例:
import dotenv
import weaviate
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import LocalFileStore
from langchain_community.document_loaders import UnstructuredFileLoader
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_weaviate import WeaviateVectorStore
from weaviate.auth import AuthApiKeydotenv.load_dotenv()# 1.創建加載器與文檔列表,并加載文檔
loaders = [UnstructuredFileLoader("./電商產品數據.txt"),UnstructuredFileLoader("./項目API文檔.md"),
]
docs = []
for loader in loaders:docs.extend(loader.load())# 2.創建文本分割器
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500,chunk_overlap=50,
)# 3.創建向量數據庫與文檔數據庫
vector_store = WeaviateVectorStore(client=weaviate.connect_to_wcs(cluster_url="https://mbakeruerziae6psyex7ng.c0.us-west3.gcp.weaviate.cloud",auth_credentials=AuthApiKey("xxxxxxxxxxxxxxxxxxxxxxxxxxxx"),),index_name="ParentDocument",text_key="text",embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
)
store = LocalFileStore("./parent-document")# 4.創建父文檔檢索器
retriever = ParentDocumentRetriever(vectorstore=vector_store,byte_store=store,child_splitter=text_splitter,
)# 5.添加文檔
retriever.add_documents(docs, ids=None)# 6.檢索并返回內容
search_docs = retriever.invoke("分享關于LLMOps的一些應用配置")
print(search_docs)
print(len(search_docs))
輸出內容會返回完整的文檔片段,而不是拆分后的片段(但是在向量數據庫中存儲的是分割后的片段)
[Document(metadata={'source': './項目API文檔.md'}, page_content='LLMOps 項目 API 文檔\n\n應用 API 接口統一以 JSON 格式返回,并且包含 3 個字段:code、data 和 message,分別代表業務狀態碼、業務數據和接口附加信息。\n\n業務狀態碼共有 6 種,其中只有 success(成功) 代表業務操作成功,其他 5 種狀態均代表失敗,并且失敗時會附加相關的信息:fail(通用失敗)、not_found(未找到)、unauthorized(未授權)、forbidden(無權限)和validate_error(數據驗證失敗)。\n\n接口示例:\n\njson\n{\n "code": "success",\n "data": {\n "redirect_url": "https://github.com/login/oauth/authorize?client_id=f69102c6b97d90d69768&redirect_uri=http%3A%2F%2Flocalhost%3A5001%2Foauth%2Fauthorize%2Fgithub&scope=user%3Aemail"\n },\n "message":...')]
2.父文檔檢索器檢索較大塊
在上面的示例中,我們使用拆分的文檔塊檢索數據原文檔,但是有時候完整文檔可能太大,我們不希望按原樣檢索它們
。在這種情況下,我們真正想要做的是先將原始文檔拆分成較大的塊(例如 1000-2000 個 Token),然后將其拆分為較小塊,接下來索引較小塊,但是檢索時返回較大塊(非原文檔)。
運行流程變更如下
在 ParentDocumentRetriever
中,只需要傳遞多一個 父文檔分割器 即可,其他流程無需任何變化,更新后的部分代碼如下
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)retriever = ParentDocumentRetriever(vectorstore=vector_store,byte_store=store,parent_splitter=parent_splitter,child_splitter=child_splitter,
)