今天,開始寫一個新的系列《大模型RAG實戰(zhàn)》。上個月我在2篇文章中,介紹了如何使用LlamaIndex框架,通過少量代碼,實現(xiàn)本地可部署和運行的大模型RAG問答系統(tǒng)。我們要開發(fā)一個生產(chǎn)級的系統(tǒng),還需要對LlamaIndex的各個組件和技術(shù)進行深度的理解、運用和調(diào)優(yōu)。本系列將會聚焦在如何讓系統(tǒng)實用上,包括:知識庫的管理,檢索和查詢效果的提升,使用本地化部署的模型等主題。我將會講解相關(guān)的組件和技術(shù),輔以代碼示例。最終這些文章中的代碼將形成一套實用系統(tǒng)。
過去一年,大模型的發(fā)展突飛猛進。月之暗面的Kimi爆火,Llama3開源發(fā)布,大模型各項能力提升之大有目共睹。
對于大模型檢索增強生成(RAG)系統(tǒng)來說,我們越來越認識到,其核心不在于模型的能力,而是在于如何更好地構(gòu)建和使用知識庫,提升檢索的效能。
1
知識庫的三類數(shù)據(jù)
無論是企業(yè)還是個人,我們構(gòu)建知識庫,自然希望高質(zhì)量的數(shù)據(jù)越多越好。這些數(shù)據(jù)主要分為三類。
文件:電腦上為數(shù)眾多的文件資料,包括方案、文稿、產(chǎn)品資料等,通常為PDF、DOCX、PPT等格式
網(wǎng)頁:收集的網(wǎng)頁信息,比如我們在微信上打開和閱讀的公眾號文章。如果覺得這些文章有用,會收藏起來
數(shù)據(jù)庫:保存在各種數(shù)據(jù)庫中的文本信息,比如企業(yè)內(nèi)部的信息系統(tǒng),會記錄用戶提出的問題與相應的解決方案
對于個人用戶,建立個人知識庫,主要是電腦上的文件資料和收藏的網(wǎng)頁信息。而對于企業(yè)來說,接入和利用已有信息系統(tǒng)的數(shù)據(jù)庫中的文本數(shù)據(jù),也非常關(guān)鍵。
LlamaIndex是一個專門針對構(gòu)建RAG系統(tǒng)開發(fā)的開源數(shù)據(jù)框架,對于以上三類數(shù)據(jù)的處理,都提供了很好的支持。
2
數(shù)據(jù)處理的四個步驟
無論是哪一類數(shù)據(jù),LlamaIndex處理數(shù)據(jù)的過程,都分為四步:
1)加載數(shù)據(jù)(Load)
LlamaIndex提供了眾多數(shù)據(jù)接入組件(Data Connector),可以加載文件、網(wǎng)頁、數(shù)據(jù)庫,并提取其中的文本,形成文檔(Document)。未來還將能提取圖片、音頻等非結(jié)構(gòu)化數(shù)據(jù)。
最常用的是SimpleDirectoryReader,用來讀取文件系統(tǒng)指定目錄中的PDF、DOCX、JPG等文件。
我們在LlamaHub上,可以找到數(shù)百個數(shù)據(jù)接入組件,用來加載各種來源與格式的數(shù)據(jù),比如電子書epub格式文件的接入組件。
2) 轉(zhuǎn)換數(shù)據(jù)(Transform)
類似傳統(tǒng)的ETL,轉(zhuǎn)換數(shù)據(jù)是對文本數(shù)據(jù)的清洗和加工。通常,輸入是文檔(Document),輸出是節(jié)點(Node)。
數(shù)據(jù)處理的過程,主要是將文本分割為文本塊(Chunk),并通過嵌入模型,對文本塊進行嵌入(Embedding)。同時,可提取元數(shù)據(jù)(Metadata),例如原文件的文件名和路徑。
這里有兩個重要的參數(shù),chunk_size和chunk_overlap,分別是文本塊分割的大小,和相互之間重疊的大小。我們需要調(diào)整這些參數(shù),從而達到最好的檢索效果。
3)建立索引(Index)
索引是一種數(shù)據(jù)結(jié)構(gòu),以便于通過大語言模型(LLM)來查詢生成的節(jié)點。
最常使用的是向量存儲索引(Vector Store Index),這種方式把每個節(jié)點(Node)的文本,逐一創(chuàng)建向量嵌入(vector embeddings),可以理解是文本語義的一種數(shù)字編碼。
不同于傳統(tǒng)的關(guān)鍵詞匹配,通過向量檢索,比如余弦相似度,我們可以找到語義相近的文本,盡管在文字上有可能截然不同。
4)存儲數(shù)據(jù)(Store)
默認情況下,以上操作生成的數(shù)據(jù)都保存在內(nèi)存中。要避免每次重來,我們需要將這些數(shù)據(jù)進行持久化處理。
LlamaIndex 提供了內(nèi)置的persisit()方法,將索引數(shù)據(jù)持久化保存在磁盤文件中。更常見的做法是通過索引存儲器(Index Store),將索引保存在向量數(shù)據(jù)庫中,如Chroma、LanceDB等。
知識庫的管理,不僅要保存索引數(shù)據(jù),也要保存所有文檔(Document)及其提取的節(jié)點(Node)。
LLamaIndex提供了文檔存儲器(Document Store),把這些文本數(shù)據(jù)保存在MongoDB、Redis等NoSQL數(shù)據(jù)庫中,這樣我們可以對每一個節(jié)點進行增刪改查。
3
代碼實現(xiàn)示例
下面結(jié)合代碼,介紹構(gòu)建知識庫的過程。
我們將使用LlamaIndex來加載和轉(zhuǎn)換文檔和網(wǎng)頁數(shù)據(jù),建立向量索引,并把索引保存在Chroma,把文檔和節(jié)點保存在MongoDB。
示例1:加載本地文件
對于在本地文件系統(tǒng)中的文件,LlamaIndex提供了非常簡便的方法來讀?。?strong>SimpleDirectoryReader。
我們只需要將已有的文件,放在指定的目錄下,比如./data,通過這個方法就可以全部加載該目錄下的所有文件,包括子目錄中的文件。
from llama_index.core import SimpleDirectoryReader
documents = SimpleDirectoryReader(input_dir='./data', recursive=True).load_data()
print(f'Loaded {len(documents)} Files')
如果,我們在知識庫中上傳了新的文件,還可以指定加載這個文件,而非讀取整個目錄。
SimpleDirectoryReader(input_files=['path/to/file1', 'path/to/file2'])
文件加載后,LlamaIndex會逐一提取其中的文本信息,形成文檔(Document)。通常一個文件對應一個文檔。
示例2:加載網(wǎng)頁信息
LlamaIndex讀取和加載網(wǎng)頁信息也很簡單。這里,我們用到另一個工具SimpleWebPageReader。
給出一組網(wǎng)頁的URL,我們使用這個工具可以提取網(wǎng)頁中的文字信息,并分別加載到文檔(Document)中。
pages = ['https://mp.weixin.qq.com/s/LQNWNp7nI_hI1iFpuZ9Dfw',
'https://mp.weixin.qq.com/s/m8eI2czdXT1knX-Q20T6OQ',
'https://mp.weixin.qq.com/s/prnDzOQ8HjUmonNp0jhRfw',
'https://mp.weixin.qq.com/s/YB44--865vYkmUJhEv73dQ',
'https://mp.weixin.qq.com/s/SzON91-fZgkQvzdzkXJoCA',
'https://mp.weixin.qq.com/s/zVlKUxJ_C6GjTh0ePS-f8w',
'https://mp.weixin.qq.com/s/gXlX_8mKnQSmI0R1me40Ag',
'https://mp.weixin.qq.com/s/z9eV8Q8TJ4c-IhvJ1Ag34w']
documents = SimpleWebPageReader(html_to_text=True).load_data(
pages
)
print(f'Loaded {len(documents)} Web Pages')
代碼中我給出的網(wǎng)頁,是我寫的《大衛(wèi)說流程》系列文章。你可以改為任何你想讀取的網(wǎng)頁。未來,你可以針對這些網(wǎng)頁內(nèi)容來向大模型提問。
使用這個工具,我們需要安裝llama-index-readers-web和html2text組件。為了行文簡潔,未來不再說明。你可以在運行代碼時根據(jù)提示,安裝所需的Python庫和組件。
示例3:分割數(shù)據(jù)成塊
接下來,我們通過文本分割器(Text Splitter)將加載的文檔(Document)分割為多個節(jié)點(Node)。
LlamaIndex使用的默認文本分割器是SentenceSplitter。我們可以設(shè)定文本塊的大小是256,重疊的大小是50。文本塊越小,那么節(jié)點的數(shù)量就越多。
from llama_index.core.node_parser import SentenceSplitter
nodes = SentenceSplitter(chunk_size=256, chunk_overlap=50).get_nodes_from_documents(documents)
print(f'Load {len(nodes)} Nodes')
之前我介紹過文本分割器Spacy,對中文支持更好。我們可以通過Langchain引入和使用。
from llama_index.core.node_parser import LangchainNodeParser
from langchain.text_splitter import SpacyTextSplitter
spacy_text_splitter = LangchainNodeParser(SpacyTextSplitter(
pipeline='zh_core_web_sm',
chunk_size = 256,
chunk_overlap = 50
))
示例4:數(shù)據(jù)轉(zhuǎn)換管道與知識庫去重
上一步給出的數(shù)據(jù)轉(zhuǎn)換方法,其實并不實用。問題在于沒有對文檔進行管理。我們重復運行時,將會重復加載,導致知識庫內(nèi)重復的內(nèi)容越來越多。
為了解決這個問題,我們可以使用LlamaIndex提供的數(shù)據(jù)采集管道(Ingestion Pipeline)的功能,默認的策略為更新插入(upserts),實現(xiàn)對文檔進行去重處理。
from llama_index.core.ingestion import IngestionPipeline
pipeline = IngestionPipeline(
transformations=[
spacy_text_splitter,
embed_model,
],
docstore=MongoDocumentStore.from_uri(uri=MONGO_URI),
vector_store=chroma_vector_store,
)
nodes = pipeline.run(documents=documents)
print(f'Ingested {len(nodes)} Nodes')
示例5:索引與存儲的配置
在上面的數(shù)據(jù)采集管道的代碼示例中,我們配置了用來生成向量索引的嵌入模型(embed_model),以及采用Chroma作為向量庫,MongoDB作為文檔庫,對數(shù)據(jù)進行持久化存儲。
嵌入模型的配置如下。這里我們通過之前介紹過的HugginFace的命令行工具,將BAAI的bge-small-zh-v1.5嵌入模型下載到本地,放在“l(fā)ocalmodels”目錄下。
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
embed_model = HuggingFaceEmbedding(model_name='./localmodels/bge-small-zh-v1.5')
然后配置向量庫,Chroma將把數(shù)據(jù)存儲在我們指定的“storage”目錄下。
import chromadb
from llama_index.vector_stores.chroma import ChromaVectorStore
db = chromadb.PersistentClient(path='./storage')
chroma_collection = db.get_or_create_collection('think')
chroma_vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
我們可以使用Redis或MongoDB來存儲處理后的文檔、節(jié)點及相關(guān)信息,包括文檔庫(docstore)和索引信息庫(index_store)。
作為示例,我們選用在本機上安裝的MongoDB。
from llama_index.core import StorageContext
from llama_index.storage.docstore.mongodb import MongoDocumentStore
from llama_index.storage.index_store.mongodb import MongoIndexStore
MONGO_URI = 'mongodb://localhost:27017'
storage_context = StorageContext.from_defaults(
docstore=MongoDocumentStore.from_uri(uri=MONGO_URI),
index_store=MongoIndexStore.from_uri(uri=MONGO_URI),
vector_store=chroma_vector_store,
)
示例6:構(gòu)建知識庫
現(xiàn)在,我們可以將此前的數(shù)據(jù)采集管道生成的文檔和節(jié)點,載入到文檔知識庫中(docstore)。
storage_context.docstore.add_documents(nodes)
print(f'Load {len(storage_context.docstore.docs)} documents into docstore')
這步完成后,我們在MongoDB中,可以找到一個名為“db_docstore”的數(shù)據(jù)庫,里面有三張表,分別是:
docstore/data
docstore/metadata
docstore/ref_doc_info
我們可以通過MongoDB,來查詢相關(guān)的文檔和節(jié)點,元數(shù)據(jù)以及節(jié)點之間的關(guān)系信息。
未來,當你有更多的文件和網(wǎng)頁需要放入知識庫中,只需要遵循以上的步驟加載和處理。
示例7:實現(xiàn)RAG問答
完成知識庫的構(gòu)建之后,我們可以設(shè)定使用本地的LLM,比如通過Ollama下載使用Gemma 2B模型。
然后,加載索引,生成查詢引擎(Query Engine),你就可以針對知識庫中的內(nèi)容進行提問了。
from llama_index.llms.ollama import Ollama
llm_ollama = Ollama(model='gemma:2b', request_timeout=600.0)
Settings.llm = llm_ollama
from llama_index.core import VectorStoreIndex
index = VectorStoreIndex.from_vector_store(
vector_store=chroma_vector_store,
)
query_engine = index.as_query_engine()
response = query_engine.query('流程有哪三個特征?')
以上,主要介紹了使用LlamaIndex構(gòu)建知識庫的過程。
未來,我們可以結(jié)合Streamlit、Flask等前端框架,進一步開發(fā)成一個完善的知識庫管理系統(tǒng),以便對知識內(nèi)容進行持續(xù)的增加與更新,并支持靈活的配置文本分割的各項參數(shù)和選擇嵌入模型。
聯(lián)系客服