LlamaIndex RouterQueryEngine — selector picks wrong engine, metadata mismatch (2026) — Fix Guide (2026) | AI Error Hub
Home Providers LlamaIndex Router selector fails
LlamaIndex Retrieval · Router Severity: Medium HTTP n/a

LlamaIndex RouterQueryEngine — selector picks wrong engine or none

<code>RouterQueryEngine</code> uses an LLM or embeddings to route each query to the best sub-engine. When routing is wrong, users get the "no info" answer even though the right sub-engine holds the answer.

By Ahmed R. · Senior AI Infrastructure Engineer Published 2026-07-26 Verified 2026-07-26

Quick fix (TL;DR)

Resolution: RouterQueryEngine takes QueryEngineTools and a selector. The selector reads tool descriptions to decide where to route. Failure modes: (a) tool descriptions too similar; (b) LLM selector wrong on ambiguous queries; (c) embedding selector missing on out-of-distribution queries. Fix by (a) writing sharply differentiated tool descriptions, (b) picking LLMSingleSelector/PydanticSingleSelector based on model, and (c) allowing multi-selection when queries legitimately span engines.

Real error messages you'll see

These are the exact strings returned by the LlamaIndex framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.

Router picks wrong sub-engine
# Tools: [policy_engine, product_engine]
# Query: "What is the return window on ACME-5000?"
# Router picks product_engine (finds product info; misses return window)
# Should have picked policy_engine or multi-select both
Selector output not parseable
ValueError: Could not parse selector output. Expected JSON with "choice" and
"reason" keys. Got: "I think you should use the first tool because..."
Multi-select when should be single
# Query: "Refund policy?"
# Router picks BOTH engines; results duplicated and confusing

Reference

Selector types

SelectorHow it decidesCost
LLMSingleSelectorLLM reads tool descs, returns one choiceOne LLM call per query
LLMMultiSelectorLLM returns list of choicesOne LLM call
PydanticSingleSelectorLLM function-call outputOne LLM call (structured)
PydanticMultiSelectorFunction-call, multipleOne LLM call
EmbeddingSingleSelectorSimilarity between query and tool descsOne embedding call

Root causes, ranked by frequency

Based on developer reports across LlamaIndex forums, GitHub issues, and Discord community during 2025–2026.

  • 26%
    Tool descriptions too similar. Two engines described in nearly identical language.
  • 18%
    LLM selector wrong on ambiguous queries. Query touches multiple domains; single-select forced to pick one.
  • 14%
    Descriptions do not list examples of what to route. Selector has to guess based on abstract description.
  • 10%
    Wrong selector class for provider. LLMSingleSelector parser fails on non-JSON output.
  • 8%
    Not using multi-select when queries span engines. "Compare policy and product info" needs both.
  • 7%
    Embedding selector on out-of-distribution query. Query semantics do not match any tool description.
  • 7%
    Tool descriptions written for humans, not LLMs. Verbose prose instead of "when to use / when not to".
  • 10%
    No fallback when router fails. If no tool selected, the whole query fails instead of degrading.

Fixes — copy-paste solutions

Fix #1

Write task-specific tool descriptions with "when to use / when not to"

The single most impactful fix for router quality.

Structure every tool description as: what domain it covers, examples of queries it should handle, and explicit anti-triggers.

router_with_rich_descriptions.py
from llama_index.core.query_engine import RouterQueryEngine
from llama_index.core.selectors import PydanticSingleSelector, PydanticMultiSelector
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from llama_index.llms.openai import OpenAI

llm = OpenAI(model="gpt-4o-mini")

# Rich tool descriptions with explicit anti-triggers
policy_tool = QueryEngineTool(
    query_engine=policy_index.as_query_engine(similarity_top_k=6),
    metadata=ToolMetadata(
        name="company_policies",
        description=(
            "Company policies including refund policy, return policy, warranty, "
            "shipping terms, and terms of service.\n\n"
            "USE THIS FOR queries about:\n"
            "- Refund windows, return conditions, warranty periods\n"
            "- Shipping options, delivery timelines\n"
            "- Terms of service, privacy policy details\n\n"
            "DO NOT USE FOR:\n"
            "- Product specifications or features\n"
            "- Pricing of specific products\n"
            "- Availability inventory"
        ),
    ),
)

product_tool = QueryEngineTool(
    query_engine=product_index.as_query_engine(similarity_top_k=6),
    metadata=ToolMetadata(
        name="product_catalog",
        description=(
            "Product catalog including features, specifications, pricing, "
            "and inventory status.\n\n"
            "USE THIS FOR queries about:\n"
            "- Specific product features or technical specs\n"
            "- Product prices and availability\n"
            "- Product comparisons or recommendations\n\n"
            "DO NOT USE FOR:\n"
            "- Refund/return policies (use company_policies)\n"
            "- Order status (use orders_api)"
        ),
    ),
)

# --- Router with a single-select LLM selector ---
router_query_engine = RouterQueryEngine(
    selector=PydanticSingleSelector.from_defaults(llm=llm),
    query_engine_tools=[policy_tool, product_tool],
    verbose=True,   # prints selection reasoning
)

# --- Test various query types ---
for query in [
    "What is the refund window?",
    "What are the specs of the ACME-5000?",
    "What is the return window for the ACME-5000 specifically?",  # ambiguous
]:
    response = router_query_engine.query(query)
    print(f"\nQ: {query}")
    print(f"A: {response.response[:200]}")
