17 Self-RAG —— 幻觉检测 + 答案质量评估

Self-RAG ------ 幻觉检测 + 答案质量评估

自适应 RAG 基础上,对"生成结果"也加入自我审查,构成真正的 Self-RAG。

解决什么问题?

自适应 RAG 只检查"检索文档是否相关",但 LLM 仍可能基于相关文档"幻觉发挥",生成文档中根本没有的内容。故再加两道闸门:

检查节点 检查内容 失败后动作
check_hallucination 答案有没有编造文档外的内容? → 重写查询重来
grade_answer 答案是否真正回答了问题? → 重写查询重来
finalize 通过双重检查 → 写入消息历史,结束

核心新概念

  1. 多级条件路由 --- 同一张图中三处 add_conditional_edges,每个节点独立路由
  2. 链式 Pydantic --- 三种评判各用不同的结构化输出模型(DocGradeResult / HallucinationResult / AnswerGradeResult)
  3. 状态扩展 --- 新增 answer、hallucination_ok、answer_ok 三个字段,answer 先暂存不写消息历史,通过检查后再由 finalize 节点写入

LangGraph 图结构(新增两个节点、两条条件边)

复制代码
  START
    ↓
  retrieve
    ↓
  grade_docs ──[irrelevant]────────────────────────────→ rewrite
             └─[relevant]──→ generate
                               ↓
                          check_hallucination
                          ├─[grounded]──→ grade_answer
                          │                ├─[useful]────→ finalize → END
                          │                └─[not_useful]→ rewrite
                          └─[hallucinated]──────────────→ rewrite
                                 ↑
                         rewrite → retrieve(循环,MAX_RETRIES 防护)

提示词

py 复制代码
GRADE_PROMPT = """你是文档相关性评判助手。
请判断以下文档内容是否能够帮助回答给定问题。

问题:{question}

文档内容(共 {n} 段):
{context}

判断标准:文档中是否含有可以直接或间接用于回答该问题的信息?
"""

HALLUCINATION_PROMPT = """你是事实核查助手。
请判断"生成答案"中的每一个关键事实是否都能在"参考文档"中找到依据。

参考文档:
{context}

生成答案:
{answer}

如果答案中含有文档之外的信息(编造事实),请将 grounded 设为 False。
只要所有事实都能在文档中找到支撑,就设为 True。
"""

ANSWER_GRADE_PROMPT = """你是答案质量评估助手。
请判断下面的答案是否真正解答了用户的问题。

用户问题:{question}

生成答案:{answer}

判断标准:
  - 答案是否直接回应了问题的核心?
  - 答案是否具体、有实质内容(而非"资料中未找到"之类的空洞回应)?
"""

REWRITE_PROMPT = """你是查询优化助手。
以下查询未能检索到满意结果,请将其改写得更具体、更易于文档检索。
只输出改写后的查询,不要任何解释。

原始查询:{question}
改写后的查询:"""

GENERATE_PROMPT = """你是专业问答助手,只能根据给定参考资料回答问题。
资料不足时,直接说"资料中未找到相关信息",不要编造。

参考资料:
{context}

问题:{question}"""

定义状态数据

py 复制代码
class DocGradeResult(BaseModel):
    """① 文档相关性评分"""

    score: str = Field(description="'relevant':表示相关,'irrelevant':表示不相关")
    reason: str = Field(description="一句话判断理由")


class HallucinationResult(BaseModel):
    """② 幻觉检测:答案是否忠实于检索文档"""

    grounded: bool = Field(
        description="True = 答案完全基于文档,False = 答案包含文档之外的内容(幻觉)"
    )
    reason: str = Field(description="一句话判断理由")


class AnswerGradeResult(BaseModel):
    """③ 答案质量:是否真正解答了用户问题"""

    useful: bool = Field(
        description="True = 答案切实回答了问题;False = 答案答非所问或过于空洞"
    )
    reason: str = Field(description="一句话判断理由")


