向量資料庫、GraphRAG、檔案搜尋 Agent,一個比一個新。結果一篇剛上 arXiv 的論文,把 51 萬份企業文件丟進同一場測試,撐到最後的卻是 1990 年代就出現的 BM25。

看到標題先別急著把向量資料庫砍掉。

這篇論文測的是一套虛構企業語料,reader 固定,干擾文件又有大量「主題很像、事實卻錯」的內容。BM25 約在 1,000 萬 corpus tokens 附近反超檔案搜尋 Agent,到了 6.01 億 tokens 仍保持領先。這個位置是相鄰量測層之間的概略標記,沒有精確到能當部署門檻。

我帶回實務的結論是:先排好全域候選,再讓 Agent 推理。

這次至少把比較條件鎖住了

多數 RAG 比較很難讀。BM25 用一套資料,GraphRAG 用另一套;reader、chunk size 和問題也不同,最後把分數排成一張表,卻說不清差距到底從哪裡來。

《BM25 Wins at Scale》做法比較乾淨。研究團隊用同一套虛構企業語料,建出 28 層互相包含的 corpus:

  • 1,144 到 511,959 份文件
  • 1.7M 到 600.8M corpus tokens
  • 500 個問題,含基本查詢、完整性、資訊衝突與找不到答案
  • 相關文件、錯誤版本與誘餌從最小層就固定存在
  • BM25、DenseRAG 與 HippoRAG 2 共用 1,200-token chunk、100-token overlap
  • 所有方法都用 Qwen3.6-27B 當 reader;適用的 retrieval pipeline 取 top-5 chunks

語料來自 wiki、email、ticket、會議紀錄、CRM、code review 等九種來源,另外混入 7.7% 放錯位置、近似重複或含舊資訊的文件。這種資料不像乾淨的 FAQ,比較接近公司知識庫真的會長成的樣子。

它畢竟是 synthetic benchmark。最難的 traps 有一部分來自 BM25 top-10,以及 BM25 top-200 候選經 dense reranking 後挑出的錯誤版本,資料建構本身含 lexical proposal path。作者另外做了直接 dense retrieval、問題改寫與 top-10 control;在論文量測的 control 裡,主要排序沒有翻轉。這些補強仍取代不了真實流量測試。

研究團隊比較七條 pipeline:

類型 方法
關鍵字檢索 BM25
向量檢索 DenseRAG
圖式檢索 HippoRAG 2、LinearRAG、MS-GraphRAG、LightRAG
Agent 搜尋 File-System Agent

這裡的 File-System Agent 很像 coding agent 查 codebase:它能列目錄、搜尋文字、讀檔,再根據前一次結果決定下一步。每題最多 80 次 LLM 呼叫。

「很像」不等於一般 coding agent。它的 list_dir 最多回 200 個名稱,grep 只能做 fixed-string search 且最多回 30 條路徑,read_doc 只讀前 8,000 字元。它沒有 regex、glob、語言索引或任意 shell。後面的結果只能外推到這種受限工具組。

小資料時 Agent 領先,資料一大就反過來

最小的 1,144 份文件裡,File-System Agent 拿到 77.4 分,BM25 是 74.7。兩者 95% 信賴區間重疊,還不能說 Agent 穩定勝出,但點估計確實較高。

每個 method/question/tier 只執行一次。95% 信賴區間來自對題目做 10,000 次 bootstrap,量的是問題樣本變動,沒有包含 Agent 多跑幾次的執行變異。

語料增加到約 1,000 萬 tokens 後,兩條曲線交叉。完整 511,959 份文件的分數變成:

方法 Combined score
BM25 50.5
File-System Agent 30.7
DenseRAG 29.9

BM25 的 50.5 看起來也不高。這不是一套「誰都做得很好,BM25 稍微贏」的 benchmark;資料規模放大後,三種方法都掉分,只是 Agent 和 DenseRAG 掉得更快。

這張主實驗表涵蓋全部 500 題。論文 Table 1 同列的 5.8K、226K 與 4.9K query tokens 是最小 1,144 份文件時的描述值,不能拿來當完整語料成本。完整語料的 token 數出現在另一組 150 題 control,兩組分數也不能直接相減。

