crw-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

crw Best Practices

crw 最佳实践

Reference documentation for developers and AI agents using crw (fastCRW) in production. Covers decision-making, integration patterns, and crw-specific operational details.
面向在生产环境中使用crw(fastCRW)的开发者和AI Agent的参考文档,涵盖决策制定、集成模式以及crw特有的运维细节。

1. Choosing the right verb

1. 选择合适的动词

Stop at the cheapest rung that answers the need. Don't reach for a heavier verb than the task requires.
NeedVerbNotes
You have a question/topic, not a URLsearchOwn search backend, no API key required. Returns titles + URLs + snippets. Add
scrapeOptions
to get markdown inline.
You have one (or a few) known URLsscrapeReturns markdown, HTML, links, or structured JSON. JS auto-detected.
You need to discover which URLs exist on a sitemapFast URL discovery via sitemap + BFS. No content fetched. Use before committing to a crawl.
You need content from many pages under a sitecrawlAsync BFS job. Poll with
crw_check_crawl_status
. Always
map
first to estimate size.
The source is a local file (PDF)parse
crw_parse_file
(MCP) or
crw scrape path/to/file.pdf
(CLI). No network call.
You need a typed JSON object from a pageextract
--extract '<schema>'
(CLI) or
extract: {schema: {...}}
(MCP/REST). Runs an LLM; costs tokens.
You want to detect what changed on a pagewatch / diff
POST /v1/change-tracking/diff
. Stateless diff primitive; no stored state needed.
Common chains:
  • search
    → pick URLs →
    scrape
    the best ones
  • search --json
    (or
    crw_search
    ) → filter in Python subprocess →
    crw scrape
    chosen URLs
  • map
    → estimate page count →
    crawl
    a bounded section → stream results
  • map "https://docs.example.com"
    → find URLs → filter for
    /docs/api/auth
    scrape
    that one URL
选择能满足需求的最低成本方案,不要使用超出任务需求的重型动词。
需求动词说明
你有问题/主题,但没有URLsearch自有搜索后端,无需API密钥。返回标题+URL+片段。添加
scrapeOptions
可获取内嵌markdown内容。
你有一个(或几个)已知URLscrape返回markdown、HTML、链接或结构化JSON。自动检测JS内容。
你需要发现站点上存在哪些URLmap通过站点地图+BFS快速发现URL,不抓取内容。在开始爬取前使用此功能。
你需要获取站点下多个页面的内容crawl异步BFS任务。使用
crw_check_crawl_status
轮询状态。务必先使用
map
估算规模。
源文件是本地PDFparse使用
crw_parse_file
(MCP)或
crw scrape path/to/file.pdf
(CLI),无需网络请求。
你需要从页面获取类型化JSON对象extractCLI使用
--extract '<schema>'
,MCP/REST使用
extract: {schema: {...}}
。运行LLM,会消耗令牌。
你想要检测页面内容的变化watch / diff使用
POST /v1/change-tracking/diff
。无状态对比原语,无需存储状态。
常见链式流程:
  • search
    → 选择URL →
    scrape
    最优结果
  • search --json
    (或
    crw_search
    ) → 在Python子进程中过滤 →
    crw scrape
    选中的URL
  • map
    → 估算页面数量 →
    crawl
    限定范围的内容 → 流式返回结果
  • map "https://docs.example.com"
    → 查找URL → 筛选
    /docs/api/auth
    路径 →
    scrape
    该URL

2. Three call surfaces

2. 三种调用方式

crw runs identically in three modes. Pick the one available in your environment.
crw在三种模式下运行逻辑一致,选择适配你环境的方式即可。

CLI (
crw
)

