传统 RAG 的一个隐藏问题
传统 RAG Pipeline 有一个从不质疑的假设:所有问题都需要检索。
用户问"RAG 系统怎么评估"------检索。 用户问"1 + 1 等于几"------也检索。 用户问"帮我写一个求最大公约数的函数"------还是检索。
后两个问题完全不需要外部知识,强行检索不仅浪费资源,还可能把无关文档塞进上下文,干扰 LLM 的判断。
2023 年 Asai 等人提出的 Self-RAG,用一套"反思机制"解决了这个问题:模型在生成过程中会输出特殊的反思 token,自主决定何时检索、检索到的内容是否相关、最终答案是否有据可查。
Self-RAG 的四个反思 token
原始论文里,Self-RAG 训练了一个能输出四种特殊 token 的模型:
| Token | 含义 | 可能的值 |
|---|---|---|
[Retrieve] |
是否需要检索? | yes / no / continue |
[IsRel] |
检索到的文档与问题相关吗? | relevant / irrelevant |
[IsSup] |
生成的内容有文档支撑吗? | fully supported / partially supported / no support |
[IsUse] |
这个回答对用户有用吗? | 1~5 分 |
这四个 token 贯穿整个生成过程,让模型在不同阶段做出自适应决策,而不是盲目地"总是检索,总是使用"。
工程实现上,不需要专门训练带这些特殊 token 的模型------用普通 LLM + Prompt 模拟这四个判断节点,已经能取得不错的效果。
用 LangGraph 实现 Self-RAG
整体流程
css
用户问题
↓
[decide] 需要检索吗?
├─ yes → [retrieve] 向量检索 top-4
│ ↓
│ [filter] 逐篇判断相关性,过滤无关文档
│ ↓
│ [rag_generate] 基于相关文档生成答案
│ ↓
└─ no → [direct_generate] 直接生成答案
↓
[support_check] 回答有文档支撑吗?
↓
输出最终答案
State 设计
LangGraph 的核心是 State------在节点之间流转的状态对象:
python
class SelfRAGState(TypedDict):
question: str
need_retrieve: str # "yes" | "no"
retrieved_docs: list[Document]
relevant_docs: list[Document]
answer: str
support_verdict: str # "supported" | "unsupported"
token_count: int
path: list[str] # 记录执行路径
关键节点实现
节点1:检索决策(decide)
python
RETRIEVE_DECISION_PROMPT = ChatPromptTemplate.from_messages([
("system",
"你是一个 RAG 系统的路由决策器。判断以下问题是否需要检索外部知识库。\n\n"
"需要检索:涉及具体技术细节、参数、推荐选型等事实性内容\n"
"不需要检索:简单常识、数学计算、逻辑推理、闲聊问候\n\n"
"只输出 yes 或 no,不要解释。"),
("human", "问题:{question}"),
])
def make_decide_node(llm):
chain = RETRIEVE_DECISION_PROMPT | llm | StrOutputParser()
def decide(state):
result = chain.invoke({"question": state["question"]})
verdict = "yes" if "yes" in result.lower() else "no"
return {**state, "need_retrieve": verdict}
return decide
节点2:相关性过滤(filter)
python
RELEVANCE_PROMPT = ChatPromptTemplate.from_messages([
("system", "判断以下文档是否与问题相关,能够帮助回答该问题。\n"
"只输出 relevant 或 irrelevant,不要解释。"),
("human", "问题:{question}\n\n文档:{document}"),
])
def make_filter_node(llm):
chain = RELEVANCE_PROMPT | llm | StrOutputParser()
def filter_docs(state):
relevant = []
for doc in state["retrieved_docs"]:
result = chain.invoke({
"question": state["question"],
"document": doc.page_content[:300],
})
if "relevant" in result.lower() and "irrelevant" not in result.lower():
relevant.append(doc)
# 兜底:全部过滤时保留原始结果
return {**state, "relevant_docs": relevant or state["retrieved_docs"]}
return filter_docs
条件路由:decide 后的分叉
python
def route_after_decide(state) -> Literal["retrieve", "direct_generate"]:
return "retrieve" if state["need_retrieve"] == "yes" else "direct_generate"
graph.add_conditional_edges(
"decide",
route_after_decide,
{"retrieve": "retrieve", "direct_generate": "direct_generate"},
)
完整 Graph 构建
python
graph = StateGraph(SelfRAGState)
graph.add_node("decide", make_decide_node(llm))
graph.add_node("retrieve", make_retrieve_node(retriever))
graph.add_node("filter", make_filter_node(llm))
graph.add_node("rag_generate", make_rag_generate_node(llm))
graph.add_node("direct_generate", make_direct_generate_node(llm))
graph.add_node("support_check", make_support_node(llm))
graph.set_entry_point("decide")
graph.add_conditional_edges("decide", route_after_decide, {...})
graph.add_edge("retrieve", "filter")
graph.add_edge("filter", "rag_generate")
graph.add_edge("rag_generate", "support_check")
graph.add_edge("direct_generate", "support_check")
graph.add_edge("support_check", END)
self_rag_app = graph.compile()
实验设计
测试集包含两类问题:
- 8 条 RAG 相关问题:涉及 Embedding 模型选型、向量数据库、分块策略等,需要检索知识库
- 3 条无需检索的问题 :
1 + 1 等于几、今天天气怎么样、用 Python 写一个求最大公约数的函数
第二类问题是关键对照------Self-RAG 能否正确识别并跳过检索?
实验结果
检索决策准确率
css
Self-RAG 检索决策明细:
[✓ 检索] 什么是 RAG 技术,它主要解决什么问题?
[✓ 检索] 企业级应用应该选择哪种向量数据库?
[✓ 检索] 中文场景应该选择哪个 Embedding 模型?
[✓ 检索] 文档分块时 Chunk Size 一般推荐多少?
[✓ 检索] RAG 系统如何进行评估?
[✓ 检索] 混合检索相比纯向量检索有什么优势?
[✓ 检索] Rerank 在 RAG 中的作用是什么?
[✓ 检索] Parent-Child 分块策略解决了什么问题?
[✗ 跳过] 1 + 1 等于几?
[✗ 跳过] 今天天气怎么样?
[✗ 跳过] 用 Python 写一个求最大公约数的函数
检索决策统计:需要检索 8 条,直接回答 3 条
11/11 全部判断正确。 8 条知识库问题全部触发检索,3 条无需检索的问题全部走直接生成路径。
相关性过滤效果
css
执行路径详情(Self-RAG):
Q1: decide→yes → retrieve → filter(4/4) → rag_generate → support→supported
Q2: decide→yes → retrieve → filter(2/4) → rag_generate → support→supported
Q3: decide→yes → retrieve → filter(2/4) → rag_generate → support→unsupported
Q4: decide→yes → retrieve → filter(1/4) → rag_generate → support→supported
Q5: decide→yes → retrieve → filter(2/4) → rag_generate → support→supported
Q6: decide→yes → retrieve → filter(4/4) → rag_generate → support→unsupported
Q7: decide→yes → retrieve → filter(1/4) → rag_generate → support→supported
Q8: decide→yes → retrieve → filter(2/4) → rag_generate → support→supported
filter(1/4) 意味着 4 篇文档里只有 1 篇被判断为相关------过滤掉了 3 篇噪声。这正是 context_precision 提升的来源:给 LLM 的上下文更干净了。
support→unsupported 出现在 Q3 和 Q6,说明 LLM 在这两条问题上的回答超出了文档内容。完整版 Self-RAG 会在这里触发重新生成,本实验的简化版直接输出。
RAGAS 指标对比
diff
======================================================================
RAGAS 指标对比(知识库相关问题,8 条)
======================================================================
指标 固定检索 Self-RAG 变化
────────────────────────────────────────────────────────
context_recall 0.625 0.625 →+0.000
context_precision 0.583 0.688 ↑+0.104 ◀
faithfulness 0.845 0.866 ↑+0.021
answer_relevancy 0.404 0.401 →-0.003
======================================================================
- context_precision +0.104:过滤节点的直接贡献。固定检索把 4 篇文档全部交给 LLM,其中可能有不相关的;Self-RAG 过滤后只保留真正相关的文档,排序自然更准。
- context_recall 持平:过滤没有丢掉相关文档(兜底逻辑起了作用)。
- faithfulness 小幅提升 +0.021:更干净的上下文带来更少的幻觉。
Token 消耗:反直觉的结果
rust
Token 消耗对比(全部 11 条问题,估算值):
固定检索总消耗:~6,600 tokens
Self-RAG 总消耗:~16,050 tokens
Self-RAG 额外消耗:约 2.4x
Self-RAG 反而消耗了更多 token------这是本实验最反直觉的结果,值得仔细分析。
原因在于:Self-RAG 的每个反思节点都要额外调用 LLM。
| 节点 | 固定检索 | Self-RAG |
|---|---|---|
| 检索决策(decide) | --- | ✓ 每条问题 1 次 |
| 相关性过滤(filter) | --- | ✓ 每篇文档 1 次(最多 4 次) |
| 生成 | ✓ | ✓ |
| Support 评估 | --- | ✓ 每条问题 1 次 |
即便跳过了 3 条问题的检索,决策节点和过滤节点的开销仍然可观。在本实验里,"需要检索"的比例是 8/11 ≈ 73%,Self-RAG 的跳过收益无法覆盖额外的反思开销。
Self-RAG 节省 token 的条件:当"不需要检索"的问题占比足够高(通常 > 50%),且检索+生成的单次成本远高于决策节点时,Self-RAG 才能实现净节省。在 RAG 系统的真实使用场景中(用户大多数时候都在问知识库里的内容),这个条件并不容易满足。
Self-RAG 的真实价值
实验揭示了 Self-RAG 的价值边界:
真正的价值是质量提升,不是节省 token。
- 相关性过滤(filter 节点)让 context_precision 提升 +0.104------比多数检索优化策略的提升都更直接
- 路由决策保证了"不该检索的问题不检索",避免了无关文档干扰
- Support 评估提供了可观测性------可以知道哪些回答是"有据可查"的,哪些可能需要兜底
Self-RAG 最适合的场景:
- 混合型对话系统:有些问题需要检索知识库,有些是通用问题(天气、计算、闲聊)
- 对回答质量有严格要求,愿意为额外的反思步骤付出 token 成本
- 需要可观测性:想知道每条回答是否有文档支撑
不适合的场景:
- 纯知识库问答系统(几乎所有问题都需要检索)
- Token 预算极度敏感的场景
- 响应延迟要求极高的实时系统(每个节点都增加延迟)
完整代码
代码已开源:
核心文件:
self_rag.py--- LangGraph 实现的完整 Self-RAG 流程
运行方式:
bash
git clone https://github.com/chendongqi/llm-in-action
cd 14-self-rag
cp .env.example .env
pip install -r requirements.txt
python self_rag.py
小结
本文用 LangGraph 实现了简化版 Self-RAG,核心发现:
- 路由决策 11/11 全部正确:LLM 能准确区分"需要检索的技术问题"和"不需要检索的通用问题"
- 质量改善明显:filter 节点过滤无关文档,context_precision 提升 +0.104
- token 消耗反增 2.4x:反思节点(decide + filter + support)的开销超过了跳过检索的收益------这是真实工程中需要权衡的核心问题
Self-RAG 的本质是把"盲目执行"升级为"有意识的决策"。代价是复杂度和成本上升,收益是更精准的上下文和更可控的生成质量。在对话型、混合型 RAG 系统中,这个权衡往往是值得的。