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-localsearch_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 停止使用工具、用已收集证据立即提交答案;从此之后 search、read_search_results、read_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.9、PI_BM25_B=0.4,README / docs/running-benchmarks.md 才把 BrowseComp-Plus 复现实验参数写成 PI_BM25_K1=25、PI_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 Concept | Source File | Key Class/Function |
|---|---|---|
| Pi-Serini extension registration and tool surface | src/pi-search/extension.ts | registerPiSearchExtension, pi.registerTool for search, read_search_results, read_document |
| Search/cache/pagination tool behavior | src/pi-search/tool_handlers.ts, src/pi-search/search_cache.ts | executeSearchTool, executeReadSearchResultsTool, executeReadDocumentTool, SearchSessionStore, buildSearchPage |
| Time-budget steering | src/pi-search/prompt_policy.ts, src/pi-search/extension.ts | BENCHMARK_TIMEOUT_SECONDS, SUBMIT_NOW_TRIGGER_RATIO=0.7, getSubmitNowDelayMs, tool-blocking logic |
| BM25 backend integration | src/extensions/pi_search.ts, src/search-providers/anserini/pi_search_backend_factory.ts, src/pi-search/searcher/adapters/anserini_bm25/adapter.ts | wrapper that injects Anserini BM25 backend into the package-owned pi-search contract |
| BrowseComp-Plus benchmark definition | src/benchmarks/browsecomp_plus.ts | browsecompPlusBenchmark, query sets q9/q100/q300/qfull, qrels, ground-truth, index path |
| Query-set launch and experiment config surface | src/orchestration/benchmark_query_set_launch.ts, src/orchestration/query_set_shared_bm25.ts, docs/running-benchmarks.md | TIMEOUT_SECONDS=300 default in query-set launch; BM25 env PI_BM25_K1, PI_BM25_B |
| Run artifacts / retrieval behavior logging | src/orchestration/run_pi_benchmark.ts | emitted surfaced_docids, previewed_docids, opened_docids, cited_docids, tool-call counts |
| Answer judge and calibration | src/evaluation/evaluate_run_with_pi.ts, src/evaluation/calibration.ts, src/evaluation/judge_prompt.ts | gold-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 + BM25 或 qwen3-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_SECONDS、PI_BM25_K1、PI_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 可能让困难问题探索不足,也可能让简单问题计算过量。