从“死板的检索”到“会思考的助手”:用 LangGraph 打造 Agentic RAG

0 阅读8分钟

传统 RAG(检索增强生成)的工作方式很直接:收到问题 → 去知识库检索相关内容 → 把内容喂给大模型 → 生成回答。这套流程在简单场景下够用,但一旦遇到复杂问题,就会暴露不少短板。

想象一下这样的场景:

  • 用户问“1+1 等于几?”——系统依然会老老实实去向量数据库里搜一遍,浪费时间和算力。
  • 用户问一个需要多步推理的复杂问题——系统只会做一次检索,无法分步骤获取信息再综合得出结论。
  • 检索到的内容质量不高——系统没有评估机制,直接把不相关的内容塞给大模型,生成的结果自然也不靠谱。

这些问题指向同一个结论:固定的“检索→生成”流水线不够智能。我们需要一个能思考、能判断、能自主规划检索策略的系统——这就是 Agentic RAG 的核心思想

本文将带你从零理解 Agentic RAG 的设计思路,并用 LangGraph 一步步实现一个智能查询路由的 RAG 系统。所有代码均基于你提供的笔记中的实际案例展开。

一、传统 RAG 的四个痛点

在动手改造之前,先搞清楚传统 RAG 到底“痛”在哪里。

痛点一:不问青红皂白,什么题都去检索

RAG 的固定流程是“收到问题→立刻检索”。但对于“1+1 等于几”这类常识问题,大模型本身就能回答,根本不需要从外部知识库检索。每次检索都要调用向量数据库、消耗 token、增加延时,属于典型的资源浪费。

痛点二:检索结果没有“质检”

传统 RAG 检索到什么就用什么,缺少对检索内容质量进行评估的环节。如果检索到的文档和问题毫无关系,系统也会照单全收,最终生成牛头不对马嘴的回答。

痛点三:处理不了多步推理的复杂问题

有些问题需要“先查 A,再查 B,最后综合得出结论”。比如:“《天龙八部》中,四大恶人排行第二的是谁?此人之子在身世揭晓前,其生父在武林中的公开身份是什么?”

这个问题需要两步:先确定“排行第二的恶人”是谁,再根据这个人的身份去查他儿子的生父信息。传统 RAG 只做一次检索,无法完成这种多步推理。

痛点四:语义检索不是万能的

向量检索(语义检索)擅长理解自然语言的含义,但遇到专业术语或精确实体时,效果反而不如关键词匹配。比如“高血糖”和“低血糖”在语义上很接近,但实际上是完全不同的概念——用语义检索反而容易混淆。

传统 RAG 的这些问题,根源在于它没有自主决策能力。而 Agentic RAG 的解决方案是:把 RAG 流程改造成一个由 AI 智能体驱动的、可自主决策的工作流。

二、Agentic RAG:让 RAG 学会“思考”

Agentic RAG 的核心思路并不复杂:不再把 RAG 当成一条固定的流水线,而是把它设计成一个有状态、可决策的工作流

具体来说,Agentic RAG 让大模型在检索流程中扮演“决策者”的角色:

  • 判断是否需要检索:简单问题直接回答,复杂问题才走检索流程。
  • 评估检索质量:检索到的内容够不够?准不准?如果不够好,可以重新检索或调整策略。
  • 规划多步检索:把复杂问题拆成多个子任务,按顺序检索再综合答案。
  • 选择合适的检索方式:该用语义检索就用向量,该用关键词匹配就用关键词。

要让大模型具备这些决策能力,我们需要一个能编排复杂工作流的工具。LangGraph 正是为此而生。

三、LangGraph 入门:用“图”来管理 AI 工作流

LangGraph 是 LangChain 生态中用于构建有状态、多步骤 AI 应用的框架。它的核心概念只有三个:状态节点

状态(State)

状态是一个共享的数据容器,贯穿整个工作流。每个节点都可以读取和修改状态,后续节点基于更新后的状态继续执行。

在 RAG 场景中,状态通常包含:用户问题、检索到的文档、生成的回答等。

javascript

// 用 LangGraph 的 Annotation 定义状态
const GraphState = Annotation.Root({
  question: Annotation,   // 用户问题
  k: Annotation,          // 检索数量
  documents: Annotation,  // 检索到的文档
  generation: Annotation  // 生成的回答
});

节点(Node)

节点是工作流中的一个执行步骤。每个节点接收当前状态,执行特定任务(比如检索、生成、路由判断),然后返回更新后的状态。

javascript

// 一个检索节点的示例
const retrieveNode = async (state) => {
  const documents = await retrieveRelevantContent(state.question, state.k);
  return { ...state, documents };
};

边(Edge)

边定义了节点之间的流转关系。普通边表示“A 执行完一定去 B”,条件边表示“A 执行完后根据状态决定去 B 还是去 C”。

javascript

// 普通边:retrieve 执行完去 generate
graph.addEdge("retrieve", "generate");

// 条件边:根据策略决定下一步
graph.addConditionalEdges("route", decideNextNode);

把三者组合起来

用 LangGraph 构建工作流的过程,就是把节点连接成,让数据在状态中流动:

javascript

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

这段代码定义了一个最简单的 RAG 流程:开始 → 检索 → 生成 → 结束。

