摘要:传统RAG最大痛点是知识库静态滞后,模型只能基于历史入库内容回答,面对最新信息极易幻觉、答非所问。本文通过Bright Data双MCP服务器方案,把实时网页抓取直接嵌入LLM推理循环,从原理、架构到完整实战,解决RAG知识过时翻车问题。
一、RAG绕不开的噩梦:知识库再大,也抵不过数据过时
很多开发者辛辛苦苦搭建完一整套 RAG 知识库系统,在本地反复测试,问答效果都十分理想。可一旦正式上线投入业务使用,就会接连翻车,用户投诉源源不断。
我们来看一个非常典型的真实场景:我向 ChatGPT 提问:“当前最新发布的苹果手机是什么型号?” 此时苹果已经正式推出 iPhone 18 系列,但是 ChatGPT 知识库存在时间截断,它根本获取不到最新信息。于是 AI 一本正经地给出完整回答,有理有据地介绍 iPhone 17 的配置、版本与售价,参数写得面面俱到,看起来完全无懈可击,可答案从头到尾都是凭空编造出来的幻觉内容。 如果换成传统离线 RAG 系统,结局同样好不到哪里去:只要向量库没有重新爬取并更新资料,系统只能检索到半年前入库的旧机型数据,只能输出早已过时的答案。
传统 RAG 的标准执行链路非常固定:提前批量抓取文档、网页资料→对长文本做切片与向量化处理→把向量数据持久化存入向量数据库。当用户发起提问,系统只在存量向量库里检索匹配片段,再把检索到的静态内容交给大模型生成最终回复。
但这套架构存在一个无法规避的致命短板:整个知识库本质上只是一份静态数据快照。电子产品型号、新闻资讯、产品定价、行业政策都在持续迭代更新,向量数据库却不会自动同步互联网最新内容。 一旦用户提出带有强时效性的问题,大模型只会陷入两难:要么调取旧数据,输出已经失效的过期信息;要么检索不到对应内容,直接脑补编造信息,产生严重的 AI 幻觉,这也是行业里大家反复吐槽的 AI “张口就错”。
想要彻底解决知识滞后带来的问答翻车问题,我们可以引入 MCP 协议 + 亮数据(Bright Data)实时网页抓取 的组合方案。大模型在推理思考的全过程中,可以借助亮数据的网页采集能力按需访问互联网,稳定抓取目标网页的最新内容,不再完全被预先导入的静态知识库束缚,真正做到实时获取一手外部信息。
二、核心方案:Bright Data双MCP服务器架构
Bright Data提供两套独立MCP服务,分工明确,配合使用可以让Agent同时掌握官方文档知识 + 互联网实时动态数据。
- Docs MCP:专门负责文档知识库,Agent推理过程可以实时查阅官方文档,不用提前把全部文档灌入向量库;
- Web MCP:专门负责外部互联网世界,完成搜索引擎检索、网页Markdown提取、批量抓取、浏览器模拟操作;
调用速率限制说明
托管版Docs MCP存在访问限流:
- 单用户每小时最多 200次请求
- 单域名每小时最多 1000次请求
提示:如果业务并发高,超出限流,可以切换本地加载完整文档llms‑full.txt方案,绕过在线服务速率约束。
三、改造后的全新RAG工作流:推理内实时抓取
传统 RAG 链路:提问→检索静态向量库→生成答案
而基于 MCP 增强的 RAG 完整链路为: search_engine 搜索引擎检索 → 提取候选 URL → scrape_as_markdown 抓取网页正文 → 向量化存储 → 检索增强生成回答
核心关键点:网页搜索与网页抓取全部运行在大模型的
reasoning‑loop推理循环内部。 它不再像传统方案那样,提前离线把海量网页批量抓取完成、预先入库;而是在用户提出问题之后,由 LLM 自主判断 “我缺少时效性最新信息”,主动触发搜索、调用抓取能力获取网页原文,拿到一手实时内容之后再整理生成回答。
在实际工程落地时,网页抓取很容易遇到反爬拦截、IP 封禁、地域访问限制等各类障碍,直接写简易爬虫稳定性很差。我们可以将亮数据(Bright Data) 作为 scrape_as_markdown 底层的网页采集底座,依托它成熟的代理网络与网页解析能力,稳定、干净地输出 markdown 格式网页正文,规避各类反爬难题,为 MCP 工具提供可靠的原始网页素材。
像 LangChain、Haystack 这些主流 RAG 框架,原生都支持 MCP 协议接入,拥有现成集成包。开发者无需从零开发复杂爬虫逻辑,只需要对接亮数据的采集能力作为底层抓取能力,即可快速搭建这套实时增强 RAG 架构。
四、实战教程:搭建一套可以查今日新闻的问答机器人
1. 环境搭建
这一步只做两件事:装依赖、配密钥。装好后才能跑搜索、抓页、切块、向量检索和最后的 LLM 回答。
安装依赖:
pip install requests langchain-community langchain-openai langchain-text-splitters chromadb sentence-transformers
配环境变量:
set API_TOKEN=你的亮数据API密钥
set WEB_UNLOCKER_ZONE=mcp_unlocker
set OPENAI_API_KEY=你的大模型密钥
| 变量 | 干什么 |
|---|---|
API_TOKEN | 亮数据鉴权;和 Cursor 里亮数据 MCP 是同一把 |
WEB_UNLOCKER_ZONE | 指定 Unlocker Zone;默认 mcp_unlocker(MCP 首次启动会建) |
OPENAI_API_KEY | 最后生成综合回答;没有也能搜、抓、检索,只是不生成 |
验证环境是否就绪:
python Agent.py -q "今天有哪些重要科技新闻?" -s "today technology news" -n 3
-q 是给人/给 LLM 的问题,-s 是给搜索引擎的英文检索词,-n 是抓前几条。
2. 统一请求函数
这一步封装亮数据唯一入口。后面的 search_engine、scrape_as_markdown 都走同一个函数,避免两套 HTTP 逻辑。
底层接口:
POST https://api.brightdata.com/request
Authorization: Bearer <API_TOKEN>
import os
import requests
API_TOKEN = os.getenv("API_TOKEN", "")
WEB_UNLOCKER_ZONE = os.getenv("WEB_UNLOCKER_ZONE", "mcp_unlocker")
BRIGHTDATA_REQUEST_URL = "https://api.brightdata.com/request"
def _brightdata_request(url: str, *, data_format: str | None = None, timeout: int = 90) -> str:
"""向亮数据发一次请求:传入目标 URL,可选要 markdown,返回原始文本。"""
if not API_TOKEN:
raise RuntimeError("未设置 API_TOKEN")
payload = {"zone": WEB_UNLOCKER_ZONE, "url": url, "format": "raw"}
if data_format:
payload["data_format"] = data_format # 抓正文时传 "markdown"
resp = requests.post(
BRIGHTDATA_REQUEST_URL,
headers={
"Authorization": f"Bearer {API_TOKEN}",
"Content-Type": "application/json",
},
json=payload,
timeout=timeout,
)
if resp.status_code != 200:
raise RuntimeError(f"HTTP {resp.status_code}: {resp.text[:400]}")
return resp.text
要点:url 是要代理访问的目标;data_format="markdown" 只在抓正文时用;反爬由 Zone 处理。
3. search_engine:搜出候选 URL
这一步用搜索引擎拿「今天相关」的候选链接。不直接猜新闻站,而是让 Google/Bing 返回 SERP,再取 organic 结果里的 URL。
做法:拼搜索 URL,加上 brd_json=1,经 _brightdata_request 拿可解析的 JSON,抽出标题和链接。
import json
from urllib.parse import quote_plus
def search_engine(query: str, engine: str = "google", top_n: int = 5) -> list[dict]:
"""按关键词搜索,返回最多 top_n 条 {url, title, description}。"""
encoded = quote_plus(query)
if engine == "bing":
target = f"https://www.bing.com/search?q={encoded}&brd_json=1"
elif engine == "yandex":
target = f"https://yandex.com/search/?text={encoded}&brd_json=1"
else:
target = f"https://www.google.com/search?q={encoded}&brd_json=1"
raw = _brightdata_request(target, timeout=90)
data = json.loads(raw)
organic = data.get("organic") or []
out = []
for item in organic:
url = item.get("link") or item.get("url") or ""
if not url.startswith("http"):
continue
out.append({
"url": url,
"title": item.get("title", ""),
"description": item.get("description") or item.get("snippet") or "",
})
if len(out) >= top_n:
break
for i, x in enumerate(out, 1):
print(f"{i}. {x['title']}\n {x['url']}")
return out
单测:
hits = search_engine("today technology news", top_n=3)
4. scrape_as_markdown:抓正文
这一步把上一步的 URL 变成可读正文。多传 data_format: markdown,亮数据返回清理后的 Markdown,后面才能切块入库。
def scrape_as_markdown(url: str) -> str:
"""抓取单个 URL 的正文,返回 Markdown 字符串。"""
content = _brightdata_request(url, data_format="markdown", timeout=120).strip()
if not content:
raise RuntimeError(f"空内容: {url}")
return content
单测:
md = scrape_as_markdown(hits[0]["url"])
print(md[:500]) # 应看到标题、段落,而不是整页 HTML
5. 切块入库
这一步把多篇 Markdown 切成小块,编成向量,放进内存 Chroma。新闻当场用,进程结束就丢,不用持久化。
为什么切块:整篇太长,检索会糊;小块更利于「问题和片段」匹配。source_url 写进 metadata,回答时才能带来源链接。
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
def store_markdown_to_vector_db(markdown_texts, source_urls, titles=None):
"""切块 → 本地嵌入 → 写入内存 Chroma,返回向量库对象。"""
splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=100)
chunks, metadatas = [], []
titles = titles or [""] * len(source_urls)
for text, url, title in zip(markdown_texts, source_urls, titles):
if not text.strip():
continue
parts = splitter.split_text(text)
chunks.extend(parts)
metadatas.extend([{"source_url": url, "title": title}] * len(parts))
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
return Chroma.from_texts(chunks, embeddings, metadatas=metadatas)
6. RAG 生成
这一步做检索增强生成:先按问题从向量库取出最相关 chunk,拼成带 URL 的上下文;有 OPENAI_API_KEY 就让模型只依据资料回答并标注来源,没有就直接返回检索到的原文。
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
def rag_query(vectordb, question: str, k: int = 4) -> str:
"""相似度检索 k 条,再(可选)用 LLM 综合成带来源的回答。"""
docs = vectordb.similarity_search(question, k=k)
context = "\n\n".join(
f"===== 来源:{d.metadata.get('title','')} | {d.metadata.get('source_url','')} =====\n{d.page_content}"
for d in docs
)
if not OPENAI_API_KEY:
return context # 没密钥:只返回检索片段
prompt = PromptTemplate(
input_variables=["question", "context"],
template=(
"你是今日新闻问答助手。只能依据下方参考资料回答,"
"每条要点后标注来源 URL。资料不足就明说,不要编造。\n\n"
"参考资料:\n{context}\n\n用户问题:{question}\n"
),
)
llm = ChatOpenAI(model=OPENAI_MODEL, temperature=0, api_key=OPENAI_API_KEY)
return (prompt | llm).invoke({"question": question, "context": context}).content
7. 串成主流程
这一步把前面串成一条流水线:搜索 → 逐条抓取 → 切块入库 → RAG 回答。单页失败就跳过,全部失败再报错。
def run_news_bot(question: str, top_n: int = 3, engine: str = "google", search_query=None) -> str:
"""端到端:搜 → 抓 → 入库 → 答。search_query 可与 question 分离(中文问、英文搜)。"""
q = search_query or question
hits = search_engine(q, engine=engine, top_n=top_n)
if not hits:
return "搜索无结果"
md_docs, urls, titles = [], [], []
for item in hits:
try:
md = scrape_as_markdown(item["url"])
md_docs.append(md[:20000]) # 防止单页过长
urls.append(item["url"])
titles.append(item.get("title", ""))
print(f"[OK] {item['url']} ({len(md)} chars)")
except Exception as e:
print(f"[FAIL] {item['url']}: {e}")
if not md_docs:
return "抓取全部失败"
vectordb = store_markdown_to_vector_db(md_docs, urls, titles)
return rag_query(vectordb, question)
命令行入口(把参数接到主流程):
if __name__ == "__main__":
import argparse
p = argparse.ArgumentParser()
p.add_argument("-q", "--question", default="今天有哪些重要科技新闻?")
p.add_argument("-s", "--search", default=None)
p.add_argument("-n", "--top-n", type=int, default=3)
p.add_argument("--engine", default="google", choices=["google", "bing", "yandex"])
args = p.parse_args()
print(run_news_bot(args.question, args.top_n, args.engine, args.search))
8. 代码运行测试
这一步用真实命令把整条链路跑通,确认终端里能看到:搜到几条 → 哪些页抓成功 → 入库 → 带 URL 的最终回答。
python Agent.py -q "用中文概括今天最重要的三条科技新闻" -s "today technology news" -n 3
运行效果:
我们通过抓取到的数据来看,都是今天最新的数据。接着也能看出亮数据的 MCP 很好用:在 Cursor 里装好之后,search_engine 和 scrape_as_markdown 直接就能调,不用自己搭代理、处理反爬,同一套 Zone 和 API_TOKEN 就能把「搜候选 URL → 抓正文 → 交给 RAG」这条链路跑通。对要做今日新闻问答这类强时效场景来说,省事的不只是接口,而是搜和抓都能稳定落到当天内容上。
五、总结
静态RAG天生无法解决知识时效性问题,单纯增加向量库、增加chunk切片优化,只能缓解,不能根治。 MCP协议带来新范式:把搜索、网页抓取、文档查阅变成大模型推理循环内原生工具。亮数据 (Bright Data)双MCP架构(Docs MCP+Web MCP),兼顾权威文档和互联网实时世界,LangChain、Haystack等主流框架开箱可用。