最基础的 RAG 实现:TF-IDF 与 BM25 检索器解析
Naïve RAG(Retrieval-Augmented
Generation)是检索增强生成的最基础实现形式,其核心流程为“检索—阅读”两阶段:首先基于用户查询从静态文档库中检索相关文档,再将这些文档作为上下文输入大语言模型(LLM)以生成答案。在
Naïve RAG 中,检索通常依赖传统的关键词匹配方法,如
TF-IDF 和 BM25。
一、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)的标准检索算法。
改进之处
- 词频饱和:词频增加到一定程度后,对相关性贡献趋于平缓;
- 文档长度归一化:避免长文档因包含更多词而天然得分更高;
- 可调参数:通过超参数精细控制检索行为。
BM25 公式(简化形式)
- |d|:当前文档长度
- avgd:语料库平均文档长度
- k1:控制词频饱和程度(通常取
1.2–2.0)
- b:控制长度归一化强度(通常取
0.75)
直观效果
- 罕见词权重更高;
- 词频高但不过度奖励;
- 长文档不会因“堆词”而占优。
BM25
在保持关键词匹配优势的同时,显著提升了检索的公平性与准确性,因此在 Naïve
RAG 中常作为首选检索器。
三、RAG 中的 TF-IDF 检索流程
在 Naïve RAG 系统中,使用 TF-IDF 进行检索的具体步骤如下:
构建文档向量库
对所有文档分词,基于整个语料库计算 IDF,为每篇文档生成 TF-IDF
向量。
处理用户查询 对 query 分词,使用相同的
IDF 词典计算其 TF-IDF 向量(确保向量空间一致)。
计算相似度
通常采用余弦相似度:
- 返回 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) ---")
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 })
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: '数据库 系统 事务'
|