前言
做 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 是 "会自己思考流程的智能体"。
二、整套闭环架构设计(核心需求)
我们要实现的能力清单:
- ✅ 路由 Router 节点:区分简单问题,不需要检索直接回答,节省资源
- ✅ Query 拆分 / 改写节点:复杂多跳问题,拆成多个子问题分步检索
- ✅ 混合检索 + Rerank:向量检索 + BM25 多路召回,RRF 融合,Rerank 精排,提升召回质量
- ✅ 评估节点(Grade) :判断检索出来的文档和问题是否相关;证据够不够支撑答案
- ✅ 循环重试机制:文档不相关,自动改写 Query,重新检索(限制最大循环次数,防止死循环)
- ✅ 网络搜索兜底:本地知识库反复检索无有效信息,调用搜索引擎获取外部实时资料
- ✅ 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"])
四、关键细节解读
- 路由节省成本 很多问答场景,一半以上用户输入是闲聊、问候、简单常识,直接跳过检索链路,大幅减少向量库查询、Rerank 推理开销。
- 循环重试(Corrective RAG) 这个模式也叫 CRAG,是 Agentic RAG 最经典的实现:检索→打分→不相关就改写 query 重查。 一定要加
max_retry上限,不然 LLM 无限循环改写查询,疯狂扣 Token。 - 混合检索 + Rerank 为什么放在这里 向量召回擅长语义相似,BM25 擅长关键词精准匹配;两路召回融合之后,再交给 CrossEncoder Rerank 做细粒度打分,解决 "字面相似、语义无关" 的召回缺陷,给后面评估环节更高质量的候选文档。
- 网络兜底的意义 知识库是静态的,没有实时信息;当本地多次检索都没有相关内容,自动切换联网搜索,避免 LLM 强行编造幻觉。
- Augment 的位置 只有经过评估、确认有效的文档,才会调用
formatDocsAsContext拼接上下文,而不是检索出来直接拼接,减少无效上下文污染 Prompt。
五、工程落地权衡(面试高频)
Q:Agentic RAG 一定比普通 RAG 更好吗? A:不是。
- 简单知识库、查询单一、追求低延迟、低成本场景:线性 RAG 足够优秀;
- 复杂多跳问答、需要实时信息、对幻觉控制要求高的场景:Agentic RAG 优势巨大。
代价:多次 LLM 调用、多次检索,延迟更高、Token 消耗更大,上线前要做限流、最大重试次数控制。
六、后续扩展方向
- 增加答案校验节点:生成答案之后,再校验一遍回答是否和文档一致;
- 多向量库路由:不同领域问题,路由到不同知识库;
- 增加可视化:LangGraph 自带
get_graph().draw_mermaid(),在前端展示思考链路; - 接入观测链路:记录每一步路由、打分、检索日志,方便调试 RAG 效果。
结语
RAG 已经从简单的 "检索拼 prompt",进化成带自主决策、反思闭环的 Agent 系统。 LangGraph 给了我们编排这套状态机的能力,把路由、检索、评估、重试、工具调用全部串起来,这也是目前工业界高阶 RAG 的主流方案。