RAG
RAG過程
離線過程:
- 加載文檔
- 將文檔按一定條件切割成片段
- 將切割的文本片段轉為向量,存入檢索引擎(向量庫)
在線過程:
- 用戶輸入Query,將Query轉為向量
- 從向量庫檢索,獲得相似度TopN信息
- 將檢索結果和用戶輸入,共同組成Prompt
- 將Prompt 輸入大模型,獲取LLM的回復
用langchian構建RAG
1. 加載文檔
LangChain中的文檔對象Document
有兩個屬性:
page_content
: str類型,記錄此文檔的內容。metadata
: dict類型,保存與此文檔相關的元數據,包括文檔ID、文件名等。
在langchain中,加載文檔使用 文檔加載器DocumentLoader
來實現,它從源文檔加載數據并返回文檔列表。每個DocumentLoader
都有其特定的參數,但它們都可以通過.load()
方法以相同的方式調用。
加載pdf
需要先安裝pypdf庫
pip install pypdf
用PyPDFLoader
加載pdf,輸入是文件路徑,輸出提取出的內容列表:
from langchain_community.document_loaders import PyPDFLoaderloader = PyPDFLoader(file_path)
pages = []
for page in loader.load():pages.append(page)
返回的文檔列表如下所示:
[
Document(metadata={'source': 'D:\\桌面\\RAG分析.pdf', 'page': 0}, page_content='11111111111111111111111111111111111111111111111111111111'),
Document(metadata={'source': 'D:\\桌面\\RAG分析.pdf', 'page': 1}, page_content='2222222222222222222222222222222222222222222222222222222')
]
加載網頁
簡單快速的文本提取
對于“簡單快速”解析,需要 langchain-community 和 beautifulsoup4 庫:
pip install langchain-community beautifulsoup4
使用WebBaseLoader
,輸入是url的列表,返回一個 Document 對象的列表,列表里的內容是一系列包含頁面文本的字符串。在底層,它使用的是 beautifulsoup4 庫。
import bs4
from langchain_community.document_loaders import WebBaseLoaderpage_url = "https://python.langchain.com/docs/how_to/chatbots_memory/"loader = WebBaseLoader(web_paths=[page_url])
docs = loader.load()assert len(docs) == 1
print(docs)
這樣提取出的基本上是頁面 HTML 中文本的轉儲,可能包含多余的信息,提取的信息如下所示(截取了首部)
[Document(metadata={'source': 'https://python.langchain.com/', 'title': 'How to add memory to chatbots | 🦜?🔗 LangChain',},page_content='\n\n\n\n\nHow to add memory to chatbots | 🦜?🔗 LangChain\n\n\n\n\n\n\nSkip to main contentJoin us at ')
]
如果了解底層 HTML 中主體文本的表示,可以通過 BeautifulSoup 指定所需的 <div>
類和其他參數。
下面僅解析文章的主體文本:
loader = WebBaseLoader(web_paths=[page_url],bs_kwargs={"parse_only": bs4.SoupStrainer(class_="theme-doc-markdown markdown"),},bs_get_text_kwargs={"separator": " | ", "strip": True},
)docs = []
async for doc in loader.alazy_load():docs.append(doc)assert len(docs) == 1
doc = docs[0]
提取出的內容如下所示:
[Document(metadata={'source': 'https://python.langchain.com/docs/how_to/chatbots_memory/'}, page_content='How to add memory to chatbots | A key feature of chatbots is their ability to use the content of previous conversational turns as context. ')
]
可以使用各種設置對 WebBaseLoader 進行參數化,允許指定請求頭、速率限制、解析器和其他 BeautifulSoup 的關鍵字參數。詳細信息參見 API 參考。
高級解析
如果想對頁面內容進行更細粒度的控制或處理,可以用langchain-unstructured進行高級解析。
pip install langchain-unstructuredpip install unstructured
注意: 如果不安裝unstructured會報錯!
下面的代碼不是為每個頁面生成一個 Document 并通過 BeautifulSoup 控制其內容,而是生成多個 Document 對象,表示頁面上的不同結構。這些結構包括章節標題及其對應的主體文本、列表或枚舉、表格等。
from langchain_unstructured import UnstructuredLoaderpage_url = "https://python.langchain.com/docs/how_to/chatbots_memory/"
loader = UnstructuredLoader(web_url=page_url)docs = []
for doc in loader.load():docs.append(doc)print(docs[:5])
輸出如下所示(太長了,截取了部分):
[Document(metadata={'image_url': 'https://colab.research.google.com/assets/colab-badge.svg', 'link_texts': ['Open In Colab'], 'link_urls': ['https://colab.research.google.com/github/langchain-ai/langchain/blob/master/docs/docs/how_to/chatbots_memory.ipynb'], 'languages': ['eng'], 'filetype': 'text/html', 'url': 'https://python.langchain.com/docs/how_to/chatbots_memory/', 'category': 'Image', 'element_id': '76f10732f139a03f24ecf55613a5116a'}, page_content='Open In Colab'), Document(metadata={'category_depth': 0, 'languages': ['eng'], 'filetype': 'text/html', 'url': 'https://python.langchain.com/docs/how_to/chatbots_memory/','category': 'Title', 'element_id': 'b6bfe64119578f39e0dd7d0287b2964a'}, page_content='How to add memory to chatbots'), Document(metadata={'languages': ['eng'], 'filetype': 'text/html', 'parent_id': 'b6bfe64119578f39e0dd7d0287b2964a', 'url': 'https://python.langchain.com/docs/how_to/chatbots_memory/', 'category': 'NarrativeText', 'element_id': 'ac3524f3e30afbdf096b186a665188ef'},page_content='A key feature of chatbots is their ability to use the content of previous conversational turns as context. This state management can take several forms, including:'), Document(metadata={'category_depth': 1, 'languages': ['eng'], 'filetype': 'text/html', 'parent_id': 'b6bfe64119578f39e0dd7d0287b2964a', 'url': 'https://python.langchain.com/docs/how_to/chatbots_memory/', 'category': 'ListItem', 'element_id': '0c9193e450bacf9a4d716208b2e7b1ee'}, page_content='Simply stuffing previous messages into a chat model prompt.'),
]
可以用doc.page_content
來輸出正文文本:
for doc in docs[:5]:print(doc.page_content)
輸出如下所示:
Open In Colab
Open on GitHub
How to add memory to chatbots
A key feature of chatbots is their ability to use the content of previous conversational turns as context. This state management can take several forms, including:
Simply stuffing previous messages into a chat model prompt.
2. 切割文檔
當文檔過長時,模型存在兩方面問題:一是可能無法完整加載至上下文窗口,二是即便能加載,從中提取有用信息也較為困難。
為破解此困境,可將文檔拆分成多個塊來進行嵌入與存儲,如此一來,分塊后還能快速精準地定位到與查詢最相關的部分。
在具體操作中,可以把文檔按照每塊 1000 個字符的規格進行拆分,同時在塊之間設置 200 個字符的重疊。之所以設置重疊,是為了降低將某塊有效信息與其關聯的重要上下文被人為割裂開的風險,保證內容的連貫性與完整性。
以下代碼采用的文本分割器是 RecursiveCharacterTextSplitter
,它會利用常見分隔符對文檔進行遞歸拆分,直至各塊大小滿足既定要求。此外,代碼還啟用了 add_start_index=True
的設置,使每個分割后的文檔塊在初始文檔中的字符起始位置得以保留,這一位置信息將以元數據屬性 “start_index” 的形式呈現。
from langchain_text_splitters import RecursiveCharacterTextSplittertext_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200, add_start_index=True
)
all_splits = text_splitter.split_documents(docs)
3. 將塊轉為向量存儲到數據庫
為了在運行時能夠對文本塊進行高效搜索,我們通常需要對文本塊進行索引,而嵌入內容是實現這一目標的常見方法。
我們將每個文檔分割后的文本塊進行嵌入處理,并將這些嵌入插入到向量數據庫中。當要搜索這些分割后的文本塊時,可以將文本搜索查詢進行嵌入,然后執行“相似性”度量,以識別出與查詢嵌入最相似的存儲文本塊。其中,余弦相似性是一種簡單且常用的相似性度量方法,它通過測量每對嵌入(高維向量)之間的角度的余弦值,來評估它們的相似程度。
我們可以通過Chroma 向量存儲和 Ollama嵌入模型,完成對所有文檔分割后的文本塊的嵌入和存儲工作。
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddingsembedding = OllamaEmbeddings(model_url="http://localhost:11434")
vectorstore = Chroma.from_documents(documents=all_splits, embedding=embedding)
4. 檢索
創建一個簡單的應用程序,接受用戶問題,搜索相關文檔,并將檢索到的文檔和初始問題傳遞給模型以返回答案,具體步驟如下:
-
定義搜索文檔的邏輯:LangChain 定義了一個檢索器接口,它封裝了一個可以根據字符串返回相關文檔的索引查詢。檢索器的唯一要求是能夠接受查詢并返回文檔,具體的底層邏輯由檢索器指定。
-
使用向量存儲檢索器:最常見的檢索器類型是向量存儲檢索器,它利用向量存儲的相似性搜索能力來促進檢索。任何
VectorStore
都可以輕松轉換為一個Retriever
,使用VectorStore.as_retriever()
方法。
retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 6})retrieved_docs = retriever.invoke("What are the approaches to Task Decomposition?")print(retrieved_docs[0].page_content)
輸出如下所示:
Tree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple reasoning possibilities at each step. It first decomposes the problem into multiple thought steps and generates multiple thoughts per step, creating a tree structure. The search process can be BFS (breadth-first search) or DFS (depth-first search) with each state evaluated by a classifier (via a prompt) or majority vote.
Task decomposition can be done (1) by LLM with simple prompting like "Steps for XYZ.\n1.", "What are the subgoals for achieving XYZ?", (2) by using task-specific instructions; e.g. "Write a story outline." for writing a novel, or (3) with human inputs.
5. 生成
生成時,將所有內容整合到一個鏈中,該鏈接受一個問題,檢索相關文檔,構建提示,將其傳遞給模型,并解析輸出。
from langchain_community.document_loaders import UnstructuredURLLoader
from langchain_community.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.retrievers import VectorStoreRetriever
from langchain_community.prompts import PromptTemplate
from langchain_community.chains import RetrievalQA# 加載文檔
page_url = "https://python.langchain.com/docs/how_to/chatbots_memory/"
loader = UnstructuredURLLoader(urls=[page_url])# 拆分文檔
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200, add_start_index=True
)
all_splits = text_splitter.split_documents(loader.load())# 創建向量存儲和嵌入
embedding = OllamaEmbeddings(model_url="http://localhost:11434")
vectorstore = Chroma.from_documents(documents=all_splits, embedding=embedding)
retriever = VectorStoreRetriever(vectorstore=vectorstore)# 構建提示和生成鏈
prompt_template = """Use the following context to answer the question at the end. If the answer isn't found in the context, just say that you don't know.{context}Question: {question}"""
PROMPT = PromptTemplate.from_template(prompt_template)# 創建 RAG 鏈
rag_chain = RetrievalQA.from_chain_type(retriever=retriever,chain_type="stuff",prompt=PROMPT
)# 執行查詢
result = rag_chain.invoke("What are the approaches to Task Decomposition?")
print(result['result'])