Rethinking Agentic Search with Pi-Serini: Is Lexical Retrieval Sufficient?

Paper: arXiv:2605.10848 Code: justram/pi-serini Code reference: main @ bbab25da (2026-05-12)

1. Motivation (研究动机)

这篇论文要回答一个很具体的问题:在 Deep Research / Agentic Search 里,如果 LLM 已经足够会推理、会调用工具,那么检索器本身是否还必须是 dense retriever 或专门训练的 reasoning retriever?作者的怀疑是,已有 BM25 baseline 可能不是“lexical retrieval 不行”,而是接口和配置太弱:通常只返回浅层 top-、把少量文档全文或摘要直接塞进上下文,导致 retrieval depth、context management 和 evidence selection 被混在一起。

传统 RAG 往往是一轮检索后生成;Deep Research 则需要多轮 query 改写、查看候选、读文档、整合证据。已有 BrowseComp-Plus baseline 多数用单一 retriever tool,一次返回 top-5 结果。这样的接口会让 BM25 的一个核心优势——廉价地返回很深的排名列表——发挥不出来,因为模型既看不到深层候选,也没有“翻页/缓存/按 docid 读取”的操作空间。

Figure 1 解读:横轴是成本、纵轴是答案准确率。红色虚线给出 Pi-Serini 族的 Pareto frontier:deepseek-v4-flash 成本最低,gpt-5.5 准确率最高。灰点是 prior work,右侧高成本 dense-retriever 或 coding-agent 方案并不总是带来更高准确率。这个图支撑论文动机:瓶颈不一定在“检索模型是否足够神经化”,也可能在 agent 如何与检索结果交互。

值得研究的原因有两层。第一,如果 BM25 在正确接口下已经足够强,就能显著降低 deep research 系统的工程复杂度和成本,不必为每个动态 corpus 维护 embedding pipeline。第二,这会改变 Search/RAG Agent 的优化方向:重点从“换更复杂的 retriever”转向“让 agent 能够更深、更可控地浏览检索结果”。

2. Idea (核心思想)

核心 insight:lexical retrieval 的上限不应在一次 top-5 工具调用里判断;当 BM25 被调到适合长文档、检索深度足够大,并且 agent 拥有搜索、翻页和按文档读取三种分离动作时,它可以为强 LLM 提供足够高召回的 evidence pool。

Pi-Serini 的创新不是发明新检索模型,而是把 BM25 放进一个最小但可控的 agentic loop:search 负责生成并缓存最多 1000 个 ranked hits,read_search_results 负责浏览同一个缓存排名的更深位置,read_document 负责按 docid 和行号读取文档。这样 retrieval depth 不再等于上下文长度,LLM 可以先看到摘要/片段,再决定是否打开全文。

与 Chen et al. 的 BrowseComp-Plus released agents 相比,Pi-Serini 不使用一次性 top-5 retriever tool;与 qwen3-embed-8b / AgentIR 这类 dense 或训练式 retriever 相比,它保留 BM25 的简单性,但把 agent-facing interface 做成更接近“搜索引擎 + 翻页 + 打开网页”的操作过程。与 file-system coding-agent 方法相比,它不假设相关文档已被预先局部化到一个可 rg/sed/nl 浏览的小工作集,而是保持真实检索场景中的大 corpus 访问边界。

3. Method (方法)

3.1 整体框架:ReAct loop + retrieval controller + Anserini BM25

作者把 deep research 写成一个 ReAct-style 交互轨迹: 其中 是第 轮 reasoning trace, 是 agent 选择的 action, 是环境返回的 observation。策略形式是: 中间轮次 可以继续推理或调用工具,最终动作 生成答案 并终止。Pi-Serini 的关键是把“LLM agent”和“search engine”之间加一个 retrieval controller,把 BM25 排名缓存、分页、文档读取和 spill file 管理都隔离在 controller 中。

Figure 4 解读:左侧是输入问题、system prompt 和 time budget;中间 LLM agent 执行 ReAct loop;蓝色的 retrieval controller 是隔离点,暴露 search / browse / read 三类 Tool API,并维护 cache、pagination、spill files;右侧 Anserini BM25 使用 。这张图强调 Pi-Serini 的贡献在 agent-search interface,而不是替换 BM25 排名函数。

直觉上,Pi-Serini 有效是因为它把“找得到”和“读得下”拆开了。BM25 可以廉价返回很深的候选列表,但 LLM 不能一次读完 1000 篇长文档;controller 先缓存深排名,再只把 top page、后续 page 或特定 document chunk 放入上下文,让 agent 用多轮行动把 token budget 花在更可能有用的证据上。

