概览

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 包:
pip install langchain langgraph
有关更多详细信息,请参阅安装指南

LangSmith

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

选择 LLM

从 LangChain 的集成套件中选择一个聊天模型:
👉 Read the OpenAI chat model integration docs
pip install -U "langchain[openai]"
import os
from langchain.chat_models import init_chat_model

os.environ["OPENAI_API_KEY"] = "sk-..."

model = init_chat_model("gpt-5.4")

1. 定义状态

首先,定义状态 schema。本教程使用三种类型:
  • AgentInput:传递给每个 subagent 的简单状态,只包含查询
  • AgentOutput:每个 subagent 返回的结果,包含来源名称和结果
  • RouterState:主工作流状态,用于跟踪查询、分类、结果和最终答案
from typing import Annotated, Literal, TypedDict
import operator


class AgentInput(TypedDict):
    """每个 subagent 的简单输入状态。"""
    query: str


class AgentOutput(TypedDict):
    """每个 subagent 的输出。"""
    source: str
    result: str


class Classification(TypedDict):
    """单个路由决策:使用哪个查询调用哪个智能体。"""
    source: Literal["github", "notion", "slack"]
    query: str


class RouterState(TypedDict):
    query: str
    classifications: list[Classification]
    results: Annotated[list[AgentOutput], operator.add]  # reducer 收集并行结果
    final_answer: str
results 字段使用 reducer(Python 中的 operator.add,JS 中的 concat 函数)将并行智能体执行的输出收集到单个列表中。

2. 为每个 vertical 定义工具

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


@tool
def search_code(query: str, repo: str = "main") -> str:
    """在 GitHub 仓库中搜索代码。"""
    return f"在 {repo} 中找到与 '{query}' 匹配的代码:src/auth.py 中的身份验证中间件"


@tool
def search_issues(query: str) -> str:
    """搜索 GitHub issues 和 pull requests。"""
    return f"找到 3 个与 '{query}' 匹配的 issues:#142(API 身份验证文档)、#89(OAuth 流程)、#203(token 刷新)"


@tool
def search_prs(query: str) -> str:
    """搜索 pull requests 以查找实现细节。"""
    return f"PR #156 添加了 JWT 身份验证,PR #178 更新了 OAuth scopes"


@tool
def search_notion(query: str) -> str:
    """在 Notion workspace 中搜索文档。"""
    return f"找到文档:'API Authentication Guide',涵盖 OAuth2 流程、API keys 和 JWT tokens"


@tool
def get_page(page_id: str) -> str:
    """按 ID 获取特定 Notion 页面。"""
    return f"页面内容:逐步身份验证设置说明"


@tool
def search_slack(query: str) -> str:
    """搜索 Slack 消息和线程。"""
    return f"在 #engineering 中找到讨论:'使用 Bearer tokens 进行 API 身份验证,刷新流程请参阅文档'"


@tool
def get_thread(thread_id: str) -> str:
    """获取特定 Slack 线程。"""
    return f"线程讨论了 API key 轮换的最佳实践"

3. 创建专用智能体

为每个 vertical 创建一个智能体。每个智能体都有领域专用工具,以及针对其知识来源优化的提示词。三个智能体都遵循相同模式,只有工具和系统提示词不同。
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model

model = init_chat_model("openai:gpt-5.4")

github_agent = create_agent(
    model,
    tools=[search_code, search_issues, search_prs],
    system_prompt=(
        "你是 GitHub 专家。通过搜索仓库、issues 和 pull requests,"
        "回答有关代码、API references 和实现细节的问题。"
    ),
)

notion_agent = create_agent(
    model,
    tools=[search_notion, get_page],
    system_prompt=(
        "你是 Notion 专家。通过搜索组织的 Notion workspace,"
        "回答有关内部流程、政策和团队文档的问题。"
    ),
)

slack_agent = create_agent(
    model,
    tools=[search_slack, get_thread],
    system_prompt=(
        "你是 Slack 专家。通过搜索团队成员分享知识和解决方案的"
        "相关线程和讨论来回答问题。"
    ),
)

4. 构建 router 工作流

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

router_llm = init_chat_model("openai:gpt-5.4-mini")


# 为分类器定义结构化输出 schema
class ClassificationResult(BaseModel):
    """将用户查询分类为智能体专用子问题的结果。"""
    classifications: list[Classification] = Field(
        description="要调用的智能体列表及其定向子问题"
    )


