本文以《天龙八部》小说知识库为例,手把手带你从 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 的核心思想:把固定的流水线升级成能自主决策的智能体。
关键改进:
- 智能路由:简单问题直接回答,复杂问题才走检索
- 可扩展性:未来可以加入多步检索、网络搜索、纠错评估等节点
设计新的 Graph
┌──→ direct_answer → END
START → route_question ─┤
└──→ retrieve → rag_generate → END
多了个 route_question 节点,它会判断问题类型,然后走不同的分支。
三、核心实现
1. 状态定义
用 LangGraph 的 Annotation 定义图的状态,比 Naive RAG 多了 strategy 和 routeReason:
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) | 问题路由判断 + 回答生成 |
| 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。