进阶篇:RAG 优化第一步:给你的问答系统加上智能路由,实现双分支分流

言

基础版 RAG 有个很明显的浪费:不管什么问题都先走一遍向量检索。其实很多简单问题、常识问题,大模型本身就能准确回答,根本不需要查知识库,白白浪费了检索的时间和算力。

本文是系列第三篇,我们给 RAG 加上一个「问题路由器」,让系统自动判断问题类型:

  • 简单问题:直接让 LLM 回答,不走检索
  • 复杂问题:走 RAG 检索流程,基于知识库回答

一、架构升级:从直线到分支

升级后的工作流变成了带条件分支的结构:

sql 复制代码
START → route_question
     ├─ simple → direct_answer → END
     └─ complex → retrieve → rag_generate → END

核心新增两个部分:

  1. route_question 路由节点:用 LLM + 结构化输出判断问题类型
  2. 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。

相关推荐
Shockang1 小时前
LangChain 智能体治理实战:预算、权限与故障恢复
langchain
inksci2 小时前
发布Ai生成的html markdown 推荐
前端·javascript·html
是江迪呀2 小时前
向全世界售卖你的产品:做了一款教程引导插件
前端·javascript·后端
码艺-Alimjan3 小时前
维吾尔语 RTL 排版的隐形利器:`\u200F`
前端·javascript
Highcharts.js4 小时前
Highcharts 数据过滤实战指南:从基础实现到性能优化
javascript·性能优化·bi·highcharts·前端可视化·数据过滤·交互图表
一木 之林5 小时前
LangChain 第9章 RAG 与项目实战:文档切分、嵌入、Milvus 与客服知识库
人工智能·计算机视觉·langchain·milvus
何何____5 小时前
TypeScript 函数类型兼容性
前端·javascript·typescript
雪芽蓝域zzs5 小时前
json数据转化成interface属性
前端·javascript·json