课程0基础Agent开发课 / RAG与向量数据库 / Self-RAG与CRAG-让RAG系统自我纠错
— 20 min read

Self-RAG与CRAG-让RAG系统自我纠错

> **[进阶选读]** 本篇适合遇到以下问题的读者:检索出来的文档质量参差不齐,模型有时候会用不相关的内容编造答案。Self-RAG 和 CRAG 让系统能主动判断"这次检索结果是否可用"并在不够时自动纠正。需要有 LangGraph 基础。

Self-RAG 与 CRAG:让 RAG 系统自我纠错

[进阶选读] 本篇适合遇到以下问题的读者:检索出来的文档质量参差不齐,模型有时候会用不相关的内容编造答案。Self-RAG 和 CRAG 让系统能主动判断"这次检索结果是否可用"并在不够时自动纠正。需要有 LangGraph 基础。


1.1 基础 RAG 的根本局限

不相关

相关

输入问题

LLM 判断

[Retrieve]
需要检索?

直接生成

执行检索

检索文档

[IsRel]
文档相关?

丢弃文档

纳入上下文

LLM 生成回答

[IsUse]
回答有用?

[IsFat]
回答忠实?

标注未支持

输出回答

Self-RAG 自适应流程——通过特殊 Token 实现检索决策、相关性评估与质量自检

标准 RAG 系统存在一个根本性的设计缺陷:对检索结果照单全收。无论向量数据库返回的文档质量如何——相关或不相关,足够或不足,准确或过时——系统都会将其塞入 Prompt 让模型生成答案。

这导致两类典型问题:

检索结果不相关时: 模型要么根据无关文档产生幻觉,要么给出"根据文档无法回答"的无效回复,而实际上正确答案可能存在于未被检索到的文档中,或者可以通过其他途径获取(如网络搜索)。

问题本身不需要检索时: 对于"1+1等于几"这类常识性问题,系统仍然执行检索流程,浪费延迟和 token。

Self-RAG 和 CRAG 是两篇独立论文提出的解决方案,分别从不同角度让 RAG 系统具备"自我判断"和"自我纠错"的能力。

LangGraph 是 LangChain 框架下的一个组件,专门用于构建有状态的 Agent 工作流,通过"节点"(执行步骤)和"边"(流转逻辑)描述复杂的决策流程。下面的代码示例都基于 LangGraph 实现。


1.2 Self-RAG 架构

Self-RAG(Self-Reflective Retrieval-Augmented Generation,自反思检索增强生成,Asai et al. 2023)的核心思想是:训练模型生成特殊的反思 token(特殊标记词),让模型自主判断何时需要检索,以及检索结果的质量

1.2.1 四种反思 token

Self-RAG 在模型的生成过程中引入四种特殊标记:

Retrieve token: 判断当前是否需要检索。模型在生成过程中自主决定是否调用检索器,而不是每次都强制检索。

ISREL token(Is Relevant): 判断检索到的文档是否与问题相关。每个检索到的文档片段都会获得一个相关性评分。

ISSUP token(Is Supported): 判断生成的答案片段是否有文档支撑。这个 token 在生成每个句子后出现,评估该句子是否基于检索文档。

ISUSE token(Is Useful): 判断整体回答是否对用户有用。作为最终的质量控制。

1.2.2 用 LangGraph 实现 Self-RAG 流程

原版 Self-RAG 需要专门微调的模型。以下用 LangGraph 实现一个基于 Prompt Engineering 的近似版本,适合生产使用:

python
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command
from langchain_openai import ChatOpenAI
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_core.messages import HumanMessage, SystemMessage

class SelfRAGState(TypedDict):
    question: str
    needs_retrieval: bool
    retrieved_docs: list[str]
    docs_relevance: list[str]      # "relevant" 或 "not_relevant"
    answer_draft: str
    is_supported: bool
    is_useful: bool
    final_answer: str
    iteration: int

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
vectorstore = Chroma(
    collection_name="knowledge_base",
    embedding_function=OpenAIEmbeddings(),
)