3.2 三个检索工具

  • search(reason, query): 输入一个简短 lexical query;后端用 plain BM25 查询 BrowseComp-Plus corpus,最多返回 1000 个 hits;controller 把完整 ranking 缓存在 session-local search_id 下,但首次只展示 ranks 1—5。
  • read_search_results(reason, search_id, offset=6, limit=10): 不重新查询后端,而是在已有 cached ranking 上分页浏览,返回 rank page、displayed docids、next_offset 和截断元数据。
  • read_document(reason, docid, offset=1, limit=200): 按 docid 读取一个后端文档,以 line-based chunks 返回;如果输出太长,返回 line range、total line count、next_offset 和 truncation metadata。

这三个动作分别对应“检索候选”“浏览排名”“读取证据”。如果移除 read_search_results,agent 无法低成本看深层 ranking;如果移除 read_document,agent 只能依赖短 excerpt;如果 search 直接塞全文,深度 又会被上下文窗口限制抵消。

3.3 轨迹日志与多层 evidence recall

Pi-Serini 不只记录最终答案,还记录每个 query 的完整 tool-call / reasoning trajectory,并维护四类文档集合: 含义分别是:search 返回过的文档、通过 read_search_results 展示过 excerpt 的文档、通过 read_document 打开过全文片段的文档、最终答案引用的文档。实验中的 retrieval behavior metric 定义为: 这种分层很重要:surfaced recall 高表示 retriever 已把证据放进缓存排名;previewed recall 高表示 agent 实际看到了相关 excerpt;behavior recall 高才表示 agent 打开或引用了相关文档。论文的主要结论正是:BM25 可以把 surfaced recall 做得很高,但 previewed/behavior 仍受 agent 导航策略限制。

3.4 Time-budget steering

Pi-Serini 不用固定 iteration cap,而是给每个 query 一个 wall-clock timeout 。主实验 秒;在 时系统注入 submit-now steer,要求 agent 停止使用工具、用已收集证据立即提交答案;从此之后 searchread_search_resultsread_document 都被 block。如果到 仍未提交,该 query 标记为 timed out。

这个设计匹配真实 deep research 服务:用户关心延迟和成本,不只是最多调用多少轮。固定 100 iterations 可能让简单问题浪费时间,也可能让困难问题在错误分支上继续走;timeout policy 把优化目标转成“在时间预算内尽早形成足够答案”。

3.5 BM25 配置与长文档适配

BrowseComp-Plus 文档很长:平均 5,179.2 words / 32,296.2 characters。论文固定使用 Anserini BM25,参数 ,retrieval depth 设为 1000。作者指出 Anserini default 更偏短文档;长文档 evidence search 需要更高 term-frequency saturation 和更强 length normalization。

Figure 3 解读:颜色越亮表示 recall 越高,红叉是 Anserini default ,蓝星是 grid search 最优附近 。论文主实验采用 ,与高 /高 区域一致。这个结果说明 default BM25 在 BrowseComp-Plus 长文档上偏 underfit,调参本身就能带来大幅 recall/accuracy 提升。

论文公式与 released code 实现差异:论文附录说明其实验 setup 对应代码库 commit 68c5e0f,而本笔记检查的是公开仓库当前 main@bbab25da。核心工具接口、cache、pagination、time-budget policy 仍一致;但当前 repo 的底层 BM25 默认值仍是 PI_BM25_K1=0.9PI_BM25_B=0.4,README / docs/running-benchmarks.md 才把 BrowseComp-Plus 复现实验参数写成 PI_BM25_K1=25PI_BM25_B=1。因此复现论文数值时必须显式设置这些 env 或读取 run artifact 中的 run_setup.json,不能把 bare default 当成论文配置。

3.6 基于 released code 的伪代码

Code reference: main @ bbab25da (2026-05-12) — pseudocode and mapping based on this commit

search:创建 cached ranking 并展示第一页

from dataclasses import dataclass
 
@dataclass
class CachedSearch:
    search_id: str
    query: str
    query_mode: str
    hits: list
 
class SearchSessionStore:
    def __init__(self, max_cached_searches: int = 32):
        self.cache = {}
        self.counter = 0
        self.max_cached_searches = max_cached_searches
 
    def create_search(self, query: str, query_mode: str, hits: list) -> CachedSearch:
        self.counter += 1
        search_id = f"s{self.counter}"
        cached = CachedSearch(search_id, query, query_mode, hits)
        self.cache[search_id] = cached
        while len(self.cache) > self.max_cached_searches:
            self.cache.pop(next(iter(self.cache)))
        return cached
 
 
