LangChain 怎麼玩?用 Document Loaders / Text Splitter 處理多種類型的資料

Posted on  Mar 7, 2024  in  LangChain , Python 程式設計 - 高階  by  Amo Chen  ‐ 6 min read

有一些 LLM 應用有提供上傳檔案並為使用者產生摘要、查詢答案的功能,譬如 ChatGPT Plus 與 ChatGPT Enterprise 可以上傳 PDF, CSV 檔等,除了能產生摘要、擷取重點資料之外,還可以進行一些數據分析,相當強大。

要做到這些強大的功能之前,有個前置作業是我們需要知道怎麼載入不同類型的資料,以及怎麼處理這些資料,而這些處理程序在 LangChain 的框架下,也有相對應的函式庫可以使用,並不需要從零開始打造。

本文將教導如何使用 LangChain 處理多種類型的資料。

本文環境

$ pip install langchain beautifulsoup4

本文需要 Ollama 指令與 Meta 公司提供的 llama2 模型(model),ollama 指令請至 Ollama 官方頁面下載安裝,安裝完成之後即可執行指令安裝 llama2 模型,其安裝指令如下:

$ ollama run llama2

p.s. 執行上述模型需要至少 8GB 記憶體, 3.8G 硬碟空間

Data Connection / Retrieval

LangChain 怎麼玩? Retrieval 篇,來做個聊天機器人(ChatBot)吧 提到過 Data Connection 與 Retrieval, 這個流程能將語言模型(language model)之外的資料與語言模型進行連結:

Data Connection

我們已經在前述教學文體驗過一遍 Data Connection 流程,不過我們並未對如何處理流程圖中的 Source 多作著墨,可以注意到 Source 步驟中以多個圖示表示資料的來源五花八門,可能有圖片、文件、網頁、 email, PDF 等等,這就是本文的重點 — 如何載入多種來源資料。

Document Loaders 與 Text Splitters

在 LangChain 的設計中,載入不同類型資料的功能稱為 “Document Loaders”, 預設支援 CSV, HTML, JSON, Markdown, PDF 等等, Document Loaders 可以從文件之中擷取文本資料與 metadata, 最後轉成統一的 LangChain Document 實例(instance),以方便進行後續的處理步驟,例如擷取網頁內的所有文字,並且將網頁的標題、語言等資訊存在 metadata, 傳回 Document(page_content="網頁內容", metadata={"title": "網頁標題", ...}) 實例。

在這些類型的資料之外, LangChain 社群也整合多種服務可供使用,包含 AWS S3, Azure Blob Storage, Cassandra, DuckDB, Email, ePub, Etherscan, Google BigQuery, Google Bigtable 等等,可說是一應俱全,各位在打造 LangChain 應用時,可以到此列表查看是否已經有相關 Document Loaders 可以使用。

理想上,如果載入資料之後不做任何處理就能給語言模型使用是最好的。

不過語言模型能接受的 tokens 數量有限(或稱 context window),所以不免要對 Document 做進一步的處理,將 Document 切成多個更小的區塊(chunk), 這個步驟是由 LangChain 的 Text Splitters 所提供,它的大致運作過程是:

  1. 將文本(text)切成多個小的且有語意的區塊(chunk),通常是以句子為單位進行切塊,要注意過小的區塊容易喪失語意
  2. 將第 1 步所產生的多個區塊進行合併,合併成所設定的區塊長度(chunk size)
  3. 為了保留前後文(上下文)的情境,進一步產生含有 overlap 的區塊

畫成圖表示的話,區塊就是下列樣貌,可以看到每個區塊會有重疊(overlap),藉此保留前後文情境:

langchain-chunks.png

這個步驟帶來幾個好處:

  1. 可以克服 Tokens 數量上限的問題。
  2. 讓語言模型可以專注在夠小的 chunks 上,而且在 Retrieval 階段,不相干/不相似的 chunks 的更容易被排除在外,進而提升語言模型的回答準確度/品質。

有好處之外,也會有壞處:

  1. 前後文的情境(context)可能消失,譬如某段落所提及的情境,是在很多個段落之前,譬如某處提到 the year ,但是 the year 是在某章節提到的 2024 ,這種情況就可能讓語言模型無法正確意識到情境中的 year 是指 2024 年。
  2. 增加複雜度(complexity),譬如 Text Splitter 的邏輯跟傳統的斷詞比較,就變比較複雜,如果要做到更好的處理步驟,譬如增加更多邏輯改善前後文的問題,也會再拉高複雜度,甚至在 Vector Store 的查詢檢索(retrieve)步驟也可能受到影響。

目前 LangChain 有多種 Text Splitters, 包含:

  1. Recursive
  2. HTML
  3. Markdown
  4. Code
  5. Tokens
  6. Character
  7. Semantic Chunker (實驗階段)

每個 Text Splitter 都有適合的情境,不過多數情況用 Recursive 即可,這也是 LangChain 目前推薦的方式,詳情請參考 Text Splitters

Data Connection

認識 Document Loaders 與 Text Splitters 之後,就更能理解 Data Connection 流程中的 Source -> Load -> Transform 做了什麼事,以及實際上要用 LangChain 的什麼元件進行處理。

實際拿新聞做示範

接下來,拿個 Elon Musk 狀告 OpenAI 的新聞示範 Source -> Load -> Transform 的過程。

