一句话理解:普通 RAG 是"检索一次就回答";Agentic RAG 会先检查检索结果是否可靠,资料不够好时改写问题并重新检索,确认有足够证据后再生成答案。
本文说明 Agentic RAG 的完整流程、检索评分的位置,以及生产环境中的重排策略;关键实现代码也直接附在对应步骤中。
一、为什么需要 Agentic RAG?
普通 RAG 的流程很直接:
text
用户问题 → 检索文档 → 生成答案
它的问题是:即使召回的资料不相关、不完整,模型通常仍会根据这些资料尝试回答,容易答非所问或缺少证据。

Agentic RAG 加入了"评估"和"决策":
text
用户问题
↓
检索文档
↓
评估文档是否相关、是否足够回答
├─ 足够好 → 生成答案
└─ 不够好 → 改写问题 → 重新检索
↓
多次失败 → 兜底回复
这里的 Agentic 不只是调用了大模型,而是系统会根据当前结果决定下一步行动:回答、重试,还是结束。
二、示例代码的整体图结构
示例使用 LangGraph 组织工作流:
text
retrieve
↓
grade
↓
should_retry_or_generate
├─ generate → END
├─ rewrite → retrieve(形成循环)
└─ fallback → END
各个函数的职责如下:
| 节点 / 函数 | 作用 |
|---|---|
| create_sample_vectorstore() | 创建示例 Chroma 向量库,并写入 LangGraph、天气、Python 等测试文档。 |
| retrieve_documents() | 用原始问题或改写后的问题,从向量库中召回 Top 3 文档。 |
| grade_documents() | 让 LLM 对每篇候选文档单独打 0~1 分,并过滤低分文档。 |
| should_retry_or_generate() | 根据平均分、保留文档数和重试次数,决定生成、改写或兜底。 |
| rewrite_query() | 当检索质量不足时,让 LLM 改写原问题。 |
| generate_answer() | 使用保留下来的文档生成答案,并要求模型注明来源。 |
| generate_fallback() | 无可用资料且重试耗尽时,返回友好的失败说明。 |
三、核心实现代码:如何把节点连成循环?
下面是工作流的关键连线代码。它先检索、再评分;评分后的路由函数决定生成、改写重试或兜底。
python
workflow = StateGraph(RAGState)
workflow.add_node("retrieve", retrieve_documents)
workflow.add_node("grade", grade_documents)
workflow.add_node("rewrite", rewrite_query)
workflow.add_node("generate", generate_answer)
workflow.add_node("fallback", generate_fallback)
workflow.set_entry_point("retrieve")
workflow.add_edge("retrieve", "grade")
workflow.add_conditional_edges(
"grade",
should_retry_or_generate,
{"rewrite": "rewrite", "generate": "generate", "fallback": "fallback"},
)
# 改写问题后,回到检索节点,形成 Agentic RAG 的自我纠错循环。
workflow.add_edge("rewrite", "retrieve")
workflow.add_edge("generate", END)
workflow.add_edge("fallback", END)
app = workflow.compile()
四、一次完整查询是怎样运行的?
以问题"如何安装 LangGraph?"为例。
1. 检索:retrieve_documents()
示例代码先从向量库取向量相似度最高的 3 篇文档:
python
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
documents = retriever.invoke(query)
此时的顺序是向量相似度排序。它只说明文档与问题语义接近,不代表每篇文档都能直接回答问题。
对应的检索节点核心代码如下。重写过问题时,优先用改写后的文本检索:
python
def retrieve_documents(state: RAGState) -> dict:
query = state.get("rewritten_query") or state["query"]
vectorstore = state.get("_vectorstore")
if not vectorstore:
vectorstore = create_sample_vectorstore()
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
documents = retriever.invoke(query)
return {"documents": documents}
2. 逐篇 LLM 打分:grade_documents()
代码会遍历每篇候选文档,将"用户问题 + 当前文档正文"交给 LLM:
python
for doc in documents:
result = chain.invoke({"query": query, "document": doc.page_content})
score = float(result.content.strip())
评分提示词要求模型只输出 0~1 的数字:
| 分数 | 含义 |
|---|---|
| 1.0 | 高度相关,文档可以直接回答问题。 |
| 0.7 | 有一定相关性,包含相关信息。 |
| 0.3 | 边缘相关,只是间接相关。 |
| 0.0 | 完全无关。 |
例如,可能得到:
| 文档 | 评分 |
|---|---|
| langgraph_install.md | 1.0 |
| langgraph_docs.md | 0.7 |
| weather.md | 0.0 |
然后代码按阈值过滤:
python
if score >= 0.5:
relevant_docs.append(doc)
因此天气文档会被丢弃,前两篇被保留。
下面是完整的评分、解析和过滤逻辑。每次循环只给 LLM 一篇文档,因此属于"逐篇评分":
python
scores = []
relevant_docs = []
for doc in documents:
chain = grading_prompt | llm
result = chain.invoke({"query": query, "document": doc.page_content})
try:
score = float(result.content.strip())
except ValueError:
score = 0.5
scores.append(score)
if score >= 0.5:
relevant_docs.append(doc)
avg_score = sum(scores) / len(scores) if scores else 0
return {
"documents": relevant_docs,
"relevance_score": avg_score,
}
3. 路由判断:should_retry_or_generate()
路由函数读取三类信息:
- relevance_score:所有候选文档分数的平均值;
- documents:过滤后仍保留的文档;
- retry_count 和 max_retries:已经重试及允许重试的次数。
它的决策逻辑是:
text
平均分 ≥ 0.5,且有保留文档 → generate
平均分不足,且还能重试 → rewrite
重试用尽,但还有文档 → generate(尽力回答)
重试用尽,也没有文档 → fallback
对应的路由函数如下:
python
def should_retry_or_generate(state: RAGState):
relevance_score = state.get("relevance_score", 0)
retry_count = state.get("retry_count", 0)
max_retries = state.get("max_retries", 2)
documents = state.get("documents", [])
if relevance_score >= 0.5 and len(documents) > 0:
return "generate"
if retry_count < max_retries:
return "rewrite"
if len(documents) > 0:
return "generate"
return "fallback"
4. 改写重试:rewrite_query()
如果资料质量不足,系统不会立刻回答,而是改写问题后重新检索:
text
原问题:怎么装 LangGraph?
改写后:LangGraph 的 pip 安装命令和 Python 版本要求是什么?
改写可以加入同义词、补足用户真正需要的信息,或换成更贴近知识库文档的表达。改写后的问题会回到检索节点,重新执行检索和评分。
查询改写节点的核心逻辑很简单:将改写结果写入状态,并把重试次数加一。
python
def rewrite_query(state: RAGState) -> dict:
query = state["query"]
retry_count = state.get("retry_count", 0)
chain = rewrite_prompt | llm
result = chain.invoke({"query": query})
rewritten = result.content.strip()
return {
"rewritten_query": rewritten,
"retry_count": retry_count + 1,
}
5. 生成或兜底
有足够相关的文档时,generate_answer() 会把它们拼成上下文:
text
Source: langgraph_install.md
文档正文......
Source: langgraph_docs.md
文档正文......
随后要求模型:只能根据给定上下文回答;资料不足时要明确说明;回答时注明来源。
多次改写后仍没有可用资料时,generate_fallback() 会告诉用户知识库中没有足够信息,并建议更换问法。
五、文档评分应该放在哪里?
在 Agentic RAG 中,文档评分应放在粗召回之后、最终生成之前:
text
用户问题
↓
粗召回:向量检索 / BM25 检索,取 Top 20~100
↓
融合、去重:合并多路检索结果
↓
重排 / 文档评分:判断"这篇是否真的能回答问题"
↓
取 Top 3~10
↓
判断证据是否足够
├─ 足够 → 生成答案
└─ 不足 → 改写 Query 后重试
当前示例中,grade_documents() 紧接在 retrieve_documents() 后,位置是正确的。它既过滤低分文档,也为后续"是否重试"的路由提供依据。
六、当前示例代码没有做什么?
教学代码已实现逐篇评分和过滤,但没有按 LLM 分数重新排序。
当前逻辑只是:
python
if score >= 0.5:
relevant_docs.append(doc)
因此保留文档仍按原先的向量检索顺序排列,而不是按 LLM 分数从高到低排列。
例如:
| 向量检索顺序 | 文档 | LLM 评分 |
|---|---|---|
| 1 | langgraph_docs.md | 0.7 |
| 2 | weather.md | 0.0 |
| 3 | langgraph_install.md | 1.0 |
过滤后,交给生成模型的顺序仍是:
text
langgraph_docs.md(0.7)
langgraph_install.md(1.0)
即使安装文档评分更高,也仍然排在后面。
七、生产中应如何改进评分与重排?
更完整的做法是:评分后过滤、排序,再限制最终上下文数量。
python
scored_docs = []
for doc in documents:
result = chain.invoke({"query": query, "document": doc.page_content})
score = float(result.content.strip())
scored_docs.append((score, doc))
# 按相关性分数从高到低排列。
scored_docs.sort(key=lambda item: item[0], reverse=True)
# 丢弃低分文档,并只将最高分的前 5 篇传给生成模型。
relevant_docs = [
doc
for score, doc in scored_docs
if score >= 0.5
][:5]
改进后的流程:
text
召回 Top-K
↓
逐篇评分
↓
过滤低分文档
↓
按分数降序重排
↓
取 Top-N 文档
↓
生成答案或触发重试
为什么不要只看平均分?
示例代码使用所有候选文档的平均分作为 relevance_score。无关文档容易把平均分拉低:
text
安装文档:1.0
介绍文档:0.7
天气文档:0.0
平均分:0.57
如果候选文档更多,即使有一篇 1.0 分的关键证据,平均分也可能偏低,从而错误触发重试。
生产中更常见的判断方式是组合使用:
- 最高分是否超过阈值;
- 高于阈值的文档数量是否足够;
- Top-N 文档的总分或平均分;
- 文档是否覆盖回答所需的关键字段、数字和条件。
例如:
text
最高分 < 0.5 → 没有可靠证据,改写后重试
最高分 ≥ 0.5,且至少有 1~3 篇合格文档 → 可以尝试生成
八、专用 Reranker 与 LLM 文档评分:合并还是分开?
1. 合并:LLM 直接评分、排序、路由
对于学习项目、文档量不大或低并发系统,可以让 LLM 一步完成评分、重排、过滤和路由:
text
粗召回 Top 20~50
↓
LLM 逐篇打分 + 重排 + 过滤
↓
分数足够 → Top 3~5 → 生成
分数不足 → 改写 Query → 重试
优点是实现简单,LLM 能理解复杂语义和问题意图。缺点是每篇候选文档都要调用 LLM,延迟与成本会随候选文档数量增长。
2. 分开:专用 Reranker 初筛,LLM 再判断证据质量
常见的生产级漏斗:
text
向量 + BM25 粗召回 Top 50~100
↓
融合、去重
↓
Cross-Encoder / 专用 Reranker 重排到 Top 10~20
↓
LLM 证据评分或充分性判断 Top 3~5
↓
生成 / 改写重试 / 要求用户澄清
两层职责不同:
| 层级 | 主要问题 | 特点 |
|---|---|---|
| 专用 Reranker | "哪篇文档和 Query 最相关?" | 快,适合比较大量候选文档。 |
| LLM 评分器 | "这些文档是否真的足够回答?是否缺少条件或存在冲突?" | 判断更细致,但更慢、更贵。 |
分开的好处是先用较快模型过滤大量候选,再让 LLM 精细检查少量高价值证据。缺点是多一个步骤,会增加延迟、成本和阈值调参工作;第一层若错误丢弃关键文档,后面的 LLM 也无法挽回。
九、不同规模项目的推荐路线
| 场景 | 推荐流程 |
|---|---|
| 学习、Demo、小知识库 | 粗召回 → LLM 评分 / 重排 → 生成或改写重试。 |
| 常规生产、需要控制延迟成本 | 向量 + BM25 → 专用 Reranker → 按分数阈值生成或重试。 |
| 高准确率、高风险问答 | 向量 + BM25 → 专用 Reranker → LLM 证据充分性检查 → 生成或重试。 |
十、实现 Agentic RAG 时的关键注意点
- 限制循环次数:设置 max_retries,避免一直改写和检索。
- 保留原始问题:改写问题只用于检索;最终回答仍应围绕用户的原始意图生成。
- 评分要可解析:要求评分模型只返回数字或结构化 JSON,并处理解析失败。
- 评分后应排序:不能只过滤,应优先把高分证据放入上下文前部。
- 限制最终文档数:即使很多文档合格,也只选 Top-N,避免噪声和 Token 成本。
- 区分"相关"与"充分":文档相关不代表足以回答;高风险场景应额外检查关键条件和证据覆盖度。
- 评估端到端效果:不要只看检索分数,还要评估答案正确率、引用正确率、重试率、延迟和成本。
十一、最后总结
Agentic RAG 的核心不是让 RAG 无限循环,而是在生成前建立"资料质量闸门":先召回、再排序和评估;资料足够才回答,资料不足就改写问题重试,最终仍找不到才诚实兜底。
对于当前示例代码,可以先完成这一版最小闭环:
text
retrieve → LLM 逐篇评分 → 过滤并按分数排序 → Top-N
↓
分数足够 → generate
分数不足 → rewrite → retrieve
重试用尽且无证据 → fallback
当文档量和请求量增长后,再把"LLM 逐篇评分"前置为"专用 Reranker 初筛",并将 LLM 留给少量高价值文档的证据充分性判断。