基于智能路由(Router)构建多源企业知识库 (Build a multi-source knowledge base with routing)
概述 (Overview)
路由模式(Router pattern)是一种典型的多智能体(Multi-agent)系统架构:由一个前置分类路由步骤对用户输入进行意图识别,将其精准分发给专用的业务智能体,最后将各路执行结果综合提炼为统一的回答。当企业知识分散在完全不同的垂直领域(Verticals)(各个知识域都需要拥有独立专用工具与提示词的专属智能体)时,该模式尤为适用。
在本教程中,你将构建一个多数据源企业知识库路由系统,协同调度三个垂直领域的专家智能体:
- GitHub 智能体:负责搜索代码仓库、Issue 议题与 Pull Request
- Notion 智能体:负责检索企业内部知识库文档与 Wiki
- 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为什么使用路由架构? (Why use a router?)
路由模式具备诸多独特的工程优势:
- 高并发并行执行 (Parallel execution):同时并发查询多个异构数据源,相比串行链式调用可成倍降低整体响应延迟。
- 领域专精智能体 (Specialized agents):每个垂直领域都拥有量身定制的专用工具和提示词优化。
- 按需选择性路由 (Selective routing):并非每个查询都需要打满所有数据源——路由器智能研判并仅激活相关领域。
- 针对性定制子问题 (Targeted sub-questions):每个智能体接收针对其垂直域改写后的精准子问题,大幅提升检索质量。
- 高品质融合输出 (Clean synthesis):将多方数据源的事实证据结构化合并为逻辑清晰的一体化答案。
Concepts
We will cover the following concepts:
- Multi-agent systems
- StateGraph for workflow orchestration
- Send API for parallel execution
TIP
Router vs. Subagents: The subagents pattern can also route to multiple agents. Use the router pattern when you need specialized preprocessing, custom routing logic, or want explicit control over parallel execution. Use the subagents pattern when you want the LLM to decide which agents to call dynamically.
Setup
Installation
This tutorial requires the langchain and langgraph packages:
npm install langchain @langchain/langgraphyarn add langchain @langchain/langgraphpnpm add langchain @langchain/langgraphFor more details, see our Installation guide.
LangSmith
Set up LangSmith to inspect what is happening inside your agent. Then set the following environment variables:
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."process.env.LANGSMITH_TRACING = "true";
process.env.LANGSMITH_API_KEY = "...";Select an LLM
Select a chat model from LangChain's suite of integrations:
OpenAI
👉 Read the OpenAI chat model integration docs
npm install @langchain/openaipnpm install @langchain/openaiyarn add @langchain/openaibun add @langchain/openaiimport { initChatModel } from "langchain";
process.env.OPENAI_API_KEY = "your-api-key";
const model = await initChatModel("gpt-5.5");import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({
model: "gpt-5.5",
apiKey: "your-api-key"
});Anthropic
👉 Read the Anthropic chat model integration docs
npm install @langchain/anthropicpnpm install @langchain/anthropicyarn add @langchain/anthropicpnpm add @langchain/anthropicimport { initChatModel } from "langchain";
process.env.ANTHROPIC_API_KEY = "your-api-key";
const model = await initChatModel("claude-sonnet-4-6");import { ChatAnthropic } from "@langchain/anthropic";
const model = new ChatAnthropic({
model: "claude-sonnet-4-6",
apiKey: "your-api-key"
});Azure
👉 Read the Azure chat model integration docs
npm install @langchain/azurepnpm install @langchain/azureyarn add @langchain/azurebun add @langchain/azureimport { initChatModel } from "langchain";
process.env.AZURE_OPENAI_API_KEY = "your-api-key";
process.env.AZURE_OPENAI_ENDPOINT = "your-endpoint";
process.env.OPENAI_API_VERSION = "your-api-version";
const model = await initChatModel("azure_openai:gpt-5.5");import { AzureChatOpenAI } from "@langchain/openai";
const model = new AzureChatOpenAI({
model: "gpt-5.5",
azureOpenAIApiKey: "your-api-key",
azureOpenAIApiEndpoint: "your-endpoint",
azureOpenAIApiVersion: "your-api-version"
});Google Gemini
👉 Read the ChatGoogle chat model integration docs
npm install @langchain/googlepnpm install @langchain/googleyarn add @langchain/googlebun add @langchain/googleimport { initChatModel } from "langchain";
process.env.GOOGLE_API_KEY = "your-api-key";
const model = await initChatModel("google:gemini-3.7-flash");import { ChatGoogle } from "@langchain/google/node";
const model = new ChatGoogle({
model: "gemini-3.7-flash",
apiKey: "your-api-key"
});Bedrock Converse
👉 Read the AWS Bedrock chat model integration docs
npm install @langchain/awspnpm install @langchain/awsyarn add @langchain/awsbun add @langchain/awsimport { initChatModel } from "langchain";
// Follow the steps here to configure your credentials:
// https://docs.aws.amazon.com/bedrock/latest/userguide/getting-started.html
const model = await initChatModel("bedrock:gpt-5.5");import { ChatBedrockConverse } from "@langchain/aws";
// Follow the steps here to configure your credentials:
// https://docs.aws.amazon.com/bedrock/latest/userguide/getting-started.html
const model = new ChatBedrockConverse({
model: "gpt-5.5",
region: "us-east-2"
});1. 定义工作流状态 (Define state)
First, define the state schemas. We use three types:
AgentInput: Simple state passed to each subagent (just a query)AgentOutput: Result returned by each subagent (source name + result)RouterState: Main workflow state tracking the query, classifications, results, and final answer
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(),
});The results field uses a reducer (operator.add in Python, a concat function in JS) to collect outputs from parallel agent executions into a single list.
2. 为各垂直领域定义专用工具 (Define tools for each vertical)
Create tools for each knowledge domain. In a production system, these would call actual APIs. For this tutorial, we use stub implementations that return mock data. We define 7 tools across 3 verticals: GitHub (search code, issues, PRs), Notion (search docs, get page), and Slack (search messages, get thread).
import { tool } from "langchain";
import { z } from "zod";
const searchCode = tool(
async ({ query, repo }) => {
return `Found code matching '${query}' in ${repo || "main"}: authentication middleware in src/auth.py`;
},
{
name: "search_code",
description: "Search code in GitHub repositories.",
schema: z.object({
query: z.string(),
repo: z.string().optional().default("main"),
}),
}
);
const searchIssues = tool(
async ({ query }) => {
return `Found 3 issues matching '${query}': #142 (API auth docs), #89 (OAuth flow), #203 (token refresh)`;
},
{
name: "search_issues",
description: "Search GitHub issues and pull requests.",
schema: z.object({
query: z.string(),
}),
}
);
const searchPrs = tool(
async ({ query }) => {
return `PR #156 added JWT authentication, PR #178 updated OAuth scopes`;
},
{
name: "search_prs",
description: "Search pull requests for implementation details.",
schema: z.object({
query: z.string(),
}),
}
);
const searchNotion = tool(
async ({ query }) => {
return `Found documentation: 'API Authentication Guide' - covers OAuth2 flow, API keys, and JWT tokens`;
},
{
name: "search_notion",
description: "Search Notion workspace for documentation.",
schema: z.object({
query: z.string(),
}),
}
);
const getPage = tool(
async ({ pageId }) => {
return `Page content: Step-by-step authentication setup instructions`;
},
{
name: "get_page",
description: "Get a specific Notion page by ID.",
schema: z.object({
pageId: z.string(),
}),
}
);
const searchSlack = tool(
async ({ query }) => {
return `Found discussion in #engineering: 'Use Bearer tokens for API auth, see docs for refresh flow'`;
},
{
name: "search_slack",
description: "Search Slack messages and threads.",
schema: z.object({
query: z.string(),
}),
}
);
const getThread = tool(
async ({ threadId }) => {
return `Thread discusses best practices for API key rotation`;
},
{
name: "get_thread",
description: "Get a specific Slack thread.",
schema: z.object({
threadId: z.string(),
}),
}
);3. 创建专用领域智能体 (Create specialized agents)
Create an agent for each vertical. Each agent has domain-specific tools and a prompt optimized for its knowledge source. All three follow the same pattern—only the tools and system prompt differ.
import { createAgent } from "langchain";
import { ChatOpenAI } from "@langchain/openai";
const llm = new ChatOpenAI({ model: "gpt-5.5" });
const githubAgent = createAgent({
model: llm,
tools: [searchCode, searchIssues, searchPrs],
systemPrompt: `
You are a GitHub expert. Answer questions about code,
API references, and implementation details by searching
repositories, issues, and pull requests.
`.trim(),
});
const notionAgent = createAgent({
model: llm,
tools: [searchNotion, getPage],
systemPrompt: `
You are a Notion expert. Answer questions about internal
processes, policies, and team documentation by searching
the organization's Notion workspace.
`.trim(),
});
const slackAgent = createAgent({
model: llm,
tools: [searchSlack, getThread],
systemPrompt: `
You are a Slack expert. Answer questions by searching
relevant threads and discussions where team members have
shared knowledge and solutions.
`.trim(),
});4. 构建路由图工作流 (Build the router workflow)
Now build the router workflow using a StateGraph. The workflow has four main steps:
- Classify: Analyze the query and determine which agents to invoke with what sub-questions
- Route: Fan out to selected agents in parallel using
Send - Query agents: Each agent receives a simple
AgentInputand returns anAgentOutput - Synthesize: Combine collected results into a coherent response
import { StateGraph, START, END, Send } from "@langchain/langgraph";
import { z } from "zod";
const routerLlm = new ChatOpenAI({ model: "gpt-5.4-mini" });
// Define structured output schema for the classifier
const ClassificationResultSchema = z.object({
classifications: z.array(z.object({
source: z.enum(["github", "notion", "slack"]),
query: z.string(),
})).describe("List of agents to invoke with their targeted sub-questions"),
});
async function classifyQuery(state: typeof RouterState.State) {
const structuredLlm = routerLlm.withStructuredOutput(ClassificationResultSchema);
const result = await structuredLlm.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 };
}
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: "No results found from any knowledge source." };
}
// Format results for synthesis
const formatted = state.results.map(
(r) => `**From ${r.source.charAt(0).toUpperCase() + r.source.slice(1)}:**\n${r.result}`
);
const synthesisResponse = await routerLlm.invoke([
{
role: "system",
content: `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: formatted.join("\n\n") }
]);
return { finalAnswer: synthesisResponse.content };
}5. 编译工作流 (Compile the workflow)
Now assemble the workflow by connecting nodes with edges. The key is using add_conditional_edges with the routing function to enable parallel execution:
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();The add_conditional_edges call connects the classify node to the agent nodes through the route_to_agents function. When route_to_agents returns multiple Send objects, those nodes execute in parallel.
6. 运行路由知识库 (Use the router)
Test your router with queries that span multiple knowledge domains:
const result = await workflow.invoke({
query: "How do I authenticate API requests?"
});
console.log("Original query:", result.query);
console.log("\nClassifications:");
for (const c of result.classifications) {
console.log(` ${c.source}: ${c.query}`);
}
console.log("\n" + "=".repeat(60) + "\n");
console.log("Final Answer:");
console.log(result.finalAnswer);Expected output:
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.The router analyzed the query, classified it to determine which agents to invoke (GitHub and Notion, but not Slack for this technical question), queried both agents in parallel, and synthesized the results into a coherent answer.
7. 深入剖析系统架构 (Understanding the architecture)
The router workflow follows a clear pattern:
意图分类阶段 (Classification phase)
The classify_query function uses structured output to analyze the user's query and determine which agents to invoke. This is where the routing intelligence lives:
- Uses a Pydantic model (Python) or Zod schema (JS) to ensure valid output
- Returns a list of
Classificationobjects, each with asourceand targetedquery - Only includes relevant sources—irrelevant ones are simply omitted
This structured approach is more reliable than free-form JSON parsing and makes the routing logic explicit.
使用 Send API 进行并行扇出 (Parallel execution with send)
The route_to_agents function maps classifications to Send objects. Each Send specifies the target node and the state to pass:
// Classifications: [{ source: "github", query: "..." }, { source: "notion", query: "..." }]
// Becomes:
[new Send("github", { query: "..." }), new Send("notion", { query: "..." })]
// Both agents execute simultaneously, each receiving only the query it needsEach agent node receives a simple AgentInput with just a query field—not the full router state. This keeps the interface clean and explicit.
使用 Reducer 收集归约结果 (Result collection with reducers)
Agent results flow back to the main state via a reducer. Each agent returns:
{ results: [{ source: "github", result: "..." }] }The reducer (operator.add in Python) concatenates these lists, collecting all parallel results into state["results"].
综合生成阶段 (Synthesis phase)
After all agents complete, the synthesize_results function iterates over the collected results:
- Waits for all parallel branches to complete (LangGraph handles this automatically)
- References the original query to ensure the answer addresses what the user asked
- Combines information from all sources without redundancy
NOTE
Partial results: In this tutorial, all selected agents must complete before synthesis.
8. 完整可运行代码示例 (Complete working example)
Here's everything together in a runnable script:
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.
*/
import { z } from "zod/v4";
import { tool } from "langchain";
import { StateGraph, START, END, Send, StateSchema, ReducedValue } from "@langchain/langgraph";
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(),
});
const searchCode = tool(
async ({ query, repo }) => {
return `Found code matching '${query}' in ${repo || "main"}: authentication middleware in src/auth.py`;
},
{
name: "search_code",
description: "Search code in GitHub repositories.",
schema: z.object({
query: z.string(),
repo: z.string().optional().default("main"),
}),
}
);
const searchIssues = tool(
async ({ query }) => {
return `Found 3 issues matching '${query}': #142 (API auth docs), #89 (OAuth flow), #203 (token refresh)`;
},
{
name: "search_issues",
description: "Search GitHub issues and pull requests.",
schema: z.object({
query: z.string(),
}),
}
);
const searchPrs = tool(
async ({ query }) => {
return `PR #156 added JWT authentication, PR #178 updated OAuth scopes`;
},
{
name: "search_prs",
description: "Search pull requests for implementation details.",
schema: z.object({
query: z.string(),
}),
}
);
const searchNotion = tool(
async ({ query }) => {
return `Found documentation: 'API Authentication Guide' - covers OAuth2 flow, API keys, and JWT tokens`;
},
{
name: "search_notion",
description: "Search Notion workspace for documentation.",
schema: z.object({
query: z.string(),
}),
}
);
const getPage = tool(
async ({ pageId }) => {
return `Page content: Step-by-step authentication setup instructions`;
},
{
name: "get_page",
description: "Get a specific Notion page by ID.",
schema: z.object({
pageId: z.string(),
}),
}
);
const searchSlack = tool(
async ({ query }) => {
return `Found discussion in #engineering: 'Use Bearer tokens for API auth, see docs for refresh flow'`;
},
{
name: "search_slack",
description: "Search Slack messages and threads.",
schema: z.object({
query: z.string(),
}),
}
);
const getThread = tool(
async ({ threadId }) => {
return `Thread discusses best practices for API key rotation`;
},
{
name: "get_thread",
description: "Get a specific Slack thread.",
schema: z.object({
threadId: z.string(),
}),
}
);
import { createAgent } from "langchain";
import { ChatOpenAI } from "@langchain/openai";
const llm = new ChatOpenAI({ model: "gpt-5.5" });
const githubAgent = createAgent({
model: llm,
tools: [searchCode, searchIssues, searchPrs],
systemPrompt: `
You are a GitHub expert. Answer questions about code,
API references, and implementation details by searching
repositories, issues, and pull requests.
`.trim(),
});
const notionAgent = createAgent({
model: llm,
tools: [searchNotion, getPage],
systemPrompt: `
You are a Notion expert. Answer questions about internal
processes, policies, and team documentation by searching
the organization's Notion workspace.
`.trim(),
});
const slackAgent = createAgent({
model: llm,
tools: [searchSlack, getThread],
systemPrompt: `
You are a Slack expert. Answer questions by searching
relevant threads and discussions where team members have
shared knowledge and solutions.
`.trim(),
});
const routerLlm = new ChatOpenAI({ model: "gpt-5.4-mini" });
// Define structured output schema for the classifier
const ClassificationResultSchema = z.object({
classifications: z
.array(
z.object({
source: z.enum(["github", "notion", "slack"]),
query: z.string(),
})
)
.describe("List of agents to invoke with their targeted sub-questions"),
});
async function classifyQuery(state: typeof RouterState.State) {
const structuredLlm = routerLlm.withStructuredOutput(
ClassificationResultSchema
);
const result = await structuredLlm.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 };
}
function routeToAgents(state: typeof RouterState.State): Send[] {
return state.classifications.map(
(c) => new Send(c.source, { query: c.query })
);
}
async function queryGithub(state: typeof RouterState.State) {
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: typeof RouterState.State) {
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: typeof RouterState.State) {
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: "No results found from any knowledge source." };
}
// Format results for synthesis
const formatted = state.results.map(
(r) =>
`**From ${r.source.charAt(0).toUpperCase() + r.source.slice(1)}:**\n${r.result}`
);
const synthesisResponse = await routerLlm.invoke([
{
role: "system",
content: `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: formatted.join("\n\n") },
]);
return { finalAnswer: synthesisResponse.content };
}
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();
const result = await workflow.invoke({
query: "How do I authenticate API requests?",
});
console.log("Original query:", result.query);
console.log("\nClassifications:");
for (const c of result.classifications) {
console.log(` ${c.source}: ${c.query}`);
}
console.log(`\n${"=".repeat(60)}\n`);
console.log("Final Answer:");
console.log(result.finalAnswer);9. 进阶:有状态路由器 (Advanced: Stateful routers)
The router we've built so far is stateless (each request is handled independently with no memory between calls). For multi-turn conversations, you need a stateful approach.
Tool wrapper approach
The simplest way to add conversation memory is to wrap the stateless router as a tool that a conversational agent can call:
import { MemorySaver } from "@langchain/langgraph";
const searchKnowledgeBase = tool(
async ({ query }) => {
const result = await workflow.invoke({ query });
return result.finalAnswer;
},
{
name: "search_knowledge_base",
description: `Search across multiple knowledge sources (GitHub, Notion, Slack).
Use this to find information about code, documentation, or team discussions.`,
schema: z.object({
query: z.string().describe("The search query"),
}),
}
);
const conversationalAgent = createAgent({
model: llm,
tools: [searchKnowledgeBase],
systemPrompt: `
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.
`.trim(),
checkpointer: new MemorySaver(),
});This approach keeps the router stateless while the conversational agent handles memory and context. The user can have a multi-turn conversation, and the agent will call the router tool as needed.
const config = { configurable: { thread_id: "user-123" } };
let conversationalAgentResult = await conversationalAgent.invoke(
{
messages: [
{ role: "user", content: "How do I authenticate API requests?" },
],
},
config
);
console.log(conversationalAgentResult.messages.at(-1)?.content);
conversationalAgentResult = await conversationalAgent.invoke(
{
messages: [
{
role: "user",
content: "What about rate limiting for those endpoints?",
},
],
},
config
);
console.log(conversationalAgentResult.messages.at(-1)?.content);TIP
The tool wrapper approach is recommended for most use cases. It provides clean separation: the router handles multi-source querying, while the conversational agent handles context and memory.
Full persistence approach
If you need the router itself to maintain state—for example, to use previous search results in routing decisions—use persistence to store message history at the router level.
WARNING
Stateful routers add complexity. When routing to different agents across turns, conversations may feel inconsistent if agents have different tones or prompts. Consider the handoffs pattern or subagents pattern instead—both provide clearer semantics for multi-turn conversations with different agents.
10. 核心总结 (Key takeaways)
The router pattern excels when you have:
- Distinct verticals: Separate knowledge domains that each require specialized tools and prompts
- Parallel query needs: Questions that benefit from querying multiple sources simultaneously
- Synthesis requirements: Results from multiple sources need to be combined into a coherent response
The pattern has three phases: decompose (analyze the query and generate targeted sub-questions), route (execute queries in parallel), and synthesize (combine results).
TIP
When to use the router pattern
Use the router pattern when you have multiple independent knowledge sources, need low-latency parallel queries, and want explicit control over routing logic.
For simpler cases with dynamic tool selection, consider the subagents pattern. For workflows where agents need to converse with users sequentially, consider handoffs.
后续进阶 (Next steps)
- Learn about handoffs for agent-to-agent conversations
- Explore the subagents pattern for centralized orchestration
- Read the multi-agent overview to compare different patterns
- Use LangSmith to debug and monitor your router