WebBaseLoader

我們以比較實用的 WebBaseLoader 作為示範,擷取 CNN 新聞的網頁內容:

from langchain_community.document_loaders import WebBaseLoader

loader = WebBaseLoader("https://edition.cnn.com/2024/03/06/tech/openai-elon-musk-emails/index.html")
documents = loader.load()

上述 3 行程式碼,就能夠輕鬆抓到 CNN 新聞網頁的資料,並且轉為 Document 。

好奇的話,可以查看一下 documents 的內容:

[Document(page_content=" \n\n\n\n\n\n\n\n\nOpenAI publishes Elon Musk’s emails. ‘We’re sad that it’s come to this’ | CNN Business\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nCNN values your feedback\n\n\n\n\n                                                        1. How relevant is this ad to you?\n
...略...)]

仔細看的話,可以發現其實 WebBaseLoader 所擷取到的資料並不都是新聞內容,還有包含廣告、新聞類別、其他新聞連結等內容,這是因為 WebBaseLoader 預設只單純下載網頁內容而已,這些網頁內容裡的雜訊當然也會影響語言模型的回應生成,不過在經過 Text Splitter 切成小區塊之後,這些雜訊很可能會在檢索階段被濾掉,因此影響會小一點,但如果可以的話,還是可以過濾掉最好,作法除了找看看有沒有相對應的 Document Loaders 可以使用之外,也可以自己客製 1 個 Document Loader 試試(其實 WebBaseLoader 是使用 Beautiful Soup 實作的,可以繼承 WebBaseLoader 並改寫 load() 方法加入過濾資料的邏輯即可) 。

WebBaseLoader 除了本文使用的方法之外,也支援下載多個網頁內容以及代理伺服器(proxy)、 asyncio, rate limit 等功能,使用上也很簡單,可以參閱 WebBaseLoader 文件

RecursiveCharacterTextSplitter

得到 Documents 之後,可以對資料做一些加工,也就是 Transform 的步驟,這個步驟可以使用 RecursiveCharacterTextSplitter 把 Documents 切成多個較小的 chunks, 這些 chunks 也只是內容較短的 Documents:

text_splitter = RecursiveCharacterTextSplitter()
docs = text_splitter.split_documents(documents)

如果想要調整 chunk 的大小,可以調整 chunk_sizechunk_overlap 2 個參數:

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,
    chunk_overlap=30,
)
docs = text_splitter.split_documents(documents)

其他還有參數可以調整,例如 length_function, is_separator_regex, keep_separator 等,詳見 RecursiveCharacterTextSplitter 原始碼。

基本上只要調整 chunk_sizechunk_overlap 就很足夠了。

完整程式碼

結合語言模型的程式碼範例如下:

from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_community.document_loaders import WebBaseLoader
from langchain.chains import create_retrieval_chain
from langchain_core.prompts import ChatPromptTemplate
from langchain_community.llms import Ollama
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_text_splitters import RecursiveCharacterTextSplitter


llm = Ollama(model='llama2')
embeddings = OllamaEmbeddings()

loader = WebBaseLoader("https://edition.cnn.com/2024/03/06/tech/openai-elon-musk-emails/index.html")
documents = loader.load()
text_splitter = RecursiveCharacterTextSplitter()
docs = text_splitter.split_documents(documents)

vector = FAISS.from_documents(docs, embeddings)
retriever = vector.as_retriever()

prompt = ChatPromptTemplate.from_messages([
    ('system', 'Answer the user\'s questions based on the below context:\n\n{context}'),
    ('user', 'Question: {input}'),
])
document_chain = create_stuff_documents_chain(llm, prompt)

retrieval_chain = create_retrieval_chain(retriever, document_chain)

context = []
input_text = input('>>> ')
while input_text.lower() != 'bye':
    response = retrieval_chain.invoke({
        'input': input_text,
        'context': context
    })
    print(response['answer'])
    context = response['context']
    input_text = input('>>> ')

上述範例執行之後,可以問語言模型誰告了 OpenAI:

>>> Who sued OpenAI last week?

而語言模型的回應也告訴我們是 Elon Musk (OpenAI 曾經的共同創辦人),甚至在 California state court 告的,以及告的理由、訴求都有正確提到:

Elon Musk, the co-founder of OpenAI, sued the company last week in California state court. Musk is asking for a jury trial and for the company, Altman, and co-founder Greg Brockman to pay back the profit they received from the business. The lawsuit alleges that the company's partnership with Microsoft violated OpenAI's founding charter and represents a breach of contract.

這就是使用 Document Loaders 以及 Text Splitters 的範例。

對了,如果把 docs = text_splitter.split_documents(documents) 改為 docs = documents 不做 Transform, 就會發現 Llama2 會胡說八道(hallucination),大家可以試試。

總結

本文到此,我們已經知道要用什麼元件處理不同類型的資料,以及怎麼用 Text Splitter 將資料切成多個較小區塊,以利語言模型進行處理。本文範例是以 1 則固定的網址作為範例,各位可以從此範例延伸,結合 Agents 就能夠做到更有彈性的 LangChain 應用!

以上!

Enjoy!

References

LangChain - Document loaders

LangChain - 3rd Party Document loaders

LangChain - Text Splitters

LangChain GitHub Repo - TextSplitter

LangChain GitHub Repo - RecursiveCharacterTextSplitter

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!