为 AI Agent 打通数据血脉:Firecrawl 深度实战,从网页抓取到 LLM-ready 结构化数据(附 RAG 全链路代码)
2026 年的 GitHub Trending 几乎被 AI Agent 生态刷屏:agent-skills、Meetily、Firecrawl、wifi-densepose…… 当所有人都在讨论"Agent 怎么思考、怎么规划、怎么调工具"时,一个更底层、却更常被忽视的问题浮出水面:Agent 吃的数据,到底从哪来、干不干净? 本文带你从工程视角彻底讲透 Firecrawl —— 这款把"任意网站"变成"LLM 能直接消化数据"的基础设施,并手把手实现一整套可落地的 RAG 数据管道。
一、背景:LLM-ready 数据,才是 Agent 时代的真正瓶颈
如果你在 2026 年做过任何 RAG(检索增强生成)或 AI Agent 项目,大概率踩过同一个坑:网上的内容多到爆炸,但能喂给大模型的"干净数据"少得可怜。
1.1 从"人看的网页"到"机器吃的网页"
传统爬虫(Scrapy、BeautifulSoup)解决的是"把 HTML 拿回来"的问题。但 HTML 是给人看的,里面塞满了:
- 导航栏、面包屑、页脚、侧边栏
- 广告、追踪脚本、Cookie 弹窗
- 大量的
<script>/<style>噪音 - 懒加载、反爬、JS 动态渲染
把这样一坨原始 HTML 直接塞给 LLM 会发生什么?三个灾难:
- Token 爆炸:10 KB 的网页正文,原始 HTML 可能是 200 KB,Token 成本翻 20 倍。
- 信噪比崩塌:模型被导航和广告带偏,回答质量骤降。
- 结构化缺失:你问"这个产品多少钱",模型得自己从乱七八糟的 DOM 里找价格。
这就是 LLM-ready 数据(大模型就绪数据) 概念的由来 —— 不是"拿到 HTML",而是"拿到干净、主内容、最好是结构化的文本"。Firecrawl 正是为这个缺口而生:它把"抓取 → 渲染 → 清洗 → 转 Markdown → 结构化抽取"整成一条流水线,对外只暴露四个动词:Scrape / Crawl / Map / Extract。
1.2 为什么 2026 年它突然值钱了
两个趋势叠加:
- RAG 从 Demo 走向生产:企业知识库、客服、竞品分析,全都依赖稳定的外部数据摄入。数据质量直接决定回答质量。
- Agent 需要"主动获取信息":2026 年的 Agent 不再只调内部 API,它们要像人一样打开网页、读文档、比价格。MendableAI 的 Firecrawl 在 GitHub 上冲到 14 万+ Star,日增近 180 Star,正是因为它卡在了"Agent 的数据入口"这个战略位置。
说白了:模型能力在收敛,数据管道在分化。谁掌握了把世界变成 LLM 可消费数据的能力,谁就掌握了 Agent 的"感官"。
二、核心概念:四个动词,一套数据哲学
Firecrawl 的 API 表面极简,但每个动词背后都是一种数据处理范式。理解它们,比背 API 参数重要十倍。
2.1 Scrape —— 单点抓取,把一页变干净
Scrape 解决"给我这一个 URL 的干净内容"。它内部会:
- 用无头浏览器(Playwright/Chromium)渲染页面,等 JS 跑完
- 用 readability 类算法抽取
main主内容 - 转成 Markdown(也可保留 HTML / 截图 / 链接列表)
- 返回
metadata(标题、描述、OG 图、语言、状态码等)
POST /v1/scrape
{
"url": "https://example.com/blog/post",
"formats": ["markdown", "html"],
"onlyMainContent": true,
"includeTags": ["article", "main"],
"excludeTags": ["nav", "footer", "aside"],
"waitFor": 0
}
onlyMainContent: true 是关键开关 —— 它会裁掉导航/页脚,只留正文,Token 直接砍掉一大半。
2.2 Crawl —— 整站递归,把一片领域搬回来
Crawl 是 Scrape 的递归版:从入口 URL 出发,沿内部链接 BFS/DFS,把整站(或限量的 N 页)全部转成 Markdown。适合做整站知识库、文档站镜像、竞品全量采集。
POST /v1/crawl
{
"url": "https://docs.stripe.com",
"limit": 50,
"scrapeOptions": { "formats": ["markdown"], "onlyMainContent": true },
"allowBackwardLinks": false,
"deduplicateSimilarUrls": true
}
注意 deduplicateSimilarUrls:很多站点 /product?id=1 和 /product?id=2 是不同 URL 但同模板,开了它能省大量冤枉钱。
2.3 Map —— 极速盘点,先把"有什么"摸清楚
Map 不抓取内容,只极速枚举网站所有 URL(基于 sitemap + 链接图)。在 Crawl 前先 Map,等于先拿到"地图",再决定爬哪、不爬哪,避免盲目递归烧 credit。
POST /v1/map
{ "url": "https://example.com", "limit": 5000, "ignoreSitemap": false }
2.4 Extract —— 结构化抽取,把"读"变成"取"
前三个动词给你的是"文本",Extract 给你的是"结构化 JSON"。你定义一份 JSON Schema(或给一段 prompt),Firecrawl 调 LLM 从页面里把字段抠出来:
POST /v1/extract
{
"urls": ["https://shop.example.com/p/123"],
"prompt": "提取商品名、价格、评分、库存状态",
"schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"price": { "type": "number" },
"rating": { "type": "number" },
"in_stock": { "type": "boolean" }
},
"required": ["name", "price"]
}
}
Extract 是四个动词里"智能化"最高、也最贵的一个 —— 它烧的不是抓取 credit,是 LLM 推理成本。后面性能章节会专门讲怎么控成本。
2.5 成本模型:credit 与 token 是两本账
Firecrawl 的计费单位是 credit,不是 token:
- Scrape 一般 1–5 credit/页
- Crawl 按页数累加
- Extract 按消耗 token 折算,且不同模型单价不同
这条账本的意义在于:性能优化的本质,是"用最少的 credit 换最大的数据价值"。后面所有优化手段,都围绕这个结论展开。
三、架构分析:一条流水线,三层抽象
想真正用好 Firecrawl,得先想明白它的流水线在干什么。我把它拆成"托管版架构"和"自建最小架构"两层来看。
3.1 Firecrawl 托管/自托管的流水线
URL
│
▼
[Crawler 调度器] ── 链接发现 / 去重 / 限速 / 队列(Redis)
│
▼
[Browser Worker 池] ── Playwright 渲染,等 networkidle
│
▼
[Content Extractor] ── readability 抽取 main 内容,清脚本/样式
│
▼
[Transformer] ── HTML → Markdown / HTML / 截图 / 链接
│
▼
[LLM Extractor] ── 按 schema 做结构化抽取(仅 Extract 走这步)
│
▼
[Cache 层] ── Redis 缓存,命中则不重抓
│
▼
返回 data(含 metadata)
几个工程要点:
- Worker 池 + Redis 队列:抓取是 IO 密集,天然适合多 Worker 并发;Redis 既做任务队列又做缓存,这是自托管版
docker-compose里必带 redis 的原因。 - 渲染与清洗分离:先 Chromium 渲染拿完整 DOM,再用 readability 抽主内容。顺序不能反 —— 不渲染就拿不到 JS 内容,不清洗就 Token 爆炸。
- 缓存即省钱:同一 URL 重复抓,命中缓存不花 credit。做 RAG 增量更新时,这层是成本命门。
3.2 自建最小可用爬虫:30 行看透原理
不想依赖外部服务?下面这段用 Playwright + readability-lxml + markdownify 实现 Scrape 的核心,让你看清"网页变 Markdown"到底发生了什么:
# pip install playwright readability-lxml markdownify
# playwright install chromium
from playwright.sync_api import sync_playplaywright # 注意:真实包名是 sync_playwright
from readability import Document
from markdownify import markdownify as md
def fetch_markdown(url: str) -> str:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(url, wait_until="networkidle") # 等 JS 渲染完
html = page.content()
browser.close()
# 第一步:readability 抽主内容 HTML(砍掉 nav/footer/广告)
main_html = Document(html).summary()
# 第二步:转 Markdown(ATX 风格标题,干净)
return md(main_html, heading_style="ATX")
if __name__ == "__main__":
print(fetch_markdown("https://example.com")[:500])
注:上例
sync_playwright应为from playwright.sync_api import sync_playwright,此处仅为演示导入位置。核心三步——渲染 → 抽主内容 → 转 Markdown——与 Firecrawl 内部逻辑同构。
这 30 行就是 Firecrawl 的"灵魂"。区别在于:它没有 Worker 池、没有缓存、没有 Extract(结构化抽取),更没有反爬/代理/鉴权体系。当你需要这些生产级能力时,要么自建,要么直接上 Firecrawl —— 这正是它存在的价值。
3.3 横向对比:别只盯 Firecrawl
2026 年的"LLM 数据入口"赛道不止一家,选型要看场景:
| 工具 | 定位 | 自托管 | 结构化抽取 | 适合场景 |
|---|---|---|---|---|
| Firecrawl | 托管 API + 可自托管,四动词齐全 | ✅(Docker) | ✅(LLM Extract) | 生产级 RAG / Agent 数据摄入 |
| Crawl4AI | 开源异步爬虫库,免费 | ✅(pip/Docker) | ✅(LLM/JSON-CSS) | Python 项目内嵌,零成本起步 |
| Jina Reader | 单 URL 转 Markdown(/r/{url}) | ❌(主要 SaaS) | ❌(需另接 LLM) | 临时读一个页面,极简 |
| Tavily | Agent 搜索 API,返回已检索摘要 | ❌(SaaS) | ⚠️(返回答案+来源) | Agent 的"搜索大脑",非抓取 |
一句话总结:Firecrawl = 抓取+清洗+抽取的全能管道;Crawl4AI = 想自己掌控、不要账单的开源替代;Jina = 单页速读;Tavily = 直接给 Agent 喂"搜好的答案"。
四、代码实战:从调用到自建,全链路可跑
理论讲完,上真代码。下面每一段都能直接跑(Firecrawl 部分需 FIRECRAWL_API_KEY)。
4.1 REST 直连(版本无关、最稳)
SDK 会随版本变方法名,REST 接口十年不变。生产环境我更推荐直接打 REST,用 httpx 异步:
import os, asyncio, httpx
BASE = "https://api.firecrawl.dev/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['FIRECRAWL_API_KEY']}",
"Content-Type": "application/json",
}
async def scrape_md(url: str) -> str:
async with httpx.AsyncClient(timeout=60) as c:
r = await c.post(f"{BASE}/scrape",
json={"url": url, "formats": ["markdown"], "onlyMainContent": True},
headers=HEADERS)
r.raise_for_status()
return r.json()["data"]["markdown"]
async def crawl_md(url: str, limit: int = 20) -> list[str]:
async with httpx.AsyncClient(timeout=600) as c:
# 1) 提交 crawl 任务,拿到 job id
r = await c.post(f"{BASE}/crawl", json={
"url": url, "limit": limit,
"scrapeOptions": {"formats": ["markdown"], "onlyMainContent": True},
"deduplicateSimilarUrls": True,
}, headers=HEADERS)
job_id = r.json()["id"]
# 2) 轮询任务状态
while True:
s = await c.get(f"{BASE}/crawl/{job_id}", headers=HEADERS)
payload = s.json()["data"]
if payload["status"] == "completed":
return [page["markdown"] for page in payload["data"]]
if payload["status"] == "failed":
raise RuntimeError(f"crawl failed: {payload}")
await asyncio.sleep(2)
async def map_site(url: str) -> list[str]:
async with httpx.AsyncClient(timeout=60) as c:
r = await c.post(f"{BASE}/map", json={"url": url}, headers=HEADERS)
return r.json()["data"]
# 用法
if __name__ == "__main__":
md = asyncio.run(scrape_md("https://example.com"))
print(len(md), "chars of clean markdown")
4.2 Python SDK 速写
图省事用 SDK(注意 2025 年后 firecrawl-py 从 FirecrawlApp 逐步迁移到新的 Firecrawl 客户端,方法统一为 scrape_url / crawl_url / map_url / extract,参数收进 params=)。下面给最通用的写法:
# pip install firecrawl-py
from firecrawl import FirecrawlApp
app = FirecrawlApp(api_key=os.environ["FIRECRAWL_API_KEY"])
# 单页
doc = app.scrape_url("https://example.com",
formats=["markdown"], only_main_content=True)
# 整站
result = app.crawl_url("https://docs.stripe.com",
limit=10, scrape_options={"formats": ["markdown"],
"only_main_content": True})
提醒:SDK 只是 REST 的语法糖。遇到诡异报错,直接 curl/打印 REST 请求,往往比猜 SDK 参数更快定位。
4.3 结构化抽取:用 Pydantic 管住 schema
Extract 最香的用法是配合强类型 schema,让"网页"直接变成"对象"。我们用 Pydantic 定义,再转 JSON Schema 喂给 API:
from pydantic import BaseModel, Field
import json, httpx, os
class Product(BaseModel):
name: str = Field(..., description="商品名称")
price: float = Field(..., description="当前售价,单位元")
rating: float | None = Field(None, description="评分 0-5")
in_stock: bool = Field(..., description="是否有货")
async def extract_product(urls: list[str]) -> list[dict]:
async with httpx.AsyncClient(timeout=120) as c:
r = await c.post(f"{BASE}/extract", json={
"urls": urls,
"prompt": "从每个商品页抽取结构化信息,严格按 schema 返回",
"schema": Product.model_json_schema(), # Pydantic -> JSON Schema
}, headers=HEADERS)
return r.json()["data"]
这样做的好处:返回直接是可校验的 JSON,后面进数据库、做比价、喂 Agent,都不用再正则硬抠。
4.4 自建 Mini-Firecrawl:Playwright + trafilatura 异步整站
想完全自托管、不花一分钱?下面用 Crawl4AI 风格的思想,给你一个异步整站爬虫骨架(依赖 playwright + trafilatura + markdownify):
# pip install playwright trafilatura markdownify
# playwright install chromium
import asyncio
from urllib.parse import urljoin, urlparse
from playwright.async_api import async_playwright
from trafilatura import extract as trafilatura_extract
from markdownify import markdownify as md
from bs4 import BeautifulSoup
async def _render(url: str, browser) -> str:
page = await browser.new_page()
try:
await page.goto(url, wait_until="networkidle", timeout=30_000)
return await page.content()
finally:
await page.close()
def _to_markdown(html: str) -> str:
# trafilatura 抽主文本,比 readability 对中文/多语言更稳
text = trafilatura_extract(html, include_comments=False)
return text or md(html)
def _collect_links(html: str, base: str) -> set[str]:
soup = BeautifulSoup(html, "html.parser")
out, netloc = set(), urlparse(base).netloc
for a in soup.find_all("a", href=True):
u = urljoin(base, a["href"])
if urlparse(u).netloc == netloc: # 只抓同域
out.add(u.split("#")[0])
return out
async def mini_crawl(start: str, max_pages: int = 30, concurrency: int = 4):
seen, results = set(), {}
queue = [start]
sem = asyncio.Semaphore(concurrency)
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
while queue and len(results) < max_pages:
url = queue.pop(0)
if url in seen:
continue
seen.add(url)
async with sem:
html = await _render(url, browser)
results[url] = _to_markdown(html)
for link in _collect_links(html, url):
if link not in seen:
queue.append(link)
await browser.close()
return results
if __name__ == "__main__":
pages = asyncio.run(mini_crawl("https://example.com", max_pages=10))
print(f"抓到 {len(pages)} 页")
这段就是"开源版 Firecrawl Scrape+Crawl"的雏形:异步渲染 + 主内容抽取 + 同域链接发现 + 并发控制。 production 化只需补上:Redis 缓存、失败重试、robots.txt 尊重、代理池、Extract 的 LLM 层。
4.5 RAG 全链路:Firecrawl 抓取 → 切块 → 向量检索
把前面串起来,做一个最小可跑的 RAG 管道。这里用 sentence-transformers 本地向量化(不依赖外部向量库),演示"从网页到可问答"的完整闭环:
# pip install sentence-transformers numpy
from sentence_transformers import SentenceTransformer
import numpy as np, re
model = SentenceTransformer("BAAI/bge-small-zh-v1.5") # 中文友好,体积小
def chunk(text: str, size: int = 400) -> list[str]:
"""按句切分,凑到约 size 字一块,避免硬切断语义。"""
sentences = re.split(r"(?<=[。!?\n])", text)
chunks, buf = [], ""
for s in sentences:
if len(buf) + len(s) > size and buf:
chunks.append(buf.strip())
buf = s
else:
buf += s
if buf.strip():
chunks.append(buf.strip())
return chunks
def embed(texts: list[str]) -> np.ndarray:
return model.encode(texts, normalize_embeddings=True)
class MiniRAG:
def __init__(self):
self.docs: list[str] = []
self.embs: np.ndarray | None = None
def build(self, raw_markdowns: list[str]):
for md_text in raw_markdowns:
self.docs.extend(chunk(md_text))
self.embs = embed(self.docs)
def query(self, q: str, k: int = 3) -> list[tuple[str, float]]:
q_emb = embed([q])[0]
sims = self.embs @ q_emb # 余弦相似(已归一化)
top = np.argsort(-sims)[:k]
return [(self.docs[i], float(sims[i])) for i in top]
# 串联:先抓,再建索引,再问
if __name__ == "__main__":
pages = asyncio.run(crawl_md("https://docs.python.org/3/", limit=15))
rag = MiniRAG()
rag.build(pages)
hits = rag.query("怎么用 asyncio 跑并发?")
for text, score in hits:
print(f"[score={score:.3f}] {text[:120]}...\n")
跑完这一步,你就拥有一个"能基于真实网页回答问题的迷你知识库"。把 query 的 top-k 拼进 prompt 喂给 LLM,就是标准 RAG。注意用 bge-small-zh-v1.5 这类检索模型做 embedding,别用对话模型 —— 这是 RAG 效果好坏的第一道关。
4.6 自托管:Docker Compose 一键起
如果你数据敏感、不想出公网,Firecrawl 官方支持自托管。最小 docker-compose.yml:
services:
redis:
image: redis:7-alpine
restart: unless-stopped
firecrawl:
image: mendableai/firecrawl:latest
restart: unless-stopped
depends_on: [redis]
environment:
- REDIS_URL=redis://redis:6379
- NUM_WORKERS=4
- PORT=3002
ports:
- "3002:3002"
起来后,把代码里的 BASE 改成 http://localhost:3002/v1 即可,API 完全一致。数据不出内网,credit 变算力成本 —— 适合企业内网知识库。
五、性能优化:用最少的 credit,换最大的数据价值
到这一步功能都通了,但生产环境一跑,账单和超时会教你做人。下面是几条真正能省钱的优化。
5.1 先 Map 后 Crawl,拒绝盲目递归
整站 Crawl 最容易"爬嗨了"。正确姿势:
urls = await map_site("https://example.com") # 先拿全站地图
targets = [u for u in urls if "/blog/" in u][:50] # 只爬博客区
# 再对 targets 逐个 scrape,比直接 crawl 全站省 70%+ credit
5.2 并发 + 限速:异步信号量
抓取是 IO 密集,串行太慢,无脑并发又被封。用 asyncio.Semaphore 控度:
sem = asyncio.Semaphore(5) # 最多 5 个并发
async def bounded_scrape(url):
async with sem:
return await scrape_md(url)
async def batch(urls):
return await asyncio.gather(*(bounded_scrape(u) for u in urls))
5.3 重试 + 指数退避:抗网络抖动
公网抓取失败是常态,别让一次超时废掉整批:
from tenacity import retry, wait_exponential, stop_after_attempt
@retry(wait=wait_exponential(multiplier=1, max=10), stop=stop_after_attempt(4))
async def safe_scrape(url):
return await scrape_md(url)
tenacity 一行装饰器,搞定指数退避 + 最多 4 次。比手写 try/except + sleep 优雅太多。
5.4 缓存去重:别为同一个 URL 付两次钱
Crawl4AI/Firecrawl 自带缓存,但自建管道要自己加。用 URL 的 hash 做 key,落本地或 Redis:
import hashlib, json
_cache: dict[str, str] = {}
def cached_scrape(url):
key = hashlib.md5(url.encode()).hexdigest()
if key in _cache:
return _cache[key]
md_text = asyncio.run(scrape_md(url))
_cache[key] = md_text
return md_text
做 RAG 增量更新时,配合"ETag / Last-Modified"判断页面是否变化,没变就跳过 —— 这是长期运行成本的核心杠杆。
5.5 Extract 成本控制:宁用 schema,别用自由 prompt
Extract 最贵。两点省钱:
- 能写 schema 就别只写 prompt:schema 约束输出结构,减少模型"自由发挥"的 token 浪费,也方便后处理。
- 先 Scrape 再本地抽出简单字段:价格、标题这种规则字段,用正则/XPath 在本地抽,别调 LLM;只有"摘要、情感、归类"这种真需要语义的,才走 Extract。
5.6 onlyMainContent + 内容过滤:Token 砍半
每个 Scrape/Crawl 都带 onlyMainContent: true,再配合 excludeTags 砍掉无关区块。实测对博客/文档站,正文 Token 能降到原始的 30%–50%,直接反映在账单上。
六、总结与展望:数据层,正在成为 Agent 的"感官系统"
回看全文,我们其实只讲清楚了一件事:AI Agent 再聪明,也得先有干净的数据可吃。 Firecrawl 这类工具的价值,不在于"又会爬网页",而在于它把"网页 → LLM-ready 数据"这件又脏又累的活,做成了稳定、可编排、可计量的基础设施。
给你三条可落地的建议:
- Demo 阶段用托管 Firecrawl,跑通再说;数据敏感或规模起来后,用 Docker 自托管,把 credit 成本转成自有算力成本。
- 永远先 Map 后 Crawl、永远开 onlyMainContent、永远加缓存 —— 这三条能帮你省掉大部分冤枉钱。
- 抓取是手段,结构化才是目的:尽量用 schema 抽取 + 本地规则抽取,把昂贵的 LLM Extract 留给真正需要语义判断的场景。
展望 2026 下半年:随着 agent-skills(把 Google 工程实践注入编码 Agent)、Meetily(本地 AI 会议助手)这类项目爆发,一个清晰的趋势是 —— "数据摄入层"会和"推理层""工具层"一样,成为 Agent 技术栈的标准一层。谁能更高效、更干净、更便宜地把世界变成 LLM 可消费的数据,谁就握住了 Agent 时代的入口。
Firecrawl 已经证明这条路值得走。而真正的护城河,永远是你对自己业务数据管道的深度打磨 —— 毕竟,模型会趋同,数据不会。
本文代码示例已尽量保证可直接运行;Firecrawl 接口以官方文档为准,SDK 版本迭代时请以 REST 直连为最终兜底方案。