前言#
在開始講解什麼是 RAG 之前,先討論討論現在我們用的這些模型或者說 LLM 有哪些缺點:
- 在沒有答案的情況下提供虛假信息。(會胡說八道)
- 當用戶需要特定的當前響應時,提供過時或通用的信息。(因為模型有訓練的截止日期)
- 從非權威來源創建響應。(因為訓練的內容來自於互聯網,所以可能會有一些不準確的內容)
- 由於術語混淆,不同的培訓來源使用相同的術語來談論不同的事情,因此會產生不準確的響應。
還有一個重要的也比較現實的原因 —— 我們自己有一些私人的數據,這些數據市面上的一些模型是肯定沒有對我們的數據進行訓練,所以單我們需要對自己的數據集進行詢問就是一個不可能的。
所以 RAG(檢索增強生成) 就出現了 —— 它通過修改與大型語言模型(LLM)的互動,使模型在回應用戶查詢時參考指定的文檔集,優先使用這些信息,而不是從自身龐大的靜態訓練數據中提取信息。這樣,LLMs 就可以使用特定領域和 / 最新信息。 用例包括讓聊天機器人訪問公司內部數據,或只提供來自權威來源的事實信息。
流程#
- 首先,需要將我們的一些知識數據使用向量模型轉化為向量,然後保存在向量數據庫當中
- 剩下的就是用戶輸入,然後我們會將用戶的數據轉化為向量,使用用戶輸入的向量對現在的向量數據庫進行檢索(Retrieval),按照相似度匹配找出最接近的幾條數據,使用大模型進行將檢索出的數據源和用戶需求的問題組合(Augmented)一起輸入給大模型進行生成(Generation),這樣 —— 完整的 RAG 就出來啦。
代碼實現#
作為一個前端開發來說,最熟悉的也就是 JavaScript,剛好市面上比較出名的langchain有 js 的版本,使得前端開發進入 AI 開發變得更加的容易,廢話不多說現在就開始吧!
簡單使用#
接下來,我們通過代碼展示如何使用 RAG。為了簡單起見,我們採用 LangChain 的 JavaScript 版本,並在本地使用 llama3:3b 模型。你可以根據需要替換其他模型。
import { ChatOllama } from "@langchain/ollama";
import { StringOutputParser } from '@langchain/core/output_parsers'
const llm = new ChatOllama({
model: 'llama3.2:3b-instruct-fp16',
temperature: 0, // 控制生成內容的隨機性
});
const res = await llm.invoke('你好');
// 對輸出文本進行格式化
const parser = new StringOutputParser();
const parsed = await parser.invoke(res);
console.log(parsed);
// 輸出:您好!我是你的AI助手,歡迎來到我們的對話中。有什么需要幫助的嗎?
加載文檔#
因為我們的目的是對自己的數據進行增強搜索,那麼第一步就是需要加載文檔,langchain 提供了許多加載不同文檔的方法(文檔加載),基本上包含了市面上所有的文檔,有興趣的可以去查看一下。本次我們採用的是 Web 遠程加載文檔。
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
const loader = new CheerioWebBaseLoader("https://goodcheng.wang/web3", {
selector: 'article' // 指定加載內容的區域
});
const doc = await loader.load();
對文檔進行分割#
為什麼要分割文檔呢?直接對整個文檔進行嵌入(embedding)看似簡單,但實際上這樣做並不高效。原因如下:
- 內存與計算限制:大規模文檔(如整本書、長篇報告等)通常非常龐大,如果直接將整個文檔進行 embedding,計算資源和內存消耗會非常高。有一個顯而易見的問題 —— 我們所用到的模型不支持這麼龐大的輸入。
- 上下文窗口限制:現有的大多數自然語言處理(NLP)模型在處理文本時有一定的上下文窗口大小限制,即模型只能處理一定數量的 token。如果文本超出了該窗口大小,超出部分就會被截斷或忽略,導致丟失大量信息。因此,將文檔分割成小塊,每塊的長度適應模型的上下文限制,可以確保模型在生成 embedding 時能夠充分利用輸入的上下文信息。
- 提升檢索與匹配精度: 對文檔進行分割還可以提高信息檢索的精度。將整個文檔 embedding 後,查詢特定問題時,embedding 可能會涵蓋太多不相關的信息,導致匹配不精確。相反,如果對文檔進行分割並對每個分段進行 embedding,查詢時可以只檢索到與問題最相關的段落,結果更加精準。
- and so on
在這裡我們將文本每 500 個字符進行分割,相鄰 100 個字符作為一個 “塊”。感興趣的可以在這個網站上自己摸索Text-splitter demo。
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
const splitter = new RecursiveCharacterTextSplitter({
chunkOverlap: 100, // 相鄰塊的重疊字符數,增強連續性
chunkSize: 500, // 每塊的字符數
});
const allSplits = await splitter.splitDocuments(doc);
console.log(allSplits[0]); // 打印分割後的第一個塊
將內容 embedding 存儲到向量數據庫當中#
接下來,將分割後的文檔內容進行嵌入,並存儲在向量數據庫中。在這個示例中,我們使用內存中的向量數據庫,實際應用中可以替換為其他存儲方案。。VectorStore
import { OllamaEmbeddings } from "@langchain/ollama";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
const embeddings = new OllamaEmbeddings({
model: "mxbai-embed-large", // Default value
baseUrl: "http://localhost:11434", // Default value
});
const vectorStore = await MemoryVectorStore.fromDocuments(allSplits, embeddings)
const vectorStoreRetriever = vectorStore.asRetriever({
k: 5,
searchType: 'similarity',
})
const find = await vectorStoreRetriever.invoke("什麼是web3.0")
console.log(find)
整合成 RAG 鏈#
最後,我們把上述所有步驟整合成一個完整的處理鏈。首先為生成的鏈編寫提示詞模板:
import {ChatPromptTemplate} from '@langchain/core/prompts'
const ragPrompt = ChatPromptTemplate.fromMessages([
[
'human',
`你是問題解答任務的助手。使用以下檢索到的上下文回答問題。如果不知道答案,就說不知道。最多使用三句話,答案要簡明扼要。
問題: {question}
上下文: {context}
答案:`
]
])
然後,使用 RunnableSequence 對流程進行鏈接:
import { formatDocumentsAsString } from "langchain/util/document";
import { RunnablePassthrough, RunnableSequence } from "@langchain/core/runnables";
const runnableRagChain = RunnableSequence.from([
{
context: vectorStoreRetriever.pipe(formatDocumentsAsString), // 檢索到的上下文
question: new RunnablePassthrough(), // 用戶問題
},
ragPrompt, // 生成的提示詞
llm, // 大語言模型
new StringOutputParser(), // 解析生成的結果
]);
const res = await runnableRagChain.invoke('web2.0的代表網站有哪些?');
console.log(res);
// 輸出:Facebook和Twitter是Web2.0的著名網站。
效果還不錯~
總結#
通過 RAG(檢索增強生成),我們能夠提升現有大語言模型的回答準確性,尤其是在需要最新或特定領域數據的場景中。本文通過 LangChain 的 JavaScript 實現展示了如何從文檔加載、分割、嵌入到最終生成的整個流程。