从线性 RAG 到 Agentic RAG|基于 LangGraph 实现自主决策 RAG 闭环

前言

做 RAG 的同学应该都踩过传统线性 RAG 的坑: 不管用户问 "你好" 还是复杂多跳问题,强制走一遍向量检索、Rerank、拼接 Prompt,浪费算力; 检索出来的文档不相关,直接丢给 LLM,幻觉满天飞; 遇到知识库不存在、实时性问题,只能硬着头皮瞎编,没有兜底方案。

传统 RAG 是一条单向流水线:检索 → Augment → 生成,固定顺序,没有思考、没有反馈、没有重试。 而 Agentic RAG(智能体式 RAG) 把检索变成 LLM 可以自主调用的工具,形成一套自我反思、自我修正的闭环,这也是现在工业界 RAG 升级的主流方案,我们用 LangGraph 来落地这套系统。

前置知识回顾(前面我们聊过的链路) Embedding 向量检索 / BM25 关键词检索 → RRF 混合召回 → BGE-Rerank 重排打分 → formatDocsAsContext 上下文 Augment 拼接

一、传统线性 RAG VS Agentic RAG

表格

对比维度 传统线性 RAG Agentic RAG(LangGraph)
执行逻辑 固定流水线,每次都强制检索 LLM 自主决策:判断要不要检索、检索几次
问题处理 只适合简单单跳问题 简单问题直接回答;复杂问题自动拆分、多跳检索
纠错能力 无评估机制,拿到文档直接生成,极易幻觉 内置文档打分、答案校验,证据不足自动改写 Query 重试
信息来源 仅本地向量知识库 本地知识库 + 网络搜索兜底
成本 稳定、延迟低、Token 消耗固定 动态消耗,复杂问题多次调用 LLM,延迟更高

一句话总结:普通 RAG 是 "脚本",Agentic RAG 是 "会自己思考流程的智能体"。

二、整套闭环架构设计(核心需求)

我们要实现的能力清单:

  1. ✅ 路由 Router 节点:区分简单问题,不需要检索直接回答,节省资源
  2. ✅ Query 拆分 / 改写节点:复杂多跳问题,拆成多个子问题分步检索
  3. ✅ 混合检索 + Rerank:向量检索 + BM25 多路召回,RRF 融合,Rerank 精排,提升召回质量
  4. ✅ 评估节点(Grade) :判断检索出来的文档和问题是否相关;证据够不够支撑答案
  5. ✅ 循环重试机制:文档不相关,自动改写 Query,重新检索(限制最大循环次数,防止死循环)
  6. ✅ 网络搜索兜底:本地知识库反复检索无有效信息,调用搜索引擎获取外部实时资料
  7. ✅ Augment + 生成:合格文档格式化拼接上下文,交给 LLM 输出答案

流转图

sql 复制代码
START
  ↓
【路由 Router】
├─ 分支1:无需检索 → 直接回答 → END
└─ 分支2:需要检索 → 【Query拆分/改写】
        ↓
【混合检索(向量+BM25)】
        ↓
【Rerank重排过滤】
        ↓
【文档相关性评估 Grade】
        ├─ 证据充足 → Augment拼接上下文 → 生成答案 → END
        ├─ 证据不足、文档不相关(未达最大重试次数)→ 回到Query改写循环重试
        └─ 本地知识库无信息 → 【网络搜索兜底】→ 网页内容重新评估 → 生成答案

LangGraph 最适合做这个场景:它本质是状态机 ,用State全程携带所有中间数据,支持条件分支、循环、断点持久化,完美实现 RAG 的闭环反思。

三、代码实现(可直接运行)

依赖安装

复制代码
pip install langgraph langchain langchain-openai sentence-transformers rank_bm25

1. 定义状态 State(整个图的 "记事本")

所有节点共享这份状态,传递问题、子查询、文档、答案、重试次数

python 复制代码
from typing import TypedDict, List, Optional
from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI
from sentence_transformers import CrossEncoder
from rank_bm25 import BM25Okapi

# 状态定义
class AgentRAGState(TypedDict):
    question: str                # 用户原始问题
    sub_queries: List[str]       # 拆分/改写后的子查询
    docs: List                   # 检索+rerank后的文档列表
    answer: Optional[str]
    retry_count: int             # 检索重试次数,防死循环
    max_retry: int

# 全局初始化
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
rerank_model = CrossEncoder("BAAI/bge-reranker-v2-m3")
MAX_RETRY = 2

2. 工具函数(混合检索 + Rerank + 格式化上下文)

这里就是我们之前聊的 formatDocsAsContext、Rerank、多路召回逻辑

