带代理的 RAG 爬虫:采集、证据、切分与索引版本化

发布时间
阅读时长约 5 分钟

Key Takeaways

一套面向生产的中文 RAG 爬虫实现方案:先定义来源授权和成功条件,再处理增量采集、代理路由、原始证据、结构化切分、索引版本与检索评估。

🧠
生产级 RAG 爬虫的目标不是“抓网页后切成固定 Token”,而是建立一条可授权、可追溯、可回滚的数据管线:来源策略 → 增量采集 → 原始证据 → 结构提取 → 版本化切分 → Embedding → 候选索引 → 检索评估 → 发布。

代理只是网络路由的一层。它可以用于地域采集、出口隔离或会话保持,但不能替代访问授权、内容质量判断、权限控制,也不应被当作绕过明确拒绝访问的手段。

先定义搜索与数据意图

在写爬虫之前,先回答三个问题:

  • 为什么采集:问答知识库、产品文档检索、市场情报,还是内部搜索?
  • 允许采什么:哪些域名、路径、语言、地区和内容类型在授权范围内?
  • 什么才算成功:不是 HTTP 200,而是得到正确版本、正确语言、可解析、可引用且允许进入索引的内容。

如果这些条件没有写进策略层,后面的代理、浏览器和向量库只会放大错误。

总体架构

来源策略库:把授权和抓取规则放在代码之外

json
{
  "sourceId": "product-docs",
  "baseUrl": "https://docs.example.com/",
  "allowedPrefixes": ["https://docs.example.com/guide/"],
  "deniedPrefixes": ["https://docs.example.com/account/"],
  "robotsReviewedAt": "2026-08-01",
  "termsReviewedAt": "2026-08-01",
  "defaultCollector": "http",
  "maxConcurrency": 2,
  "minDelayMs": 1000,
  "freshnessHours": 24,
  "owner": "knowledge-platform"
}

robots.txt 是站点向爬虫表达抓取规则的标准机制之一,但 RFC 9309 明确指出它不是访问授权机制。因此,robots、服务条款、隐私要求、合同授权和应用层权限必须分别评估。

URL Frontier 与短租约

任务记录至少保存调度和幂等信息:

json
{
  "url": "https://docs.example.com/guide/install",
  "sourceId": "product-docs",
  "priority": 50,
  "nextFetchAt": "2026-08-07T01:00:00Z",
  "lastStatus": 200,
  "etag": "...",
  "lastModified": "...",
  "contentSha256": "...",
  "failureCount": 0,
  "leaseUntil": null
}

Worker 领取任务时写入短租约;只有租约持有者能够提交本轮采集结果。超时任务可重新入队,但写入文档版本时仍需使用 URL + 内容哈希或版本 ID 做幂等保护。

用 HTTP 条件请求减少无效下载

HTTP 条件请求适合文档类来源。ETag 可配合 If-None-MatchLast-Modified 可配合 If-Modified-Since;服务器确认表示未变化时可返回 304 Not Modified。按 HTTP Semantics,若同时发送两种条件,If-None-Match 的判断优先于 If-Modified-Since

下面的示例保留 TLS 验证,并对重定向、超时、响应类型和响应体大小设置边界:

python
from dataclasses import dataclass
import httpx

MAX_BODY_BYTES = 5 * 1024 * 1024
ALLOWED_CONTENT_TYPES = ("text/html", "text/plain", "application/xhtml+xml")

@dataclass
class CrawlItem:
    url: str
    etag: str | None = None
    last_modified: str | None = None

async def fetch(item: CrawlItem, client: httpx.AsyncClient) -> dict:
    headers: dict[str, str] = {}
    if item.etag:
        headers["If-None-Match"] = item.etag
    if item.last_modified:
        headers["If-Modified-Since"] = item.last_modified

    async with client.stream("GET", item.url, headers=headers) as response:
        if response.status_code == 304:
            return {"type": "not_modified", "url": item.url}

        response.raise_for_status()

        content_type = response.headers.get("content-type", "").lower()
        if not any(t in content_type for t in ALLOWED_CONTENT_TYPES):
            raise ValueError(f"unsupported content type: {content_type!r}")

        body = bytearray()
        async for chunk in response.aiter_bytes():
            body.extend(chunk)
            if len(body) > MAX_BODY_BYTES:
                raise ValueError("response body exceeds 5 MiB limit")

        return {
            "type": "fetched",
            "url": str(response.url),
            "body": bytes(body),
            "etag": response.headers.get("etag"),
            "last_modified": response.headers.get("last-modified"),
            "content_type": content_type,
        }

