<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      淺談 RAG 并基于 NodeJS 實(shí)現(xiàn)基礎(chǔ)向量檢索服務(wù)

      RAG Retrieval-Augmented Generation是一種用于自然語言處理的模型架構(gòu),結(jié)合了檢索Retrieval和生成Generation兩種技術(shù)。而RAG服務(wù)在知識問答、代碼生成、事實(shí)驗(yàn)證、專業(yè)領(lǐng)域檢索等任務(wù)中表現(xiàn)出色,能夠通過檢索相關(guān)知識來增強(qiáng)生成模型的回答質(zhì)量和準(zhǔn)確性。

      實(shí)際上,當(dāng)前RAG相關(guān)建設(shè)已經(jīng)比較成熟,目前看起來其實(shí)并不太趕得上潮流,但學(xué)習(xí)RAG最好的時間是22年底,其次是現(xiàn)在。RAG服務(wù)和當(dāng)前的AI Infra建設(shè)有著密切的關(guān)系,作為基礎(chǔ)建設(shè)的RAG是一個以搜索為核心,圍繞各種數(shù)據(jù)、知識、LLMs等服務(wù)協(xié)作運(yùn)行的復(fù)雜系統(tǒng)。

      描述

      實(shí)際上我們當(dāng)前聊的主要是RAG中的RA部分,這部分還是比較偏向于傳統(tǒng)的NLP任務(wù),本文中主要涉及的是文本的向量化和向量檢索。而在G這部分則結(jié)合了檢索和生成的框架,主要用于增強(qiáng)生成模型的能力,并且能夠根據(jù)提問以及召回內(nèi)容提高生成文本的質(zhì)量和準(zhǔn)確性。

      向量檢索是RAG服務(wù)的核心部分,但本質(zhì)上的主要目標(biāo)是內(nèi)容檢索,因此我們并非僅限于使用向量方法來檢索內(nèi)容。基于圖數(shù)據(jù)庫的GraphRAG,或者是ElasticSearch基于倒排索引的全文檢索能力,都是可以來實(shí)現(xiàn)RAG的檢索服務(wù)的,這樣我們可以將相關(guān)檢索方法來統(tǒng)一召回并排名。

      此外,我們在使用LLMs的時候,是比較難以獲取最新的知識的,畢竟GPT實(shí)際上是生成式預(yù)訓(xùn)練模型的縮寫,既然是預(yù)訓(xùn)練模型自然是不能實(shí)時進(jìn)行訓(xùn)練獲取最新知識的。而如果我們使用RL強(qiáng)化學(xué)習(xí)、SFT監(jiān)督微調(diào)等方式來進(jìn)行微調(diào)訓(xùn)練,需要更高的標(biāo)注內(nèi)容以及計(jì)算資源等,成本比較高。

      因此,RAG服務(wù)是較簡單且成本低的模式,以此來提供給LLMs輸入Context,而實(shí)際上我們調(diào)優(yōu)Prompt的方式也可以算作是提供Context的一種方式,當(dāng)然諸如Function CallMCP也可以作為給予LLMs上下文的方式。而簡單來說,RAG服務(wù)具有以下的使用場景:

      • 需要獲取較新的知識內(nèi)容: 當(dāng)需要查詢特定領(lǐng)域的最新研究進(jìn)展、或者企業(yè)內(nèi)部實(shí)時更新的文檔,包括我們常用的聯(lián)網(wǎng)搜索場景,也可以認(rèn)為是特定形式的RAG。當(dāng)然如果需要獲取最新的相關(guān)情況,還需要結(jié)合Function Call的方式,例如查詢實(shí)時天氣等。
      • 需要提供特定領(lǐng)域的知識: 當(dāng)需要查詢特定領(lǐng)域的專業(yè)知識,例如醫(yī)學(xué)、法律等領(lǐng)域的專業(yè)文檔,或者是企業(yè)內(nèi)部的知識庫內(nèi)容,這些內(nèi)容通常是專業(yè)、私有、非公開或未包含在LLMs通用訓(xùn)練數(shù)據(jù)中的知識,因此可以結(jié)合RAG服務(wù)來提供相關(guān)內(nèi)容。
      • 增強(qiáng)透明性與可解釋性: 在需要審核、溯源或理解LLMs決策依據(jù)的場景,例如金融分析報告、法律建議初稿、醫(yī)療信息查詢等。RAG系統(tǒng)可以同時返回生成答案和其依據(jù)的檢索來源,這為用戶提供了驗(yàn)證答案正確性的途徑,也增加了系統(tǒng)輸出的透明度和可信度。
      • 數(shù)據(jù)長尾分布的檢索: 當(dāng)數(shù)據(jù)分布呈現(xiàn)長尾形態(tài)時,通用LLMs可能因訓(xùn)練數(shù)據(jù)覆蓋不足而無法給出滿意答案。RAG能夠通過檢索覆蓋到稀有或少見案例,提升模型在這些長尾數(shù)據(jù)上的表現(xiàn),當(dāng)然這本身也會比較依賴于RAG服務(wù)本身的檢索能力。
      • 垂直搜索與智能問答: 針對特定領(lǐng)域或企業(yè)內(nèi)容的智能搜索和問答機(jī)器人,RAG天然適合此類場景。用戶問題觸發(fā)對專屬知識庫的檢索,檢索到的相關(guān)內(nèi)容被用于生成精準(zhǔn)、簡潔、符合上下文的答案,提供比傳統(tǒng)關(guān)鍵詞匹配更自然、信息量更大。

      本文實(shí)現(xiàn)了非常基礎(chǔ)的RAG示例,Embedding使用輕量的all-MiniLM-L6-v2模型,向量檢索的數(shù)據(jù)庫使用輕量的hnswlib-node實(shí)現(xiàn),以此可以在node中直接運(yùn)行起來 https://github.com/WindRunnerMax/webpack-simple-environment/tree/master/packages/hnsw-rag

      實(shí)際上當(dāng)前很多云服務(wù)商以及開源項(xiàng)目提供了開箱即用RAG服務(wù)的實(shí)現(xiàn),而我們自然可以根據(jù)需求來決定是否可以接入開箱即用的服務(wù)。只是若是我們需要精細(xì)地調(diào)配很多功能,例如自定義分片策略等,就比較依賴服務(wù)是否暴露相關(guān)實(shí)現(xiàn),因此我們不一定可以直接處理。

      我們通常都會明確專業(yè)的人做專業(yè)的事,開箱即用并沒有什么問題。但是如果之前看過我的 從零實(shí)現(xiàn)富文本編輯器 系列文章的話,就可能會感覺出來,越依賴瀏覽器的能力就越需要處理默認(rèn)行為帶來的復(fù)雜Case,我們使用開箱即用的服務(wù)也同樣如此。如果自定義場景要求比較高,那么就會面臨不受控的情況。

      所以在這篇文章中我們實(shí)現(xiàn)了簡單的RAG服務(wù),也可以從側(cè)面反映出來我們能夠在RAG服務(wù)上做些什么精調(diào)的方案,來提高召回率和召回效果。在知道了我們可以對服務(wù)做哪些方面的參數(shù)和數(shù)據(jù)調(diào)整后,才能夠比較有針對性地進(jìn)行優(yōu)化。

      文本向量化

      先前已經(jīng)提到了,我們在這里主要的側(cè)重點(diǎn)還是傾向傳統(tǒng)的NLP檢索任務(wù),或者我們可以說是文本的搜索任務(wù)。因此簡化一下我們需要做的事情就是 預(yù)處理-編碼-存儲-檢索 這四部分,在本節(jié)中我們主要介紹文本的預(yù)處理和編碼方式。

      數(shù)據(jù)分片

      在數(shù)據(jù)預(yù)處理環(huán)節(jié),我們主要需要做的是數(shù)據(jù)清洗以及分片,數(shù)據(jù)清洗部分主要是去除文本內(nèi)容中的噪音、重復(fù)內(nèi)容、過濾低信息量文本等等,這部分的策略可以根據(jù)具體的業(yè)務(wù)需求來進(jìn)行調(diào)整。而分片則是將較大的文章切分為較小的片段,以便于后續(xù)的向量化和檢索。

      我們在這里主要討論的是分片方式,分片的方式解決了兩個問題,一是文檔的內(nèi)容會很長,若是直接將文檔內(nèi)容作為整體進(jìn)行向量化,可能會導(dǎo)致向量的維度過高,計(jì)算和存儲成本較大。二是文檔內(nèi)容的語義信息可能會分布在不同的段落中,大部分內(nèi)容可能與用戶問題無關(guān),整體向量化后召回效果可能欠佳。

      實(shí)際上還有個額外的原因,通過這種方式我們可以避免整篇文檔被召回,這樣可以省去不少的Token消耗。并且長上下文給予LLMs時,最開始的內(nèi)容可能還是會被遺忘。因此分片是比較公認(rèn)的處理方案,分而治之的思想在這里就很合適,并且分片的方式我們可以去精調(diào),來提高召回率以及召回效果。

      接下來我們可以考慮分片的方式,常見的分片方式有 固定大小分片、基于句段分片、基于結(jié)構(gòu)分片 等方式。在這里我們以下面的一段文本為例,簡單介紹一下分片的方式。需要注意的是,由于我們是為了演示用的,分片切的比較小,而實(shí)際上分片通常選的是512/1024/4096等長度的片段。

      ## 深度學(xué)習(xí)
      深度學(xué)習(xí)是機(jī)器學(xué)習(xí)的一個子集,使用多層神經(jīng)網(wǎng)絡(luò)來模擬人腦處理信息的方式。它在圖像識別、語音識別和自然語言處理等領(lǐng)域取得了顯著進(jìn)展。
      

      最基本的分片方式是按照固定的長度分片,即固定一個最大長度,然后根據(jù)這個長度值直接切分。這種簡單粗暴的方式可以快速實(shí)現(xiàn)原型,但可能會導(dǎo)致語義信息的丟失。例如,若我們將上面的文本按照每個分片最大長度為30字符進(jìn)行分片,得到的結(jié)果是:

      [
        "## 深度學(xué)習(xí)\n深度學(xué)習(xí)是機(jī)器學(xué)習(xí)的一個子集,使用多層神經(jīng)網(wǎng)",
        "絡(luò)來模擬人腦處理信息的方式。它在圖像識別、語音識別和自然語言",
        "處理等領(lǐng)域取得了顯著進(jìn)展。"
      ]
      

      雖然文本看起來非常規(guī)整,但實(shí)際上語義信息被切割得比較碎片化,而且部分片段可能包含填充或冗余信息,導(dǎo)致檢索精度降低。而處理這個問題比較常見的思路是采用overlap重疊分片的方式,即在每個分片之間保留一定的重疊部分,以便于保留更多的上下文信息,我們在上述基礎(chǔ)上重疊5個字符:

      [
        "## 深度學(xué)習(xí)\n深度學(xué)習(xí)是機(jī)器學(xué)習(xí)的一個子集,使用多層神經(jīng)網(wǎng)絡(luò)來模擬人",
        "多層神經(jīng)網(wǎng)絡(luò)來模擬人腦處理信息的方式。它在圖像識別、語音識別和自然語言處理等領(lǐng)域",
        "和自然語言處理等領(lǐng)域取得了顯著進(jìn)展。"
      ]
      

      雖然看起來是好了不少,但實(shí)際上這種方式仍然會導(dǎo)致語義信息的丟失,尤其是當(dāng)分片長度較短時,可能會導(dǎo)致上下文信息的缺失,而上述提到的分片長度比較大的情況下是表現(xiàn)相對好一些的。在本文的示例中,就是采用的這種簡單的分片方式來進(jìn)行分片的。

      /**
       * 文本分片方法
       */
      const spiltTextChunks = (text: string): string[] => {
        // 這里是直接根據(jù) 固定長度 + overlap 分片,此外還有 結(jié)構(gòu)分片 等策略
        const chunkSize = 100;
        const overlap = 20;
        const chunks: string[] = [];
        for (let i = 0; i < text.length; i += chunkSize - overlap) {
          const chunk = text.slice(i, i + chunkSize);
          if (chunk.trim().length > 0) {
            chunks.push(chunk.trim());
          }
        }
        return chunks;
      };
      

      第二種分片的方式是基于句段分片,即根據(jù)文本中的句子或段落進(jìn)行分片。這種方式可以更好地保留語義信息,因?yàn)檫@樣通常會在句子或段落的邊界進(jìn)行切分,而不是任意長度的字符切分。例如,我們可以將上面的文本按照句子進(jìn)行分片,得到的結(jié)果是:

      [
        "## 深度學(xué)習(xí)",
        "深度學(xué)習(xí)是機(jī)器學(xué)習(xí)的一個子集,使用多層神經(jīng)網(wǎng)絡(luò)來模擬人腦處理信息的方式。",
        "它在圖像識別、語音識別和自然語言處理等領(lǐng)域取得了顯著進(jìn)展。"
      ]
      

      這種方式看起來會好很多,語義信息保留得比較完整,避免中間分割,確保片段包含連貫思想,有效維護(hù)文檔的原始流程和上下文完整性。但是實(shí)際上還是面臨一些問題,例如切出的句子長度太短,或者是切出的句子太長超出了我們的Max Token限制等問題。

      因此我們也需要針對其實(shí)際表現(xiàn)來額外增加一些策略,例如較短句子中我們可以不斷拼接后續(xù)的內(nèi)容,直至其逼近Max Token限制。而較長的句子我們可以采用固定分片方式進(jìn)行分割處理,來確保每個片段都在合理的長度范圍內(nèi),這樣可以更好地平衡語義信息的完整性和片段長度的限制。

      [
        "## 深度學(xué)習(xí)\n深度學(xué)習(xí)是機(jī)器學(xué)習(xí)的一個子集,使用多層神經(jīng)網(wǎng)絡(luò)來模擬人腦處理信息的方式。它在圖像識別、語音識別和自然語言處理等領(lǐng)域取得了顯著進(jìn)展。"
      ]
      

      第三種分片方式是基于結(jié)構(gòu)分片,即根據(jù)文本的結(jié)構(gòu)進(jìn)行分片,例如標(biāo)題、段落、列表等,特別是我們給予模型的輸入通常都是Markdown格式的文本,因此其天然就具有一定的結(jié)構(gòu)化信息。基于結(jié)構(gòu)分片可以更好地保留文本的層次和邏輯關(guān)系,避免語義信息的丟失。

      [
        "- 列表1\n- 列表2\n- 列表3",
        "## 深度學(xué)習(xí)\n深度學(xué)習(xí)是機(jī)器學(xué)習(xí)的一個子集,使用多層神經(jīng)網(wǎng)絡(luò)來模擬人腦處理信息的方式。它在圖像識別、語音識別和自然語言處理等領(lǐng)域取得了顯著進(jìn)展。"
      ]
      

      實(shí)際上,當(dāng)前并沒有一種普遍適用的最佳分片策略,分片的復(fù)雜點(diǎn)在于確定片段的最佳大小和結(jié)構(gòu)。如果片段過小,它們可能會丟失關(guān)鍵的上下文信息,從而變得不那么有意義。相反如果片段過大,檢索過程的效率會降低,并且可能會無意中包含不相關(guān)的信息,從而稀釋結(jié)果的相關(guān)性。

      除了上述策略,分片時還需考慮其他重要因素,理想情況下,片段應(yīng)保留語義單元,例如完整的句子、段落或主題,以確保連貫性并防止意義中斷。采用重疊片段或者滑動窗口其實(shí)還是個比較好的方案,有助于在片段邊界處保持上下文,確保關(guān)鍵信息或過渡上下文不會在片段之間丟失。

      此外,我們甚至還可以讓LLMs來幫助我們進(jìn)行分片,語言模型其實(shí)更擅長理解和處理自然語言文本,這也是一種基于語義分片的策略。然而這種方式在處理大量數(shù)據(jù)的時候就需要考慮效率以及計(jì)算資源問題了,而且LLMs也并非銀彈,具體效果還是需要構(gòu)建評測集合來驗(yàn)證。

      還有一個比較重要的點(diǎn)是,我們在分片的時候是可以并入元信息的,這點(diǎn)是很容易被忽視的。元信息可以是文檔的標(biāo)題、作者、發(fā)布時間等,這些信息可以幫助模型更好地理解片段的上下文和背景,甚至如果我們有文檔相關(guān)的人工打標(biāo)信息,會更好地幫助我們進(jìn)行重排以及內(nèi)容生成。

      編碼方式

      在數(shù)據(jù)預(yù)處理完成后,我們就需要考慮數(shù)據(jù)存儲的方式了。在這里我們首先需要考慮一個問題,計(jì)算機(jī)是否能直接處理文本,答案自然是否定的。計(jì)算機(jī)本質(zhì)上是只能處理數(shù)字的,因此我們需要將文本轉(zhuǎn)換為計(jì)算機(jī)可以理解的數(shù)字形式,這個過程稱為編碼。

      那么我們考慮字體的這種形式,如果我們直接將文本處理成UTF-8編碼的字節(jié)流的話,是不是就可以直接存儲了。在這種情況下存儲自然是可以直接存儲的,但檢索的時候就會比較麻煩,我們希望的是實(shí)現(xiàn)語義搜索,需要理解詞語和短語背后的含義和意圖,而UTF-8自然不能直接提供語義信息。

      此外,NLP常用的Token代表的是詞組而非字也是類似的考慮,單詞通常包含更豐富的語義信息,而單個字符通常沒有這樣的語義。例如,蘋果這個詞有明確的含義,而單個的字符蘋和果分別并不能傳達(dá)完整的語義。且分詞可以減少文本序列的長度,降低復(fù)雜度和計(jì)算量。

      而在編碼的實(shí)現(xiàn)中,我們通常都會使用向量來表示詞組,那么最簡單的向量化方式是One-Hot編碼方式。即將每個詞組表示為一個高維稀疏向量,其中只有一個元素為1,其余元素為0。這種方式簡單直觀,但存在維度災(zāi)難和語義信息稀疏的問題。

      自然   1   0   0   0   [1, 0, 0, 0]  
      語言   0   1   0   0   [0, 1, 0, 0]
      處理   0   0   1   0   [0, 0, 1, 0]
      任務(wù)   0   0   0   1   [0, 0, 0, 1]
      

      類似維度都是詞匯表大小的還有TF-IDF編碼方式,即詞頻-逆文檔頻率?。TF詞頻,一個詞在單篇文檔中出現(xiàn)的次數(shù)越多,可能越重要。?IDF逆文檔頻率`,如果一個詞在所有文檔中出現(xiàn)得越普遍比如停用詞,就越不重要。同樣會導(dǎo)致產(chǎn)生非常高的緯度,導(dǎo)致向量語義信息稀疏。

      ?TF-IDF編碼主要應(yīng)用的方向是文檔轉(zhuǎn)化為結(jié)構(gòu)化、數(shù)值化的向量表示,便于進(jìn)行文本分類、聚類、相似度計(jì)算等任務(wù)。是通過詞統(tǒng)計(jì)的形式來處理文檔級的特征信息,而不是單個詞的語義信息,因此實(shí)際上跟One-Hot編碼的目標(biāo)是不一樣的,這里主要是舉例高緯稀疏向量的問題。

      文檔	自然	 語言	 處理	  任務(wù)
      D1	   0.223   0.511   0     	0
      D2     0.223   0	   0.916	0.121
      D3	   0.223   0.511   0.916	0
      

      而在NLP的詞向量生成的發(fā)展中,Google提出的Word2Vec模型是一個重要的里程碑。通過神經(jīng)網(wǎng)絡(luò)模型,將文本中的單詞映射到高維向量空間,使得語義相似的詞在該空間中的距離較近。這個算法可以產(chǎn)生稠密向量,并且捕捉到詞語間的語義關(guān)系。

      其中兩種主要的訓(xùn)練方法是CBOWSkip-GramCBOW通過上下文預(yù)測中心詞,類似于完形填空,而Skip-Gram則是反過來,通過中心詞預(yù)測上下文。這兩種方法在訓(xùn)練事,就能有效地聯(lián)系到詞語的語義信息,并且生成的向量維度通常較低(稠密),能夠更好地表示詞語之間的關(guān)系。

       dog                            faster
          \                         /        \
          dogs     cats        fast   slower  fastest
            \      /                 /      \
             animals              slow      slowest
      

      Word2Vec本身已經(jīng)非常成熟,但是其本身還是存在一定的局限性。其詞向量的意思固定,每個詞只有一個向量,無法表達(dá)多義詞,例如蘋果公司與水果蘋果。其次,忽視了詞語順序,直接把句子看成詞的集合,忽略了詞序,例如貓追狗和狗追貓的向量一樣。

      而在我們的RAG系統(tǒng)中,輸入的基準(zhǔn)都是句段,而不僅僅是簡單的詞匯集合,因此使用Word2Vec的方式還不夠。由此Google又提出了Transformers架構(gòu)Attention Is All You Need,采用動態(tài)上下文編碼的方式、長距離依賴捕捉等方式解決上述的問題。

      I ate an [apple].             apple => [0.8, -0.2, 1.1]
      I bought an [apple] phone.    apple => [1.5, 0.3, -0.7]
      

      然而Transformers同樣并非銀彈,其計(jì)算復(fù)雜度較高,尤其是長文本的處理,因此在實(shí)際應(yīng)用中會面臨計(jì)算資源和時間成本的問題。此外,因?yàn)檫@種一詞多義的表達(dá),如果數(shù)據(jù)集不夠大或者不夠多樣化,可能會導(dǎo)致模型無法很好地捕捉到詞語的多義性和上下文信息,因此需要的語料是巨量的。

      說句題外話,GPTBERT都是基于Transformer實(shí)現(xiàn),以當(dāng)年我的理解來說,BERT無疑是更優(yōu)秀的模型,其能夠理解文本,后續(xù)只需要跟隨一個解碼器,例如全連接層就可以輕松實(shí)現(xiàn)諸如文本分類的任務(wù)。而由于BERT實(shí)際是文本編碼,將其再接GPT作為解碼器生成文本自然可行。

      • 編碼器: 編碼器的主要任務(wù)是將輸入序列轉(zhuǎn)換為一種表示,這種表示能夠捕捉輸入序列的全局信息。
      • 解碼器: 解碼器的主要任務(wù)是根據(jù)某種輸入,可能是編碼器的輸出或之前的生成結(jié)果,逐步生成輸出序列。
      • GPT模型: GPT采用單向Transformer解碼器,只能利用上文信息,適合生成任務(wù)。通過自回歸語言模型預(yù)測下一個詞,訓(xùn)練目標(biāo)是最大化序列的聯(lián)合概率,適合于文本生成類任務(wù),文本翻譯、生成式回答等。
      • BERT模型: 使用雙向Transformer編碼器,能同時捕捉上下文信息,適合理解任務(wù)。通過MLM隨機(jī)掩碼部分詞并預(yù)測,NSP判斷兩個句子是否連續(xù),適合于理解分析類任務(wù),文本分類、相似度比較等。

      但是實(shí)際上目前看還是GPT成功了,還是以文本生成+Prompt這種看起來迂回式地完成任務(wù),看起來更容易普及一些。生成式預(yù)訓(xùn)練的單向解碼器,配合提示詞可以完成多種任務(wù),無需針對每個任務(wù)單獨(dú)設(shè)計(jì)模型結(jié)構(gòu)或訓(xùn)練方式。

      回到我們的基礎(chǔ)RAG服務(wù),我們使用的是非常輕量的all-MiniLM-L6-v2模型來進(jìn)行文本的向量化。具體來說是INT8量化的版本,因?yàn)槲覀兊闹饕繕?biāo)是跑DEMO,而不是追求最高的精度和性能,真正跑的話可以使用服務(wù)商提供的服務(wù),例如bge-m3text-embedding-3等模型。

      import { env, pipeline } from "@xenova/transformers";
      
      // 初始化編碼模型
      const model = "Xenova/all-MiniLM-L6-v2";
      env.localModelPath = path.resolve(__dirname, "../", "embedding");
      const featureExtractor = await pipeline("feature-extraction", model);
      
      /**
       * 文本 embedding
       */
      const embeddingTextChunk = async (
        featureExtractor: FeatureExtractionPipeline,
        text: string
      ): Promise<number[]> => {
        const output = await featureExtractor(text, {
          pooling: "mean",
          normalize: true,
        });
        return Array.from(output.data);
      };
      

      向量檢索

      在數(shù)據(jù)預(yù)處理以及數(shù)據(jù)編碼部分完成后,我們就需要處理數(shù)據(jù)存儲和數(shù)據(jù)檢索的方式了。雖然這部分是看起來過于專業(yè),就像是計(jì)算向量的相似度這種問題,我們通常無法過于干涉底層的實(shí)現(xiàn)細(xì)節(jié),但實(shí)際上我們還是可以對其進(jìn)行一些優(yōu)化和調(diào)整,也就是所謂的調(diào)參或者稱為煉丹。

      向量距離

      在前邊我們費(fèi)了那么大的勁介紹了文本向量化的實(shí)現(xiàn)方式,現(xiàn)在終于將其派上用場了。最開始我們就提到了,RAG服務(wù)是以搜索為核心的服務(wù),那么在文本向量化的情況下,搜索的核心就是對比兩段文本的向量相似度,即用戶Query和即將要召回的文本片段的向量相似度。

      而對比文本相似度的意義就在于,向量距離越小,語義相似度越高。常見的向量向量相似度計(jì)算方法有曼哈頓距離L1、歐氏距離L2、余弦相似度。在NLP領(lǐng)域中通常使用的是余弦相似度,主要是出于下面的一些原因:

      • 注重方向而非長度,余弦相似度通過計(jì)算向量夾角的余弦值,忽略向量的模長,只關(guān)注方向差異。這使得其更適合衡量文本語義相似性,因?yàn)槲谋鞠蛄康拈L度可能受詞頻、文檔長度等無關(guān)因素影響。
      • 適用于高維稀疏向量,余弦相似度在高維空間中表現(xiàn)良好,能夠有效處理高維稀疏向量的相似性計(jì)算問題。高維空間中,歐氏距離容易失效,所有向量兩兩距離趨于相似,而余弦相似度能更敏感地捕捉方向差異。
      • 計(jì)算效率高且便于歸一化,余弦相似度的計(jì)算只需要點(diǎn)積和模長計(jì)算,計(jì)算復(fù)雜度較低,尤其適合大規(guī)模文本數(shù)據(jù)的相似性檢索任務(wù)。余弦距離的計(jì)算范圍在[-1, 1]之間,易于歸一化處理,而L1L2距離的計(jì)算結(jié)果為[0, +∞)

      那么再回到我們最簡單的RAG服務(wù)的實(shí)現(xiàn)中,同樣使用的是輕量的hnswlib-node服務(wù)來實(shí)現(xiàn)向量檢索的功能。hnswlib是一個高效的近似最近鄰搜索庫,支持高維稠密向量的快速檢索,同樣適用于大規(guī)模向量數(shù)據(jù)集。

      import { HierarchicalNSW } from "hnswlib-node";
      
      const embeddingDIM = 384; // 嵌入向量的維度
      const maxElements = 1000; // 最大元素?cái)?shù)量
      const efConstruction = 200; // 構(gòu)建時動態(tài)候選列表大小
      const M = 16; // 每個節(jié)點(diǎn)的最大連接數(shù)
      const vectorStore = new HierarchicalNSW("cosine", embeddingDIM);
      vectorStore.initIndex(maxElements, efConstruction, M);
      

      接下來我們需要將預(yù)設(shè)的文本內(nèi)容切片并且將其向量化存儲到hnswlib中,首先將文本內(nèi)容切片,然后針對每個切片做向量化處理,最后將向量存儲到hnswlib中。這里其實(shí)有點(diǎn)業(yè)務(wù)邏輯,hnswlib僅支持傳入一個主鍵id來存儲向量,因此我們還有一個并行存儲來實(shí)際存儲切片和meta數(shù)據(jù)。

      const textChunks = spiltTextChunks(doc);
      for (let chunkIndex = 0; chunkIndex < textChunks.length; chunkIndex++) {
        const chunk = textChunks[chunkIndex];
        const chunkId = `doc_${docIndex}_chunk_${chunkIndex}`;
        const vector = await embeddingTextChunk(featureExtractor, chunk);
        const documentChunk: DocumentChunk = {
          id: chunkId,
          content: chunk,
          metadata: { docIndex, chunkIndex },
        };
        const { label } = await getParallelStoreLabel(documentChunk);
        vectorStore.addPoint(vector, label);
      }
      

      那么接下來我們就可以再使用hnswlib來進(jìn)行向量檢索了。在用戶傳入查詢內(nèi)容之后,需要對其進(jìn)行向量化處理,然后使用hnswlibsearchKnn方法來進(jìn)行向量檢索,返回與查詢內(nèi)容最相似的文本片段,這就是完整的一個召回流程了。

      const searchQuery = "機(jī)器學(xué)習(xí)是什么?";
      const queryEmbedding = await embeddingTextChunk(featureExtractor, searchQuery);
      const searchResults = vectorStore.searchKnn(queryEmbedding, /** TopK */ 3);
      const results = searchResults.neighbors.map((index, i) => ({
        id: labelMap[index].id,
        chunk: labelMap[index].content,
        metadata: labelMap[index].metadata,
        score: 1 - searchResults.distances[i],
      }));
      

      這里還有個比較有趣的點(diǎn),我們的向量檢索實(shí)際上是召回了與查詢內(nèi)容相關(guān)的分片id,而具體內(nèi)容是我們從并行存儲中獲取的。那么我們在存儲到向量數(shù)據(jù)庫的內(nèi)容還可以二次清洗,例如Md的標(biāo)題格式,用戶輸入通常不會攜帶##標(biāo)記,檢索時可能會更好匹配,召回時在從并行存儲中獲取實(shí)際內(nèi)容。

      // 向量數(shù)據(jù)庫
      ["機(jī)器學(xué)習(xí)", 0]
      
      // 并行存儲
      ["## 機(jī)器學(xué)習(xí)", 0]
      

      當(dāng)然這里的實(shí)現(xiàn)還是與向量數(shù)據(jù)庫本身的實(shí)現(xiàn)有關(guān),如果數(shù)據(jù)庫本身支持存儲meta信息,那么我們就可以直接從向量數(shù)據(jù)庫中獲取到相關(guān)的內(nèi)容,而不需要額外的并行存儲了。實(shí)際上大多支持向量查詢的數(shù)據(jù)庫都支持存儲meta信息,因此我們可以直接從向量數(shù)據(jù)庫中獲取到相關(guān)的內(nèi)容。

      多路召回

      在最開始我們也提到了,我們使用向量檢索的方式本質(zhì)上是為了對比語義的相似性,從而進(jìn)行檢索召回,核心的目標(biāo)就是模糊搜索。那么我們自然可以結(jié)合多種搜索方式,例如倒排索引、知識圖譜等方式,克服單一方法的局限性,從而實(shí)現(xiàn)更全面、更精確的信息檢索。

      雖然我們在前邊研究了很多詞向量的編碼方式,發(fā)現(xiàn)與LLMs同源的transformers存在比較好的向量相似度計(jì)算效果,但是并非傳統(tǒng)的關(guān)鍵詞檢索就完全不適用。實(shí)際上,關(guān)鍵詞檢索在處理特定類型的查詢時仍然非常有效,如果結(jié)合分片的關(guān)鍵字元信息則可以讓用戶動態(tài)調(diào)整檢索權(quán)重。

      組合向量檢索和關(guān)鍵詞搜索的方式有很多,例如先使用關(guān)鍵詞搜索進(jìn)行初步篩選,然后再使用向量檢索進(jìn)行精細(xì)化的結(jié)果排序。或者是先使用向量檢索進(jìn)行召回,然后再使用關(guān)鍵詞搜索進(jìn)行過濾和排序。不過現(xiàn)在普遍是兩者同時啟動,然后將兩種方式的召回結(jié)果統(tǒng)一加權(quán)排序/RRF等算法,取TopN即可。

      %%{init: {"theme": "neutral" } }%% graph LR Q[Query] --> M{多路召回} M -->|權(quán)重α| V[向量召回] M -->|權(quán)重β| K[關(guān)鍵詞召回] M -->|權(quán)重γ| S[圖譜召回] V & K & S --> F[加權(quán)融合/RFF] F --> R[Top-K結(jié)果]

      此外,如果對于Query重寫要求非常高的話還是可以對較小模型進(jìn)行微調(diào)來用的。LLMs雖然通常都是通用模型,但是現(xiàn)在也在向著專精化的方向發(fā)展,例如Claude系列模型對于問題更傾向于代碼實(shí)現(xiàn)來回答,而非常見的問答模式。專有任務(wù)也有相關(guān)的模型,例如embeddingrerank任務(wù)。

      其實(shí)在這里也體現(xiàn)出來RAG檢索部分的復(fù)雜性,能看出來RAG不僅僅是簡單的向量相似性搜索,而是可能涉及多種技術(shù)協(xié)同工作的復(fù)雜系統(tǒng)。想要做出來簡單的系統(tǒng)比較簡單,但是想要做出高質(zhì)量的RAG服務(wù)就需要綜合考慮多種因素。

      召回重排

      召回重排Re-RankingRAG流程中的一個關(guān)鍵后處理步驟,目標(biāo)是在進(jìn)一步優(yōu)化初步檢索階段的結(jié)果,從而提高最終生成響應(yīng)的準(zhǔn)確性和相關(guān)性。這里并不是簡單的排序,而是對檢索到的文檔進(jìn)行上下文相關(guān)性評估,確保最有意義的片段浮現(xiàn)到頂部。

      重排這部分可以做兩件事,首先是對召回的結(jié)果進(jìn)行過濾,去除掉一些明顯不相關(guān)的片段。其次是對召回的結(jié)果進(jìn)行排序,將最相關(guān)的片段排在前面。實(shí)際上如果跑我們的簡單RAG服務(wù)也可以發(fā)現(xiàn),我們覺得最相關(guān)的片段并不一定是最前面的片段,因此重排是非常有必要的。

      重排這部分是可以顯著提高召回結(jié)果的質(zhì)量的,尤其是在召回結(jié)果數(shù)量較多的情況下。NLP領(lǐng)域傳統(tǒng)的方法可以使用交叉編碼器Cross-Encoder,例如使用BERT作為交叉編碼器,來執(zhí)行NSP任務(wù),計(jì)算查詢與文檔的精細(xì)相關(guān)性,從而對召回結(jié)果重排。

      比較現(xiàn)代的方案通常都是用LLMs來進(jìn)行重排,LLMs可以通過更復(fù)雜的上下文理解和生成能力來對召回結(jié)果進(jìn)行重排。將召回的數(shù)據(jù)即粗排數(shù)據(jù),外加用戶Query作為輸入,給予LLMs處理后得到精排的數(shù)據(jù),這屬于提高召回結(jié)果質(zhì)量的方式,為了保證效率和成本通常選擇較小的模型來處理。

      此外,還記得我們在數(shù)據(jù)分片的時候提到的元信息嗎,在重排的時候也可以將元信息作為輸入的一部分,例如更新時間等,幫助LLMs更好地理解片段的上下文和背景,從而提高重排的效果。如果能夠提供一些人工打標(biāo)的樣本數(shù)據(jù),甚至可以進(jìn)一步提高重排的效果。

      Embedding模型類似,現(xiàn)在的云服務(wù)商也會提供ReRanker模型的服務(wù),例如BGE-Reranker-v2-M3Qwen3-Reranker等等。類似專門精調(diào)的ReRanker模型通常是專業(yè)任務(wù)模型,能夠提供更低的延時以及更穩(wěn)定的服務(wù),適用于較大的任務(wù)規(guī)模。

      %%{init: {"theme": "neutral" } }%% graph LR A[召回結(jié)果-粗排列表] --> B{重排策略} B --> C[傳統(tǒng)方法] C --> C1[交叉編碼器] --> F[精排列表] C --> C2[特征融合] --> F B --> D[LLM方法] D --> D1[評分模板] --> F D --> D2[ReRanker模型] --> F

      在具體的實(shí)踐中,我們可以會先調(diào)整召回的數(shù)量,以便于得到更多的結(jié)果,然后再使用重排來過濾和排序這些結(jié)果。而實(shí)際上這些相對更加的復(fù)雜任務(wù)加入到我們的系統(tǒng)里并不一定是正向的結(jié)果,因?yàn)椴豢杀苊獾卦斐闪烁邚?fù)雜行和響應(yīng)耗時,因此還是需要看具體的場景和需求來處理RAG任務(wù)。

      LLMs

      表面上看起來LLMsRAG系統(tǒng)中主要扮演著生成器的角色,但其作用遠(yuǎn)不止于此。實(shí)際上當(dāng)前的RAG服務(wù)已經(jīng)深度融合了LLMs的能力,因此我們不可避免地需要考慮LLMsRAG服務(wù)中的應(yīng)用,不過在這里我們會討論的比較簡單一些。

      查詢改寫

      RAG服務(wù)中,用戶的查詢內(nèi)容通常是自然語言的形式,這種情況用戶的查詢內(nèi)容可能會存在拼寫錯誤、多意圖、過于口語化、過于寬泛、上下文聯(lián)系等問題。因此我們需要對用戶的查詢內(nèi)容進(jìn)行改寫,以便于更好地匹配到相關(guān)的文檔片段。我們可以舉幾個例子:

      • 拼寫錯誤: microsft office下載方法 => Microsoft Office下載方法
      • 多意圖: 失眠導(dǎo)致頭痛怎么辦 => 因失眠引起的頭痛癥狀及綜合治療方案
      • 口語化: 我想找一個離我當(dāng)前位置不太遠(yuǎn)的價格合理的意大利餐廳 => 附近的意大利餐廳
      • 過于寬泛: 奧運(yùn)會獎牌榜 => 2024年巴黎奧運(yùn)會獎牌榜
      • 上下文關(guān)聯(lián): 多輪對話,Python安裝教程 - Mac系統(tǒng)的 => Mac系統(tǒng)的Python安裝教程

      用戶的查詢內(nèi)容通常來說并非最合適的,特別是在比較專業(yè)的領(lǐng)域性知識庫場景下,將用戶輸入的內(nèi)容進(jìn)行比較規(guī)范化的改寫是很有必要的。這個過程其實(shí)跟業(yè)務(wù)相關(guān)性比較大,而LLMs是非常適合處理這種自然語言的改寫任務(wù)的,理論上而言改寫之后應(yīng)該會有更正向的表現(xiàn)。

      而我們也可以實(shí)現(xiàn)一些通用的策略來處理用戶查詢改寫的問題,再具體的領(lǐng)域規(guī)則以及專業(yè)詞匯等,就需要特殊處理了。在這部分同樣需要保持微妙的平衡,過于具體的改寫可能排除相關(guān)結(jié)果,而過于寬泛的改寫則可能導(dǎo)致不相關(guān)結(jié)果。

      • 去噪: 糾正拼寫錯誤或語法問題,使查詢更清晰 。
      • 分解: 將包含多個意圖的復(fù)雜查詢分解為結(jié)構(gòu)化的子查詢。
      • 擴(kuò)展: 為原始查詢添加相關(guān)術(shù)語或同義詞,以擴(kuò)大檢索范圍,提高召回率。
      • 重構(gòu): 將口語化或模糊的查詢重寫為更正式、更精確的表達(dá),使其與知識庫中的內(nèi)容更匹配。

      輸入優(yōu)化

      在現(xiàn)代的RAG服務(wù)中,LLMs已經(jīng)穿行在了整個流程中,除了查詢改寫之外,LLMs還可以用于輸入優(yōu)化。這個輸入代指的部分非常寬泛,因?yàn)?code>RAG服務(wù)本質(zhì)上就是文本的處理服務(wù),而LLMs在文本處理方面有著非常強(qiáng)大的能力,因此理論上涉及文本處理的事情都可以由LLMs介入。

      首先,LLMs可以用于文本的分片和編碼,雖然我們在前邊已經(jīng)介紹了文本分片和編碼的方式,但是實(shí)際上LLMs可以更好地理解文本的語義和結(jié)構(gòu),因此可以更智能地進(jìn)行文本分片和編碼。例如LLMs可以根據(jù)文本的語義和結(jié)構(gòu)自動確定分片的大小和位置,從而更好地保留文本的上下文信息。

      其次,LLMs可以用于構(gòu)建知識圖譜的實(shí)體關(guān)系提取,微軟開源的GraphRAG就是利用LLMs來提供文檔內(nèi)的命名實(shí)體,然后利用這些知識來構(gòu)建知識圖譜。構(gòu)建知識圖譜除了可以用于知識庫的構(gòu)建,還可以用于知識庫的知識相關(guān)性、覆蓋度檢查等,從而提高RAG服務(wù)的智能化水平。

      再者,LLMs可以廣泛用于多模態(tài)內(nèi)容信息的提取,例如從圖像、音頻等非文本內(nèi)容中提取信息。最開始我們一直處理的內(nèi)容都是文本,這些多模態(tài)信息我們也是需要先轉(zhuǎn)換為文本再進(jìn)行Embedding流程,而現(xiàn)在我們可以直接對多模態(tài)的內(nèi)容做Embedding,省去了轉(zhuǎn)文本的步驟。

      最后,我們可以發(fā)現(xiàn)整個RAG服務(wù)的流程中,涉及了非常多的環(huán)節(jié),特別是我們?nèi)绻尤肓烁鄶U(kuò)展服務(wù),例如多路召回、召回重排等環(huán)節(jié)。那么是不是可以類似流程編排的形式,進(jìn)行節(jié)點(diǎn)的自由組合。甚至類似于Function Call的形式,讓模型來決定如何執(zhí)行后續(xù)流程。

      生成增強(qiáng)

      生成增強(qiáng)這部分就是比較常見的LLMs生成內(nèi)容的能力了,這部分其實(shí)比較簡單。主要是將召回的片段內(nèi)容作為上下文信息,如果有必要的話還可以結(jié)合原始文檔的部分內(nèi)容,結(jié)合用戶的查詢內(nèi)容,使用LLMs來生成最終的回答內(nèi)容。

      這個過程可以看作是一個文本生成任務(wù),LLMs可以根據(jù)召回的片段內(nèi)容和用戶的查詢內(nèi)容來生成更自然、更流暢的回答。畢竟,如果只是簡單地將召回的片段內(nèi)容直接返回給用戶,可能會導(dǎo)致回答不夠連貫或者缺乏上下文信息,用戶讀起來也比較難。

      我們這里其實(shí)還可以思考一個問題,既然最終都是用LLMs來生成回答,那么為什么還需要召回重排。實(shí)際上這里主要目的還是提升召回結(jié)果的質(zhì)量,尤其是在召回結(jié)果數(shù)量較多的情況下,減少噪音以及提高相關(guān)性。這也是分而治之的體現(xiàn),合理的流程設(shè)計(jì)可以讓每個環(huán)節(jié)都發(fā)揮最大的作用。

      其實(shí)到這里,我們已經(jīng)基本完成了整個流程。我個人覺得RAG系統(tǒng)的一個顯著優(yōu)勢是其能夠提供生成答案的來源,這種透明度增加了用戶對系統(tǒng)輸出的信任,而不是考慮是不是模型幻覺。我們也可以再深入提升體驗(yàn),并且點(diǎn)擊跳轉(zhuǎn)后可以直接定位到原文位置上,這理論上應(yīng)該是一個不錯的體驗(yàn)。

      總結(jié)

      在本文中我們討論了RAG服務(wù)的基礎(chǔ)實(shí)現(xiàn),主要包括數(shù)據(jù)預(yù)處理、數(shù)據(jù)編碼、向量檢索、多路召回、召回重排以及LLMs的應(yīng)用等方面。我們通過一個簡單的示例來展示如何實(shí)現(xiàn)一個基礎(chǔ)的RAG服務(wù),并且介紹了相關(guān)的技術(shù)細(xì)節(jié)和實(shí)現(xiàn)方式。

      除了上述的應(yīng)用場景,RAG服務(wù)作為LLMsContext補(bǔ)充服務(wù)也發(fā)揮了重要作用。前些時間我在知乎看到了一個問題,如果LLMs存在足夠長的輸入,是否有必要使用RAG服務(wù)來補(bǔ)充Context信息。實(shí)際上這個問題我之前也考慮過,目前來說答案自然是否定的。

      最明顯的問題就是成本,盡管現(xiàn)在Token的成本已經(jīng)大幅度下降,但是使用RAG和全部作為Prompt輸入相比成本差異還是巨大的。其次,即使Transformer可以有更長的上下文,但是越長的內(nèi)容,模型的處理時間和資源消耗也會顯著增加,過長的上下文也同樣會造成遺忘。

      此外,最開始我是想使用LangChain來搭建這個最簡單的RAG服務(wù)的,但是發(fā)現(xiàn)LangChain的服務(wù)封裝的太復(fù)雜了,反而不利于理解RAG的基本實(shí)現(xiàn)方式了。其實(shí)這也與最開始我們討論的問題呼應(yīng),若是需要開箱即用的服務(wù),LangChain是不錯的選擇,反之則需要更靈活的實(shí)現(xiàn)方式。

      每日一題

      參考

      posted @ 2025-08-06 10:22  WindRunnerMax  閱讀(368)  評論(0)    收藏  舉報
      ?Copyright    @Blog    @WindRunnerMax
      主站蜘蛛池模板: 国产超碰无码最新上传| 成年女人免费毛片视频永久| 欧美成人精品手机在线| 国产精品福利自产拍久久| 四虎影视一区二区精品 | 欧美片内射欧美美美妇| 久久精品国产蜜臀av| 无码福利一区二区三区| 久久亚洲精品亚洲人av| 91高清免费国产自产拍| 女人与牲口性恔配视频免费| 99久久99久久久精品久久| 久久精品久久精品久久精品| 亚洲人成人网站色www| 亚洲国产性夜夜综合| 亚洲一区二区三区丝袜| 亚洲精品午夜国产VA久久成人 | 国产欧美亚洲精品第1页| 亚洲人成网线在线播放VA| 久久精品国产亚洲夜色av网站| 国产精品综合色区在线观| 2019国产精品青青草原| 男女性高爱潮免费网站| 天堂网av最新版在线看| 久久精品国产再热青青青| 国产精品区免费视频| 亚洲中文字幕有综合久久| 麻豆国产成人AV在线播放| 特黄 做受又硬又粗又大视频| 色综合激情丁香七月色综合 | 久热这里只有精品6| 亚洲精品日韩中文字幕| 久久精品一偷一偷国产| 国产成人午夜精品影院| 岢岚县| 免费看亚洲一区二区三区| 亚洲成人av综合一区| 灵璧县| 日韩大片高清播放器| 午夜福利国产精品视频| 久久精品一本到99热免费|