不建向量库:SAG2 用一个 25.87 MiB 的 SQLite 索引拿到 86.62 召回

SAG2 把语义判断从建库挪到提问之后。一个 SQLite 索引加 BM25 与 JEV 判断,在 MuSiQue、2WikiMultiHopQA、HotpotQA 上拿到对比方法里最高的 R@5 与 R@10,建库 2.36 秒、零次模型调用。消融把增益拆成两轮扩张与共享上下文两块,代价写在最后一节。

语义判断可以挪到提问之后。索引只留词与词频,一个 SQLite 文件,建库 2.36 秒,MuSiQue 的 R@10 从 54.41 抬到 86.62。

原文是《SAG2: Embedding-Free Multi-Hop Retrieval with BM25 and JEV》,Zleap AI 的 Yuchao Wu,arXiv 编号 2610.22588,全文 15 页(arXiv 页面 ,PDF )。代码在 Zleap-AI/SAG-Benchmark 。它是同一位作者前作 SAG 的续篇,SAG 用 MySQL 存事件与实体、用 Elasticsearch 存向量,这篇把两者都去掉。下面每个数字都能回到原文的表号,凡是我自己除出来的都标「我算」。

一、埃里克·霍特的出生地#

附录 A 里有一道具体的题,问埃里克·霍特出生在哪。语料里有一段讲霍特本人,它直接命中问题,也进了 BM25 的前一百。另一段讲蒙特贝洛,纽约州的那个地方,只有在知道「霍特出生在蒙特贝洛」之后才有用,而它和原问题的词重叠几乎为零,前一百里没有它。

SAG2 的流程是这样转的。种子只能从高分段落里取,霍特那段正好在里面。种子正文里的「蒙特贝洛」被当成一个可用的词,权重按四个因子算出来,然后拿这个词单独去查一遍索引,蒙特贝洛那段就进来了。最后十条里两段都在,这道题的召回从 50% 变成 100%。

这道题不是孤例。一千道 MuSiQue 题里,有 389 道的最终结果包含至少一段不在初始 BM25 前一百里的证据,这样补进来的段落一共 433 段。BM25 的 R@10 是 54.41,套上这套流程是 86.62。

指标BM25SAG2差
MuSiQue R@1054.4186.62+32.21
MuSiQue 全证据率 AE@1017.4069.50+52.10
2WikiMultiHopQA R@1071.5097.43+25.93
HotpotQA R@1086.9099.00+12.10

差的这 32 点里,没有一次向量运算。

二、之前到哪一步了#

多跳检索的老问题是「中间那段怎么找」。三族做法各自卡在一个位置。

一族代表天花板在哪
稠密召回Contriever、BGE、GTE-Qwen2、NV-Embed-v2编码器建库时定死,查询只能改写,不能改候选
结构化增强SAG、HippoRAG 2建库要抽实体或建图,抽错就永久少一条边
伪相关反馈RM3、BERT-QE、GAR反馈文本靠启发式挑,或要预先算好邻居图

第二族在这篇的表格里最强。同一张表上,NV-Embed-v2 在 MuSiQue 的 R@10 是 77.27,SAG 是 83.37,SAG2 是 86.62。结构带来的提升比换更大的编码器更明显,这也是它值得继续走的原因。

SAG 的代价在准备阶段。它要用大模型把语料抽成事件与实体记录,再单独算一遍向量,两个数据库各存一半。作者在附录 D 里给了前作的建库账,4.57 小时、28.26 百万 token。这个数字和 SAG2 的 2.36 秒不在一个量级上,硬件与模型配置也不同,论文特意写明这不是同口径的测速。

SAG2 换掉的正是这一段。它保留 SAG 的原则,把关联放到查询时按问题算,但索引里只放原文和词频。

三、一个文件,三次打分#

索引阶段只有本地文本处理。语料按相邻段落合并到 2,200 字符左右,超长段落保持完整,然后分词写进 SQLite 的 FTS5,字段是标题、小标题、正文,BM25 权重 2、2、1。MuSiQue 的 11,656 段全部进库用了 2.36 秒,文件 25.87 MiB,模型调用零次,费用零。

