Crawl4AI 自适应爬取(Adaptive Crawling)实战:按查询生长、在信息饱和时停止的"知识电容器"
Adaptive Crawling(自适应爬取)是 Crawl4AI 中一种与传统深度爬取截然不同的思路:它不追求"爬遍全站",而是围绕一个具体查询(query)动态生长知识库,并通过覆盖率、一致性、饱和度三类统计信号判断"何时该停止"。本文基于仓库中的官方博文 adaptive-crawling-revolution.md 展开,并结合 crawl4ai/adaptive_crawler.py 的源码实现,带你完整掌握其信息论基础、两种策略(statistical / embedding)的原理与全部配置参数,以及 digest 主循环的实际工作方式。
一、什么是 Adaptive Crawling:一个"知识电容器"
Crawl4AI 的作者将 Adaptive Crawling 类比为电容器——储存信息,并在需要时精准释放。它的核心范式转变可以概括为一句话:从 "crawl everything, hope for the best"(爬取一切、听天由命)变为 "crawl intelligently, know when to stop"(智能爬取、知道何时停止)。
原文档给出的动机非常直接:许多团队误以为"用了 LLM 就等于高效",但 LLM 只是让事情"变得可能",而非"变得聪明"。将蛮力深度爬取与 LLM 处理叠加,浪费的不仅是时间,更是 token 与算力开销。原博文报告的对比数据是(引自 原文):
| 方式 | 页面数 | 有效内容 | token 成本 | 耗时 |
|---|---|---|---|---|
| 传统深度爬取 | 500 页 | 50 页有效 | $15 | 约 2 小时 |
| 自适应爬取 | 15 页 | 14 页有效 | $2 | 约 10 分钟 |
另一个更具代表性的场景是构建客服知识库:传统做法爬整个文档站(max_depth=5)得到 1,200 页、约 100 页有用;自适应做法按真实用户查询 payment processing errors refund policies 生长,45 页中 42 页全部相关。原文强调,关键不是"爬得更少",而是"爬得对"(crawling right)。
在 Crawl4AI 中,这套能力由 crawl4ai 包直接导出,导入方式见 crawl4ai/init.py:
from crawl4ai import AsyncWebCrawler, AdaptiveCrawler, AdaptiveConfig
二、信息论基础:三个智能支柱
原博文的第一原则是"纯统计,没有魔法"——先用经典统计方法,不依赖 embedding 或 LLM。仓库源码中这一原则落在 StatisticalStrategy 类上,它维护词频(term frequency)、文档频率(document frequency)两张表,并用三个指标构成置信度。这三个指标即原文所称的"Three Pillars of Intelligence"。
2.1 Coverage(覆盖率):广度传感器
回答的不是"有没有页面",而是"有没有对的页面"。源码实现位于 adaptive_crawler.py 的 _calculate_coverage:
- 对查询分词,统计每个查询词在知识库中的文档覆盖率
doc_coverage = df / total_documents; - 叠加归一化对数频率信号
freq_signal = log(1+tf) / log(1+max_tf); - 合成
term_score = doc_coverage * (1 + 0.5 * freq_signal),对所有查询词取平均后开平方根(平方根曲线使"部分覆盖"与"良好覆盖"更易区分),最终截断在[0, 1]。
2.2 Consistency(一致性):连贯性检测器
多来源的信息应当相互印证。源码(adaptive_crawler.py)用成对文档的 Jaccard 相似度衡量:
overlap = len(terms_i & terms_j) / len(terms_i | terms_j)
consistency = sum(overlaps) / len(overlaps)
页面之间共识越多,置信度越高;若彼此冲突(重叠低),系统会推断还需要更多数据。
2.3 Saturation(饱和度):效率守护者
原文称之为"最关键的指标":当新页面不再贡献新信息时就停止爬取。源码(adaptive_crawler.py)通过 new_terms_history(每页新增词数)计算:
saturation = 1 - (recent_rate / initial_rate)
初期每页带来大量新词,近期速率下降则饱和度逼近 1,触发停止。
2.4 加权置信度与停止条件
calculate_confidence(adaptive_crawler.py)将三者加权合成总置信度(源码中硬编码了默认权重):
confidence = 0.4 * coverage + 0.3 * consistency + 0.3 * saturation
注意 AdaptiveConfig 中同时提供了 coverage_weight=0.4、consistency_weight=0.3、saturation_weight=0.3 三个可配置权重(见 AdaptiveConfig),但当前 calculate_confidence 使用的是固定权重——从源码结构看,配置权重主要起声明与校验作用(validate() 会断言三者之和为 1),实际调用仍以硬编码值计算。
统计策略的停止判定 should_stop(adaptive_crawler.py)有四个条件,满足任一即停:
confidence >= confidence_threshold(默认 0.7);crawled_urls >= max_pages(默认 20);- 待爬队列
pending_links为空; saturation >= saturation_threshold(默认 0.8)。
三、"网络爬取的 A*":信息气味与链接评分
原文把这套机制称为 "information scenting"——像 A* 寻路一样,不随机追随每个链接,而是按"对当前及未来查询贡献有意义信息的概率"排序。这正是博文中的信息增益思想在源码里的落地:
# 信息增益计算——自适应爬取的核心(原文示意)
def calculate_information_gain(new_page, knowledge_base):
new_terms = extract_terms(new_page) - existing_terms(knowledge_base)
overlap = calculate_overlap(new_page, knowledge_base)
gain = len(new_terms) / (1 + overlap) # 新词多、重叠少 → 增益高
return gain
源码中,StatisticalStrategy.rank_links(adaptive_crawler.py)对每个候选链接计算三个分量:
score = (config.relevance_weight * relevance + # 默认 0.5
config.novelty_weight * novelty + # 默认 0.3
config.authority_weight * authority) # 默认 0.2
- relevance:链接预览文本(text/title/meta 的 title、description、keywords)与查询的匹配度;若链接在爬取阶段已做过 BM25 上下文打分(
link.contextual_score),则直接采用;否则退化为查询词重叠率; - novelty:链接文本中有多少比例是新词(
link_terms - existing_terms),未知时给 0.5; - authority:权重存在但当前恒取 1.0(
_calculate_authority已注释停用,adaptive_crawler.py)。
链接预览数据来自 _crawl_with_preview(adaptive_crawler.py),它在每次 arun 时挂载 LinkPreviewConfig(include_internal=True, query=query, concurrency=5, timeout=link_preview_timeout, max_links=50) 并开启 score_links=True,让 Crawl4AI 的链接预览机制为每个内链抓取 head 数据并用查询词做 BM25 打分——这就是"气味"的来源。
四、两种策略:统计基线与嵌入语义增强
4.1 Statistical 策略:精确词、快而字面
strategy="statistical" 是默认策略,纯统计、零模型依赖,适合查询词与页面用词高度一致的场景。StatisticalStrategy 还内置了 BM25 参数(k1=1.2、b=0.75,见 adaptive_crawler.py)作为相关性打分的备选路径。
4.2 Embedding 策略:语义空间中的覆盖
EmbeddingStrategy(adaptive_crawler.py)在统计基线之上叠加语义理解,其关键步骤是 map_query_semantic_space(adaptive_crawler.py):
- 用对话模型把查询扩成
n_query_variations个语义变体(多生成 30% 用于留出验证集,80/20 划分,原查询始终留在训练侧); - 用
embedding_model(默认sentence-transformers/all-MiniLM-L6-v2)嵌入这些变体,得到查询的"语义邻域点云"; - 用带缓存的向量化余弦距离矩阵(
_compute_distance_matrix)衡量知识库文档对查询点的覆盖。
由此实现博文所说的"自动扩展查询、映射覆盖空间、智能识别缺口":对 authentication oauth 这类查询,它能理解 "auth"、"login"、"SSO" 属于同一语义区域,而不是字面匹配。
4.3 无关检测:知道何时"认输"
原文强调的最实用特性是:embedding 策略知道何时放弃。源码中它对应 EmbeddingStrategy.should_stop(adaptive_crawler.py)的最低相关性阈值检查:
# 检查置信度是否低于最低阈值(完全不相关)
if confidence < min_confidence_threshold and len(state.crawled_urls) > 0:
state.metrics['stopped_reason'] = 'below_minimum_relevance_threshold'
state.metrics['is_irrelevant'] = True
return True
这就是博文示例中"用意大利面查询爬 Python 官方文档,置信度 5%(低于阈值),仅爬 2 页即停,stopped_reason 为 below_minimum_relevance_threshold"的底层实现。阈值由 embedding_min_confidence_threshold(默认 0.1)控制,可通过 state.metrics 读取停止原因。
五、digest 主循环:一次完整的自适应爬取
所有策略最终由 AdaptiveCrawler(adaptive_crawler.py)编排。构造函数会调用 config.validate() 校验参数合法性,再按 config.strategy 创建对应策略。核心入口 digest(start_url, query, resume_from=None) 的循环结构(adaptive_crawler.py)如下:
- 初始化/恢复状态:若传
resume_from,从 JSON 文件反序列化CrawlState(支持断点续爬),否则新建; - 查询空间扩展(仅 embedding 策略且非恢复时):执行
map_query_semantic_space,把查询变体与嵌入存入state.query_embeddings; - 初始爬取:用
LinkPreviewConfig爬取start_url,把成功的CrawlResult加入知识库,内链(dict 或 Links 对象两种形态均兼容)进入pending_links; - 循环(depth < max_depth):
calculate_confidence计算当前置信度;should_stop判定停止(含上面各类阈值);rank_links对候选链接评分,若最高分低于min_gain_threshold(默认 0.1)则停止——"增益太低不值得再爬";- 取 top
top_k_links(默认 3)个链接,_crawl_batch用asyncio.gather并行爬取,失败的 URL 被过滤并打印; - 新知识并入知识库,新内链去重后追加到待爬队列,
update_state更新词频/嵌入等统计量; - 若配置
save_state,每轮落盘一次state_path;
- 收尾:计算最终置信度(embedding 策略额外走
get_quality_confidence映射为 0.7–0.95 区间的"质量置信度"),写入pages_crawled、depth_reached指标后返回CrawlState;若AdaptiveCrawler自己创建的AsyncWebCrawler,会在finally中确保释放浏览器资源。
CrawlState(adaptive_crawler.py)承载全部运行态:已爬 URL 集合、知识库、待爬链接、TF/DF 表、new_terms_history,以及 embedding 策略专用的 kb_embeddings、query_embeddings、expanded_queries、semantic_gaps。其 save/load 以 JSON 持久化(numpy 数组转 list 存储),这为知识库的跨会话生长与断点恢复提供了基础。
六、AdaptiveConfig 全参数速查
以下默认值直接来自 AdaptiveConfig 的源码注释,validate()(adaptive_crawler.py)会对全部参数做断言校验。
核心控制
| 参数 | 默认值 | 说明 |
|---|---|---|
confidence_threshold |
0.7 | 置信度达到该值即停止 |
max_depth |
5 | 扩展轮数上限(每轮爬一批 top-k 链接) |
max_pages |
20 | 最大爬取页面数 |
top_k_links |
3 | 每轮选取的最高分链接数 |
min_gain_threshold |
0.1 | 候选链接最高分低于此值则停止 |
strategy |
"statistical" |
statistical / embedding(其他取值抛 ValueError) |
三支柱权重与链接评分权重(两组权重各自必须和为 1)
| 参数 | 默认值 | 说明 |
|---|---|---|
coverage_weight |
0.4 | 覆盖率权重 |
consistency_weight |
0.3 | 一致性权重 |
saturation_weight |
0.3 | 饱和度权重 |
relevance_weight |
0.5 | 链接相关性权重 |
novelty_weight |
0.3 | 链接新信息权重 |
authority_weight |
0.2 | 链接权威性权重 |
saturation_threshold |
0.8 | 饱和度停止阈值 |
consistency_threshold |
0.7 | 一致性阈值 |
Embedding 策略专属
| 参数 | 默认值 | 说明 |
|---|---|---|
embedding_model |
sentence-transformers/all-MiniLM-L6-v2 |
本地嵌入模型(可经 embedding_llm_config 改用 LLM 嵌入 API) |
n_query_variations |
10 | 查询语义变体数量(另多生成 30% 做验证) |
coverage_threshold |
0.85 | 覆盖目标 |
alpha_shape_alpha |
0.5 | 覆盖形状参数(高维下退化为质心+半径统计模型) |
embedding_min_confidence_threshold |
0.1 | 低于该值判定"完全不相关"并立即停止 |
embedding_coverage_radius |
0.2 | 查询点被视为"已覆盖"的余弦距离半径,越小要求越严 |
embedding_k_exp |
1.0 | 距离→得分的指数衰减系数,score = exp(-k * distance) |
embedding_nearest_weight |
0.7 | 最近邻在混合得分中的权重(与 top-k 权重之和须为 1) |
embedding_top_k_weight |
0.3 | top-k 平均得分权重 |
embedding_overlap_threshold |
0.85 | 与知识库相似度超此值的冗余链接会被降权 |
embedding_min_relative_improvement |
0.1 | 每批次的最小相对提升,低于则停 |
embedding_validation_min_score |
0.3 | 验证分低于此值不信任收敛,防止过早停止 |
link_preview_timeout |
5.0 | 链接预览超时(秒) |
持久化与 LLM 配置
| 参数 | 默认值 | 说明 |
|---|---|---|
save_state / state_path |
False / None |
每轮把 CrawlState 写入 JSON,配合 digest(resume_from=path) 断点续爬 |
embedding_llm_config |
None |
嵌入 API 配置(LLMConfig 或 dict);为 None 时用本地 sentence-transformers |
query_llm_config |
None |
查询扩展(对话补全)配置;缺省时回退到 embedding_llm_config,再缺省用内置默认 provider |
七、实战:按需求生长的知识库
7.1 最小可运行示例
这是原文 "Try It Yourself" 的完整代码,参数与源码默认值一一对应:
from crawl4ai import AsyncWebCrawler, AdaptiveCrawler, AdaptiveConfig
async with AsyncWebCrawler() as crawler:
# 选择策略
config = AdaptiveConfig(
strategy="embedding", # 或 "statistical"
embedding_min_confidence_threshold=0.1 # 低于此值判定无关并停止
)
adaptive = AdaptiveCrawler(crawler, config)
result = await adaptive.digest(
start_url="https://your-docs.com",
query="your users' actual questions"
)
adaptive.print_stats()
print(f"Found {adaptive.confidence:.0%} of needed information")
print(f"In just {len(result.crawled_urls)} pages")
注意两个策略的典型表现差异(原文给出的对比示例):查询 authentication oauth 时,统计策略按精确词搜索、爬 12 页、置信度 78%,"快但字面";embedding 策略理解 "auth/login/SSO" 的同义关系,爬 8 页、置信度 92%。
7.2 观察"动态生长"
原文用了一个漂亮的比喻:知识库像过饱和溶液中的晶体——加一个查询(种子),相关信息围绕它结晶;换查询,知识结构随之调整。用两个不同查询连续 digest 同一站点:
# 周一:客户问认证
auth_knowledge = await adaptive.digest(
"https://docs.api.com",
"oauth jwt authentication"
)
# 周二:客户问限流——爬虫在已有知识上继续生长
rate_limit_knowledge = await adaptive.digest(
"https://docs.api.com",
"rate limiting throttling quotas"
)
配合 save_state=True, state_path="./kb_state.json",第二次可以用 resume_from 直接恢复第一次的状态,而不是从零爬起。
7.3 查看统计与导出知识库
adaptive.print_stats(detailed=False):基于 rich 表格输出页数、唯一词数、内容长度、置信度、coverage/consistency/saturation(统计策略)或 validation score/Is Sufficient(embedding 策略);detailed=True还会打印 top 20 词、逐 URL 的新增词数、DF 分布与查询空间样本(adaptive_crawler.py);adaptive.coverage_stats属性返回同一指标的 dict 形式,便于程序化消费;adaptive.is_sufficient:统计策略比较confidence >= confidence_threshold,embedding 策略以"验证集是否通过"为准;export_knowledge_base(filepath, format="jsonl")/import_knowledge_base(filepath)(adaptive_crawler.py):以 JSONL 导出/导入知识库,每条记录含 url、markdown 内容、链接与爬取元数据(爬取顺序、爬取时置信度),方便把知识库喂给下游 LLM 或 RAG 管线。
更多可运行的示例脚本可参考仓库的 docs/examples/adaptive_crawling/ 目录(basic_usage.py、custom_strategies.py、embedding_configuration.py 等),配套 API 文档见 docs/md_v2/core/adaptive-crawling.md。
八、渐进式路线图:统计 → 嵌入 → LLM
原文给出了 Adaptive Crawling 的三阶段路线,与源码现状可以互相印证:
- Phase 1(已实现):统计基础——纯信息论,不依赖昂贵模型,
StatisticalStrategy即其落点; - Phase 2(当前可用):嵌入增强——在统计基线上叠加语义理解,可选(
strategy="embedding"),默认模型为本地小模型all-MiniLM-L6-v2,不引入 LLM 成本即可工作; - Phase 3(规划中):LLM 集成——LLM 只用于复杂推理、"外科手术式"使用,且始终架在统计基础之上。从源码结构看,
query_llm_config让 LLM 目前仅参与查询扩展(对话补全),链接选择与停止判定仍由统计/向量信号完成——这与"LLM 不浪费在批量处理上"的理念一致。
仓库根目录还有一篇数学框架长文 PROGRESSIVE_CRAWLING.md,是对同一思想的更完整推导,适合作延伸阅读。
九、总结:处理"对"的数据,而不是处理更多数据
Adaptive Crawling 的设计哲学可以浓缩为原博文结尾的那句话:Grow knowledge on demand. Stop when you have enough.(按需生长知识,够了就停。)落到工程上,它是三件事的组合:
- 用 TF/DF、Jaccard、词发现速率等统计信号量化"够不够"(coverage / consistency / saturation);
- 用查询感知的链接预览 + 信息增益排序决定"先爬谁"(relevance / novelty 加权评分);
- 用
digest循环把两者闭环起来,并让 embedding 策略在语义空间里进一步识别覆盖缺口、检测无关查询。
对维护文档站知识库、API 文档 RAG、竞品资料监测这类"查询驱动"的采集场景,这套机制把"爬满 1,000 页"的问题转化为"回答一个查询需要多少页"的问题——这也正是 Crawl4AI 中 AdaptiveCrawler 区别于 crawl_bfs/dfs 等既有深度爬取策略的核心价值。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00