Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Enhancement-overlay SOP for query-type routing — sending a query to the right index / tool / engine *before* retrieving, not after. Activate when a calling agent owns a retrieval or answering surface that fronts more than one handler (a summary index, a vector index, a text-to-SQL engine, a tool) and the inbound queries differ in kind: "summarize this doc" vs "find the clause about X" vs "how many orders shipped in Q3". Encodes the one non- negotiable insight — **one retriever cannot serve all q
.claude/skills/agentsope-agentsop-query-routing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 113% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 160% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 311% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 276% | 0% |
| case-20 | ✓→✗ | ▼ Worse | 295% | 0% |
> Third-person operating model for a coder agent that owns a multi-handler > answering surface. Audience is the LLM agent writing/reviewing the routing > code — not the end user.
> One sentence: A retriever is shaped by the query type it was built for; > a summary index, a vector index, and a text-to-SQL engine are not > interchangeable — so classify the query and route first, then retrieve.
This is an ENHANCE overlay. It distills the cross-framework routing pattern from three source skills. For the per-framework API, cross-link the base skill: [[llamaindex]] (RouterQueryEngine), [[agentsop-dify]] (Question Classifier node), [[agentsop-langgraph]] (conditional edges).
Activate when any of the following holds:
and a query must be dispatched to exactly one (or a few) of them.
the contract say about termination"), summarize ("give me the gist of doc Y"), compute/aggregate ("how many tickets closed last week"), compare ("diff the 2024 vs 2025 policy").
VectorStoreIndex (or any single retriever) is being stretched to answerquery types it was not built for, and quality is uneven across the mix.
RouterQueryEngine,SelectorPromptTemplate, LLMSingleSelector, Dify Question Classifier, LangGraph add_conditional_edges, "intent classifier", "text-to-SQL or RAG".
to call.
Do not activate when:
failure mode for no benefit (see §6 anti-pattern A1).
is filtering, route to [[agentsop-multi-tenant-rag]].
a static edge / linear pipeline, not a router.
Three principles. Violating any of them produces a router that misroutes silently or routes when it should not.
The index taxonomy is not cosmetic. From [[llamaindex]]: a SummaryIndex is a "small, fan-out synthesis" primitive — it reads every node to digest a doc; a VectorStoreIndex is top-k semantic lookup — it reads the few most similar chunks; a text-to-SQL engine answers aggregate/compute queries that no chunk contains the answer to. Ask a vector index to "summarize the whole document" and it returns 4 arbitrary chunks; ask a summary index "what is the late-fee clause" and it fans out over the whole corpus wastefully. The query type names the correct primitive. Routing is the act of recovering that name at runtime.
> Operational corollary: index.as_query_engine() over a single > VectorStoreIndex answering a heterogeneous query mix is the symptom this > skill exists to fix. The fix is per-type handlers + a router on top.
Routing is a classification step that runs before any retrieval. It reads only the query (and optionally light context) and emits a destination, not an answer. This ordering is what bounds latency and cost: you pay for the router once, then exactly one downstream handler, instead of fanning out to all of them and merging. LlamaIndex's RouterQueryEngine, Dify's Question Classifier node feeding IF/ELSE branches, and LangGraph's conditional edge over state are the same shape — a selector function (query) -> handler_id evaluated up front. The three frameworks differ only in how the selector is implemented (§7).
Every router — LLM, embedding, or keyword — picks among destinations described in words or examples. In LlamaIndex the signal is the QueryEngineTool.description; in Dify it is the class label + instruction; in LangGraph it is whatever the routing function reads off state plus the node names. From [[llamaindex]] Dilemma 3: "invest in `QueryEngineTool.description` — it's the only signal the router/agent sees." A misroute is, four times out of five, a bad description, not a bad model. Fix the description before swapping the router type.
> The fourth case is genuinely ambiguous queries — for those, Principle of > Fallback (§3 Stage 4) applies: route to a safe default, never guess silently.
Five stages. Each gates the next. Stop and reconsider at the first "no".
Gate questions:
single index, no router (anti-pattern A1).
label them. If >90% are one kind → build that handler well; skip the router.
If all three are yes, continue.
Build the table before writing the router. One row per query kind:
| Query kind | Example | Correct handler | Primitive | |---|---|---|---| | Summarize / digest | "summarize the Q3 report" | summary engine | SummaryIndex | | Fact lookup | "what is the late-fee clause" | vector engine | VectorStoreIndex + filters | | Compute / aggregate | "how many orders shipped in Q3" | text-to-SQL engine | NL2SQL over the DB | | Compare / multi-hop | "diff 2024 vs 2025 policy" | decomposition engine | SubQuestionQueryEngine | | Out of scope | "what's the weather" | default / refuse | fallback handler |
This table is the spec for both the handlers and the router. Mapping mirrors [[llamaindex]] Stage 4 ("Compose for query heterogeneity").
Implement and test each engine independently against its own query kind before wiring the router. A misroute is undebuggable if the handlers themselves are wrong. Author each handler's description / label here (Principle 3) — the destination metadata is part of the handler, not the router.
Three router families, cheapest to most capable:
| Family | How it decides | Pick when | |---|---|---| | Keyword / rule | regex / substring / heuristic over the query | destinations are lexically distinct ("SELECT", "summarize", file extensions); latency-critical; cost-critical | | Embedding / semantic | embed query, nearest destination description | destinations semantically distinct but not lexically; no per-call LLM budget; deterministic-ish | | LLM / selector | LLM reads query + descriptions, returns choice (single or multi) | destinations need reasoning to disambiguate; multi-select needed; quality > latency |
Default ladder: start keyword if the types are lexically separable; else LLM selector; reach for embedding when you want a middle point (no LLM hop, better than keyword). Always emit a confidence / score, never just a label.
Every router must define behavior for the unrouteable query:
default handler (usually the broad vector index) or an explicit "I can't answer that" path. Never let an ambiguous query silently hit a random branch.
{query_hash, chosen_handler, score, fell_back}so misroutes are observable, not anecdotal.
A router with no fallback is the single most common production failure here (anti-pattern A2).
Format: Trigger / Action / Output / Evidence.
compute / compare / out-of-scope), map each kind to its correct handler + primitive (Stage 1 table).
and router design.
[[llamaindex]] Stage 0 ("What is the query distribution?") +Stage 4 heterogeneity table.
SummaryIndex for digest,VectorStoreIndex for lookup, NL2SQL for compute, SubQuestionQueryEngine for compare). Test each against its own kind in isolation. Author its description/label.
description (Principle 3).
[[llamaindex]] OP-06 RouteByQueryType; OP-07 DecomposeMultiHop.default for no-match. No model call.
cases.
[[agentsop-dify]] IF/ELSE node over query; [[agentsop-langgraph]] conditionaledge as a pure Python predicate (add_conditional_edges).
descriptions / few examples per route; pick argmax; threshold for fallback.
[[llamaindex]] EmbeddingSingleSelector family; router docs.handler's description; return chosen id(s) + reasoning. In LlamaIndex: RouterQueryEngine(selector=LLMSingleSelector.from_defaults(), query_engine_tools=[...]).
LLM round-trip.
[[llamaindex]] OP-06 + Dilemma 3; SelectorPromptTemplate.default (broad vector index) or explicit refuse path. Never best-guess silently.
[[agentsop-langgraph]] conditional edge can return a "__default__"branch; [[agentsop-dify]] Question Classifier has a built-in "other/else" class.
{ts, query_hash, chosen_handler, selector_score, fell_back}on every decision; surface a misroute dashboard.
[[agentsop-dify]] 7-class trace incl. routing; [[agentsop-langgraph]]LangSmith trace of the conditional edge.
add 1-2 disambiguating examples; re-evaluate on a labeled routing eval set.
[[llamaindex]] Dilemma 3 ("the only signal the router sees").困境: A 3-way router (summary / lookup / SQL) misroutes ~12% of queries — some lookup queries land on the SQL engine and error out. The team is split: fine-tune / swap to a bigger LLM selector, vs. add a catch-all fallback.
约束:
generic, lowering answer quality for the 12%.
engine "thinks" it can answer.
决策步骤:
The clustering reveals it's a description problem, not a model problem (the SQL engine's description over-claims).
"aggregate/count/sum over the orders table only"; add 2 negative examples. Re-run the routing eval set. Misroute drops to ~4%.
→ broad vector index, not SQL.
cost/benefit usually says no.
结果: Description fix + fallback, no model change. The two moves are complementary, not either/or: improve the classifier signal (descriptions) and add a fallback for the irreducible ambiguity. Mirrors [[llamaindex]] Dilemma 3's "fix the supervisor before switching paradigms" logic and [[agentsop-langgraph]]'s "hitting the limit means the logic is wrong" stance.
可提取的操作: OP-08, OP-06, OP-07.
困境: An LLM selector routes correctly but adds ~600ms + a token cost to every query. Traffic is high-QPS and most queries are lexically obvious ("summarize…", SQL-shaped, or a plain question). Is the LLM hop worth it?
约束:
per-query cost line.
tail.
[[agentsop-dify]]'s known per-node latency overhead and [[agentsop-langgraph]]'s"checkpoint serialisation adds overhead, latency budget <200ms" boundary both argue against an LLM hop on every request.
决策步骤:
router classifies with high confidence? Sample says ~80%.
~20% (no confident keyword match) escalates to the LLM selector. This is the routing analog of [[llamaindex]]'s "exhaust cheap knobs first" and [[agentsop-langgraph]]'s "promote upward only as needed" ladder.
slow correct one; when in doubt, escalate.
结果: A two-tier (cascade) router — cheap router handles the obvious majority instantly, LLM selector handles the ambiguous minority. Cost and p95 drop ~5×; accuracy is preserved because the cheap tier only acts when confident. Reserve the LLM selector for where reasoning is actually required (Principle 2 + the §3 Stage 3 ladder).
可提取的操作: OP-03, OP-04, OP-05, OP-06.
困境: Some queries legitimately need two handlers ("summarize the contract and tell me the late-fee clause"). A single-select router forces a wrong binary choice.
约束:
决策步骤:
single-select + fallback; accept occasional follow-up.
LLMMultiSelector /Dify branching to multiple nodes / LangGraph Send fan-out to multiple handlers) and add a synthesis node to merge results.
fan-out + merge problem with the cost of both.
结果: Single-select by default; multi-select only on the measured multi-intent slice, paired with explicit synthesis. Mirrors [[agentsop-langgraph]] "don't fan out with Send for fixed-cardinality work."
可提取的操作: OP-05, OP-07.
| # | Anti-pattern | Why it's wrong | Correct move | |---|---|---|---| | A1 | Adding a router when one index already serves every query | Pure overhead: an extra hop + a new failure mode for zero benefit | Single index; route only when ≥2 handlers differ in fit (Stage 0) | | A2 | Router with no fallback / default branch | Ambiguous or out-of-scope queries hit a random or erroring handler | Confidence threshold → documented default / refuse (OP-06) | | A3 | LLM selector on every query when keyword would do | p95 latency + per-query token cost for separable traffic | Tier: cheap router first, LLM only for the ambiguous tail (Dilemma 2) | | A4 | Vague destination descriptions | Router can't disambiguate; misroutes blamed on the model | Author precise descriptions + examples; fix here first (OP-08) | | A5 | No routing decision logging | Misroutes surface as user complaints, not metrics | Log {query, chosen, score, fell_back} per decision (OP-07) | | A6 | Routing by tenant/permission instead of query kind | That's access control, not query routing | Use [[agentsop-multi-tenant-rag]] filter at the store; route by kind only | | A7 | Multi-select as the default | Doubles cost + needs merge for mostly single-intent traffic | Single-select default; multi only on measured multi-intent slice (Dilemma 3) | | A8 | Best-guess on low confidence | Silent wrong route degrades the answer with no signal | Below threshold → fallback, never guess (OP-06) | | A9 | Tuning the router before the handlers work | A misroute is undebuggable atop broken handlers | Build + verify each handler in isolation first (Stage 2) |
[[agentsop-multi-tenant-rag]].static edge / linear pipeline, no selector.
→ measure first; tier to a keyword/embedding front, escalate rarely (Dilemma 2).
→ that's an agent loop, not a one-shot router; use [[agentsop-langgraph]] cycles.
RouterQueryEngine / classifier with no default / other branch.QueryEngineTool(description="index") — uselessly vague description.tenant_id — that's filtering, not routing.(query, expected_handler) pairs gating router changes.The same selector (query) -> handler_id surface across the three base skills. All verified against the source SKILLs (May 2026). Cross-link the base skill for the full API.
RouterQueryEngine + selectors → [[llamaindex]]pythonfrom llama_index.core.query_engine import RouterQueryEngine from llama_index.core.selectors import LLMSingleSelector # or LLMMultiSelector, # EmbeddingSingleSelector from llama_index.core.tools import QueryEngineTool tools = [ QueryEngineTool.from_defaults( query_engine=summary_engine, description="Useful for SUMMARIZING or digesting an entire document."), QueryEngineTool.from_defaults( query_engine=vector_engine, description="Useful for LOOKING UP a specific fact or clause."), ] router = RouterQueryEngine( selector=LLMSingleSelector.from_defaults(), # LLM selector family query_engine_tools=tools, # descriptions are the routing signal )
The selector reads each QueryEngineTool.description (the only routing signal — Principle 3). Selector families: LLMSingleSelector, LLMMultiSelector, EmbeddingSingleSelector, PydanticSingleSelector. SelectorPromptTemplate customizes the LLM prompt. RouterQueryEngine over per-task indices is "often the correct top-level shape, not a single monolithic VectorStoreIndex" ([[llamaindex]] Principle 3 + OP-06).
[[agentsop-dify]]The Question Classifier node (an LLM node) takes the query and emits one of N declared classes; each class wires to a downstream branch (Knowledge Retrieval / LLM / Code / HTTP / SQL-via-Code). It is the visual analog of an LLM selector. The built-in "other" class is the fallback (OP-06). Routing logic that doesn't need an LLM uses the IF/ELSE node (keyword/rule router, OP-03). Node taxonomy: Question Classifier, Parameter Extractor, IF/ELSE. The classifier's class label + instruction is the routing signal (Principle 3).
[[agentsop-langgraph]]pythondef route(state) -> str: # the selector function q = state["query"] if looks_like_sql(q): return "sql" # keyword tier (OP-03) if score := classify(q): return score.label # LLM/embedding tier (OP-04/05) return "vector_default" # fallback (OP-06) graph.add_conditional_edges("router", route, {"sql": "sql_node", "summary": "summary_node", "vector_default": "vector_node"})
"Graph topology is just routing logic over state… a conditional edge reads state and picks a next node" ([[agentsop-langgraph]] Principle 3). The mapping dict's keys are the destinations; the route function is the selector — it can be keyword, embedding, or LLM, or a tier of all three (Dilemma 2). For genuine multi-route fan-out, return a list of Send(...) instead of one label.
| | LlamaIndex | Dify | LangGraph | |---|---|---|---| | Router primitive | RouterQueryEngine | Question Classifier node | add_conditional_edges | | Selector impl | LLM / Embedding / Pydantic selector | LLM classifier (or IF/ELSE for rules) | any Python fn (keyword/embed/LLM) | | Routing signal | QueryEngineTool.description | class label + instruction | node names + what route() reads | | Fallback | selector default / catch-all tool | "other" class | a "__default__" branch | | Multi-route | LLMMultiSelector | branch to multiple nodes | list of Send(...) | | Deep skill | [[llamaindex]] | [[agentsop-dify]] | [[agentsop-langgraph]] |
The three are the same pattern — classify the query up front, dispatch to the structurally-correct handler, fall back when uncertain. Pick the framework your stack already uses; the routing discipline (Principles 1-3, Stages 0-4) is identical.
references/R1-source-evidence.md — every cited claim resolved to its sourceSKILL line.
intermediate/operation_candidates.json — machine-readable operation list.[[name]])[[llamaindex]] — llamaindex-sop-skill/SKILL.md (RouterQueryEngine,selectors, OP-06 RouteByQueryType, Dilemma 3, Index taxonomy).
[[agentsop-dify]] — dify-sop-skill/SKILL.md (Question Classifier / IF-ELSE nodes,node taxonomy, per-node latency boundary).
[[agentsop-langgraph]] — langgraph-sop-skill/SKILL.md (conditional edges asrouters, Principle 3 topology-as-routing, Send fan-out, latency boundary).
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 30,937 | 29,783 | -4% | 1 | 1 | 0% | 5,539 | 11,804 | +113% | 0 | 0 | — |
case-02 | fail→fail | 17,472 | 19,314 | +11% | 1 | 1 | 0% | 2,746 | 9,628 | +251% | 0 | 0 | — |
case-03 | fail→pass | 19,112 | 15,035 | -21% | 1 | 1 | 0% | 3,515 | 9,140 | +160% | 0 | 0 | — |
case-04 | pass→pass | 11,488 | 9,089 | -21% | 1 | 1 | 0% | 1,691 | 7,760 | +359% | 0 | 0 | — |
case-05 | pass→pass | 13,417 | 9,438 | -30% | 1 | 1 | 0% | 1,938 | 7,874 | +306% | 0 | 0 | — |
case-06 | pass→pass | 14,959 | 11,596 | -22% | 1 | 1 | 0% | 2,166 | 8,185 | +278% | 0 | 0 | — |
case-07 | fail→pass | 13,985 | 11,926 | -15% | 1 | 1 | 0% | 2,024 | 8,313 | +311% | 0 | 0 | — |
case-08 | pass→pass | 13,218 | 9,163 | -31% | 1 | 1 | 0% | 1,986 | 7,720 | +289% | 0 | 0 | — |
case-09 | pass→pass | 14,633 | 14,495 | -1% | 1 | 1 | 0% | 2,433 | 8,907 | +266% | 0 | 0 | — |
case-10 | fail→pass | 13,554 | 10,745 | -21% | 1 | 1 | 0% | 2,136 | 8,040 | +276% | 0 | 0 | — |
case-11 | pass→pass | 6,150 | 4,778 | -22% | 1 | 1 | 0% | 1,090 | 7,163 | +557% | 0 | 0 | — |
case-12 | pass→pass | 14,719 | 18,422 | +25% | 1 | 1 | 0% | 2,791 | 9,503 | +240% | 0 | 0 | — |
case-13 | pass→pass | 6,479 | 3,687 | -43% | 1 | 1 | 0% | 1,129 | 6,949 | +516% | 0 | 0 | — |
case-14 | pass→pass | 11,562 | 4,681 | -60% | 1 | 1 | 0% | 1,788 | 7,152 | +300% | 0 | 0 | — |
case-15 | pass→pass | 8,822 | 3,675 | -58% | 1 | 1 | 0% | 1,275 | 6,924 | +443% | 0 | 0 | — |
case-16 | pass→pass | 12,356 | 9,061 | -27% | 1 | 1 | 0% | 1,884 | 7,771 | +312% | 0 | 0 | — |
case-17 | pass→pass | 16,112 | 18,981 | +18% | 1 | 1 | 0% | 2,317 | 9,438 | +307% | 0 | 0 | — |
case-18 | pass→pass | 12,451 | 14,935 | +20% | 1 | 1 | 0% | 1,909 | 8,718 | +357% | 0 | 0 | — |
case-19 | pass→pass | 15,435 | 18,384 | +19% | 1 | 1 | 0% | 2,612 | 9,246 | +254% | 0 | 0 | — |
case-20 | pass→fail | 15,654 | 17,937 | +15% | 1 | 1 | 0% | 2,349 | 9,286 | +295% | 0 | 0 | — |
case-21 | pass→pass | 17,482 | 17,705 | +1% | 1 | 1 | 0% | 3,323 | 9,477 | +185% | 0 | 0 | — |
case-22 | pass→pass | 12,806 | 9,805 | -23% | 1 | 1 | 0% | 1,918 | 7,916 | +313% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.