async def example(item: CrawlItem) -> dict:
    timeout = httpx.Timeout(15.0, connect=5.0)
    limits = httpx.Limits(max_connections=20, max_keepalive_connections=10)
    async with httpx.AsyncClient(
        follow_redirects=True,
        timeout=timeout,
        limits=limits,
        headers={"User-Agent": "AuthorizedKnowledgeCrawler/1.0"},
    ) as client:
        return await fetch(item, client)

这里的 5 MiB、连接数和超时都是示例值,应根据来源和运行环境测量后调整,而不是当作通用最佳值。

即使存在 ETagLast-Modified,关键来源仍应定期做完整内容哈希校验,因为缓存验证器是否准确取决于源站实现。

HTTP 优先,浏览器只做有理由的回退

优先使用普通 HTTP 抓取;只有在以下情况才考虑浏览器:

  • 授权页面的业务正文依赖 JavaScript 渲染;
  • 需要完成已授权的交互才能得到数据;
  • 静态 HTML 缺失关键内容;
  • 需要保存视觉状态或截图证据。

浏览器回退必须继承同一个 sourceId、授权策略、语言/地域要求和证据 Schema,不能成为绕过 401/403 的隐式备用通道。

代理路由:按任务需求选择,不按失败码盲目换 IP

json
{
  "policy": "direct-first",
  "allowedRoutes": ["direct", "residential"],
  "country": "DE",
  "sessionMode": "rotating",
  "maxRouteAttempts": 2
}

可把路由策略拆成几类:

  • direct-first:直接访问能获得正确且允许的数据时,不增加代理层;
  • fixed-market:需要验证某一国家/语言版本时固定地域;
  • sticky-browser-task:同一个多步骤浏览器任务保持会话路由;
  • rotating-independent:相互独立的任务之间轮换出口,用于容量和隔离,而不是规避明确访问限制。

404、Schema 错误、权限不足或来源明确拒绝访问,都不应通过不断换 IP 重试。代理也不会改变所有浏览器指纹或保证通过反自动化系统。

原始证据库:向量库不能成为唯一事实来源

建议保存:

  • 原始响应或经过批准的快照;
  • HTTP 状态、最终 URL、内容类型;
  • 采集时间与 Collector 版本;
  • 路由地域和会话模式,但移除账号密码;
  • 内容 SHA-256;
  • 必要时的截图或渲染证据;
  • 来源策略版本和授权标签。

这样可以回答“这个答案来自哪一版页面”,也能在提取器或 Embedding 发生回归时重新处理原始证据,而不必重新访问源站。

URL 规范化:不要过度合并

同时保留:

  1. 请求 URL;
  2. 最终 URL;
  3. 规范 URL。

可以移除明确的追踪参数,但不要把语言、版本、地区、筛选条件或分页参数不同的页面错误合并。Canonical 标签可以作为信号,但不应无条件覆盖你的数据模型。

先提取结构,再切分

json
{
  "documentId": "product-docs:install:v17",
  "sourceUrl": "https://docs.example.com/guide/install",
  "title": "安装指南",
  "language": "zh",
  "sections": [
    {
      "headingPath": ["安装", "Linux"],
      "text": "...",
      "sourceOffsets": [1200, 2040]
    }
  ]
}

直接按原始 HTML 切块通常会把导航、页脚、重复菜单和隐藏 UI 一并送入 Embedding。更稳妥的做法是先提取正文结构,再以标题、段落、代码块和表格等语义边界切分。

Chunk 元数据决定是否可追溯

每个 Chunk 至少保存:

  • Chunk ID;
  • 文档版本;
  • 标题路径;
  • 来源 URL;
  • 原文偏移或可定位锚点;
  • 内容哈希;
  • Embedding 模型与版本;
  • 权限标签;
  • 生效与失效时间。

