>title>Agentic RAG · 进阶 AD4

🧠 Agentic RAG

AD3 的 RAG 是"无脑检索+生成"——检索到什么就用什么。但检索结果可能不相关、生成可能幻觉。Agentic RAG 让系统自我反思:检索后判断质量、不相关就改写 query 重试、生成后验证忠实度。本章精读 LangGraph 官方三种反思 RAG 范式,并用 StateGraph 从零实现 Corrective RAG。

本章目标

  • 理解 Agentic RAG 的核心思想:检索-评估-修正的反思循环
  • 掌握三种反思范式:CRAG / Self-RAG / Adaptive RAG 的差异
  • 精读 LangGraph 官方 CRAG notebook 的图结构
  • 用 StateGraph 从零实现一个 Corrective RAG(含评分/路由/改写/web回退)
  • 理解"检索即工具"的 Agentic RAG 范式

为什么需要 Agentic RAG

AD3 解决了"检索质量",但仍是单次流水线——检索完就生成,不管结果好不好。真实问题:

问题流水线 RAG 的反应Agentic RAG 的反应
检索到的文档不相关硬塞给 LLM,生成废话评分→不相关→改写 query 重试 / 转 web 搜索
LLM 生成的内容与文档不符(幻觉)无检测,直接返回验证生成 vs 文档一致性,不符则重新生成
问题不需要检索(闲聊)也走检索流程(浪费)先路由判断:该检索还是直接回答
💡 Agentic RAG 的本质

把 RAG 从"线性流水线"升级为"带条件分支和循环的图"——这正是 LangGraph 擅长的。关键转折:在检索前后插入"判断节点",用 LLM 做评估决策。这把 RAG 变成了一个微型 Agent。

三种反思范式

LangGraph 官方提供了三种自我反思 RAG 的实现(examples/rag/),反思深度递增:

① CRAG 纠正性 retrieve 检索 grade 评分文档相关性 相关? generate web+改写 不相关→转 web search 本章实现 ★ ② Self-RAG 自反思 retrieve + grade generate ① 检测幻觉(生成vs文档) ② 检查是否回答了问题 双重验证:忠实+有用 不通过→改写重试 ③ Adaptive 自适应 route 路由判断 类型? RAG检索 web_search generate(含 grade 验证) + Self-RAG 的验证
图 AD4.1 · 三种反思范式,反思深度递增:CRAG(检索评分)→ Self-RAG(+生成验证)→ Adaptive(+前置路由)
范式反思点核心机制源码 notebook
CRAG检索后文档相关性评分→不相关转 weblanggraph_crag.ipynb
Self-RAG检索后 + 生成后+ 幻觉检测 + 有用性检查langgraph_self_rag.ipynb
Adaptive前置 + 检索后 + 生成后+ 问题路由(该不该检索)langgraph_adaptive_rag.ipynb

精读 CRAG 的图结构

Corrective RAG 是最经典、本章要实现的。先看官方 notebook 的图结构(examples/rag/langgraph_crag.ipynb):

📄 langgraph_crag.ipynb:524 · CRAG 的图节点与边 python
# CRAG 的 5 个节点
workflow.add_node("retrieve", retrieve)              # 检索文档
workflow.add_node("grade_documents", grade_documents) # ★ 评分文档相关性
workflow.add_node("generate", generate)              # 生成答案
workflow.add_node("transform_query", transform_query) # 改写 query
workflow.add_node("web_search_node", web_search)     # web 搜索回退

# 关键条件边:评分后决定走哪
workflow.add_conditional_edges(
    "grade_documents",
    decide_to_generate,   # :473 路由函数
    {
        "transform_query": "transform_query",  # 不相关→改写
        "generate": "generate",                # 相关→生成
    },
)
# 改写后重新检索(形成反思循环)
workflow.add_edge("transform_query", "retrieve")

CRAG 的核心:grade_documents + decide_to_generate

📄 langgraph_crag.ipynb:393/473 · 评分与路由 python
def grade_documents(state):
    """★ 核心反思:用 LLM 逐文档判断是否相关。"""
    filtered_docs = []
    for doc in state["documents"]:
        # 用结构化输出让 LLM 判断 yes/no
        score = retrieval_grader.invoke({"question": q, "document": doc})
        if score.binary_score == "yes":
            filtered_docs.append(doc)   # 保留相关文档
    return {"documents": filtered_docs}

def decide_to_generate(state):
    """★ 路由:相关文档够不够?"""
    if state["documents"]:         # 有相关文档
        return "generate"          # → 直接生成
    return "transform_query"       # 无相关文档 → 改写 query 重试
