【学习记录】RAG优化系列:基于 TF‑IDF 的相关句子提取——轻量级文本压缩与精炼

在 RAG 系统中,检索到的文本块往往包含冗余信息,直接送入 LLM 会浪费 token 并可能引入噪声。基于 TF‑IDF 的相关句子提取 是一种经典且轻量的方法,能够从文本块中筛选出与用户查询最相关的句子,从而实现文本压缩和关键信息聚焦。本文详解原理、完整代码实现、AI 评估方法以及面试高频问答,助你掌握这一实用技巧。


📌 目录

  1. 为什么需要句子级提取?
  2. TF‑IDF 原理简介
  3. 句子提取流程与代码实现
  4. AI 评估:相关性打分(0~10)
  5. 面试官怎么问 & 怎么答
  6. 总结与最佳实践

一、为什么需要句子级提取?

在 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 工作流程图

文本块

句子分割

TF‑IDF 向量化

用户查询

计算相似度

排序取 top_k 句子

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_tokenizespacy,它们经过专门训练,能处理复杂边界。


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 系统中简单高效的优化手段。它不需要昂贵的模型,仅靠统计特征就能剔除大量无关内容。掌握本文的原理和代码,你可以在自己的项目中快速实现文本精炼,提升问答质量。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