不要预设一个适用于所有内容的固定 Token 大小。实际 Chunk 尺寸需要根据文档结构、Embedding 模型限制和检索评估结果确定。

索引版本化与灰度发布

json
{
  "indexVersion": "kb-2026-08-07-01",
  "embeddingModel": "model-name-version",
  "documents": 1842,
  "chunks": 11240,
  "sourceSnapshot": "snapshot-2026-08-07",
  "createdAt": "2026-08-07T00:18:00+08:00"
}

上面的数量只是 Schema 示例,不代表 BytesFlows 的生产数据。

新索引应先生成候选版本,完成检索评估后再切换别名或版本指针,并保留至少一个可回滚版本。不要直接覆盖唯一的生产索引。

检索评估:验证答案,而不只是验证“向量写进去了”

准备来自真实用户任务的问题集,至少检查:

  • 目标文档/证据是否被召回;
  • 引用是否真正支持答案;
  • 过期内容是否进入答案;
  • 无充分证据时系统能否拒答;
  • 是否发生权限泄漏;
  • 是否被重复来源占满 Top-K;
  • 中文、英文、代码等不同内容类型的效果;
  • 检索与生成链路的延迟和成本。

评价指标应根据业务定义。不要在没有标注集和实验数据时宣称固定“准确率”或“召回率”。

Freshness 调度:从变化率推导,而不是写死频率

可把来源分为高变化、中变化和低变化,并记录历史变更间隔。一个可操作的起点是:

  • 高频变化内容:从较短周期开始,并设置抓取频率上限;
  • 稳定文档:延长周期,优先使用条件请求;
  • 政策、权限和关键配置页:可以使用定期哈希检查或事件触发,但仍遵守来源策略。

最终频率应根据实际变化率、业务时效需求、来源限制和成本动态调整,而不是因为内容类型叫“价格”或“Release Notes”就固定成某个小时数。

失败分类与停止条件

现象默认动作进入重试前要确认什么
404记录并降低优先级是否为临时发布、错误 URL 或重定向变化
410作为明确移除信号处理是否需要删除索引版本或保留历史证据
401/403停止自动重试并复核授权凭证、权限、来源策略是否仍有效
429降低速率Retry-After 或来源提供的速率指导
5xx有限指数退避总重试预算和熔断条件
异常 MIME隔离是否被重定向到登录页、错误页或下载文件
提取失败保留证据,阻止索引发布正文提取器是否发生回归
错语言/错地区不入索引路由、Accept-Language、Cookie 与页面实际输出

安全与隐私检查清单

域名、协议和路径白名单在发请求前校验;
防止任意 URL 抓取造成 SSRF;
网页正文视为不可信数据,不直接当作 Agent 指令执行;
下载文件默认不执行;
凭证、代理密码和正文日志分离;
Chunk 继承来源权限;
支持删除、重新切分和重新索引;
采集权限与问答服务权限分离;
每个答案可追踪到来源证据和文档版本。

相关内容

参考标准与文档

FAQ

RAG 爬虫一定需要代理吗?

不需要。能直接、稳定并获授权访问的数据源,通常应优先减少不必要的网络层。代理更适合有明确地域、出口隔离或会话路由需求的场景。

HTTP 200 就代表可以写入向量库吗?

不代表。还需要验证最终 URL、内容类型、语言/地区、正文提取结果、权限标签和内容质量。挑战页、登录页和错误模板也可能返回 200。

为什么要保存原始证据,而不是只保存 Chunk?

因为提取器、切分策略和 Embedding 模型都会变化。保留原始证据才能复现某一版索引,并在处理逻辑升级后重新生成派生数据。

404 和 410 应该一样处理吗?

不建议。404 可能来自错误 URL、临时状态或路由变化;410 更明确表示资源已经被移除。两者都应保存状态和历史证据,但自动删除策略不应完全相同。

AV
技术团队背书同行架构师审查 & 实测校验

Alex Vance

核心基础设施架构团队

本文由 BytesFlows 工程团队审查。代码示例适用于合规的公开网页数据采集、QA 自动化、SEO 监测与市场研究工作流。实际基准表现可能因目标站点防爬策略、地理位置、客户端运行环境及请求频率而异。