RAG 系统进化论(四):Modular RAG,从固定流水线到动态工作流

RAG 系统进化论(四):Modular RAG,从固定流水线到动态工作流

本文导读: Modular RAG(模块化 RAG)不再要求所有问题经过同一条固定流水线,而是把检索、数据源和生成步骤拆成可组合模块。本文将介绍 Router(路由器)、条件分支、工具封装、并行检索、结果融合、状态与工作流,理解系统怎样根据问题动态选择更合适的处理路径。

上一篇介绍了 Advanced RAG(进阶式 RAG)。

我们通过改进文档切块、查询转换、混合检索和检索后处理,让进入大模型的证据更加准确:

text 复制代码
用户问题
  ↓
查询优化
  ↓
BM25(关键词相关性排序)+ 向量检索
  ↓
融合、重排与上下文压缩
  ↓
大模型回答

这条流程已经比 Naive RAG(基础 RAG)强大很多,但它仍然隐含了一个前提:

所有问题都适合经过同一条检索流水线。

真实系统并不是这样。

假设我们正在为电商客服构建 RAG,用户可能提出四类问题:

text 复制代码
问题一:定制版键盘支持七天无理由退货吗?
问题二:我的订单什么时候到?
问题三:上个月华东地区的键盘退款率是多少?
问题四:AX-2048-C 最近频繁出现 E1007,是产品故障吗?

它们需要的数据完全不同:

问题 需要的数据源 适合的处理方式
退货政策 文档知识库 混合检索政策文档
订单物流 订单 API(Application Programming Interface,应用程序接口) 根据订单号查询实时状态
地区退款率 SQL(Structured Query Language,结构化查询语言)数据库 查询并计算结构化数据
产品故障分析 产品手册、售后工单、监控数据 并行检索多个来源后合并

如果把所有问题都送进向量数据库:

  • 知识库不会知道用户订单的实时位置;
  • 文档检索无法准确计算退款率;
  • 单一数据源也无法完成跨来源故障分析。

这不是检索"准不准"的问题,而是系统一开始就选错了处理方式。

于是,RAG 从优化一条固定流水线,继续演进到 Modular RAG(模块化 RAG):

把 RAG 拆成可以独立组合的模块,再根据问题和中间结果动态选择需要的模块。

什么是 Modular RAG?

Naive RAG 和常见的 Advanced RAG 更像一条固定生产线:

text 复制代码
问题 → 固定检索器 → 固定排序器 → 固定提示词 → 大模型

Modular RAG 则把系统拆成多个可组合模块:

text 复制代码
查询理解模块
路由模块
文档检索模块
关键词检索模块
SQL 查询模块
业务 API 模块
结果融合模块
生成模块

系统不再要求所有问题依次经过全部模块,而是根据当前任务选择路径:

Modular RAG 的核心变化可以概括为:

text 复制代码
固定步骤
   ↓
可复用模块
   ↓
按条件选择模块
   ↓
组合成动态工作流

这里的"动态"并不意味着系统可以随意行动。

通常仍由开发者预先定义:

  • 系统有哪些模块;
  • 每个模块可以访问什么数据;
  • 哪些路径可以执行;
  • 分支失败后如何处理;
  • 最多允许多少次调用。

系统只是在这些边界内选择合适路径。

Modular RAG 的核心技术

这一阶段常见的技术包括:

技术 作用
Router(路由器) 判断问题应该进入哪条处理路径
Conditional Routing(条件路由) 根据问题或中间结果决定下一步
Multi-source Retrieval(多数据源检索) 连接文档、搜索引擎、数据库和业务接口
Parallel Retrieval(并行检索) 同时查询多个互不依赖的数据源
Result Fusion(结果融合) 把不同来源的结果整理为统一证据
Tool Calling(工具调用) 用结构化参数调用检索器、数据库或业务能力
State(状态)与 Workflow Orchestration(工作流编排) 保存中间数据,并管理节点、分支和执行顺序

下面沿着一次请求的执行过程,逐个说明这些技术。

第一步:Router 决定问题走哪条路

Router(路由器)是 Modular RAG 的入口。

它接收用户问题,输出一个结构化的路由结果:

text 复制代码
输入:
"我的订单 20260729001 到哪里了?"

输出:
route = order_api
orderId = 20260729001

