我会一步一步详细解释每一行代码,并用通俗易懂的方式举例,让你更好地理解这段 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?"))