从 RAG 到 Agentic RAG:第一步,让模型决定「要不要检索」

摘要:普通 RAG 通常只有一条固定流水线:用户提问 → 向量检索 → 拼接上下文 → LLM 生成答案。它能工作,却不会判断。这篇从一个最基础的 RAG 出发,用 LangGraph 加入问题路由,让模型第一次真正参与系统控制流。

RAG 很容易做出 Demo。

准备一个向量数据库,把用户问题转成向量,检索几个相关片段,再塞给大模型:

复制代码
Question
   ↓
Retrieve
   ↓
Generate
   ↓
Answer

代码甚至可以简单到只有两个节点:

sql 复制代码
const graph = new StateGraph(GraphState)
  .addNode("retrieve", retrieveNode)
  .addNode("generate", generateNode)
  .addEdge(START, "retrieve")
  .addEdge("retrieve", "generate")
  .addEdge("generate", END)
  .compile();

这已经是一个完整的 RAG。

但问题也恰恰出在这里:

它太听话了。

无论用户问什么,它都老老实实执行:

复制代码
检索 → 生成

它不会想:

这个问题真的需要查知识库吗?

这就是普通 RAG 和 Agentic RAG 之间一个非常重要的分界点。


一、Naive RAG 的问题不是不能检索,而是只会检索

假设我们做的是一个《天龙八部》知识库。

用户问:

复制代码
阿朱的结局是什么?

这个问题显然适合走知识库。

因为它涉及小说中的具体情节:

复制代码
问题
 ↓
Milvus
 ↓
找到阿朱相关章节
 ↓
LLM 根据原文回答

没有问题。

但如果用户问:

复制代码
什么是向量数据库?

我们的 Naive RAG 还是会执行:

markdown 复制代码
什么是向量数据库?
        ↓
搜索《天龙八部》
        ↓
返回几个莫名其妙的小说片段
        ↓
交给 LLM

这就很荒谬了。

真正的问题不在 Milvus,也不在 Embedding。

而在于:

我们的程序没有决策能力。

它只是一个固定的数据处理流水线。


二、真正的升级:让 LLM 参与控制流

所以 Agentic RAG 的第一步,并不是突然增加十几个 Agent。

也不是给模型塞一堆 Tool。

而是做一件很小、但非常关键的事情:

让模型判断下一步该怎么走。

原来的流程:

sql 复制代码
START
  ↓
retrieve
  ↓
generate
  ↓
END

现在变成:

sql 复制代码
                 ┌→ direct_answer → END
                 │
START → router ──┤
                 │
                 └→ retrieve → rag_generate → END

程序第一次出现了真正意义上的分支

模型不再只是最后负责回答问题。

它开始参与:

复制代码
下一步执行哪个节点?

这才是这一阶段最值得理解的地方。


三、先定义 Graph State

我们的 Graph 需要维护几份数据:

php 复制代码
const GraphState = Annotation.Root({
  question: Annotation,
  k: Annotation,

  strategy: Annotation,
  routeReason: Annotation,

  documents: Annotation,
  generation: Annotation,
});

可以把它理解成整个工作流共享的一块状态:

复制代码
GraphState

question
   用户问题

k
   检索数量

strategy
   路由结果

routeReason
   为什么这么路由

documents
   检索出来的文档

generation
   最终回答

节点执行过程中,不需要自己把整个 State 从头维护一遍。

例如路由节点只负责产生:

kotlin 复制代码
return {
  strategy: route.strategy,
  routeReason: route.reason,
};

LangGraph 会把这些字段更新回 Graph State。

所以理解 LangGraph 时,可以先记住一句话:

节点读取当前 State,返回自己想修改的部分。

这样看代码会清晰很多。


四、让模型只负责一件事:分类

接下来定义路由结果:

csharp 复制代码
const RouteSchema = z.object({
  strategy: z.enum(["simple", "complex"]),
  reason: z.string(),
});

我们不要让模型自由发挥:

复制代码
我觉得这个问题可能需要检索......

而是要求它严格输出:

css 复制代码
{
  strategy: "complex",
  reason: "涉及《天龙八部》具体人物情节"
}

这里使用:

ini 复制代码
model.withStructuredOutput(RouteSchema);

得到一个受 Zod Schema 约束的模型。

ini 复制代码
const router = model.withStructuredOutput(RouteSchema);

然后让它完成分类:

ini 复制代码
const routeQuestionNode = async (state) => {
  const router = model.withStructuredOutput(RouteSchema);

  const route = await router.invoke(`
    你是问答路由器,请判断用户问题是否需要外部检索。

    规则:

    simple:
    常识问答、简短定义,
    无需特定小说细节即可回答。

    complex:
    需要《天龙八部》具体情节、
    人物关系、章节事实、
    原文细节或证据支持。

    用户问题:
    ${state.question}
  `);

  return {
    strategy: route.strategy,
    routeReason: route.reason,
  };
};

这里的大模型已经不是"回答模型"了。

它承担的是另一种角色:

复制代码
Router

输入:

复制代码
阿朱的结局是什么?

可能得到:

css 复制代码
{
  strategy: "complex",
  reason: "涉及小说具体人物结局,需要小说内容支持"
}

而输入:

复制代码
什么是向量数据库?

可能得到:

css 复制代码
{
  strategy: "simple",
  reason: "属于通用技术概念,无需查询小说知识库"
}

这时候模型还没有回答用户。

它只是完成了:

复制代码
分类

五、真正决定程序怎么走的是 Edge

这是我认为 LangGraph 最值得理解的一点。