CLI(
crw

Best for scripting, one-shot queries, and agent bash calls. Binary must be on PATH.
bash
crw search "query" --json --limit 5
crw scrape "https://example.com" --format json
crw map "https://docs.example.com"
crw scrape "report.pdf"                              # local PDF auto-detected
Use CLI when: the binary is on PATH and you're in a Bash context. Especially good for the dynamic-search pattern (pipe into Python subprocess).
最适合脚本编写、一次性查询和Agent的bash调用。二进制文件必须在PATH路径中。
bash
crw search "query" --json --limit 5
crw scrape "https://example.com" --format json
crw map "https://docs.example.com"
crw scrape "report.pdf"                              # 自动检测本地PDF
使用CLI的场景: 二进制文件在PATH中且处于Bash环境,尤其适合动态搜索模式(管道到Python子进程)。

MCP tools (
crw_search
,
crw_scrape
,
crw_map
,
crw_crawl
,
crw_parse_file
)

MCP工具(
crw_search
,
crw_scrape
,
crw_map
,
crw_crawl
,
crw_parse_file

Best inside an MCP-capable agent harness. The MCP server runs the engine either in-process (embedded mode, ~6 MB RAM, no server) or as a proxy to a REST endpoint.
crw_scrape(url="https://example.com", formats=["markdown"], onlyMainContent=true)
crw_search(query="query", limit=5)
crw_map(url="https://docs.example.com", limit=200)
MCP output bounds (defaults): content truncated to ~15,000 chars per call;
crw_map
returns ≤ 100 URLs. Both carry
truncated: true
when clipped. Pass
maxLength: 0
/
limit: 0
to opt out.
Use MCP when: you're inside Claude Code, Cursor, Windsurf, or any harness that manages MCP connections. Lower per-call overhead than REST for agent loops.
最适合支持MCP的Agent框架。MCP服务器可通过进程内模式(嵌入式,约6MB内存,无需独立服务器)运行引擎,或作为REST端点的代理。
crw_scrape(url="https://example.com", formats=["markdown"], onlyMainContent=true)
crw_search(query="query", limit=5)
crw_map(url="https://docs.example.com", limit=200)
MCP输出限制(默认): 每次调用内容截断至约15000字符;
crw_map
最多返回100个URL。当内容被截断时,返回结果会包含
truncated: true
。传入
maxLength: 0
/
limit: 0
可取消限制。
使用MCP的场景: 在Claude Code、Cursor、Windsurf或任何管理MCP连接的框架中使用。Agent循环中的单次调用开销低于REST。

REST API (
/v1/scrape
,
/v1/search
, etc.)

REST API(
/v1/scrape
,
/v1/search
等)

Best for application code, cross-language clients (Go, Java, Ruby), or when you need a shared microservice. Firecrawl-compatible — SDK swap is one
api_url
change.
python
undefined
最适合应用代码、跨语言客户端(Go、Java、Ruby)或需要共享微服务的场景。兼容Firecrawl — 仅需修改
api_url
即可切换SDK。
python
undefined

Python SDK (pip install crw)

Python SDK(pip install crw)

from crw import CrwClient client = CrwClient(api_url="https://api.fastcrw.com", api_key="crw_live_...") result = client.scrape("https://example.com", formats=["markdown"]) results = client.search("AI news", limit=10)
from crw import CrwClient client = CrwClient(api_url="https://api.fastcrw.com", api_key="crw_live_...") result = client.scrape("https://example.com", formats=["markdown"]) results = client.search("AI news", limit=10)

Drop-in for Firecrawl SDK

替代Firecrawl SDK

from firecrawl import FirecrawlApp app = FirecrawlApp(api_url="https://api.fastcrw.com", api_key="crw_live_...")

**Use REST when:** writing application code, needing async crawl jobs with polling,
or integrating with frameworks like LangChain / CrewAI / LlamaIndex.
from firecrawl import FirecrawlApp app = FirecrawlApp(api_url="https://api.fastcrw.com", api_key="crw_live_...")

**使用REST的场景:** 编写应用代码、需要带轮询的异步爬取任务,或与LangChain / CrewAI / LlamaIndex等框架集成。

3. Post-filtering strategy stack

3. 后过滤策略栈

Raw web results carry noise. Apply these in order, stopping when you have enough signal.
原始网页结果存在噪音,按以下顺序应用过滤,直到获得足够有效信息。

Layer 1: Rank/order-based triage (free)

第一层:基于排名的筛选(免费)

The search backend's raw score is unreliable (engine-dependent, often null). Position is the reliable signal — it reflects the aggregator's Reciprocal Rank Fusion over N engines. Default: trust the top 3-5 results unless they're obviously off-topic.
python
undefined
搜索后端的原始分数不可靠(依赖引擎,常为null)。位置是可靠信号 —— 它反映了聚合器对多个引擎的 reciprocal Rank Fusion 结果。默认规则:除非明显偏离主题,否则信任前3-5个结果。
python
undefined

Rely on position, not score

依赖位置而非分数

top = [r for r in results if r['position'] <= 5]
undefined
top = [r for r in results if r['position'] <= 5]
undefined

Layer 2: Regex / keyword density filter (cheap)

第二层:正则表达式/关键词密度过滤(低成本)

Before fetching full pages, filter descriptions for relevance. Drop results whose description doesn't contain any query-adjacent term.
python
keywords = {'commercializ', 'battery', 'production', '2025', '2026'}
relevant = [r for r in results
            if any(kw in r['description'].lower() for kw in keywords)]
After scraping full markdown, apply paragraph-level filtering:
python
for para in markdown.split('\n\n'):
    if len(para) > 60 and any(kw in para.lower() for kw in keywords):
        print(para)
在抓取完整页面之前,先过滤描述内容的相关性。剔除描述中不包含任何与查询相关词汇的结果。
python
keywords = {'commercializ', 'battery', 'production', '2025', '2026'}
relevant = [r for r in results
            if any(kw in r['description'].lower() for kw in keywords)]
抓取完整markdown内容后,应用段落级过滤:
python
for para in markdown.split('\n\n'):
    if len(para) > 60 and any(kw in para.lower() for kw in keywords):
        print(para)

Layer 3: LLM verify (expensive — use sparingly)

第三层:LLM验证(高成本 —— 谨慎使用)

When layers 1-2 aren't precise enough, send a small batch of candidate snippets to a cheap model for binary relevance classification.
python
import anthropic

def is_relevant(snippet: str, query: str) -> dict:
    """Returns {is_match: bool, confidence: float, reasoning: str}"""
    client = anthropic.Anthropic()
    msg = client.messages.create(
        model="claude-haiku-4-5",   # cheap model for classification
        max_tokens=128,
        messages=[{
            "role": "user",
            "content": (
                f"Query: {query}\n\n"
                f"Snippet: {snippet[:500]}\n\n"
                "Does this snippet directly answer or provide evidence for the query? "
                "Reply with JSON only: {\"is_match\": true/false, \"confidence\": 0-1, "
                "\"reasoning\": \"one sentence\"}"
            )
        }]
    )
    import json
    return json.loads(msg.content[0].text)
Gate: only call LLM-verify on snippets that passed layers 1-2. Don't send all 10 results through an LLM — pick the 3-5 most promising first.
当第一层和第二层过滤不够精确时,将少量候选片段发送给低成本模型进行二元相关性分类。
python
import anthropic

def is_relevant(snippet: str, query: str) -> dict:
    """返回 {is_match: bool, confidence: float, reasoning: str}"""
    client = anthropic.Anthropic()
    msg = client.messages.create(
        model="claude-haiku-4-5",   # 用于分类的低成本模型
        max_tokens=128,
        messages=[{
            "role": "user",
            "content": (
                f"Query: {query}\n\n"
                f"Snippet: {snippet[:500]}\n\n"
                "此片段是否直接回答或为查询提供证据?仅返回JSON:{\"is_match\": true/false, \"confidence\": 0-1, "
                "\"reasoning\": \"一句话说明\"}"
            )
        }]
    )
    import json
    return json.loads(msg.content[0].text)
限制: 仅对通过第一层和第二层过滤的片段调用LLM验证。不要将所有10个结果都发送给LLM —— 先挑选3-5个最有希望的结果。

4. Context-window hygiene

4. 上下文窗口规范

The single most important practice. See crw-dynamic-search for the full pattern. Summary:
  • Never pipe
    crw search --json
    or
    crw scrape --format json
    bare into context.
    Always filter in a Python subprocess — only your
    print()
    output enters context.
  • Write large results to
    .crw/
    or
    /tmp/
    , not stdout.
    Use
    crw scrape -o .crw/page.json
    then read selectively with
    grep
    or a Python heredoc.
  • MCP truncation is your first line of defense (default ~15K chars). But don't rely on it alone — a 15K char page is still 3,500+ tokens.
  • Target 150-600 tokens per source in your filtered output. If you're printing more from a single page, you're probably including boilerplate.
这是最重要的实践。完整模式请参考crw-dynamic-search。摘要:
  • 切勿将
    crw search --json
    crw scrape --format json
    的原始输出直接传入上下文。
    务必在Python子进程中过滤 —— 只有你的
    print()
    输出会进入上下文。
  • 将大型结果写入
    .crw/
    /tmp/
    目录,而非标准输出。
    使用
    crw scrape -o .crw/page.json
    ,然后通过
    grep
    或Python heredoc选择性读取。
  • MCP截断是你的第一道防线(默认约15000字符)。但不要完全依赖它 —— 15000字符的页面仍相当于3500+令牌。
  • 目标是每个来源的过滤输出为150-600令牌。 如果单个页面输出内容过多,很可能包含了冗余内容。

5. Self-hosted Hybrid RAG pattern

5. 自托管混合RAG模式

crw is optimized for the retrieve → filter → embed pipeline. Typical setup:
crw search "query" → top-N results (titles + snippets)
→ scrape top 3-5 full pages → filter to relevant paragraphs
→ embed filtered paragraphs → merge with local vector store
→ retrieve top-K chunks → feed to generation model
Why crw for RAG:
  • Search costs $0 per query (no per-call API fees)
  • Recurring crawls use VPS cost, not per-page credits
  • crw_crawl
    +
    jsonSchema
    can extract typed objects per page directly — skip the embed step for structured data
Python RAG skeleton:
python
from crw import CrwClient

client = CrwClient()  # embedded mode, no server

def retrieve_and_chunk(query: str, top_n: int = 5) -> list[str]:
    results = client.search(query, limit=top_n)
    chunks = []
    for r in results:
        # Scrape full page if the snippet isn't sufficient
        page = client.scrape(r['url'], formats=['markdown'])
        md = page.get('markdown', '') or ''
        # Split into paragraphs, keep non-trivial ones
        for para in md.split('\n\n'):
            para = para.strip()
            if len(para) > 100:
                chunks.append(para)
    return chunks
For a local vector store (Chroma, Qdrant, pgvector): embed these chunks, upsert with URL + position as metadata, then merge vector-store retrieval results with fresh
crw search
results at query time (hybrid retrieval).
crw针对检索 → 过滤 → 嵌入管道进行了优化。典型配置:
crw search "query" → 前N个结果(标题+片段)
→ 抓取前3-5个完整页面 → 过滤出相关段落
→ 嵌入过滤后的段落 → 与本地向量存储合并
→ 检索前K个片段 → 输入到生成模型
为何选择crw用于RAG:
  • 搜索每次查询成本为0(无单次调用API费用)
  • 定期爬取仅消耗VPS成本,无按页计费
  • crw_crawl
    +
    jsonSchema
    可直接从每个页面提取类型化对象 —— 结构化数据可跳过嵌入步骤
Python RAG框架:
python
from crw import CrwClient

client = CrwClient()  # 嵌入式模式,无需服务器

def retrieve_and_chunk(query: str, top_n: int = 5) -> list[str]:
    results = client.search(query, limit=top_n)
    chunks = []
    for r in results:
        # 如果片段不够,则抓取完整页面
        page = client.scrape(r['url'], formats=['markdown'])
        md = page.get('markdown', '') or ''
        # 拆分为段落,保留有意义的内容
        for para in md.split('\n\n'):
            para = para.strip()
            if len(para) > 100:
                chunks.append(para)
    return chunks
对于本地向量存储(Chroma、Qdrant、pgvector):嵌入这些片段,将URL+位置作为元数据插入,然后在查询时将向量存储的检索结果与最新的
crw search
结果合并(混合检索)。

6. Common pitfalls

6. 常见陷阱

ProblemImpactSolution
Piping raw JSON into context50K-500K chars enters context; token waste, reasoning degradationAlways filter in a Python subprocess — see crw-dynamic-search
Trusting
score
for triage
The search backend's scores are engine-dependent, often
null
; wrong results picked
Triage by
position
(rank order) + keyword density in
description
Crawling without mapping firstCommitting to a 500-page crawl when you needed 20 pagesAlways
crw map
first to estimate site size; cap with
maxPages
JS rendering on every scrapeUnnecessary browser spawn on plain-HTML pages; slowcrw auto-detects SPAs — don't add
--js
/
renderJs: true
unless the page is blank
Blocking on crawl job pollAgent hangs waiting for async crawlSet a poll interval (5-10s), set
maxPages
to bound job size, check
status: "completed"
Ignoring
truncated: true
Missing content from MCP calls; silent data lossCheck for
truncated: true
in MCP responses; pass
maxLength: 0
if you need full content
Writing one-shot scripts to
/tmp/
Wasteful; file left behindUse heredocs for one-shot filtering; only write data (JSON results) to
/tmp/
Scraping
robots.txt
-blocked pages
403/empty response; wasted callcrw respects
robots.txt
by default; use
--stealth
+ proxy for legitimate access to blocked pages
问题影响解决方案
将原始JSON传入上下文50000-500000字符进入上下文;令牌浪费,推理能力下降务必在Python子进程中过滤 —— 参考crw-dynamic-search
依赖
score
进行筛选
搜索后端的分数依赖引擎,常为
null
;选择错误结果
通过
position
(排名顺序)+
description
中的关键词密度进行筛选
未先执行map就开始爬取原本只需要20个页面,却启动了500页的爬取任务务必先使用
crw map
估算站点规模;通过
maxPages
设置上限
每次抓取都启用JS渲染在纯HTML页面上不必要地启动浏览器;速度慢crw会自动检测SPA —— 除非页面空白,否则不要添加
--js
/
renderJs: true
阻塞等待爬取任务轮询Agent因等待异步爬取而挂起设置轮询间隔(5-10秒),通过
maxPages
限制任务规模,检查
status: "completed"
忽略
truncated: true
MCP调用丢失内容;数据无声丢失检查MCP响应中的
truncated: true
;如果需要完整内容,传入
maxLength: 0
将一次性脚本写入
/tmp/
浪费资源;文件残留使用heredoc进行一次性过滤;仅将数据(JSON结果)写入
/tmp/
抓取
robots.txt
禁止的页面
返回403/空响应;调用浪费crw默认遵守
robots.txt
;如需合法访问被阻止页面,使用
--stealth
+代理

7. crw-specific operational awareness

7. crw特有的运维认知

Unlike credit-based APIs (Firecrawl, Tavily), crw's costs are infra-denominated. The right mental model: you're paying for VPS time and renderer pool capacity, not per-page fees.
与基于信用的API(Firecrawl、Tavily)不同,crw的成本基于基础设施消耗。正确的认知模型:你支付的是VPS时间和渲染池容量费用,而非按页计费。

Search backend rate limits and politeness

搜索后端速率限制与合规性

  • Public instances rate-limit or block JSON requests — always use a local instance (
    crw setup --local
    boots one via Docker).
  • The self-hosted search backend has no built-in per-client rate limit, but the upstream engines (Google, Bing, DDG) do. Burst too hard and engines start returning 429s or CAPTCHAs to your instance.
  • Practical safe rate: 2-4 searches/second burst, < 1/second sustained. Space parallel searches with a short sleep or process them in series.
  • --category news
    and
    --time-range week
    bypass the general engine pool — lighter on upstream rate limits.
  • 公共实例会限制或阻止JSON请求 —— 务必使用本地实例
    crw setup --local
    通过Docker启动)。
  • 自托管搜索后端没有内置的客户端速率限制,但上游引擎(Google、Bing、DDG)有。请求过于频繁会导致引擎向你的实例返回429错误或验证码。
  • 安全速率建议:突发请求2-4次/秒,持续请求<1次/秒。并行搜索之间添加短暂延迟或串行处理。
  • --category news
    --time-range week
    会绕过通用引擎池 —— 对上游速率限制的影响更小。

Renderer pool sizing

渲染池规模

crw runs a renderer ladder per request (HTTP → LightPanda → Chrome by default; additional tiers such as playwright and chrome_proxy are available via config).
  • HTTP tier is instant and stateless (no pool cost).
  • LightPanda is lightweight (~50 MB) but single-process per binary instance. Under load, requests queue behind the LightPanda instance.
  • Chrome (optional,
    docker compose --profile heavy
    ) is the stealth fallback. Each Chrome instance is ~200 MB RAM. Scale by running multiple Chrome instances or pointing at a remote CDP endpoint via
    [renderer.chrome] ws_url
    in your server config (the
    CRW_CDP_URL
    env var is honored by
    crw scrape --js
    in CLI mode only, not by server/MCP mode).
  • If you see consistent p90 timeouts, you're likely hitting the renderer queue. Add more Chrome instances or switch to fast mode (LightPanda-only, lower recall but faster tail).
crw为每个请求运行渲染器阶梯(默认HTTP → LightPanda → Chrome;通过配置可启用playwright、chrome_proxy等额外层级)。
  • HTTP层级:即时、无状态(无池成本)。
  • LightPanda:轻量级(约50MB)但每个二进制实例为单进程。高负载下,请求会在LightPanda实例后排队。
  • Chrome(可选,
    docker compose --profile heavy
    ):隐身 fallback。每个Chrome实例约占200MB内存。可通过运行多个Chrome实例或在服务器配置中指向远程CDP端点(
    [renderer.chrome] ws_url
    )进行扩展(
    CRW_CDP_URL
    环境变量仅在CLI模式的
    crw scrape --js
    中生效,服务器/MCP模式不支持)。
  • 如果出现持续的p90超时,可能是遇到了渲染器队列瓶颈。添加更多Chrome实例或切换到快速模式(仅LightPanda,召回率较低但尾部延迟更快)。

Proxy rotation

代理轮换

Self-hosted crw supports per-request BYOP (bring-your-own-proxy) via
--proxy URL
(CLI) or
proxy
/
proxyRotation
(MCP/REST). Rotation modes:
round_robin
,
random
,
sticky_per_host
.
  • LightPanda can't proxy — when a proxy is active, LightPanda is skipped (fail-closed). Only the HTTP and Chrome tiers route through the proxy.
  • If using proxies for scraping targets that block cloud IPs, set
    proxyRotation: "sticky_per_host"
    so sessions from the same domain always hit the same exit IP (avoids anti-bot CAPTCHA triggers from IP-hopping mid-session).
  • Proxy rotation applies to
    scrape
    ,
    crawl
    , and
    map
    — not
    search
    (which goes to your local search backend, not directly to search engines).
自托管crw支持通过
--proxy URL
(CLI)或
proxy
/
proxyRotation
(MCP/REST)实现按请求自带代理(BYOP)。轮换模式:
round_robin
random
sticky_per_host
  • LightPanda不支持代理 —— 启用代理时,会跳过LightPanda(故障关闭)。只有HTTP和Chrome层级会通过代理路由。
  • 如果使用代理抓取阻止云IP的目标,设置
    proxyRotation: "sticky_per_host"
    ,确保同一域名的请求始终使用相同的出口IP(避免因会话中IP切换触发反机器人验证码)。
  • 代理轮换适用于
    scrape
    crawl
    map
    —— 不适用于
    search
    (请求发送到本地搜索后端,而非直接到搜索引擎)。

Managed vs self-hosted call-surface differences

托管版与自托管版调用方式差异

FeatureSelf-hostedManaged (
api.fastcrw.com
)
SearchRequires a local search-backend sidecarIncluded (managed backend)
Proxy poolBYOP via configManaged proxy network
Rate limitingToken-bucket (configurable)Per-plan limits;
X-RateLimit-*
headers
CreditsN/A500 one-time lifetime free credits
AGPL obligationApplies if you expose to third partiesCarve-out included
功能自托管版托管版(
api.fastcrw.com
搜索需要本地搜索后端 sidecar内置(托管后端)
代理池通过配置自带代理托管代理网络
速率限制令牌桶(可配置)按计划限制;支持
X-RateLimit-*
响应头
信用额度一次性终身免费500额度
AGPL协议义务如果向第三方开放则适用包含豁免条款

8. Links

8. 链接