RAG 检索增强笔记:找到更多,不等于找到更好

这篇主要讲什么

这一份 PDF 对应 Part 15 ~ Part 18,重点在检索后的增强处理,以及长上下文带来的问题。

我读完后的一个直接感受是:

很多人以为 RAG 的难点在“找不到”,但实际系统里更常见的问题是“找到了太多,反而不知道该信谁”。

所以这篇更像是在处理检索结果的质量控制。

1. Re-ranking:先粗找一批,再精排一次

重排序的思路不复杂:

先用向量检索快速找出一批可能相关的内容,再用更强的方式重新判断谁更相关,把真正重要的内容排到前面。

这和搜索引擎的思路很像。

因为第一轮检索追求的是:

别漏掉。

而第二轮排序追求的是:

把最该看的放前面。

原文里提到两类方式:

  • RAG-Fusion 这种多查询融合后的排序
  • Cohere Rerank 这类专门的重排器

我对重排序的理解是:

它不是替代检索,而是在补检索的短板。

向量检索擅长快速召回,但不一定最会排前后。重排序就是补上“判断谁更重要”的那一步。

2. CRAG:先怀疑一下检索结果靠不靠谱

PDF 里给了 CRAG 的深入资料入口。虽然正文没展开很多代码,但方向很明确:

不要默认检索出来的内容一定可靠,而是要对检索结果做评估。

如果检索质量不好,就做修正,比如补检索、换策略、或者走别的流程。

我很认同这个思路,因为它本质上是在给 RAG 加一层“自检”。

很多 RAG 系统最大的问题就是:

拿到第一批结果就直接答,系统完全没有“这批资料到底靠不靠谱”的意识。

而 CRAG 这类方法提醒我们,检索结果也该被审查。

3. Self-RAG:让系统边答边反思

Self-RAG 也是一种很有代表性的思路。

它不是把“检索”和“生成”看成一次性动作,而是让模型在过程中判断:

  • 现在需不需要检索
  • 当前证据够不够
  • 回答是否需要修正

这说明更成熟的 RAG,不再是固定流水线,而是带一点动态决策能力。

我自己的看法是:

Self-RAG 很像把一个死板流程,慢慢变成一个会边走边判断的系统。

这很合理,但也意味着流程会更复杂,更依赖评估和调参。

4. Long Context:上下文很长,不代表问题自动解决

这一部分我觉得特别重要。

很多人看到大模型上下文窗口越来越长,就会产生一种想法:

既然能塞很多内容进去,是不是就不需要认真做检索了?

这篇给出的方向其实是在提醒大家:

长上下文不等于高质量上下文。

把很多材料都丢进去,可能带来这些问题:

  • 关键信息被淹没
  • 无关信息干扰判断
  • 成本上升
  • 响应变慢

我自己的观点也很明确:

长上下文是能力,不是借口。它可以放宽限制,但不能替代检索设计。

如果系统没有筛选能力,只会把更多噪音一起送进模型,那窗口再大也只是“更昂贵地混乱”。

这份内容的核心思想

我觉得这一篇最核心的一句话是:

召回更多内容只是开始,后面还要判断哪些内容真的值得相信、值得放进最终上下文。

我自己的理解

我会把这一篇理解成 RAG 的“质量控制层”。

基础检索像是海选,重排序像复试,CRAG 和 Self-RAG 则像现场质检。

而长上下文问题提醒我们:

别把“能装很多”误以为“用得很好”。

实践里的一个朴素原则

如果让我给这一篇提炼一个最实用的原则,我会写成:

宁可给模型少而准的上下文,也不要给它多而乱的上下文。

因为高质量 RAG 的目标,从来不是“塞满窗口”,而是“把最有用的证据放进去”。

一句总结

这一篇让我最深的印象是:RAG 到后期,拼的已经不只是检索能力,而是筛选能力、判断能力和上下文管理能力。找到很多不是终点,找到对的、排好顺序、控制噪音,系统才会稳。

核心代码实现

方法 1:Re-ranking

1
2
3
4
5
6
7
8
9
10
11
12
def rerank(question):
# 第一步:先粗召回
candidate_docs = retriever.invoke(question, k=10)

# 第二步:再重排
scored = []
for doc in candidate_docs:
score = rerank_model.score(question, doc.page_content)
scored.append((doc, score))

ranked = sorted(scored, key=lambda x: x[1], reverse=True)
return [doc for doc, _ in ranked[:4]]

方法 2:CRAG

1
2
3
4
5
6
7
8
9
10
11
def crag_answer(question):
docs = retriever.invoke(question, k=5)
quality = judge_retrieval_quality(question, docs)

if quality == "good":
return llm.invoke(build_prompt(question, docs))

# 如果觉得检索结果不够好,就补救
repaired_question = rewrite_question(question)
new_docs = retriever.invoke(repaired_question, k=5)
return llm.invoke(build_prompt(question, new_docs))

方法 3:Self-RAG

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def self_rag(question):
state = {"question": question, "docs": [], "answer": None}

if need_retrieval(state):
state["docs"] = retriever.invoke(question, k=4)

draft = llm.invoke(build_prompt(question, state["docs"]))

if answer_not_grounded(draft, state["docs"]):
extra_docs = retriever.invoke(rewrite_question(question), k=4)
state["docs"].extend(extra_docs)
draft = llm.invoke(build_prompt(question, state["docs"]))

state["answer"] = draft
return state["answer"]

方法 4:长上下文控制

1
2
3
4
5
6
7
8
9
10
11
12
13
def build_context(question):
docs = rerank(question)

# 不要无脑塞满,把最有用的几段留下
selected = select_top_chunks(docs, max_tokens=4000)
context = "\n\n".join(doc.page_content for doc in selected)
return context

def answer_with_long_context_control(question):
context = build_context(question)
return llm.invoke(
f"请只根据以下高相关上下文回答:\n{context}\n\n问题:{question}"
)