def execute_search_tool(backend, store: SearchSessionStore, query: str):
    raw_query = query.strip()
    if not raw_query:
        raise ValueError("search.query must be non-empty")
 
    response = backend.search(query=raw_query, query_mode="plain", max_results=1000)
    cached = store.create_search(raw_query, "plain", response.hits)
    first_page = build_search_page(cached, offset=1, limit=5)
    return truncate_or_spill(format_search_page_text(first_page), full_json=first_page)

read_search_results:在同一 ranking 上分页浏览

def read_search_results(store: SearchSessionStore, search_id: str, offset: int | None = None, limit: int | None = None):
    offset = offset or 6
    limit = limit or 10
    cached = store.cache.get(search_id)
    if cached is None:
        raise ValueError("unknown search_id; call search(...) first")
 
    page = build_search_page(cached, offset=offset, limit=limit)
    # page includes returned rank range, displayed docids, and next_offset.
    return truncate_or_spill(format_search_page_text(page), full_json=page)

read_document:按 docid 分块读取文档

def read_document(backend, docid: str, offset: int | None = None, limit: int | None = None):
    offset = offset or 1
    limit = limit or 200
    response = backend.read_document(docid=docid, offset=offset, limit=limit)
    if not response.found:
        raise ValueError("docid not found; choose a docid from search results")
 
    rendered = format_document_lines(
        docid=docid,
        lines=response.lines,
        returned_start=response.returned_offset_start,
        returned_end=response.returned_offset_end,
        total_lines=response.total_units,
        next_offset=response.next_offset,
    )
    return truncate_or_spill(rendered, full_json=response)

time-budget policy:0.7T 后注入 submit-now steer 并禁用工具

def get_submit_now_delay_ms(timeout_seconds: float | None):
    if timeout_seconds is None or timeout_seconds <= 0:
        return None
    return max(1, int(timeout_seconds * 0.7 * 1000))
 
 
def run_query_with_time_budget(agent, timeout_seconds: int = 300):
    submit_delay = get_submit_now_delay_ms(timeout_seconds)
    tools_blocked = False
 
    def on_submit_now_timer():
        nonlocal tools_blocked
        tools_blocked = True
        agent.queue_user_steer(
            "Time budget is nearly exhausted. Stop using tools immediately "
            "and submit your best answer right now."
        )
 
    schedule_timer(submit_delay, on_submit_now_timer)
    return agent.run_until_final_answer_or_timeout(
        timeout_seconds=timeout_seconds,
        before_tool_call=lambda tool_name: not tools_blocked,
    )

retrieval behavior 评估:从 run artifact 聚合 docid 集合

def evaluate_retrieval_behavior(run_record, evidence_docids, gold_docids):
    surfaced = set(run_record["surfaced_docids"])
    previewed = set(run_record["previewed_docids"])
    opened = set(run_record["opened_docids"])
    cited = set(run_record["cited_docids"])
    behavior = opened | cited
 
    def recall(observed, relevant):
        return len(observed & set(relevant)) / max(1, len(relevant))
 
    return {
        "surfaced_evidence": recall(surfaced, evidence_docids),
        "surfaced_gold": recall(surfaced, gold_docids),
        "previewed_evidence": recall(previewed, evidence_docids),
        "previewed_gold": recall(previewed, gold_docids),
        "behavior_evidence": recall(behavior, evidence_docids),
        "behavior_gold": recall(behavior, gold_docids),
    }

3.7 Code-to-paper mapping

Code reference: main @ bbab25da (2026-05-12) — pseudocode and mapping based on this commit

Paper ConceptSource FileKey Class/Function
Pi-Serini extension registration and tool surfacesrc/pi-search/extension.tsregisterPiSearchExtension, pi.registerTool for search, read_search_results, read_document
Search/cache/pagination tool behaviorsrc/pi-search/tool_handlers.ts, src/pi-search/search_cache.tsexecuteSearchTool, executeReadSearchResultsTool, executeReadDocumentTool, SearchSessionStore, buildSearchPage
Time-budget steeringsrc/pi-search/prompt_policy.ts, src/pi-search/extension.tsBENCHMARK_TIMEOUT_SECONDS, SUBMIT_NOW_TRIGGER_RATIO=0.7, getSubmitNowDelayMs, tool-blocking logic
BM25 backend integrationsrc/extensions/pi_search.ts, src/search-providers/anserini/pi_search_backend_factory.ts, src/pi-search/searcher/adapters/anserini_bm25/adapter.tswrapper that injects Anserini BM25 backend into the package-owned pi-search contract
BrowseComp-Plus benchmark definitionsrc/benchmarks/browsecomp_plus.tsbrowsecompPlusBenchmark, query sets q9/q100/q300/qfull, qrels, ground-truth, index path
Query-set launch and experiment config surfacesrc/orchestration/benchmark_query_set_launch.ts, src/orchestration/query_set_shared_bm25.ts, docs/running-benchmarks.mdTIMEOUT_SECONDS=300 default in query-set launch; BM25 env PI_BM25_K1, PI_BM25_B
Run artifacts / retrieval behavior loggingsrc/orchestration/run_pi_benchmark.tsemitted surfaced_docids, previewed_docids, opened_docids, cited_docids, tool-call counts
Answer judge and calibrationsrc/evaluation/evaluate_run_with_pi.ts, src/evaluation/calibration.ts, src/evaluation/judge_prompt.tsgold-answer judge with openai-codex/gpt-5.3-codex; confidence parsing; calibration error