class RAGState(TypedDict):
    messages: Annotated[list, add_messages]
    question: str
    context: list[str]
    grade: str
    answer: str
    hallucination_ok: bool
    answer_ok: bool
    retry_count: int

重写节点:重置幻觉/答案检查标志,

py 复制代码
def make_rewrite_node(llm: ChatOpenAI):
    """查询重写节点:重置幻觉/答案检查标志,避免旧值干扰新一轮循环。"""

    def rewrite_node(state: RAGState) -> dict:
        question = state["question"]
        prompt = REWRITE_PROMPT.format(question=question)
        response = llm.invoke([HumanMessage(content=prompt)])
        new_question = response.content.strip()
        retry = state.get("retry_count", 0) + 1
        print(f"[重写] 第 {retry} 次:'{question}' → '{new_question}'")
        return {
            "question": new_question,
            "retry_count": retry,
            "answer": "",
            "hallucination_ok": False,
            "answer_ok": False,
        }

    return rewrite_node

生成节点:将答案存入 state"answer"

py 复制代码
def make_generate_node(llm: ChatOpenAI):
    def generate_node(state):
        context = state.get("context")
        question = state["question"]
        context_text = "\n\n".join(context) if context else "无相关文档"
        prompt = GENERATE_PROMPT.format(question=question, context=context_text)
        response = llm.invoke(
            [SystemMessage(content="你是专业问答助手"), HumanMessage(content=prompt)]
        )
        answer = response.content.strip()
        return {"answer": answer}

    return generate_node

幻觉检测,检查生成答案是否忠实于检索文档。

py 复制代码
def make_hallucination_node(llm: ChatOpenAI):
    """
    幻觉检测
    检查生成答案是否忠实于检索文档。
    使用 HallucinationResult(含 grounded: bool)做结构化输出。
    """
    checker = llm.with_structured_output(HallucinationResult, method="function_calling")

    def hallucination_node(state):
        context = state.get("context")
        answer = state.get("answer")
        if not answer:
            print("[幻觉检测] 无答案可检查")
            return {"hallucination_ok": False}
        context_text = "\n\n".join(context) if context else "无相关文档"
        prompt = HALLUCINATION_PROMPT.format(context=context_text, answer=answer)
        result: HallucinationResult = checker.invoke([HumanMessage(content=prompt)])
        status_text = "✓ 无幻觉" if result.grounded else "✗ 检测到幻觉"
        print(f"[幻觉检测] {status_text}({result.reason})")
        return {"hallucination_ok": result.grounded}

    return hallucination_node

答案质量评估

py 复制代码
def make_answer_grade_node(llm: ChatOpenAI):
    """
    答案质量评估
    检查答案是否真正解答了用户问题。
    只有通过幻觉检测(hallucination_ok=True)才会执行到此节点。
    """
    grader = llm.with_structured_output(AnswerGradeResult, method="function_calling")

    def answer_grade_node(state):
        question = state["question"]
        answer = state["answer"]
        prompt = ANSWER_GRADE_PROMPT.format(question=question, answer=answer)
        result: AnswerGradeResult = grader.invoke([HumanMessage(content=prompt)])
        status = "✓ 答案有效" if result.useful else "✗ 答案无效"
        print(f"[答案质量] {status}({result.reason})")
        return {"answer_ok": result.useful}

    return answer_grade_node

最终检查

py 复制代码
def make_finalize_node():
    """
    将通过双重检查的答案写入消息历史。
    只有 hallucination_ok=True 且 answer_ok=True 才会到达此节点。
    """

    def finalize_node(state):
        answer = state["answer"]
        print("[完成] 答案已通过双重检查,写入消息历史")
        return {"messages": [AIMessage(content=answer)]}

    return finalize_node

文档相关性路由函数

py 复制代码
def route_after_grade(state: RAGState) -> str:
    grade = state.get("grade")
    if grade == "relevant":
        return "generate"
    retry_count = state.get("retry_count", 0)
    if retry_count >= MAX_RETRIES:
        return "generate"
    return "rewrite"