⚠️ 这就是"反思"的本质

注意 grade_documents额外的一次 LLM 调用,专门用来"评估检索质量"。这就是反思的代价——更准但更贵更慢(多了评分调用 + 可能的改写重试循环)。生产中要权衡:简单问答用流水线 RAG,高准确性要求用 Agentic RAG。

实战:从零实现 Corrective RAG

🚀 完整可运行的 CRAG

pip install langgraph langchain-openai。这是 CRAG 的完整 StateGraph 实现。

📄 ad4_crag.py · 从零实现 Corrective RAG python
# pip install langgraph langchain-openai
# export OPENAI_API_KEY="sk-..."
import operator
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

model = ChatOpenAI(model="gpt-4o-mini")

# ============ ① 状态 ============
class State(TypedDict):
    question: str
    documents: Annotated[list, operator.add]   # 检索到的文档(累积)
    generation: str                             # 生成的答案
    web_search_needed: bool                     # 是否需要 web 回退

# ============ ② 检索节点(假装检索,实际接向量库)============
def retrieve(state: State) -> dict:
    """检索文档。这里用假数据模拟。"""
    fake_docs = ["文档A:关于LangChain的内容...", "文档B:无关内容..."]
    return {"documents": [{"content": d} for d in fake_docs]}

# ============ ③ ★ 评分节点(核心反思)============
from pydantic import BaseModel, Field

class Grade(BaseModel):
    """文档相关性评分。"""
    binary_score: str = Field(description="yes 或 no")

grader = model.with_structured_output(Grade)

def grade_documents(state: State) -> dict:
    """★ 反思核心:逐文档评估相关性,过滤掉不相关的。"""
    question = state["question"]
    filtered = []
    for doc in state["documents"]:
        grade = grader.invoke(
            f"问题:{question}\n文档:{doc['content']}\n这文档与问题相关吗?只回 yes/no"
        )
        if grade.binary_score.lower() == "yes":
            filtered.append(doc)
    # 关键:如果过滤后没有相关文档,标记需要 web 回退
    return {
        "documents": [],  # 清空旧文档(避免重复累积,这里简化处理)
        "web_search_needed": len(filtered) == 0,
    }

# ============ ④ 路由:决定生成还是改写回退 ============
def decide_after_grade(state: State) -> str:
    """评分后的路由决策。"""
    if state.get("web_search_needed"):
        return "transform_query"   # 无相关文档 → 改写重试
    return "generate"              # 有相关文档 → 生成

# ============ ⑤ 改写 query(反思循环的"修正")============
def transform_query(state: State) -> dict:
    """改写 query 以更好检索。"""
    rewriter = (
        ChatPromptTemplate.from_messages([
            ("system", "改写下面的问题使其更适合检索。只输出改写后的问题。"),
            ("human", "{question}"),
        ])
        | model | StrOutputParser()
    )
    better = rewriter.invoke({"question": state["question"]})
    return {"question": better}   # 用新 query 重试检索

# ============ ⑥ 生成节点 ============
def generate(state: State) -> dict:
    """基于文档生成答案。"""
    context = "\n".join(d["content"] for d in state["documents"]) or "(无相关文档)"
    chain = (
        ChatPromptTemplate.from_messages([
            ("system", "根据上下文回答。{context}"),
            ("human", "{question}"),
        ])
        | model | StrOutputParser()
    )
    answer = chain.invoke({"context": context, "question": state["question"]})
    return {"generation": answer}

# ============ ⑦ 构建 CRAG 图 ============
graph = StateGraph(State)
graph.add_node("retrieve", retrieve)
graph.add_node("grade_documents", grade_documents)
graph.add_node("generate", generate)
graph.add_node("transform_query", transform_query)

graph.add_edge(START, "retrieve")
graph.add_edge("retrieve", "grade_documents")
# ★ 条件边:评分后路由
graph.add_conditional_edges("grade_documents", decide_after_grade,
    {"generate": "generate", "transform_query": "transform_query"})
# 改写后回到检索(反思循环!)
graph.add_edge("transform_query", "retrieve")
graph.add_edge("generate", END)

app = graph.compile()

# ============ ⑧ 运行 ============
result = app.invoke({
    "question": "LangChain 的 LCEL 是什么?",
    "documents": [], "generation": "", "web_search_needed": False,
})
print("问题:", result["question"])
print("答案:", result["generation"])
# 流程:retrieve → grade → (不相关?) → transform_query → retrieve → grade → (相关) → generate
CRAG 的反思循环 START retrieve grade ★ 反思 相关? generate → END transform_query 改写 重试检索(循环)
图 AD4.2 · CRAG 反思循环:检索→评分→不相关则改写重试,相关则生成