4. Experimental Setup (实验设置)

4.1 Dataset

主实验使用 BrowseComp-Plus:830 个 deep research queries,100,195 篇文档。平均每个 query 有 6.1 篇 evidence documents 和 2.9 篇 gold documents;每篇文档平均 5,179.2 words / 32,296.2 characters。Evidence documents 是回答问题所需文档,gold documents 是更严格的子集,既支持回答又语义上包含最终答案。

4.2 Baselines

比较对象包括:Chen et al. (2025) released agents:o3 / gpt-5 + BM25qwen3-embed-8b;Meng et al. (2026):gpt-5.2 + qwen3-embed-8b;Chen et al. (2026) AgentIR:Tongyi-DR + AgentIR-4B。这些 baseline 多数使用单一 retriever tool,直接返回 top- search results,且 。Figure 1 还把 file-system coding-agent 方法作为参考点画出,但因为它们假设文档子集已被本地 materialized,不纳入 Table 1 的同设定比较。

4.3 Metrics

答案质量使用 gold-answer LLM judge:judge 接收 question、agent final response 和 benchmark correct answer,判断 extracted final answer 是否与 correct answer 语义等价;默认 judge model 是 openai-codex/gpt-5.3-codex,JSON mode,180 秒 timeout。表中的 Acc. 是答案准确率;Calib. 来自代码中的 calibration error,把 final response 的自报 Confidence: 与 correctness 分桶后计算误差,越低越好;Cost ($) 按附录 token pricing 汇总。

Retrieval behavior 使用三层 recall:Surfaced Recall 基于 ,Previewed Recall 基于 ,Behavior Recall 基于 。每层都分别对 evidence docs 和 gold docs 计算 recall。

4.4 Model / hardware / hyperparameters

论文评测多个 frontier LLM families:DeepSeek 的 deepseek-v4-flash / deepseek-v4-pro,Anthropic 的 claude-haiku-4.5 / claude-opus-4.7,OpenAI 的 gpt-5 / gpt-5.2 / gpt-5.4-mini / gpt-5.4 / gpt-5.5。Agent harness 使用 Pi,不启用 sub-agents 或额外 orchestration modules;移除 Pi 默认 system prompts 和 built-in tools 后,注入本文的 deep research prompt 与三个检索工具。

主要检索配置:Anserini BM25 over BrowseComp-Plus corpus,,retrieval depth = 1000;主实验 per-query timeout 秒,在 触发 submit-now steer。论文没有训练新模型,因此没有 GPU training steps / LR / batch size;计算成本主要来自 LLM inference 和 BM25 retrieval backend。Released code 中复现实验入口通过 npm run run:benchmark:query-set:shared-bm25 / sharded variants、TIMEOUT_SECONDSPI_BM25_K1PI_BM25_B 和 BrowseComp-Plus query slices 配置。

5. Experimental Results (实验结果)

5.1 Main results:BM25 + 强 agent loop 可以超过 dense retriever baselines

Table 1 解读:同为 gpt-5 + BM25,released baseline 准确率是 58.3%,Pi-Serini 提升到 74.6%,成本从 USD 400.4 降到 USD 94.9。Pi-Serini gpt-5 还略高于 released gpt-5 + qwen3-embed-8b 的 73.0%。最高准确率来自 gpt-5.5 + BM25:83.1%,同时 surfaced evidence recall 达 94.7%、surfaced gold recall 94.4%。低成本端,deepseek-v4-flash + BM25 成本只有 USD 28.9,准确率 68.1%,surfaced evidence/gold recall 为 94.5/95.7。

这些数字说明两点。第一,早期 BM25 baseline 低并不等于 BM25 不适合 deep research;在足够深的 retrieval 和工具接口下,它可以超过 dense retriever baseline。第二,answer quality 仍强依赖 LLM:claude-opus-4.7 成本高但准确率 69.8%,而 gpt-5.5 达到 83.1%,说明同样的 BM25 evidence pool 需要强 reasoning/tool-use model 才能充分转化为答案。

