如何构建生产级 RAG 系统并将 LLM Token成本降低 70%
Advanced Bot Mitigation Engineer
关键要点:
- 干净的Markdown是LLM真正想要的格式。 原始HTML主要由导航、脚本、广告位置和内联样式组成——这是一种浪费上下文窗口并降低检索质量的噪声。Scrapeless的
scrape_markdown返回页面的可读主体作为干净的Markdown,因此传递给您的嵌入模型的文本就是页面所涉及的文本。 - 管道分为四个步骤:发现 → 提取 → 分块 → 嵌入。 找到重要的URL,使用云浏览器将每个URL渲染为干净的Markdown,将Markdown分割成适合您模型的重叠块,然后嵌入并持久化到向量数据库中,以用于增强生成的检索。
- JavaScript重的页面和反机器人墙由平台处理。 许多高价值源通过客户端渲染来补充其内容,或位于机器人验证挑战之后。Scrapeless Scraping Browser在一个真实的反检测云浏览器中渲染页面,并提供住宅出口,因此您得到的Markdown是完全渲染的页面,而不是空壳。
- 两个表面,一个原语。 当AI代理驱动管道时,从Scrapeless MCP服务器调用
scrape_markdown,或者当脚本控制循环时,通过Python SDK创建云浏览器会话。两者均使用相同的反检测云浏览器。 - 无状态MCP工具在其有效负载前缀中加上
Response:\n\n。 当您通过MCP服务器读取scrape_markdown输出时,在分块之前去除该前缀——这是一行代码的修复,可以防止多余的头出现在您的语料库中。 - 反检测云浏览器,195多个国家的住宅代理。 Scrapeless Scraping Browser在每个会话中处理JavaScript渲染、住宅代理出口和指纹随机化(UA、时区、WebGL、canvas),以便构建语料库的脚本可以专注于文本质量,而不是规避技术。
- 免费开始。 新的Scrapeless帐户包括免费的Scraping Browser运行时——请在Scrapeless注册。
介绍:给您的模型输入文本,而不是页面界面
语言模型的好坏取决于它所读取的文本。无论您是在组装微调语料库、建立基于检索增强生成(RAG)的知识库还是基于实时市场数据为代理提供支持,输入阶段决定了后续一切的上限。垃圾进不仅仅是垃圾出——它是浪费的令牌、污染的嵌入,以及在呈现饼干横幅而非答案的情况下的检索。
问题在于,现代网络是为浏览器和人类构建的,而非为嵌入模型。一个典型的文章页面包含几千字的实际内容,被数万字的导航菜单、分享按钮、相关帖子网格、评论小部件、饼干通知、跟踪脚本和内联CSS包裹着。将这些原始HTML输入到嵌入器中,信号在标记中淹没。更糟的是,越来越多的页面在初始加载后通过JavaScript渲染其主要内容,因此简单的HTTP获取返回的是一个空容器。还有一些网站存在反机器人挑战,完全阻止自动收集。
这篇文章将介绍一个基于Scrapeless Scraping Browser的Python工作流,将混乱的公共网页转换为干净、分块、准备好嵌入的文本。管道有四个步骤——发现URL、提取干净的Markdown、为RAG分块、嵌入到向量数据库——而scrape_markdown在提取阶段负责重任,通过返回任何页面的可读主体作为干净的Markdown。有关同一原语的代理框架版本,请参阅LangChain集成文章。
您可以构建的内容
干净文本提取是多种LLM和RAG系统的基础:
- 基于您自己文档的RAG。 爬取文档网站或知识库以提取干净的Markdown,进行分块并嵌入,以便支持代理根据当前文档而非过时的训练版本回答。
- 微调和继续预训练语料库。 从公共文章和参考资料中组装大型去重文本数据集,收集时即已剔除多余内容。
- 为代理提供实时网页支持。 渲染代理在查询时所需的页面,并提供干净的Markdown,以便答案引用当前的页面。
- 竞争和市场情报。 将公共产品页面、博客文章和发布说明转化为可搜索的向量索引,以供分析师或LLM查询。
- 新闻和研究监测。 定期提取出版商和期刊页面,标准化为Markdown,并嵌入以在不断变化的来源中进行语义搜索。
- 内部语义搜索。 在您的团队依赖的公共参考资料上建立私有检索层,按计划保持更新。
为什么选择Scrapeless Scraping Browser
Scrapeless Scraping Browser 是一个可定制的反检测云浏览器,专为网络爬虫和 AI 代理设计。特别是针对 LLM 和 RAG 文本管道,它带来了:
- 干净的 Markdown 提取。
scrape_markdown渲染一个 URL 并以 Markdown 形式返回可读内容——保留标题、段落、列表、表格和链接;剥离导航、脚本、广告位和内联样式。这是嵌入模型最佳读取的格式。 - 云端 JavaScript 渲染。 完整的 Chromium 在提取之前为页面加水分,因此单页应用、延迟加载部分和在初始请求后注入的内容都被捕获,而不是被漏掉。
- 195+ 个国家的住宅代理。 地理绑定页面返回本地读者看到的内容,并且在每个会话中自动轮换——这就是实际文章与地区封锁页面之间的区别。
- 每个会话的反检测指纹识别——用户代理、时区、语言、屏幕分辨率、WebGL 和画布每个会话都会随机化,因此高价值来源能够保持可访问,而无需每次请求调整指纹。
- 一个原始工具,两种表现形式。 同一个云浏览器既可以作为代理驱动管道的 MCP 工具访问,也可以作为脚本驱动管道的 Python SDK 会话访问,因此相同的提取步骤可组合成任意架构。
获取 免费计划 的 API 密钥,请访问 Scrapeless。
先决条件
- Python 3.10 或更新版本。
- 一个 Scrapeless 账户和 API 密钥——请在 app.scrapeless.com 注册,并从 设置 → API 密钥管理 复制密钥。
- 一个嵌入模型的 API 密钥(如果计划进行嵌入,下面的示例使用 OpenAI;任何嵌入提供商通过交换一行代码即可工作)。
- 对
pip和venv的基本熟悉。
完整的 SDK 和工具参考见 docs.scrapeless.com。
安装
有两种方法可以访问同一个云浏览器。请选择符合谁驱动管道的选项——代理还是脚本。
选项 A — Python SDK(脚本驱动)
对于一个负责发现 → 提取 → 分块 → 嵌入循环的脚本,安装 Scrapeless Python SDK 及你打算使用的嵌入和向量存储库:
bash
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install scrapeless openai chromadb tiktoken
将你的 API 密钥导出到当前 shell。SDK 自动从环境读取 SCRAPELESS_API_KEY:
bash
export SCRAPELESS_API_KEY="your_api_token_here"
export OPENAI_API_KEY="your_openai_token_here"
选项 B — MCP 服务器(代理驱动)
对于一个调用工具的 AI 代理,运行 Scrapeless MCP 服务器。它向任何支持 MCP 的客户端暴露 scrape_markdown、scrape_html、google_search 及一系列浏览器工具:
bash
npx -y scrapeless-mcp-server
将你的 MCP 客户端指向命令,并在服务器配置中将 API 密钥作为 SCRAPELESS_KEY 环境变量传递。代理可以直接调用 scrape_markdown。
管道一目了然
发现 URLs 提取干净文本 分块以用于 RAG 嵌入 + 存储
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ ┌──────────────┐
│ google_search│ │ scrape_markdown │ │ split into │ │ embed each │
│ 或网站地图 │ ────► │ (云浏览器 │ ────► │ ~500–1000-tok │ ────► │ chunk, upsert │
│ 或种子列表 │ │ 渲染 + 清理) │ │ overlapping │ │ 到向量数据库 │
└──────────────┘ └──────────────────┘ │ chunks │ └──────────────┘
└──────────────┘
四个阶段,清晰分离。发现决定 哪些 页面进入语料库;提取决定 文本有多干净;分块决定 检索的便利性;嵌入使其 可搜索。Scrapeless 管理前两个阶段——即网络反击的阶段——而标准库负责后两个阶段。
第一步 — 发现 URLs
语料库从一系列 URLs 开始。三种常见来源几乎涵盖所有情况:
- 已经拥有的种子列表或网站地图——最简单的情况;直接跳到第二步。
- 网站爬取——从某个部分根部开始,跟踪域内链接到有限深度。
- 搜索发现——当相关页面事先未知时,进行搜索。
Scrapeless MCP 服务器提供了一个 google_search 工具,返回结构化行的自然结果,这是发现主题源 URL 的干净方式。每一行携带 position、title、link、snippet 和 source:
python
# discover.py — 从搜索查询收集候选 URLs
# (MCP 工具参数采用 camelCase;这展示了返回的形状)
results = [
json
[
{"position": 1, "title": "检索增强生成的解释", "link": "https://example.com/guides/rag-explained", "source": "example.com"},
{"position": 2, "title": "RAG 的分块策略", "link": "https://example.com/blog/chunking-strategies", "source": "example.com"}
]
urls = [row["link"] for row in results]
保持发现阶段的真实性:去重网址,删除无关域,限制数量,然后再考虑每页渲染预算。一个聚焦的 200 个网址的语料库比一个嘈杂的 2000 个网址的要更好检索。
第 2 步 — 使用 scrape_markdown 提取干净的 Markdown
这是决定语料库质量的阶段。scrape_markdown 在反检测云浏览器中渲染网址——JavaScript 运行,页面变得可用,住宅出口保持内容可达——并将可读正文返回为干净的 Markdown。标题保持为标题,列表保持为列表,表格保持为表格,所有非内容的部分会被剔除。
代理驱动 (MCP)
当代理调用工具时,它会将 Markdown 作为工具结果返回。有一个细节对于语料库卫生很重要:无状态 MCP 工具在文本负载前加上 Response:\n\n 前缀。 在文本进入语料库之前,去掉该头部,否则它会出现在你的第一个块的顶部:
python
# clean_mcp_payload.py — 在分块之前规范化 MCP 工具结果
PREFIX = "Response:\n\n"
def clean_markdown(tool_result: str) -> str:
"""从 MCP scrape_markdown 结果中去除无状态工具 'Response:' 前缀。"""
if tool_result.startswith(PREFIX):
tool_result = tool_result[len(PREFIX):]
return tool_result.strip()
脚本驱动 (Python SDK)
当脚本控制循环时,使用 SDK 创建一个云浏览器会话并渲染每个网址。SDK 从环境中读取 SCRAPELESS_API_KEY;proxy_country 固定住宅出口(在 SDK 上为 snake_case):
python
# extract.py — 将每个发现的 URL 渲染为干净的 Markdown
from scrapeless import Scrapeless
from scrapeless.types import ICreateBrowser
client = Scrapeless() # 读取 SCRAPELESS_API_KEY
session = client.browser.create(
ICreateBrowser(proxy_country="US", session_ttl=240)
)
def fetch_markdown(url: str) -> str:
"""在云浏览器中渲染 URL 并返回干净的 Markdown 正文文本。"""
# 会话在 session.browser_ws_endpoint 暴露一个 CDP 端点;
# 驱动它导航到 `url`,让页面充水,然后读取
# 为语料库清理后的 Markdown 正文。
# `render_to_markdown` 是你自己的帮助程序:驱动 CDP 端点进行导航,
# 等待充水,然后将清理后的 HTML 转换为 Markdown。对于没有帮助程序可写的
# 一体机结果,使用上面展示的 MCP `scrape_markdown` 工具,它直接返回 Markdown。
markdown = render_to_markdown(session, url)
return markdown.strip()
documents = []
for url in urls:
text = fetch_markdown(url)
if len(text) > 200: # 跳过接近空白或阻塞的页面
documents.append({"url": url, "text": text})
最后的短长度保护值得保留:返回的 Markdown 只有几十个字符的页面通常是同意墙或空容器,而不是文章,应该避免污染语料库。
在免费计划中获取你的 API 密钥:Scrapeless
Markdown 还是 HTML?
scrape_markdown 和 scrape_html 提供相同的渲染。区别在于返回内容和你对其的处理:
scrape_markdown |
scrape_html |
|
|---|---|---|
| 输出 | 干净可读的 Markdown | 完整渲染的 HTML |
| 标准处理 | 导航、脚本、广告被剔除 | 存在 — 自己剔除 |
| 最佳用途 | LLM 训练和 RAG 输入 | 自定义 CSS 选择器提取 |
| 下游的令牌成本 | 低 — 仅内容 | 高 — 包含标记 |
| 结构保留 | 标题、列表、表格、链接 | 完整 DOM |
对于 LLM 或 RAG 语料库,Markdown 是默认格式。它将内容而非其他东西交给嵌入模型,能比 CSS 选择器更好地适应 DOM 转换,并且在每个下游阶段占用更少的令牌。仅在需要针对特定布局运行自己的选择器时才使用 scrape_html。
第 3 步 — 为 RAG 进行分块
嵌入模型具有有限的输入大小,检索在每个存储单元是连贯段落时工作最佳。分块将干净的 Markdown 拆分为重叠的窗口。实用默认值是 每块 500-1000 个令牌,重叠 10-15% — 足够大以容纳一个完整的思想,足够小以保持检索的准确性,且有重叠确保一个跨越边界的句子在至少一个块中保持完整。
python
# chunk.py — 将干净的 Markdown 拆分为重叠的、令牌大小的块
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
python
def chunk_text(text: str, max_tokens: int = 800, overlap: int = 100):
"""生成清理过的 Markdown 文档中的重叠令牌窗口。"""
tokens = enc.encode(text)
step = max_tokens - overlap
for start in range(0, len(tokens), step):
window = tokens[start:start + max_tokens]
if not window:
break
yield enc.decode(window)
chunks = []
for doc in documents:
for i, piece in enumerate(chunk_text(doc["text"])):
chunks.append({
"id": f"{doc['url']}#chunk-{i}",
"url": doc["url"],
"chunk_index": i,
"text": piece,
})
由于输入已经是干净的 Markdown,因此分块器不必处理饼干横幅或 <script> 块将段落分成两部分。在令牌窗口之前按 Markdown 标题进行分割,即使源文档具有明确的标题结构——在一个部分内分块,而不是跨越两个——这也更好地保持了相关内容在一起。
一个单一的块记录如下所示:
json
{
"id": "https://example.com/guides/rag-explained#chunk-0",
"url": "https://example.com/guides/rag-explained",
"chunk_index": 0,
"token_count": 800,
"text": "## 检索增强生成\n\nRAG 通过在查询时检索最相关的段落并将其作为上下文传递给模型,将语言模型嵌入外部语料库。检索质量直接依赖于源文本提取和分块的干净程度 ..."
}
第 4 步 — 嵌入并持久化到向量数据库
最终阶段将每个块转换为向量并存储以供检索。下面的示例使用本地 Chroma 存储和 OpenAI 嵌入;对于 pgvector、Pinecone 或任何其他向量数据库,形状是相同的——交换客户端并保持记录不变:
python
# embed.py — 嵌入每个块并插入至向量存储
import chromadb
from openai import OpenAI
oai = OpenAI()
db = chromadb.PersistentClient(path=".chroma")
collection = db.get_or_create_collection("web_corpus")
def embed(texts: list[str]) -> list[list[float]]:
resp = oai.embeddings.create(model="text-embedding-3-small", input=texts)
return [d.embedding for d in resp.data]
batch = chunks[:64] # 批量嵌入
collection.upsert(
ids=[c["id"] for c in batch],
documents=[c["text"] for c in batch],
embeddings=embed([c["text"] for c in batch]),
metadatas=[{"url": c["url"], "chunk_index": c["chunk_index"]} for c in batch],
)
url 和 chunk_index 元数据随每个向量传递,因此当检索到一个块时,您可以引用源页面并重新组装相邻块以获得更完整的上下文。这些元数据也是让您通过 id 进行插入或更新的原因——刷新页面会在原地替换其块,而不是复制它们。
您所获得的
存储在向量数据库中的语料库是一个干净、嵌入的、源链接块的列表。检索到的记录如下所示:
json
{
"id": "https://example.com/blog/chunking-strategies#chunk-2",
"document": "### 重叠\n\n相邻块之间 10-15% 的重叠确保句子在至少一个窗口中完整,而这使得针对两个想法之间缝隙的查询的召回率提高 ...",
"metadata": {
"url": "https://example.com/blog/chunking-strategies",
"chunk_index": 2
},
"distance": 0.18
}
// 模式准确反映了第 4 步的插入或更新输出。字段值是示例。
关于何时在实时网络上运行此操作的一些诚实观察:
- Markdown 质量反映页面结构。 具有干净语义 HTML 的页面转换为优秀的 Markdown;而由通用
<div>汤构建的页面则可接受,但可能将侧边栏说明合并到主体中。在信任大型语料库之前,先检查一部分转换页面。 - 补水时间因网站而异。 大多数页面在 Markdown 被读取时已经完全渲染,但有些通过延迟请求补充其主要内容;对于这些页面,在读取之前,请稍等片刻以让页面稳定下来。
- 在两个层面上去重。 在发现时删除重复的 URL,并在嵌入之前删除近重复的块(哈希或相似性阈值)——联合文章和模板页脚会膨胀语料库并偏差检索。
- 固定输出区域以适应地理变化内容。 本地化内容的站点根据地区返回不同文本;将
proxy_country设置为您希望在语料库中包含的区域的版本,以保持数据集一致。 - 保持长度限制。 仅返回几十个字符的页面通常是同意墙或空容器,而不是内容——在分块之前将其过滤掉。
结论:建立可扩展的干净文本管道
发现 → 提取 → 分块 → 嵌入的过程大约缩减为六十行 Python 代码。承载阶段是提取,而 scrape_markdown 扮演了这一角色:反检测云浏览器渲染页面,住宅出口保持可达,返回的是可读的正文,形式为干净的 Markdown——嵌入模型最易读取的格式。分块和嵌入则是针对已经清理过的文本的标准库操作。
有关将同一 Scrapeless Scraping Browser 原语与具有类型化输出和向量存储的代理框架相连的内容,请参见 LangChain 集成帖子。有关组成搜索、渲染和提取工作系统的更多端到端模式,请参见 AI 代理使用案例帖子。贯穿所有模式的共通点是:锁定区域,返回 Markdown 而非 HTML,分块时有重叠,并在嵌入前进行去重。
准备构建您的 AI 驱动数据管道吗?
加入我们的社区以领取免费计划,连接正在 Scrapeless 上构建 LLM 和 RAG 数据管道的开发者:Discord · Telegram。
在 Scrapeless 注册以获取免费的 Scraping Browser 运行时,并将上述模式调整为您的管道所需的来源、区域和分块大小。计划和限制详见 scrapeless.com/en/pricing。
常见问题
问:为 LLM 或 RAG 语料库抓取网站文本合法吗?
在大多数法域,抓取公开可见的数据是广泛允许的,但各国和网站的服务条款有所不同。请查看目标网站的服务条款,尊重 robots.txt(如适用),在没有合法依据的情况下不要收集个人数据,并咨询律师以应付商业规模的语料库。构建训练或 RAG 数据集并不改变合法访问仅公开数据的基本义务。
问:为什么对 LLM 输入使用 Markdown 而非原始 HTML?
原始 HTML 主要是标记——导航、脚本、广告位、内联样式——这稀释了内容信号,增加了标记成本,并污染了嵌入。来自 scrape_markdown 的干净 Markdown 保留了页面实际涉及的标题、段落、列表、表格和链接,去掉其余部分,这样嵌入模型只读取内容而无其他。Markdown 也比 CSS 选择器提取更能经受 DOM 变化。
问:我应该使用多大块大小来进行 RAG?
一个实用的默认设置是每块 500–1000 个标记,重叠 10–15%。较小的块提高检索精度,但可能割裂一个想法;较大的块保留更多上下文,但稀释了相关性。根据您的嵌入模型输入大小和查询来调整——短小的事实查找倾向于较小的块,综合性问题则倾向于较大的块。在进行标记窗口操作前在 Markdown 标题上分割,以保持相关内容在一起。
问:我需要代理吗?
是的,对于大多数值得收集的公共来源。住宅出口使地理定位和反机器人保护的页面可达,并使重 JavaScript 页面渲染真实内容,而不是阻断页面。Scrapeless Scraping Browser 在 195 个国家通过住宅代理路由;设置 proxy_country 以锁定您所需内容版本的区域。
问:我该如何去重和清理语料库?
在两个层面进行去重:在发现阶段删除重复 URL,并在嵌入前使用内容哈希或相似性阈值删除近重复的块。由于 scrape_markdown 已经去除了样板,剩余的清理工作很轻——断长度守卫以丢弃几乎空的页面,和可选的注意标题的分割通常就足够了。
问:为什么 MCP 的 scrape_markdown 结果以 Response: 开头?
Scrapeless MCP 服务器上的无状态工具在其文本有效负载前加上 Response:\n\n。这是一种传输层头,而不是页面内容的一部分。在分块之前去掉它——步骤 2 中的一行 clean_markdown 辅助函数会处理它——这样前缀就不会出现在您的第一个块的顶部。
问:我可以在没有 AI 代理的情况下运行这个吗?
可以。步骤 2 中的 Python SDK 路径完全拥有发现 → 提取 → 分块 → 嵌入的循环,且无需涉及代理。当代理决定要收集哪些页面时,MCP 服务器是推荐的路径;当脚本决定时,SDK 是推荐的路径。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