很多人第一次看 Agent Workflow,会把所有注意力放在 Node 上:

复制代码
这个 Node 调了哪个模型?
这个 Node Prompt 怎么写?

但实际上:

Node 决定"做什么",Edge 决定"接下来做什么"。

定义一个条件函数:

ini 复制代码
const decideNext = (state) => {
  return state.strategy === "simple"
    ? "direct_answer"
    : "retrieve";
};

然后交给:

scss 复制代码
.addConditionalEdges()
php 复制代码
.addConditionalEdges(
  "route_question",
  decideNext,
  {
    direct_answer: "directAnswer",
    retrieve: "retrieve",
  }
)

于是:

scss 复制代码
routeQuestionNode
      ↓
strategy
      ↓
decideNext()

如果:

ini 复制代码
strategy === "simple"

走:

复制代码
directAnswer

如果:

ini 复制代码
strategy === "complex"

走:

复制代码
retrieve

这已经不是传统的:

scss 复制代码
await fn1();
await fn2();
await fn3();

而是在描述:

复制代码
一个状态驱动的执行图。

六、两条路线终于分开了

完整 Graph:

php 复制代码
const graph = new StateGraph(GraphState)
  .addNode("route_question", routeQuestionNode)
  .addNode("directAnswer", directAnswerNode)
  .addNode("retrieve", retrieveNode)
  .addNode("rag_generate", generateNode)

  .addEdge(START, "route_question")

  .addConditionalEdges(
    "route_question",
    decideNext,
    {
      direct_answer: "directAnswer",
      retrieve: "retrieve",
    }
  )

  .addEdge("directAnswer", END)
  .addEdge("retrieve", "rag_generate")
  .addEdge("rag_generate", END)

  .compile();

现在两个问题的执行轨迹已经完全不同。

普通问题

sql 复制代码
什么是向量数据库?

START
  ↓
route_question
  ↓
simple
  ↓
directAnswer
  ↓
END

根本不会访问 Milvus。


小说知识问题

sql 复制代码
阿朱的结局是什么?

START
  ↓
route_question
  ↓
complex
  ↓
retrieve
  ↓
Milvus
  ↓
rag_generate
  ↓
END

只有真正需要小说知识的时候才进行检索。


七、这已经算 Agentic RAG 了吗?

严格来说,它还非常初级。

但它已经出现了一个关键变化。

Naive RAG:

复制代码
程序员决定流程
scss 复制代码
retrieve();
generate();

现在:

复制代码
LLM 的判断
   ↓
影响 Graph 的执行路径

也就是说:

LLM 开始参与系统决策,而不只是生成文本。

这正是 Agentic 系统非常核心的思想。

所以我更愿意把现在这个版本称为:

复制代码
Agentic RAG 的第一步

而不是已经完成了一个多么智能的 Agent。

因为现在还有一个非常明显的问题没有解决。


八、检索出来的内容就一定靠谱吗?

现在我们的复杂问题会走:

复制代码
question
   ↓
retrieve
   ↓
documents
   ↓
generate

但这里偷偷做了一个非常危险的假设:

向量数据库返回的 Top K 文档都是有用的。

实际上并不是。

向量搜索只能告诉我们:

复制代码
这些文本在向量空间里比较相似。

它并不能保证:

复制代码
这些内容真的能够回答用户的问题。

例如用户问:

复制代码
阿朱为什么替父亲赴死?

向量库可能搜到:

复制代码
阿朱和乔峰相处的片段

语义确实相关。

但未必包含:

复制代码
为什么赴死

如果直接把这些内容交给模型生成答案,RAG 仍然可能产生幻觉。

所以现在的 Graph:

复制代码
route
 ↓
retrieve
 ↓
generate

还缺一个非常关键的节点:

复制代码
grade_documents

下一步,Graph 会继续进化成:

markdown 复制代码
                ┌───────────────┐
                │               ↓
retrieve → grade_documents → generate
                │
                └→ 不相关

但"不相关"之后怎么办?

重新搜索?

修改问题?

扩大 Top K?

还是换一种检索策略?

到了这里,RAG 才真正开始从一条固定流水线,变成一个:

会判断执行结果,并根据结果调整下一步动作的系统。

这也是下一篇要解决的问题。

相关推荐
火山引擎开发者社区1 小时前
一图看懂 ADrive 跨产品协作实践:文件通了,Agent 就通了
人工智能
蒲公英内测分发1 小时前
App 更新一次就换一个二维码?智能硬件配套 App 的版本和下载入口怎么管理
人工智能·测试工具·智能家居·智能硬件
Tp_jh1 小时前
AMD 395本地部署Qwen3.8-Flash-Next实测,端侧AGI时代也要来了
图像处理·人工智能·opencv·语言模型·自然语言处理·千问
xcLeigh1 小时前
让大模型长出手脚,自己写SQL查数据库(Function Calling初探)
数据库·人工智能·sql·时序数据库·timechoai
锋行天下1 小时前
LangGraph 模拟大模型调用工具失败后重试直到成功
人工智能
FL16238631291 小时前
胸片诊断肺炎结核积液识别分割数据集labelme格式3751张4类别
人工智能
天远数科2 小时前
零信任架构实战:基于天远全能消金报告构建自动化信贷评估网关
运维·人工智能·架构·自动化
云上先途2 小时前
属性标注是什么?主要解决什么问题?
大数据·人工智能
罗西的思考2 小时前
[Agent Memory / 强化学习] MemPO源码学习笔记 --- (2)--- 训练
人工智能·算法·机器学习