What happened / 发生了什么
同时检索多个知识库时,每个知识库都有独立的 doc.db 和 FTS5 索引,bm25() 使用各自索引中的文档频率与长度统计,原始分数不在同一尺度上。
当前 SparseRetriever.retrieve() 会分别调用每个知识库的 search_sparse(),随后把结果直接加入同一个列表,并按各索引返回的原始 BM25 分数进行全局排序:
results = fts_results + fallback_results
results.sort(key=lambda x: x.score, reverse=True)
这会使小知识库中的精确匹配被大知识库中仅部分包含关键词的结果压低。
匿名化复现:
- 知识库 A:包含数百个通用帮助文档 chunk
- 知识库 B:仅包含少量 FAQ chunk
- 查询:
如何重置管理员密码?
- 知识库 B 中存在标题与查询完全一致的 chunk
诊断结果:
Dense rank: 1
Sparse rank after cross-KB merge: > 50
Small-KB exact match local sparse rank: 1
Final fused rank: outside the default top 5
这里不是分词或内容未命中:精确 FAQ 在自己的知识库中是稀疏检索第 1 名,但其原始 BM25 分数无法与另一个独立索引的分数直接比较。
该行为可能与 #7648 有关。此前内存 BM25 fallback 会先合并所有选中知识库的 chunk,再基于同一个 corpus 计算 BM25;FTS5 路径改为每库独立检索后,跨库直接比较了不同统计空间的原始分数。
Reproduce / 如何复现?
- 创建两个知识库:
- 知识库 A 导入较多通用帮助文档,使其包含大量与测试查询部分重合的常见词。
- 知识库 B 仅导入少量 FAQ,其中一条标题为
如何重置管理员密码?。
- 在同一会话中同时启用两个知识库。
- 使用
如何重置管理员密码? 进行检索。
- 分别记录:
- 每个知识库内的 FTS5 排名;
- 合并后的 sparse rank;
- RRF 最终排名。
- 可以观察到知识库 B 的精确匹配在库内排名第 1,但在跨库合并后被排到知识库 A 的大量候选之后。
最小代码层复现可以让两个 fake DocumentStorage 返回各自有序、但数值尺度明显不同的 FTS5 分数,然后调用 SparseRetriever.retrieve() / RankFusion.fuse()。
Expected behavior / 预期行为
独立索引返回的检索结果应作为独立排名列表参与融合,不应直接比较不同索引的原始 BM25 分数。
AstrBot 已使用 RRF 融合 dense/sparse 结果,可以进一步按每个知识库内的 sparse rank 计算 RRF 贡献。这样既不依赖跨索引分数可比性,也与 RRF 组合独立检索结果集的设计一致。
同时应增加大小差异明显的多知识库回归测试。
Actual behavior / 实际行为
SparseRetriever 先按不可比较的跨索引 BM25 原始分数生成一个全局 sparse rank,错误排名随后进入 RRF,使精确匹配无法进入较小的 top_m_final。
AstrBot version, deployment method, provider, platform / AstrBot 版本、部署方式、提供商和消息平台
- AstrBot:v4.26.7,当前
master 仍可确认相关实现
- 部署方式:Linux 源码部署(uv)
- Embedding:OpenAI-compatible embedding provider
- 消息平台:WebChat;该问题与消息平台无关,也可以通过知识库检索 API 复现
OS
Linux
Logs / 报错日志
该问题不会抛出异常。以下为匿名化诊断输出:
large_kb_matches=50
small_kb_matches=1
small_kb_exact_match_local_rank=1
small_kb_exact_match_merged_sparse_rank=>50
dense_rank=1
fused_rank=>5
相关代码:
astrbot/core/knowledge_base/retrieval/sparse_retriever.py
astrbot/core/knowledge_base/retrieval/rank_fusion.py
astrbot/core/db/vec_db/faiss_impl/document_storage.py
Are you willing to submit a PR? / 你愿意提交 PR 吗?
Code of Conduct
What happened / 发生了什么
同时检索多个知识库时,每个知识库都有独立的
doc.db和 FTS5 索引,bm25()使用各自索引中的文档频率与长度统计,原始分数不在同一尺度上。当前
SparseRetriever.retrieve()会分别调用每个知识库的search_sparse(),随后把结果直接加入同一个列表,并按各索引返回的原始 BM25 分数进行全局排序:这会使小知识库中的精确匹配被大知识库中仅部分包含关键词的结果压低。
匿名化复现:
如何重置管理员密码?诊断结果:
这里不是分词或内容未命中:精确 FAQ 在自己的知识库中是稀疏检索第 1 名,但其原始 BM25 分数无法与另一个独立索引的分数直接比较。
该行为可能与 #7648 有关。此前内存 BM25 fallback 会先合并所有选中知识库的 chunk,再基于同一个 corpus 计算 BM25;FTS5 路径改为每库独立检索后,跨库直接比较了不同统计空间的原始分数。
Reproduce / 如何复现?
如何重置管理员密码?。如何重置管理员密码?进行检索。最小代码层复现可以让两个 fake
DocumentStorage返回各自有序、但数值尺度明显不同的 FTS5 分数,然后调用SparseRetriever.retrieve()/RankFusion.fuse()。Expected behavior / 预期行为
独立索引返回的检索结果应作为独立排名列表参与融合,不应直接比较不同索引的原始 BM25 分数。
AstrBot 已使用 RRF 融合 dense/sparse 结果,可以进一步按每个知识库内的 sparse rank 计算 RRF 贡献。这样既不依赖跨索引分数可比性,也与 RRF 组合独立检索结果集的设计一致。
同时应增加大小差异明显的多知识库回归测试。
Actual behavior / 实际行为
SparseRetriever先按不可比较的跨索引 BM25 原始分数生成一个全局 sparse rank,错误排名随后进入 RRF,使精确匹配无法进入较小的top_m_final。AstrBot version, deployment method, provider, platform / AstrBot 版本、部署方式、提供商和消息平台
master仍可确认相关实现OS
Linux
Logs / 报错日志
该问题不会抛出异常。以下为匿名化诊断输出:
相关代码:
astrbot/core/knowledge_base/retrieval/sparse_retriever.pyastrbot/core/knowledge_base/retrieval/rank_fusion.pyastrbot/core/db/vec_db/faiss_impl/document_storage.pyAre you willing to submit a PR? / 你愿意提交 PR 吗?
Code of Conduct