路由器需要判断的不是"答案是什么",而是:

应该由哪个模块负责寻找答案?

路由可以依据什么?

最常见的三种方式是:

1. 规则路由

Rule-based Routing(基于规则的路由)使用关键词、正则表达式或已有业务字段进行判断。

text 复制代码
包含订单号,并询问物流 → order_api
包含"退款率""销售额" → analytics_sql
包含"政策""规则" → document_search

它速度快、成本低、结果可预测,适合边界明确的问题。

但表达方式变复杂后,规则容易膨胀,也难以理解模糊意图。

2. 分类模型或大模型路由

系统也可以让分类模型或 语言模型从预先允许的路由中选择一个或多个。

这种方式更能理解自然语言,但输出必须经过结构校验,不能让模型生成任意工具名称和参数。

3. 规则与模型组合

实际系统通常先处理确定性强的规则,再让模型判断剩余问题:

text 复制代码
有明确订单号 → 直接进入订单模块
有固定报表名称 → 直接进入统计模块
其他自然语言问题 → 交给模型分类

这样既保留稳定性,也能处理复杂表达。

一个简化的路由器如下:

ts 复制代码
async function routeQuestion(
  question: string,
): Promise<RouteDecision> {
  // 优先识别订单号,确定性强的问题不必调用大模型
  const orderId = extractOrderId(question);
  if (orderId && asksAboutDelivery(question)) {
    return {
      routes: ["order_api"],
      parameters: { orderId },
      reason: "问题包含订单号并询问物流状态",
    };
  }

  // 识别明确的统计意图,将问题交给受控的 SQL 查询模块
  if (asksForBusinessMetric(question)) {
    return {
      routes: ["analytics_sql"],
      parameters: extractMetricConditions(question),
      reason: "问题需要聚合结构化业务数据",
    };
  }

  // 对剩余问题使用模型分类,但只能从允许的路由中选择
  return await classifyIntoAllowedRoutes(question, [
    "document_search",
    "support_ticket_search",
    "monitoring_search",
  ]);
}

伪代码中的 reason 不是答案,而是路由理由。记录它有助于后续排查系统为什么选中了某条路径。

第二步:Conditional Routing 决定下一步

Router 通常根据原始问题选择入口。

Conditional Routing(条件路由)则可以根据中间结果继续决定下一步。

例如,用户问:

text 复制代码
AX-2048-C 出现 E1007 怎么处理?

系统先搜索产品手册:

text 复制代码
如果手册已经找到完整处理步骤
→ 直接生成答案

如果手册只解释错误码,没有解决方法
→ 继续查询售后工单

如果型号不存在
→ 请求用户确认型号

条件路由让工作流不必提前执行所有模块:

可以把它理解为普通程序中的 if / else,只是判断条件可能来自业务规则,也可能来自模型对中间结果的分类。

ts 复制代码
function chooseNextStep(
  state: WorkflowState,
): WorkflowNode {
  // 没有识别出产品型号时,先向用户收集必要信息
  if (!state.productId) {
    return "request_more_information";
  }

  // 手册已经提供完整答案时,不再调用其他数据源
  if (state.manualEvidenceIsSufficient) {
    return "generate_answer";
  }

  // 手册证据不足时,继续查询历史工单寻找处理经验
  return "search_support_tickets";
}

不过,"证据是否足够"本身并不容易判断。Modular RAG 可以建立这条分支,却不保证判断一定正确,这会成为下一篇 Corrective RAG(纠错式 RAG)要解决的问题。

第三步:把不同数据源封装成工具

Multi-source Retrieval(多数据源检索)意味着系统可以从多种数据源获取信息,例如:

text 复制代码
Vector Store(向量存储):语义检索产品文档
Elasticsearch(搜索与分析引擎):检索型号、错误码和关键词
SQL 数据库:查询订单、库存和统计数据
API(Application Programming Interface,应用程序接口):获取物流和售后状态
外部搜索:查找公开且需要更新的信息

这些数据源的查询方式和返回格式不同。

如果让工作流直接依赖每种底层实现,路由和编排代码会很快变得混乱。

Modular RAG 通常把它们封装成统一的 Tool(工具):

text 复制代码
工具名称:get_order_status
输入:orderId
输出:订单状态、物流节点、预计送达时间

