【LangChain】实现网页内容问答:从 gov.cn 抓取到 LLM 回答的完整链路
用 LangChain 实现网页内容问答从 gov.cn 抓取到 LLM 回答的完整链路通过一个实际案例完整拆解 LangChainWebBaseLoader的bs_kwargs透传机制以及如何利用预制 LCEL 链和 BeautifulSoup 完成「网页抓取 → 文本分割 → LLM 问答」的全流程。场景与需求在实际项目中我们经常遇到这样的需求需要从官方发布平台如政府网、新闻网站获取最新政策/会议内容内容篇幅较长直接塞给 LLM 会超出上下文限制网页包含大量导航栏、页脚、广告等无关噪音需要精准提取正文本文以中国 gov.cn 的一篇会议报道为例演示如何用 LangChain 的WebBaseLoaderRecursiveCharacterTextSplitter 预制 LCEL 链解决上述问题。完整代码fromlangchain_community.document_loadersimportWebBaseLoaderfromlangchain.text_splitterimportRecursiveCharacterTextSplitterfrombs4importSoupStrainerimportbs4# # 1. 加载文档精准提取网页正文# loaderWebBaseLoader(web_pathhttps://www.gov.cn/yaowen/liebiao/202512/content_7050416.htm,# 关键通过 bs_kwargs 将参数透传给 BeautifulSoup# SoupStrainer 只保留 idUCAP-CONTENT 的元素gov.cn 的正文容器bs_kwargs{parse_only:bs4.SoupStrainer(idUCAP-CONTENT)})docsloader.load()# # 2. 分割文档长文本分块处理# text_splitterRecursiveCharacterTextSplitter(chunk_size1000,# 每块最多 1000 字符chunk_overlap50# 相邻块重叠 50 字符保证语义连贯)documentstext_splitter.split_documents(docs)print(f共分割为{len(documents)}个文档块)# # 3. 执行问答链基于上下文回答问题# # chain 需预先定义如 LLM PromptTemplate Retrieverreschain.invoke({input:会议具体说了哪些内容?,context:documents})print(res)核心机制拆解1. WebBaseLoader 与 BeautifulSoup 的协作bs_kwargs 的透传机制这是最容易困惑的地方。WebBaseLoader没有内置 BeautifulSoup 对象而是在load()被调用时现场创建BeautifulSoup 实例并将bs_kwargs原封不动地传递过去。完整流程初始化阶段 web_pathURL - 暂存目标网址 bs_kwargs{...} - 暂存 BeautifulSoup 解析参数 执行阶段loader.load() ① 用 requests/urllib 下载 URL 的完整 HTML ② 现场创建 BeautifulSoup 实例将 HTML 作为第一个参数 bs_kwargs 用 ** 展开后传入 ③ BeautifulSoup 用 SoupStrainer 过滤只保留指定元素 ④ 返回纯净的 Document 对象源码层面的逻辑简化版classWebBaseLoader(BaseLoader):def__init__(self,web_path,bs_kwargsNone,**kwargs):self.web_pathweb_path self.bs_kwargsbs_kwargsor{}# 暂存解析参数def_scrape(self,url:str)-BeautifulSoup:htmlrequests.get(url).text# 下载 HTML# 关键在这里html 作为位置参数bs_kwargs 作为关键字参数用 ** 展开returnBeautifulSoup(html,# 第一个参数HTML 内容html.parser,# 默认解析器**self.bs_kwargs# 展开用户传入的参数)为什么叫透传步骤发生了什么你传bs_kwargs把参数打包进字典LangChain 收到原封不动不做任何处理LangChain 传递用**把字典拆包直接塞进 BeautifulSoup透就是透明、穿透的意思——LangChain 自己不解析这些参数的含义只是负责从你这端穿透到 BeautifulSoup 那端。如果 BeautifulSoup 今天新增了一个from_encoding参数你直接写进bs_kwargs里就能用LangChain 不需要改任何代码。Python**解包语法defdemo(a,b,c):print(a,b,c)kwargs{b:2,c:3}demo(1,**kwargs)# 等价于 demo(1, b2, c3)对应到 WebBaseLoader# 你写的bs_kwargs{parse_only:bs4.SoupStrainer(idUCAP-CONTENT)}# LangChain 内部展开后等价于BeautifulSoup(html,# 位置参数 1html.parser,# 位置参数 2parse_onlySoupStrainer(idUCAP-CONTENT)# **bs_kwargs 展开)再举一个多参数的实际例子# 假设你想同时用两个 BeautifulSoup 特性bs_kwargs{parse_only:bs4.SoupStrainer(idUCAP-CONTENT),# 只解析指定元素from_encoding:gbk# 指定编码}# LangChain 内部展开后等价于BeautifulSoup(html,html.parser,parse_onlySoupStrainer(idUCAP-CONTENT),from_encodinggbk)2. SoupStrainer 不是静态函数是过滤器类bs4.SoupStrainer(idUCAP-CONTENT)这是在创建一个 SoupStrainer 实例不是调用静态函数。SoupStrainer 的本质它是 BeautifulSoup 的一个过滤器类用来告诉 BeautifulSoup“我只关心符合这些条件的 HTML 元素其他的都扔掉”。frombs4importSoupStrainer# 创建实例只匹配 idUCAP-CONTENT 的元素strainerSoupStrainer(idUCAP-CONTENT)# 等价写法strainerSoupStrainer(attrs{id:UCAP-CONTENT})完整流程拆解importbs4# 步骤 1创建过滤器实例my_filterbs4.SoupStrainer(idUCAP-CONTENT)# 此时 my_filter 是一个对象内部存储了过滤规则{id: UCAP-CONTENT}# 步骤 2把这个过滤器放进字典bs_kwargs{parse_only:my_filter}# 步骤 3WebBaseLoader 暂存这个字典loaderWebBaseLoader(web_path...,bs_kwargsbs_kwargs)# 步骤 4load() 时LangChain 展开 bs_kwargsBeautifulSoup(html,html.parser,parse_onlymy_filter)# 等价于BeautifulSoup(html, html.parser, **{parse_only: my_filter})3. 为什么用idUCAP-CONTENT这是中国政府网的内容管理系统标识。查看任意 gov.cn 文章源码结构如下dividUCAP-CONTENT!-- 正文容器 --p会议强调了.../pp下一步工作要求.../p/divfooter.../footer!-- 页脚噪音 --divclassrelated.../div!-- 相关推荐噪音 --使用SoupStrainer(idUCAP-CONTENT)可以精准提取正文过滤导航栏/页脚/广告减少 Token 消耗提升 LLM 回答质量避免无关内容干扰语义理解提示不同网站正文容器的id/class不同使用前建议先查看目标网页源码。常见正文容器标识包括idcontent、classarticle-content、idmain等。4. 文本分割的策略RecursiveCharacterTextSplitter(chunk_size1000,chunk_overlap50)参数作用chunk_size1000控制单块大小适配 LLM 上下文窗口chunk_overlap50块间重叠防止关键信息被切分在边界处丢失为什么需要重叠假设一段关键句横跨两个块的分界点[块1结尾] ...会议提出了三 [块2开头] 项重要要求...没有重叠时LLM 单独看到任何一块都无法理解完整语义。50 字符的重叠提供了上下文缓冲带。预制 LCEL 链的详细说明代码中的chain.invoke()里的chain属于 LangChain 的预制 LCEL 链Built-in Chain。什么是预制 LCEL 链LangChain 已经事先做好了很多 LCEL 链可以直接复用。完整列表可参考官方 APIhttps://reference.langchain.com/python/langchain_classic/chains/注意在 “Deprecated classes” 和 “Deprecated functions” 中的属于被声明废弃不建议使用。重点链create_stuff_documents_chainfromlangchain.chains.combine_documentsimportcreate_stuff_documents_chain功能将多个文档内容合并成一个长文本然后一次性交给 LLM 处理。这正是代码中chain.invoke({context: documents})的底层行为——documents是分割后的多个文档块create_stuff_documents_chain会把它们自动拼接成一个长字符串塞进提示模板。常见预制 LCEL 链对比链类型用途典型场景create_stuff_documents_chain把多个文档塞进提示模板合并后一次性给 LLM文档块较少、总长度在上下文窗口内create_retrieval_chain检索器 文档链的组合长文档 RAG 标准方案ConversationalRetrievalChain带历史记忆的检索问答多轮对话客服LLMChain已废弃简单的提示模板 LLM属于 Deprecated不建议使用方案 Acreate_stuff_documents_chain适合文档块少fromlangchain_core.promptsimportChatPromptTemplatefromlangchain_openaiimportChatOpenAIfromlangchain.chains.combine_documentsimportcreate_stuff_documents_chain# 定义提示模板预留 {context} 和 {input} 两个变量promptChatPromptTemplate.from_messages([(system,你是一个专业的政策解读助手请基于以下上下文回答问题。),(human, 上下文: {context} 问题: {input} )])# create_stuff_documents_chain: 自动将多个文档合并成一个长文本chaincreate_stuff_documents_chain(llmChatOpenAI(modelgpt-4),promptprompt)# 调用时传入文档列表内部自动合并reschain.invoke({input:会议具体说了哪些内容?,context:documents# documents 是 Document 对象列表内部自动拼接})关键点create_stuff_documents_chain会自动处理documents的格式转换不需要你手动用\\n\\n.join()拼接。方案 Bcreate_retrieval_chain文档块多的推荐方案fromlangchain_community.vectorstoresimportFAISSfromlangchain_openaiimportOpenAIEmbeddings,ChatOpenAIfromlangchain.chains.combine_documentsimportcreate_stuff_documents_chainfromlangchain.chainsimportcreate_retrieval_chainfromlangchain_core.promptsimportChatPromptTemplate# 向量化存储 检索器vectorstoreFAISS.from_documents(documents,OpenAIEmbeddings())retrievervectorstore.as_retriever()# 文档链: 负责把检索到的文档塞进提示模板promptChatPromptTemplate.from_template( 基于以下上下文回答问题: {context} 问题: {input} )document_chaincreate_stuff_documents_chain(ChatOpenAI(),prompt)# 检索链: 检索器 文档链的组合rag_chaincreate_retrieval_chain(retriever,document_chain)# 调用时只需传问题检索和上下文组装自动完成resrag_chain.invoke({input:会议具体说了哪些内容?})预制链的核心价值手动拼接繁琐: 写提示模板 - 格式化字符串 - 调用 LLM API - 解析 JSON 响应 - 提取文本 预制 LCEL 链简洁: chain.invoke({input: 问题, context: documents}) - 直接拿到答案预制链帮你处理了变量替换把{input}{context}替换成实际值文档合并create_stuff_documents_chain自动将多个Document拼接成字符串API 调用自动处理重试、超时、流式输出输出解析从 LLM 的原始响应中提取可用文本废弃链的警示在官方 API 文档中标记为“Deprecated”的类和函数不建议使用。例如传统的LLMChain# 已废弃不建议使用fromlangchain.chainsimportLLMChain chainLLMChain(llmllm,promptprompt)应替换为 LCEL 管道写法或create_stuff_documents_chain等预制链。进阶从基础链到 RAG 检索链当文档块很多时直接用所有文档作为context会超出 LLM 上下文限制。这时需要用RAG检索增强生成模式先检索最相关的块再送入 LLM。fromlangchain_community.vectorstoresimportFAISSfromlangchain_openaiimportOpenAIEmbeddings,ChatOpenAIfromlangchain.chains.combine_documentsimportcreate_stuff_documents_chainfromlangchain.chainsimportcreate_retrieval_chain# 向量化存储vectorstoreFAISS.from_documents(documents,OpenAIEmbeddings())retrievervectorstore.as_retriever()# 构建 RAG 链document_chaincreate_stuff_documents_chain(ChatOpenAI(),prompt)rag_chaincreate_retrieval_chain(retriever,document_chain)# 调用时只需传问题检索和上下文组装自动完成resrag_chain.invoke({input:会议具体说了哪些内容?})踩坑与最佳实践问题原因解决方案抓取内容为空网站有反爬机制加requests_kwargs{headers: {...}}模拟浏览器正文提取不全id/class选择器错误先手动查看网页源码确认容器标识LLM 回答遗漏要点块大小过大或重叠不足调小chunk_size增大chunk_overlap回答包含无关信息未过滤网页噪音务必使用SoupStrainer精准定位正文总结本文通过一个 gov.cn 网页问答实例演示了 LangChain 文档处理的三板斧加载—WebBaseLoaderSoupStrainer精准抓取正文。核心在于理解bs_kwargs的透传机制LangChain 不内置 BeautifulSoup 对象而是在load()时现场创建并用**解包将参数原样传递给 BeautifulSoup。分割—RecursiveCharacterTextSplitter长文本分块保语义。问答— 预制 LCEL 链如create_stuff_documents_chain、create_retrieval_chain封装了提示模板 - 文档合并 - LLM - 解析的全流程让开发者只需关注业务逻辑无需手动拼接底层细节。这套模式不仅适用于政府网站也可迁移到新闻聚合、政策监控、竞品分析等任何需要从网页提取内容并问答的场景。