四、实战:用 LangGraph 实现智能查询路由

理解了 LangGraph 的基本概念后,我们来解决传统 RAG 的第一个痛点:所有问题都走检索流程,浪费资源

解决方案是引入一个路由节点,让大模型先判断问题的类型,再决定走哪条路。

4.1 定义路由策略

首先,我们需要定义两种策略:

  • simple:简单问题(常识、简短定义),不需要检索,大模型直接回答。
  • complex:复杂问题(需要特定知识、具体情节、多步推理),需要走 RAG 检索流程。

为了让大模型的结构化输出更可靠,我们使用 zod 定义输出格式:

javascript

import { z } from 'zod';

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

4.2 实现路由节点

路由节点的任务很明确:接收用户问题,让大模型判断该走哪条路。

javascript

const routeQuestionNode = async (state) => {
  // 使用 withStructuredOutput 强制模型按 schema 输出
  const router = model.withStructuredOutput(RouteSchema);
  
  const route = await router.invoke(`
    你是问答路由器,请判断用户问题是否需要外部检索。
    规则:
    - simple:常识问答、简短定义、无需特定小说细节即可回答。
    - complex:需要《天龙八部》具体情节、人物关系、章节事实、原文细节。
    
    用户问题:${state.question}
  `);

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

这里的关键是 withStructuredOutput——它让大模型不仅输出文本,还能按照我们定义的 RouteSchema 输出结构化的 JSON 数据,便于后续节点做条件判断。

4.3 设计完整的图结构

有了路由节点,我们就可以设计一个带分支的图:

text

开始 → 路由判断
         ├─ simple → 直接回答 → 结束
         └─ complex → 检索 → 生成 → 结束

用代码表示:

javascript

const graph = new StateGraph(GraphState)
  .addNode("route", routeQuestionNode)
  .addNode("retrieve", retrieveNode)
  .addNode("generate", generateNode)
  .addNode("directAnswer", directAnswerNode)
  .addEdge(START, "route")
  .addConditionalEdges("route", decideNextNode)
  .addEdge("retrieve", "generate")
  .addEdge("generate", END)
  .addEdge("directAnswer", END)
  .compile();

条件边 decideNextNode 的逻辑:

javascript

const decideNextNode = (state) => {
  if (state.strategy === "simple") {
    return "directAnswer";
  } else {
    return "retrieve";
  }
};

4.4 两种回答节点的实现

直接回答节点(简单问题):

javascript

const directAnswerNode = async (state) => {
  const response = await model.invoke(`请简洁回答:${state.question}`);
  return { ...state, generation: response.content };
};

生成节点(复杂问题,基于检索结果):

javascript

const generateNode = async (state) => {
  const context = state.documents
    .map((item, i) => `[片段 ${i+1}] 章节:${item.chapter_num} 内容:${item.content}`)
    .join("\n");
    
  const prompt = `基于以下内容回答问题:\n${context}\n问题:${state.question}`;
  const response = await model.invoke(prompt);
  return { ...state, generation: response.content };
};

4.5 效果对比

有了查询路由后,系统处理问题的方式发生了明显变化:

问题类型示例传统 RAGAgentic RAG(带路由)
简单问题“1+1 等于几?”检索+生成(浪费资源)直接回答(省时省力)
复杂问题“阿朱的结局是什么?”检索+生成检索+生成
多步推理“四大恶人排行第二的是谁?此人之子...”一次检索(信息不足)可进一步拆解为多步

路由节点只是 Agentic RAG 的第一步。基于同样的思路,我们还可以继续扩展:

  • 评估节点:检索后让大模型判断内容是否相关,不相关则重新检索。
  • 多步检索节点:把复杂问题拆成子任务,循环执行“检索→评估→再检索”。
  • 混合检索节点:根据问题类型,在向量检索和关键词检索之间切换。

五、完整的流程示意图

用 Mermaid 绘制出的工作流图示例如下(来自笔记中 naive-rag.mjs 的实际输出):

text

graph TD
    START([start]) --> retrieve[retrieve]
    retrieve --> generate[generate]
    generate --> END([end])

加上路由节点后,流程图变为:

text

graph TD
    START([start]) --> route[route]
    route -->|simple| directAnswer[directAnswer]
    route -->|complex| retrieve[retrieve]
    retrieve --> generate[generate]
    directAnswer --> END([end])
    generate --> END([end])

六、总结

本文从一个具体问题出发——传统 RAG 的固定流程不够智能——逐步展开了 Agentic RAG 的设计思路和实现方法。

核心观点可以概括为三点:

  1. RAG 不应该是一条固定的流水线。不同的问题需要不同的处理策略,系统应该有自主决策的能力。
  2. LangGraph 是用“图”来管理 AI 工作流的工具。它通过状态(共享数据)、节点(执行步骤)和(流转规则)三个概念,让开发者可以灵活编排复杂的 AI 流程
  3. 查询路由是 Agentic RAG 的入门级实践。通过让大模型判断问题类型(简单/复杂),系统可以绕过不必要的检索流程,节省资源、提升响应速度。

从查询路由开始,你还可以继续往 Agentic RAG 的方向深入:加入检索质量评估、实现多步推理、集成混合检索策略……每一步都是在让 RAG 系统从“死板的工具”变成“会思考的助手”。