概览

router pattern 是一种 multi-agent 架构,其中路由步骤会对输入进行分类,并将其定向到专用智能体,再将结果合成为一个组合响应。当组织知识分布在不同 verticals 中时,这种模式尤其有效。这里的 verticals 指彼此独立的知识领域,每个领域都需要带有专用工具和提示词的智能体。 在本教程中,你将构建一个多源知识库 router,并通过一个贴近真实企业场景的示例展示这些优势。系统将协调三个专家:
  • 一个搜索代码、issues 和 pull requests 的 GitHub agent
  • 一个搜索内部文档和 wiki 的 Notion agent
  • 一个搜索相关线程和讨论的 Slack agent
当用户询问“如何对 API 请求进行身份验证?”时,router 会将查询拆解为面向不同来源的子问题,并行路由到相关智能体,然后将结果合成为一个连贯答案。

为什么使用 router?

router pattern 提供以下优势:
  • 并行执行:同时查询多个来源,与顺序方法相比可降低延迟。
  • 专用智能体:每个 vertical 都有针对其领域优化的专用工具和提示词。
  • 选择性路由:并非每个查询都需要每个来源,router 会智能选择相关 verticals。
  • 定向子问题:每个智能体都会收到针对其领域定制的问题,从而提升结果质量。
  • 清晰合成:来自多个来源的结果会被组合成单个连贯响应。

概念

本教程将介绍以下概念:
Router 与 Subagentssubagents pattern 也可以路由到多个智能体。当你需要专用预处理、自定义路由逻辑,或希望显式控制并行执行时,请使用 router pattern。当你希望由 LLM 动态决定调用哪些智能体时,请使用 subagents pattern。

设置

安装

本教程需要 langchainlanggraph 包:
npm install langchain @langchain/langgraph
有关更多详细信息,请参阅安装指南

LangSmith

设置 LangSmith,以检查智能体内部发生的情况。然后设置以下环境变量:
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."

选择 LLM

从 LangChain 的集成套件中选择一个聊天模型:
👉 Read the OpenAI chat model integration docs
npm install @langchain/openai
import { initChatModel } from "langchain";

process.env.OPENAI_API_KEY = "your-api-key";

const model = await initChatModel("gpt-5.4");

1. 定义状态

首先,定义状态 schema。本教程使用三种类型:
  • AgentInput:传递给每个 subagent 的简单状态,只包含查询
  • AgentOutput:每个 subagent 返回的结果,包含来源名称和结果
  • RouterState:主工作流状态,用于跟踪查询、分类、结果和最终答案
import { StateSchema, ReducedValue } from "@langchain/langgraph";
import { z } from "zod/v4";

const AgentOutput = z.object({
  source: z.string(),
  result: z.string(),
});

const RouterState = new StateSchema({
  query: z.string(),
  classifications: z.array(
    z.object({
      source: z.enum(["github", "notion", "slack"]),
      query: z.string(),
    })
  ),
  results: new ReducedValue(
    z.array(AgentOutput).default(() => []),
    { reducer: (current, update) => current.concat(update) }
  ),
  finalAnswer: z.string(),
});
results 字段使用 reducer(Python 中的 operator.add,JS 中的 concat 函数)将并行智能体执行的输出收集到单个列表中。

2. 为每个 vertical 定义工具

为每个知识领域创建工具。在生产系统中,这些工具会调用真实 API。在本教程中,使用返回模拟数据的 stub 实现。本教程跨 3 个 verticals 定义 7 个工具:GitHub(搜索代码、issues、PRs)、Notion(搜索文档、获取页面)和 Slack(搜索消息、获取线程)。
import { tool } from "langchain";
import { z } from "zod";

const searchCode = tool(
  async ({ query, repo }) => {
    return `在 ${repo || "main"} 中找到与 '${query}' 匹配的代码:src/auth.py 中的身份验证中间件`;
  },
  {
    name: "search_code",
    description: "在 GitHub 仓库中搜索代码。",
    schema: z.object({
      query: z.string(),
      repo: z.string().optional().default("main"),
    }),
  }
);

const searchIssues = tool(
  async ({ query }) => {
    return `找到 3 个与 '${query}' 匹配的 issues:#142(API 身份验证文档)、#89(OAuth 流程)、#203(token 刷新)`;
  },
  {
    name: "search_issues",
    description: "搜索 GitHub issues 和 pull requests。",
    schema: z.object({
      query: z.string(),
    }),
  }
);