The "USE THIS FOR / DO NOT USE FOR" pattern is dramatically more effective than free-form prose descriptions. LLMs read the structure well and route far more consistently.
Fix #2

Use PydanticMultiSelector for queries that span domains

When the answer legitimately needs both engines.

Multi-select allows the router to pick multiple tools. Combine with a response synthesizer that consolidates outputs across engines.

multi_select_router.py
from llama_index.core.query_engine import RouterQueryEngine
from llama_index.core.selectors import PydanticMultiSelector

router = RouterQueryEngine(
    selector=PydanticMultiSelector.from_defaults(llm=llm, max_outputs=2),
    query_engine_tools=[policy_tool, product_tool, orders_tool],
    verbose=True,
)

# Query that spans multiple engines
response = router.query(
    "What is the return window for the ACME-5000, and is it currently in stock?"
)
print(response)

# --- Router with LLM selector for models that do not do function calling ---
from llama_index.core.selectors import LLMSingleSelector, LLMMultiSelector

# LLMSingleSelector: outputs "the answer is: X" free-text; parser extracts the choice
# Use when your LLM does NOT support structured output well
router_llm = RouterQueryEngine(
    selector=LLMSingleSelector.from_defaults(llm=llm),
    query_engine_tools=[policy_tool, product_tool],
)

# --- Fallback strategy ---
# When all tools score low, fall back to a "generic" engine
generic_engine = general_index.as_query_engine()
generic_tool = QueryEngineTool(
    query_engine=generic_engine,
    metadata=ToolMetadata(
        name="general_knowledge",
        description="General knowledge fallback for queries that do not fit other tools.",
    ),
)

router_with_fallback = RouterQueryEngine(
    selector=PydanticSingleSelector.from_defaults(llm=llm),
    query_engine_tools=[policy_tool, product_tool, generic_tool],
    verbose=True,
)
The generic fallback tool is the antidote to router failures. When your router cannot confidently pick one of the specific tools, the fallback gives the user something instead of an error.
Fix #3

Log routing decisions and A/B test selector types

Router quality varies by LLM and by query distribution.

Track which tool the router picks per query, sample manually or with an eval, and swap selectors if one is consistently better on your query mix.

router_observability.py
from llama_index.core.query_engine import RouterQueryEngine
from llama_index.core.selectors import (
    PydanticSingleSelector,
    LLMSingleSelector,
    EmbeddingSingleSelector,
)
from llama_index.embeddings.openai import OpenAIEmbedding

# --- Build multiple routers with different selectors ---
tools = [policy_tool, product_tool, orders_tool]

routers = {
    "pydantic": RouterQueryEngine(
        selector=PydanticSingleSelector.from_defaults(llm=llm),
        query_engine_tools=tools,
    ),
    "llm": RouterQueryEngine(
        selector=LLMSingleSelector.from_defaults(llm=llm),
        query_engine_tools=tools,
    ),
    "embedding": RouterQueryEngine(
        selector=EmbeddingSingleSelector.from_defaults(
            embed_model=OpenAIEmbedding(model="text-embedding-3-small"),
        ),
        query_engine_tools=tools,
    ),
}

# --- Test set with expected routing ---
test_set = [
    ("What is the refund window?", "company_policies"),
    ("Show me the ACME-5000 features", "product_catalog"),
    ("Where is my order?", "orders_api"),
    # ...
]

for name, router in routers.items():
    correct = 0
    for query, expected_tool in test_set:
        response = router.query(query)
        # metadata.selector_result shows which was selected
        selected = getattr(response, "metadata", {}).get("selector_result", {}).get("selection", "?")
        # (the exact attribute name may vary; verify in your version)
        if selected == expected_tool:
            correct += 1
    print(f"{name} selector: {correct}/{len(test_set)} correct")

# Also log embedding selector cost: it does not call the LLM per query,
# which makes it cheaper — worth it if quality matches
For high-volume, latency-sensitive workloads, EmbeddingSingleSelector is much cheaper than LLM-based selectors. Test carefully — quality is sometimes worse than LLM-based but often acceptable.

Prevention checklist

Ship these seven safeguards once and this error stops appearing in your logs.

  • Write tool descriptions with explicit "USE FOR" and "DO NOT USE FOR" sections.
  • Include example queries in tool descriptions when queries are ambiguous.
  • Choose PydanticSingleSelector for OpenAI/Anthropic; LLMSingleSelector for others.
  • Use MultiSelector when queries legitimately need multiple tools.
  • Add a generic fallback tool so router "unable to route" does not fail the user.
  • Log routing decisions in production for evaluation.
  • A/B test selector types on your query distribution; embedding selector is cheaper.

Frequently asked questions

RouterQueryEngine for deterministic routing with fixed sub-engines. Agent for open-ended interactions where the model may need to combine results, retry, or use tools other than query engines.
With multi-select and default response synthesis, yes — it concatenates or summarizes across selected tools. For custom combining logic, use SubQuestionQueryEngine instead.
One LLM call per query for LLM/Pydantic selectors. One embedding call for embedding selector. Add this to your total per-query cost when budgeting.
Router quality degrades with many tools. Consider a two-level hierarchy: router picks a category, then a category-specific router picks the engine. Or migrate to a proper agent that can plan multi-step tool use.
Yes — pass prompt_template_str to the selector. Customize when you need domain-specific routing hints or a different output format.

Get the weekly AI-error digest

New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.