图 1 左边一列是建库时干的事,右边一列是每个问题重干一遍的事。稠密检索把语义压进向量,SAG 把语义压进事件表,SAG2 两项都不做,判断留到右边
轻点放大

查询阶段是三次打分,两次扩张。

第一轮用问题分词后最多 80 个词做 OR 查询,取前 100 条,交给 JEV 逐条判一次,此时上下文为空。判完取前十名里还没扩张过的 10 段当种子,从种子正文里选 20 个扩张词,每个词单独查一次索引,按词的优先级交错合并,攒够 100 条新候选。候选池是受保护的初始 5 段、累计种子、新候选三块的并集,去重后 115 条,全部重判一次。

第二轮重复同样的动作,但新候选只放开 20 条,剩下的名额用历史分数靠前的旧候选填满,池子 125 条。默认最多两轮扩张,也就是三次打分,之后不再调模型,只做一次本地重排,把前十名用 TF-IDF 余弦加行列式点过程调一下顺序,第一位不动。

图 2 左边是三次打分各自的池子大小,右边是这套配置加起来的上限 340 次判断。100 加 115 加 125,就是 340
轻点放大

340 这个数是配置算出来的(我算,论文第 3.5 节给了同样的分解)。它数的是判了多少条,不是发了多少 HTTP 请求,也不是花多少 token。一条长段落、一段十段的共享上下文,都会让单次判断变贵,论文自己把这句话写在了上限旁边。

四、判据是一条是非题#

整篇最值得抄走的东西是那段提示词。JEV 的 Noul 接口接收一段自然语言上下文和若干问题,返回每个问题答 yes 的概率,不生成推理过程。SAG2 用它问一件事,这条候选本身是不是一条完整答案链里必需的事实。

四句限定把这件简单的事说得比较严。

  • 中间环节的识别事实也算,霍特出生在哪这种中间答案要判 yes。
  • 上下文只能用来连接事实,不能把只出现在上下文里的事实算到候选头上。
  • 只要沾边、或者只是可选背景,都不算。
  • 不许用外部知识。

第三句是防止判断模型被自己的上下文骗。第四句是防止它用常识补齐语料里没有的桥。这两条一加,同一条候选在不同轮次可以拿到不同的分数,分数是带状态的。

轮次上下文判多少条候选池
第一次空100初始 100
第二次种子加前 10 条新候选1155 + 10 + 100
第三次上一轮前十与它们的暂定分,并要求对着引用的事实重估1255 + 20 + 100

扩张词的挑选也不是随手取高频词。每个词的优先级是四个因子的乘积,取所有种子里的最大值。

a_r(t) = max over seeds d [ (1 + log tf(t,d)) · I(t) · (1 + g_q(t,d)) · w_r(d) ]
I(t)   = log(1 + (N - df(t) + 0.5) / (df(t) + 0.5))
w_r(d) = 0.5 + 0.5 · s_r(d) / max_s          (区间 0.5 到 1)

四项分别管频率、稀有度、与原问题的连接、种子自身的证据分。w 的下界是 0.5,分数低的种子降权而不丢弃。g_q 那一项是个细节,一个词可能出现在种子的好几个句子里,取与原问题共享稀有词最多的那个句子,这样「蒙特贝洛」这种出现在关键句里的词会浮上来。

五、三个基准的召回#

一千道题的开发集,语料是题目自带的支撑段与干扰段。三个数据集上 SAG2 的 R@5 与 R@10 都是九种方法里最高的,SAG 与 HippoRAG 2 跟在后面。

方法MuSiQue R@1R@5R@102Wiki R@10HotpotQA R@10
BM2528.5347.4754.4171.5086.90
NV-Embed-v234.0268.8477.2780.7597.70
HippoRAG 230.6565.1373.7693.4097.15
SAG36.8280.3683.3793.5797.70
SAG234.3080.6886.6297.4399.00

MuSiQue 的 R@1 和 R@2 是 SAG 更高,36.82 对 34.30,63.62 对 62.18,SAG2 的优势要把截断放宽到 5 和 10 才出现。HotpotQA 上 SAG 与 NV-Embed-v2 的 R@10 都是 97.70,SAG2 的 99.00 只多 1.30 点,这道题的语料本来就容易。

