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

文章来源声明: 原文作者:东风破_; 来源站点:掘金; 原文链接:https://juejin.cn/post/7685648064543031342; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

觅优短评:价值在于用 LangGraph 把“是否检索”变成模型可决策的分支,适合想从 Demo 迈向 Agentic RAG 的开发者。检索质量评估是下一道坎。

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

RAG 很容易做出 Demo。

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

Question
   ↓
Retrieve
   ↓
Generate
   ↓
Answer

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

const graph <span>=</span> <span>new</span> StateGraph(GraphState)
  .addNode("retrieve", retrieveNode)
  .addNode("generate", generateNode)
  .addEdge(<span>START</span>, "retrieve")
  .addEdge("retrieve", "generate")
  .addEdge("generate", <span>END</span>)
  .compile();

这已经是一个完整的 RAG。

但问题也恰恰出在这里:

它太听话了。

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

检索 → 生成

它不会想:

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

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


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

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

用户问:

阿朱的结局是什么?

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

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

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

没有问题。

但如果用户问:

什么是向量数据库?

我们的 Naive RAG 还是会执行:

什么是向量数据库?
<span>        ↓
搜索《天龙八部》
        ↓
返回几个莫名其妙的小说片段
        ↓
交给 LLM
</span>

这就很荒谬了。

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

而在于:

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

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


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

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

也不是给模型塞一堆 Tool。

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

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

原来的流程:

<span>START</span>
  ↓
retrieve
  ↓
generate
  ↓
<span>END</span>

现在变成:

                 ┌→ direct_answer → <span>END</span>
                 │
<span>START</span> → router ──┤
                 │
                 └→ retrieve → rag_generate → <span>END</span>

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

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

它开始参与:

下一步执行哪个节点?

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


三、先定义 Graph State

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

<span>const</span> <span>GraphState</span> = Annotation.<span>Root</span>({
  <span>question</span>: Annotation,
  <span>k</span>: Annotation,

  <span>strategy</span>: Annotation,
  <span>routeReason</span>: Annotation,

  <span>documents</span>: Annotation,
  <span>generation</span>: Annotation,
});

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

GraphState

question
   用户问题

k
   检索数量

strategy
   路由结果

routeReason
   为什么这么路由

documents
   检索出来的文档

generation
   最终回答

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

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

<span>return</span> {
  strategy: route.strategy,
  routeReason: route.reason,
};

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

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

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

这样看代码会清晰很多。


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

接下来定义路由结果:

<span>const</span> RouteSchema = z.<span>object</span>({
  strategy: z.<span>enum</span>([<span>"simple"</span>, <span>"complex"</span>]),
  reason: z.<span>string</span>(),
});

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

我觉得这个问题可能需要检索……

而是要求它严格输出:

{
  strategy: <span>"complex"</span>,
  reason: <span>"涉及《天龙八部》具体人物情节"</span>
}

这里使用:

model.withStructuredOutput(RouteSchema)<span>;</span>

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

const <span>router</span> = model.withStructuredOutput(RouteSchema)<span>;</span>

然后让它完成分类:

const <span>routeQuestionNode</span> = async (state) => {
  const <span>router</span> = model.withStructuredOutput(RouteSchema)<span>;</span>

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

    规则:

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

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

    用户问题:
    ${state.question}
  `)<span>;</span>

  return {
    strategy: route.strategy,
    routeReason: route.reason,
  }<span>;</span>
}<span>;</span>

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

它承担的是另一种角色:

Router

输入:

阿朱的结局是什么?

可能得到:

{
  strategy: <span>"complex"</span>,
  reason: <span>"涉及小说具体人物结局,需要小说内容支持"</span>
}

而输入:

什么是向量数据库?

可能得到:

{
  strategy: <span>"simple"</span>,
  reason: <span>"属于通用技术概念,无需查询小说知识库"</span>
}

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

它只是完成了:

分类


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

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

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

这个 Node 调了哪个模型?
这个 Node Prompt 怎么写?

但实际上:

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

定义一个条件函数:

const <span>decideNext</span> = (state) => {
  return <span>state.strategy</span> === <span>"simple"</span>
    ? "direct_answer"
    : "retrieve"<span>;</span>
}<span>;</span>

然后交给:

<span>.addConditionalEdges</span>()

.<span>addConditionalEdges</span>(
  <span>"route_question"</span>,
  decideNext,
  {
    <span>direct_answer</span>: <span>"directAnswer"</span>,
    <span>retrieve</span>: <span>"retrieve"</span>,
  }
)

于是:

routeQuestionNode
      ↓
strategy
      ↓
<span>decideNext</span>()

如果:

<span>strategy</span> === <span>"simple"</span>

走:

directAnswer

如果:

<span>strategy</span> === <span>"complex"</span>

走:

retrieve

这已经不是传统的:

await <span>fn1</span>();
await <span>fn2</span>();
await <span>fn3</span>();

而是在描述:

一个状态驱动的执行图。


六、两条路线终于分开了

完整 Graph:

<span>const</span> <span>graph</span> = <span>new</span> <span>StateGraph</span>(GraphState)
  .<span>addNode</span>(<span>"route_question"</span>, routeQuestionNode)
  .<span>addNode</span>(<span>"directAnswer"</span>, directAnswerNode)
  .<span>addNode</span>(<span>"retrieve"</span>, retrieveNode)
  .<span>addNode</span>(<span>"rag_generate"</span>, generateNode)

  .<span>addEdge</span>(START, <span>"route_question"</span>)

  .<span>addConditionalEdges</span>(
    <span>"route_question"</span>,
    decideNext,
    {
      <span>direct_answer</span>: <span>"directAnswer"</span>,
      <span>retrieve</span>: <span>"retrieve"</span>,
    }
  )

  .<span>addEdge</span>(<span>"directAnswer"</span>, END)
  .<span>addEdge</span>(<span>"retrieve"</span>, <span>"rag_generate"</span>)
  .<span>addEdge</span>(<span>"rag_generate"</span>, END)

  .<span>compile</span>();

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

普通问题

什么是向量数据库?

<span>START</span>
  ↓
route_question
  ↓
simple
  ↓
directAnswer
  ↓
<span>END</span>

根本不会访问 Milvus。


小说知识问题

阿朱的结局是什么?

<span>START</span>
  ↓
route_question
  ↓
complex
  ↓
retrieve
  ↓
Milvus
  ↓
rag_generate
  ↓
<span>END</span>

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


七、这已经算 Agentic RAG 了吗?

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

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

Naive RAG:

程序员决定流程

<span>retrieve</span>();
<span>generate</span>();

现在:

LLM 的判断
   ↓
影响 Graph 的执行路径

也就是说:

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

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

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

Agentic RAG 的第一步

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

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


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

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

question
   ↓
retrieve
   ↓
documents
   ↓
generate

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

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

实际上并不是。

向量搜索只能告诉我们:

这些文本在向量空间里比较相似。

它并不能保证:

这些内容真的能够回答用户的问题。

例如用户问:

阿朱为什么替父亲赴死?

向量库可能搜到:

阿朱和乔峰相处的片段

语义确实相关。

但未必包含:

为什么赴死

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

所以现在的 Graph:

route
 ↓
retrieve
 ↓
generate

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

grade_documents

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

<span>                ┌───────────────┐
                │               ↓
retrieve → grade_documents → generate
                │
                └→ 不相关
</span>

但“不相关”之后怎么办?

重新搜索?

修改问题?

扩大 Top K?

还是换一种检索策略?

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

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

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