概述
这个 **路由器模式** is a multi-agent 架构,其中路由步骤对输入进行分类并将其定向到专门的智能体,结果被综合成组合响应。当您的组织知识分布在不同的 **垂直领域** (独立的知识领域,每个领域都需要自己的智能体,配备专门的工具和提示词)。
在本教程中,您将构建一个多源知识库路由器,通过真实的企业场景展示这些优势。该系统将协调三个专业智能体:
- - A **GitHub 智能体** 用于搜索代码、议题和拉取请求。
- - A **Notion 智能体** 用于搜索内部文档和维基。
- - A **Slack 智能体** 用于搜索相关话题和讨论。
当用户询问"如何对 API 请求进行身份验证?"时,路由器将查询分解为针对特定来源的子问题,并行地将它们路由到相关的智能体,然后将结果综合成一个连贯的答案。
graph LR
A([Query]) --> B[Classify]
B --> C[GitHub agent]
B --> D[Notion agent]
B --> E[Slack agent]
C --> F[Synthesize]
D --> F
E --> F
F --> G([Combined answer])
classDef trigger fill:#F6FFDB,stroke:#6E8900,stroke-width:2px,color:#2E3900
classDef process fill:#E5F4FF,stroke:#006DDD,stroke-width:2px,color:#030710
class A,G trigger
class B,C,D,E,F process
为什么使用路由器?
路由器模式提供了几个优势:
- 并行执行:同时查询多个数据源,与顺序方法相比减少延迟。
- 专业智能体:每个垂直领域都有专门针对其领域优化的工具和提示词。
- 选择性路由:并非每个查询都需要使用所有数据源——路由器智能地选择相关的垂直领域。
- 有针对性的子问题:每个智能体都会收到一个针对其领域量身定制的问题,从而提高结果质量。
- 简洁综合:来自多个数据源的结果被整合成一个单一、连贯的响应。
概念
我们将涵盖以下概念:
- - 多智能体系统
- - StateGraph 用于工作流编排
- - Send API 用于并行执行
设置
安装
本教程需要 langchain 和 langgraph packages:
pip install langchain langgraph
uv add langchain langgraph
conda install langchain langgraph -c conda-forge
更多详情,请参阅我们的 安装指南.
LangSmith
设置 LangSmith 检查代理内部发生了什么。然后设置以下环境变量:
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = getpass.getpass()
选择一个 LLM
从 LangChain 的集成套件中选择一个聊天模型:
1. 定义状态
首先,定义状态模式。我们使用三种类型:
- - **
AgentInput**:传递给每个子代理的简单状态(只是一个查询) - - **
AgentOutput**:每个子代理返回的结果(来源名称 + 结果) - - **
RouterState**:跟踪查询、分类、结果和最终答案的主工作流状态
from typing import Annotated, Literal, TypedDict
class AgentInput(TypedDict):
"""Simple input state for each subagent."""
query: str
class AgentOutput(TypedDict):
"""Output from each subagent."""
source: str
result: str
class Classification(TypedDict):
"""A single routing decision: which agent to call with what query."""
source: Literal["github", "notion", "slack"]
query: str
class RouterState(TypedDict):
query: str
classifications: list[Classification]
results: Annotated[list[AgentOutput], operator.add] # Reducer collects parallel results
final_answer: str
该 results 字段使用一个 **reducer** (operator.add (在 Python 中是 reducer,在 JS 中是 concat 函数)将并行代理执行的结果收集到单个列表中。
2. 为每个垂直领域定义工具
为每个知识领域创建工具。在生产系统中,这些工具会调用实际的 API。在本教程中,我们使用返回模拟数据的 stub 实现。我们定义了 3 个垂直领域的 7 个工具:GitHub(搜索代码、issues、PR)、Notion(搜索文档、获取页面)和 Slack(搜索消息、获取线程)。
from langchain.tools import tool
@tool
def search_code(query: str, repo: str = "main") -> str:
"""Search code in GitHub repositories."""
return f"Found code matching '{query}' in {repo}: authentication middleware in src/auth.py"
@tool
def search_issues(query: str) -> str:
"""Search GitHub issues and pull requests."""
return f"Found 3 issues matching '{query}': #142 (API auth docs), #89 (OAuth flow), #203 (token refresh)"
@tool
def search_prs(query: str) -> str:
"""Search pull requests for implementation details."""
return f"PR #156 added JWT authentication, PR #178 updated OAuth scopes"
@tool
def search_notion(query: str) -> str:
"""Search Notion workspace for documentation."""
return f"Found documentation: 'API Authentication Guide' - covers OAuth2 flow, API keys, and JWT tokens"
@tool
def get_page(page_id: str) -> str:
"""Get a specific Notion page by ID."""
return f"Page content: Step-by-step authentication setup instructions"
@tool
def search_slack(query: str) -> str:
"""Search Slack messages and threads."""
return f"Found discussion in #engineering: 'Use Bearer tokens for API auth, see docs for refresh flow'"
@tool
def get_thread(thread_id: str) -> str:
"""Get a specific Slack thread."""
return f"Thread discusses best practices for API key rotation"
3. 创建专业代理
为每个垂直领域创建一个代理。每个代理都有特定领域的工具和针对其知识源优化的提示。所有三个代理遵循相同的模式——只是工具和系统提示不同。
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-5.5")
github_agent = create_agent(
model,
tools=[search_code, search_issues, search_prs],
system_prompt=(
"You are a GitHub expert. Answer questions about code, "
"API references, and implementation details by searching "
"repositories, issues, and pull requests."
),
)
notion_agent = create_agent(
model,
tools=[search_notion, get_page],
system_prompt=(
"You are a Notion expert. Answer questions about internal "
"processes, policies, and team documentation by searching "
"the organization's Notion workspace."
),
)
slack_agent = create_agent(
model,
tools=[search_slack, get_thread],
system_prompt=(
"You are a Slack expert. Answer questions by searching "
"relevant threads and discussions where team members have "
"shared knowledge and solutions."
),
)
4. 构建路由器工作流
现在使用 StateGraph 构建路由器工作流。工作流有四个主要步骤:
- **分类**:分析查询并确定调用哪些代理以及使用哪些子问题
- **路由**:使用
Send - **查询代理**:每个代理接收一个简单的
AgentInput并返回一个AgentOutput - **综合**:将收集到的结果合并成一个连贯的回复
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")
# Define structured output schema for the classifier
class ClassificationResult(BaseModel): # [!code highlight]
"""Result of classifying a user query into agent-specific sub-questions."""
classifications: list[Classification] = Field(
description="List of agents to invoke with their targeted sub-questions"
)
def classify_query(state: RouterState) -> dict:
"""Classify query and determine which agents to invoke."""
structured_llm = router_llm.with_structured_output(ClassificationResult) # [!code highlight]
result = structured_llm.invoke([
{
"role": "system",
"content": """Analyze this query and determine which knowledge bases to consult.
For each relevant source, generate a targeted sub-question optimized for that source.
Available sources:
- github: Code, API references, implementation details, issues, pull requests
- notion: Internal documentation, processes, policies, team wikis
- slack: Team discussions, informal knowledge sharing, recent conversations
Return ONLY the sources that are relevant to the query. Each source should have
a targeted sub-question optimized for that specific knowledge domain.
Example for "How do I authenticate API requests?":
- github: "What authentication code exists? Search for auth middleware, JWT handling"
- notion: "What authentication documentation exists? Look for API auth guides"
(slack omitted because it's not relevant for this technical question)"""
},
{"role": "user", "content": state["query"]}
])
return {"classifications": result.classifications}
def route_to_agents(state: RouterState) -> list[Send]:
"""Fan out to agents based on classifications."""
return [
Send(c["source"], {"query": c["query"]}) # [!code highlight]
for c in state["classifications"]
]
def query_github(state: AgentInput) -> dict:
"""Query the GitHub agent."""
result = github_agent.invoke({
"messages": [{"role": "user", "content": state["query"]}] # [!code highlight]
})
return {"results": [{"source": "github", "result": result["messages"][-1].content}]}
def query_notion(state: AgentInput) -> dict:
"""Query the Notion agent."""
result = notion_agent.invoke({
"messages": [{"role": "user", "content": state["query"]}] # [!code highlight]
})
return {"results": [{"source": "notion", "result": result["messages"][-1].content}]}
def query_slack(state: AgentInput) -> dict:
"""Query the Slack agent."""
result = slack_agent.invoke({
"messages": [{"role": "user", "content": state["query"]}] # [!code highlight]
})
return {"results": [{"source": "slack", "result": result["messages"][-1].content}]}
def synthesize_results(state: RouterState) -> dict:
"""Combine results from all agents into a coherent answer."""
if not state["results"]:
return {"final_answer": "No results found from any knowledge source."}
# Format results for synthesis
formatted = [
f"**From {r['source'].title()}:**\n{r['result']}"
for r in state["results"]
]
synthesis_response = router_llm.invoke([
{
"role": "system",
"content": f"""Synthesize these search results to answer the original question: "{state['query']}"
- Combine information from multiple sources without redundancy
- Highlight the most relevant and actionable information
- Note any discrepancies between sources
- Keep the response concise and well-organized"""
},
{"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 函数连接分类节点到代理节点。当 route_to_agents 返回多个 Send 对象时,这些节点并行执行。
6. 使用路由器
使用跨多个知识领域的查询测试您的路由器:
result = workflow.invoke({
"query": "How do I authenticate API requests?"
})
print("Original query:", result["query"])
print("\nClassifications:")
for c in result["classifications"]:
print(f" {c['source']}: {c['query']}")
print("\n" + "=" * 60 + "\n")
print("Final Answer:")
print(result["final_answer"])
预期输出:
Original query: How do I authenticate API requests?
Classifications:
github: What authentication code exists? Search for auth middleware, JWT handling
notion: What authentication documentation exists? Look for API auth guides
============================================================
Final Answer:
To authenticate API requests, you have several options:
1. **JWT Tokens**: The recommended approach for most use cases.
Implementation details are in `src/auth.py` (PR #156).
2. **OAuth2 Flow**: For third-party integrations, follow the OAuth2
flow documented in Notion's 'API Authentication Guide'.
3. **API Keys**: For server-to-server communication, use Bearer tokens
in the Authorization header.
For token refresh handling, see issue #203 and PR #178 for the latest
OAuth scope updates.
路由器分析了查询,对其进行分类以确定调用哪些代理(对于这个技术问题,调用了 GitHub 和 Notion,但没有调用 Slack),并行查询了两个代理,并将结果综合成一个连贯的答案。
7. 理解架构
路由器工作流遵循一个清晰的模式:
分类阶段
该 classify_query 函数使用 **结构化输出** 分析用户查询并确定调用哪些代理。路由智能就位于此处:
- - 使用 Pydantic 模型(Python)或 Zod 模式(JS)来确保有效输出
- - 返回一个
Classification对象列表,每个对象包含source和目标query - - 只包含相关来源——不相关的会被直接省略
这种结构化方法比自由格式的 JSON 解析更可靠,并使路由逻辑清晰明确。
使用 send 进行并行执行
函数将分类映射到 route_to_agents 函数将分类映射到 Send 对象。每个 Send 指定目标节点和要传递的状态:
# Classifications: [{"source": "github", "query": "..."}, {"source": "notion", "query": "..."}]
# Becomes:
[Send("github", {"query": "..."}), Send("notion", {"query": "..."})]
# Both agents execute simultaneously, each receiving only the query it needs
每个代理节点接收一个简单的 AgentInput ,只包含一个 query 字段——而不是完整的路由状态。这保持了接口的简洁和明确。
使用 reducer 收集结果
代理结果通过 **reducer**流回主状态。每个代理返回:
{"results": [{"source": "github", "result": "..."}]}
Reducer(operator.add 在 Python 中)连接这些列表,将所有并行结果收集到 state["results"].
综合阶段
所有代理完成后, synthesize_results 函数迭代收集到的结果:
- - 等待所有并行分支完成(LangGraph 会自动处理)
- - 引用原始查询以确保答案针对用户提出的问题
- - 合并所有来源的信息而不冗余
8. 完整的可运行示例
以下是整合在一个可运行脚本中的全部内容:
View complete code
"""
Multi-Source Knowledge Router Example
This example demonstrates the router pattern for multi-agent systems.
A router classifies queries, routes them to specialized agents in parallel,
and synthesizes results into a combined response.
"""
from typing import Annotated, Literal, TypedDict
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
from pydantic import BaseModel, Field
# State definitions
class AgentInput(TypedDict):
"""Simple input state for each subagent."""
query: str
class AgentOutput(TypedDict):
"""Output from each subagent."""
source: str
result: str
class Classification(TypedDict):
"""A single routing decision: which agent to call with what query."""
source: Literal["github", "notion", "slack"]
query: str
class RouterState(TypedDict):
query: str
classifications: list[Classification]
results: Annotated[list[AgentOutput], operator.add]
final_answer: str
# Structured output schema for classifier
class ClassificationResult(BaseModel):
"""Result of classifying a user query into agent-specific sub-questions."""
classifications: list[Classification] = Field(
description="List of agents to invoke with their targeted sub-questions"
)
# Tools
@tool
def search_code(query: str, repo: str = "main") -> str:
"""Search code in GitHub repositories."""
return f"Found code matching '{query}' in {repo}: authentication middleware in src/auth.py"
@tool
def search_issues(query: str) -> str:
"""Search GitHub issues and pull requests."""
return f"Found 3 issues matching '{query}': #142 (API auth docs), #89 (OAuth flow), #203 (token refresh)"
@tool
def search_prs(query: str) -> str:
"""Search pull requests for implementation details."""
return f"PR #156 added JWT authentication, PR #178 updated OAuth scopes"
@tool
def search_notion(query: str) -> str:
"""Search Notion workspace for documentation."""
return f"Found documentation: 'API Authentication Guide' - covers OAuth2 flow, API keys, and JWT tokens"
@tool
def get_page(page_id: str) -> str:
"""Get a specific Notion page by ID."""
return f"Page content: Step-by-step authentication setup instructions"
@tool
def search_slack(query: str) -> str:
"""Search Slack messages and threads."""
return f"Found discussion in #engineering: 'Use Bearer tokens for API auth, see docs for refresh flow'"
@tool
def get_thread(thread_id: str) -> str:
"""Get a specific Slack thread."""
return f"Thread discusses best practices for API key rotation"
# Models and agents
model = init_chat_model("openai:gpt-5.5")
router_llm = init_chat_model("openai:gpt-5.4-mini")
github_agent = create_agent(
model,
tools=[search_code, search_issues, search_prs],
system_prompt=(
"You are a GitHub expert. Answer questions about code, "
"API references, and implementation details by searching "
"repositories, issues, and pull requests."
),
)
notion_agent = create_agent(
model,
tools=[search_notion, get_page],
system_prompt=(
"You are a Notion expert. Answer questions about internal "
"processes, policies, and team documentation by searching "
"the organization's Notion workspace."
),
)
slack_agent = create_agent(
model,
tools=[search_slack, get_thread],
system_prompt=(
"You are a Slack expert. Answer questions by searching "
"relevant threads and discussions where team members have "
"shared knowledge and solutions."
),
)
# Workflow nodes
def classify_query(state: RouterState) -> dict:
"""Classify query and determine which agents to invoke."""
structured_llm = router_llm.with_structured_output(ClassificationResult)
result = structured_llm.invoke([
{
"role": "system",
"content": """Analyze this query and determine which knowledge bases to consult.
For each relevant source, generate a targeted sub-question optimized for that source.
Available sources:
- github: Code, API references, implementation details, issues, pull requests
- notion: Internal documentation, processes, policies, team wikis
- slack: Team discussions, informal knowledge sharing, recent conversations
Return ONLY the sources that are relevant to the query."""
},
{"role": "user", "content": state["query"]}
])
return {"classifications": result.classifications}
def route_to_agents(state: RouterState) -> list[Send]:
"""Fan out to agents based on classifications."""
return [
Send(c["source"], {"query": c["query"]})
for c in state["classifications"]
]
def query_github(state: AgentInput) -> dict:
"""Query the 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:
"""Query the 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:
"""Query the 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:
"""Combine results from all agents into a coherent answer."""
if not state["results"]:
return {"final_answer": "No results found from any knowledge source."}
formatted = [
f"**From {r['source'].title()}:**\n{r['result']}"
for r in state["results"]
]
synthesis_response = router_llm.invoke([
{
"role": "system",
"content": f"""Synthesize these search results to answer the original question: "{state['query']}"
- Combine information from multiple sources without redundancy
- Highlight the most relevant and actionable information
- Note any discrepancies between sources
- Keep the response concise and well-organized"""
},
{"role": "user", "content": "\n\n".join(formatted)}
])
return {"final_answer": synthesis_response.content}
# Build workflow
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()
)
if __name__ == "__main__":
result = workflow.invoke({
"query": "How do I authenticate API requests?"
})
print("Original query:", result["query"])
print("\nClassifications:")
for c in result["classifications"]:
print(f" {c['source']}: {c['query']}")
print("\n" + "=" * 60 + "\n")
print("Final Answer:")
print(result["final_answer"])
9. 进阶:有状态路由
我们目前构建的路由是 **无状态** 的(每个请求独立处理,调用之间无记忆)。对于多轮对话,您需要一个 **有状态** approach.
工具封装方法
添加对话记忆最简单的方法是将无状态路由封装为一个工具,供对话代理调用:
from langgraph.checkpoint.memory import InMemorySaver
@tool
def search_knowledge_base(query: str) -> str:
"""Search across multiple knowledge sources (GitHub, Notion, Slack).
Use this to find information about code, documentation, or team discussions.
"""
result = workflow.invoke({"query": query})
return result["final_answer"]
conversational_agent = create_agent(
model,
tools=[search_knowledge_base],
system_prompt=(
"You are a helpful assistant that answers questions about our organization. "
"Use the search_knowledge_base tool to find information across our code, "
"documentation, and team discussions."
),
checkpointer=InMemorySaver(),
)
这种方法在保持路由无状态的同时,让对话代理处理记忆和上下文。用户可以进行多轮对话,代理会根据需要调用路由工具。
config = {"configurable": {"thread_id": "user-123"}}
result = conversational_agent.invoke(
{"messages": [{"role": "user", "content": "How do I authenticate API requests?"}]},
config
)
print(result["messages"][-1].content)
result = conversational_agent.invoke(
{"messages": [{"role": "user", "content": "What about rate limiting for those endpoints?"}]},
config
)
print(result["messages"][-1].content)
完整持久化方法
如果您需要路由本身维护状态——例如,在路由决策中使用之前的搜索结果——请使用 持久化 在路由级别存储消息历史。
10. 关键要点
路由模式在以下情况下表现出色:
- 不同的垂直领域:每个领域都需要专门工具和提示词的知识领域
- 并行查询需求:可从同时查询多个来源中受益的问题
- 综合需求:需要将多个来源的结果合并成连贯的响应
该模式有三个阶段: **分解** (分析查询并生成有针对性的子问题), **路由** (并行执行查询),以及 **综合** (合并结果)。