python 复制代码
def formatDocsAsContext(docs):
    """Augment阶段:把文档列表拼接成上下文字符串"""
    docs = docs or []
    chunks = []
    for idx, d in enumerate(docs):
        meta = d.get("metadata", {})
        source = meta.get("source", "未知来源")
        chunks.append(f"【片段{idx+1}】来源:{source}\n{d['page_content']}")
    return "\n\n".join(chunks)

def hybrid_retrieve(query: str, vector_store, bm25: BM25Okapi):
    """混合检索:向量检索 + BM25关键词检索,这里简化演示"""
    # 向量召回Top20、BM25召回Top20,RRF融合(省略RRF融合代码)
    candidate_docs = vector_store.similarity_search(query, k=20)
    return candidate_docs

def rerank_docs(query: str, docs: List):
    """Rerank精排,返回前5条高分文档"""
    pairs = [[query, doc["page_content"]] for doc in docs]
    scores = rerank_model.predict(pairs)
    scored_docs = sorted(zip(docs, scores), key=lambda x:x[1], reverse=True)
    # 过滤低分,取Top5
    return [d for d, s in scored_docs if s > 0.3][:5]

3. 各个 Node 节点实现

节点 1:路由 Router(判断是否需要检索)

ini 复制代码
def router_node(state: AgentRAGState):
    question = state["question"]
    prompt = f"""
你是RAG路由分类器,只输出一个单词:retrieve / direct
direct:闲聊、问候、常识类不需要检索知识库的问题
retrieve:需要查阅知识库资料才能回答的问题
问题:{question}
"""
    res = llm.invoke(prompt).content.strip()
    return {"route": res}

节点 2:Query 改写 / 拆分节点(复杂问题生成子查询)

ini 复制代码
def rewrite_query_node(state: AgentRAGState):
    q = state["question"]
    prompt = f"""
对下面问题进行改写或者拆分子问题,方便检索,输出json数组
问题:{q}
"""
    res = llm.invoke(prompt).content
    # 这里简化,直接返回改写后的query
    return {"sub_queries": [q], "retry_count": state["retry_count"] + 1}

节点 3:检索 + Rerank 节点

ini 复制代码
def retrieve_rerank_node(state: AgentRAGState):
    sub_q = state["sub_queries"][0]
    # 这里替换成你的向量库、bm25实例
    raw_docs = hybrid_retrieve(sub_q, vector_store=None, bm25=None)
    final_docs = rerank_docs(sub_q, raw_docs)
    return {"docs": final_docs}

节点 4:文档评估 Grade 节点(核心反思环节)

ini 复制代码
def grade_doc_node(state: AgentRAGState):
    docs = state["docs"]
    question = state["question"]
    if len(docs) == 0:
        return {"grade": "no_knowledge"}
    context = formatDocsAsContext(docs)
    prompt = f"""
判断文档是否和问题相关,只输出:relevant / irrelevant
问题:{question}
文档:{context}
"""
    grade = llm.invoke(prompt).content.strip()
    return {"grade": grade}

节点 5:生成答案节点

python 复制代码
def generate_node(state: AgentRAGState):
    context = formatDocsAsContext(state["docs"])
    q = state["question"]
    prompt = f"""
基于下面的资料回答问题,不要编造信息
资料:
{context}
问题:{q}
"""
    ans = llm.invoke(prompt).content
    return {"answer": ans}

def direct_answer_node(state: AgentRAGState):
    """不需要检索,直接回答"""
    ans = llm.invoke(f"回答用户问题:{state['question']}").content
    return {"answer": ans}

def web_search_node(state: AgentRAGState):
    """网络兜底搜索节点,你可以接入SerpAPI、Tavily"""
    # web_docs = tavily.search(state["question"])
    return {"docs": []}

4. 构建图 + 条件分支(LangGraph 核心)

scss 复制代码
# 路由分支函数
def route_after_router(state: AgentRAGState):
    if state["route"] == "direct":
        return "direct_answer"
    else:
        return "rewrite_query"

# 评估后的分支
def route_after_grade(state: AgentRAGState):
    g = state["grade"]
    retry = state["retry_count"]
    max_r = MAX_RETRY
    if g == "relevant":
        return "generate"
    elif g == "irrelevant" and retry < max_r:
        return "rewrite_query"
    else:
        return "web_search"


builder = StateGraph(AgentRAGState)
# 注册节点
builder.add_node("router", router_node)
builder.add_node("direct_answer", direct_answer_node)
builder.add_node("rewrite_query", rewrite_query_node)
builder.add_node("retrieve_rerank", retrieve_rerank_node)
builder.add_node("grade_doc", grade_doc_node)
builder.add_node("generate", generate_node)
builder.add_node("web_search", web_search_node)

