12.Agentic RAG

一句话理解:普通 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 时的关键注意点

  1. 限制循环次数:设置 max_retries,避免一直改写和检索。
  2. 保留原始问题:改写问题只用于检索;最终回答仍应围绕用户的原始意图生成。
  3. 评分要可解析:要求评分模型只返回数字或结构化 JSON,并处理解析失败。
  4. 评分后应排序:不能只过滤,应优先把高分证据放入上下文前部。
  5. 限制最终文档数:即使很多文档合格,也只选 Top-N,避免噪声和 Token 成本。
  6. 区分"相关"与"充分":文档相关不代表足以回答;高风险场景应额外检查关键条件和证据覆盖度。
  7. 评估端到端效果:不要只看检索分数,还要评估答案正确率、引用正确率、重试率、延迟和成本。

十一、最后总结

Agentic RAG 的核心不是让 RAG 无限循环,而是在生成前建立"资料质量闸门":先召回、再排序和评估;资料足够才回答,资料不足就改写问题重试,最终仍找不到才诚实兜底。

对于当前示例代码,可以先完成这一版最小闭环:

text 复制代码
retrieve → LLM 逐篇评分 → 过滤并按分数排序 → Top-N
  ↓
分数足够 → generate
分数不足 → rewrite → retrieve
重试用尽且无证据 → fallback

当文档量和请求量增长后,再把"LLM 逐篇评分"前置为"专用 Reranker 初筛",并将 LLM 留给少量高价值文档的证据充分性判断。

相关推荐
zed_231 小时前
让答案有出处:citations 引用溯源
人工智能
长江后浪博客1 小时前
Python + YOLOv8 疲劳驾驶 AI 视觉检测入门:从模型训练到 ONNX 实时摄像头检测完整实战
人工智能·python·yolo·疲劳驾驶检测·onnx·yolov8
聪明蛋子哟1 小时前
告别API“翻译”之苦:从OpenAPI到MCP,统一AI与工具集成的桥梁
人工智能
海上小飞龙1 小时前
大模型推理的两阶段:一次 Prefill,加上多次 Decode
人工智能·深度学习·语言模型
hhzz2 小时前
【OpenCV 入门到精通 01】认识 OpenCV 与计算机视觉:从零建立全局认知
人工智能·python·opencv·计算机视觉·开源
m4Rk_2 小时前
【论文阅读】Agent 记忆机制(62):DCM-Agent——用双簇记忆化解优化问题的多范式冲突
论文阅读·人工智能·学习·开源·github
xian_wwq2 小时前
【学习笔记】深度认知系列-第13讲AI Agent时代到来——从“回答问题”到“执行任务”
人工智能·笔记·学习
程序员cxuan2 小时前
GPT - 6 Astra 的使用焚诀
人工智能·后端·程序员
golang学习记2 小时前
Cursor Origin:Cursor要造一个AI时代的Github
人工智能·github·cursor