5.2 Retrieval behavior:surfaced 高,previewed/behavior 仍是瓶颈

Pi-Serini 多数变体 surfaced recall 都超过 90%。gpt-5.5 + BM25 的 surfaced evidence/gold recall 为 94.7/94.4,高于 Table 1 中 dense-retriever baselines 的 79.0/81.3 或 79.2。和 released gpt-5 + BM25 相比,Pi-Serini gpt-5 + BM25 的 previewed gold recall 从 66.5 提升到 70.0,同时维持类似的 previewed evidence recall。

但是 surfaced recall 与 behavior recall 仍有差距。例如 gpt-5.5 + BM25 的 surfaced evidence recall 是 94.7,而 behavior evidence recall 是 58.9;deepseek-v4-flash + BM25 surfaced gold recall 95.7,但 behavior gold recall 60.6。这说明 BM25 已把大量正确证据放进 ranking cache,剩余挑战是 agent 是否会翻到、打开并引用这些证据。

5.3 Tool usage 和成本

Table 2 解读:Pi-Serini 并没有靠无限增加工具调用取胜。同样 gpt-5 + BM25,released baseline 平均 23.2 次 tool calls,Pi-Serini 只有 15.2 次;gpt-5.2 + qwen3-embed-8b baseline 平均 73.8 次 search calls,但准确率 45.1%。Pi-Serini 的调用结构更细:例如 gpt-5.5 + BM25 平均 13.5 次 search、5.0 次 read、0.8 次 browse,总计 19.3。

成本结果也支持这个结论:Pi-Serini gpt-5 + BM25 从 released baseline 的 USD 400.4 降到 USD 94.9;gpt-5.5 + BM25 虽是最强结果,成本 USD 291.6 仍低于 released gpt-5 baselines 的 USD 400.4 / USD 360.7。论文把这解释为:BM25 cheap + cache/pagination + timeout policy 让 ablation 和 future deep research eval 更可负担。

5.4 Ablation:retrieval depth 是 surfaced recall 的关键

Figure 2 解读:当 search 返回的 top- 从 5 增到 100,surfaced evidence recall 从 70.48% 增到 86.22%,previewed evidence recall 从 70.48% 到 74.07%;当 继续到 1000,surfaced evidence recall 达 95.78%,surfaced gold recall 达 97.33%。但 previewed recall 在 附近最高(evidence 74.67%),之后不再上升,说明“把证据放进 cache”不等于“agent 会看见证据”。

这组 ablation 是论文最核心的证据之一:BM25 需要足够 retrieval depth 才能暴露证据,而 agent 需要更好的 ranking navigation 才能把 surfaced evidence 转成 previewed/behavior evidence。

5.5 Ablation:BM25 tuning 和 termination policy

Table 3 解读:在 100-query subset 上,default BM25 + 300s timeout 的 accuracy 是 64.0%,surfaced recall 84.6%,cost USD 24.1;tuned BM25 + 同样 300s timeout 提升到 accuracy 82.0%、surfaced recall 95.7%、previewed recall 70.4%、behavior recall 52.2%,cost 反而降到 USD 21.7。论文还报告 grid-search best accuracy 81.1%、surfaced recall 94.0%,与 接近。

Termination policy 方面,tuned BM25 下 MaxIter100 的 accuracy 是 76.0%,cost USD 24.9;300s timeout 的 accuracy 更高(82.0%)且 cost 更低(USD 21.7)。3600s timeout accuracy 83.0%、cost USD 26.3,并没有带来与时间上限成比例的收益。这说明 timeout steering 可以作为更贴近产品场景的预算控制方式。

5.6 论文结论与限制

总体结论:在 BrowseComp-Plus 上,一个 well-configured lexical retriever 加上适合 agentic loop 的工具接口,足以支持有效 deep research;dense retriever 或训练式 retriever 不是唯一可行路径。Pi-Serini 的结果更像是“BM25 + deep retrieval + controllable interface + strong LLM”的组合效应,而不是单独 BM25 的胜利。

作者明确承认三点限制。第一,当前接口仍相对 minimal,previewed/behavior recall 明显低于 surfaced recall,说明 agent navigation 仍有改进空间。第二,实验只覆盖 BrowseComp-Plus,尚未证明能泛化到 multilingual queries、domain-specific corpora 或动态 open-web setting。第三,timeout 是简单预算策略;不同 query 难度不同,固定 300s 可能让困难问题探索不足,也可能让简单问题计算过量。