const searchPrs = tool(
  async ({ query }) => {
    return `PR #156 添加了 JWT 身份验证,PR #178 更新了 OAuth scopes`;
  },
  {
    name: "search_prs",
    description: "搜索 pull requests 以查找实现细节。",
    schema: z.object({
      query: z.string(),
    }),
  }
);

const searchNotion = tool(
  async ({ query }) => {
    return `找到文档:'API Authentication Guide',涵盖 OAuth2 流程、API keys 和 JWT tokens`;
  },
  {
    name: "search_notion",
    description: "在 Notion workspace 中搜索文档。",
    schema: z.object({
      query: z.string(),
    }),
  }
);

const getPage = tool(
  async ({ pageId }) => {
    return `页面内容:逐步身份验证设置说明`;
  },
  {
    name: "get_page",
    description: "按 ID 获取特定 Notion 页面。",
    schema: z.object({
      pageId: z.string(),
    }),
  }
);

const searchSlack = tool(
  async ({ query }) => {
    return `在 #engineering 中找到讨论:'使用 Bearer tokens 进行 API 身份验证,刷新流程请参阅文档'`;
  },
  {
    name: "search_slack",
    description: "搜索 Slack 消息和线程。",
    schema: z.object({
      query: z.string(),
    }),
  }
);

const getThread = tool(
  async ({ threadId }) => {
    return `线程讨论了 API key 轮换的最佳实践`;
  },
  {
    name: "get_thread",
    description: "获取特定 Slack 线程。",
    schema: z.object({
      threadId: z.string(),
    }),
  }
);

3. 创建专用智能体

为每个 vertical 创建一个智能体。每个智能体都有领域专用工具,以及针对其知识来源优化的提示词。三个智能体都遵循相同模式,只有工具和系统提示词不同。
import { createAgent } from "langchain";
import { ChatOpenAI } from "@langchain/openai";

const llm = new ChatOpenAI({ model: "gpt-5.4" });

const githubAgent = createAgent({
  model: llm,
  tools: [searchCode, searchIssues, searchPrs],
  systemPrompt: `
你是 GitHub 专家。通过搜索仓库、issues 和 pull requests,
回答有关代码、API references 和实现细节的问题。
  `.trim(),
});

const notionAgent = createAgent({
  model: llm,
  tools: [searchNotion, getPage],
  systemPrompt: `
你是 Notion 专家。通过搜索组织的 Notion workspace,
回答有关内部流程、政策和团队文档的问题。
  `.trim(),
});

const slackAgent = createAgent({
  model: llm,
  tools: [searchSlack, getThread],
  systemPrompt: `
你是 Slack 专家。通过搜索团队成员分享知识和解决方案的
相关线程和讨论来回答问题。
  `.trim(),
});

4. 构建 router 工作流

现在使用 StateGraph 构建 router 工作流。该工作流有四个主要步骤:
  1. 分类:分析查询,并确定用哪些子问题调用哪些智能体
  2. 路由:使用 Send 并行 fan out 到选定智能体
  3. 查询智能体:每个智能体接收简单的 AgentInput,并返回 AgentOutput
  4. 合成:将收集到的结果组合成连贯响应
import { StateGraph, START, END, Send } from "@langchain/langgraph";
import { z } from "zod";

const routerLlm = new ChatOpenAI({ model: "gpt-5.4-mini" });


// 为分类器定义结构化输出 schema
const ClassificationResultSchema = z.object({
  classifications: z.array(z.object({
    source: z.enum(["github", "notion", "slack"]),
    query: z.string(),
  })).describe("要调用的智能体列表及其定向子问题"),
});


async function classifyQuery(state: typeof RouterState.State) {
  const structuredLlm = routerLlm.withStructuredOutput(ClassificationResultSchema);

  const result = await structuredLlm.invoke([
    {
      role: "system",
      content: `分析此查询,并确定要查询哪些知识库。
为每个相关来源生成针对该来源优化的定向子问题。

可用来源:
- github:代码、API references、实现细节、issues、pull requests
- notion:内部文档、流程、政策、团队 wiki
- slack:团队讨论、非正式知识分享、近期对话

仅返回与查询相关的来源。每个来源都应包含
针对该特定知识领域优化的定向子问题。

以“如何对 API 请求进行身份验证?”为例:
- github:"存在哪些身份验证代码?搜索 auth middleware 和 JWT handling"
- notion:"存在哪些身份验证文档?查找 API auth guides"
(省略 slack,因为它与这个技术问题无关)`
    },
    { role: "user", content: state.query }
  ]);

  return { classifications: result.classifications };
}