按标注跳数拆开看,结论更清楚。跳数越多,第二次扩张带来的增量越大,2 跳加 2.32 点、3 跳加 3.27 点、4 跳加 6.17 点(最后一项逐项相减是 6.18,论文写 6.17,差在小数进位)。

图 3 三组柱子是按跳数拆的 R@10,菱形是两轮之后的全证据率。四跳那一组的柱子在涨,菱形只有 28.31
轻点放大

四跳的 AE@10 只有 28.31。全证据率按整条证据链算,比召回率严格,七成以上的四跳问题,十条结果里凑不齐全部证据。词法入口补得上中间那一段,补不上从头到尾的四段。

六、五十万篇企业文档#

第二个实验换了场景。EnterpriseRAG-Bench 是合成的企业内部通信集合,51.2 万篇文档,包括消息、邮件、工单、会议记录。切片约 1,000 字符,上限 1,200,参数一个没改,按源文档去重后取前十。

题型nBM25向量Bash AgentSAG2
基础问答17577.748.661.786.86
语义改写12543.224.829.658.40
文内推理4090.057.575.0100.00
项目相关4065.549.243.283.58
带约束3085.083.378.396.67
信息冲突2082.555.072.592.50
完整性2046.533.459.076.24
杂项2090.075.0100.0100.00
总体47068.446.055.881.10

这张表最刺眼的是向量那一列。46.0 不但低于 BM25 的 68.4,还在每一类上都低,语义改写那类只有 24.8。论文引的是基准发布方给出的数,我没有复现。一个解释是内部语料里全是产品名、工单号、人名,向量把这些专有串磨平了,词法反而原样匹配。

SAG2 在这一列上到 81.10。项目相关 83.58 与完整性 76.24 两行是扩张真正帮上忙的地方,这两类问题的共同点是答案分散在多份文档里,只看一份不够。语义改写只有 58.40,词法入口救不回一个共同词都没有的问法。

图 4 八类题型的文档召回,灰条 BM25,紫条 SAG2,竖虚线是向量检索的总体 46.0。最低的一行是语义改写
轻点放大

Bash Agent 的 55.8 那一列不能直接比,它的输出集大小不固定,不是固定取十篇。论文自己写了这一条,也没有给三者对齐算力预算。

七、增益不在候选池#

这套东西最容易被误读成一件事,就是把候选池做大。论文用三个对照把这件事拆开了,三组都在同一千道 MuSiQue 题上跑。

对照改动R@10与 SAG2 的差判断次数
扩张深度BM25 直接排54.41−32.210
加 JEV 但不扩张67.93−18.6999,925
扩张一轮83.36−3.26211,081
扩张两轮86.620331,325
判断预算一次性 100 条池,判满同样的次数69.82−16.80329,194
共享上下文保留扩张策略,去掉共享证据76.25−10.37330,990

不扩张、只让 JEV 重排一次,拿到 67.93,判断模型本身值 13.52 点。第二行到第三行值 15.43 点,来自一轮扩张。第三行到第四行值 3.26 点,边际收益掉下来了,作者把一轮的配置标成更便宜的工作点,少 36.29% 的判断(我算)。

判断预算那一组最能说明问题。对照组把每条题目的一次性候选池拉到 331 条左右,判满 SAG2 用掉的次数,池子比单轮的 100 条宽三倍多,R@10 只有 69.82,差 16.80 点,两者判断次数只差 0.64% 我算。剩 984 道题做成严格等次数的子集,差 16.62 点,结论不变。把预算换成容量不管用,把预算换成「分两轮花」才有用。

上下文那一组拆的是另一半。保留扩张策略、去掉共享证据,R@10 掉到 76.25,全证据率从 69.50 掉到 52.20,判断次数几乎没变。这 10 点来自后一轮看得见前一轮读过什么,和判多少条无关。

图 5 左边一组是扩张深度,右边两条是对 SAG2 的两次减法。判断次数对齐后仍差 16.80,拆掉上下文差 10.37
轻点放大

论文还比了三个替代方案。RM3 单独用比 BM25 差,47.83 对 54.41。GAR-BM25 加上末轮 JEV 到 73.11,判断次数只有 10 万,比 SAG2 少 69.82%,召回低 13.51 点。这两个数是这套架构与已有反馈方法的真实距离,也说明钱花在中间的每一轮是有回报的。

