RAG优化系列:基于 TF‑IDF 的相关句子提取——轻量级文本压缩与精炼
本文介绍了一种基于TF-IDF的句子提取方法,用于优化RAG系统中的文本检索。该方法通过计算句子与查询的TF-IDF向量相似度,筛选出最相关的句子,从而减少冗余信息并提高回答准确性。文章详细讲解了TF-IDF原理、代码实现流程(包括句子分割、向量化和相似度计算),并提供了AI评估方法(使用LLM进行相关性打分)。此外,还总结了面试常见问题及解答,如TF-IDF优缺点、top_k选择策略等。该方法轻
【学习记录】RAG优化系列:基于 TF‑IDF 的相关句子提取——轻量级文本压缩与精炼
在 RAG 系统中,检索到的文本块往往包含冗余信息,直接送入 LLM 会浪费 token 并可能引入噪声。基于 TF‑IDF 的相关句子提取 是一种经典且轻量的方法,能够从文本块中筛选出与用户查询最相关的句子,从而实现文本压缩和关键信息聚焦。本文详解原理、完整代码实现、AI 评估方法以及面试高频问答,助你掌握这一实用技巧。
📌 目录
一、为什么需要句子级提取?
在 RAG 流程中,检索器通常会返回整个文本块(chunk),每个块可能有几十到几百字。但其中可能只有一两句话真正回答了用户的问题。如果直接将整个块送入 LLM:
- 浪费 token:增加成本,尤其当上下文窗口有限时。
- 引入噪声:无关信息可能干扰模型生成答案。
- 降低答案准确性:模型可能被冗余内容分散注意力。
句子级提取 的目标:从候选块中挑出最相关的若干句子,作为 LLM 的上下文。这样做可以:
- 减少 token 使用量。
- 提高信噪比。
- 允许在固定上下文窗口中塞入更多有用信息。
二、TF‑IDF 原理简介
TF‑IDF(Term Frequency-Inverse Document Frequency) 是一种用于信息检索和文本挖掘的经典统计方法,衡量一个词语对文档的重要程度。
- TF(词频):某个词在当前文档中出现的频率。频率越高,重要性越大。
- IDF(逆文档频率):衡量词语的普遍重要性。如果一个词在很多文档中都出现(如“的”、“是”),则 IDF 低;如果只在少数文档中出现(如“深度学习”),则 IDF 高。
TF‑IDF 公式:
[
\text{TF-IDF}(t,d) = \text{TF}(t,d) \times \text{IDF}(t)
]
在句子提取场景中,我们把每个句子当作一个“文档”,用户查询作为“查询文档”。计算每个句子的 TF‑IDF 向量与查询向量的余弦相似度(或内积),得分越高的句子与查询在词频上越相关。
三、句子提取流程与代码实现
3.1 工作流程图
3.2 完整代码(带 AI 评估)
import numpy as np
import re
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import openai
# ----------------------------- 1. 句子分割 -----------------------------
def split_sentences(text: str) -> list:
"""支持中英文标点的简单句子分割(生产环境建议使用 nltk/spacy)"""
# 匹配句号、感叹号、问号、分号等
sentences = re.split(r'(?<=[。!?;.!?;])', text)
return [s.strip() for s in sentences if s.strip()]
# ----------------------------- 2. TF‑IDF 句子提取 ---------------------
def extract_relevant_sentences(chunk_text: str, query: str, top_k: int = 3) -> list:
"""
返回与 query 最相关的 top_k 个句子(按余弦相似度排序)
"""
sentences = split_sentences(chunk_text)
if not sentences:
return []
# 构建语料:所有句子 + 查询(仅用于统一向量空间,查询不参与 IDF 计算更严谨)
corpus = sentences + [query]
vectorizer = TfidfVectorizer().fit(corpus)
sent_vectors = vectorizer.transform(sentences)
query_vector = vectorizer.transform([query])
# 计算余弦相似度
sims = cosine_similarity(sent_vectors, query_vector).flatten()
# 按相似度降序取 top_k
top_indices = np.argsort(sims)[::-1][:top_k]
return [sentences[i] for i in top_indices if sims[i] > 0]
# ----------------------------- 3. 示例用法 -----------------------------
if __name__ == "__main__":
chunk = """机器学习是人工智能的一个分支。它使计算机能够从数据中学习而不需要明确编程。
深度学习是机器学习的子集,使用多层神经网络。强化学习通过与环境交互获得奖励信号。"""
query = "什么是深度学习?"
top_sentences = extract_relevant_sentences(chunk, query, top_k=2)
print("提取的相关句子:")
for i, sent in enumerate(top_sentences, 1):
print(f"{i}. {sent}")
# ----------------------------- 4. AI 相关性评分 ---------------------
# 需要设置有效的 OpenAI API Key(或 DeepSeek 等兼容接口)
client = openai.OpenAI(api_key="your-api-key", base_url="https://api.openai.com/v1")
def llm_score_relevance(query: str, sentence: str) -> int:
"""使用大语言模型打分,0~10"""
prompt = f"""请根据以下句子与用户问题的相关程度,给出 0 到 10 之间的整数分数。
0=完全不相关,10=完全回答了问题。只输出数字。
用户问题:{query}
句子:{sentence}
分数:"""
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
max_tokens=2
)
try:
score = int(response.choices[0].message.content.strip())
except:
score = 0
return max(0, min(10, score))
print("\n相关性评分:")
for sent in top_sentences:
score = llm_score_relevance(query, sent)
print(f"【{score}分】 {sent[:50]}...")
输出示例:
提取的相关句子:
1. 深度学习是机器学习的子集,使用多层神经网络。
2. 机器学习是人工智能的一个分支。
相关性评分:
【10分】 深度学习是机器学习的子集,使用多层神经网络...
【3分】 机器学习是人工智能的一个分支...
四、AI 评估:相关性打分(0~10)
上述代码中的 llm_score_relevance 函数利用大语言模型对提取的每个句子进行打分。这为我们提供了自动评估手段:
- 对于多个查询,可以计算平均得分。
- 可以对比不同
top_k、不同文本块大小下的效果。 - 可以与更复杂的语义方法(如 Sentence‑BERT)做对比。
局限性:LLM 自身的偏差(如偏好长句、特定词汇)仍需注意,最好结合人工抽检。
五、面试官怎么问 & 怎么答
Q1:为什么使用 TF‑IDF 来提取相关句子?它有什么优缺点?
答:TF‑IDF 是无监督统计方法,不需要训练,速度快,可解释性强。
- 优点:轻量、适合快速原型、对领域无关。
- 缺点:无法捕捉同义词、近义词(如“汽车”≠“轿车”);对短文本效果差;依赖分词质量。
Q2:你的代码中用 cosine_similarity 计算相似度,与内积相比有何优势?
答:余弦相似度会对向量进行归一化,消除模长影响,只比较方向。TF‑IDF 向量的模长受句子长度影响较大,用余弦相似度更公平。内积在某些情况下也能排序,但不如余弦相似度稳定。我已改用 cosine_similarity。
Q3:如何选择 top_k 的值?过小或过大有什么影响?
答:top_k 取决于所需上下文的长度和 LLM 的 token 限制。
- 过小(如 1 句):可能遗漏关键信息。
- 过大(如 10 句):可能引入噪声,且浪费 token。
建议先凭经验设 2~5,然后用验证集通过 LLM 评分或下游任务效果调优。
Q4:如果句子与查询没有共同词汇,TF‑IDF 相似度为零,如何应对?
答:这是词袋模型的固有缺陷。解决方案:
- 使用语义嵌入(如 Sentence‑BERT)代替 TF‑IDF。
- 或采用混合策略:TF‑IDF 召回候选,再用 Cross‑Encoder 精排。
- 在提示词中要求 LLM 处理零匹配情况。
Q5:你的句子分割函数很简单,有没有更 robust 的方法?
答:正则分割容易在缩写(如“U.S.”)、小数点(“3.14”)、换行等场景出错。生产环境建议使用 nltk.sent_tokenize 或 spacy,它们经过专门训练,能处理复杂边界。
Q6:如何评估这种句子提取方法的效果?你 demo 中的 LLM 评分是否可靠?
答:LLM 评分是一种快速自动评估手段,尤其在无标注数据时。但 LLM 可能受自身偏见影响,最好结合:
- 人工抽样验证。
- 计算提取句子与黄金答案之间的 ROUGE / BLEU / 语义相似度。
- 在最终 RAG 系统上测试答案准确率提升。
Q7:在 RAG 系统中,什么时候应该做句子级提取而不是直接使用整个块?
答:当文本块较长(超过 200 词)且包含冗余信息,或者我们需要精炼上下文以节省 token 时,适合做句子提取。对于本身就短小的块(如少于 50 词),直接使用整个块更简单。句子提取常作为后处理环节,插在检索之后、LLM 生成之前。
六、总结与最佳实践
| 维度 | 说明 |
|---|---|
| 核心思想 | 用 TF‑IDF 计算句子与查询的词频相似度,筛选最相关的句子 |
| 实现步骤 | 句子分割 → TF‑IDF 向量化 → 余弦相似度 → 取 top_k |
| 评估方法 | LLM 打分(0~10)、人工评估、ROUGE/BLEU |
| 适用场景 | 文本块较长、需要压缩上下文、成本敏感的系统 |
| 不适用场景 | 语义相近但词汇不同(如同义词)、句子极短(<5 词) |
| 最佳实践 | 使用 cosine_similarity 而非内积;用 nltk/spacy 做句子分割;可与其他检索技术混合(如先召回再提取) |
句子级提取 是 RAG 系统中简单高效的优化手段。它不需要昂贵的模型,仅靠统计特征就能剔除大量无关内容。掌握本文的原理和代码,你可以在自己的项目中快速实现文本精炼,提升问答质量。
更多推荐



所有评论(0)