LlamaIndex SubQuestionQueryEngine — over-decomposition and vague sub-queries
<code>SubQuestionQueryEngine</code> is the LlamaIndex pattern for complex questions that need answers from multiple sources. It decomposes the user's query into per-tool sub-questions, runs each, and synthesizes the aggregate.
Quick fix (TL;DR)
SubQuestionQueryEngine uses an LLM to break the query into sub-questions, each targeted at a specific QueryEngineTool. Failure modes: (a) too many sub-questions (cost blowup); (b) sub-questions that are vague or off-topic; (c) tools do not have the info the sub-question expects. Fix by (a) capping question_gen output, (b) writing rich tool descriptions, and (c) enabling use_async for parallel sub-question execution.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.
# Query: "Compare our pricing to competitors" # Generated 14 sub-questions across 3 tools # Cost: $0.87 for one query
# Sub-question 1: "General information about pricing" # Sub-question 2: "Company information" # ← too vague to retrieve well; poor final answer
# Product tool asked: "What is your refund policy?" # Product tool returns nothing useful; final answer incomplete
Reference
When to use SubQuestionQueryEngine
| Query type | Recommendation |
|---|---|
| Simple lookup | Plain query engine |
| Domain-specific with one topic | RouterQueryEngine |
| Multi-part question spanning domains | SubQuestionQueryEngine |
| Iterative / conversational | Chat engine or agent |
| Complex reasoning + tools | Agent |
Root causes, ranked by frequency
Based on developer reports across LlamaIndex forums, GitHub issues, and Discord community during 2025–2026.
- 26%Question generator produces too many sub-questions. Cost multiplies; retrieval quality drops.
- 18%Sub-questions too vague. Generic like "info about X" instead of specific.
- 14%Wrong tool for a sub-question. Question generator misroutes based on sparse tool descriptions.
- 10%use_async=False on many sub-questions. Sequential execution takes N × single_query time.
- 8%Question generator LLM different from synthesis LLM. Config confusion; wrong LLM used for one step.
- 7%Sub-question overlap. Two sub-questions ask the same thing to different tools; redundant work.
- 10%Final synthesis loses per-sub-question source attribution. User cannot cite which source answered what.
- 7%Query gen prompt too permissive. Default prompt generates 5-10 sub-questions on many queries.
Fixes — copy-paste solutions
Configure SubQuestionQueryEngine with async and a bounded question generator
Enable use_async=True so sub-questions run in parallel. Configure the question generator with a custom prompt that limits sub-question count.
from llama_index.core.query_engine import SubQuestionQueryEngine from llama_index.core.question_gen import LLMQuestionGenerator from llama_index.core.tools import QueryEngineTool, ToolMetadata from llama_index.llms.openai import OpenAI llm = OpenAI(model="gpt-4o-mini") # --- Tools with rich descriptions --- policy_tool = QueryEngineTool( query_engine=policy_index.as_query_engine(similarity_top_k=6), metadata=ToolMetadata( name="policies", description="Company policies: refund, return, warranty, shipping, ToS.", ), ) product_tool = QueryEngineTool( query_engine=product_index.as_query_engine(similarity_top_k=6), metadata=ToolMetadata( name="products", description="Product catalog: features, specs, pricing.", ), ) # --- Custom question generator prompt (capped) --- CUSTOM_QUESTION_GEN_PROMPT = """ Given a user question and a list of tools, generate the MINIMUM number of sub-questions (at most 4) needed to answer the user's question. Each sub-question should be targeted at exactly one tool and be specific enough to retrieve relevant information. Do NOT generate sub-questions that are vague or redundant. Do NOT generate more than 4 sub-questions total. Tools: {tools_str} User question: {query_str} Sub-questions (JSON array): """ # --- SubQuestionQueryEngine with async and bounded generator --- question_gen = LLMQuestionGenerator.from_defaults( llm=llm, prompt_template_str=CUSTOM_QUESTION_GEN_PROMPT, ) subq_engine = SubQuestionQueryEngine.from_defaults( query_engine_tools=[policy_tool, product_tool], question_gen=question_gen, llm=llm, use_async=True, # parallel sub-question execution verbose=True, # prints sub-questions ) response = subq_engine.query( "Compare the refund policy with product warranty terms for the ACME-5000." ) print(response) print(f"\nSource nodes: {len(response.source_nodes)}")
Log per-sub-question source attribution
SubQuestionQueryEngine tracks intermediate results. Expose the mapping from sub-question to answer to source so downstream can cite properly.
from llama_index.core.query_engine import SubQuestionQueryEngine from llama_index.core.callbacks import CallbackManager, LlamaDebugHandler, CBEventType # Enable the debug handler to see all events debug = LlamaDebugHandler(print_trace_on_end=False) cb_manager = CallbackManager([debug]) subq_engine = SubQuestionQueryEngine.from_defaults( query_engine_tools=[policy_tool, product_tool], llm=llm, use_async=True, verbose=True, callback_manager=cb_manager, ) response = subq_engine.query("...") print(f"Final: {response.response}") # --- Extract per-sub-question intermediate steps --- sub_events = debug.get_events(CBEventType.SUB_QUESTION) for event in sub_events: payload = event.payload or {} sub_q_ans = payload.get("sub_question") # type: SubQuestionAnswerPair if sub_q_ans: print(f"\nSub-question: {sub_q_ans.sub_q.sub_question}") print(f"Tool: {sub_q_ans.sub_q.tool_name}") print(f"Answer: {sub_q_ans.answer[:200]}") print(f"Sources: {[n.metadata.get('source') for n in sub_q_ans.sources or []]}") # --- For UI: attach citations per sub-answer --- def render_response_with_citations(response, debug): output = [response.response, "\n\n**Sources:**"] for event in debug.get_events(CBEventType.SUB_QUESTION): sq = (event.payload or {}).get("sub_question") if sq: output.append( f"- **{sq.sub_q.sub_question}** ({sq.sub_q.tool_name}): " f"{[n.metadata.get('source', '?') for n in sq.sources or []]}" ) return "\n".join(output) print(render_response_with_citations(response, debug))
Reserve SubQuestionQueryEngine for actually-decomposable queries
For queries that can be answered from one tool, RouterQueryEngine or plain query engine is cheaper and simpler. Use SubQuestion when the query truly spans multiple domains.
# --- Bad match for SubQuestion — single-domain query --- # query = "What is the refund policy?" # Just use policy_engine directly # --- Good match for SubQuestion — multi-domain query --- # query = "Compare the refund policy against the ACME-5000 warranty" # Needs policies AND products; sub-questions naturally decompose # --- Bad match for SubQuestion — iterative reasoning --- # query = "Investigate why sales dropped last quarter and recommend a fix" # Needs iterative reasoning + planning; use an agent # --- Simple heuristic router --- from llama_index.core.query_engine import RouterQueryEngine, SubQuestionQueryEngine # Set up both engines router_engine = RouterQueryEngine( selector=PydanticSingleSelector.from_defaults(llm=llm), query_engine_tools=[policy_tool, product_tool], ) subq_engine = SubQuestionQueryEngine.from_defaults( query_engine_tools=[policy_tool, product_tool], llm=llm, use_async=True, ) # --- Meta-router that picks between them --- def smart_query(query: str) -> str: # Heuristic: multi-domain markers multi_domain_markers = [ "compare", "and", "also", "both", "in addition", "how does X relate to Y", "along with", ] if any(marker in query.lower() for marker in multi_domain_markers): return subq_engine.query(query).response return router_engine.query(query).response # --- Or use an LLM upfront to route --- def llm_smart_query(query: str) -> str: """Use a cheap LLM call to decide.""" decision = llm.complete( f"Should this query be answered by decomposing into sub-questions " f"across multiple domains, or by a single specialized tool?\n\n" f"Query: {query}\n\n" f"Answer 'single' or 'decompose':" ).text.strip().lower() if "decompose" in decision: return subq_engine.query(query).response return router_engine.query(query).response
Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Only use SubQuestionQueryEngine when queries span multiple domains — otherwise use a router or single engine.
- Cap sub-question count via a custom question_gen prompt.
- Always set
use_async=True; sequential decomposition is impractically slow. - Log per-sub-question sources for citation and evaluation.
- Write rich tool descriptions — the question generator uses them to target sub-questions.
- Monitor cost per query in production; a single decomposition can cost 5-10x a plain query.
- For heterogeneous query mix, combine with a router that decides "decompose or not".
Frequently asked questions
prompt_template_str to LLMQuestionGenerator, or write a custom generator subclassing BaseQuestionGenerator. Useful for domain-specific decomposition patterns.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.