传统 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 效果对比
有了查询路由后,系统处理问题的方式发生了明显变化:
| 问题类型 | 示例 | 传统 RAG | Agentic 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 的设计思路和实现方法。
核心观点可以概括为三点:
- RAG 不应该是一条固定的流水线。不同的问题需要不同的处理策略,系统应该有自主决策的能力。
- LangGraph 是用“图”来管理 AI 工作流的工具。它通过状态(共享数据)、节点(执行步骤)和边(流转规则)三个概念,让开发者可以灵活编排复杂的 AI 流程。
- 查询路由是 Agentic RAG 的入门级实践。通过让大模型判断问题类型(简单/复杂),系统可以绕过不必要的检索流程,节省资源、提升响应速度。
从查询路由开始,你还可以继续往 Agentic RAG 的方向深入:加入检索质量评估、实现多步推理、集成混合检索策略……每一步都是在让 RAG 系统从“死板的工具”变成“会思考的助手”。