GraphRAG 的問題更早發生在建索引。HippoRAG 2 與 LinearRAG 做到 131,876 份文件就停了,MS-GraphRAG 停在 8,750,LightRAG 只完成 2,254。論文依實測速度外推,LightRAG 跑完整語料可能需要 102B generative tokens 與約四個單機年。

這些是外推值,不是實際等了四年。平行化也能縮短日曆時間。但它把常被藏起來的成本攤開了:有些 GraphRAG 在 demo corpus 很漂亮,搬到十萬份文件時,問題可能不是答得準不準,而是索引根本蓋不完。

BM25 贏在先看到全域,不只贏在關鍵字

File-System Agent 每次只能從局部線索往外走。文件少時,它可以讀幾份、換個關鍵字再查,靠推理修正方向。文件到了 51 萬份,目錄分支和近似結果一起增加,逐步探索越來越難碰到正確候選。

論文裡最關鍵的 control 是 Agent+BM25。

研究團隊另取固定 150 題,在獨立 scoring session 裡保留 Agent model、harness、80-call budget 與 judge,把 raw tree tools 和對應操作指令換成 BM25 search tool,再讓 Agent 讀候選 chunk。這組數字不能和前面的 500 題主實驗直接比較:

方法 Combined score 文件召回率 每題 LLM calls 每題 tokens
原始檔案 Agent 36.9 36.8 36.12 895K
原生 BM25 54.8 65.6 1.00
Agent+BM25 69.4 72.4 5.79 101K

原生 BM25 的 score 與 recall 是這 150 題共同 rejudge 的結果;論文只提供 5.8K 的完整 500 題描述性 token 平均,我沒有硬塞進表內。兩個 Agent 變體則來自同一組 control:換掉候選發現方式後,分數多了 32.5,token 剩約九分之一。模型與 budget 沒變,差距主要來自搜尋介面。

這也解釋 DenseRAG 為什麼沒有自然勝出。企業問題常含專案代號、人名、日期、錯誤碼與產品名稱;干擾文件又刻意做到「語意相近、版本錯誤」。向量很容易把同主題的舊決策排進來,BM25 對精確詞彙反而有優勢。

但別把這段讀成「向量搜尋沒用」。論文只用 Qwen3-Embedding-0.6B 代表 dense retrieval,固定 top-5,也沒測 BM25 加向量的 hybrid。換更強 embedding、reranker、query expansion 或不同 chunk strategy,排序可能改變。

先跑一個 BM25 baseline

如果你還沒建立檢索基準,先做一個便宜版本。下面是整理過、方便重現的最小範例,中文斷詞用 jieba,排序用 rank-bm25

1
python -m pip install jieba rank-bm25
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import jieba
from rank_bm25 import BM25Okapi

documents = [
{"id": "ADR-017", "text": "支付服務在 2026 年 5 月改用 Redis 分散式鎖"},
{"id": "ADR-008", "text": "支付服務曾評估 SQL Server application lock"},
{"id": "INC-204", "text": "Redis 鎖逾時造成重複扣款的事故紀錄"},
]

def tokens(text: str) -> list[str]:
return [word.strip() for word in jieba.cut(text) if word.strip()]

index = BM25Okapi([tokens(doc["text"]) for doc in documents])
query = tokens("支付服務 2026 年 5 月改用哪一種分散式鎖")
scores = index.get_scores(query)

ranked = sorted(
zip(documents, scores),
key=lambda item: item[1],
reverse=True,
)

for doc, score in ranked[:2]:
print(doc["id"], round(float(score), 3), doc["text"])

這段適合驗證流程,不適合直接扛 51 萬份文件。rank-bm25 專案自己也建議,大規模 production 換成更高效能的檢索套件;實際系統也可以直接用搜尋引擎。Elasticsearch 的文字欄位預設就是 BM25 similarity,不必先調 k1b 才能開始。

論文說 BM25 build cost 是零,指的是零 model tokens。CPU、儲存空間、斷詞與倒排索引建置一樣要付,別把 token accounting 讀成零成本。

評估時別只看平均分

