Agentic Routing: Dynamic Context Selection in Complex RAG Pipelines
By Bishwambhar SenIn simple Retrieval-Augmented Generation (RAG) applications, every query is routed through a single vector database. While this works for homogeneous datasets, it fails when applications must interact with diverse data sources.
For example, a user asking for "Q3 sales figures" requires a SQL query over a database. A user asking for "our company's policy on remote work" requires a semantic search over a vector database. A user asking for "the weather in San Francisco today" requires a web search tool.
Agentic Routing solves this by using LLM-based logic to dynamically determine the best destination or tool for each query.
Agentic Query Routing Architecture
The Architecture of Agentic Routing
An agentic router behaves as a traffic controller:
- It receives a query.
- It evaluates the query against metadata descriptions of available routes.
- It selects the optimal route (or set of routes).
- It calls the target system, aggregates the result, and returns it to the generator.
The routing decision can be formulated as a classification function f(q):
f(q) = c \in \{C_1, C_2, \dots, C_k\}
where each C_i represents a distinct retrieval channel. This is typically achieved using LLM function calling or structured JSON outputs.
Python Implementation of an Agentic Router
Here is an implementation of a dynamic router using Pydantic and OpenAI's structured outputs:
from pydantic import BaseModel, Field
from typing import Literal
import openai
# Define the routing target schema
class RouteChoice(BaseModel):
target: Literal["vector_db", "sql_db", "web_search", "direct_response"] = Field(
description="The best data source to resolve the user query."
)
rationale: str = Field(description="Brief explanation for the choice.")
def route_query(query: str) -> RouteChoice:
client = openai.OpenAI()
system_prompt = "You are a routing agent for a query processing system."
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": query}
],
response_format=RouteChoice,
temperature=0.0
)
return response.choices[0].message.parsed
# Test cases
test_queries = [
"What is the average transaction size in our sales table?",
"What is our company policy on remote work?",
"What is the weather in San Francisco today?",
"Thanks, that was helpful!"
]
for q in test_queries:
choice = route_query(q)
print(f"Query: {q}")
print(f" Route: {choice.target} ({choice.rationale})")
Implementing Fallback Routing
In production systems, routing can also operate in a multi-hop or fallback configuration:
- If a vector search retrieves zero-score results, the router can automatically fallback to a web search or request user clarification.
- If a SQL query fails due to syntax errors, the router can feed the error back to the LLM to rewrite the query.
Conclusion
The router is a single point of failure, and that is the thing most implementations underestimate. Every query passes through one classification decision, and when that decision is wrong the downstream system does not degrade gracefully — it confidently answers from the wrong source. A query about last quarter's revenue routed to the vector store returns a policy document, and the generator will happily write an answer grounded in it. Silent wrong answers are considerably worse than an error message.
Two design consequences follow. First, a route the LLM cannot classify should be allowed to say so: add an explicit unclear option to the schema and route it to a clarifying question rather than forcing a guess among four bad options. Second, log every routing decision with its rationale. Routing accuracy is measurable — label a couple hundred real queries and check them — and it is the number to watch, because a router at 85% means roughly one in seven users gets an answer built on the wrong data.
It is also worth asking whether you need a router at all. With two or three sources and a modest query volume, querying all of them in parallel and letting the re-ranker sort the merged results is often faster than an LLM classification hop, and it has no misrouting failure mode. Routing starts paying for itself when the sources are genuinely expensive to hit — a slow SQL warehouse, a metered web search API — or when there are enough of them that fan-out is not viable. Below that threshold you are adding a fallible classifier to save a cost you were not really paying.