Context Budgeting in Agent RAG: What to Include, What to Drop
Master context window management for agent RAG systems with strategies for token budgeting, relevance scoring, compression, and dynamic context allocation using Gemini API patterns.
Published on • September 15, 2026
AI Assistant

Your context window is a finite resource. Every token you spend on irrelevant context is a token stolen from quality answers. Context budgeting is the discipline of allocating your context budget across retrieved documents, conversation history, system prompts, and tool outputs.
The Context Budget Problem
A typical agent RAG request uses context for:
- System prompt: 500-2,000 tokens
- Conversation history: 2,000-20,000 tokens
- Retrieved documents: 5,000-30,000 tokens
- Tool outputs: 1,000-10,000 tokens
- Available for response: 2,000-4,000 tokens
With Gemini’s 1M+ token context windows, you might think budgeting doesn’t matter. But larger context doesn’t mean better answers—it means more noise for the model to sift through.
Budget Allocation Strategies
Fixed Budget Allocation
from dataclasses import dataclass
@dataclass
class ContextBudget:
system_prompt: int = 2000
conversation_history: int = 8000
retrieved_documents: int = 15000
tool_outputs: int = 5000
response_reserve: int = 3000
@property
def total(self) -> int:
return (self.system_prompt + self.conversation_history +
self.retrieved_documents + self.tool_outputs +
self.response_reserve)
def remaining_for_retrieval(self, used: dict) -> int:
return max(0, self.retrieved_documents - used.get("retrieved", 0))
Dynamic Budget Allocation
class DynamicBudgetAllocator:
def __init__(self, total_budget: int = 128000):
self.total_budget = total_budget
def allocate(self, query: str, history_length: int, num_tools: int) -> dict:
# Estimate system prompt size
system_tokens = 1500
# Allocate for history based on complexity
history_ratio = min(0.3, history_length * 0.02)
history_budget = int(self.total_budget * history_ratio)
# Reserve for tool outputs
tool_budget = num_tools * 3000
# Response reserve
response_budget = 4000
# Everything else goes to retrieval
retrieval_budget = (
self.total_budget -
system_tokens -
history_budget -
tool_budget -
response_budget
)
return {
"system": system_tokens,
"history": history_budget,
"retrieval": retrieval_budget,
"tools": tool_budget,
"response": response_budget,
}
Filtering Retrieved Content
Relevance Scoring
import numpy as np
class RelevanceScorer:
def __init__(self, embedder, llm):
self.embedder = embedder
self.llm = llm
def score_and_filter(
self,
query: str,
documents: list[str],
budget_tokens: int,
min_score: float = 0.3
) -> list[dict]:
# Embed query and documents
query_embedding = self.embedder.embed_query(query)
doc_embeddings = self.embedder.embed_documents(documents)
# Compute similarity scores
scores = np.dot(doc_embeddings, query_embedding)
# Score each document
scored_docs = []
for i, (doc, score) in enumerate(zip(documents, scores)):
token_count = estimate_tokens(doc)
relevance = float(score)
# Value = relevance / cost
value = relevance / max(token_count / 1000, 0.1)
scored_docs.append({
"content": doc,
"relevance": relevance,
"tokens": token_count,
"value": value,
"index": i,
})
# Sort by value and fit within budget
scored_docs.sort(key=lambda x: x["value"], reverse=True)
selected = []
tokens_used = 0
for doc in scored_docs:
if doc["relevance"] < min_score:
continue
if tokens_used + doc["tokens"] > budget_tokens:
# Try to truncate
remaining = budget_tokens - tokens_used
if remaining > 200:
doc["content"] = truncate_to_tokens(doc["content"], remaining)
doc["tokens"] = remaining
selected.append(doc)
break
selected.append(doc)
tokens_used += doc["tokens"]
return selected
Progressive Disclosure
class ProgressiveDisclosure:
"""Load more context only when needed."""
def __init__(self, vector_store, llm):
self.vector_store = vector_store
self.llm = llm
def retrieve_with_budget(self, query: str, budget_tokens: int) -> dict:
# Stage 1: Get summaries (low token cost)
summaries = self.vector_store.similarity_search(query, k=10, search_type="mmr")
summary_text = "\n".join([f"- {s.metadata['title']}: {s.page_content[:100]}" for s in summaries])
# Ask LLM which documents are relevant
selection_prompt = f"""Given the query: "{query}"
Which of these documents are most relevant? Return indices (comma-separated).
{summary_text}
Relevant indices:"""
selected_indices = self.llm.invoke([HumanMessage(content=selection_prompt)])
indices = parse_indices(selected_indices.content)
# Stage 2: Load full content only for selected documents
full_docs = []
tokens_used = estimate_tokens(summary_text)
for idx in indices:
if idx < len(summaries):
doc = summaries[idx]
full_content = self.vector_store.get_full_document(doc.id)
doc_tokens = estimate_tokens(full_content)
if tokens_used + doc_tokens <= budget_tokens:
full_docs.append({
"title": doc.metadata["title"],
"content": full_content,
"tokens": doc_tokens,
})
tokens_used += doc_tokens
return {
"documents": full_docs,
"total_tokens": tokens_used,
"budget": budget_tokens,
"utilization": tokens_used / budget_tokens,
}
Compression Strategies
Extractive Compression
def extractive_compress(document: str, query: str, max_tokens: int) -> str:
"""Keep only the most relevant sentences."""
sentences = split_into_sentences(document)
# Score each sentence by relevance to query
scored = []
for sent in sentences:
# Simple keyword overlap scoring
query_words = set(query.lower().split())
sent_words = set(sent.lower().split())
overlap = len(query_words & sent_words) / max(len(query_words), 1)
scored.append((sent, overlap))
# Select top sentences that fit budget
scored.sort(key=lambda x: x[1], reverse=True)
selected = []
tokens_used = 0
for sent, score in scored:
sent_tokens = estimate_tokens(sent)
if tokens_used + sent_tokens <= max_tokens:
selected.append(sent)
tokens_used += sent_tokens
return " ".join(selected)
LLM-Based Summarization
def summarize_for_context(document: str, query: str, max_tokens: int) -> str:
"""Use LLM to create a query-focused summary."""
prompt = f"""Summarize the following document, focusing on information relevant to: "{query}"
Keep the summary under {max_tokens} tokens.
Document:
{document}
Query-focused summary:"""
response = llm.invoke([HumanMessage(content=prompt)])
return response.content
History Windowing
class ConversationHistoryManager:
def __init__(self, max_tokens: int = 8000):
self.max_tokens = max_tokens
def manage_history(self, messages: list) -> list:
if estimate_tokens(messages) <= self.max_tokens:
return messages
# Strategy 1: Keep system message + recent messages
system_msgs = [m for m in messages if m.role == "system"]
other_msgs = [m for m in messages if m.role != "system"]
# Always keep first user message (original intent)
if other_msgs:
first_user = other_msgs[0]
recent = other_msgs[1:]
# Add recent messages that fit
kept = [first_user]
tokens_used = estimate_tokens(system_msgs + [first_user])
for msg in reversed(recent):
msg_tokens = estimate_tokens([msg])
if tokens_used + msg_tokens <= self.max_tokens:
kept.insert(1, msg)
tokens_used += msg_tokens
else:
break
return system_msgs + kept
return messages[-10:] # Fallback: last 10 messages
Monitoring Context Usage
class ContextBudgetMonitor:
def __init__(self):
self.metrics = []
def record(self, query: str, allocation: dict, actual_usage: dict):
self.metrics.append({
"timestamp": time.time(),
"query_length": estimate_tokens([query]),
"allocation": allocation,
"actual_usage": actual_usage,
"utilization": actual_usage.get("total", 0) / allocation.get("total", 1),
"retrieval_utilization": actual_usage.get("retrieved", 0) / max(allocation.get("retrieval", 1), 1),
})
def get_insights(self) -> dict:
recent = self.metrics[-100:]
return {
"avg_utilization": sum(m["utilization"] for m in recent) / len(recent),
"avg_retrieval_utilization": sum(m["retrieval_utilization"] for m in recent) / len(recent),
"over_budget_rate": sum(1 for m in recent if m["utilization"] > 1.0) / len(recent),
"under_budget_rate": sum(1 for m in recent if m["utilization"] < 0.5) / len(recent),
}
Context budgeting is about quality, not just fitting tokens. An agent with 80% well-chosen context outperforms one with 100% stuffed context. Budget wisely, compress aggressively, and always reserve room for the model to think.