爬虫架构的三代演进:从 cheerio 到 Agent 再到 Crawl4AI

魏远标 Lv7

做政府/部委政策抓取 1 年,我们的爬虫架构经历了三次大的演进。这篇文章复盘三代爬虫的设计决策、成本数据和踩过的坑,希望对做类似场景(多站点、政策合规、Agent 工程化)的同学有参考价值。


一、背景:为什么”政策爬虫”是个难题

去年加入贝斯平,做的第一个 AI 项目是 Chatomni 政策洞察智能体——给合规部门用的”政策 Copilot”,用户问”营改增对小微企业有什么影响”,系统自动检索最新的政府文件、解读条款、输出结构化建议。

这套系统的输入是政策文件,而政策文件来自政府/部委网站——50+ 个站点、每月新增 1 万+ 政策、单条平均 3-5 KB。

听起来是个”标准的爬虫需求”,但真做起来全是坑:

  1. 站点改版频繁:某部委网站一年改版 3 次,每次改版 CSS selector 全部失效
  2. JS 渲染:~40% 的站点是 SPA,cheerio 直接抓不到内容
  3. 反爬对抗:高频访问触发 IP 黑名单
  4. 结构差异大:每个站点的”政策列表页 + 详情页”结构都不一样
  5. 新站点接入慢:每加一个站点,1 个工程师调 selector + 写测试要 1 人天

一年下来,我们的爬虫架构迭代了三代。


二、第一代:cheerio 经典流水线(2025.09 – 2025.10)

最朴素的做法:每个站点写一份 selector 配置 + cheerio 解析 + 定时调度

1
2
3
4
5
6
7
8
9
10
const siteConfig = {
url: 'https://www.example.gov.cn/policy/',
listSelector: '.policy-list .item', // 列表项 selector
titleSelector: '.item-title', // 标题 selector
dateSelector: '.item-date', // 日期 selector
nextPageSelector: '.pagination .next', // 翻页 selector
};

const crawler = new CheerioCrawler(siteConfig);
const policies = await crawler.run();

优点

  • 简单稳定,单个工程师 2 小时上手
  • 速度快(秒级抓一个页面)
  • 几乎零成本(纯 Node.js,无 LLM 调用)

致命问题

  • 新站点接入 1 人天/个:每个站点都要人工调 selector + 写单元测试
  • 站点改版即崩溃:CSS selector 一变,爬虫全废,维护成本线性增长
  • ❌ **JS 渲染站点覆盖率 0%**:40% 的站点直接抓不到内容

跑了 2 周,新加 5 个站点花了 5 人天,而且半夜被 oncall 叫醒 3 次——都是站点改版导致 selector 失效。


三、第二代:Claude Agent SDK 自决策爬虫(2025.10 – 2025.12)

痛定思痛,决定换思路:让 LLM 自己决定怎么抓

架构

用 Claude Agent SDK 设计了一个站点分析 Skill,让 Agent 自己分析 HTML 结构、自己写 selector、自己验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const agent = new ClaudeAgent({
skills: ['site-analyze', 'crawler-skill'],
mcp_servers: ['crawler_fetch', 'cheerio_parse', 'asset_download'],
});

// 第一次访问新站点:Agent 自动分析 + 生成配置
await agent.runSkill('site-analyze', {
url: 'https://www.example.gov.cn/',
save_config_to: 'sites/example.json',
});

// 后续抓取:用 Agent 生成的配置
const policies = await agent.runSkill('crawler-skill', {
site_id: 'example',
});

效果

指标 第一代 cheerio 第二代 Agent
新站点接入成本 1 人天 0(Agent 自动适配)
站点改版适应成本 1 人天/次 0(自动重新分析)
JS 渲染站点覆盖率 0% ~90%
抓取成功率 ~85% ~95%
单条抓取成本 < ¥0.001 ¥0.05 – 0.10

看起来很美好——Agent 解决了所有问题。

烧钱的过程

但跑了一个月后,账单让我傻眼了:

  • 50 个站点 × 每天 1 次 Agent 分析 × ¥0.07/次 = ¥105/月
  • 还有 ~5% 的失败站点 fallback 重跑 = ¥15/月
  • 总成本:¥120/月,仅爬虫一项

这还只是爬虫,不包含 RAG 检索、问答生成的成本。更糟的是:

  • Agent 跑一次站点分析要 5-15 秒(50 个站点排队跑要 10+ 分钟)
  • Agent 输出不稳定(同样的输入偶尔会给出不同的 selector 路径)
  • 失败排查困难(要看 LLM 思考过程才知道哪步走偏)

四、第三代:Crawl4AI + LiteLLM(2025.12 – 至今)

第二代的本质问题:让 LLM 干”机械活”是杀鸡用牛刀

Agent 擅长的是”需要推理的复杂决策”,但抓取列表页这件事有固定模式:访问 URL → 找列表项 → 找详情链接 → 访问详情 → 提取字段。这不需要推理,需要的是快速、稳定的规则化抽取

于是引入了 Crawl4AI——一个专门为 LLM 时代设计的爬虫框架,核心能力:

  1. headless Chromium 渲染(自动处理 JS)
  2. CSS selector + XPath + LLM 抽取 三种模式
  3. 内置反爬对抗(UA 轮换、代理池)
  4. 结构化输出(JSON Schema 强类型)

新架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import asyncio
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, LLMExtractionStrategy
from pydantic import BaseModel

