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

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

前言

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

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

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

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

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

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

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

流程图:

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

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

数据准备:电子书入库

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

javascript 复制代码
// 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 里每条记录长这样:

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

检索与生成

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

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

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

javascript 复制代码
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

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

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

三、核心实现

1. 状态定义

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

javascript 复制代码
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 返回结构化的判断结果:

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

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

然后用 withStructuredOutput 让 LLM 按 schema 返回:

javascript 复制代码
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 决定走哪条路:

javascript 复制代码
const decideNext = (state) =>
  state.strategy === 'simple' ? "direct_answer" : "retrieve"

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

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

javascript 复制代码
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

javascript 复制代码
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) 问题路由判断 + 回答生成
Embedding text-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。

相关推荐
晚安日记wanna2 小时前
SSR 为什么不适合登录态从水合冲突到缓存串号
前端·react.js·面试
晚安日记wanna2 小时前
压测 TPS 卡在 800CPU 只跑四成六步定位法
运维·面试·测试
晚安日记wanna2 小时前
批量请求失败只弹一个 Toast面试官想听五层
前端·面试·架构
晚安日记wanna2 小时前
TS const 类型参数一道面试题的四层追问
前端·面试·typescript
晚安日记wanna2 小时前
大表 DDL 面试翻车现场Online DDL 为什么还会锁死业务
数据库·面试·架构
晚安日记wanna2 小时前
MySQL 主从延迟别只答并行复制
数据库·面试·架构
SamDeepThinking2 小时前
Java 17内存屏障:为什么需要,怎么用
java·后端·面试
CC数分2 小时前
2026年金融数据分析岗位需要哪些工具?2027届秋招备考指南
面试·数据分析
ocean21035 小时前
2025-2026年C语言专题面试高频知识点洞察
c语言·面试·面试经验