工具名称:search_policy
输入:query、productId、region
输出:相关政策证据及来源

工具名称:query_refund_metrics
输入:product、region、dateRange
输出:退款数量、订单数量、退款率

Tool Calling(工具调用)指的是模型或工作流使用结构化参数选择并调用这些工具。

模型可以负责:

  • 从自然语言中提取参数;
  • 从允许列表中选择工具;
  • 根据工具结果组织回答。

真正的数据访问仍由后端执行,并受到参数校验、身份认证和权限控制。

下面定义一个统一的工具接口:

ts 复制代码
interface RetrievalTool<Input, Output> {
  // 工具名称用于路由和调用,必须来自系统允许列表
  name: string;

  // 工具说明告诉路由器它适合解决什么问题
  description: string;

  // 输入校验阻止缺失参数或危险参数进入数据源
  validate(input: unknown): Input;

  // 执行函数访问真实数据源,并返回结构化结果
  execute(input: Input, context: UserContext): Promise<Output>;
}

// 文档检索、订单接口和统计查询都遵循同一种工具约定
const tools = {
  document_search: documentSearchTool,
  order_api: orderStatusTool,
  analytics_sql: refundMetricsTool,
  support_ticket_search: supportTicketSearchTool,
  monitoring_search: monitoringSearchTool,
};

统一接口带来三个好处:

  1. Router 不需要知道底层使用向量库、Elasticsearch 还是网络接口;
  2. 每个工具可以独立测试、替换和设置权限;
  3. 工作流可以用一致方式记录输入、输出、耗时和错误。

第四步:单路检索还是并行检索?

路由结果不一定只有一个数据源。

简单问题通常只需要一条路径:

text 复制代码
"订单什么时候到?"
→ 订单 API

复杂问题可能需要多个来源:

text 复制代码
"AX-2048-C 最近频繁出现 E1007,是产品故障吗?"
→ 产品手册:E1007 的官方定义
→ 售后工单:最近相关故障数量
→ 监控数据:故障是否集中在某个固件版本

如果几个查询互不依赖,可以使用 Parallel Retrieval(并行检索),同时执行它们。

text 复制代码
串行查询:
手册 300ms + 工单 500ms + 监控 400ms ≈ 1200ms

并行查询:
max(300ms, 500ms, 400ms) ≈ 500ms

实际耗时还包含网络和工作流开销,但并行可以避免把互不依赖的等待时间简单相加。

ts 复制代码
async function retrieveFromRoutes(
  decision: RouteDecision,
  context: UserContext,
) {
  // 将路由结果转换为受控的工具调用,不允许执行未知工具
  const calls = decision.routes.map((route) =>
    buildValidatedToolCall(route, decision.parameters),
  );

  // 多个互不依赖的数据源并行查询,减少整体等待时间
  const settledResults = await Promise.allSettled(
    calls.map((call) =>
      tools[call.name].execute(call.input, context),
    ),
  );

  // 分别记录成功结果和失败信息,避免单个来源失败拖垮全部查询
  return collectToolResults(settledResults);
}

并行不是越多越好。

如果第二个查询依赖第一个查询返回的产品 ID,就必须串行执行;如果一个来源已经足以回答,也没有必要继续调用其他来源。

第五步:Result Fusion 合并不同来源

并行查询结束后,系统得到的结果可能完全不同:

text 复制代码
产品手册:
一段包含错误码解释的文本

售后工单:
37 条相关工单及其时间、产品批次

监控数据:
固件版本 v3.2.1 的故障率为 4.8%

Result Fusion(结果融合)负责把这些结果整理成大模型可以共同使用的证据。

它比上一篇的 RRF(Reciprocal Rank Fusion,倒数排名融合)范围更广。RRF 主要融合多路文本检索排名;Result Fusion 还要处理数据库记录、实时状态和统计结果等不同类型的数据。

它通常包含四步:

  1. 统一结果格式;
  2. 去除重复内容;
  3. 保留来源、时间和权限信息;
  4. 根据问题选择相关证据。

可以先定义统一的 Evidence(证据)结构:

ts 复制代码
interface Evidence {
  // 统一后的证据正文,可以来自文档、数据库记录或接口结果
  content: string;

