ChatGPT 的出現,讓我們看到了大語言模型 ( Large Language Model, LLM ) 在語言和代碼理解、人類指令遵循、基本推理等多方面的能力,但幻覺問題 Hallucinations[1] 仍然是當前大語言模型面臨的一個重要挑戰。簡單來說,幻覺問題是指 LLM 生成不正確、荒謬或者與事實不符的結果。此外,數據新鮮度 ( Data Freshness ) 也是 LLM 在生成結果時出現的另外一個問題,即 LLM 對于一些時效性比較強的問題可能給不出或者給出過時的答案。而通過檢索外部相關信息的方式來增強 LLM 的生成結果是當前解決以上問題的一種流行方案,這里把這種方案稱為 檢索增強 LLM ( Retrieval Augmented LLM )。這篇長文將對檢索增強 LLM 的方案進行一個相對全面的介紹。主要內容包括:
-
檢索增強 LLM 的概念介紹、重要性及其解決的問題
-
檢索增強 LLM 的關鍵模塊及其實現方法
-
檢索增強 LLM 的一些案例分析和應用
這篇文章算是自己對這個領域的一篇學習總結,所以可能不是很專業和深入,也難免會有一些不準確的地方,歡迎討論。為了方便后期的更新和網頁閱讀,我也創建了一個 Github 倉庫 A Guide to Retrieval Augmented LLM [2]
01 什么是檢索增強 LLM
檢索增強 LLM ( Retrieval Augmented LLM ),簡單來說,就是給 LLM 提供外部數據庫,對于用戶問題 ( Query ),通過一些信息檢索 ( Information Retrieval, IR ) 的技術,先從外部數據庫中檢索出和用戶問題相關的信息,然后讓 LLM 結合這些相關信息來生成結果。這種模式有時候也被稱為 檢索增強生成 ( Retrieval Augmented Generation, RAG )。下圖是一個檢索增強 LLM 的簡單示意圖。
OpenAI 研究科學家 Andrej Karpathy 前段時間在微軟 Build 2023 大會上做過一場關于 GPT 模型現狀的分享 State of GPT[3],這場演講前半部分分享了 ChatGPT 這類模型是如何一步一步訓練的,后半部分主要分享了 LLM 模型的一些應用方向,其中就對檢索增強 LLM 這個應用方向做了簡單介紹。下面這張圖就是 Andrej 分享中關于這個方向的介紹。
傳統的信息檢索工具,比如 Google/Bing 這樣的搜索引擎,只有檢索能力 ( Retrieval-only ),現在 LLM 通過預訓練過程,將海量數據和知識嵌入到其巨大的模型參數中,具有記憶能力 ( Memory-only )。從這個角度看,檢索增強 LLM 處于中間,將 LLM 和傳統的信息檢索相結合,通過一些信息檢索技術將相關信息加載到 LLM 的工作內存 ( Working Memory ) 中,即 LLM 的上下文窗口 ( Context Window ),亦即 LLM 單次生成時能接受的最大文本輸入。
不僅 Andrej 的分享中提到基于檢索來增強 LLM 這一應用方式,從一些著名投資機構針對 AI 初創企業技術棧的調研和總結中,也可以看到基于檢索來增強 LLM 技術的廣泛應用。比如今年6月份紅杉資本發布了一篇關于大語言模型技術棧的文章 The New Language Model Stack[4],其中就給出了一份對其投資的33家 AI 初創企業進行的問卷調查結果,下圖的調查結果顯示有 88% 左右的創業者表示在自己的產品中有使用到基于檢索增強 LLM 技術。
無獨有偶,美國著名風險投資機構 A16Z 在今年6月份也發表了一篇介紹當前 LLM 應用架構的總結文章 Emerging Architectures for LLM Applications[5],下圖就是文章中總結的當前 LLM 應用的典型架構,其中最上面 Contextual Data 引入 LLM 的方式就是一種通過檢索來增強 LLM 的思路。
02 檢索增強 LLM 解決的問題
為什么要結合傳統的信息檢索系統來增強 LLM ?換句話說,基于檢索增強的 LLM 主要解決的問題是什么?這部分內容參考自普林斯頓大學陳丹琦小組之前在 ACL 2023 大會上關于基于檢索的語言模型的分享 ACL 2023 Tutorial: Retrieval-based Language Models and Applications[6]
長尾知識
雖然當前 LLM 的訓練數據量已經非常龐大,動輒幾百 GB 級別的數據量,萬億級別的標記數量 ( Token ),比如 GPT-3 的預訓練數據使用了3000 億量級的標記,LLaMA 使用了 1.4 萬億量級的標記。訓練數據的來源也十分豐富,比如維基百科、書籍、論壇、代碼等,LLM 的模型參數量也十分巨大,從幾十億、百億到千億量級,但讓 LLM 在有限的參數中記住所有知識或者信息是不現實的,訓練數據的涵蓋范圍也是有限的,總會有一些長尾知識在訓練數據中不能覆蓋到。
對于一些相對通用和大眾的知識,LLM 通常能生成比較準確的結果,而對于一些長尾知識,LLM 生成的回復通常并不可靠。ICML 會議上的這篇論文 Large Language Models Struggle to Learn Long-Tail Knowledge[7],就研究了 LLM 對基于事實的問答的準確性和預訓練數據中相關領域文檔數量的關系,發現有很強的相關性,即預訓練數據中相關文檔數量越多,LLM 對事實性問答的回復準確性就越高。從這個研究中可以得出一個簡單的結論 —— LLM 對長尾知識的學習能力比較弱。下面這張圖就是論文中繪制的相關性曲線。
為了提升 LLM 對長尾知識的學習能力,容易想到的是在訓練數據加入更多的相關長尾知識,或者增大模型的參數量,雖然這兩種方法確實都有一定的效果,上面提到的論文中也有實驗數據支撐,但這兩種方法是不經濟的,即需要一個很大的訓練數據量級和模型參數才能大幅度提升 LLM 對長尾知識的回復準確性。而通過檢索的方法把相關信息在 LLM 推斷時作為上下文 ( Context ) 給出,既能達到一個比較好的回復準確性,也是一種比較經濟的方式。下面這張圖就是提供相關信息的情況下,不同大小模型的回復準確性,對比上一張圖,可以看到對于同一參數量級的模型,在提供少量相關文檔參與預訓練的情況下,讓模型在推斷階段利用相關信息,其回復準確性有了大幅提升。
私有數據
ChatGPT 這類通用的 LLM 預訓練階段利用的大部分都是公開的數據,不包含私有數據,因此對于一些私有領域知識是欠缺的。比如問 ChatGPT 某個企業內部相關的知識,ChatGPT 大概率是不知道或者胡編亂造。雖然可以在預訓練階段加入私有數據或者利用私有數據進行微調,但訓練和迭代成本很高。此外,有研究和實踐表明,通過一些特定的攻擊手法,可以讓 LLM 泄漏訓練數據,如果訓練數據中包含一些私有信息,就很可能會發生隱私信息泄露。比如這篇論文 Extracting Training Data from Large Language Models[8] 的研究者們就通過構造的 Query 從 GPT-2 模型中提取出了個人公開的姓名、郵箱、電話號碼和地址信息等,即使這些信息可能只在訓練數據中出現一次。文章還發現,較大規模的模型比較小規模的更容易受到攻擊。
如果把私有數據作為一個外部數據庫,讓 LLM 在回答基于私有數據的問題時,直接從外部數據庫中檢索出相關信息,再結合檢索出的相關信息進行回答。這樣就不用通過預訓練或者微調的方法讓 LLM 在參數中記住私有知識,既節省了訓練或者微調成本,也一定程度上避免了私有數據的泄露風險。
數據新鮮度
由于 LLM 中學習的知識來自于訓練數據,雖然大部分知識的更新周期不會很快,但依然會有一些知識或者信息更新得很頻繁。LLM 通過從預訓練數據中學到的這部分信息就很容易過時。比如 GPT-4 模型使用的是截止到 2021-09 的預訓練數據,因此涉及這個日期之后的事件或者信息,它會拒絕回答或者給出的回復是過時或者不準確的。下面這個示例是問 GPT-4 當前推特的 CEO 是誰,GPT-4 給出的回復還是 Jack Dorsey,并且自己會提醒說回復可能已經過時了。
如果把頻繁更新的知識作為外部數據庫,供 LLM 在必要的時候進行檢索,就可以實現在不重新訓練 LLM 的情況下對 LLM 的知識進行更新和拓展,從而解決 LLM 數據新鮮度的問題。
來源驗證和可解釋性
通常情況下,LLM 生成的輸出不會給出其來源,比較難解釋為什么會這么生成。而通過給 LLM 提供外部數據源,讓其基于檢索出的相關信息進行生成,就在生成的結果和信息來源之間建立了關聯,因此生成的結果就可以追溯參考來源,可解釋性和可控性就大大增強。即可以知道 LLM 是基于什么相關信息來生成的回復。Bing Chat 就是利用檢索來增強 LLM 輸出的典型產品,下圖展示的就是 Bing Chat 的產品截圖,可以看到其生成的回復中會給出相關信息的鏈接。
利用檢索來增強 LLM 的輸出,其中很重要的一步是通過一些檢索相關的技術從外部數據中找出相關信息片段,然后把相關信息片段作為上下文供 LLM 在生成回復時參考。有人可能會說,隨著 LLM 的上下文窗口 ( Context Window ) 越來越長,檢索相關信息的步驟是不是就沒有必要了,直接在上下文中提供盡可能多的信息。比如 GPT-4 模型當前接收的最大上下文長度是 32K, Claude 模型最大允許 100K[9] 的上下文長度。
雖然 LLM 的上下文窗口越來越大,但檢索相關信息的步驟仍然是重要且必要的。一方面當前 LLM 的網絡架構決定了其上下文窗口的長度是會有上限的,不會無限增長。另外看似很大的上下文窗口,能容納的信息其實比較有限,比如 32K 的長度可能僅僅相當于一篇大學畢業論文的長度。另一方面,有研究表明,提供少量更相關的信息,相比于提供大量不加過濾的信息,LLM 回復的準確性會更高。比如斯坦福大學的這篇論文 Lost in the Middle[10] 就給出了下面的實驗結果,可以看到 LLM 回復的準確性隨著上下文窗口中提供的文檔數量增多而下降。
利用檢索技術從大量外部數據中找出與輸入問題最相關的信息片段,在為 LLM 生成回復提供參考的同時,也一定程度上過濾掉一些非相關信息的干擾,便于提高生成回復的準確性。此外,上下文窗口越大,推理成本越高。所以相關信息檢索步驟的引入也能降低不必要的推理成本。
03 關鍵模塊
為了構建檢索增強 LLM 系統,需要實現的關鍵模塊和解決的問題包括:
-
數據和索引模塊: 如何處理外部數據和構建索引
-
查詢和檢索模塊: 如何準確高效地檢索出相關信息
-
響應生成模塊: 如何利用檢索出的相關信息來增強 LLM 的輸出
數據和索引模塊
數據獲取
數據獲取模塊的作用一般是將多種來源、多種類型和格式的外部數據轉換成一個統一的文檔對象 ( Document Object ),便于后續流程的處理和使用。文檔對象除了包含原始的文本內容,一般還會攜帶文檔的元信息 ( Metadata ),可以用于后期的檢索和過濾。元信息包括但不限于:
-
時間信息,比如文檔創建和修改時間
-
標題、關鍵詞、實體(人物、地點等)、文本類別等信息
-
文本總結和摘要
有些元信息可以直接獲取,有些則可以借助 NLP 技術,比如關鍵詞抽取、實體識別、文本分類、文本摘要等。既可以采用傳統的 NLP 模型和框架,也可以基于 LLM 實現。
外部數據的來源可能是多種多樣的,比如可能來自
-
Google 套件里各種 Doc 文檔、Sheet 表格、Slides 演示、Calendar 日程、Drive 文件等
-
Slack、Discord 等聊天社區的數據
-
Github、Gitlab 上托管的代碼文件
-
Confluence 上各種文檔
-
Web 網頁的數據
-
API 返回的數據
-
本地文件
外部數據的類型和文件格式也可能是多樣化的,比如
-
從數據類型來看,包括純文本、表格、演示文檔、代碼等
-
從文件存儲格式來看,包括 txt、csv、pdf、markdown、json 等格式
外部數據可能是多語種的,比如中文、英文、德文、日文等。除此之外,還可能是多模態的,除了上面討論的文本模態,還包括圖片、音頻、視頻等多種模態。不過這篇文章中討論的外部數據將限定在文本模態。
在構建數據獲取模塊時,不同來源、類型、格式、語種的數據可能都需要采用不同的讀取方式。
文本分塊
文本分塊是將長文本切分成小片段的過程,比如將一篇長文章切分成一個個相對短的段落。那么為什么要進行文本分塊?一方面當前 LLM 的上下文長度是有限制的,直接把一篇長文全部作為相關信息放到 LLM 的上下文窗口中,可能會超過長度限制。另一方面,對于長文本來說,即使其和查詢的問題相關,但一般不會通篇都是完全相關的,而分塊能一定程度上剔除不相關的內容,為后續的回復生成過濾一些不必要的噪聲。
文本分塊的好壞將很大程度上影響后續回復生成的效果,切分得不好,內容之間的關聯性會被切斷。因此設計一個好的分塊策略十分重要。分塊策略包括具體的切分方法 ( 比如是按句子切分還是段落切分 ),塊的大小設為多少合適,不同的塊之間是否允許重疊等。Pinecone 的這篇博客 Chunking Strategies for LLM Applications[11] 中就給出了一些在設計分塊策略時需要考慮的因素。
-
原始內容的特點:原始內容是長文 ( 博客文章、書籍等 ) 還是短文 ( 推文、即時消息等 ),是什么格式 ( HTML、Markdown、Code 還是 LaTeX 等 ),不同的內容特點可能會適用不同的分塊策略;
-
后續使用的索引方法:目前最常用的索引是對分塊后的內容進行向量索引,那么不同的向量嵌入模型可能有其適用的分塊大小,比如 sentence-transformer 模型比較適合對句子級別的內容進行嵌入,OpenAI 的 text-embedding-ada-002 模型比較適合的分塊大小在 256~512 個標記數量;
-
問題的長度:問題的長度需要考慮,因為需要基于問題去檢索出相關的文本片段;
-
檢索出的相關內容在回復生成階段的使用方法:如果是直接把檢索出的相關內容作為 Prompt 的一部分提供給 LLM,那么 LLM 的輸入長度限制在設計分塊大小時就需要考慮。
分塊實現方法
那么文本分塊具體如何實現?一般來說,實現文本分塊的整體流程如下:
-
將原始的長文本切分成小的語義單元,這里的語義單元通常是句子級別或者段落級別;
-
將這些小的語義單元融合成更大的塊,直到達到設定的塊大小 ( Chunk Size ),就將該塊作為獨立的文本片段;
-
迭代構建下一個文本片段,一般相鄰的文本片段之間會設置重疊,以保持語義的連貫性。
那如何把原始的長文本切分成小的語義單元? 最常用的是基于分割符進行切分,比如句號 ( . )、換行符 ( )、空格等。除了可以利用單個分割符進行簡單切分,還可以定義一組分割符進行迭代切分,比如定義 [" ", " ", " ", ""]
這樣一組分隔符,切分的時候先利用第一個分割符進行切分 ( 實現類似按段落切分的效果 ),第一次切分完成后,對于超過預設大小的塊,繼續使用后面的分割符進行切分,依此類推。這種切分方法能比較好地保持原始文本的層次結構。
對于一些結構化的文本,比如代碼,Markdown,LaTeX 等文本,在進行切分的時候可能需要單獨進行考慮:
-
比如 Python 代碼文件,分割符中可能就需要加入類似
class
,def
這種來保證類和函數代碼塊的完整性; -
比如 Markdown 文件,是通過不同層級的 Header 進行組織的,即不同數量的 # 符號,在切分時就可以通過使用特定的分割符來維持這種層級結構。
文本塊大小的設定也是分塊策略需要考慮的重要因素,太大或者太小都會影響最終回復生成的效果。文本塊大小的計算方法,最常用的可以直接基于字符數進行統計 ( Character-level ),也可以基于標記數進行統計 ( Token-level )。至于如何確定合適的分塊大小,這個因場景而異,很難有一個統一的標準,可以通過評估不同分塊大小的效果來進行選擇。
上面提到的一些分塊方法在 LangChain[12] 中都有相應的實現。比如下面的代碼示例
from langchain.text_splitter import CharacterTextSplitterfrom langchain.text_splitter import RecursiveCharacterTextSplitter, Language# text splittext_splitter = RecursiveCharacterTextSplitter( # Set a really small chunk size, just to show.
chunk_size = 100,
chunk_overlap = 20,
length_function = len,
add_start_index = True,
)# code splitpython_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON,
chunk_size=50,
chunk_overlap=0 )# markdown splitmd_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.MARKDOWN,
chunk_size=60,
chunk_overlap=0 )
數據索引
經過前面的數據讀取和文本分塊操作后,接著就需要對處理好的數據進行索引。索引是一種數據結構,用于快速檢索出與用戶查詢相關的文本內容。它是檢索增強 LLM 的核心基礎組件之一。
下面介紹幾種常見的索引結構。為了說明不同的索引結構,引入節點(Node)的概念。在這里,節點就是前面步驟中對文檔切分后生成的文本塊(Chunk)。下面的索引結構圖來自 LlamaIndex 的文檔 How Each Index Works[13]。
鏈式索引
鏈式索引通過鏈表的結構對文本塊進行順序索引。在后續的檢索和生成階段,可以簡單地順序遍歷所有節點,也可以基于關鍵詞進行過濾。
樹索引
樹索引將一組節點 ( 文本塊 ) 構建成具有層級的樹狀索引結構,其從葉節點 (原始文本塊) 向上構建,每個父節點都是子節點的摘要。在檢索階段,既可以從根節點向下進行遍歷,也可以直接利用根節點的信息。樹索引提供了一種更高效地查詢長文本塊的方式,它還可以用于從文本的不同部分提取信息。與鏈式索引不同,樹索引無需按順序查詢。
關鍵詞表索引
關鍵詞表索引從每個節點中提取關鍵詞,構建了每個關鍵詞到相應節點的多對多映射,意味著每個關鍵詞可能指向多個節點,每個節點也可能包含多個關鍵詞。在檢索階段,可以基于用戶查詢中的關鍵詞對節點進行篩選。
向量索引
向量索引是當前最流行的一種索引方法。這種方法一般利用文本嵌入模型 ( Text Embedding Model ) 將文本塊映射成一個固定長度的向量,然后存儲在向量數據庫中。檢索的時候,對用戶查詢文本采用同樣的文本嵌入模型映射成向量,然后基于向量相似度計算獲取最相似的一個或者多個節點。
上面的表述中涉及到向量索引和檢索中三個重要的概念: 文本嵌入模型、相似向量檢索和向量數據庫。下面一一進行詳細說明。
文本嵌入模型
文本嵌入模型 ( Text Embedding Model ) 將非結構化的文本轉換成結構化的向量 ( Vector ),目前常用的是學習得到的稠密向量。
當前有很多文本嵌入模型可供選擇,比如
-
早期的 Word2Vec、GloVe 模型等,目前很少用。
-
基于孿生 BERT 網絡預訓練得到的 Sentence Transformers[14] 模型,對句子的嵌入效果比較好
-
OpenAI 提供的 text-embedding-ada-002[15] 模型,嵌入效果表現不錯,且可以處理最大 8191 標記長度的文本
-
Instructor[16] 模型,這是一個經過指令微調的文本嵌入模型,可以根據任務(例如分類、檢索、聚類、文本評估等)和領域(例如科學、金融等),提供任務指令而生成相對定制化的文本嵌入向量,無需進行任何微調
-
BGE[17] 模型: 由智源研究院開源的中英文語義向量模型,目前在MTEB中英文榜單都排在第一位。
下面就是評估文本嵌入模型效果的榜單 MTEB Leaderboard[18] (截止到 2023-08-18 )。值得說明的是,這些現成的文本嵌入模型沒有針對特定的下游任務進行微調,所以不一定在下游任務上有足夠好的表現。最好的方式一般是在下游特定的數據上重新訓練或者微調自己的文本嵌入模型。
相似向量檢索
相似向量檢索要解決的問題是給定一個查詢向量,如何從候選向量中準確且高效地檢索出與其相似的一個或多個向量。首先是相似性度量方法的選擇,可以采用余弦相似度、點積、歐式距離、漢明距離等,通常情況下可以直接使用余弦相似度。其次是相似性檢索算法和實現方法的選擇,候選向量的數量量級、檢索速度和準確性的要求、內存的限制等都是需要考慮的因素。
當候選向量的數量比較少時,比如只有幾萬個向量,那么 Numpy 庫就可以實現相似向量檢索,實現簡單,準確性高,速度也很快。國外有個博主做了個簡單的基準測試發現 Do you actually need a vector database[19] ,當候選向量數量在 10 萬量級以下時,通過對比 Numpy 和另一種高效的近似最近鄰檢索實現庫 Hnswlib[20] ,發現在檢索效率上并沒有數量級的差異,但 Numpy 的實現過程更簡單。
下面就是使用 Numpy 的一種簡單實現代碼:
import numpy as np# candidate_vecs: 2D numpy array of shape N x D# query_vec: 1D numpy array of shape D# k: number of top k similar vectorssim_scores = np.dot(candidate_vecs, query_vec)
topk_indices = np.argsort(sim_scores)[::-1][:k]
topk_values = sim_scores[topk_indices]
對于大規模向量的相似性檢索,使用 Numpy 庫就不合適,需要使用更高效的實現方案。Facebook團隊開源的 Faiss[21] 就是一個很好的選擇。Faiss 是一個用于高效相似性搜索和向量聚類的庫,它實現了在任意大小的向量集合中進行搜索的很多算法,除了可以在CPU上運行,有些算法也支持GPU加速。Faiss 包含多種相似性檢索算法,具體使用哪種算法需要綜合考慮數據量、檢索頻率、準確性和檢索速度等因素。
Pinecone 的這篇博客 Nearest Neighbor Indexes for Similarity Search[22] 對 Faiss 中常用的幾種索引進行了詳細介紹,下圖是幾種索引在不同維度下的定性對比:
向量數據庫
上面提到的基于 Numpy 和 Faiss 實現的向量相似檢索方案,如果應用到實際產品中,可能還缺少一些功能,比如:
-
數據托管和備份
-
數據管理,比如數據的插入、刪除和更新
-
向量對應的原始數據和元數據的存儲
-
可擴展性,包括垂直和水平擴展
所以向量數據庫應運而生。簡單來說,向量數據庫是一種專門用于存儲、管理和查詢向量數據的數據庫,可以實現向量數據的相似檢索、聚類等。目前比較流行的向量數據庫有 Pinecone[23]、Vespa[24]、Weaviate[25]、Milvus[26]、Chroma[27] 、Tencent Cloud VectorDB[28]等,大部分都提供開源產品。
Pinecone 的這篇博客 What is a Vector Database[29] 就對向量數據庫的相關原理和組成進行了比較系統的介紹,下面這張圖就是文章中給出的一個向量數據庫常見的數據處理流程:
-
索引: 使用乘積量化 ( Product Quantization ) 、局部敏感哈希 ( LSH )、HNSW 等算法對向量進行索引,這一步將向量映射到一個數據結構,以實現更快的搜索。
-
查詢: 將查詢向量和索引向量進行比較,以找到最近鄰的相似向量。
-
后處理: 有些情況下,向量數據庫檢索出最近鄰向量后,對其進行后處理后再返回最終結果。
向量數據庫的使用比較簡單,下面是使用 Python 操作 Pinecone 向量數據庫的示例代碼:
# install python pinecone client# pip install pinecone-clientimport pinecone
# initialize pinecone clientpinecone.init(api_key="YOUR_API_KEY", environment="YOUR_ENVIRONMENT")# create index pinecone.create_index("quickstart", dimension=8, metric="euclidean")# connect to the indexindex = pinecone.Index("quickstart")# Upsert sample data (5 8-dimensional vectors) index.upsert([
("A", [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]),
("B", [0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2]),
("C", [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3]),
("D", [0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4]),
("E", [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5])
])# queryindex.query(
vector=[0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3],
top_k=3,
include_values=True
)
# Returns: # {'matches': [{'id': 'C', # 'score': 0.0, # 'values': [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3]}, # {'id': 'D', # 'score': 0.0799999237, # 'values': [0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4]}, # {'id': 'B', # 'score': 0.0800000429, # 'values': [0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2]}], # 'namespace': ''}# delete index pinecone.delete_index("quickstart")
查詢和檢索模塊
查詢變換
查詢文本的表達方法直接影響著檢索結果,微小的文本改動都可能會得到天差萬別的結果。直接用原始的查詢文本進行檢索在很多時候可能是簡單有效的,但有時候可能需要對查詢文本進行一些變換,以得到更好的檢索結果,從而更可能在后續生成更好的回復結果。下面列出幾種常見的查詢變換方式。
變換一: 同義改寫
將原始查詢改寫成相同語義下不同的表達方式,改寫工作可以調用 LLM 完成。比如對于這樣一個原始查詢: What are the approaches to Task Decomposition?
,可以改寫成下面幾種同義表達:
How can Task Decomposition be approached?
What are the different methods for Task Decomposition?
What are the various approaches to decomposing tasks?
對于每種查詢表達,分別檢索出一組相關文檔,然后對所有檢索結果進行去重合并,從而得到一個更大的候選相關文檔集合。通過將同一個查詢改寫成多個同義查詢,能夠克服單一查詢的局限,獲得更豐富的檢索結果集合。
變換二: 查詢分解
有相關研究表明 ( self-ask[30],ReAct[31] ),LLM 在回答復雜問題時,如果將復雜問題分解成相對簡單的子問題,回復表現會更好。這里又可以分成單步分解和多步分解。
單步分解將一個復雜查詢轉化為多個簡單的子查詢,融合每個子查詢的答案作為原始復雜查詢的回復。
對于多步分解,給定初始的復雜查詢,會一步一步地轉換成多個子查詢,結合前一步的回復結果生成下一步的查詢問題,直到問不出更多問題為止。最后結合每一步的回復生成最終的結果。
變換三: HyDE
HyDE[32],全稱叫 Hypothetical Document Embeddings,給定初始查詢,首先利用 LLM 生成一個假設的文檔或者回復,然后以這個假設的文檔或者回復作為新的查詢進行檢索,而不是直接使用初始查詢。這種轉換在沒有上下文的情況下可能會生成一個誤導性的假設文檔或者回復,從而可能得到一個和原始查詢不相關的錯誤回復。下面是論文中給出的一個例子:
排序和后處理
經過前面的檢索過程可能會得到很多相關文檔,就需要進行篩選和排序。常用的篩選和排序策略包括:
-
基于相似度分數進行過濾和排序
-
基于關鍵詞進行過濾,比如限定包含或者不包含某些關鍵詞
-
讓 LLM 基于返回的相關文檔及其相關性得分來重新排序
-
基于時間進行過濾和排序,比如只篩選最新的相關文檔
-
基于時間對相似度進行加權,然后進行排序和篩選
回復生成模塊
回復生成策略
檢索模塊基于用戶查詢檢索出相關的文本塊,回復生成模塊讓 LLM 利用檢索出的相關信息來生成對原始查詢的回復。LlamaIndex 中有給出一些不同的回復生成策略。
一種策略是依次結合每個檢索出的相關文本塊,每次不斷修正生成的回復。這樣的話,有多少個獨立的相關文本塊,就會產生多少次的 LLM 調用。另一種策略是在每次 LLM 調用時,盡可能多地在 Prompt 中填充文本塊。如果一個 Prompt 中填充不下,則采用類似的操作構建多個 Prompt,多個 Prompt 的調用可以采用和前一種相同的回復修正策略。
回復生成 Prompt 模板
下面是 LlamaIndex 中提供的一個生成回復的 Prompt 模板。從這個模板中可以看到,可以用一些分隔符 ( 比如 ——— ) 來區分相關信息的文本,還可以指定 LLM 是否需要結合它自己的知識來生成回復,以及當提供的相關信息沒有幫助時,要不要回復等。
template = f'''
Context information is below.
---------------------
{context_str}
---------------------
Using both the context information and also using your own knowledge, answer the question: {query_str}
If the context isn't helpful, you can/don’t answer the question on your own.
'''
下面的 Prompt 模板讓 LLM 不斷修正已有的回復。
template = f'''
The original question is as follows: {query_str}
We have provided an existing answer: {existing_answer}
We have the opportunity to refine the existing answer (only if needed) with some more context below.
------------
{context_str}
------------
Using both the new context and your own knowledege, update or repeat the existing answer.
'''
04 案例分析和應用
ChatGPT 檢索插件
ChatGPT 檢索插件 ChatGPT Retrieval Plugin[33] 是 OpenAI 官方給出的一個通過檢索來增強 LLM 的范例,實現了讓 ChatGPT 訪問私有知識的一種途徑,其在 Github 上的開源倉庫短時間內獲得了大量關注。下面是 ChatGPT 檢索插件內部原理的一張示意圖(圖片來源: openai-chatgpt-retrieval-plugin-and-postgresql-on-azure[34])。
在 API 接口設計上,檢索插件提供了下面幾種接口:
-
/upsert
: 該接口將上傳的一個或多個文本文檔,先切分成文本塊,每個文本塊大小在 200 個 Token,然后利用 OpenAI 的 文本嵌入模型將文本塊轉換成向量,最后連同原始文本和元信息存儲在向量數據庫中,代碼倉庫中實現了對幾乎所有主流向量類數據庫的支持。 -
/upsert-file
: 該接口允許上傳 PDF、TXT、DOCX、PPTX 和 MD 格式的單個文件,先轉換成純文本后,后續處理流程和/upsert
接口一樣。 -
/query
: 該接口實現對給定的查詢,返回和查詢最相關的幾個文本塊,實現原理也是基于相似向量檢索。用戶可以在請求中通過filter
參數對文檔進行過濾,通過top_k
參數指定返回的相關文本塊數量。 -
/delete
: 該接口實現從向量數據庫中對一個或多個文檔進行刪除操作。
LlamaIndex 和 LangChain
LlamaIndex[35] 是一個服務于 LLM 應用的數據框架,提供外部數據源的導入、結構化、索引、查詢等功能,這篇文章的結構和內容有很大一部分是參考 LlamaIndex 的文檔,文章中提到的很多模塊、算法和策略,LlamaIndex 基本都有對應的實現,提供了相關的高階和低階 API。
LlamaIndex 主要包含以下組件和特性:
-
數據連接器:能從多種數據源中導入數據,有個專門的項目 Llama Hub[36],可以連接多種來源的數據
-
數據索引:支持對讀取的數據進行多種不同的索引,便于后期的檢索
-
查詢和對話引擎:既支持單輪形式的查詢交互引擎,也支持多輪形式的對話交互引擎
-
應用集成:可以方便地與一些流行的應用進行集成,比如 ChatGPT、LangChain、Flask、Docker等
下面是 LlamaIndex 整體框架的一張示意圖。
除了 LlamaIndex,LangChain[37] 也是當前流行的一種 LLM 應用開發框架,其中也包含一些檢索增強 LLM 的相關組件,不過相比較而言,LlamaIndex 更側重于檢索增強 LLM 這一相對小的領域,而 LangChain 覆蓋的領域更廣,比如會包含 LLM 的鏈式應用、Agent 的創建和管理等。下面這張圖就是 LangChain 中 Retrieval[38] 模塊的整體流程示意圖,包含數據加載、變換、嵌入、向量存儲和檢索,整體處理流程和 LlamaIndex 是一樣的。
Github Copilot 分析
Github Copilot[39] 是一款 AI 輔助編程工具。如果使用過就會發現,Github Copilot 可以根據代碼的上下文來幫助用戶自動生成或者補全代碼,有時候可能剛寫下類名或者函數名,又或者寫完函數注釋,Copilot 就給出了生成好的代碼,并且很多時候可能就是我們想要實現的代碼。由于 Github Copilot 沒有開源,網上有人對其 VSCode 插件進行了逆向分析,比如 copilot internals[40] 和 copilot analysis[41],讓我們可以對 Copilot 的內部實現有個大概的了解。簡單來說,Github Copilot 插件會收集用戶在 VSCode 編程環境中的多種上下文信息構造 Prompt,然后把構造好的 Prompt 發送給代碼生成模型 ( 比如 Codex ),得到補全后的代碼,顯示在編輯器中。如何檢索出相關的上下文信息 ( Context ) 就是其中很重要的一個環節。Github Copilot 算是檢索增強 LLM 在 AI 輔助編程方向的一個應用。
需要說明的是,上面提到的兩份逆向分析是幾個月之前做的,Github Copilpot 目前可能已經做了很多的更新和迭代,另外分析是原作者閱讀理解逆向后的代碼得到的,所以可能會產生一些理解上的偏差。而下面的內容是我結合那兩份分析產生的,因此有些地方可能是不準確甚至是錯誤的,但不妨礙我們通過 Copilot 這個例子來理解上下文信息對增強 LLM 輸出結果的重要性,以及學習一些上下文相關信息檢索的實踐思路。
下面是一個 Prompt 的示例,可以看到包含前綴代碼信息 ( prefix ),后綴代碼信息 ( suffix ),生成模式 ( isFimEnabled ),以及 Prompt 不同組成元素的起始位置信息 ( promptElementRanges )。
拋開代碼生成模型本身的效果不談,Prompt 構造的好壞很大程度上會影響代碼補全的效果,而上下文相關信息 ( Context ) 的提取和構成很大程度上又決定了 Prompt 構造的好壞。讓我們來看一下 Github Copilot 的 Prompt 構造中有關上下文相關信息抽取的一些關鍵思路和實現。
Copilot 的 Prompt 包含不同類型的相關信息,包括
-
BeforeCursor
:光標前的內容 -
AfterCursor
:光標后的內容 -
SimilarFile
:與當前文件相似度較高的代碼片段 -
ImportedFile
:import 依賴 -
LanguageMarker
:文件開頭的語言標記 -
PathMarker
:文件的相對路徑信息
其中相似代碼片段的抽取,會先獲取最近訪問過的多份同種語言的文件,作為抽取相似代碼片段的候選文檔。然后設定窗口大小 ( 比如默認為 60 行 ) 和步長 ( 比如默認為 1 行 ),以滑動窗口的方式將候選文檔切分成代碼塊。接著計算每個切分后的代碼塊和當前文件的相似度,最后保留相似度較高的幾個代碼塊。這里當前文件的獲取是從當前光標往前截取窗口大小的內容,相似度的度量采用的是 Jaccard 系數,具體來說,會對代碼塊中的每一行進行分詞,過濾常見的代碼關鍵字 ( 比如 if, then, else, for 這些),得到一個標記 ( Token ) 集合,然后就可以在當前代碼塊和候選代碼塊的 Token 集合之間計算 Jaccard 相似度。在 Copilot 的場景下,這種相似度的計算方式簡單有效。
$$
上面的一篇分析文章中將 Prompt 的組成總結成下面的一張圖。
構造好 Prompt 后,Copilot 還會判斷是否有必要發起請求,代碼生成模型的計算是非常耗費算力的,因此有必要過濾一些不必要的請求。其中一個判斷是利用簡單的線性回歸模型對 Prompt 進行打分,當分數低于某個閾值時,請求就不會發出。這個線性回歸模型利用的特征包括像代碼語言、上一次代碼補全建議是否被采納或拒絕、上一次采納或拒絕距現在的時長、光標左邊的字符等。通過分析模型的權重,原作者給出了一些觀察:
-
一些編程語言的權重相對于其他語言權重要更高 ( php > js > python > rust > … ),PHP 權重最高,果然 PHP是世界上最好的語言 ( ^_^ )。
-
右半邊括號 ( 比如
)
,]
) 的權重要低于左半邊括號,這是符合邏輯的。
通過對 Github Copilot 這個編程輔助工具的分析可以看到:
-
檢索增強 LLM 的思路和技術在 Github Copilot 的實現中發揮著重要作用
-
上下文相關信息 ( Context ) 可以是一個廣義概念,可以是相關的文本或者代碼片段,也可以是文件路徑、相關依賴等,每個場景都可以定義其特定的上下文元素
-
相似性的度量和相似檢索方法可以因場景而異,不一定所有場景都需要用余弦相似度,都需要通過向量相似檢索的方式找出相關文檔,比如 Copilot 的實現中就利用簡單的 Jaccard 系數來計算分詞后 Token 集合的相似度,簡單高效。
文檔和知識庫的檢索與問答
檢索增強 LLM 技術的一個典型應用是知識庫或者文檔問答,比如針對企業內部知識庫或者一些文檔的檢索與問答等。這個應用方向目前已經出現了很多商業化和開源的產品。比如 Mendable[42] 就是一款商業產品,能提供基于文檔的 AI 檢索和問答能力。上面提到的 LlamaIndex 和 LangChain 項目官方文檔的檢索能力就是由 Mendable 提供的。下面就是一張使用截圖,可以看到 Mendable 除了會給出生成的回復,也會附上參考鏈接。
除了商業產品,也有很多類似的開源產品。比如
-
Danswer[43]: 提供針對企業內部文檔的問答功能,能實現多種來源的數據導入,支持傳統的檢索和基于 LLM 的問答,能智能識別用戶的搜索意圖,從而采用不同的檢索策略,支持用戶和文檔的權限管理,以及支持Docker部署等
-
PandaGPT[44]: 支持用戶上傳文件,然后可以針對文件內容進行提問
-
FastGPT[45]: 一個開源的基于 LLM 的 AI 知識庫問答平臺
-
Quivr[46]: 這個開源項目能實現用戶對個人文件或者知識庫的檢索和問答,期望成為用戶的「第二大腦」
-
ChatFiles[47]: 又一個基于 LLM 的文檔問答開源項目
下面這張圖是 ChatFiles 項目的技術架構圖,可以發現這類項目的基本模塊和架構都很類似,基本都遵從檢索增強 LLM 的思路,這類知識庫問答應用幾乎成為 LLM 領域的 Hello World 應用了。
-
檢索
+關注
關注
0文章
26瀏覽量
13102 -
GitHub
+關注
關注
3文章
457瀏覽量
16000 -
LLM
+關注
關注
0文章
215瀏覽量
243
原文標題:05 參考
文章出處:【微信號:zenRRan,微信公眾號:深度學習自然語言處理】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論