一、簡介
Langchain-ChatGLM 相信大家都不陌生,近幾周計劃出一個原始碼解讀,先解鎖langchain的一些基礎用法。
文件問答過程大概分為以下5部分,在Langchain中都有體現。
- 上傳解析文件
- 文件向量化、儲存
- 文件召回
- query向量化
- 文件問答
今天主要講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