LangChain 实战:基于网页数据的 RAG 问答及来源溯源
1. 背景与目标
在构建检索增强生成(RAG)系统时,数据源的选择至关重要。传统的 RAG 实践常使用本地 PDF 或文档作为知识库,但在实际业务场景中,在线网页数据往往更具时效性和丰富性。
本教程将综合展示如何利用 LangChain 实现网络数据 + RAG 问答的完整流程。除了基本的问答功能外,重点在于如何在返回结果中添加答案的来源引用。这一功能在 RAG 应用中非常关键:
- 可追溯性:让用户了解答案生成的依据,防止模型幻觉。
- 信任度:展示参考原文,增加回答的专业性和可信度。
- 调试辅助:当回答错误时,可以通过来源快速定位是检索问题还是生成问题。
2. 环境准备
确保已安装必要的依赖库。以下代码基于 langchain-community 和 langchain-openai 等最新组件结构编写。
pip install langchain langchain-community langchain-openai chromadb beautifulsoup4 requests
主要导入模块如下:
import bs4
from langchain import hub
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.vectorstores import Chroma
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
3. 加载网页数据
3.1 使用 WebBaseLoader
LangChain 提供了 WebBaseLoader 类专门用于加载网页内容。它底层利用 urllib 获取 HTML,并通过 BeautifulSoup 进行解析。
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()
参数详解:
web_paths: 需要加载的 URL 列表。
bs_kwargs: 传递给 BeautifulSoup 的参数。这里使用了 SoupStrainer,仅提取包含特定 CSS 类的标签内容,避免加载导航栏、页脚等无关信息,提高数据质量。
3.2 处理编码与异常
在实际生产中,网页可能存在编码问题或动态加载内容。建议在初始化时设置 encoding 参数,并开启 continue_on_failure 以跳过无法访问的链接。
loader = WebBaseLoader(
web_paths=(url,),
encoding="utf-8",
continue_on_failure=True,
)
4. 数据分块
向量数据库对文本长度有限制,且过长的上下文会影响检索精度。我们使用 RecursiveCharacterTextSplitter 进行智能分块。
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
splits = text_splitter.split_documents(docs)
策略说明:
chunk_size: 每个文本块的最大字符数,设为 1000 适合大多数语义单元。
chunk_overlap: 相邻块之间的重叠字符数,设为 200 可以保留上下文连贯性,防止关键信息被切断。
5. 向量化与存储
选用 Chroma 作为轻量级向量数据库,配合 OpenAIEmbeddings 进行嵌入计算。
vectorstore = Chroma.from_documents(
documents=splits,
embedding=OpenAIEmbeddings()
)
若需持久化保存,可在初始化 Chroma 时指定 persist_directory 路径。
6. 基础 RAG Chain 组装
首先构建一个基础的问答链,验证检索与生成流程是否通畅。
prompt = hub.pull("rlm/rag-prompt")
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
数据流解析:
retriever 执行检索,返回文档列表。
format_docs 将文档列表拼接为字符串。
RunnablePassthrough() 透传用户问题。
- 两者组合后填入 Prompt 的
context 和 question 字段。
- LLM 生成最终文本,由
StrOutputParser 输出。
7. 加入 Sources(答案来源)
为了展示答案的依据,我们需要修改 Chain 结构,使其同时返回 answer 和 context。
7.1 核心代码实现
rag_chain_from_docs = (
RunnablePassthrough.assign(context=(lambda x: format_docs(x["context"])))
| prompt
| llm
| StrOutputParser()
)
rag_chain_with_source = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
).assign(answer=rag_chain_from_docs)
7.2 逻辑原理解析
RunnableParallel: 允许并行执行多个操作。这里同时运行 retriever(获取原始文档)和 RunnablePassthrough(传递问题)。
.assign(): 将并行结果合并,并额外计算 answer 字段。
rag_chain_from_docs: 接收包含 context 和 question 的字典,格式化 context 后送入 LLM。
7.3 运行示例
result = rag_chain_with_source.invoke("What is Task Decomposition?")
print(result)
输出结果将包含两个关键字段:
context: 检索到的原始文档片段。
answer: 基于文档生成的回答。
8. 常见问题与优化建议
8.1 检索相关性不足
如果检索到的内容与问题不相关,可调整 retriever 的参数,如 search_k (top_k) 或引入重排序模型(Rerank)。
8.2 网页加载失败
部分网站可能禁止爬虫。遇到此情况可尝试:
- 更换 User-Agent。
- 使用专门的爬虫工具预处理数据。
- 检查
verify_ssl 配置。
8.3 响应速度优化
对于大量网页数据,建议先进行批量向量化入库,查询时使用异步模式或缓存机制。
9. 总结
本文详细介绍了使用 LangChain 构建基于网页数据的 RAG 问答系统的全过程。通过 WebBaseLoader 获取实时网络数据,利用 Chroma 进行向量存储,并重点实现了答案来源溯源功能。该功能通过 RunnableParallel 巧妙地将检索结果与生成结果并行处理,既保证了回答的准确性,又增强了系统的可解释性。掌握这些技术点,有助于构建更专业、更可信的企业级 AI 应用。