你至少要準備一小組人工標記的 query 與相關文件。下面這個標準函式不依賴任何套件,可以先算每題的 Recall@5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def recall_at_k(
ranked_ids: list[str],
relevant_ids: set[str],
k: int = 5,
) -> float:
if not relevant_ids:
raise ValueError("relevant_ids 不可為空")

hits = set(ranked_ids[:k]) & relevant_ids
return len(hits) / len(relevant_ids)


cases = [
{
"kind": "identifier",
"ranked": ["ADR-017", "INC-204", "ADR-008"],
"relevant": {"ADR-017"},
},
{
"kind": "conflict",
"ranked": ["ADR-008", "ADR-017", "INC-204"],
"relevant": {"ADR-017", "INC-204"},
},
]

for case in cases:
score = recall_at_k(case["ranked"], case["relevant"], k=2)
print(case["kind"], score)

relevant_ids 為空時,Recall@K 沒有定義,所以範例直接報錯。對「資料不存在」的問題要另算拒答率、false-positive rate 或 answerability accuracy,不要塞一份假的 relevant document 讓公式能跑。

不要把所有 query 混成一個漂亮平均值。至少分開看:

  • 精確識別碼、錯誤碼與專案名稱
  • 自然語言改寫
  • 多文件彙整
  • 新舊版本衝突
  • 資料不存在,系統應該拒答

這篇論文也出現同樣分化。在 42,587 份文件時,File-System Agent 在 intra-document、project-related、completeness 與 conflicting-information 四類領先;BM25 在另外五類最佳或打平,miscellaneous 則由 graph system 領先。你需要的是自己流量的分布,不是抄論文的總分。

向量搜尋該放在哪裡

BM25 抓精確字詞,向量搜尋補語意改寫。兩邊各取一份 ranking,再用 Reciprocal Rank Fusion(RRF)合併,是比直接砍掉其中一邊更務實的做法。

RRF 不直接混兩套尺度不同的分數,只看名次。下面的版本可以直接執行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from collections.abc import Sequence

def rrf(
rankings: Sequence[Sequence[str]],
rank_constant: int = 60,
) -> list[tuple[str, float]]:
scores: dict[str, float] = {}

for ranking in rankings:
for rank, doc_id in enumerate(ranking, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + (
1.0 / (rank_constant + rank)
)

return sorted(scores.items(), key=lambda item: item[1], reverse=True)


bm25_hits = ["ADR-017", "INC-204", "ADR-008"]
vector_hits = ["INC-204", "DOC-991", "ADR-017"]

for doc_id, score in rrf([bm25_hits, vector_hits]):
print(doc_id, round(score, 6))

Elasticsearch 官方也建議用 RRF 做 hybrid search。它能把標準文字檢索與 kNN retriever 放在同一個請求裡。不過 hybrid 不保證必勝;RRF 的 rank_window_size、兩邊取多少候選、chunk 粒度都要用同一組標記資料重跑。

範例假設同一份 ranking 內沒有重複 ID。正式接資料時先去重,避免單一 retriever 替同一份文件重複加分。

Agent 放最後一段,工作反而更像 Agent

我會把 production RAG 切成三層:

  1. BM25 做全域候選發現,先保住識別碼、日期與專有名詞。
  2. 向量搜尋補改寫與概念相近的 query,必要時用 RRF 合併。
  3. Agent 只在縮小後的候選裡重查、比對衝突、跨文件整理證據,再由 reader 產生答案。

這個順序看起來把 Agent 綁住了,實際上是把 token 留給它擅長的部分。讓模型在 51 萬份文件裡猜路徑,不叫自主,叫沒有索引。

候選數可以先從 20 到 50 估,但這只是起始假設。最後仍要用標記資料掃過不同 K 值,觀察 recall、延遲與 token 成本。

還有一個邊界要留著:論文的 Agent+BM25 每題仍花 101K tokens。它的 69.4 分值得注意,不代表每個查詢都該啟動 Agent。精確查詢一次 retrieval 加 reader 就能答,多文件彙整或衝突判斷才值得付那筆帳。

我原本以為這篇論文的看點是老演算法打臉新方法。讀完後更像一堂資料結構課:先建立能看見全域的索引,再談模型要怎麼行動。

下次遇到 RAG 找不到明明存在的文件,我會先查 Recall@5 和候選清單。先別急著換更大的模型。