AI Agent Routing: A Practical Guide to Intent Classification and Routing Implementation
Purpose
This post demonstrates how to implement intent classification and routing for AI agent systems.
Environment
- Python 3.10+
- LangChain/LangGraph
- Pydantic for structured output
- OpenAI API (GPT-4o-mini for routing)
The Problem
When I built my first multi-purpose AI agent, I got this problem:
"Stop asking one prompt to do everything""Monolithic prompts lead to unreliable behavior""The AI tries to handle every request type and fails at all of them"Here’s what my initial setup looked like:
def single_agent(user_request): response = llm.invoke([ SystemMessage(content=""" You are a helpful assistant that can: - Write poems - Tell stories - Tell jokes - Answer questions - Process refunds - Handle complaints ... """), HumanMessage(content=user_request) ]) return responseBut when I tried to use this in production, I got these results:
- Context overload from massive prompts
- Inconsistent behavior across request types
- Difficult debugging when things went wrong
- High token costs from verbose system messages
- Poor performance on specialized tasks
The Solution
I built a routing system that separates intent classification from execution. The key insight: “The AI does not actually do the work. It simply chooses which function to run.”
Here’s the architecture:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ User │ → │ Router │ → │ Specialized ││ Request │ │ Classifier │ │ Agent │└─────────────┘ └─────────────┘ └─────────────┘ │ ├─→ Story Agent ├─→ Joke Agent ├─→ Poem Agent └─→ Fallback AgentThe router does these things:
- Classifies intent into structured categories
- Routes request to appropriate specialized agent
- Returns control flow to main orchestrator
- Handles unknown intents with fallback
Implementation
Here’s my intent classification schema:
from typing import Literalfrom pydantic import BaseModel, Field
class Route(BaseModel): """Structured output for routing decisions""" step: Literal["poem", "story", "joke"] = Field( None, description="The next step in the routing process" )Here’s the router implementation:
from langchain_openai import ChatOpenAIfrom langchain_core.messages import SystemMessage, HumanMessagefrom schemas import Route
# Use fast model for routingrouter_llm = ChatOpenAI(model="gpt-4o-mini")router = router_llm.with_structured_output(Route)
def classify_intent(user_request: str) -> str: """Classify user request into one of three categories"""
route_instructions = """Classify the user request into one of:- 'story': User wants a story- 'joke': User wants a joke- 'poem': User wants a poem
Return ONLY the category name, nothing else."""
decision = router.invoke([ SystemMessage(content=route_instructions), HumanMessage(content=user_request) ])
return decision.stepWhen I run this:
$ python router.py "Tell me a funny story about robots"story
$ python router.py "Write a haiku about debugging"poem
$ python router.py "Make me laugh with a programming joke"jokeI get structured, predictable routing decisions.
How It Works
The complete workflow uses LangGraph’s StateGraph for orchestration:
from langgraph.graph import StateGraph, START, ENDfrom typing import TypedDict
class State(TypedDict): input: str decision: str output: str
# Define specialized agentsdef story_agent(state: State) -> dict: response = story_llm.invoke(state["input"]) return {"output": response.content}
def joke_agent(state: State) -> dict: response = joke_llm.invoke(state["input"]) return {"output": response.content}
def poem_agent(state: State) -> dict: response = poem_llm.invoke(state["input"]) return {"output": response.content}
# Routing logicdef route_decision(state: State): if state["decision"] == "story": return "story_agent" elif state["decision"] == "joke": return "joke_agent" elif state["decision"] == "poem": return "poem_agent"
# Build workflowbuilder = StateGraph(State)builder.add_node("router", llm_call_router)builder.add_node("story_agent", story_agent)builder.add_node("joke_agent", joke_agent)builder.add_node("poem_agent", poem_agent)
builder.add_edge(START, "router")builder.add_conditional_edges("router", route_decision)
graph = builder.compile()The Intent Classifier Pattern
For more complex routing, I use a dedicated classifier node:
from langgraph.types import Command
async def intent_classifier(state: State) -> Command[Literal["refund_agent", "question_answering_agent"]]: """Classify intent and route to appropriate agent"""
route_instructions = """Classify the user request into one of:- 'refund': User wants to return or get refund for purchase- 'question_answering': User has general questions about products
Return ONLY the category name, nothing else."""
response = router_llm.invoke( [{"role": "system", "content": route_instructions}, *state["messages"]] )
# Return Command to control flow return Command(goto=response["intent"] + "_agent")This pattern gives me:
- Type-safe routing with Literal types
- Explicit flow control with Command objects
- Async support for production workloads
- Clear separation of routing logic
Advanced: Parallel Multi-Agent Routing
When a query needs multiple sources, I use parallel routing:
from langgraph.types import Send
class ClassificationResult(BaseModel): classifications: list[dict]
def classify_query(state: RouterState) -> dict: """Classify query into multiple sub-queries"""
structured_llm = router_llm.with_structured_output(ClassificationResult)
instructions = """Break down the user's question into sub-queries for:- 'github': Code examples and implementation details- 'notion': Documentation and guides- 'slack': Team discussions and context
Return structured classifications."""
result = structured_llm.invoke([ SystemMessage(content=instructions), HumanMessage(content=state["query"]) ])
return {"classifications": result.classifications}
def dispatch_to_agents(state: RouterState): """Dispatch to multiple agents in parallel"""
agent_map = { "github": "github_agent", "notion": "notion_agent", "slack": "slack_agent" }
return [ Send(agent_map[c["source"]], {"query": c["query"]}) for c in state["classifications"] ]When I test this:
$ python parallel_router.py "How do I authenticate API requests?"
Classifications:1. github: "authentication code examples python"2. notion: "API authentication documentation"3. slack: "authentication best practices discussion"
Executing parallel queries...GitHub Agent: Found auth.py with OAuth implementationNotion Agent: Found API docs section 3.2Slack Agent: Found thread from 2025-12 about token refreshModel Selection Strategy
I use different models for different purposes:
# Fast, cheap model for routingrouter_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# Powerful model for executionstory_llm = ChatOpenAI(model="gpt-4o", temperature=0.7)joke_llm = ChatOpenAI(model="gpt-4o", temperature=0.9)poem_llm = ChatOpenAI(model="gpt-4o", temperature=0.8)This approach:
- Reduces costs (mini models for classification)
- Improves quality (powerful models for execution)
- Increases speed (fast routing decisions)
- Enables optimization per task
Common Mistakes
I tried these approaches first:
Mistake 1: Too Many Intents
# BAD: 20+ intents leads to confusionclass Route(BaseModel): step: Literal[ "poem", "story", "joke", "haiku", "sonnet", "limerick", "ballad", "ode", "epic", "free_verse", # ... 15 more categories ]This failed because:
- Router couldn’t distinguish between similar intents
- Classification accuracy dropped below 60%
- Users confused about which intent to trigger
Mistake 2: Vague Routing Prompts
# BAD: Unclear instructionsroute_instructions = "Figure out what the user wants"This failed because:
- Router made inconsistent decisions
- Same query got different classifications
- No way to debug routing logic
Mistake 3: No Fallback Agent
# BAD: No handling for unknown intentsdef route_decision(state: State): if state["decision"] == "story": return "story_agent" # What if decision is something else?This failed because:
- System crashed on edge cases
- Poor user experience for unexpected inputs
- No graceful degradation
Best Practices
Here’s what works in production:
1. Use Structured Output
# GOOD: Pydantic models enforce valid routingclass Route(BaseModel): step: Literal["poem", "story", "joke"]
router = llm.with_structured_output(Route)2. Keep Router State Minimal
class State(TypedDict): input: str # User request decision: str # Routing decision output: str # Final response3. Add Fallback Agent
def route_decision(state: State): routing_map = { "story": "story_agent", "joke": "joke_agent", "poem": "poem_agent" } return routing_map.get(state["decision"], "fallback_agent")4. Test Routing Accuracy
test_cases = [ ("Write me a funny story", "story"), ("Tell a joke about programming", "joke"), ("Compose a haiku", "poem"),]
accuracy = sum( 1 for query, expected in test_cases if classify_intent(query) == expected) / len(test_cases)
print(f"Routing accuracy: {accuracy * 100}%")Why This Matters
The routing pattern transformed my AI agent architecture from monolithic chaos to modular reliability. Before implementing it, I spent hours debugging why a single prompt couldn’t handle diverse request types. Now I have:
- Clear separation of concerns
- Specialized optimization per task
- Easier testing and debugging
- Lower costs with model selection
- Better user experience
Intent classification and routing is fundamental to building production-ready multi-agent systems. The key insight is simplicity: a lightweight router classifies intent, conditional logic directs flow, and specialized agents handle execution.
Summary
In this post, I showed how to implement intent classification and routing for AI agent systems. The key point is that separating routing from execution - “the AI chooses which function to run” - enables systems that are easier to debug, test, and scale.
Start with 3-5 clear intent categories, use structured output for reliable routing decisions, and reserve powerful models for execution while using fast models for classification.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 LangChain
- 👨💻 LangGraph Documentation
- 👨💻 Semantic Router
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments