从指定 URL抓取网页内容获取文本并使用 LangChain 构建一个 RAG(检索增强生成)系统并用langsmith监测数据检索流程,我保证超简单教程

2025-05-31 04:42:30

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