function routeToAgents(state: typeof RouterState.State): Send[] {
  return state.classifications.map(
    (c) => new Send(c.source, { query: c.query })  
  );
}


async function queryGithub(state: AgentInput) {
  const result = await githubAgent.invoke({
    messages: [{ role: "user", content: state.query }]  
  });
  return { results: [{ source: "github", result: result.messages.at(-1)?.content }] };
}


async function queryNotion(state: AgentInput) {
  const result = await notionAgent.invoke({
    messages: [{ role: "user", content: state.query }]  
  });
  return { results: [{ source: "notion", result: result.messages.at(-1)?.content }] };
}


async function querySlack(state: AgentInput) {
  const result = await slackAgent.invoke({
    messages: [{ role: "user", content: state.query }]  
  });
  return { results: [{ source: "slack", result: result.messages.at(-1)?.content }] };
}


async function synthesizeResults(state: typeof RouterState.State) {
  if (state.results.length === 0) {
    return { finalAnswer: "未从任何知识来源找到结果。" };
  }

  // 格式化结果以便合成
  const formatted = state.results.map(
    (r) => `**来自 ${r.source.charAt(0).toUpperCase() + r.source.slice(1)}:**\n${r.result}`
  );

  const synthesisResponse = await routerLlm.invoke([
    {
      role: "system",
      content: `合成这些搜索结果,以回答原始问题:"${state.query}"

- 合并来自多个来源的信息,避免冗余
- 突出最相关且可操作的信息
- 标注不同来源之间的差异
- 保持响应简洁且结构清晰`
    },
    { role: "user", content: formatted.join("\n\n") }
  ]);

  return { finalAnswer: synthesisResponse.content };
}

5. 编译工作流

现在通过用边连接节点来组装工作流。关键是将 add_conditional_edges 与路由函数一起使用,以启用并行执行:
const workflow = new StateGraph(RouterState)
  .addNode("classify", classifyQuery)
  .addNode("github", queryGithub)
  .addNode("notion", queryNotion)
  .addNode("slack", querySlack)
  .addNode("synthesize", synthesizeResults)
  .addEdge(START, "classify")
  .addConditionalEdges("classify", routeToAgents, ["github", "notion", "slack"])
  .addEdge("github", "synthesize")
  .addEdge("notion", "synthesize")
  .addEdge("slack", "synthesize")
  .addEdge("synthesize", END)
  .compile();
add_conditional_edges 调用通过 route_to_agents 函数将 classify 节点连接到 agent 节点。当 route_to_agents 返回多个 Send 对象时,这些节点会并行执行。

6. 使用 router

使用跨多个知识领域的查询来测试 router:
const result = await workflow.invoke({
  query: "如何对 API 请求进行身份验证?"
});

console.log("原始查询:", result.query);
console.log("\n分类:");
for (const c of result.classifications) {
  console.log(`  ${c.source}: ${c.query}`);
}
console.log("\n" + "=".repeat(60) + "\n");
console.log("最终答案:");
console.log(result.finalAnswer);
预期输出:
原始查询:如何对 API 请求进行身份验证?

分类:
  github: 存在哪些身份验证代码?搜索 auth middleware 和 JWT handling
  notion: 存在哪些身份验证文档?查找 API auth guides

============================================================

最终答案:
要对 API 请求进行身份验证,你有以下几种选择:

