一文搞懂 Agentic RAG:用 LangGraph 打造会思考的智能检索架构

25 阅读4分钟

本文以《天龙八部》小说知识库为例,手把手带你从 Naive RAG 走向 Agentic RAG,用 LangGraph 实现一个能自主路由、可纠错的智能 RAG 系统。

前言

做过 RAG 的同学都知道,最基础的 RAG 流程就是:检索 → 拼上下文 → 生成回答。简单直接,但用着用着你会发现一堆问题:

  • 问"1+1等于几"也要走一遍向量检索?纯浪费 token
  • 检索回来的内容不相关,LLM 照样一本正经地胡说
  • 遇到"四大恶人排行第二的人的儿子的生父在武林中的公开身份是什么"这种多跳问题,直接歇菜

这些问题的根源在于:Naive RAG 没有"脑子",它不会判断、不会规划、不会纠错。

今天我们就来升级——用 LangGraph 搭建一个 Agentic RAG,让 RAG 学会"思考"。

一、Naive RAG:能跑但不够聪明

先回顾一下最基础的 RAG 长什么样:

const graph = new StateGraph(GraphState)
  .addNode("retrieve", retrieveNode)    // 检索
  .addNode("generate", generateNode)    // 生成
  .addEdge(START, "retrieve")
  .addEdge("retrieve", "generate")
  .addEdge("generate", END)
  .compile()

流程图:

START → retrieve → generate → END

所有问题都走同一条路:先检索,再生成。简单粗暴。

数据准备:电子书入库

在检索之前,我们得先把数据灌进向量库。以《天龙八部》epub 文件为例:

// 1. 加载 EPUB,按章节拆分
const loader = new EPubLoader(EPUB_FILE, { splitChapters: true });
const documents = await loader.load();

// 2. 每个章节再拆成 500 字的小片段(重叠 50 字保持上下文连贯)
const textSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 500,
  chunkOverlap: 50,
});

// 3. 每个片段调用 embedding API 转成向量,批量写入 Milvus
for (const chapter of documents) {
  const chunks = await textSplitter.splitText(chapter.pageContent);
  await insertChunksBatch(chunks, bookId, chapterNum);
}

Milvus 里每条记录长这样:

{
  id: "1_3_5",                    // 书id_章节号_片段序号
  book_id: 1,
  book_name: "天龙八部",
  chapter_num: 3,
  content: "乔峰道:阿朱...",
  vector: [0.12, -0.03, ...]     // 1024维向量
}

检索与生成

检索时,把用户问题转成向量,去 Milvus 里找最相似的 top-k 个片段:

const docsWithScores = await vectorStore.similaritySearchWithScore(question, k);
// 返回:[ [Document, 相似度分数], ... ]

然后把检索到的片段拼成上下文,交给 LLM 生成回答:

const context = documents.map((item, i) =>
  `[片段 ${i+1}] 章节: 第 ${item.chapter_num}章\n内容:${item.content}`
).join("\n\n");

const prompt = `你是《天龙八部》小说助手,请根据以下内容回答问题:
${context}
用户问题:${question}`;

Naive RAG 的三大痛点

痛点一:所有问题都走检索,浪费资源

"1+1等于几" 这种常识问题,完全不需要去向量库里搜一遍。白白浪费了 embedding 调用 + 向量检索的时间和 token。

痛点二:没有纠错机制

检索回来的片段可能跟问题完全不相关,但 LLM 还是会硬着头皮编答案。

痛点三:处理不了复杂问题

"四大恶人排行第二的人是谁?此人之子的生父在武林中的公开身份是什么?"——这种需要先查 A、再查 B 的多跳问题,单次检索根本搞不定。

二、Agentic RAG:让 RAG 学会思考

Agentic RAG 的核心思想:把固定的流水线升级成能自主决策的智能体

关键改进:

  1. 智能路由:简单问题直接回答,复杂问题才走检索
  2. 可扩展性:未来可以加入多步检索、网络搜索、纠错评估等节点

设计新的 Graph

            ┌──→ direct_answer → END
START → route_question ─┤
            └──→ retrieve → rag_generate → END

多了个 route_question 节点,它会判断问题类型,然后走不同的分支。

