Langchain-ChatGLM原始碼解讀(一)-文件資料上傳

有何m不可發表於2024-03-14

一、簡介

Langchain-ChatGLM 相信大家都不陌生,近幾周計劃出一個原始碼解讀,先解鎖langchain的一些基礎用法。

文件問答過程大概分為以下5部分,在Langchain中都有體現。

  1. 上傳解析文件
  2. 文件向量化、儲存
  3. 文件召回
  4. query向量化
  5. 文件問答
Langchain-ChatGLM原始碼解讀(一)-文件資料上傳

今天主要講langchain在上傳解析文件時是怎麼實現的。

二、文件解析邏輯,以txt型別的檔案解析為例子

step1:尋找上傳邏輯入口:local_doc_qa.py ,關注TextLoader(),ChineseTextSplitter()

def load_file(filepath, sentence_size=SENTENCE_SIZE, using_zh_title_enhance=ZH_TITLE_ENHANCE):
    if filepath.lower().endswith(".md"):
        loader = UnstructuredFileLoader(filepath, mode="elements")
        docs = loader.load()
    elif filepath.lower().endswith(".txt"):
        loader = TextLoader(filepath, autodetect_encoding=True)
        textsplitter = ChineseTextSplitter(pdf=False, sentence_size=sentence_size)
        docs = loader.load_and_split(textsplitter)

step2:解析txt/pdf等原始檔案,不同型別的檔案有不同種類多Loader,比如txt檔案有TextLoader,具體load()實現如下:

def load(self) -> List[Document]:
    """Load from file path."""    text = ""    
    try:
        with open(self.file_path, encoding=self.encoding) as f:
            text = f.read()
    except UnicodeDecodeError as e:
        if self.autodetect_encoding:
            detected_encodings = detect_file_encodings(self.file_path)
            for encoding in detected_encodings:
                logger.debug("Trying encoding: ", encoding.encoding)
                try:
                    with open(self.file_path, encoding=encoding.encoding) as f:
                        text = f.read()
                    break                except UnicodeDecodeError:
                    continue        else:
            raise RuntimeError(f"Error loading {self.file_path}") from e
    except Exception as e:
        raise RuntimeError(f"Error loading {self.file_path}") from e

    metadata = {"source": self.file_path}
    return [Document(page_content=text, metadata=metadata)]

此時輸入:file_path,輸出:List[Document],其中,Document物件如下:

page_content:原生txt讀取的所有內容

metadata:檔案地址路徑,metadata = {"source": self.file_path}

class Document(BaseModel):
    """Interface for interacting with a document."""    
    page_content: str    
    metadata: dict = Field(default_factory=dict)

step3:load_and_split()進行文件切割

輸入:Optional[TextSplitter],輸出:List[Document]

def load_and_split(self, text_splitter: Optional[TextSplitter] = None) -> List[Document]:
    """Load documents and split into chunks."""    
    if text_splitter is None:
        _text_splitter: TextSplitter = RecursiveCharacterTextSplitter()
    else:
        _text_splitter = text_splitter
    docs = self.load()
    return _text_splitter.split_documents(docs)

然後在split_documents()中分別對documents進行解壓縮,最後巢狀呼叫create_documents()方法,

def split_documents(self, documents: List[Document]) -> List[Document]:
    """Split documents."""    
    texts = [doc.page_content for doc in documents]
    metadatas = [doc.metadata for doc in documents]
    return self.create_documents(texts, metadatas=metadatas)

create_documents()方法將每一篇文件傳入split_text(text)進行解析,其中

輸入:texts: List[str],輸出:List[Document]

def create_documents(
    self, texts: List[str], metadatas: Optional[List[dict]] = None) -> List[Document]:
    """Create documents from a list of texts."""    
    _metadatas = metadatas or [{}] * len(texts)
    documents = []
    for i, text in enumerate(texts):
        for chunk in self.split_text(text):
            new_doc = Document(
                page_content=chunk, metadata=copy.deepcopy(_metadatas[i])
            )
            documents.append(new_doc)
    return documents

可以看到又進行了一個函式巢狀,最後定位到chinese_text_splitter.py的split_text(),其中具體邏輯就不展示了,這一塊也是一個方法過載的過程,溯源起來發現ChineseTextSplitter中的split_text()繼承了TextSplitter中的split_text()抽象方法然後進行例項化。例項化後的split_text()幹了件啥事?就是把一篇文件按要求切成多個chunk,最後放在一個list裡。

輸入:text: str,輸出:List[str]

def split_text(self, text: str) -> List[str]:   ##此處需要進一步最佳化邏輯
    paas

step4:load_and_split()進行文件切割把切好的chunk和對應的metadata重新組合形成一個Document物件,需要注意的是⚠️,此時Document物件放的是文章的某一個chunk,而不是整篇文章。最後返回一個List

def create_documents(
    self, texts: List[str], metadatas: Optional[List[dict]] = None) -> List[Document]:
    """Create documents from a list of texts."""    
    _metadatas = metadatas or [{}] * len(texts)
    documents = []
    for i, text in enumerate(texts):
        for chunk in self.split_text(text):
            new_doc = Document(
                page_content=chunk, metadata=copy.deepcopy(_metadatas[i])
            )
            documents.append(new_doc)
    return documents

三、雜談

如果想加入自己的文件切割方法該怎麼做?可以重寫一個類似於ChineseTextSplitter()方法就可以了!

胖友,請不要忘了一鍵三連點贊哦!

轉載請註明出處:QA Weekly

相關文章