当你的RAG系统在监管审计时被问"这个答案是怎么检索出来的",向量检索给不出可复现的答案——而BM25+TF-IDF+RRF可以。
一、RAG不等于向量检索
提到RAG(Retrieval-Augmented Generation),多数人的第一反应是:Embedding + 向量数据库 + 余弦相似度。但在监管行业,这套方案有一个致命缺陷——不可复现。
同一个查询,今天检索出文档A,明天因为Embedding模型版本升级,可能检索出文档B。当监管问"你是怎么找到这个答案的",你无法给出bit-for-bit一致的检索路径。
regulated-rag 给出了一个不同的答案:用BM25 + TF-IDF双路检索 + RRF融合,做到零向量依赖、确定性可复现、Hit@1=100%。
本文从源码层面解析这套方案的算法原理和实现细节。
二、为什么单一检索器不够
2.1 BM25的强项与短板
BM25(Best Matching 25)是经典的关键词检索算法,基于词频(TF)和逆文档频率(IDF),核心公式:
score(D, Q) = Σ IDF(qi) · (f(qi, D) · (k1 + 1)) / (f(qi, D) + k1 · (1 - b + b · |D| / avgdl))
BM25的强项是精确术语匹配:查询"反洗钱"时,文档中必须出现"反洗钱"三个字才能命中。这在监管合规场景是硬性要求——你不能用"反洗钱"的语义近似词"反资金流动"来替代。
但BM25有短板:短查询不稳定。当查询只有2-3个词时,BM25的IDF权重区分度不够,容易因为文档长度差异导致排序偏差。实测中,单BM25在6份产品手册的测试集上,Hit@1(第一个结果命中正确答案)只有67%。
2.2 TF-IDF能补什么位
TF-IDF采用余弦相似度计算查询与文档的向量夹角。与BM25不同的是,TF-IDF将查询和文档都表示为词频向量,然后计算夹角余弦:
# TF-IDF余弦相似度核心逻辑
def cosine_similarity(query_vec, doc_vec):
dot = sum(q * d for q, d in zip(query_vec, doc_vec))
norm_q = math.sqrt(sum(q * q for q in query_vec))
norm_d = math.sqrt(sum(d * d for d in doc_vec))
return dot / (norm_q * norm_d) if norm_q * norm_d > 0 else 0
余弦相似度对短文本更稳定,因为它归一化了向量长度,消除了文档长度差异带来的偏差。当查询"5G专网资费"时,TF-IDF能更稳定地匹配到含"5G专网"和"资费"的短段落。
但TF-IDF也有短板:精确匹配能力弱于BM25。因为它是基于词频向量的整体相似度,不像BM25那样有较强的IDF区分度。
结论:BM25和TF-IDF是互补关系。BM25擅长精确术语匹配,TF-IDF擅长短查询稳定排序。单一检索器都有盲区,双路并行才能覆盖。
三、RRF融合算法的数学原理
3.1 RRF公式
RRF(Reciprocal Rank Fusion)是一种保守的排名融合方法,核心思想是不看分数,只看排名:
# RRF融合核心逻辑
def rrf_fuse(bm25_results, tfidf_results, k=60):
scores = {
}
for rank, doc in enumerate(bm25_results):
scores[doc] = scores.get(doc, 0) + 1.0 / (k + rank + 1)
for rank, doc in enumerate(tfidf_results):
scores[doc] = scores.get(doc, 0) + 1.0 / (k + rank + 1)
return sorted(scores, key=scores.get, reverse=True)
公式:score += 1.0 / (k + rank + 1)
rank:文档在该检索器结果中的排名(0-indexed)k:平滑常数,默认60
3.2 为什么k=60
k的作用是控制头部排名和尾部排名的分数差距:
- k=0时,排名0的文档得分1.0,排名1的得分0.5,差距0.5——头部权重过大
- k=60时,排名0的文档得分1/61≈0.0164,排名1的得分1/62≈0.0161,差距0.0003——排名间差距更平缓
- k→∞时,所有排名得分趋于相等——失去排序意义
k=60是原论文(Cormack et al., 2009)经验值,在多个数据集上表现最优。它的本质是让两个检索器的排名组合而不是分数组合来决定最终顺序,避免了BM25和TF-IDF分数量纲不同导致的融合偏差。
3.3 融合效果直觉
假设查询"5G专网资费标准":
- BM25排名:[文档C, 文档A, 文档B](精确匹配"5G专网"和"资费")
- TF-IDF排名:[文档A, 文档C, 文档D](短查询稳定排序)
RRF融合:
- 文档C:1/61 + 1/62 = 0.0325
- 文档A:1/62 + 1/61 = 0.0325
- 文档B:1/63 + 0 = 0.0159
- 文档D:0 + 1/63 = 0.0159
文档C和文档A分数相同(两个检索器都排在前列),文档B和文档D分数相同(只有一个检索器排在末尾)。最终顺序由两个检索器的共识决定,而非单一检索器的偏好。
四、源码实现解析
4.1 BM25 Okapi变体
regulated-rag的BM25实现采用Okapi BM25变体,参数k1=1.5、b=0.75,并做了一个关键增强——对section标题做3倍加权:
# 简化版BM25实现(源码核心逻辑)
class BM25Retriever:
def __init__(self, documents, k1=1.5, b=0.75):
self.k1 = k1
self.b = b
self.documents = documents
self.avgdl = sum(len(d.tokens) for d in documents) / len(documents)
# 预计算IDF
self.idf = self._compute_idf(documents)
def score(self, query_tokens, doc):
# section标题做3倍加权:标题匹配权重是正文的3倍
title_tokens = tokenize(doc.section) * 3
body_tokens = doc.tokens
effective_tokens = title_tokens + body_tokens
score = 0
for q in query_tokens:
if q not in self.idf:
continue
f = effective_tokens.count(q)
dl = len(effective_tokens)
score += self.idf[q] * (f * (self.k1 + 1)) / \
(f + self.k1 * (1 - self.b + self.b * dl / self.avgdl))
return score
3倍加权的业务意义:产品手册的section标题(如"资费标准""覆盖范围")是文档结构的语义锚点。查询词命中标题比命中正文更有价值,3倍加权让BM25的排序更符合人类直觉。
4.2 TF-IDF余弦相似度
TF-IDF实现采用标准余弦相似度,作为BM25的短查询补充:
# 简化版TF-IDF实现
class TfidfRetriever:
def __init__(self, documents):
self.documents = documents
self.idf = self._compute_idf(documents)
self.doc_vectors = [self._vectorize(d) for d in documents]
def _vectorize(self, doc):
# 文档表示为TF-IDF向量
tokens = tokenize(doc.content)
return {
t: tokens.count(t) * self.idf.get(t, 0) for t in set(tokens)}
def search(self, query, top_k=10):
query_vec = self._vectorize_text(query)
scores = [cosine_similarity(query_vec, dv) for dv in self.doc_vectors]
return sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:top_k]
4.3 RRF融合实现
class HybridRetriever:
def __init__(self, documents, k=60):
self.bm25 = BM25Retriever(documents)
self.tfidf = TfidfRetriever(documents)
self.k = k
def search(self, query, top_k=5):
# 双路并行检索
bm25_results = self.bm25.search(query, top_k=20)
tfidf_results = self.tfidf.search(query, top_k=20)
# RRF融合
scores = {
}
for rank, doc_id in enumerate(bm25_results):
scores[doc_id] = scores.get(doc_id, 0) + 1.0 / (self.k + rank + 1)
for rank, doc_id in enumerate(tfidf_results):
scores[doc_id] = scores.get(doc_id, 0) + 1.0 / (self.k + rank + 1)
return sorted(scores, key=scores.get, reverse=True)[:top_k]
整个HybridRetriever的核心只有30行左右,但效果显著。
五、Benchmark实测:Hit@1从67%到100%
5.1 测试集设计
regulated-rag内置产品手册RAG引擎,包含6份产品文档作为测试集,每份文档对应10个标准查询(共60个查询),人工标注了每个查询的正确答案段落。
5.2 三种方案对比
| 检索方案 | Hit@1 | Hit@3 | MRR | 延迟 |
|---|---|---|---|---|
| 单BM25 | 67% | 89% | 0.76 | 1.2ms |
| 单TF-IDF | 58% | 82% | 0.69 | 1.1ms |
| BM25+TF-IDF+RRF | 100% | 100% | 1.00 | 2.8ms |
关键发现:
- 单BM25的Hit@1=67%,意味着1/3的查询第一个结果不是正确答案
- 单TF-IDF的Hit@1=58%,短查询稳定但精确匹配弱
- 双路融合后Hit@1=100%——所有60个查询的第一个结果都是正确答案
- 延迟2.8ms,满足实时检索需求(对比向量检索通常50-200ms)
5.3 为什么融合后能达到100%
关键在于BM25和TF-IDF的错误模式不重叠:
- BM25错的20个查询(短查询排序偏差),TF-IDF全部答对
- TF-IDF错的25个查询(精确匹配弱),BM25全部答对
- 两者的错误集没有交集,RRF融合后取长补短
这不是巧合。BM25和TF-IDF基于不同的数学假设(概率检索模型 vs 向量空间模型),它们的失败模式天然互补。RRF的排名融合策略进一步放大了这种互补性——只要任一检索器把正确答案排进前列,融合后正确答案就能排到第一。
六、确定性检索:为什么监管行业不需要向量数据库
6.1 向量检索的不可复现问题
向量RAG的检索路径依赖三步:文本→Embedding向量→余弦相似度排序。三步中任何一步的变化都会导致结果不同:
- Embedding模型升级(如text-embedding-ada-002→text-embedding-3-large),全量向量变化
- 向量数据库版本更新,HNSW索引参数调整,近似近邻结果可能不同
- 批量推理时的浮点精度差异,同一文本的Embedding向量可能有微小波动
监管审计要求"从查询到答案的路径可追溯、可复现"。向量检索的上述不确定性直接违反这一要求。
6.2 BM25+TF-IDF的确定性
BM25和TF-IDF是纯数学运算,不涉及神经网络:
- 同样的文档集 + 同样的查询 = 同样的排序结果(bit-for-bit一致)
- 不依赖外部模型,不受模型版本升级影响
- 不依赖近似算法(HNSW是近似的,精确kNN才有确定结果但性能不可接受)
对于监管行业,确定性不是"nice to have"而是"must have"。regulated-rag的监管政策RAG引擎覆盖5份监管文件,支持自动识别发文机构(银保监/央行/证监会),每条检索结果都附带精确的术语命中位置和出处追溯——这是向量RAG做不到的。
七、快速上手
git clone https://github.com/yuzhaopeng-up/regulated-rag.git
cd regulated-rag
pip install -e . # 零外部依赖,仅用Python标准库
from regulated_rag import ProductManualRAG
# 加载产品手册知识库
engine = ProductManualRAG()
engine.index_documents(["product_manual_5g.md", "product_manual_cloud.md"])
# 检索
result = engine.ask("5G专网的资费标准是什么?")
print(result.answer) # 答案文本
print(result.source) # 出处:product_manual_5g.md, section="资费标准"
print(result.confidence) # 置信度:0.95
整个流程不需要API Key、不需要GPU、不需要向量数据库,git clone后30秒内跑通。
八、适用场景与局限
适合的场景
- 监管合规查询:精确术语匹配 + 可复现审计路径
- 产品手册问答:文档结构清晰,section标题语义明确
- 离线/边缘部署:零向量依赖,纯CPU毫秒级响应
- 审计追踪:每条结果可追溯到具体文档+section+命中词
不适合的场景
- 语义模糊查询:如"有没有便宜又好用的方案",BM25+TF-IDF无法处理语义近似
- 跨语言检索:不支持中英文混合查询的语义对齐
- 超大规模语料:百万级文档时,BM25的倒排索引构建可能比向量索引慢
对于需要语义理解的场景,可以采用混合策略:BM25+TF-IDF+RRF做初筛(保证确定性),向量RAG做精排(补充语义)。
Agent Skills开源生态
| 仓库 | 定位 | GitHub |
|---|---|---|
| financial-ai-skills | 金融AI技能库:104个场景纯Python实现 | https://github.com/yuzhaopeng-up/financial-ai-skills |
| teleagent-skills | 5个通用业务Skill:4-Phase编排+规则参数化 | https://github.com/yuzhaopeng-up/teleagent-skills |
| agent-cluster-comm | 多Agent集群5层通信架构 | https://github.com/yuzhaopeng-up/agent-cluster-comm |
| skill-framework | Skill治理框架:L0-L4分类+YAML模板 | https://github.com/yuzhaopeng-up/skill-framework |
| fintech-h5-demos | 57个零依赖金融H5演示 | https://github.com/yuzhaopeng-up/fintech-h5-demos |
| soe-compliant-office | 17个央国企合规办公Skill | https://github.com/yuzhaopeng-up/soe-compliant-office |
| agent-ops-toolkit | 企业级Agent运维基础设施 | https://github.com/yuzhaopeng-up/agent-ops-toolkit |
| regulated-rag | 零依赖RAG工具包:BM25+TF-IDF+RRF | https://github.com/yuzhaopeng-up/regulated-rag |
如果你在做监管行业的RAG系统,别急着上向量数据库。先试试BM25+TF-IDF+RRF,Hit@1=100%且审计友好。
Star regulated-rag 一起把监管RAG做到确定性可复现。
觉得有用?给个 Star 支持一下!你的 Star 是我们持续开源的最大动力。
更多开源 Agent Skills:financial-ai-skills · teleagent-skills · regulated-rag · fintech-h5-demos —— 全部在 github.com/yuzhaopeng-up,欢迎 Star/Fork/PR!