  // 来源类型帮助系统区分手册、工单、数据库和业务接口
  sourceType: "document" | "database" | "api";

  // 来源标识用于展示引用和回查原始数据
  sourceId: string;

  // 数据时间用于判断实时状态和历史文档是否冲突
  updatedAt: string;

  // 相关性只在同一种可比较的评分体系内使用
  relevance?: number;
}

然后把各类结果转换为统一证据:

ts 复制代码
function fuseResults(
  toolResults: ToolResult[],
): Evidence[] {
  // 将文档片段、SQL 结果和 API 响应转换为统一证据结构
  const normalized = toolResults.flatMap(normalizeToEvidence);

  // 根据来源 ID 和内容指纹删除重复结果
  const uniqueEvidence = deduplicateEvidence(normalized);

  // 保留更新时间较新、来源明确且当前用户有权访问的证据
  const validEvidence = uniqueEvidence.filter(
    (item) => isFresh(item) && isAllowed(item),
  );

  // 根据当前问题选择最有用的证据,并保留原始来源
  return selectRelevantEvidence(validEvidence);
}

这里不能简单地把所有原始分数相加。

BM25 分数、向量相似度、数据库统计值和 API 状态不是同一种量。结果融合更重要的是统一语义和来源,而不是强行制造一个总分。

第六步:用 State 和 Workflow 管理整个过程

当系统只有一条检索链路时,普通函数调用已经足够:

text 复制代码
retrieve() → rerank() → generate()

加入路由、并行和条件分支后,系统需要知道:

  • 当前识别出了什么意图;
  • 已经调用过哪些工具;
  • 哪些工具成功或失败;
  • 当前收集了哪些证据;
  • 下一步应该进入哪个节点;
  • 是否应该停止。

State(状态)用于保存一次请求在不同步骤之间共享的数据:

ts 复制代码
interface WorkflowState {
  // 保存原始问题,避免多次改写后丢失用户真实意图
  question: string;

  // 保存用户身份与权限范围,供每个工具执行前检查
  user: UserContext;

  // 保存路由结果和需要传给工具的结构化参数
  route?: RouteDecision;

  // 保存不同工具返回并统一后的证据
  evidence: Evidence[];

  // 保存工具错误,供降级、重试或最终回答使用
  errors: ToolError[];

  // 保存当前节点,帮助工作流决定下一步
  currentNode: string;
}

Workflow Orchestration(工作流编排)负责定义节点以及节点之间的连接:

text 复制代码
节点:分析问题、执行路由、查询文档、调用 API、查询 SQL、融合证据、生成答案

连接:
分析问题 → 执行路由
执行路由 → 一个或多个数据源
数据源 → 融合证据
融合证据 → 生成答案

LangGraph(用于构建有状态工作流的框架)这类工具,可以用 State、Node(节点)和 Edge(边)表达这种流程。

不过,文章关注的是模块化思想,而不是某个框架的 API 写法。无论使用什么工具,核心都是:

每个节点只完成一项职责,状态显式传递,分支条件清晰可检查。

把 Modular RAG 串成完整流程

现在把前面的模块组合起来:

ts 复制代码
async function answerWithModularRAG(
  question: string,
  user: UserContext,
) {
  // 初始化工作流状态,保存原始问题、用户身份和空证据集合
  const state: WorkflowState = {
    question,
    user,
    evidence: [],
    errors: [],
    currentNode: "route_question",
  };

  // 根据问题选择文档、订单、统计或多数据源路径
  state.route = await routeQuestion(state.question);

  // 执行一个或多个允许的工具,并收集成功结果与失败信息
  const toolResults = await retrieveFromRoutes(
    state.route,
    state.user,
  );

  // 把不同数据源转换为统一证据,去重并保留来源
  state.evidence = fuseResults(toolResults.successes);
  state.errors = toolResults.errors;

  // 没有足够证据时进入受控的补充信息或降级路径
  if (!hasEnoughEvidence(state.evidence)) {
    return handleInsufficientEvidence(state);
  }

  // 根据原始问题和统一证据生成带来源的回答
  return generateGroundedAnswer(
    state.question,
    state.evidence,
  );
}

完整工作流如下:

四类问题现在会怎样执行?

回到文章开头的四个问题:

退货政策