# 节点1:判断是否需要检索
def assess_retrieval_need(state: SelfRAGState) -> Command:
    response = llm.invoke([
        SystemMessage(content="""判断以下问题是否需要从知识库检索信息才能准确回答。
对于常识性问题、数学计算、明确的事实(如"中国首都是哪里")回答:no_retrieval
对于需要专业知识、最新信息、或具体领域知识的问题回答:needs_retrieval
只输出 no_retrieval 或 needs_retrieval,不加任何解释。"""),
        HumanMessage(content=state["question"])
    ])

    decision = response.content.strip().lower()
    needs = "needs_retrieval" in decision

    if needs:
        return Command(goto="retrieve", update={"needs_retrieval": True})
    else:
        return Command(goto="generate_direct", update={"needs_retrieval": False})

# 节点2a:直接回答(不需要检索)
def generate_direct(state: SelfRAGState) -> Command:
    response = llm.invoke([HumanMessage(content=state["question"])])
    return Command(
        goto="assess_usefulness",
        update={"answer_draft": response.content, "is_supported": True}
    )

# 节点2b:检索文档
def retrieve(state: SelfRAGState) -> Command:
    results = vectorstore.similarity_search(state["question"], k=4)
    docs = [doc.page_content for doc in results]
    return Command(goto="assess_relevance", update={"retrieved_docs": docs})

# 节点3:评估检索文档的相关性
def assess_relevance(state: SelfRAGState) -> Command:
    relevance_list = []
    for doc in state["retrieved_docs"]:
        response = llm.invoke([
            SystemMessage(content="""判断以下文档片段是否与给定问题相关。
输出 relevant 或 not_relevant,不加任何解释。"""),
            HumanMessage(content=f"问题:{state['question']}\n\n文档片段:{doc[:300]}")
        ])
        relevance_list.append(response.content.strip().lower())

    # 过滤:只保留相关文档
    relevant_docs = [
        doc for doc, rel in zip(state["retrieved_docs"], relevance_list)
        if "relevant" in rel and "not" not in rel
    ]

    if not relevant_docs:
        # 没有相关文档:直接生成,标记不支持
        return Command(
            goto="generate_direct",
            update={"docs_relevance": relevance_list, "retrieved_docs": []}
        )

    return Command(
        goto="generate_with_docs",
        update={"docs_relevance": relevance_list, "retrieved_docs": relevant_docs}
    )

# 节点4:基于相关文档生成答案
def generate_with_docs(state: SelfRAGState) -> Command:
    context = "\n\n".join(state["retrieved_docs"])
    response = llm.invoke([
        SystemMessage(content="严格基于提供的文档内容回答问题,不添加文档中没有的信息。"),
        HumanMessage(content=f"文档:\n{context}\n\n问题:{state['question']}")
    ])
    return Command(goto="assess_support", update={"answer_draft": response.content})

# 节点5:评估答案是否有文档支撑
def assess_support(state: SelfRAGState) -> Command:
    if not state["retrieved_docs"]:
        return Command(goto="assess_usefulness", update={"is_supported": False})

    context = "\n\n".join(state["retrieved_docs"])
    response = llm.invoke([
        SystemMessage(content="""判断给定答案中的主要陈述是否都能从文档中找到支撑。
如果答案中存在文档未提及的关键陈述,输出 not_supported。
否则输出 supported。
只输出这两个词之一。"""),
        HumanMessage(content=f"文档:\n{context}\n\n答案:{state['answer_draft']}")
    ])

    supported = "not_supported" not in response.content.lower()
    return Command(goto="assess_usefulness", update={"is_supported": supported})

