Self-RAG ------ 幻觉检测 + 答案质量评估
自适应 RAG 基础上,对"生成结果"也加入自我审查,构成真正的 Self-RAG。
解决什么问题?
自适应 RAG 只检查"检索文档是否相关",但 LLM 仍可能基于相关文档"幻觉发挥",生成文档中根本没有的内容。故再加两道闸门:
| 检查节点 | 检查内容 | 失败后动作 |
|---|---|---|
| check_hallucination | 答案有没有编造文档外的内容? | → 重写查询重来 |
| grade_answer | 答案是否真正回答了问题? | → 重写查询重来 |
| finalize | 通过双重检查 | → 写入消息历史,结束 |
核心新概念
- 多级条件路由 --- 同一张图中三处 add_conditional_edges,每个节点独立路由
- 链式 Pydantic --- 三种评判各用不同的结构化输出模型(DocGradeResult / HallucinationResult / AnswerGradeResult)
- 状态扩展 --- 新增 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")