1. **JWT Tokens**:大多数使用场景的推荐方法。
   实现细节位于 `src/auth.py`(PR #156)。

2. **OAuth2 Flow**:对于第三方集成,请遵循 Notion 的
   'API Authentication Guide' 中记录的 OAuth2 流程。

3. **API Keys**:对于服务器到服务器通信,请在 Authorization
   header 中使用 Bearer tokens。

关于 token 刷新处理,请查看 issue #203 和 PR #178,
以了解最新 OAuth scope 更新。
router 分析查询并进行分类,以确定要调用哪些智能体。对于这个技术问题,它调用 GitHub 和 Notion,但不调用 Slack。随后,router 并行查询两个智能体,并将结果合成为连贯答案。

7. 理解架构

router 工作流遵循清晰模式:

分类阶段

classify_query 函数使用 structured output 分析用户查询,并确定要调用哪些智能体。路由智能就在这一阶段:
  • 使用 Pydantic model(Python)或 Zod schema(JS)确保输出有效
  • 返回 Classification 对象列表,每个对象都包含 source 和定向 query
  • 仅包含相关来源,不相关来源会被直接省略
这种结构化方法比自由形式 JSON 解析更可靠,也让路由逻辑更明确。

使用 send 并行执行

route_to_agents 函数将分类结果映射到 Send 对象。每个 Send 都指定目标节点和要传递的状态:
// 分类结果:[{ source: "github", query: "..." }, { source: "notion", query: "..." }]
// 变为:
[new Send("github", { query: "..." }), new Send("notion", { query: "..." })]
// 两个智能体同时执行,每个智能体只接收它需要的查询
每个 agent 节点都会收到一个简单的 AgentInput,只包含 query 字段,而不是完整 router 状态。这使接口保持清晰且明确。

使用 reducer 收集结果

智能体结果通过 reducer 流回主状态。每个智能体返回:
{ results: [{ source: "github", result: "..." }] }
reducer(Python 中的 operator.add)会拼接这些列表,将所有并行结果收集到 state["results"] 中。

合成阶段

所有智能体完成后,synthesize_results 函数会遍历收集到的结果:
  • 等待所有并行分支完成,LangGraph 会自动处理
  • 引用原始查询,确保答案回应用户提出的问题
  • 合并所有来源的信息,避免冗余
部分结果:在本教程中,所有选定智能体都必须完成后才会进行合成。

8. 完整可运行示例

下面是整合所有内容的可运行脚本:

9. 进阶:有状态 router

到目前为止构建的 router 是无状态的,每个请求都会独立处理,调用之间没有记忆。对于多轮对话,你需要一种有状态方法。

工具包装方法

添加对话记忆的最简单方法,是将无状态 router 包装为一个可由对话式智能体调用的工具:
import { MemorySaver } from "@langchain/langgraph";

const searchKnowledgeBase = tool(
  async ({ query }) => {
    const result = await workflow.invoke({ query });
    return result.finalAnswer;
  },
  {
    name: "search_knowledge_base",
    description: `跨多个知识来源(GitHub、Notion、Slack)搜索。
使用此工具查找有关代码、文档或团队讨论的信息。`,
    schema: z.object({
      query: z.string().describe("搜索查询"),
    }),
  }
);

const conversationalAgent = createAgent({
  model: llm,
  tools: [searchKnowledgeBase],
  systemPrompt: `
你是一个有帮助的助手,负责回答有关我们组织的问题。
使用 search_knowledge_base 工具,在我们的代码、文档和团队讨论中
查找信息。
  `.trim(),
  checkpointer: new MemorySaver(),
});
这种方法让 router 保持无状态,而由对话式智能体处理记忆和上下文。用户可以进行多轮对话,智能体会按需调用 router 工具。
const config = { configurable: { thread_id: "user-123" } };
let conversationalAgentResult = await conversationalAgent.invoke(
  {
    messages: [
      { role: "user", content: "如何对 API 请求进行身份验证?" },
    ],
  },
  config
);
console.log(conversationalAgentResult.messages.at(-1)?.content);

conversationalAgentResult = await conversationalAgent.invoke(
  {
    messages: [
      {
        role: "user",
        content: "这些 endpoints 的 rate limiting 怎么处理?",
      },
    ],
  },
  config
);
console.log(conversationalAgentResult.messages.at(-1)?.content);
对于大多数使用场景,推荐使用工具包装方法。它提供了清晰分离:router 处理多源查询,对话式智能体处理上下文和记忆。

完整持久化方法

如果你需要 router 本身维护状态,例如在路由决策中使用之前的搜索结果,请使用 persistence 在 router 层级存储消息历史。
有状态 router 会增加复杂性。 跨轮次路由到不同智能体时,如果智能体的语气或提示词不同,对话可能显得不一致。可以改用 handoffs patternsubagents pattern,二者都能为涉及不同智能体的多轮对话提供更清晰的语义。

10. 关键要点

当你具备以下需求时,router pattern 尤其适用:
  • 不同 verticals:彼此独立的知识领域,每个领域都需要专用工具和提示词
  • 并行查询需求:适合同时查询多个来源的问题
  • 合成需求:需要将多个来源的结果组合为连贯响应
该模式包含三个阶段:decompose(分析查询并生成定向子问题)、route(并行执行查询)和 synthesize(组合结果)。
何时使用 router pattern当你有多个独立知识来源、需要低延迟并行查询,并且希望显式控制路由逻辑时,请使用 router pattern。对于动态工具选择这类更简单的场景,请考虑 subagents pattern。对于智能体需要按顺序与用户对话的工作流,请考虑 handoffs

后续步骤