Sub-Question Decomposition — Divide and Compose¶
What problem does this solve?¶
A multi-part question asks several things. "What concentration risks does Palantir disclose for government customers, and how does its competitive landscape make those risks worse?" wants both the concentration risk and the competitive analysis. Standard retrieval finds chunks about one or the other; the model has to invent the connection. Sub-question decomposition explicitly splits the question into independent sub-questions, retrieves and answers each, then composes the answers. Each retrieval is focused; the composition step is just text concatenation plus one final LLM call. Quality improves on multi-part questions; cost is one LLM call per sub-question plus the composer.
Where it came from¶
The pattern dates to LlamaIndex's SubQuestionQueryEngine in 2023. The technique is older in different forms (multi-hop QA in IR, decomposition prompting), but LlamaIndex packaged it cleanly for RAG. The decomposition-prompting line of research (Khot et al., 2022) provided the conceptual scaffolding — show the LLM what good decomposition looks like via few-shot, then let it generate sub-questions on demand.
By 2025 it had become a standard for production systems whose users ask multi-part questions — analyst tools, research assistants, anywhere queries genuinely have N>1 retrievable parts. Modern implementations parallelise the sub-question retrievals, cap the decomposition depth, and pair the composer with a faithfulness check to avoid the cascading-error failure mode.
Where it fits in the RAG landscape¶
Sub-question decomposition is the heaviest of the query transformations. Each sub-question is a full RAG call — embedding, retrieval, generation — so the cost multiplies with the number of sub-questions.
- HyDE / Multi-query / Step-back (Recipes 13-15). Transform one query into another query, retrieve once.
- Sub-question (this recipe). Transform one query into multiple queries, run them all, compose.
- Iterative multi-hop (Recipe 26, MULTI_HOP branch). Transform sequentially based on what came back. Each step's question is informed by the previous step's answer.
Sub-question is parallel decomposition; multi-hop is sequential. The two compose in production agents — the multi-hop branch can run sub-question decomposition at each hop, or sub-question can run multi-hop for each part.
When to use it (and when not to)¶
Use sub-question decomposition for multi-part questions and comparative analysis. Anywhere the user asks "what about X and Y", anywhere a research-style question genuinely needs multiple distinct pieces of evidence. Skip it for single-part questions. The decomposition step adds latency without value, and the composer call wastes another LLM round-trip. Skip it when sub-questions don't decompose cleanly. "What is the meaning of life?" is not multi-part; it's vague. Decomposing a vague question produces vague sub-questions and useless retrievals.
The intuition¶
Three intuitions:
Each sub-question retrieves cleanly. Splitting "X and Y" into "X" and "Y" lets each retrieval focus on one topic, where a combined query would pull noisy chunks for both. Sharper retrieval per sub-question means better partial answers.
The composer is doing real work. Combining partial answers requires reasoning about consistency, contradiction, and emphasis. The composer prompt matters — it has to instruct reconciliation, not just concatenation.
Runaway decomposition is the failure mode. "What is Palantir?" can be decomposed into 10 sub-questions if the model is allowed. Cap it. Most production systems set the maximum at 4 sub-questions per query and reject the decomposition (falling back to flat RAG) if the model proposes more.
Architecture¶
flowchart TB Q[Multi-part question] --> D[LLM: decompose
into sub-questions] D --> S1[Sub-question 1] D --> S2[Sub-question 2] D --> S3[Sub-question 3] S1 --> R1[RAG] S2 --> R2[RAG] S3 --> R3[RAG] R1 --> P1[Partial 1] R2 --> P2[Partial 2] R3 --> P3[Partial 3] P1 --> C[LLM: compose
final answer] P2 --> C P3 --> C
References¶
- 📚 LlamaIndex SubQuestionQueryEngine — Reference implementation.
- 📄 Decomposition prompting — Khot et al., 2022 — The technique applied generally to LLM reasoning.
- 📚 LangChain MultiVector Retriever — Related composition pattern.
- 📄 Adaptive-RAG (Recipe 26) — Routes multi-hop questions to a sub-question-like branch.
- 📄 Self-Ask prompting — Iterative self-decomposition; cousin pattern.
- 📚 LangGraph multi-step QA tutorial — How to wrap decomposition in a stateful graph.
Setup¶
Pick a provider via the PROVIDER env var; everything below is provider-agnostic. The default is Nebius. Tracing is off by default in published notebooks so the outputs are clean — flip COOKBOOK_TRACING=phoenix to send spans to a local Phoenix UI.
import os
os.environ.setdefault('PROVIDER', 'nebius')
os.environ.setdefault('COOKBOOK_TRACING', 'off')
from cookbook.providers import LLMClient
from cookbook.tracing import init_tracing
client = LLMClient()
print(f'Provider: {client.provider} | Chat model: {client.chat_model}')
print(init_tracing())
Provider: nebius | Chat model: meta-llama/Llama-3.3-70B-Instruct Tracing disabled.
Build the Pipeline, Step by Step¶
Step 1 — Index the SEC 10-K¶
10-K filings are loaded with multi-part questions because they cover risk, business model, financials, governance. Standard setup.
from cookbook.corpora import load_sec_10k
from cookbook.chunkers import fixed_window
from cookbook.stores import QdrantBackend
docs = list(load_sec_10k())
chunks = fixed_window(docs, target_tokens=320, overlap_tokens=32)
vectors = client.embed([c.text for c in chunks])
store = QdrantBackend('subq', dim=len(vectors[0]))
store.add([c.text for c in chunks], vectors, ids=[c.chunk_id for c in chunks])
print(f'Indexed {len(chunks)} chunks.')
C:\Users\faree\Desktop\rag\rag-cookbook-2026\.venv\Lib\site-packages\tqdm\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm
Indexed 295 chunks.
Step 2 — Decompose into sub-questions¶
JSON-formatted output. We constrain the number to 2-4 to prevent runaway decomposition.
import json
import re
DECOMPOSE_PROMPT = (
'Break the question into 2-4 self-contained sub-questions whose individual answers, '
'combined, fully answer the original. Each sub-question must be answerable independently.\n\n'
'Respond as JSON: {{"sub_questions": ["...", "..."]}}\n\n'
'Question: {q}'
)
def decompose(question: str) -> list[str]:
raw = client.chat(DECOMPOSE_PROMPT.format(q=question))
m = re.search(r'\{.*\}', raw, flags=re.DOTALL)
if not m:
return [question]
try:
data = json.loads(m.group())
return list(data.get('sub_questions', [question]))
except json.JSONDecodeError:
return [question]
q = 'What concentration risks does Palantir disclose for government customers, and how does its competitive landscape make those risks worse?'
subs = decompose(q)
for s in subs:
print(f' - {s}')
- What concentration risks related to government customers does Palantir disclose? - What is the competitive landscape of Palantir's industry? - How does the competitive landscape exacerbate the concentration risks associated with government customers for Palantir?
Step 3 — Answer each sub-question¶
Standard RAG per sub-question. We collect the partial answers and the contexts they used.
def answer_subquestion(sub: str) -> tuple[str, list[str]]:
qv = client.embed([sub])[0]
hits = store.search(qv, top_k=4)
contexts = [h.text for h in hits]
ans = client.chat(
'Use these passages.\n' + '\n\n'.join(contexts) + f'\nQuestion: {sub}\nAnswer:'
)
return ans, contexts
for s in subs:
a, _ = answer_subquestion(s)
print(f'Q: {s}')
print(f' {a[:200]}...')
print()
Q: What concentration risks related to government customers does Palantir disclose? Palantir discloses the following concentration risks related to government customers: 1. Changes in the government's attitude towards the company or its platforms as viable or acceptable software sol... Q: What is the competitive landscape of Palantir's industry? According to the passage, Palantir is fundamentally competing with the internal software development efforts of its potential customers. Organizations often try to build their own data platforms befor... Q: How does the competitive landscape exacerbate the concentration risks associated with government customers for Palantir? The competitive landscape exacerbates the concentration risks associated with government customers for Palantir in several ways: 1. **Bid protests and disputes**: The presence of competitors can lead...
Step 4 — Compose the final answer¶
The composer reads partial answers and produces a coherent response. The composer prompt explicitly asks for reconciliation of any conflicts.
COMPOSE_PROMPT = (
'Combine the partial answers below into one coherent response to the original question. '
'Reconcile any conflicts; do not invent facts.\n\n'
'Partial answers:\n{partials}\n\n'
'Original question: {q}\nFinal answer:'
)
partials = []
all_contexts = []
for s in subs:
a, ctx = answer_subquestion(s)
partials.append(f'Sub-Q: {s}\nA: {a}')
all_contexts.extend(ctx)
final = client.chat(COMPOSE_PROMPT.format(partials='\n\n'.join(partials), q=q))
print(final)
Palantir discloses several concentration risks related to government customers, including changes in government attitudes, appeals and disputes, adoption of new laws or regulations, budgetary constraints, influence from third parties, changes in political or social attitudes, potential delays or changes in government appropriations or procurement processes, and increased or unexpected costs. These risks may cause governments to delay or refrain from purchasing Palantir's platforms and services, reduce the size or payment amounts of purchases, or have an adverse effect on the company's business, results of operations, financial condition, and growth prospects. The competitive landscape of Palantir's industry, characterized by a mix of internal development efforts and external solutions, exacerbates these concentration risks. Organizations often try to build their own data platforms before turning to buy Palantir's solutions, and the company competes against a range of alternatives, including custom-built solutions, outside consultants, IT services companies, packaged enterprise and open source software, and significant internal IT resources. This competitive landscape increases the risks associated with government customers in several ways, including bid protests and disputes, influence from third parties, competition for contracts, changes in government attitudes, and budgetary constraints. As a result, the presence of competitors makes it more challenging for Palantir to secure new contracts, renew existing ones, and navigate budgetary constraints, ultimately increasing the concentration risks associated with its government customers.
Step 5 — Wrap as answer_question¶
Standard contract.
def answer_question(question: str) -> tuple[str, list[str]]:
subs = decompose(question)
partials, all_ctx = [], []
for s in subs:
a, ctx = answer_subquestion(s)
partials.append(f'Sub-Q: {s}\nA: {a}')
all_ctx.extend(ctx)
final = client.chat(COMPOSE_PROMPT.format(partials='\n\n'.join(partials), q=question))
return final, all_ctx
ans, _ = answer_question('How does AIP commercial revenue relate to government concentration risks?')
print(ans[:400])
AIP commercial revenue is $1,295,902 for the year ended December 31, 2024, and it is generated from commercial customers who pay to use the software platforms built by the company. The revenue is recognized ratably over the contract term, which is generally one to five years in length. The commercial revenue increased by $293.1 million, or 29%, for the year ended December 31, 2024, compared to 202
Look Inside¶
Inspect — decompositions across three questions¶
Read what the decomposer produces.
for question in [
'How does Palantir generate revenue and what risks does it disclose?',
'Compare AIP to Foundry and Gotham.',
'What governance structures protect founders, and what challenges do they create for investors?',
]:
print(f'Q: {question}')
for s in decompose(question):
print(f' - {s}')
print()
Q: How does Palantir generate revenue and what risks does it disclose? - What are the primary sources of revenue for Palantir? - What are the main methods by which Palantir generates revenue from its products and services? - What types of risks does Palantir disclose in its financial reports and regulatory filings? - How do the disclosed risks potentially impact Palantir's financial performance and future growth prospects? Q: Compare AIP to Foundry and Gotham. - What are the key features and functionalities of AIP? - How do the features and functionalities of Foundry compare to AIP? - How do the features and functionalities of Gotham compare to AIP? - How do Foundry and Gotham compare to each other in relation to AIP? Q: What governance structures protect founders, and what challenges do they create for investors? - What governance structures are typically used to protect founders' interests? - How do these governance structures impact investors, and what challenges do they pose? - What are the potential risks and drawbacks for investors when dealing with founder-protective governance structures? - How can investors mitigate or navigate these challenges to ensure their interests are aligned with those of the founders?
Inspect — single-part query¶
If the question is single-part, the decomposer should return one sub-question (or the original).
for question in [
'What is AIP?',
'In what state is Palantir incorporated?',
]:
subs = decompose(question)
print(f' Q: {question}')
print(f' n_subs: {len(subs)}')
Q: What is AIP?
n_subs: 3
Q: In what state is Palantir incorporated?
n_subs: 2
Inspect — cost¶
Sub-question decomposition is expensive. Count LLM calls per query.
from cookbook import _cache
before = _cache.stats()['entries']
_ = answer_question('How does Palantir generate revenue and what risks does it disclose?')
after = _cache.stats()['entries']
print(f'New cache entries: {after - before}')
print('Rough breakdown for 3 sub-questions:')
print(' 1 decomposition LLM call')
print(' 3 query embeds (one per sub)')
print(' 3 per-sub answer LLM calls')
print(' 1 compose LLM call')
print(' = 8 total LLM-equivalent calls')
New cache entries: 0 Rough breakdown for 3 sub-questions: 1 decomposition LLM call 3 query embeds (one per sub) 3 per-sub answer LLM calls 1 compose LLM call = 8 total LLM-equivalent calls
Inspect — read one partial answer¶
Make sure individual sub-question answers are coherent before composition. If they're not, composition cannot save them.
q = "How does Palantir's AIP relate to its government revenue concentration risk?"
subs = decompose(q)
for s in subs[:2]:
a, _ = answer_subquestion(s)
print(f'Sub-Q: {s}')
print(f' {a[:300]}')
print()
17:47:40 - LiteLLM:WARNING: common_utils.py:979 - litellm: could not pre-load bedrock-runtime response stream shape — Bedrock event-stream decoding will be unavailable. Error: No module named 'botocore'
17:47:40 - LiteLLM:WARNING: common_utils.py:24 - litellm: could not pre-load sagemaker-runtime response stream shape — SageMaker event-stream decoding will be unavailable. Error: No module named 'botocore'
Sub-Q: What is Palantir's AIP and its significance to the company's revenue streams? According to the provided passages, Palantir's Additional Paid-in Capital (AIP) is $10,193,970 as of December 31, 2024, and $9,122,173 as of December 31, 2023. The significance of AIP to the company's revenue streams is not directly stated in the passages. However, AIP represents the amount of mone
Sub-Q: How does Palantir's government revenue contribute to its overall revenue concentration? According to the passage, Palantir's government revenue has been increasing over the years, with $1,569,605 in 2024, $1,222,215 in 2023, and $1,071,776 in 2022. The government segment's contribution margin has remained relatively stable, ranging from 58% to 60% over the three years. As a percentage
Run It¶
End-to-end on a multi-part question.
ans, _ = answer_question('What competitive landscape does Palantir describe, and how does it shape risks tied to government customers?')
print('=== Sub-question composed answer ===')
print(ans)
=== Sub-question composed answer === Palantir describes its competitive landscape as one where it "fundamentally competes with the internal software development efforts of our potential customers." This means that many organizations try to build their own data platforms before considering purchasing Palantir's software. Additionally, Palantir faces competition from a "patchwork of custom solutions, outside consultants, IT services companies, packaged enterprise and open source software, and significant internal IT resources." In other words, the company competes with a variety of alternative solutions that potential customers may use to meet their data platform needs, rather than traditional direct competitors. This competitive landscape shapes risks for Palantir's government customers in several ways. The influence of third parties, competition from other contractors, changes in government attitudes, budgetary constraints, and regulatory changes can all impact Palantir's ability to secure or maintain contracts with government customers. These factors contribute to the risks associated with Palantir's government customers, including delays or reductions in purchases, changes in procurement processes, and potential adverse effects on the company's business, financial condition, and growth prospects. Specific risks tied to government customers in this landscape include changes in fiscal or contracting policies, facility clearance requirements, government certifications, changes in the political environment, appeals, disputes, or litigation, budgetary constraints, influence by third parties, changes in political or social attitudes, delays or changes in government appropriations or procurement processes, and increased or unexpected costs. These risks can cause governments and governmental agencies to delay or refrain from purchasing Palantir's platforms and services, reduce the size or payment amounts of purchases, or have an adverse effect on the company's business, results of operations, financial condition, and growth prospects. Ultimately, these risks can significantly impact Palantir's operations and profitability. Disruptions to third-party services, limitations on liability, reputation and brand damage, increased costs and resource allocation, operational interruptions, and compliance and regulatory risks can all affect the company's financial condition and growth prospects. Effective risk management and mitigation strategies are essential to minimize the potential effects of these risks and ensure the long-term success of the company.
Side by Side: Vanilla Baseline vs This Technique¶
Vanilla vs sub-question decomposition on a multi-part question.
from cookbook.baselines import vanilla_pipeline
q = 'What competitive landscape does Palantir describe, and how does it shape risks tied to government customers?'
base = vanilla_pipeline(q, corpus='sec-10k-pltr', top_k=5)
ours_a, _ = answer_question(q)
import pandas as pd
pd.DataFrame([
{'pipeline': 'vanilla', 'preview': base.answer[:200]},
{'pipeline': 'sub-question', 'preview': ours_a[:200]},
])
| pipeline | preview | |
|---|---|---|
| 0 | vanilla | Palantir describes a competitive landscape whe... |
| 1 | sub-question | Palantir describes its competitive landscape a... |
Knobs to Turn¶
Five knobs in priority order:
- Sub-question cap. 2-4 is the cookbook default. Higher invites runaway decomposition and balloons cost without quality gain.
- Decomposer model. Mid-tier is fine; a frontier model is overkill for decomposition. Save your top-tier budget for the per-sub-question answering.
- Per-sub-question
k. We use 4. Higher catches more recall but inflates the partial-answer length, which makes composition harder. - Composer prompt. "Reconcile conflicts" matters. Without it the composer often picks the longest partial answer and ignores the rest.
- Parallelise. Sub-question answers are independent. Run them concurrently with asyncio or a thread pool to cut latency by 2-4x.
Evaluate on a Slice¶
Run the recipe's answer_question over a small slice of the hand-curated eval set. Full RAGAS metrics are exercised in recipes/09-evaluation-and-production/ragas-triad-eval.ipynb; here we just print a quick spot-check table so you can eyeball whether the technique is on track.
from cookbook.corpora import load_eval_questions
from cookbook.eval import EvalSample
qs = load_eval_questions()
qs = [q for q in qs if q['corpus'] == 'sec-10k-pltr']
samples = []
for row in qs[:5]:
answer, contexts = answer_question(row['question'])
samples.append({
'question': row['question'],
'expected': row['answer'],
'actual': answer[:200],
'contexts_retrieved': len(list(contexts)),
})
import pandas as pd
pd.DataFrame(samples)
| question | expected | actual | contexts_retrieved | |
|---|---|---|---|---|
| 0 | What are Palantir's two principal software pla... | Palantir markets Gotham and Foundry, with Apol... | Palantir's two principal software platforms ar... | 8 |
| 1 | What is the stated mission of Palantir accordi... | Palantir's stated mission is to make instituti... | The stated mission of Palantir is not explicit... | 16 |
| 2 | What customer segments does Palantir distinguish? | Government customers and commercial customers,... | Palantir distinguishes between two main custom... | 16 |
| 3 | What is AIP, as described in the filing? | The Artificial Intelligence Platform, Palantir... | AIP stands for Artificial Intelligence Platfor... | 16 |
| 4 | Name one risk factor Palantir highlights relat... | Concentration with a small number of governmen... | One risk factor Palantir highlights related to... | 12 |
Closing Thoughts¶
Three failure modes:
- Bad decomposition. Sub-questions that overlap heavily produce redundant retrievals. Sub-questions that don't cover the original miss information. Test on a labelled slice.
- Cascading errors. If one sub-question's answer is wrong, the composer carries the error forward. Self-RAG (Recipe 24) over each partial answer reduces this.
- Cost explosion. Latency and token cost scale with N sub-questions. Cap aggressively.
Compose with adaptive routing (Recipe 26) — only decompose for genuinely multi-part questions. Compose with CRAG (Recipe 25) on each partial — fallback to web when the corpus misses.