幻觉检测路由函数

py 复制代码
def route_after_halluicination(state: RAGState) -> str:
    """
    幻觉检测结果路由。
      - 无幻觉(grounded)→ 进入答案质量检查
      - 有幻觉            → 重写查询重来
      - 已达重试上限      → 强制进入质量检查(避免死循环)
    """
    retry = state.get("retry_count", 0)
    if retry >= MAX_RETRIES:
        return "grade_answer"
    return "grade_answer" if state.get("hallucination_ok") else "rewrite"

答案质量路由函数

py 复制代码
def route_after_answer_grade(state: RAGState) -> str:
    """
    答案质量结果路由。
      - 答案有效   → 写入消息历史,结束
      - 答案无效   → 重写查询重来
      - 已达重试上限 → 强制结束
    """
    retry = state.get("retry_count", 0)
    if retry >= MAX_RETRIES:
        return "finalize"
    return "finalize" if state.get("answer_ok") else "rewrite"

画图

py 复制代码
def build_graph(retriever: EnsembleRetriever, llm: ChatOpenAI, embedding_api_key: str):
    builder = StateGraph(RAGState)
    builder.add_node("retrieve", make_retrieve_node(retriever, embedding_api_key))
    builder.add_node("grade_docs", make_grade_node(llm))
    builder.add_node("rewrite", make_rewrite_node(llm))
    builder.add_node("generate", make_generate_node(llm))
    builder.add_node("check_hallucination", make_hallucination_node(llm))
    builder.add_node("grade_answer", make_answer_grade_node(llm))
    builder.add_node("finalize", make_finalize_node())

    builder.add_edge(START, "retrieve")
    builder.add_edge("retrieve", "grade_docs")
    builder.add_edge("generate", "check_hallucination")
    builder.add_edge("rewrite", "retrieve")
    builder.add_edge("finalize", END)

    builder.add_conditional_edges(
        "grade_docs", route_after_grade, {"generate": "generate", "rewrite": "rewrite"}
    )
    builder.add_conditional_edges(
        "check_hallucination",
        route_after_halluicination,
        {"grade_answer": "grade_answer", "rewrite": "rewrite"},
    )
    builder.add_conditional_edges(
        "grade_answer",
        route_after_answer_grade,
        {"finalize": "finalize", "rewrite": "rewrite"},
    )

    graph = builder.compile()
    return graph

main

py 复制代码
result = graph.invoke(
    {
        "messages": [HumanMessage(content=user_input)],
        "question": user_input,
        "context": [],
        "grade": "",
        "answer": "",
        "hallucination_ok": False,
        "answer_ok": False,
        "retry_count": 0,
    }
)
print(f"助手:{result['messages'][-1].content}\n")
相关推荐
叠层归一研究院1 小时前
如何用程序搭建一个 AGI 种子系统(三):生长如何对接物理与数学宇宙
人工智能·python·算法·机器学习·transformer·agi
jufeng13071 小时前
【系列:手搓自主 AI Agent:Hermes 架构原理剖析 · 第 5 篇】
python·ai agent·上下文压缩
AndrewHZ1 小时前
图像处理入门008 | 阶段总结:环境测试与基础概念测验
图像处理·python·opencv·计算机视觉·cv
阿弱1 小时前
graph-core 的边与命令模式设计
java·后端·agent
武子康1 小时前
DeepSeek Harness:Cordis 如何让插件可卸载、可依赖、可重组
人工智能·llm·agent
leeyi1 小时前
Callback 源码:aspect_inject 切面注入(第87篇-E73)
aigc·agent·ai编程
北斗落凡尘1 小时前
LangGraph 入门实战(10)--时光回溯
python·langchain
今天AI了吗1 小时前
Agent & AI 名词大扫盲
数据库·人工智能·python·sql·rust
爱丶不疚1 小时前
在 dsh 仓库里扒到的宝藏工作流:详解 .agents/notes 决策沉淀系统
前端·agent·vibecoding