def classify_query(state: RouterState) -> dict:
    """对查询进行分类,并确定要调用哪些智能体。"""
    structured_llm = router_llm.with_structured_output(ClassificationResult)

    result = structured_llm.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}


def route_to_agents(state: RouterState) -> list[Send]:
    """根据分类结果 fan out 到智能体。"""
    return [
        Send(c["source"], {"query": c["query"]})
        for c in state["classifications"]
    ]


def query_github(state: AgentInput) -> dict:
    """查询 GitHub agent。"""
    result = github_agent.invoke({
        "messages": [{"role": "user", "content": state["query"]}]
    })
    return {"results": [{"source": "github", "result": result["messages"][-1].content}]}


def query_notion(state: AgentInput) -> dict:
    """查询 Notion agent。"""
    result = notion_agent.invoke({
        "messages": [{"role": "user", "content": state["query"]}]
    })
    return {"results": [{"source": "notion", "result": result["messages"][-1].content}]}


def query_slack(state: AgentInput) -> dict:
    """查询 Slack agent。"""
    result = slack_agent.invoke({
        "messages": [{"role": "user", "content": state["query"]}]
    })
    return {"results": [{"source": "slack", "result": result["messages"][-1].content}]}


def synthesize_results(state: RouterState) -> dict:
    """将所有智能体的结果组合成连贯答案。"""
    if not state["results"]:
        return {"final_answer": "未从任何知识来源找到结果。"}

    # 格式化结果以便合成
    formatted = [
        f"**来自 {r['source'].title()}:**\n{r['result']}"
        for r in state["results"]
    ]

    synthesis_response = router_llm.invoke([
        {
            "role": "system",
            "content": f"""合成这些搜索结果,以回答原始问题:"{state['query']}"

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

    return {"final_answer": synthesis_response.content}

5. 编译工作流

现在通过用边连接节点来组装工作流。关键是将 add_conditional_edges 与路由函数一起使用,以启用并行执行:
workflow = (
    StateGraph(RouterState)
    .add_node("classify", classify_query)
    .add_node("github", query_github)
    .add_node("notion", query_notion)
    .add_node("slack", query_slack)
    .add_node("synthesize", synthesize_results)
    .add_edge(START, "classify")
    .add_conditional_edges("classify", route_to_agents, ["github", "notion", "slack"])
    .add_edge("github", "synthesize")
    .add_edge("notion", "synthesize")
    .add_edge("slack", "synthesize")
    .add_edge("synthesize", END)
    .compile()
)
add_conditional_edges 调用通过 route_to_agents 函数将 classify 节点连接到 agent 节点。当 route_to_agents 返回多个 Send 对象时,这些节点会并行执行。

6. 使用 router

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

print("原始查询:", result["query"])
print("\n分类:")
for c in result["classifications"]:
    print(f"  {c['source']}: {c['query']}")
print("\n" + "=" * 60 + "\n")
print("最终答案:")
print(result["final_answer"])
预期输出:
原始查询:如何对 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": "..."}]
# 变为:
[Send("github", {"query": "..."}), 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 包装为一个可由对话式智能体调用的工具:
from langgraph.checkpoint.memory import InMemorySaver


@tool
def search_knowledge_base(query: str) -> str:
    """跨多个知识来源(GitHub、Notion、Slack)搜索。

    使用此工具查找有关代码、文档或团队讨论的信息。
    """
    result = workflow.invoke({"query": query})
    return result["final_answer"]


conversational_agent = create_agent(
    model,
    tools=[search_knowledge_base],
    system_prompt=(
        "你是一个有帮助的助手,负责回答有关我们组织的问题。"
        "使用 search_knowledge_base 工具,在我们的代码、文档和团队讨论中"
        "查找信息。"
    ),
    checkpointer=InMemorySaver(),
)
这种方法让 router 保持无状态,而由对话式智能体处理记忆和上下文。用户可以进行多轮对话,智能体会按需调用 router 工具。
config = {"configurable": {"thread_id": "user-123"}}

result = conversational_agent.invoke(
    {"messages": [{"role": "user", "content": "如何对 API 请求进行身份验证?"}]},
    config
)
print(result["messages"][-1].content)

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

完整持久化方法

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

10. 关键要点

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

后续步骤