text 复制代码
"定制版键盘支持七天无理由退货吗?"
→ Router 识别为政策问题
→ 检索政策文档
→ 返回规则和质量问题例外

订单物流

text 复制代码
"我的订单 20260729001 什么时候到?"
→ Router 提取订单号
→ 调用订单 API
→ 返回实时物流节点

经营统计

text 复制代码
"上个月华东地区的键盘退款率是多少?"
→ Router 提取时间、地区和产品条件
→ 调用受控 SQL 工具
→ 根据订单数和退款数计算结果

这里不应该让大模型直接生成任意 SQL,更不能让它获得超出当前用户权限的数据访问能力。

复杂故障

text 复制代码
"AX-2048-C 最近频繁出现 E1007,是产品故障吗?"
→ 并行查询产品手册、售后工单和监控数据
→ 将错误码定义、工单数量和故障率统一为证据
→ 综合多个来源生成结论

同一个 RAG 系统由此具备了处理不同问题的能力,而不是把所有数据强行塞进一个向量数据库。

Modular RAG 解决了什么?

1. 不同问题可以选择不同数据源

文档问题查知识库,实时状态调 API,统计问题查数据库,复杂问题组合多个来源。

2. 不同问题可以选择不同处理步骤

简单问题走短路径,复杂问题才执行多查询、并行检索和结果融合,避免所有请求都承担相同成本。

3. 模块可以独立维护和复用

路由器、文档检索器、订单工具和结果融合器职责分开,可以独立测试和替换。

4. RAG 从线性管线变成工作流

系统开始拥有分支、并行、状态和受控的工具调用,为后续检查证据和纠正错误打下基础。

Modular RAG 还留下了什么问题?

模块化解决了路径选择问题,却没有自动保证证据正确。

例如:

text 复制代码
Router 正确选择了政策知识库
        ↓
检索结果却是已经失效的旧政策
        ↓
系统仍然根据错误证据生成答案

还可能出现:

  • Router 把问题分到了错误路径;
  • 工具返回了与问题无关的结果;
  • 多个数据源之间存在冲突;
  • 证据数量很多,但仍不足以支持结论;
  • 工具调用失败后,系统不知道应该重试、改写还是拒答;
  • 动态分支增加了调试和评测难度。

Modular RAG 能决定"去哪里查",但还缺少一个关键能力:

检查查到的证据是否相关、充分、可信,并在不满足条件时主动纠正。

下一篇:Corrective RAG 与 Self-RAG(纠错式与自反思式 RAG)

下一篇将介绍 Corrective RAG 和 Self-RAG。

系统会在检索和生成过程中加入检查:

text 复制代码
检索证据
  ↓
判断是否相关、是否充分
  ↓
  ├── 证据足够 → 生成答案
  ├── 证据不足 → 改写问题并重新检索
  ├── 数据源不合适 → 切换其他来源
  └── 无法获得证据 → 明确拒绝回答

Modular RAG 让系统学会了选择流程。

Corrective RAG 与 Self-RAG 要解决的是:

选完流程、拿到证据之后,系统怎样发现自己可能错了,并采取纠正措施?

相关推荐
IT_陈寒15 小时前
Vite静态资源路径这个坑差点让我加班到凌晨
前端·人工智能·后端
颜酱15 小时前
15 | 安全执行 SQL 并返回查询结果
人工智能
神经蛙199615 小时前
🌍 别再硬编码中文了!Python Web 项目国际化(i18n)完全指南
后端·python
新芒15 小时前
海尔洗衣机智慧洗护:AI赋能洗烘护全面进化
人工智能
二月龙15 小时前
Spring 事务失效的 8 种场景,很多老手依然频繁踩雷
后端
掘金酱15 小时前
「TRAE Work 实战帮」征文启动!你沉淀的经验,值得被看见!
前端·人工智能·后端
橙子家15 小时前
Windows 上同时安装多个 node 版本
前端
颜酱15 小时前
14 | 验证并修正 LLM 生成的 SQL
人工智能·python
AI创界者15 小时前
AIGC进阶】Sulphur-2 视频生成大模型离线实战:文生视频/图生视频本地一键部署整合包解压即用与调优指南
人工智能·aigc·音视频
长大198815 小时前
MyBatis 常见性能陷阱:N+1 查询、一级缓存踩坑解决方案
后端