八、代价#

项数值性质
建库时间2.36 秒 / 11,656 段论文实测
索引体积25.87 MiB论文实测,约 2.27 KiB 一段(我算)
建库模型调用0 次,$0论文实测
查询模型费用100 题 $0.433769,1,000 题 $4.326379论文实测,单题约 $0.0043(我算)
单次判断费用约 1.3e-5 美元我算,按 331,325 次除
判断次数上限340 每条问题配置推出,不计 token 与延迟
在线延迟未报告论文说明并发混在一起,没法分离
JEV 权重未公开只有托管 API,模型版本 typesafe/jev-1.13-20260917

最后两行是这套东西真正的门槛。索引在本地,判断在别人的服务上,问题与候选正文都要出网。企业内部语料碰上数据不出域的规矩,这套架构就停在图纸上。作者把这个风险写在局限一节,也把「可本地部署的判断模型」列为下一步,没有给出可行的时间表。

第二处代价在索引侧。新证据必须和某个扩张词有字面匹配,而扩张词是从上一轮证据的正文里选出来的。同义词、别名、缩写,以及完全没有共同词的说法都进不来。附录 A 那个蒙特贝洛的例子成立,是因为那段正文里恰好写着这个词。语义改写那一列 58.40 就是这个缺口的价格。

第三处是可比性。SAG 那一行用的是 BGE-Large 加 Qwen3.6-Flash,SAG2 用的是 JEV,两个完整系统各自带着自己的判断模型比,差出来的 3.25 点不能全记在架构头上。论文在局限里承认了这一点。

九、怎么自己核,以及两个还没答案的问题#

能在一小时内复现的部分不多,能核的部分不少。

要核什么去哪里看什么
340 怎么来的第 3.5 节与表 C1100 + 115 + 125,池子三块并集
增益是不是预算附录 B.1329,194 对 331,325,差 0.64%
上下文值多少图 5 右半与表 A186.62 到 76.25
扩张词怎么算第 3.3 节公式四项乘积,种子权重下界 0.5
判据原文附录 C四句限定,尤其「上下文只用来连接」
建库账表 3 与表 D12.36 秒、25.87 MiB、零模型调用

想动手的话,取一份自己的语料,用 SQLite FTS5 建索引,把前一百条的标题与正文拼成一条是非题,问一个便宜模型「这条是不是答案链里必需的一步」,拿到概率重排。这一步不需要 JEV,任何能给 yes 概率的模型都能试。如果重排之后的召回比 BM25 高出十几点,第二步的扩张就值得写。

第一个还没答案的问题是扩张的边际收益在哪停。一轮值 15.43 点,两轮值 3.26 点,曲线在这里已经很平,第三轮会不会变成负数,论文没跑。340 次判断那条上限是按两轮算的,如果真实语料的段落更长、候选更多,这个上限会先被 token 顶破。

第二个问题在索引规模上。MuSiQue 的语料只有 11,656 段,BM25 前一百条里已经包含大部分相关证据。换成五百万段的私有语料,前一百条的起点会更差,第一轮的种子质量会跟着掉,整套流程依赖「种子还不错」这个前提。企业那一组做到 51.2 万篇,拿到的是文档级召回,没有回答第一轮种子质量下降多少。这个数决定了它在真实库里能不能用同一套参数。

参考#

  • Wu, Y.《SAG2: Embedding-Free Multi-Hop Retrieval with BM25 and JEV》,arXiv:2610.22588,2026 年 10 月。https://arxiv.org/abs/2610.22588
  • Wu, Y. 等《SAG: SQL-Retrieval Augmented Generation with Query-Time Dynamic Hyperedges》,arXiv:2606.15971,2026。https://arxiv.org/abs/2606.15971
  • Sun, Y. 等《EnterpriseRAG-Bench: A RAG Benchmark for Company Internal Knowledge》,arXiv:2605.05253,2026。https://arxiv.org/abs/2605.05253
  • 代码与评测脚本。https://github.com/Zleap-AI/SAG-Benchmark
  • TypeSafe,Jev 的 Noul 接口文档。https://typesafe.ai

输入以在 96 篇文章中查找

浏览全部文章