言
基础版 RAG 有个很明显的浪费:不管什么问题都先走一遍向量检索。其实很多简单问题、常识问题,大模型本身就能准确回答,根本不需要查知识库,白白浪费了检索的时间和算力。
本文是系列第三篇,我们给 RAG 加上一个「问题路由器」,让系统自动判断问题类型:
- 简单问题:直接让 LLM 回答,不走检索
- 复杂问题:走 RAG 检索流程,基于知识库回答
一、架构升级:从直线到分支
升级后的工作流变成了带条件分支的结构:
sql
START → route_question
├─ simple → direct_answer → END
└─ complex → retrieve → rag_generate → END
核心新增两个部分:
- route_question 路由节点:用 LLM + 结构化输出判断问题类型
- direct_answer 节点:直接调用 LLM 回答简单问题
二、结构化输出的作用
路由节点需要稳定输出「simple/complex」两种结果,不能让 LLM 自由发挥文字。这里用 Zod 定义输出 Schema,配合 LLM 的结构化输出能力,保证每次都返回符合格式的结果。
三、完整代码逐段解析
1. 新增依赖与状态扩展
引入 Zod 用于结构化输出校验,状态中新增 strategy 和 routeReason 字段,记录路由结果和理由。
php
import { z } from 'zod';
// 状态扩展
const GraphState = Annotation.Root({
question: Annotation,
k: Annotation,
strategy: Annotation, // 路由策略:simple / complex
routeReason: Annotation, // 路由理由
documents: Annotation,
generation: Annotation
})
2. 定义路由 Schema
用 Zod 定义输出结构,strategy 是枚举值,只能是 simple 或 complex,reason 是判断理由。
csharp
const RouteSchema = z.object({
strategy: z.enum(["simple", "complex"]),
reason: z.string()
});
3. 路由节点实现
给 LLM 加上结构化输出约束,传入路由规则和用户问题,返回判断结果并更新状态。
javascript
const routeQuestionNode = async (state) => {
console.log('___ROUTE-QUESTION___');
// 给模型加上结构化输出约束
const router = model.withStructuredOutput(RouteSchema);
const route = await router.invoke(`
你是问答路由器,请判断用户问题是否需要外部检索。
规则:
- simple: 常识问答、简短定义、无需特定小说细节即可回答。
- complex: 需要《天龙八部》具体情节、人物关系、章节事实、原文细节或证据支持。
用户问题: ${state.question}
`);
console.log(`路由策略:${route.strategy} ${route.reason}`);
return {
strategy: route.strategy,
routeReason: route.reason
}
}
4. 直接回答节点
对于 simple 类型的问题,直接调用 LLM 回答,不经过检索流程。
ini
const directAnswerNode = async (state) => {
console.log('----DIRECT_ANSWER----');
process.stdout.write("\n [AI 回答(流式)] \n");
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);
}
process.stdout.write("\n");
return {
documents: [],
generation
}
}
5. 条件边与图构建
用 addConditionalEdges 添加条件边,根据路由结果决定走哪个分支。
php
// 条件判断函数:根据 state.strategy 返回下一个节点名
const decideNext = (state) => (state.strategy === 'simple' ? "direct_answer" : "retrieve");
const graph = new StateGraph(GraphState)
.addNode("route_question", routeQuestionNode)
.addNode("direct_answer", directAnswerNode)
.addNode("retrieve", retrieveNode)
.addNode("rag_generate", generateNode)
.addEdge(START, "route_question")
// 条件分支:路由节点 → 直接回答 或 检索
.addConditionalEdges("route_question", decideNext, {
direct_answer: "direct_answer",
retrieve: "retrieve"
})
.addEdge("retrieve", "rag_generate")
.addEdge("direct_answer", END)
.addEdge("rag_generate", END)
.compile();
6. 主函数调用
和基础版类似,只是初始状态多了路由相关字段。
javascript
async function main () {
const question = "阿朱是怎么死的?";
const k = 5;
// Milvus 连接代码省略,与基础版一致...
const result = await graph.invoke({
question,
k: Number.isFinite(k)?k:5,
strategy: "",
routeReason: "",
documents: [],
generation: ""
});
console.log(JSON.stringify(result));
}
main().then(err => console.error(err));
四、两种场景的运行效果
场景 1:简单常识问题
csharp
___ROUTE-QUESTION___
路由策略:simple 这是基础常识问题,无需检索小说内容
[AI 回答(流式)]
阿朱是金庸小说《天龙八部》中的人物。
场景 2:小说细节问题
markdown
___ROUTE-QUESTION___
路由策略:complex 需要小说具体情节细节,必须检索原文
----检索中----
命中 5 条相关片段...
[AI回答(流式)]
阿朱是被萧峰一掌打死的...
五、升级收益与下一步
本次升级带来的提升
- 节省资源:常识问题跳过检索,减少向量数据库查询次数
- 响应更快:简单问题少了一次检索耗时,延迟大幅降低
- 架构清晰:路由逻辑独立,后续可以继续扩展更多分支
仍然存在的问题
对于多跳链式问题,单轮检索还是不够。比如:
《天龙八部》中四大恶人排行第二的是谁?此人之子在身世揭晓前,其生父在武林中的公开身份是什么?
这种问题需要先查「四大恶人第二是谁」,再查「他的儿子是谁」,再查「儿子的生父公开身份」,单轮检索根本答不准。
下一篇,我们就进入终极版:子问题拆解 + 多轮循环检索 + 智能规划器,实现真正的多跳推理 RAG。