Naïve RAG
Zhongjun Qiu 元婴开发者

最基础的 RAG 实现:TF-IDF 与 BM25 检索器解析

Naïve RAG(Retrieval-Augmented Generation)是检索增强生成的最基础实现形式,其核心流程为“检索—阅读”两阶段:首先基于用户查询从静态文档库中检索相关文档,再将这些文档作为上下文输入大语言模型(LLM)以生成答案。在 Naïve RAG 中,检索通常依赖传统的关键词匹配方法,如 TF-IDFBM25

一、TF-IDF:从评分函数到向量表示

核心思想

TF-IDF 衡量一个词在特定文档中的重要性,其逻辑包含两部分:

  • 词频(TF):词在当前文档中出现越频繁,越可能重要;
  • 逆文档频率(IDF):词在整个语料库中出现的文档越少,越具有区分性。

其中 N 为文档总数,DF(t) 为包含词 t 的文档数量。 TF-IDF(t, d) = TF(t, d) × IDF(t)

两种视角:函数 vs 向量

在信息检索理论中,TF-IDF 是一个评分函数,用于计算某个词对某篇文档的权重。 但在实际工程(如 RAG)中,TF-IDF 被用作文档的向量表示

  • 每个文档被表示为一个向量,其每一维对应语料库中的一个词,值为该词在该文档中的 TF-IDF 权重;
  • 查询(query)同样被转换为 TF-IDF 向量;
  • 通过计算 query 向量与所有文档向量的余弦相似度,选出最相关的 top-k 文档。

关键理解:TF-IDF 本身是 term-doc 的权重公式,但在 RAG 中,它被“批量应用”于整个语料库,形成稀疏的向量空间模型(Term-Document Matrix),从而支持高效的相似度检索。

优缺点

  • 优点:实现简单、计算高效、可解释性强、对关键词匹配效果好;
  • 缺点:忽略语义、无法处理同义词/多义词、对文档长度敏感、不考虑词序和上下文。

二、BM25:TF-IDF 的现代进化版

BM25 是对 TF-IDF 的重要改进,已成为主流搜索引擎(如 Elasticsearch、Lucene)的标准检索算法。

改进之处

  1. 词频饱和:词频增加到一定程度后,对相关性贡献趋于平缓;
  2. 文档长度归一化:避免长文档因包含更多词而天然得分更高;
  3. 可调参数:通过超参数精细控制检索行为。

BM25 公式(简化形式)

  • |d|:当前文档长度
  • avgd:语料库平均文档长度
  • k1:控制词频饱和程度(通常取 1.2–2.0)
  • b:控制长度归一化强度(通常取 0.75)

直观效果

  • 罕见词权重更高;
  • 词频高但不过度奖励;
  • 长文档不会因“堆词”而占优。

BM25 在保持关键词匹配优势的同时,显著提升了检索的公平性与准确性,因此在 Naïve RAG 中常作为首选检索器。


三、RAG 中的 TF-IDF 检索流程

在 Naïve RAG 系统中,使用 TF-IDF 进行检索的具体步骤如下:

  1. 构建文档向量库 对所有文档分词,基于整个语料库计算 IDF,为每篇文档生成 TF-IDF 向量。

  2. 处理用户查询 对 query 分词,使用相同的 IDF 词典计算其 TF-IDF 向量(确保向量空间一致)。

  3. 计算相似度 通常采用余弦相似度

  1. 返回 top-k 文档 将得分最高的若干文档作为上下文,输入 LLM 生成最终答案。

注意:TF-IDF 向量是静态、稀疏、无语义的嵌入,仅反映词汇共现统计,无法捕捉“苹果(水果)”与“苹果(公司)”的区别,也无法理解“汽车”和“车辆”的语义相近性。


四、代码示例:TF-IDF 在 RAG 中的实际应用

以下 Python 示例展示了如何用 sklearn 实现 TF-IDF 检索:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

docs = ["数据库 系统 事务", "大模型 检索 RAG 系统"]
vectorizer = TfidfVectorizer()
doc_vectors = vectorizer.fit_transform(docs)

# 输出词汇表 (列索引到分词的映射)
print("--- 词汇表 (Column Index: Term) ---")
# TfidfVectorizer.vocabulary_ 是 {term: index},我们需要反转它
vocabulary_inv = {v: k for k, v in vectorizer.vocabulary_.items()}
sorted_vocabulary = [vocabulary_inv[i] for i in range(len(vocabulary_inv))]
print(sorted_vocabulary)
print("------------------------------------")


print("--- Doc Vectors (TF-IDF Matrix) ---")
print(doc_vectors.todense())
print("-----------------------------------")


query = ["RAG 检索", "系统"]
query_vec = vectorizer.transform(query)

print("--- Query Vectors ---")
print(query_vec.todense())
print("---------------------")

scores = cosine_similarity(query_vec, doc_vectors)

results = []
for i, query_score_row in enumerate(scores):
top_doc_index = np.argmax(query_score_row)
print(query[i], ": Document Score", query_score_row, top_doc_index)
most_similar_doc = docs[top_doc_index]

results.append({
"Query": query[i],
"Most Similar Document": most_similar_doc
})

# 7. 打印结果
for result in results:
print(f"Query: '{result['Query']}' -> Best Match: '{result['Most Similar Document']}'")

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--- 词汇表 (Column Index: Term) ---
['rag', '事务', '大模型', '数据库', '检索', '系统']
------------------------------------
--- Doc Vectors (TF-IDF Matrix) ---
[[0. 0.6316672 0. 0.6316672 0. 0.44943642]
[0.53404633 0. 0.53404633 0. 0.53404633 0.37997836]]
-----------------------------------
--- Query Vectors ---
[[0.70710678 0. 0. 0. 0.70710678 0. ]
[0. 0. 0. 0. 0. 1. ]]
---------------------
RAG 检索 : Document Score [0. 0.75525556] 1
系统 : Document Score [0.44943642 0.37997836] 0
Query: 'RAG 检索' -> Best Match: '大模型 检索 RAG 系统'
Query: '系统' -> Best Match: '数据库 系统 事务'
 REWARD AUTHOR
 Comments
Comment plugin failed to load
Loading comment plugin