Applications that search different knowledge sources need one entry point that chooses a retrieval path for each question. A LlamaIndex router query engine provides that entry point by selecting a query engine tool before forwarding the question.
Each QueryEngineTool pairs a query engine with metadata that describes its subject. The selector returns the chosen tool index and a reason, while RouterQueryEngine exposes that decision through the response's selector_result metadata.
The KeywordSelector class treats billing and refund separately from incident and outage. It rejects questions that match both term sets or neither set, so an unsupported question cannot silently fall through to a default route. An application can replace this deterministic selector with PydanticSingleSelector after its language model is configured.
Steps to build a LlamaIndex router query engine:
- Install the LlamaIndex core package in the active Python environment.
$ python3 -m pip install --upgrade llama-index-core
Related: How to install LlamaIndex with pip
- Create router_query_engine.py with the imports, mock model, and deterministic query engine class.
- router_query_engine.py
from llama_index.core import Settings from llama_index.core.base.base_selector import MultiSelection, SingleSelection from llama_index.core.llms import MockLLM from llama_index.core.query_engine import CustomQueryEngine, RouterQueryEngine from llama_index.core.selectors import BaseSelector from llama_index.core.tools import QueryEngineTool Settings.llm = MockLLM(max_tokens=32) class StaticQueryEngine(CustomQueryEngine): label: str answer: str def custom_query(self, query_str): return f"{self.label}: {self.answer}"
MockLLM prevents RouterQueryEngine from resolving a hosted default model during this credential-free routing check. The static engines isolate the routing decision from retrieval and generation behavior.
- Append the keyword selector with four explicit routing boundaries.
class KeywordSelector(BaseSelector): def _get_prompts(self): return {} def _update_prompts(self, prompts): return None def _select(self, choices, query): query_text = query.query_str.lower() billing_match = any( term in query_text for term in ("billing", "refund") ) incident_match = any( term in query_text for term in ("incident", "outage") ) if billing_match and incident_match: raise ValueError("Question matched both routes.") if not billing_match and not incident_match: raise ValueError("Question matched no route.") if billing_match: selection = SingleSelection( index=0, reason="Billing terms matched the billing tool metadata.", ) else: selection = SingleSelection( index=1, reason="Incident terms matched the operations tool metadata.", ) return MultiSelection(selections=[selection]) async def _aselect(self, choices, query): return self._select(choices, query)
Billing-only questions select index 0, incident-only questions select index 1, mixed questions raise an ambiguity error, and unrelated questions raise an unmatched error. The asynchronous method delegates to the same decision.
- Append the billing and operations query engine instances.
billing_engine = StaticQueryEngine( label="billing_policy", answer="Refund requests are reviewed before a credit is issued.", ) operations_engine = StaticQueryEngine( label="operations_playbook", answer="Priority incidents page the on-call engineer within 15 minutes.", )
- Wrap both query engines in QueryEngineTool objects with distinct descriptions.
tools = [ QueryEngineTool.from_defaults( query_engine=billing_engine, name="billing_policy", description="Answers billing policy and refund process questions.", ), QueryEngineTool.from_defaults( query_engine=operations_engine, name="operations_playbook", description="Answers incident response and outage questions.", ), ]
An LLM-backed selector evaluates these descriptions, so describe the questions each engine can answer rather than repeating only its name.
- Instantiate RouterQueryEngine with the selector and both query engine tools.
router = RouterQueryEngine(selector=KeywordSelector(), query_engine_tools=tools)
- Append fail-capable assertions for the billing-only and incident-only routes.
route_checks = [ ("Who reviews refund requests?", "billing_policy"), ("What happens during a priority incident?", "operations_playbook"), ] for question, expected_tool in route_checks: response = router.query(question) selector_result = response.metadata["selector_result"] selected_tool = tools[selector_result.ind].metadata.name assert selected_tool == expected_tool print(f"question={question}") print(f"selected_tool={selected_tool}") print(f"answer={response}")
Each assertion stops the program if a recognized question reaches the wrong query engine.
- Append fail-capable assertions for ambiguous and unmatched questions.
rejection_checks = [ ( "How does billing respond to an outage?", "Question matched both routes.", ), ("Where is the staff handbook?", "Question matched no route."), ] for question, expected_message in rejection_checks: try: router.query(question) except ValueError as error: assert str(error) == expected_message print(f"question={question}") print(f"route_error={error}") else: raise AssertionError(f"Expected route error: {expected_message}")
The else branch stops the program if either rejected question is accepted, while the message assertion distinguishes the ambiguous and unmatched boundaries.
- Run router_query_engine.py to verify all four routing boundaries.
$ python3 router_query_engine.py question=Who reviews refund requests? selected_tool=billing_policy answer=billing_policy: Refund requests are reviewed before a credit is issued. question=What happens during a priority incident? selected_tool=operations_playbook answer=operations_playbook: Priority incidents page the on-call engineer within 15 minutes. question=How does billing respond to an outage? route_error=Question matched both routes. question=Where is the staff handbook? route_error=Question matched no route.
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.