# 节点6:评估答案是否有用
def assess_usefulness(state: SelfRAGState) -> Command:
    iteration = state.get("iteration", 0)

    response = llm.invoke([
        SystemMessage(content="""判断以下答案是否直接、有效地回答了问题。
如果答案有用,输出 useful。
如果答案无法回答问题或没有实质内容,输出 not_useful。
只输出这两个词之一。"""),
        HumanMessage(content=f"问题:{state['question']}\n\n答案:{state['answer_draft']}")
    ])

    is_useful = "not_useful" not in response.content.lower()

    # 如果无用且还有重试次数,尝试重新生成
    if not is_useful and iteration < 2:
        return Command(
            goto="retrieve",
            update={"is_useful": False, "iteration": iteration + 1}
        )

    return Command(
        goto=END,
        update={"is_useful": is_useful, "final_answer": state["answer_draft"]}
    )

# 构建图
builder = StateGraph(SelfRAGState)
for name, fn in [
    ("assess_retrieval_need", assess_retrieval_need),
    ("generate_direct", generate_direct),
    ("retrieve", retrieve),
    ("assess_relevance", assess_relevance),
    ("generate_with_docs", generate_with_docs),
    ("assess_support", assess_support),
    ("assess_usefulness", assess_usefulness),
]:
    builder.add_node(name, fn)

builder.add_edge(START, "assess_retrieval_need")
self_rag_graph = builder.compile()

1.3 Corrective RAG(CRAG)架构

CRAG(Corrective RAG,纠正式检索增强生成,Yan et al. 2024)的关注点与 Self-RAG 不同:它不修改模型训练,而是在检索和生成之间加入一个评估-纠正环节,根据检索质量动态选择处理策略。

1.3.1 三种处理策略

CRAG 对每个检索结果评分,然后根据得分选择策略:

策略一:直接使用(Correct) — 检索质量高,直接用检索结果生成答案。

策略二:补充搜索(Ambiguous) — 检索质量中等,结合向量检索结果和网络搜索结果共同生成。

策略三:完全替换(Incorrect) — 检索质量差,放弃向量检索结果,完全使用网络搜索。

1.3.2 用 LangGraph 实现 CRAG

python
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command
from langchain_openai import ChatOpenAI
from langchain_community.tools import TavilySearchResults
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_core.messages import HumanMessage, SystemMessage

class CRAGState(TypedDict):
    question: str
    retrieved_docs: list[str]
    retrieval_grade: Literal["correct", "ambiguous", "incorrect"]
    web_results: list[str]
    final_context: list[str]
    answer: str

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
vectorstore = Chroma(
    collection_name="knowledge_base",
    embedding_function=OpenAIEmbeddings(),
)
web_search = TavilySearchResults(max_results=3)  # 需要 TAVILY_API_KEY

# 节点1:从向量库检索
def retrieve_from_vectorstore(state: CRAGState) -> dict:
    results = vectorstore.similarity_search(state["question"], k=4)
    docs = [doc.page_content for doc in results]
    return {"retrieved_docs": docs}

# 节点2:评估检索质量(核心节点)
def grade_retrieval(state: CRAGState) -> Command:
    docs_text = "\n\n---\n\n".join(state["retrieved_docs"])

    response = llm.invoke([
        SystemMessage(content="""评估检索到的文档与问题的相关性。
根据以下标准给出评分:
- correct:文档高度相关,包含回答问题所需的完整信息
- ambiguous:文档部分相关,可以提供一些信息但不够完整,需要补充
- incorrect:文档与问题不相关,无法用于回答问题

只输出 correct、ambiguous 或 incorrect 三者之一。"""),
        HumanMessage(content=f"问题:{state['question']}\n\n检索文档:\n{docs_text[:2000]}")
    ])

    grade = response.content.strip().lower()
    if grade not in ["correct", "ambiguous", "incorrect"]:
        grade = "ambiguous"  # 默认保守策略

    if grade == "correct":
        # 直接进入生成,不需要网络搜索
        return Command(
            goto="generate_answer",
            update={
                "retrieval_grade": "correct",
                "final_context": state["retrieved_docs"]
            }
        )
    elif grade == "ambiguous":
        # 补充网络搜索
        return Command(
            goto="web_search",
            update={"retrieval_grade": "ambiguous"}
        )
    else:
        # 完全使用网络搜索
        return Command(
            goto="web_search",
            update={"retrieval_grade": "incorrect", "retrieved_docs": []}
        )