class Policy(BaseModel):
title: str
url: str
publish_date: str
content: str
attachments: list[str]

async def crawl_site(site_config):
config = CrawlerRunConfig(
extraction_strategy=LLMExtractionStrategy(
provider="litellm/openai-compatible",
schema=Policy.model_json_schema(),
instruction="从以下 HTML 中提取政策信息,包括标题、URL、发布日期、正文和附件链接",
),
)
async with AsyncWebCrawler() as crawler:
results = await crawler.arun_many(
urls=site_config.start_urls,
config=config,
)
return [Policy.model_validate(r.extracted_data) for r in results]

关键设计:Agent 作为救火 fallback

完全抛弃 Agent 不现实——Crawl4AI 在某些复杂站点(嵌套 iframe、强反爬)还是会失败。

最终架构是 Crawl4AI 为主 + Agent 救火

1
2
3
4
5
6
7
async def smart_crawl(site_config):
try:
# 主路径:Crawl4AI(95% 场景)
return await crawl4ai_crawl(site_config)
except ExtractionError:
# Fallback:Agent(5% 复杂场景)
return await agent_crawl(site_config)

为了让 Crawl4AI 和 Agent 结果对齐,加了 shadow mode 影子模式

1
2
3
4
5
6
7
8
9
10
# 新方案上线前,并行跑一周对比
async def shadow_compare(site_config):
old_result = await agent_crawl(site_config) # 旧方案
new_result = await crawl4ai_crawl(site_config) # 新方案

diff_rate = compute_diff_rate(old_result, new_result)
metrics.record("crawler_diff_rate", diff_rate)

if diff_rate > 0.05: # 差异 > 5%,人工 review
alert_oncall(f"Crawl4AI vs Agent diff {diff_rate:.1%}")

shadow 模式跑了一周,结果:

  • 内容一致性:96.5%
  • Crawl4AI 比 Agent 多抓了 ~2% 的”边缘 case”(新格式的附件等)
  • 结论:完全可以切到 Crawl4AI

最终效果

指标 第一代 cheerio 第二代 Agent 第三代 Crawl4AI
新站点接入成本 1 人天 0 0
站点改版适应成本 1 人天/次 0 0
JS 渲染覆盖率 0% 90% 98%
抓取成功率 85% 95% 97%
单条成本 < ¥0.001 ¥0.07 ¥0.004
月成本(50 站点) < ¥1 ¥120 ¥15

五、踩过的坑(写给后来的同学)

坑 1:不要让 LLM 干机械活

Agent 不是万能的。让 LLM 做需要推理的事(理解用户意图、规划工具调用、生成结构化内容),别让它做重复的机械活(CSS selector、HTML 解析、HTTP 请求)。

Agent 跑站点分析一次要 ¥0.07,Crawl4AI 跑一次 ¥0.004——17 倍的成本差距,质量反而 Crawl4AI 更稳定。

坑 2:一定要做 Shadow Mode 对比

切新方案前绝不能直接切换。Shadow mode(影子模式)是让你跑新方案的同时保留旧方案,对比结果差异。

我们的 96.5% 一致性数据,让切换决策有了底气。如果不做 shadow,你永远不知道新方案在哪里偷偷掉了内容。

坑 3:成本失控是静悄悄的

第二代 Agent 跑了一个月才意识到烧了 ¥120。如果做每日成本告警(成本超过阈值就报警),可能一周就能发现。

1
2
3
4
5
6
# Prometheus 告警规则
- alert: CrawlerCostSpike
expr: increase(crawler_cost_usd[1h]) > 5
for: 10m
annotations:
summary: "Crawler cost spike: ${{ $value }} in 1h"

坑 4:失败排查必须有 trace

Agent 失败时,最难的是”它到底哪一步走偏了”。我们后来接入了 LangSmith 做 Agent trace,把每一步的 input/output/latency/token cost 都记下来:

1
2
3
4
5
6
7
8
[Step 1] fetch_url("https://...")
↳ latency: 1.2s, tokens: 0
[Step 2] parse_html(html)
↳ latency: 0.8s, tokens: 1500
↳ output: {items: [...], nextPage: null}
[Step 3] ❌ extract_policy(item)
↳ ERROR: "title not found in expected selector"
↳ 排查:站点改版,selector .policy-title 变为 .doc-title

没有 trace,Agent 失败就是”它说不通”;有了 trace,每一步都可解释、可修复


六、总结:选择爬虫方案的决策树

1
2
3
4
5
6
7
8
9
10
11
12
13
你的场景是什么?

├── 单站点 / 内部数据
│ └── cheerio / Playwright 够了,别用 LLM

├── 多站点 / 站点结构稳定
│ └── cheerio + 配置化(第一代)

├── 多站点 / 站点结构频繁变化
│ └── Crawl4AI(第三代)✅ 推荐

└── 极端复杂 / 需要推理决策
└── Agent 作为救火 fallback

核心原则

  1. 能用规则就别用 LLM:成本差 10-100 倍
  2. Agent 是手术刀,不是锤子:只在需要推理的地方用
  3. Shadow Mode 是切换方案的护身符:别裸切
  4. 成本监控要早做:等账单来了就晚了

七、参考资料


作者:魏远标,贝斯平 AI 架构师,13 年 To B 软件架构经验,最近 2 年聚焦 AI Native 应用架构。技术博客:javai.tech

欢迎在评论区交流你的爬虫架构演进经验~