一、介紹
1.1、背景
在使用 OpenAI 提供的 GPT 系列模型時,我們可能會發現對于一些簡單的問題,例如中文事實性問題,AI 往往會編造答案。而當詢問最近發生的新聞事件時,AI 會直接表示自己不知道未來21年的情況。
為了解決這個問題,ChatGPT 在釋出最新的GPT-4模型後也推出了插件子產品,可以支援通過插件的方式連接配接到外部第三方應用程式。然而,即使使用了第三方應用程式,我們也不能保證所需資訊恰好被其他人提供。此外,并非所有資訊和問題都适合向所有人公開,有些資訊可能僅限公司内部使用。
今天我們将探讨如何利用 OpenAI 的大型語言模型能力,并且僅在指定資料上進行操作以滿足我們的需求。
1.2、大語言模型的不足之處
為了測試ChatGPT的常識水準,我們詢問了一個普遍已知的問題:“魯迅先生去日本學習醫學的時候,他的導師是誰?”然而,ChatGPT給出的答案一會兒是兒島潤三,一會又是橋本秀夫,并不是大家所熟知的藤野先生。
這種情況的發生與大型語言模型的原理和訓練資料集有關。大型語言模型利用訓練樣本中文本之間的前後關系,通過前面的文本預測接下來出現的文本機率。如果某個特定答案在訓練樣本中頻繁出現,模型會收斂于該答案,并給出準确回答。然而,如果相關文本較少,訓練過程中會存在一定程度的随機性,導緻回答可能不準确或牽強。
在GPT-3模型中,雖然整體訓練語料很豐富,但其中隻有不到1%是中文語料。是以,在問及與中文相關的知識或常識問題時,模型給出的回答往往不可靠。
當然,我們可以采取一種解決方案:增加更多高品質的中文語料并使用這些資料重新訓練一個新模型。另外,在我們希望AI能夠正确回答問題時,我們可以收集相關資料,并利用OpenAI提供的"微調"(Fine-tune)接口,在基礎模型上進行進一步訓練。
這種方案确實是可以的,隻是成本可能會很高。對于上面舉的例子,如果隻是缺乏一些文本資料,這種方法可能還能接受。但對于時效性要求較高的資訊類資訊,這種方法就不太可行了。
例如,我們想讓AI告訴我們前一天的最新資訊新聞或者某個比賽的結果,顯然我們不太可能每隔幾個小時就進行單獨的訓練或微調模型,這樣幹成本太高了。
在處理此類問題時,我們可能需要考慮其他解決方案。例如,在AI模型中內建第三方資料源或API來擷取實時更新的資訊資訊,并與語言模型結合使用。這樣可以在不重新訓練或微調模型的情況下獲得及時和準确的答案。
1.3、Bing Chat 的解決方案
用過New Bing的朋友可能都知道,它每次回答之前就是先聯網搜尋,然後在組合提示傳回響應結果,微軟在Bing搜尋引擎中添加了ChatGPT的問答功能,效果似乎也不錯。那麼Bing是如何實作這一功能的呢?可能的解決方案就是先搜尋,後提示(Prompt)。
1)首先,我們通過搜尋的方式找到與所提問問題最相關的語料。可以使用傳統基于關鍵詞搜尋技術,也可以使用嵌入式相似度進行語義搜尋技術。
2)然後,将與問題語義最接近的幾條内容作為提示語提供給AI。請AI參考這些内容,并據此回答問題。
當我們将《藤野先生》中的兩個段落提供給AI,并要求它根據這些段落回答原始問題時,會得到正确的答案。您也可以自行測試一下。
這是利用大語言模型的常見模式之一。因為大語言模型本身具備兩種能力。
。例如,當我們問AI如何制作東坡肉時,它能夠給出準确回答是因為訓練語料中已經有了相關知識,我們通常稱之為"世界知識"。
。這種能力不需要在訓練語料中存在相同内容。大語言模型本身具備"思維能力",可以進行閱讀了解。在這個過程中,“知識”并不是由模型自己提供的,而是我們臨時提供給模型的上下文資訊。如果沒有提供上下文資訊,并再次問相同的問題,模型将無法回答。
1.4、LlamaIndex的應用場景
在之前的文章中,我介紹了一系列LLM相關的話題,主要還是圍繞 LangChain 去展開的,介紹了 LangChain 在文本摘要、、資料庫查詢、聊天機器人方面的應用案例, 是一個非常優化的架構,确實在流程組合方面有很強的優勢。當我們了解了LLM在生成和推理方面的強大之處,同時知道了GPT這種模型的局限後(隻在公開資料上訓練),其實在很多業務場景下可能更需要通過擴充專業性的知識庫來回答,是以如果你想去擴充預先訓練的知識庫,就需要插入上下文或微調模型。後者目前來講成本還是比較高的,可能不太适合正常的企業或者個人。
我們需要一個全面的工具包來幫助為 LLM 執行外部資料的擴充,這就是 的用武之地,LlamaIndex 是一個“資料架構”,可幫助您建構 LLM 應用程式。今天我們就來介紹如何使用 LlamaIndex 進行資料攝取和索引的上下文實戰。這裡的 LlamaIndex 跟前一陣臉書開源的 LLaMA 模型沒有任何關系。至于它和LangChain之間是個什麼關系,能否項目協同後面再來一一介紹。
二、什麼是LlamaIndex?
(以前稱為 GPT Index)是一個開源項目,它在 LLM 和外部資料源(如 API、PDF、SQL 等)之間提供一個簡單的接口進行互動。它提了供結構化和非結構化資料的索引,有助于抽象出資料源之間的差異。所需的上下文,處理當上下文視窗過大時的限制,并有助于在查詢期間在成本和性能之間進行權衡。
LllamaIndex 以專用索引的形式提供獨特的資料結構:
- 向量存儲索引:最常用,允許您回答對大型資料集的查詢。
- 樹索引:對于總結文檔集合很有用。
- 清單索引:對于合成一個結合了多個資料源資訊的答案很有用。
- 關鍵字表索引:用于将查詢路由到不同的資料源。
- 結構化存儲索引:對于結構化資料(例如 SQL 查詢)很有用。
- 知識圖譜索引:對于建構知識圖譜很有用。
LlamaIndex 還通過 提供資料連接配接器,LlamaHub 是一個開源存儲庫,包含了各種資料加載器,如本地目錄、Notion、Google Docs、Slack、Discord 等。
LlamaIndex 的目标是通過先進技術增強文檔管理,提供一種直覺有效的方法來使用LLM和創新索引技術搜尋和總結文檔。下圖為 LlamaIndex 的整體工作流程:
知識庫文檔被切分,每個切分被存儲在一個節點對象中,這些節點對象将與其他節點一起形成一個圖(索引)。這種切分的主要原因是LLM有限的輸入token容量,是以,在提示中以一種平滑、連續的方式提供大型文檔的政策将是有幫助的。
圖索引可以是一個簡單的清單結構、樹結構或關鍵字表。此外,還可以從不同的索引中組合一個索引。當我們想要将文檔組織成一個層次結構以獲得更好的搜尋結果時,這個很有用。例如,我們可以在Confluence、Google Docs和電子郵件上建立單獨的清單索引,并在清單索引上建立一個覆寫性的樹索引。
三、什麼是LangChain?
是一個開源庫,旨在建構具備 LLM 強大功能的應用程式。LangChain最初是用Python編寫的,現在也有一個Javascript實作。它可用于聊天機器人、文本摘要、資料生成、問答等應用場景。從廣義上講,它支援以下子產品:
- 提示:管理LLM作為輸入的文本。
- LLM:圍繞底層LLM的API包裝器。
- 文檔加載器:用于加載文檔和其他資料源的接口。
- Utils:用于計算或與其他來源(如嵌入、搜尋引擎等)互動的實用程式。
- 鍊:調用LLM和實用程式的順序;朗鍊的真正價值。
- 索引:合并自己的資料的最佳做法。
- 代理:使用 LLM 決定要執行的操作以及順序。
- 記憶體:代理或鍊調用之間的狀态持久性。
關于 LangChain 的具體應用案例,請檢視前面的幾篇文章。
四、LangChain 和 LLamaIndex 的差別與聯系
LlamaIndex的重點放在了Index上,也就是通過各種方式為文本建立索引,有通過LLM的,也有很多并非和LLM相關的。LangChain的重點在 Agent 和 Chain 上,也就是流程組合上。可以根據你的應用組合兩個,如果你覺得問答效果不好,可以多研究一下LlamaIndex。如果你希望有更多外部工具或者複雜流程可以用,可以多研究一下LangChain。
盡管LlamaIndex和LangChain在它們的主要賣點上有很多重疊,即資料增強的摘要和問答,但它們也有一些差別。LangChain提供了更細粒度的控制,并覆寫了更廣泛的用例。然而,LlamaIndex的一個很大的優勢是,這在語料庫增長到一定大小時非常有幫助。
總的來說,這兩個有用的庫都很新,還在發展階段,每周或每月都會有比較大的更新。也許LangChain在不久的将來吞并了LlamaIndex,提供了一個完整統一的解決方案。
五、如何使用 LLamaIndex 建構和查詢本地文檔索引
接下來我們就用LlamaIndex來實作建構外部文檔索引進行檢索,不過,我們不需要從零開始編寫代碼。因為這種模式非常常見,有人已經為它編寫了一個開源Python包,名為。是以,在此例中我們可以直接使用這個軟體包,并通過幾行代碼來測試它是否能夠回答與魯迅先生所寫《藤野先生》相關的問題。
首先通過pip來安裝llama-index:
pip install llama-index
pip install langchain
我把從網上找到的《藤野先生》這篇文章變成了一個 txt 檔案,放在了 data/index_luxun 這個目錄下。
import openai, os
from llama_index import GPTVectorStoreIndex, SimpleDirectoryReader
os.environ["OPENAI_API_KEY"] = "your-openai-api-key"
openai.api_key = os.environ.get("OPENAI_API_KEY")
documents = SimpleDirectoryReader('/content/data/luxun').load_data()
index = GPTVectorStoreIndex.from_documents(documents)
index.set_index_id("index_luxun")
index.storage_context.persist('./storage')
輸出結果:
INFO:llama_index.token_counter.token_counter:> [build_index_from_nodes] Total LLM token usage: 0 tokens
INFO:llama_index.token_counter.token_counter:> [build_index_from_nodes] Total embedding token usage: 6763 tokens
1)、首先,我們通過一個叫做 SimpleDirectoryReader 的資料加載器,将整個./data/index_luxun 的目錄給加載進來。這裡面的每一個檔案,都會被當成是一篇文檔。
2)、然後,我們将所有的文檔交給了 GPTSimpleVectorIndex 建構索引。顧名思義,它會把文檔分段轉換成一個個向量,然後存儲成一個索引。
3)、最後,我們會把對應的索引存下來,存儲的結果就是一個 json 檔案。後面,我們就可以用這個索引來進行相應的問答。
from llama_index import StorageContext, load_index_from_storage
# rebuild storage context
storage_context = StorageContext.from_defaults(persist_dir='./storage')
# load index
index = load_index_from_storage(storage_context, index_id="index_luxun")
query_engine = index.as_query_engine(response_mode="tree_summarize")
response = query_engine.query("根據上下文内容,告訴我魯迅先生在日本學醫的老師是誰?")
print(response)
進行問答也隻需幾行代碼。我們可以使用GPTSimpleVectorIndex的 load_from_disk 函數,将之前生成的索引加載到記憶體中。然後,通過對 Index 執行個體調用 query 函數,就能夠擷取問題的答案。通過外部索引,我們可以準确地獲得問題的答案。
INFO:llama_index.token_counter.token_counter:> [query] Total LLM token usage: 2984 tokens
INFO:llama_index.token_counter.token_counter:> [query] Total embedding token usage: 34 tokens
魯迅先生在日本學習醫學的老師是藤野嚴九郎先生。
确實,通過使用外部索引,我們可以輕松地獲得問題的答案。讓我們再試一下其他的問題,看看它是否能夠回答正确。
response = query_engine.query("根據上下文内容,魯迅先生是去哪裡學的醫學?")
print(response)
輸出結果:
> Got node text: 藤野先生
東京也無非是這樣。上野的櫻花爛熳的時節,望去确也像绯紅的輕雲,但花下也缺不了成群結隊的“清國留學生”的速成班,頭頂上盤着大辮子,頂得學生制帽的頂上高高聳起,形成一座富士山。也有解散辮子,盤得平的,除下帽來,油光可鑒,宛如小姑娘的發髻一般,還要将脖子扭幾扭。實在标緻極了。
中國留學生會館的門房裡有幾本書買,有時還值得去一轉;倘在上午,裡面的幾間洋房裡倒也還可以坐坐的。但到傍晚,有...
INFO:llama_index.token_counter.token_counter:> [query] Total LLM token usage: 2969 tokens
INFO:llama_index.token_counter.token_counter:> [query] Total embedding token usage: 26 tokens
魯迅先生去仙台的醫學專門學校學習醫學。
它仍然正确回答了問題。那麼,我們搜尋到的内容,在這個過程裡面是如何送出給 OpenAI 的呢?我們就來看看下面的這段代碼就知道了。
from llama_index import Prompt
query_str = "魯迅先生去哪裡學的醫學?"
DEFAULT_TEXT_QA_PROMPT_TMPL = (
"Context information is below. \n"
"---------------------\n"
"{context_str}"
"\n---------------------\n"
"Given the context information and not prior knowledge, "
"answer the question: {query_str}\n"
)
QA_PROMPT = Prompt(DEFAULT_TEXT_QA_PROMPT_TMPL)
query_engine = index.as_query_engine(text_qa_template=QA_PROMPT)
response = query_engine.query(query_str)
print(response)
這段代碼裡,我們定義了一個 QA_PROMPT 的對象,并且為它設計了一個模版。
1)、這個模版的開頭,我們告訴 AI,我們為 AI 提供了一些上下文資訊(Context information)。
2)、模版裡面支援兩個變量,一個叫做 context_str,另一個叫做 query_str。context_str 的地方,在實際調用的時候,會被通過 Embedding 相似度找出來的内容填入。而 query_str 則是會被我們實際提的問題替換掉。
3)、實際提問的時候,我們告訴 AI,隻考慮上下文資訊,而不要根據自己已經有的先驗知識(prior knowledge)來回答問題。
我們就是這樣,把搜尋找到的相關内容以及問題,組合到一起變成一段提示語,讓 AI 能夠按照我們的要求來回答問題。那我們再問一次 AI,看看答案是不是沒有變。
輸出結果:
魯迅先生去仙台的醫學專門學校學習醫學。
這一次 AI 還是正确地回答出了魯迅先生是去仙台的醫學專門學校學習的。我們再試一試,問一些不相幹的問題,會得到什麼答案,比如我們問問西遊記的作者是誰。
QA_PROMPT_TMPL = (
"下面的“我”指的是魯迅先生 \n"
"---------------------\n"
"{context_str}"
"\n---------------------\n"
"根據這些資訊,請回答問題: {query_str}\n"
"如果您不知道的話,請回答不知道\n"
)
QA_PROMPT = Prompt(QA_PROMPT_TMPL)
query_engine = index.as_query_engine(text_qa_template=QA_PROMPT)
response = query_engine.query("請問西遊記的作者是誰?")
print(response)
輸出結果:
不知道
可以看到,AI 的确按照我們的指令回答不知道,而不是胡答一氣。
六、如何使用 LLamaIndex 生成文檔摘要總結
還有一個常見的使用 llama-index 的Python庫的應用場景,就是生成文章摘要。前面我們已經介紹過使用适當的提示語(Prompt)來完成文本聚類。然而,如果要總結一篇論文甚至一本書,由于由于OpenAI的API接口最多隻能支援4096個Token,顯然是不夠用的。
為了解決這個問題。我們可以通過對文本進行分段小結,并對這些小結再次進行總結來達到目的。可以将文章或書籍建構成一個樹狀索引,其中每個節點代表其子節點内容的摘要。最後,在整棵樹的根節點處就可以得到整篇文章或整本書的總結。
事實上,llama-index 本身就内置了這樣的功能。下面我們就來看看要實作這個功能,我們的代碼應該怎麼寫。
首先,我們先來安裝一下 spaCy 這個 Python 庫,并且下載下傳一下對應的中文分詞分句需要的模型。
pip install spacy
python -m spacy download zh_core_web_sm
下面的代碼非常簡單,我們選擇了 llama-index 中最簡單的索引結構 。但是根據我們的需求,我們做了兩個優化。
首先,在索引中,我們指定了一個 ,這樣在向OpenAI發起請求時,會使用ChatGPT模型。這個模型比較快速、價格也相對較低。預設情況下,llama-index使用的模型是 ,其價格比 高出十倍。當我們隻進行幾輪對話時,這種價格差異還不太明顯。但如果你要處理幾十本書的内容,成本就會大幅增加。是以,在這裡我們設定了模型輸出内容不超過1024個Token長度,以確定摘要不會太長,并避免合并無關内容。
其次,我們使用 來進行中文文本分割。預設情況下,llama-index對中文支援和效果都不太好。幸運的是它允許自定義文本分割方式。由于我們選用的文章是中文且标點符号也是中文的,是以我們選擇了中文語言模型進行分割操作。同時限制分割後的每個段落長度不超過 2048 個Token。你可以根據實際處理文章内容和特性來自定義這些參數設定。
from langchain.chat_models import ChatOpenAI
from langchain.text_splitter import SpacyTextSplitter
from llama_index import GPTListIndex, LLMPredictor, ServiceContext
from llama_index.node_parser import SimpleNodeParser
# define LLM
llm_predictor = LLMPredictor(llm=ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo", max_tokens=1024))
text_splitter = SpacyTextSplitter(pipeline="zh_core_web_sm", chunk_size = 2048)
parser = SimpleNodeParser(text_splitter=text_splitter)
documents = SimpleDirectoryReader('./data/luxun').load_data()
nodes = parser.get_nodes_from_documents(documents)
service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor)
list_index = GPTListIndex(nodes=nodes, service_context=service_context)
query_engine = list_index.as_query_engine(response_mode="tree_summarize")
response = query_engine.query("下面魯迅先生以第一人稱‘我’寫的内容,請你用中文總結一下:")
print(response)
輸出結果:
魯迅先生在文章中以第一人稱‘我’寫了他在仙台學醫期間的經曆,包括他與藤野先生的關系、學習醫學的困難和他最終離開醫學專業的決定。他對藤野先生的感激和敬仰一直延續至今。
在建構 索引時,并不會建立 ,是以索引的建立速度很快,也不會消耗大量的Token數量。它隻是根據您設定的索引結構和分割方式建立了一個清單形式的索引。
接下來,我們可以讓AI幫助我們對這篇文章進行總結。同樣地,提示語本身非常重要,是以我們強調文章是魯迅先生以第一人稱“我”來寫的。由于我們想要按照樹狀結構對文章進行總結,是以設定了一個參數 response_mode = "tree_summarize"。這個參數将會根據之前提到的樹狀結構來對整篇文章進行總結。
實際上,它會将每個文本段落通過查詢中的提示語進行摘要。然後再通過查詢中的提示語繼續對多個摘要内容進行進一步總結。
from llama_index import GPTTreeIndex
# define LLM
tree_index = GPTTreeIndex(nodes=nodes, service_context=service_context)
query_engine = tree_index.as_query_engine(mode="summarize")
response = query_engine.query("下面魯迅先生以第一人稱‘我’寫的内容,請你用中文總結一下:")
print(response)
輸出結果:
魯迅先生在這篇文章中講述了他在日本留學期間的經曆,包括他遇到的人和事,以及他的學習情況。他特别提到了他的解剖學教授藤野嚴九郎,藤野先生對他的學習和講義進行了指導和修改。此外,魯迅還提到了一些關于中國文化的誤解,比如中國女人裹腳的問題。最後,他還引用了《新約》上的句子,表達了他的思考和感悟。
可以看到,我們隻使用了幾行代碼就成功完成了對整篇文章的總結。總體而言,傳回的結果還算不錯。
七、LLamaIndex 在多模态識别圖檔中的應用
llama_index不僅可以索引文本資訊,還可以索引包含圖檔和插圖等多媒體資訊的書籍。這就是所謂的。當然,實作這種能力需要借助一些多模态模型來将文本和圖檔聯系在一起。後面我們将專門介紹關于圖像的多模态模型。
現在我們來看一個 llama_index 官方樣例庫中提供的例子:拍下吃飯時的發票,并詢問吃了什麼、花了多少錢的情況。
%pip install torch transformers sentencepiece Pillow
from llama_index import SimpleDirectoryReader, VectorStoreIndex
from llama_index.readers.file.base import (
DEFAULT_FILE_READER_CLS,
ImageReader,
)
from llama_index.response.notebook_utils import (
display_response,
display_image,
)
from llama_index.indices.query.query_transform.base import (
ImageOutputQueryTransform,
)
# NOTE: we add filename as metadata for all documents
filename_fn = lambda filename: {'file_name': filename}
receipt_reader = SimpleDirectoryReader(
input_dir='./data/image',
file_metadata=filename_fn,
)
receipt_documents = receipt_reader.load_data()
我們首先使用上面定義的自定義image parser和metadata function來攝取我們的收據圖像。這樣我們就可以得到圖像文檔而不僅僅是文本文檔。
我們像往常一樣建構一個簡單的向量索引,但與以前不同的是,我們的索引除了文本外還包含圖像。
receipts_index = VectorStoreIndex.from_documents(receipt_documents)
然後,我們隻需要用自然語言提出問題來查詢索引,就能找到對應的圖檔。在提問時,我們專門設計了一個ImageOutputQueryTransform,主要是為了在輸出結果時,在圖檔外部添加<img>标簽以便在Notebook中顯示。
from llama_index.query_engine import TransformQueryEngine
query_engine = receipts_index.as_query_engine()
query_engine = TransformQueryEngine(query_engine, query_transform=ImageOutputQueryTransform(width=400))
receipts_response = query_engine.query(
'When was the last time I went to McDonald\'s and how much did I spend?',
)
display_response(receipts_response)
輸出結果:
The last time you went to McDonald's was on 03/10/2018 at 07:39:12 PM and you spent $26.15.
在OpenAI及整個大語言模型生态系統快速發展的背景下,llama-index庫也在迅速疊代。在我自己使用過程中,也遇到了各種小錯誤。對于中文支援也存在一些小缺陷。不過,作為一個開源項目,它已經擁有了很好的生态系統,尤其是提供了許多DataConnector選項。這些選項包括PDF、ePub等電子書格式以及YouTube、Notion、MongoDB等外部資料源和API接入資料甚至本地資料庫的資料。您可以在 llamahub.ai 上檢視社群開發的各種不同資料源格式的DataConnector。
八、總結與展望
通過本文的介紹,您已經掌握了如何輕松上手使用 llama-index 這個Python包。借助它,您可以迅速将外部資料庫轉換為索引,并通過提供的查詢接口向文檔提問。同時,您還可以利用分片和樹形結構管理索引來生成摘要。
llama-index 不僅具備豐富的功能,而且作為一個Python庫,它仍在不斷發展和完善。它極大地便利了與大語言模型相關的應用程式開發。相關文檔可在官方網站查閱,代碼也是開源的。如遇問題,您還可以直接檢視源代碼以深入了解。
實際上,llama-index 提供了一種創新的大語言模型應用設計模式。它通過先為外部資料庫建立索引,再在每次提問時從資料庫中搜尋相關内容,最後利用AI的語義了解能力基于搜尋結果回答問題。
在索引和搜尋的前兩步,我們可以使用 OpenAI 的 Embedding 接口,也可以使用其他大語言模型的 Embedding 接口,或者使用傳統的文本搜尋技術。隻有在問答的最後一步,才必須使用 OpenAI 的接口。我們不僅可以索引文本資訊,還可以通過其他模型将圖檔轉換為文本并進行索引,實作所謂的多模态功能。
通過今天給出的幾個例子,希望您也能開始建立自己的本地資料庫,并将自己的資料集交給AI進行索引。這樣您就能擁有一個專屬于自己的AI了。
Jupyter Notebook 的完整代碼:
九、推薦閱讀
的功能非常強大,并且源代碼裡也專門提供了示例部分。你可以去看一下它的官方文檔以及示例,了解它可以用來幹什麼。
官方文檔:
源碼以及示例: