Agent routing is the “traffic command center” of a multi-Agent system—it decides which Agent should handle a user request. A well-designed routing system can significantly improve throughput, reduce latency, and increase accuracy. This post dives into the architecture design, strategy selection, and production practice of routing mechanisms in ADK Go.
Architectural Position of the Routing System
Position in the System
User Request
│
▼
┌───────────────┐
│ Router │ ← Routing layer (focus of this post)
│ (Dispatch Hub) │
└───────┬───────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Agent A │ │ Agent B │ │ Agent C │
│(Weather)│ │(Order) │ │(General)│
└─────────┘ └─────────┘ └─────────┘
Core responsibilities of the routing system:
- Intent recognition: understand the type of user request.
- Agent matching: find the most suitable Agent to handle it.
- Load balancing: avoid overloading a single point.
- Degradation and fault tolerance: switch quickly on failure.
- Cache optimization: avoid repeated routing decisions.
Routing Strategy System
1. Rule-Based Routing
Match based on predefined rules, with the highest determinism:
// Rule-based routing engine
type RuleRouter struct {
rules []RoutingRule
defaultAgent *agent.Agent
cache *RouteCache
metrics *RouterMetrics
}
type RoutingRule struct {
Name string
Priority int // higher number = higher priority
Matcher RuleMatcher // matcher
TargetAgent *agent.Agent // target Agent
Preprocessors []Preprocessor // preprocessor chain
Transformers []Transformer // input transformers
}
// Rule matcher interface
type RuleMatcher interface {
Match(input string, context *RoutingContext) (bool, float64)
}
// Keyword matcher
type KeywordMatcher struct {
Keywords []string
MatchMode MatchMode // ANY | ALL | EXACT
CaseSensitive bool
}
func (m *KeywordMatcher) Match(input string, context *RoutingContext) (bool, float64) {
inputLower := strings.ToLower(input)
matched := 0
for _, keyword := range m.Keywords {
kw := keyword
if !m.CaseSensitive {
kw = strings.ToLower(kw)
}
if strings.Contains(inputLower, kw) {
matched++
if m.MatchMode == MatchModeAny {
return true, 1.0
}
}
}
switch m.MatchMode {
case MatchModeAll:
return matched == len(m.Keywords), float64(matched) / float64(len(m.Keywords))
case MatchModeExact:
return matched == 1 && len(m.Keywords) == 1, float64(matched)
default:
return matched > 0, float64(matched) / float64(len(m.Keywords))
}
}
// Regex matcher
type RegexMatcher struct {
Patterns []*regexp.Regexp
Logic LogicMode // AND | OR
}
func (m *RegexMatcher) Match(input string, context *RoutingContext) (bool, float64) {
matched := 0
for _, pattern := range m.Patterns {
if pattern.MatchString(input) {
matched++
if m.Logic == LogicModeOR {
return true, 1.0
}
}
}
if m.Logic == LogicModeAND {
return matched == len(m.Patterns), float64(matched) / float64(len(m.Patterns))
}
return matched > 0, float64(matched) / float64(len(m.Patterns))
}
// Semantic matcher (based on embeddings)
type SemanticMatcher struct {
embeddings map[string][]float64 // semantic vectors of Agents
threshold float64 // similarity threshold
model EmbeddingModel // embedding model
}
func (m *SemanticMatcher) Match(input string, context *RoutingContext) (bool, float64) {
inputVec, err := m.model.Embed(input)
if err != nil {
return false, 0
}
bestScore := 0.0
for _, agentVec := range m.embeddings {
score := cosineSimilarity(inputVec, agentVec)
if score > bestScore {
bestScore = score
}
}
return bestScore >= m.threshold, bestScore
}
2. LLM-Based Routing
Use the LLM’s semantic understanding for intelligent routing:
type LLMRouter struct {
model agent.Model
agentRegistry map[string]*AgentDescriptor
promptTemplate string
cache *RouteCache
}
type AgentDescriptor struct {
Agent *agent.Agent
Name string
Description string
Capabilities []string
Examples []string
}
func (r *LLMRouter) Route(ctx context.Context, input string) (*agent.Agent, error) {
// 1. Check cache
if cached := r.cache.Get(input); cached != nil {
return cached.(*agent.Agent), nil
}
// 2. Build routing prompt
prompt := r.buildRoutingPrompt(input)
// 3. Call LLM for decision
response, err := r.model.GenerateContent(ctx, prompt)
if err != nil {
return nil, fmt.Errorf("llm routing failed: %w", err)
}
// 4. Parse decision result
decision, err := r.parseRoutingDecision(response)
if err != nil {
return nil, fmt.Errorf("parse routing decision: %w", err)
}
// 5. Get target Agent
target, ok := r.agentRegistry[decision.AgentName]
if !ok {
return nil, fmt.Errorf("unknown agent: %s", decision.AgentName)
}
// 6. Cache decision
r.cache.Set(input, target.Agent, 5*time.Minute)
return target.Agent, nil
}
func (r *LLMRouter) buildRoutingPrompt(input string) string {
var sb strings.Builder
sb.WriteString("You are an intelligent routing system. Choose the most suitable Agent to handle the user input.\n\n")
sb.WriteString("Available Agents:\n")
for name, desc := range r.agentRegistry {
sb.WriteString(fmt.Sprintf("\n[%s]\n", name))
sb.WriteString(fmt.Sprintf("Description: %s\n", desc.Description))
sb.WriteString(fmt.Sprintf("Capabilities: %s\n", strings.Join(desc.Capabilities, ", ")))
if len(desc.Examples) > 0 {
sb.WriteString(fmt.Sprintf("Examples: %s\n", strings.Join(desc.Examples, "; ")))
}
}
sb.WriteString(fmt.Sprintf("\nUser input: %s\n", input))
sb.WriteString("\nPlease output in JSON format: {\"agent\": \"AgentName\", \"confidence\": 0.95, \"reason\": \"reason for selection\"}")
return sb.String()
}
3. Hybrid Routing
Combine the speed of rule-based routing with the accuracy of LLM routing:
type HybridRouter struct {
ruleRouter *RuleRouter // fast path
llmRouter *LLMRouter // intelligent path
fallbackAgent *agent.Agent // fallback Agent
// Strategy configuration
ruleThreshold float64 // rule match confidence threshold
llmThreshold float64 // LLM routing confidence threshold
useLLMForUnknown bool // use LLM for unknown inputs
}
func (r *HybridRouter) Route(ctx context.Context, input string) (*agent.Agent, error) {
// Layer 1: rule-based routing (fast path)
ruleResult, confidence, err := r.ruleRouter.Match(input)
if err == nil && confidence >= r.ruleThreshold {
return ruleResult, nil
}
// Layer 2: LLM routing (intelligent path)
if r.useLLMForUnknown || confidence > 0 {
llmResult, err := r.llmRouter.Route(ctx, input)
if err == nil {
return llmResult, nil
}
}
// Fallback
if r.fallbackAgent != nil {
return r.fallbackAgent, nil
}
return nil, fmt.Errorf("no suitable agent found for input: %s", input)
}
Load-Aware Scheduling
Dynamic Load Balancing
type LoadAwareRouter struct {
agents []*LoadAwareAgent
strategy LoadBalanceStrategy
healthChecker *HealthChecker
}
type LoadAwareAgent struct {
Agent *agent.Agent
CurrentLoad float64 // current load (0-1)
AvgLatency time.Duration // average latency
ErrorRate float64 // error rate
LastChecked time.Time // last check time
Weight float64 // weight
}
type LoadBalanceStrategy interface {
Select(agents []*LoadAwareAgent, input string) *LoadAwareAgent
}
// Weighted round-robin
func (s *WeightedRoundRobin) Select(agents []*LoadAwareAgent, input string) *LoadAwareAgent {
var best *LoadAwareAgent
bestScore := -1.0
for _, a := range agents {
// Skip unhealthy or overloaded Agents
if a.ErrorRate > 0.5 || a.CurrentLoad > 0.9 {
continue
}
// Composite score: weight / (load + normalized latency + error rate)
latencyNorm := float64(a.AvgLatency.Milliseconds()) / 1000.0
score := a.Weight / (a.CurrentLoad + latencyNorm + a.ErrorRate + 0.1)
if score > bestScore {
bestScore = score
best = a
}
}
return best
}
// Consistent hashing (ensure same input routes to same Agent)
type ConsistentHashStrategy struct {
ring *consistent.Consistent
}
func (s *ConsistentHashStrategy) Select(agents []*LoadAwareAgent, input string) *LoadAwareAgent {
agentName, err := s.ring.Get(input)
if err != nil {
return nil
}
for _, a := range agents {
if a.Agent.Name() == agentName {
return a
}
}
return nil
}
// Least connections
func (s *LeastConnections) Select(agents []*LoadAwareAgent, input string) *LoadAwareAgent {
var best *LoadAwareAgent
minConnections := int(^uint(0) >> 1) // MaxInt
for _, a := range agents {
connections := a.Agent.ActiveConnections()
if connections < minConnections && a.CurrentLoad < 0.9 {
minConnections = connections
best = a
}
}
return best
}
Health Checks and Auto-Eviction
type HealthChecker struct {
checkInterval time.Duration
agents map[string]*AgentHealth
}
type AgentHealth struct {
State HealthState
LastCheck time.Time
SuccessCount int
FailureCount int
ConsecutiveFailures int
}
type HealthState int
const (
HealthStateHealthy HealthState = iota
HealthStateDegraded
HealthStateUnhealthy
)
func (hc *HealthChecker) Start(ctx context.Context) {
ticker := time.NewTicker(hc.checkInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
hc.checkAll()
}
}
}
func (hc *HealthChecker) checkAll() {
for id, health := range hc.agents {
// Perform health check
err := hc.performCheck(id)
if err != nil {
health.FailureCount++
health.ConsecutiveFailures++
if health.ConsecutiveFailures >= 3 {
health.State = HealthStateUnhealthy
} else if health.ConsecutiveFailures >= 1 {
health.State = HealthStateDegraded
}
} else {
health.SuccessCount++
health.ConsecutiveFailures = 0
health.State = HealthStateHealthy
}
health.LastCheck = time.Now()
}
}
Routing Cache and Optimization
Multi-Level Cache Architecture
type MultiLevelRouteCache struct {
l1 *LRUCache // L1: in-memory cache, O(1) access
l2 *RedisCache // L2: distributed cache, shared across instances
l3 *PersistentCache // L3: persistent cache, survives restart
}
func (c *MultiLevelRouteCache) Get(input string) (*agent.Agent, error) {
// L1 lookup
if agent := c.l1.Get(input); agent != nil {
return agent, nil
}
// L2 lookup
if agent := c.l2.Get(input); agent != nil {
c.l1.Set(input, agent, time.Minute) // backfill L1
return agent, nil
}
// L3 lookup
if agent := c.l3.Get(input); agent != nil {
c.l2.Set(input, agent, time.Hour) // backfill L2
c.l1.Set(input, agent, time.Minute) // backfill L1
return agent, nil
}
return nil, fmt.Errorf("cache miss")
}
func (c *MultiLevelRouteCache) Set(input string, agent *agent.Agent, ttl time.Duration) {
c.l1.Set(input, agent, ttl/10) // L1 caches 1/10 TTL
c.l2.Set(input, agent, ttl) // L2 caches full TTL
c.l3.Set(input, agent, ttl*24) // L3 caches 24x TTL
}
Cache Preheating
func (r *Router) PreheatCache(ctx context.Context, sampleInputs []string) error {
for _, input := range sampleInputs {
agent, err := r.Route(ctx, input)
if err != nil {
log.Printf("Preheating failed for '%s': %v", input, err)
continue
}
// Pre-cache result
r.cache.Set(input, agent, 24*time.Hour)
}
return nil
}
Hands-On Scenario: Multi-Function Intelligent Assistant
Full Implementation
package main
import (
"context"
"fmt"
"log"
"regexp"
"strings"
"time"
"github.com/google/adk-go/agent"
"github.com/google/adk-go/team"
"github.com/google/adk-go/tool"
)
// IntelligentAssistant multi-function intelligent assistant
type IntelligentAssistant struct {
router *team.HybridRouter
}
func NewIntelligentAssistant(model agent.Model) (*IntelligentAssistant, error) {
// 1. Create domain-specific Agents
// Weather Agent
weatherAgent, err := agent.New(agent.Config{
Name: "weather-expert",
Model: model,
Instruction: `You are a weather expert. Provide accurate weather forecasts, clothing advice, and travel reminders.`,
Tools: []tool.Tool{
tool.NewWeatherQueryTool(),
tool.NewAirQualityTool(),
},
Timeout: 10 * time.Second,
})
if err != nil {
return nil, err
}
// Order Agent
orderAgent, err := agent.New(agent.Config{
Name: "order-expert",
Model: model,
Instruction: `You are an order processing expert. Help users query, modify, and cancel orders.`,
Tools: []tool.Tool{
tool.NewOrderQueryTool(),
tool.NewOrderModifyTool(),
},
Timeout: 15 * time.Second,
})
if err != nil {
return nil, err
}
// Logistics Agent
logisticsAgent, err := agent.New(agent.Config{
Name: "logistics-expert",
Model: model,
Instruction: `You are a logistics query expert. Track packages and estimate delivery time.`,
Tools: []tool.Tool{
tool.NewPackageTrackingTool(),
},
Timeout: 10 * time.Second,
})
if err != nil {
return nil, err
}
// Writing Agent
writingAgent, err := agent.New(agent.Config{
Name: "writing-expert",
Model: model,
Instruction: `You are a writing assistant. Help users draft and polish various copy.`,
Timeout: 20 * time.Second,
})
if err != nil {
return nil, err
}
// General Agent
generalAgent, err := agent.New(agent.Config{
Name: "general-assistant",
Model: model,
Instruction: `You are a general assistant. Answer users' various questions in a friendly and professional manner.`,
Timeout: 15 * time.Second,
})
if err != nil {
return nil, err
}
// 2. Build rule-based router
ruleRouter := team.NewRuleRouter()
// Weather rule (high priority)
ruleRouter.AddRule(team.RoutingRule{
Name: "weather-rule",
Priority: 100,
Matcher: team.NewKeywordMatcher([]string{
"weather", "temperature", "rain", "snow", "air quality", "clothing", "umbrella",
}, team.MatchModeAny, false),
TargetAgent: weatherAgent,
})
// Order rule
ruleRouter.AddRule(team.RoutingRule{
Name: "order-rule",
Priority: 90,
Matcher: team.NewCompositeMatcher(team.LogicModeOR,
team.NewKeywordMatcher([]string{"order", "purchase", "payment", "refund", "cancel"}, team.MatchModeAny, false),
team.NewRegexMatcher([]*regexp.Regexp{
regexp.MustCompile(`order number[:]?\\s*\\d+`),
}, team.LogicModeOR),
),
TargetAgent: orderAgent,
})
// Logistics rule
ruleRouter.AddRule(team.RoutingRule{
Name: "logistics-rule",
Priority: 90,
Matcher: team.NewKeywordMatcher([]string{
"express", "logistics", "package", "shipped", "delivered", "where",
}, team.MatchModeAny, false),
TargetAgent: logisticsAgent,
})
// Writing rule
ruleRouter.AddRule(team.RoutingRule{
Name: "writing-rule",
Priority: 80,
Matcher: team.NewKeywordMatcher([]string{
"write", "create", "polish", "revise", "copy", "article", "email",
}, team.MatchModeAny, false),
TargetAgent: writingAgent,
})
// 3. Build LLM router (for fuzzy matching)
llmRouter := team.NewLLMRouter(model, map[string]*team.AgentDescriptor{
"weather-expert": {
Agent: weatherAgent,
Description: "Handle weather-related queries",
Capabilities: []string{"weather forecast", "air quality", "clothing advice"},
Examples: []string{"How is the weather today", "Do I need an umbrella tomorrow"},
},
"order-expert": {
Agent: orderAgent,
Description: "Handle order-related operations",
Capabilities: []string{"order query", "order modification", "refund processing"},
Examples: []string{"Check my order", "I want to cancel my order"},
},
// ... other Agents
})
// 4. Build hybrid router
hybridRouter := team.NewHybridRouter(
team.WithRuleRouter(ruleRouter),
team.WithLLMRouter(llmRouter),
team.WithFallbackAgent(generalAgent),
team.WithRuleThreshold(0.8),
team.WithLLMThreshold(0.7),
team.WithCache(team.NewMultiLevelRouteCache()),
)
return &IntelligentAssistant{router: hybridRouter}, nil
}
func (a *IntelligentAssistant) Handle(ctx context.Context, userInput string) (string, error) {
// Route to suitable Agent
selectedAgent, err := a.router.Route(ctx, userInput)
if err != nil {
return "", fmt.Errorf("routing failed: %w", err)
}
log.Printf("[Router] Input routed to %s", selectedAgent.Name())
// Execute Agent
result, err := selectedAgent.Run(ctx, userInput)
if err != nil {
return "", fmt.Errorf("agent execution failed: %w", err)
}
return result, nil
}
func main() {
ctx := context.Background()
assistant, err := NewIntelligentAssistant(model)
if err != nil {
log.Fatalf("Failed to create assistant: %v", err)
}
// Test different scenarios
testCases := []string{
"How is the weather in Beijing today",
"Check my order 12345",
"Where is my package",
"Help me write a resignation letter",
"What is quantum computing",
}
for _, input := range testCases {
response, err := assistant.Handle(ctx, input)
if err != nil {
log.Printf("Error: %v", err)
continue
}
fmt.Printf("\nUser: %s\nAssistant: %s\n", input, response)
}
}
Observability of Routing Decisions
Tracing and Auditing
type RoutingDecision struct {
RequestID string
Timestamp time.Time
Input string
InputHash string
SelectedAgent string
RouteType string // "rule" | "llm" | "fallback"
Confidence float64
LatencyMs int64
RulesChecked []string
CacheHit bool
}
func (r *Router) logDecision(decision *RoutingDecision) {
// Log to console
logData, _ := json.Marshal(decision)
log.Printf("[RoutingDecision] %s", logData)
// Record metrics
r.metrics.RoutingLatency.WithLabelValues(decision.RouteType).Observe(float64(decision.LatencyMs))
r.metrics.RoutingCounter.WithLabelValues(decision.SelectedAgent, decision.RouteType).Inc()
if decision.CacheHit {
r.metrics.CacheHitCounter.Inc()
} else {
r.metrics.CacheMissCounter.Inc()
}
}
Common Questions in Depth
Q: How do I choose between rule-based and LLM routing?
A: Decision matrix:
| Dimension | Rule-Based Routing | LLM Routing |
|---|---|---|
| Latency | <1ms | 500ms-2s |
| Accuracy | High (deterministic) | High (semantic) |
| Cost | Low | High (per LLM call) |
| Maintenance | Requires rule updates | Self-adapting |
| Suitable for | Clear classification | Fuzzy semantics |
Production recommendation: use a hybrid approach, with rule-based routing handling 80% of clear requests and LLM routing handling 20% of fuzzy requests.
Q: How long does the routing cache last?
A: Multi-level expiration strategy:
- L1 memory: 1-5 minutes, based on access frequency
- L2 Redis: 1 hour, based on time
- L3 persistent: 24 hours, based on LRU
For dynamic content (e.g., “weather today”), disable caching or use a very short TTL.
Q: How do I handle routing errors (wrong Agent selected)?
A: Three layers of protection:
- Confidence threshold: return the general Agent when confidence is low.
- User confirmation: ask the user for intent on fuzzy requests.
- Auto-correction: automatically try other Agents when an Agent fails.
func (r *Router) RouteWithFallback(ctx context.Context, input string) (*agent.Agent, error) {
agent, confidence, err := r.route(ctx, input)
if err != nil {
return r.fallbackAgent, nil
}
if confidence < 0.5 {
// Low confidence, may need user confirmation
return r.clarificationAgent, nil
}
return agent, nil
}
Next Steps
You now have a deep understanding of Agent routing dynamic dispatch and intelligent scheduling. Module 5 is complete; next, enter Module 6: Streaming—Streaming principles, event handling, and real-time communication.
← Custom Workflow | Streaming Principles →
Want to learn more Go ADK hands-on? Follow the “Mengshou Programming” channel for weekly updates on Go and AI programming.