三、核心实现

1. 状态定义

用 LangGraph 的 Annotation 定义图的状态,比 Naive RAG 多了 strategyrouteReason

const GraphState = Annotation.Root({
  question: Annotation,      // 用户问题
  k: Annotation,             // 检索数量
  strategy: Annotation,      // 路由策略:simple 或 complex
  routeReason: Annotation,   // 路由原因
  documents: Annotation,     // 检索到的文档
  generation: Annotation     // 生成的回答
})

2. 路由节点:用 Zod 做结构化输出

路由节点是整个 Agentic RAG 的"大脑"。我们用 Zod 定义输出 schema,让 LLM 返回结构化的判断结果:

const RouteSchema = z.object({
  strategy: z.enum(["simple", "complex"]),  // 只能二选一
  reason: z.string()                         // 判断理由
});

z.enum(["simple", "complex"]) 保证 LLM 只能返回这两个值之一,不会乱来。

然后用 withStructuredOutput 让 LLM 按 schema 返回:

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
  }
}

3. 条件分支:decideNext

路由判断完后,根据 strategy 决定走哪条路:

const decideNext = (state) =>
  state.strategy === 'simple' ? "direct_answer" : "retrieve"

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

简单问题不走检索,直接让 LLM 回答:

const directAnswerNode = async (state) => {
  let generation = "";
  const stream = await model.stream(`你是一个中文回答助手,请简洁回答问题。
    问题:${state.question}`);

  for await (const chunk of stream) {
    const text = typeof chunk.content === 'string' ? chunk.content : "";
    if (!text) continue;
    generation += text;
    process.stdout.write(text);  // 流式打印,打字机效果
  }

  return { generation, documents: [] }
}

5. 检索 + 生成节点(复杂问题)

复杂问题走原来的 RAG 流程:检索 → 拼上下文 → 生成。

6. 组装 Graph

const graph = new StateGraph(GraphState)
  .addNode("route_question", routeQuestionNode)
  .addNode("direct_answer", directAnswerNode)
  .addNode("retrieve", retrieveNode)
  .addNode("rag_generate", generateNode)
  .addEdge(START, "route_question")
  // 条件分支:根据 strategy 决定走哪条路
  .addConditionalEdges("route_question", decideNext, {
    direct_answer: "direct_answer",
    retrieve: "retrieve"
  })
  .addEdge("retrieve", "rag_generate")
  .addEdge("direct_answer", END)
  .addEdge("rag_generate", END)
  .compile();

注意 addConditionalEdges——这是 LangGraph 实现条件分支的关键 API。第一个参数是源节点,第二个是决策函数,第三个是分支映射。

四、技术栈总结

组件技术选型作用
编排框架LangGraph状态图、条件分支、流程控制
LLM通义千问(qwen-plus)问题路由判断 + 回答生成
Embeddingtext-embedding-v3(1024维)文本转向量
向量数据库Milvus存储和检索向量
数据验证Zod结构化输出 schema 约束
数据加载LangChain EPubLoader解析 EPUB 电子书

五、下一步优化方向

目前的 Agentic RAG 只实现了"智能路由"这一个增强点,还可以继续加:

  • 检索质量评估:检索完让 LLM 打分,分数太低就重新检索或换策略
  • 多步检索:复杂问题拆成多个子问题,分步检索再综合
  • 混合检索:语义检索 + 关键词检索(BM25),解决精确实体匹配问题
  • 网络搜索兜底:本地知识库找不到的,去网络搜索补充

这些节点都可以像搭积木一样加到 LangGraph 的 StateGraph 里,这也是 Agentic RAG 的魅力——架构可扩展,逻辑可组合

总结

Naive RAG 是"流水线工人",Agentic RAG 是"有脑子的工人"。

核心区别在于:Agentic RAG 在流程中加入了决策节点(路由判断)和条件分支(简单/复杂走不同路径),让整个系统从"固定流程"升级为"自主规划"。

LangGraph 的 StateGraph + 条件边,天然适合实现这种有分支、有循环的智能流程。如果你的 RAG 系统还停留在"所有问题一锅炖"的阶段,不妨试试 Agentic RAG。