# 连线
builder.add_edge(START, "router")
builder.add_conditional_edges("router", route_after_router)
builder.add_edge("rewrite_query", "retrieve_rerank")
builder.add_edge("retrieve_rerank", "grade_doc")
builder.add_conditional_edges("grade_doc", route_after_grade)
builder.add_edge("web_search", "grade_doc")
builder.add_edge("direct_answer", END)
builder.add_edge("generate", END)

graph = builder.compile()

5. 调用示例

python 复制代码
res = graph.invoke({
    "question": "Rerank和Embedding模型的区别?",
    "retry_count": 0,
    "max_retry": MAX_RETRY,
    "sub_queries": [],
    "docs": [],
    "answer": None
})
print(res["answer"])

四、关键细节解读

  1. 路由节省成本 很多问答场景,一半以上用户输入是闲聊、问候、简单常识,直接跳过检索链路,大幅减少向量库查询、Rerank 推理开销。
  2. 循环重试(Corrective RAG) 这个模式也叫 CRAG,是 Agentic RAG 最经典的实现:检索→打分→不相关就改写 query 重查。 一定要加max_retry上限,不然 LLM 无限循环改写查询,疯狂扣 Token。
  3. 混合检索 + Rerank 为什么放在这里 向量召回擅长语义相似,BM25 擅长关键词精准匹配;两路召回融合之后,再交给 CrossEncoder Rerank 做细粒度打分,解决 "字面相似、语义无关" 的召回缺陷,给后面评估环节更高质量的候选文档。
  4. 网络兜底的意义 知识库是静态的,没有实时信息;当本地多次检索都没有相关内容,自动切换联网搜索,避免 LLM 强行编造幻觉。
  5. Augment 的位置 只有经过评估、确认有效的文档,才会调用formatDocsAsContext拼接上下文,而不是检索出来直接拼接,减少无效上下文污染 Prompt。

五、工程落地权衡(面试高频)

Q:Agentic RAG 一定比普通 RAG 更好吗? A:不是。

  • 简单知识库、查询单一、追求低延迟、低成本场景:线性 RAG 足够优秀;
  • 复杂多跳问答、需要实时信息、对幻觉控制要求高的场景:Agentic RAG 优势巨大。

代价:多次 LLM 调用、多次检索,延迟更高、Token 消耗更大,上线前要做限流、最大重试次数控制。

六、后续扩展方向

  1. 增加答案校验节点:生成答案之后,再校验一遍回答是否和文档一致;
  2. 多向量库路由:不同领域问题,路由到不同知识库;
  3. 增加可视化:LangGraph 自带get_graph().draw_mermaid(),在前端展示思考链路;
  4. 接入观测链路:记录每一步路由、打分、检索日志,方便调试 RAG 效果。

结语

RAG 已经从简单的 "检索拼 prompt",进化成带自主决策、反思闭环的 Agent 系统。 LangGraph 给了我们编排这套状态机的能力,把路由、检索、评估、重试、工具调用全部串起来,这也是目前工业界高阶 RAG 的主流方案。

相关推荐
可乐_不渴2 小时前
【Linux】 进程虚拟地址空间
java·linux·javascript
vipxieliang2 小时前
JavaScript 深浅拷贝全解析:从 JSON 到 structuredClone
前端·javascript
Rain的Java大神之路3 小时前
100 亿条数据如何快速写入 MySQL?从 231 天到 3.5 小时,我把面试官问懵了
java·后端·面试
利威尔xu3 小时前
Go IM 源码实战(7):断线三天重连,水位算错就漏消息
redis·后端·微服务·面试·golang
范同学~3 小时前
项目添加水印
java·前端·javascript
软件技术NINI3 小时前
基于PHP和MySQL的快递代收点管理系统的设计与实现
开发语言·javascript·数据库·mysql·毕业设计·php·毕设
Dovis(誓平步青云)4 小时前
一个分区超温就全棚浇水,控制系统怎样按区下策略
开发语言·前端·javascript·人工智能·机器学习
Sailing4 小时前
🚀🚀【封闭结束5】Agent 中的 SSE 流式推送与 HITL 暂停恢复
前端·面试·agent
Lzh编程小栈4 小时前
STM32TIM基础定时器深度解析:时基原理、核心寄存器、影子寄存器、基础定时器和中断与DMA联动、代码实战及面试考点全解
stm32·单片机·面试