RAG应用总是“张口就错”?Bright Data MCP+实时抓取组合方案实测

54 阅读11分钟

摘要:传统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等主流框架开箱可用。