Self-RAG:生成后双重验证

CRAG 只在检索后反思。Self-RAG(langgraph_self_rag.ipynb)在生成后也反思——两个检查:

📄 langgraph_self_rag.ipynb · 双重验证 python
# Self-RAG 在 generate 之后加两个验证节点:

# 验证①:幻觉检测——生成的内容是否忠实于文档?
def grade_generation_vs_documents(state):
    """生成 vs 文档:有没有编造(幻觉)?"""
    grade = hallucination_grader.invoke({
        "documents": state["documents"],
        "generation": state["generation"],
    })
    if grade.binary_score == "no":   # 检测到幻觉
        return "not supported"        # → 重新生成

    # 验证②:有用性——生成的内容是否回答了问题?
    grade = answer_grader.invoke({
        "question": state["question"],
        "generation": state["generation"],
    })
    if grade.binary_score == "yes":
        return "useful"               # 有用 → 结束
    return "not useful"               # 没回答问题 → 改写重试

# 条件边:三态路由
graph.add_conditional_edges("generate", grade_generation_vs_documents,
    {"not supported": "generate",      # 幻觉→重生
     "useful": END,                    # 有用→结束
     "not useful": "transform_query"}) # 没用→改写
🧭 双重验证的价值与代价

Self-RAG 比CRAG 更严格:既查"对不对"(忠实度,防幻觉),又查"答没答"(有用性,防跑题)。但每次验证都是一次 LLM 调用,一个完整循环可能调用 5-8 次 LLM。适合高准确性场景(医疗/法律/金融问答),不适合高频低成本场景

Adaptive RAG:前置路由

Adaptive(langgraph_adaptive_rag.ipynb)在最前面加一个路由——先判断问题类型,决定走哪条路:

📄 langgraph_adaptive_rag.ipynb:636 · 前置路由 python
def route_question(state):
    """问题路由:根据问题类型决定检索方式。"""
    source = question_router.invoke({"question": state["question"]})
    if source == "web_search":
        return "web_search"      # 时事/外部知识 → web
    elif source == "vectorstore":
        return "retrieve"        # 内部文档 → 向量检索
# 然后接 CRAG/Self-RAG 的反思流程

Agentic RAG:检索即工具

最彻底的范式:把检索本身变成 Agent 的一个工具langgraph_agentic_rag.ipynb)。Agent 自己决定何时检索、检索什么、检索几次:

📄 检索即工具(最灵活的范式) python
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent

@tool
def retrieve(query: str) -> str:
    """检索知识库。当你需要查找信息时使用。"""
    docs = vectorstore.invoke(query)
    return "\n".join(d.page_content for d in docs)

@tool
def web_search(query: str) -> str:
    """搜索网络。当知识库没有时使用。"""
    ...

# 把检索变成工具,交给 ReAct Agent 自主决策
agent = create_react_agent(model, tools=[retrieve, web_search])
# Agent 自己决定:调几次 retrieve?要不要 web_search?什么时候够了?
# 这就是 OpenCode 的范式(用 grep/glob/read 检索代码)
💡 这正是 OpenCode 的做法

OpenCode 不用向量 RAG,它用 grep/glob/read 作为 Agent 工具——Agent 自己决定搜什么代码、读哪个文件、搜几次。这就是 Agentic RAG 范式在编码领域的体现。相比 CRAG/Self-RAG 的固定图结构,"检索即工具"更灵活,但依赖 Agent 的判断力。

与生产实践对照

Agentic RAG 概念OpenCode 对应
grade_documents 反思评分LLM 判断读到的文件是否有用
transform_query 改写重试换个关键词重新 grep
web_search 回退webfetch/websearch 工具
检索即工具(最彻底)★ grep/glob/read 都是 Agent 工具
路由判断(Adaptive)LLM 决定用 read 还是 grep 还是 bash

何时用哪种

场景推荐范式
简单文档问答,准确度要求一般流水线 RAG(AD3)
文档质量参差,需要过滤噪声CRAG(本章实现)
高准确性(医疗/法律/金融),防幻觉Self-RAG
问题类型多样(有的要检索有的要 web)Adaptive RAG
数据源动态/可操作(如代码库)Agentic RAG(检索即工具)

小结

下一主题 · 记忆系统

下一个主题 AD5 · 短期记忆深度:深入 Agent 的记忆机制。精读 BaseCheckpointSaver 四方法、thread_id 语义、时间旅行调试、状态分支,把 LG6 的检查点讲深讲透。