教程地址:https://github.com/datawhalechina/all-in-rag/
感謝datawhale的教程,以下筆記大部分內容來自該教程
文章目錄
- 基于LangChain框架的RAG實現
- 初始化設置
- 數據準備
- 索引構建
- 查詢與檢索
- 生成集成
- 低代碼(基于LlamaIndex)
conda activate all-in-rag
# 假設當前在 all-in-rag 項目的根目錄下
cd code/C1
#運行代碼
python 01_langchain_example.py
輸出的參數:
- content: 這是最核心的部分,即大型語言模型(LLM)根據你的問題和提供的上下文生成的具體回答。
- additional_kwargs: 包含一些額外的參數,在這個例子中是 {‘refusal’: None},表示模型沒有拒絕回答。
- response_metadata: 包含了關于LLM響應的元數據。
- token_usage: 顯示了本次調用消耗的token數量,包括完成(completion_tokens)、提示(prompt_tokens)和總量(total_tokens)。
- model_name: 使用的LLM模型名稱,當前是 deepseek-chat。
- system_fingerprint, id, service_tier, finish_reason, logprobs: 這些是更詳細的API響應信息,例如 finish_reason: ‘stop’ 表示模型正常完成了生成。
- id: 本次運行的唯一標識符。
- usage_metadata: 與 response_metadata 中的 token_usage 類似,提供了輸入和輸出token的統計。
如果沒有魔法,且下載模型失敗
在shell中從huggingface下載模型
export HF_ENDPOINT=https://hf-mirror.com
hf download --local-dir ./bge-small-zh-v1.5 BAAI/bge-small-zh-v1.5
在使用的時候顯示指定位置在哪個路徑。
基于LangChain框架的RAG實現
初始化設置
import os
# os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
from dotenv import load_dotenv
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_core.prompts import ChatPromptTemplate
from langchain_deepseek import ChatDeepSeek# 加載環境變量
load_dotenv()
數據準備
- 加載原始文檔: 先定義Markdown文件的路徑,然后使用TextLoader加載該文件作為知識源
markdown_path = "../../data/C1/markdown/easy-rl-chapter1.md"
loader = TextLoader(markdown_path)
docs = loader.load()
- 文本分塊 (Chunking): 為了便于后續的嵌入和檢索,長文檔被分割成較小的、可管理的文本塊(chunks)。這里采用了遞歸字符分割策略,使用其默認參數進行分塊。當不指定參數初始化 RecursiveCharacterTextSplitter() 時,其默認行為旨在最大程度保留文本的語義結構:
- 默認分隔符與語義保留: 按順序嘗試使用一系列預設的分隔符 [“\n\n” (段落), “\n” (行), " " (空格), “” (字符)] 來遞歸分割文本。這種策略的目的是盡可能保持段落、句子和單詞的完整性,因為它們通常是語義上最相關的文本單元,直到文本塊達到目標大小。
- 保留分隔符: 默認情況下 (keep_separator=True),分隔符本身會被保留在分割后的文本塊中。
- 默認塊大小與重疊: 使用其基類 TextSplitter 中定義的默認參數 chunk_size=4000(塊大小)和 chunk_overlap=200(塊重疊)。這些參數確保文本塊符合預定的大小限制,并通過重疊來減少上下文信息的丟失。
text_splitter = RecursiveCharacterTextSplitter()
texts = text_splitter.split_documents(docs)
索引構建
初始化中文嵌入模型+構建向量存儲
HugginfaceEmbeddings+InMemoeyVectorStore
初始化中文嵌入模型: 使用HuggingFaceEmbeddings加載之前在初始化設置中下載的中文嵌入模型。配置模型在CPU上運行,并啟用嵌入歸一化 (normalize_embeddings: True)。
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5",model_kwargs={'device': 'cpu'},#配置模型在CPU上運行encode_kwargs={'normalize_embeddings': True}#啟用嵌入歸一化
)
構建向量存儲: 將分割后的文本塊 (texts) 通過初始化好的嵌入模型轉換為向量表示,然后使用InMemoryVectorStore將這些向量及其對應的原始文本內容添加進去,從而在內存中構建出一個向量索引。
vectorstore = InMemoryVectorStore(embeddings)
vectorstore.add_documents(texts)
查詢與檢索
索引構建完畢后,便可以針對用戶問題進行查詢與檢索:
定義用戶查詢: 設置一個具體的用戶問題字符串。
question = "文中舉了哪些例子?"
在向量存儲中查詢相關文檔: 使用向量存儲的similarity_search方法,根據用戶問題在索引中查找最相關的 k (此處示例中 k=3) 個文本塊。
retrieved_docs = vectorstore.similarity_search(question, k=3)
準備上下文: 將檢索到的多個文本塊的頁面內容 (doc.page_content) 合并成一個單一的字符串,并使用雙換行符 (“\n\n”) 分隔各個塊,形成最終的上下文信息 (docs_content) 供大語言模型參考。
docs_content = "\n\n".join(doc.page_content for doc in retrieved_docs)
使用 “\n\n” (雙換行符) 而不是 “\n” (單換行符) 來連接不同的檢索文檔塊,主要是為了在傳遞給大型語言模型(LLM)時,能夠更清晰地在語義上區分這些獨立的文本片段。雙換行符通常代表段落的結束和新段落的開始,這種格式有助于LLM將每個塊視為一個獨立的上下文來源,從而更好地理解和利用這些信息來生成回答。
生成集成
最后一步是將檢索到的上下文與用戶問題結合,利用大語言模型(LLM)生成答案:
- 構建提示詞模板: 使用ChatPromptTemplate.from_template創建一個結構化的提示模板。此模板指導LLM根據提供的上下文 (context) 回答用戶的問題 (question),并明確指出在信息不足時應如何回應。
prompt = ChatPromptTemplate.from_template("""請根據下面提供的上下文信息來回答問題。
請確保你的回答完全基于這些上下文。
如果上下文中沒有足夠的信息來回答問題,請直接告知:“抱歉,我無法根據提供的上下文找到相關信息來回答此問題。”上下文:
{context}問題: {question}回答:"""
配置大語言模型: 初始化ChatDeepSeek客戶端,配置所用模型 (deepseek-chat)、生成答案的溫度參數 (temperature=0.7)、最大Token數 (max_tokens=2048) 以及API密鑰 (從環境變量加載)。
llm = ChatDeepSeek(model="deepseek-chat",temperature=0.7,max_tokens=2048,api_key=os.getenv("DEEPSEEK_API_KEY")
)
調用LLM生成答案并輸出: 將用戶問題 (question) 和先前準備好的上下文 (docs_content) 格式化到提示模板中,然后調用ChatDeepSeek的invoke方法獲取生成的答案。
answer = llm.invoke(prompt.format(question=question, context=docs_content))
print(answer)
# 文本分塊
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, # 增大塊大小,每個塊約1000字符chunk_overlap=200, # 增大重疊部分,保持上下文連貫性separators=["\n\n", "\n", " ", ""] # 自定義分割符)
調整chunksize和chunk_overlap
print(answer.content)#獲取回答
參數調整對輸出的影響
- chunk_size 的影響:
- 較小的chunk_size(如 500):文本塊更細碎,檢索到的信息可能更精準但不完整
- 較大的chunk_size(如 2000):文本塊更完整,上下文信息更豐富,但可能包含冗余內容
- chunk_overlap 的影響:
- 較小的chunk_overlap(如 50):文本塊獨立性強,可能丟失跨塊的上下文關聯
- 較大的chunk_overlap(如 300):增強了塊之間的連貫性,適合處理有連續邏輯的文本,但會增加冗余
低代碼(基于LlamaIndex)
在RAG方面,LlamaIndex提供了更多封裝好的API接口,這無疑降低了上手門檻,下面是一個簡單實現:
import os
# os.environ['HF_ENDPOINT']='https://hf-mirror.com'
from dotenv import load_dotenv
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.llms.deepseek import DeepSeek
from llama_index.embeddings.huggingface import HuggingFaceEmbeddingload_dotenv()
#環境設置
#模型配置
Settings.llm = DeepSeek(model="deepseek-chat", api_key=os.getenv("DEEPSEEK_API_KEY"))
## 設置使用 DeepSeek 的聊天模型作為語言模型
Settings.embed_model = HuggingFaceEmbedding("BAAI/bge-small-zh-v1.5")
## 設置使用 BAAI 的中文小型嵌入模型進行文本向量化documents = SimpleDirectoryReader(input_files=["../../data/C1/markdown/easy-rl-chapter1.md"]).load_data()
## 從指定路徑加載 Markdown 文檔內容
index = VectorStoreIndex.from_documents(documents)
# 將文檔內容轉換為向量索引,便于后續的相似性搜索
query_engine = index.as_query_engine()
# 創建查詢引擎,用于處理自然語言查詢
print(query_engine.get_prompts()) # 打印使用的提示詞模板
print(query_engine.query("文中舉了哪些例子?")) # 執行查詢并打印結果