Skip to content

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:

agent.py
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 response

But 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:

Routing Architecture
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ User │ → │ Router │ → │ Specialized │
│ Request │ │ Classifier │ │ Agent │
└─────────────┘ └─────────────┘ └─────────────┘
├─→ Story Agent
├─→ Joke Agent
├─→ Poem Agent
└─→ Fallback Agent

The 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:

schemas.py
from typing import Literal
from 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:

router.py
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from schemas import Route
# Use fast model for routing
router_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.step

When I run this:

Terminal window
$ 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"
joke

I get structured, predictable routing decisions.

How It Works

The complete workflow uses LangGraph’s StateGraph for orchestration:

workflow.py
from langgraph.graph import StateGraph, START, END
from typing import TypedDict
class State(TypedDict):
input: str
decision: str
output: str
# Define specialized agents
def 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 logic
def 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 workflow
builder = 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:

classifier.py
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:

parallel_router.py
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:

Terminal window
$ 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 implementation
Notion Agent: Found API docs section 3.2
Slack Agent: Found thread from 2025-12 about token refresh

Model Selection Strategy

I use different models for different purposes:

model_config.py
# Fast, cheap model for routing
router_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# Powerful model for execution
story_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 confusion
class 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 instructions
route_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 intents
def 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 routing
class 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 response

3. 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments