上层能力:map / search / extract / deep-research / llmstxt
30 秒导读: 前四章讲的是"怎么把一个 URL 抓成 LLM-ready 的数据"(内核)。这一章讲的是建在内核之上的五个高阶端点——它们自己几乎不写抓取代码,而是把 scrapeURL 抓取内核、WebCrawler + 队列、搜索后端当成积木,按不同的编排方式拼出"列全站 URL""搜索+全文""跨页结构化抽取""迭代式研究""自动生成 llms.txt"这五种能力。看这一章,重点看编排,不看抓取本身。
1. 这是什么(五个端点,一句话各就各位)
Firecrawl 对外不止"抓一个页面"。在抓取内核之上,它还提供五个做决策、做编排的端点。它们的共同点是:自己不负责把 HTML 变成 markdown(那是内核的活),而是决定"抓哪些页、抓完怎么拼、要不要再抓一轮"。
| 端点 | 白话 | 复用了谁 | 同步/异步 |
|---|---|---|---|
| Map | 只列出一个站点有哪些 URL,不抓正文 | sitemap + 搜索后端 + 索引 | 同步 |
| Search | 搜索引擎结果,可选对每条结果抓全文 | 搜索后端 + scrapeURL | 同步 |
| Extract | 给 schema + prompt,跨多页抽出结构化数据 | Map + scrapeURL + LLM | 异步(入队) |
| Deep Research | 就一个问题反复"搜-抓-分析",产出研究报告 | Search+scrape + LLM | 异步(入队) |
| Generate llms.txt | 为整站自动生成 llms.txt 索引文件 | Map + scrapeURL + LLM | 异步(入队) |
一句话直觉: 内核是"手"(抓一页),这五个端点是"脑"——决定手往哪伸、伸几次、抓回来的东西怎么组织。越往下(Extract → Deep Research)编排越像一个 agent:会自己规划、自己判断"够不够、要不要再来一轮"。
用起来什么样(以 Map 为例,最简单):
# 只要 URL 列表,不要正文
curl -X POST https://api.firecrawl.dev/v2/map \
-H 'Authorization: Bearer fc-YOUR_KEY' \
-d '{"url": "https://docs.example.com", "search": "pricing"}'
# → { "success": true, "links": [ {"url":"...","title":"..."}, ... ] }
Extract / Deep Research / llms.txt 因为要跑很久,返回的是一个 id,你再去轮询 /extract/{id} 等状态——这就是它们"异步入队"的原因(见 §2)。
2. 顶层全景(上层怎么骑在内核上)
怎么读这张图: 从下往上是依赖方向——上层调用下层,下层从不知道上层存在。左边同步(HTTP 里直接算完返回),右边异步(丢进队列,worker 慢 慢跑)。
同步端点 (controller 里算完就返回) | 异步端点 (入队 → worker)
┌──────────────┬───────────────────┐ | ┌──────────┬──────────┬──────────┐
│ Map │ Search │ | │ Extract │ Deep │ llms.txt │
│ 列全站 URL │ 搜索+可选抓全文 │ | │ 结构化抽取│ Research │ 生成索引 │
└──────┬───────┴─────────┬─────────┘ | └────┬─────┴────┬─────┴────┬─────┘
│ │ | │ │ │
│ ┌────────┴─────┐ | │ │ │
▼ ▼ ▼ | ▼ ▼ ▼
┌─────────────────┐ ┌──────────────┐ | ┌──────────────────────────────┐
│ 搜索后端 │ │ scrapeURL │◄──┼───┤ 编排层(analyze/rerank/plan) │
│ fireEngine/ │ │ (抓一页) │ | │ Map + scrapeURL + LLM 循环 │
│ searxng/ddg │ └──────────────┘ | └──────────────────────────────┘
└─────────────────┘ │ |
│ │ | 全部最终都落到 ▼
└──────────┬──────────┘ |
▼ ▼
┌────────────────────────────────────────────┐
│ 内核 (01-03):scrapeURL 编排 + 引擎回退 │
│ + HTML→markdown 转换 │
│ 基础设施 (04):WebCrawler + 自建队列 NuQ │
└────────────────────────────────────────────┘
部件一句话职责:
| 部件 | 干什么 | 在哪 |
|---|---|---|
| Map controller | 校验/计费/超时,委托 getMapResults | apps/api/src/controllers/v2/map.ts:22 mapController |
| Map 核心 | sitemap+搜索+索引三路并发出 URL,去重过滤 | apps/api/src/lib/map-utils.ts:82 getMapResults |
| Search controller | 校验/预留信用点,委托 executeSearch | apps/api/src/controllers/v2/search.ts:32 searchController |
| Search 编排 | 调搜索后端 → 可选批量抓全文 → 可选高亮 | apps/api/src/search/execute.ts:62 executeSearch |
| 搜索后端 | fireEngine→searxng→DuckDuckGo 三级回退 | apps/api/src/search/index.ts:8 search |
| Extract 编排 | 拆 schema→选 URL→抓→补全→合并 | apps/api/src/lib/extract/extraction-service.ts:78 performExtraction |
| Deep Research | 迭代"搜-抓-分析",到深度/时间/URL 上限停 | apps/api/src/lib/deep-research/deep-research-service.ts:26 performDeepResearch |
| llms.txt | Map 出 URL → 逐页抓 → LLM 写描述 | apps/api/src/lib/generate-llmstxt/generate-llmstxt-service.ts:65 performGenerateLlmsTxt |
三个异步端点的 worker 入口都在同一处:apps/api/src/services/queue-worker.ts:52 processDeepResearchJobInternal、:131 processGenerateLlmsTxtJobInternal(Extract 走独立进程 extract-worker.ts)。它们只是"取任务→调上面的 performXxx→写回状态",队列机制本身见 04 章。
3. 逐个能力:各自的独特编排
下面每个能力只讲它多做了什么——抓取/转换/队列这些下层机制不重复,交叉引用即可。
3.1 Map:不抓正文,只"列门牌号"
要解决的小问题: 我想知道 docs.example.com 底下总共有哪些页,但不想把每页都抓一遍(太贵太慢)。
思路: 三路信息源并发拿 URL,合并去重——谁都不完美,合起来覆盖最广:
| 信息源 | 来自哪 | 特点 |
|---|---|---|
| 搜索后端 | site:域名 查询,分页拉 | 覆盖被搜索引擎收录的页 |
| 内部索引 | Firecrawl 自己的历史抓取索引 | 命中过的站点秒回 |
| sitemap | 站点自己声明的 sitemap.xml | 站长明确列出的页 |
真实编排在 map-utils.ts:82 getMapResults:先 resolveRedirects 定位真实域名,再把索引查询和搜索分页同时发(map-utils.ts:240 Promise.all([queryIndex(...), fetchAllPages()])),搜索结果按 fireEngineMap(search/fireEngine.ts:54)一页 100 条最多凑到 MAX_FIRE_ENGINE_RESULTS = 100(map-utils.ts:28)。
关键细节(它省了什么):
- 不进 scrapeURL——Map 全程不抓正文,所以便宜、快。这是它和其他四个端点最大的区别。
search参数会改写查询:传了search就把site:域名变成<search> site:域名,再对结果做余弦相似度重排(map-utils.ts:309performCosineSimilarityV2),把最相关的 URL 提前。- 一堆兜底过滤:同域过滤、子域过滤、路径前缀过滤(
filterByPath)、dedupeMapDocumentArray去重(map-utils.ts:37,同 URL 优先保留带 title 的)。 sitemap: "only"时直接只走 sitemap,连搜索都跳过(map-utils.ts:163)。- controller 层还有个体贴处:结果 ≤1 条且用户没指定
limit=1时,提示"试试映射根域名"(map.ts:245)。
Map 是其他能力的地基:Extract 的 URL 发现、llms.txt 的站点枚举,底层都在调
getMapResults。
3.2 Search:搜索结果 + 可选全文 + 可选高亮
要解决的小问题: 我要的不是"某个已知 URL 的内容",而是"关于 X 的网页有哪些,顺便把正文也给我"。
思路——三段式,后两段可选:
1. 搜 → 2. (可选) 抓全文 → 3. (可选) 高亮
搜索后端 对每条结果 用索引里的片段
三级回退 跑 scrapeURL 替换搜索引擎给的 snippet
第 1 段 搜索后端的三级回退(search/index.ts:8 search)——按环境配置择优,失败就降级:
fireEngine(配了 FIRE_ENGINE_BETA_URL?) → searxng(配了 SEARXNG_ENDPOINT?) → DuckDuckGo(兜底)
executeSearch(search/execute.ts:62)先按 limit * 2 多要一些结果做缓冲(:79 num_results_buffer),再按 web/images/news 各自截断到 limit。
第 2 段 抓全文——只有请求里带了 scrapeOptions.formats 才触发(execute.ts:152 shouldScrape)。此时:
getItemsToScrape(search/scrape.ts:144)从 web/news/images 三类结果里挑出没被 blocklist 挡掉的 URL;scrapeSearchResults批量走 scrapeURL 内核抓正文;mergeScrapedContent把抓到的 markdown 塞回对应结果对象。
第 3 段 高亮(实验特性)——把搜索引擎给的短 snippet,换成 Firecrawl 索引里更贴合查询的高亮片段。三重门槛全过才生效(execute.ts:199):请求 highlights=true + 团队 highlightsBeta flag + 相关环境就绪(highlightsEnvReady())。注意它跑在抓取之后——因为 mergeScrapedContent 会重建结果对象,高亮必须最后动才不被覆盖(源码注释明确点出这个顺序陷阱)。
关键细节:
- 计费分两笔:搜索本身按结果数算
searchCredits(execute.ts:148),抓全文的部分由 scrape 作业自己计费,Search 只算搜索那笔。 - 同步端点:全程在 controller 里跑完返回,不入队——因为抓的页数受
limit约束,可控。
3.3 Extract:agent 式跨页结构化抽取
要解决的小问题: "把这几个(甚至一整站)页面里的产品名+价格+评分抽成一张 JSON 表"——目标页可能几十上百个,字段可能是一个数组的实体(多产品)也可能是单一答案(公司总部在哪)。
思路: 这是五个端点里编排最像 agent 的一个。核心 pipeline 在 extraction-service.ts:78 performExtraction,分六步:
① 补 URL 没给 urls 就用 prompt 生成 SERP 查询 → search() 拿 10 条
② 拆 schema analyzeSchemaAndPrompt:判断是"多实体"还是"单答案"
③ 选 URL 每个 url 跑 processUrl:Map 发现子页 → LLM reranker 筛相关
④ 批量抓 对选中的 URL 走 scrapeURL(multi-entity 分 50 个一块)
⑤ 补全 multiEntity 逐块抽 / singleAnswer 一次性抽
⑥ 合并 transformArrayToObject → 去重 → mixSchemaObjects 合并两路
② 拆 schema 是精华。 analyzeSchemaAndPrompt(lib/extract/completions/analyzeSchemaAndPrompt.ts:13)让 LLM 判断:schema 里有没有"装着大量同类项的数组"?有就标 isMultiEntity,并挑出哪些 key 是多实体(multiEntityKeys)。这决定后面走两条完全不同的路:
- 多实体(
extraction-service.ts:328):spreadSchemas把 schema 拆成"单答案部分 + 多实体部分",多实体部分把抓到的文档切成 50 个一块(chunkSize),每块调batchExtractPromise逐个抽,再deduplicateObjectsArray去重、mergeNullValObjs合并空值。 - 单答案(
extraction-service.ts:688):所有文档抓完喂给singleAnswerCompletion(:862)一次性抽出。
③ 选 URL 里藏着 Map+reranker 的复用。 processUrl(lib/extract/url-processor.ts:145)对每个入口 URL:先 getMapResults 发现全站子页(复用 §3.1),再用 LLM 把可能上千条链接按与 prompt 的相关度重排+截断(url-processor.ts:345 rerankLinksWithLLM,超过 100 条还会跑第二遍)。这一步把"整站"收敛成"最可能有答案的几十页",省下大量抓取成本。
两路结果最后由 mixSchemaObjects(extraction-service.ts:928)按原始 schema 缝合成一个对象返回。
入队路径: controllers/v2/extract.ts:25 extractController 只做校验(blocklist、ZDR 拒绝),生成 extractId,saveExtract 建初始状态,addExtractJobToQueue(extract.ts:110)丢进队列,立刻返回 id;真正的抽取在独立的 extract-worker.ts 进程里跑,过程中不断 updateExtract 写步骤状态供轮询。
一处诚实的实现细节:
extraction-service.ts里的performExtraction是 fire-1 谱系的参考实现,但在本 commit,它没有export、也没有被 worker 直接调用——extract-worker.ts:59实际跑的是同族的performExtraction_F0(fire-0,在lib/extract/fire-0/extraction-service-f0.ts)。两者结构高度一致(拆 schema→选 URL→抓→抽→合并),本节按performExtraction讲清算法骨架;运行时默认端点走的是 fire-0 变体,agent.model=v3-beta则被引导到新的/agent端点(extract.ts:52)。
3.4 Deep Research:迭代式"搜-抓-分析"直到够了
要解决的小问题: 给一个开放问题("对比 2024 年主流向量数据库"),自动去网上反复查、读、总结,最后产出一份带引用的报告——而不是一次搜索就交差。
思路: 一个带三重刹车的循环。核心在 deep-research-service.ts:26 performDeepResearch,循环体 :65:
while 未达最大深度 且 已分析URL < maxUrls:
① 生成查询 LLM 根据"当前主题+已有发现"生成 3 个 SERP 查询
② 并行搜抓 3 个查询并发 searchAndScrapeSearchResult(搜+抓全文)
③ 去重 跳过已见过的 URL,新 URL 记进 findings
④ 分析规划 LLM analyzeAndPlan:学到了啥?还有哪些 gap?要不要继续?
⑤ 定下一轮 把最大的 gap 设为下一轮主题
(随时检查:超时 / 达到 maxUrls / 连续失败 3 次 → break)
最后:generateFinalAnalysis 把所有 findings 合成报告(markdown / json)
状态与 LLM 分两个类管(research-manager.ts):
ResearchStateManager(:24):存 findings(只留最近 50 条防爆内存,:79)、seenUrls(去重)、深度、sources,每步updateDeepResearch写 Redis 供前端看进度。总步数按maxDepth * 5估(:43)。ResearchLLMService(:151):三个 LLM 动作——generateSearchQueries(据已有发现出新查询)、analyzeAndPlan(判断继续/停止、找 gap)、generateFinalAnalysis(合成报告,用o3-mini,:346)。
独特点:
- 它自己决定何时停——不是固定 N 轮,而是
analyzeAndPlan返回shouldContinue:false或没有 gap 就收尾(deep-research-service.ts:330)。这就是"迭代"和"固定爬取"的本质区别。 - 每轮的"搜+抓"直接复用 Search 那套(
searchAndScrapeSearchResult),只是写死了formats:["markdown"]、onlyMainContent、带 4 小时缓存(maxAge)。 - 计费按实际分析的 URL 数:
Math.min(urlsAnalyzed, maxUrls)(:404)。
3.5 Generate llms.txt:给整站自动写"AI 索引"
要解决的小问题: llms.txt 是给 LLM 看的站点索引(每行"标题 + 链接 + 一句话描述")。手写太累,能不能扫一遍站点自动生成?
思路——最直白的两段式(generate-llmstxt-service.ts:65 performGenerateLlmsTxt):
① Map 出 URL getMapResults(limit≤5000) 列出全站页
② 逐页抓+描述 每 10 个一批:scrapeDocument 抓正文 → LLM 写"3-4词标题+9-10词描述"
同时产出两份:llms.txt(索引行) + llms-full.txt(全文,带分页标记)
真实实现::132 调 Map 拿 URL;:153 每 10 个 URL 一批 Promise.all 并发,每页 scrapeDocument 抓 markdown 后,喂 gpt-4o-mini 用固定 prompt 生成极短标题和描述(:191);累加进 llmstxt(- [标题](url): 描述)和 llmsFulltxt。
独特点:
- 两份产物:
llms.txt(轻,只有链接+描述)和llms-full.txt(重,含全文,用<|firecrawl-page-N-lllmstxt|>分页标记分隔,返回前用removePageSeparators清掉)。 - 整站级缓存:先查
getLlmsTextFromCache,命中就按maxUrls裁剪页数/条目直接返回(:92);算完saveLlmsTextToCache存下来。 - 边算边写进度:每批完
updateGeneratedLlmsTxt更新,长任务可轮询。 - 计费按 URL 数(
:270)。
4. 巧妙之处(可借鉴的编排模式)
① 上层零抓取 代码,全靠委托。 五个端点没有一行自己的 HTML 解析——Map 复用搜索后端+索引,Search/Extract/Research/llms.txt 复用 scrapeURL。想加新能力,拼积木即可。参考 generate-llmstxt-service.ts:132(Map)+:160(scrape)+:185(LLM)三行就是一个完整的新端点骨架。
② "先便宜地选,再贵地抓"的漏斗。 Extract 的 processUrl 先 Map(不抓,便宜)发现上千链接,再 LLM reranker 收敛到几十条,才真正抓正文(url-processor.ts:345)。Deep Research 同理——先搜(便宜)再决定抓哪些。把贵操作放在漏斗最窄处。
③ 让 LLM 做"结构决策"而非只做抽取。 analyzeSchemaAndPrompt 用 LLM 判断 schema 该走多实体还是单答案两条路(extraction-service.ts:231);Deep Research 用 LLM 判断"还要不要再查一轮"(analyzeAndPlan)。LLM 在这里是调度器,不只是抽取器。
④ 同步/异步按可控性分。 Map/Search 页数受 limit 约束、可控 → 同步算完返回;Extract/Research/llms.txt 可能跑几分钟 → 入队 + 轮询 + 边跑边写进度。判断标准是"最坏耗时是否有界"。
⑤ 分页与缓存前置。 Map 的 fireEngineMap 分页拉且缓存 48 小时(map-utils.ts:246);llms.txt 整站级缓存;Deep Research 抓取带 4 小时 maxAge。上层能力天然重复访问同一批 URL,缓存收益极高。
5. 边界与局限(诚实)
- Map 不保证完整:它 靠 sitemap+搜索+索引三路拼,搜索引擎没收录、sitemap 没列、索引没抓过的页就漏。结果 ≤1 条时的"试根域名"提示(
map.ts:245)正是承认这一点。 - Extract 的 fire-1/fire-0 分叉:
extraction-service.ts的performExtraction(fire-1)在本 commit 未被 worker 调用,实际跑performExtraction_F0(fire-0,见 §3.3 诚实说明)。读代码时别把这个文件当成运行时唯一路径。 - Deep Research 受三重上限硬切:深度、
maxUrls、timeLimit任一到就停(deep-research-service.ts:65),外加连续失败 3 次熔断(research-manager.ts:31maxFailedAttempts)。findings 只留最近 50 条,超长研究会丢早期上下文。 - 搜索质量依赖后端:自托管无 fireEngine/searxng 时退到 DuckDuckGo,
fireEngineMap会warn提示"结果可能与云端不同"(search/fireEngine.ts:68)。 - 高亮是实验特性,三重门槛任一不满足就静默退回搜索引擎原始 snippet(
execute.ts:199)。
6. 代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| Map 端点 | apps/api/src/controllers/v2/map.ts | mapController |
| Map 核心(三路并发+过滤) | apps/api/src/lib/map-utils.ts | getMapResults、dedupeMapDocumentArray、queryIndex |
| Map 搜索后端调用 | apps/api/src/search/fireEngine.ts | fireEngineMap |
| Search 端点 | apps/api/src/controllers/v2/search.ts | searchController |
| Search 三段编排 | apps/api/src/search/execute.ts | executeSearch |
| 搜索后端三级回退 | apps/api/src/search/index.ts | search |
| 搜索结果选抓+合并 | apps/api/src/search/scrape.ts | getItemsToScrape、scrapeSearchResults、mergeScrapedContent |
| 高亮(实验) | apps/api/src/search/highlights.ts | applySearchHighlights、highlightsEnvReady |
| Extract 端点(入队) | apps/api/src/controllers/v2/extract.ts | extractController |
| Extract 核心 pipeline(fire-1 参考) | apps/api/src/lib/extract/extraction-service.ts | performExtraction |
| Extract 运行时(fire-0,worker 实调) | apps/api/src/lib/extract/fire-0/extraction-service-f0.ts | performExtraction_F0 |
| schema 拆分决策 | apps/api/src/lib/extract/completions/analyzeSchemaAndPrompt.ts | analyzeSchemaAndPrompt |
| URL 选取(Map+rerank) | apps/api/src/lib/extract/url-processor.ts | processUrl |
| 链接重排 | apps/api/src/lib/extract/reranker.ts | rerankLinksWithLLM |
| Extract worker 进程 | apps/api/src/services/extract-worker.ts | processExtractJob |
| Deep Research 主循环 | apps/api/src/lib/deep-research/deep-research-service.ts | performDeepResearch |
| Deep Research 状态/LLM | apps/api/src/lib/deep-research/research-manager.ts | ResearchStateManager、ResearchLLMService |
| llms.txt 生成 | apps/api/src/lib/generate-llmstxt/generate-llmstxt-service.ts | performGenerateLlmsTxt |
| Research/llms.txt worker 入口 | apps/api/src/services/queue-worker.ts | processDeepResearchJobInternal、processGenerateLlmsTxtJobInternal |