我会一步一步详细解释每一行代码,并用通俗易懂的方式举例,让你更好地理解这段 RAG(Retrieval-Augmented Generation,检索增强生成)流程的代码。
首先环境文件.env文件配置如下:
相关密钥需要自己去官方网站申请
代码的整体流程如下:
抓取网页内容(从指定 URL 获取文本)。切分文本(将大段内容拆分成小块)。创建向量数据库(用 FAISS 存储文本的嵌入向量)。检索相关文本(从数据库中找到与问题相关的文本)。使用 LLM(大模型)生成答案(结合检索结果,回答用户问题)。
第一部分:索引构建(Indexing)
1. 导入必要的库
import bs4
from langchain import hub
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.vectorstores import FAISS
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_groq import ChatGroq
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
解释
bs4:BeautifulSoup,用于解析网页 HTML 结构,提取有用内容。langchain.hub:LangChain Hub,一个存储 prompt(提示词)的地方,方便复用预设的 prompt。RecursiveCharacterTextSplitter:递归字符文本分割器,用于将长文本切成小块,以适应 LLM 处理。WebBaseLoader:网页加载器,可以抓取网页内容并解析文本。FAISS:Facebook AI Similarity Search,用于存储和快速检索嵌入向量。StrOutputParser:用于解析 LLM 生成的文本输出。RunnablePassthrough:一个“直通”组件,数据不会被修改,直接传递下去。ChatGroq:调用 Groq 平台的大模型(如 LLaMA 3)。HuggingFaceBgeEmbeddings:从 Hugging Face 加载 BGE 模型,将文本转换为向量。
2. 加载网页文档
loader = WebBaseLoader(
web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(
class_=("post-content", "post-title", "post-header")
)
),
)
docs = loader.load()
解释
WebBaseLoader:从网页加载内容(相当于用爬虫抓取页面)。web_paths:指定要抓取的网页地址。bs_kwargs:传递给 BeautifulSoup 的参数,parse_only=bs4.SoupStrainer(...) 表示只提取网页中 class 名称为 post-content、post-title 和 post-header 的内容。loader.load():真正执行爬取,并把文本内容存到 docs 变量中。
举例
假设你在浏览一个技术博客,WebBaseLoader 相当于一个机器人,专门去获取文章的标题和正文内容,忽略网页的广告、导航栏等无关信息。
3. 切分文本
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
解释
RecursiveCharacterTextSplitter:递归地按字符切分文本,确保每块内容不会太长,但仍然有上下文。chunk_size=1000:每个文本块最多 1000 个字符。chunk_overlap=200:每个相邻的文本块重叠 200 个字符,防止信息丢失。split_documents(docs):对网页抓取的内容进行切分,存到 splits 变量中。
举例
假设你有一本书,每页 1000 个字,你想把它拆分成小段落,但又不想让每一页的内容割裂,所以新的一页会重复上页的最后 200 个字,这样即使某段内容被分成不同的块,仍然保持完整信息。
4. 计算文本嵌入(Embedding)
model_name = "BAAI/bge-small-en"
model_kwargs = {"device": "cpu"}
encode_kwargs = {"normalize_embeddings": True}
hf_embeddings = HuggingFaceBgeEmbeddings(
model_name=model_name, model_kwargs=model_kwargs, encode_kwargs=encode_kwargs
)
解释
BAAI/bge-small-en:使用 BGE(BAAI General Embeddings)小型英文模型,把文本转换为向量(embedding)。model_kwargs={"device": "cpu"}:在 CPU 上运行(如果你有 GPU,可以改成 cuda)。normalize_embeddings=True:确保嵌入向量的值在同一范围内,有助于提高检索效果。
举例
你可以把这部分理解成给每个文本块生成一个唯一的“指纹”,方便后续的检索。例如,“机器学习” 这个短语可能会被转换成 [0.1, 0.3, -0.2, ...] 这样的一串数字。
5. 创建 FAISS 向量存储
vectorstore = FAISS.from_documents(documents=splits, embedding=hf_embeddings)
retriever = vectorstore.as_retriever()
解释
FAISS.from_documents(...):把文本块的嵌入向量存入 FAISS 数据库,方便后续检索。vectorstore.as_retriever():把 FAISS 变成一个检索器(Retriever),用于搜索与问题相关的文本块。
举例
你可以把 FAISS 想象成一个超级搜索引擎,但它不是按照关键词匹配,而是按照向量相似度匹配。 注:retriever 本身并不是检索到的文本块,而是一个 检索器(Retriever),它的作用是 根据输入问题,从 FAISS 数据库中找出最相关的文本块。
你可以把 retriever 理解为一个搜索引擎,它的作用是找到相关的内容,但并不包含具体的内容,只有调用它时才会返回结果。
第二部分:问答系统(RAG Pipeline)
6. 加载 Prompt
prompt = hub.pull("rlm/rag-prompt")
意思是: 从 LangChain Hub 获取一个预设的 Prompt 模板,用于指引 LLM 生成回答。
这里的rlm/rag-prompt是一个提示模板,可通过点击查看具体内容
7. 选择 LLM(大模型)
llm = ChatGroq(model="llama3-8b-8192", temperature=0)
这里使用 Groq 平台的 LLaMA 3-8B 模型。temperature=0 让输出尽可能确定(不会生成随机内容)。
8. 定义 RAG 流程
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
执行顺序:
retriever | format_docs:先从 FAISS 取出相关的文本块,并格式化为字符串。RunnablePassthrough():问题直接传递,不做修改。prompt:填充 prompt 模板,形成完整的提示词。llm:使用 LLaMA 3-8B 生成答案。StrOutputParser():解析 LLM 输出的文本。
9. 询问问题
print(rag_chain.invoke("What is Task Decomposition?"))
最终,它会:
检索与 Task Decomposition 相关的文本。用 LLM 生成答案。打印最终回答。
总结
这段代码实现了: ✅ 从网页获取内容 ✅ 切分并嵌入文本 ✅ 用 FAISS 存储向量 ✅ 通过 LLM 回答问题 🚀
这就是一个完整的 RAG 流程! 🎯
完整代码如下:
import os
os.environ['LANGCHAIN_TRACING_V2'] = 'true'
os.environ['LANGCHAIN_ENDPOINT'] = 'https://api.smith.langchain.com'
os.environ['LANGCHAIN_PROJECT'] = 'advanced-rag'
os.environ['LANGCHAIN_API_KEY'] = os.getenv("LANGCHAIN_API_KEY")
os.environ['GROQ_API_KEY'] = os.getenv("GROQQ_API_KEY")
import bs4
from langchain import hub
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.vectorstores import FAISS
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_groq import ChatGroq
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
#### INDEXING ####
# Load Documents
loader = WebBaseLoader(
web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(
class_=("post-content", "post-title", "post-header")
)
),
)
docs = loader.load()
##1 - 0 - 1000 , 800 - 1800
# Split - Chunking
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
# Embed
model_name = "BAAI/bge-small-en" # BAAI/bge-small-zh-v1.5 BAAI/bge-small-en
model_kwargs = {"device": "cpu"}
encode_kwargs = {"normalize_embeddings": True}
hf_embeddings = HuggingFaceBgeEmbeddings(
model_name=model_name, model_kwargs=model_kwargs, encode_kwargs=encode_kwargs
)
vectorstore = FAISS.from_documents(documents=splits,
embedding=hf_embeddings)
retriever = vectorstore.as_retriever() # Dense Retrieval - Embeddings/Context based
#### RETRIEVAL and GENERATION ####
# Prompt
prompt = hub.pull("rlm/rag-prompt")
print(prompt)
# LLM
llm = ChatGroq(model="llama3-8b-8192", temperature=0)
# Post-processing
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
# Chain
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# Question
print(rag_chain.invoke("What is Task Decomposition?"))
最新发布