# 节点3:网络搜索(Fallback)
def web_search_node(state: CRAGState) -> Command:
    try:
        results = web_search.invoke(state["question"])
        web_docs = [r["content"] for r in results if isinstance(r, dict) and "content" in r]
    except Exception as e:
        web_docs = [f"网络搜索失败:{str(e)}"]

    if state["retrieval_grade"] == "ambiguous":
        # 合并:向量检索 + 网络搜索
        combined = state["retrieved_docs"] + web_docs
    else:
        # 只用网络搜索
        combined = web_docs

    return Command(
        goto="generate_answer",
        update={"web_results": web_docs, "final_context": combined}
    )

# 节点4:生成最终答案
def generate_answer(state: CRAGState) -> dict:
    context = "\n\n".join(state["final_context"])
    response = llm.invoke([
        SystemMessage(content="基于提供的文档内容准确回答问题,如果文档不足以回答,请如实说明。"),
        HumanMessage(content=f"文档:\n{context}\n\n问题:{state['question']}")
    ])
    return {"answer": response.content}

# 构建图
builder = StateGraph(CRAGState)
builder.add_node("retrieve_from_vectorstore", retrieve_from_vectorstore)
builder.add_node("grade_retrieval", grade_retrieval)
builder.add_node("web_search_node", web_search_node)
builder.add_node("generate_answer", generate_answer)

builder.add_edge(START, "retrieve_from_vectorstore")
builder.add_edge("retrieve_from_vectorstore", "grade_retrieval")
builder.add_edge("generate_answer", END)

crag_graph = builder.compile()

# 使用示例
result = crag_graph.invoke({
    "question": "LangGraph 0.2 版本有哪些新特性?",
    "retrieved_docs": [],
    "retrieval_grade": "correct",
    "web_results": [],
    "final_context": [],
    "answer": "",
})
print(result["answer"])

1.3.3 CRAG 决策流程图

correct
文档高度相关

ambiguous
部分相关

incorrect
文档不相关

ambiguous

incorrect

开始

向量库检索
Top-K 文档

检索质量评估
LLM Grader

final_context
= 向量检索结果

网络搜索
Tavily API

final_context
= 向量结果 + 网络结果

final_context
= 仅网络搜索结果

生成答案

结束


1.4 Self-RAG 与 CRAG 的对比

对比维度 Self-RAG CRAG
核心机制 模型内生成反思 token,自主判断 外部评估器评估检索质量
模型要求 原版需要微调;近似实现用 Prompt 任何 LLM 均可使用
是否需要检索 模型自主判断(Retrieve token) 总是先检索,评估后决定
检索失败处理 重试检索 切换到网络搜索
外部工具依赖 需要 Web Search API(如 Tavily)
答案质量控制 ISSUP + ISUSE 双重检验 通过上下文质量间接控制
实现复杂度 较高(多个反思环节) 中等(评估 + 路由)
适用场景 对答案忠实度要求极高;不希望依赖外部搜索 知识库覆盖不完整;允许联网补充
延迟影响 多次 LLM 调用,延迟较高 额外一次评估调用 + 可能的网络搜索

选型建议:

选 CRAG 的场景:知识库可能覆盖不全,或者需要回答最新信息(新闻、文档更新);对实现复杂度敏感;不需要严格控制是否联网。

选 Self-RAG(近似实现)的场景:私有环境,不能访问互联网;对答案忠实度要求极高;需要系统能够主动判断"我不知道"而不是给出错误答案。

两者也可以组合:先用 CRAG 的评估机制判断检索质量,再用 Self-RAG 的 ISSUP 机制验证生成答案的支撑度。

本页目录