Routing Between Many Agent Tools

AI AgentsLLMArchitectureBackend
Share on LinkedIn

Tool selection accuracy falls off a cliff around tool fifteen. I've watched an agent with thirty tools call delete_user when the user asked for account settings — not because the model is careless, but because thirty tool descriptions in context creates a needle-in-a-haystack problem that gets worse with every tool you add. The fix isn't better prompting; it's a routing layer that presents the model with five relevant tools instead of fifty. Tool routing is the scaling mechanism nobody builds until their agent starts calling the wrong API.

The two-stage pattern

User query → Router (50 tools → 5-8 candidates) → LLM (picks 1-2 tools) → Execute

Stage 1 is fast and cheap. Stage 2 is the model doing what it's good at — choosing among a small, relevant set.

Tool groups with intent routing

Organize tools into groups and classify intent first:

TOOL_GROUPS = {
    "account": ["get_profile", "update_profile", "change_password", "delete_account"],
    "billing": ["get_invoices", "create_refund", "update_payment_method"],
    "orders": ["list_orders", "get_order", "cancel_order", "track_shipment"],
    "support": ["create_ticket", "search_kb", "escalate"],
}

async def route_tools(user_message: str) -> list[str]:
    intent = await classify_intent(user_message)  # cheap model call
    primary = TOOL_GROUPS.get(intent, [])
    # Always include search as fallback
    return primary + ["search_kb"]

The classifier is a 50-token call to a small model: "Classify this message: account, billing, orders, or support." It costs fractions of a cent and runs in 100ms.

Embedding-based routing for dynamic catalogs

When tools change frequently or groups aren't obvious:

async def route_by_embedding(query: str, top_k: int = 8) -> list[ToolDef]:
    query_embedding = await embed(query)
    scores = [
        (tool, cosine_similarity(query_embedding, tool.embedding))
        for tool in all_tools
    ]
    return [tool for tool, score in sorted(scores, key=lambda x: -x[1])[:top_k]]

Pre-compute tool description embeddings offline. At runtime, embed the query and return top-K. Include tool name, description, and one example use case in the embedded text for better matching.

Tool description quality matters more than count

Before adding routing, fix your tool descriptions. The routing layer can't compensate for:

# Bad — model can't distinguish tools
{"name": "process", "description": "Process a request"}

# Good — specific, disambiguated
{"name": "create_refund", "description": "Issue a refund for a completed order. Requires order_id and amount. Use only after verifying eligibility with get_order."}

Add negative scope to descriptions: "Use for X, NOT for Y." This helps both the router and the model.

Dynamic tool loading per workflow phase

Combine routing with graph workflows — different phases expose different tools:

Phase Available tools
Intake classify_intent, search_kb, get_profile
Research search_kb, fetch_document, query_database
Action create_refund, send_email, update_record
Review get_audit_log, summarize

The model never sees action tools during intake. This is routing by architecture, not by algorithm, and it's the most reliable approach.

Sub-agents as extreme routing

When tool groups have different security boundaries, route to sub-agents instead of tool groups:

Each sub-agent's routing problem is trivial because its catalog is already narrow.

Measuring routing quality

Track these metrics:

Low recall means your router is too aggressive — widen top-K or fix group boundaries. Low selection accuracy with high recall means descriptions or prompts need work, not routing.

Log routing decisions in your agent traces for debugging misfires.

Embedding-based tool retrieval

When tool catalogs exceed 20–30 entries, flat routing groups aren't enough. Embed tool descriptions and retrieve top-K by query similarity:

async def route_tools(query: str, k: int = 5) -> list[Tool]:
    query_vec = embed(query)
    candidates = vector_index.search(query_vec, top_k=k * 2)
    return [t for t in candidates if t.risk_tier <= session.max_risk_tier][:k]

Combine embedding retrieval with hard filters:

Embedding recall without filters causes dangerous tool exposure — a "delete" tool retrieved because the user said "remove from my cart."

Fallback and escalation paths

Routing should degrade gracefully:

Situation Behavior
No tool above confidence threshold Ask clarifying question
Single low-confidence match Confirm before executing write tools
Router timeout Fall back to safe read-only tool set
Tool execution failure Re-route with error context, max 2 retries

Never loop indefinitely re-routing the same failed tool. Escalate to human after N failures on write operations.

Production checklist

Pair with tool use error recovery when routed tools fail mid-execution.

Production checklist

Most production routing incidents trace to stale tool descriptions, not embedding model quality — schedule a monthly catalog review with the teams that own each tool.

Resources

Frequently asked questions

How many tools can an LLM agent handle reliably?

Most models degrade in tool selection accuracy beyond 10–15 simultaneously available tools. Beyond that, use a routing layer that pre-selects a relevant subset (5–8 tools) based on intent classification or embedding similarity, then presents only those to the model for final selection.

What is tool routing vs tool selection?

Tool routing is a pre-filtering step that narrows 50 tools down to 5–8 candidates based on the user's intent. Tool selection is the model's choice among those candidates. Routing is deterministic or classifier-based; selection is LLM-based. Splitting the problem improves accuracy on both.

Should tool routing use embeddings or a classifier?

Use a lightweight intent classifier for well-defined domains with stable tool sets — it's faster and cheaper. Use embedding similarity between the user query and tool descriptions when tools are frequently added or the domain is open-ended. Hybrid approaches work best: classifier for coarse routing, embeddings for fine selection within a group.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →