搜索引擎层:两阶段检索、引擎池与过滤
30 秒导读: 研究策略里那句
self.search.run(q)(见 01)看着只是"搜一下",背后其实是一整套机制:LDR 有 30 多个搜索源(arXiv、PubMed、SearXNG、Tavily、GitHub、本地文档库……),接口、返回格式、限流规则各不相同。本章讲清 LDR 怎么把它们统一成同一个run()契约,以及这个run()内部的两阶段检索(先便宜后贵)、引擎装配(工厂+注册表)、结果过滤(三层去噪)、自适应限流(学等待时间)。
1. 这节讲什么(先建立直觉)
一句话: 这一层是 LDR 的"搜索适配器 + 检索流水线"。它把任何一个搜索源都包成一个 BaseSearchEngine 子类,对外只暴露一个方法 run(query) -> List[dict];方法内部统一走"两阶段检索 + 过滤 + 限流"。
它要解决的真实痛点: 想象你要接 30 个搜索 API。有的返回 JSON,有的要爬 HTML;有的自带好排序(Google),有的只按关键词命中(arXiv);有的免费,有的按次收费还限流。如果每个策略都去直接调这些 API,代码会炸成一团。LDR 的做法是:所有差异都收进子类,策略层只面对一个干净的 run()。
本章边界(不越界):
| 不讲什么 | 去哪看 |
|---|---|
| 策略层的研究主循环、怎么决定搜什么 | 01-research-engine-and-strategies.md |
| LangGraph 智能体怎么把引擎当工具动态挑选 | 03-langgraph-agent-strategy.md |
| egress 出站管制策略的判定 细节(scope、PDP) | 06-security-egress-library-news.md |
本章只讲:run() 内部发生了什么、引擎怎么被分类和装配、结果怎么被过滤、限流怎么自适应。egress 校验在本章只作为"流水线里的一站"点到为止,判定逻辑留给 06。
2. 顶层全景:一次 run(query) 的旅程
先看大盘。一个策略拿到某个引擎实例后调 engine.run("量子纠错 2024 进展"),内部像这样流动(从上到下是时间顺序,命中失败即降级):
engine.run(query) ← 唯一对外入口 (search_engine_base.py:594)
│
┌─────────────────┴──────────────────┐
│ ① egress 出站校验 (放行才继续) │ _verify_egress_scope → 细节见 06
└─────────────────┬──────────────────┘
│
┌─────────────────┴──────────────────┐
│ ② tenacity 重试壳 (限流才重试) │ @retry + AdaptiveWait
└─────────────────┬──────────────────┘
│ 每次尝试跑一遍 _execute_search:
▼
┌────────────────────────────────────────────────────┐
│ 阶段一 · 便宜 │
│ ③ _get_previews(query) 抓一批"预览"(标题+摘要) │ ← 子类必须实现
│ ④ (科学引擎) DOI → OpenAlex 源 ID 富化 │
│ ⑤ 预览过滤器 preview_filters (如期刊声誉) │
│ ⑥ LLM 相关性过滤 _filter_for_relevance (可选) │
└───────────────────────────┬────────────────────────┘
│ 只有留下来的项才进阶段二
▼
┌────────────────────────────────────────────────────┐
│ 阶段二 · 贵 │
│ ⑦ _get_full_content(kept) 抓全文(爬网页/下 PDF) │ ← 子类可覆盖
│ ⑧ 内容过滤器 content_filters │
└───────────────────────────┬────────────────────────┘
▼
⑨ 记录 metrics + 学限流 等待时间 → 返回 List[dict]
部件一句话职责:
| 部件 | 干什么 | 文件 |
|---|---|---|
BaseSearchEngine | 抽象基类,定义 run() 骨架与两阶段契约 | web_search_engines/search_engine_base.py |
各 *SearchEngine 子类 | 实现 _get_previews / _get_full_content,吸收源差异 | web_search_engines/engines/*.py |
create_search_engine | 工厂:按引擎名+设置装配出实例 | web_search_engines/search_engine_factory.py |
ENGINE_REGISTRY | 注册表:引擎名 → 实现类的硬编码映射 | web_search_engines/engine_registry.py |
filter_previews_for_relevance | 引擎内 LLM 相关性过滤 | web_search_engines/relevance_filter.py |
CrossEngineFilter | 策略层:合并多引擎结果后统一重排/重编号 | advanced_search_system/filters/cross_engine_filter.py |
AdaptiveRateLimitTracker | 记录成功/退避,学每个引擎的等待时间 | web_search_engines/rate_limiting/tracker.py |