Skip to content
Blog

Agentic Retrieval: Letting the Agent Decide What to Fetch

Move beyond static RAG with agentic retrieval. Let agents decide what to search, when to stop, and how to combine information from multiple sources.

Published on September 8, 2026

AI Assistant

Traditional RAG is a two-step process: retrieve then generate. But real research isn’t that simple. You search, read, realize you need different terms, search again, find a contradiction, dig deeper. Agentic retrieval gives agents this same iterative, adaptive capability — letting them decide what to fetch, evaluate what they found, and search again if needed.

From Static to Agentic RAG

Static RAG (The Old Way)

User Query → Embed Query → Vector Search → Top-K Results → LLM Generate

Problems:

  • One-shot retrieval misses relevant context
  • No ability to refine searches based on initial results
  • Can’t combine multiple retrieval strategies
  • Fails when the answer requires synthesizing across sources

Agentic RAG (The New Way)

User Query

Agent: What do I need to know?
    ├── Search 1: Vector similarity
    ├── Search 2: Keyword search (if vector fails)
    ├── Search 3: Structured query (if data is tabular)

Agent: Did I find enough?
    ├── Yes → Generate answer
    └── No → Refine query, search again

Implementation with LlamaIndex

Setting Up the Agent

from llama_index.core.agent import ReActAgent
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from llama_index.core import (
    VectorStoreIndex,
    SimpleKeywordTableIndex,
    Settings,
)
from llama_index.llms.openai import OpenAI

Settings.llm = OpenAI(model="gpt-4o")

# Create multiple retrieval indexes
vector_index = VectorStoreIndex.from_documents(documents)
keyword_index = SimpleKeywordTableIndex.from_documents(documents)

# Create query engines
vector_engine = vector_index.as_query_engine(
    similarity_top_k=5,
    response_mode="compact"
)

keyword_engine = keyword_index.as_query_engine(
    response_mode="compact"
)

Define Retrieval Tools

Give the agent multiple retrieval strategies:

from llama_index.core.tools import FunctionTool

def vector_search(query: str) -> str:
    """Search using semantic similarity. Best for conceptual queries."""
    response = vector_engine.query(query)
    return str(response)

def keyword_search(query: str) -> str:
    """Search using exact keyword matching. Best for specific terms, names, IDs."""
    response = keyword_engine.query(query)
    return str(response)

def combined_search(query: str) -> str:
    """Search using both semantic and keyword approaches. Most comprehensive."""
    vector_response = vector_engine.query(query)
    keyword_response = keyword_engine.query(query)
    
    return f"""Vector Results:
{vector_response}

Keyword Results:
{keyword_response}"""

# Wrap as tools
retrieval_tools = [
    FunctionTool.from_defaults(
        fn=vector_search,
        name="vector_search",
        description="Semantic search for conceptual queries"
    ),
    FunctionTool.from_defaults(
        fn=keyword_search,
        name="keyword_search",
        description="Keyword search for specific terms"
    ),
    FunctionTool.from_defaults(
        fn=combined_search,
        name="combined_search",
        description="Comprehensive search using both methods"
    ),
]

Create the Agentic Retriever

agent = ReActAgent.from_tools(
    tools=retrieval_tools,
    llm=Settings.llm,
    verbose=True,
    max_iterations=10,
    system_prompt="""You are a research agent that retrieves information from documents.

Your strategy:
1. Start with vector_search for conceptual understanding
2. Use keyword_search for specific terms, names, or IDs
3. Use combined_search when you need comprehensive coverage
4. If initial results are insufficient, refine your query and search again
5. Synthesize findings from multiple searches into a coherent answer

Always explain your reasoning for which search strategy you choose."""
)

# Use the agent
response = agent.chat("What are the key differences between LangChain and LlamaIndex?")

Advanced Agentic Retrieval Patterns

Multi-Hop Retrieval

When answers require information from multiple documents:

class MultiHopRetriever:
    def __init__(self, agent, max_hops: int = 3):
        self.agent = agent
        self.max_hops = max_hops
    
    def retrieve(self, query: str) -> str:
        context = []
        current_query = query
        
        for hop in range(self.max_hops):
            # Retrieve for current query
            results = self.agent.chat(
                f"Search for: {current_query}\n"
                f"Context so far: {' '.join(context)}"
            )
            
            context.append(str(results))
            
            # Ask agent if more information is needed
            follow_up = self.agent.chat(
                f"Based on what you found, is there a follow-up "
                f"question that would help answer: {query}?\n"
                f"If yes, provide the follow-up question. "
                f"If no, respond with 'DONE'."
            )
            
            if "DONE" in str(follow_up).upper():
                break
            
            current_query = str(follow_up)
        
        # Synthesize all context
        final = self.agent.chat(
            f"Original question: {query}\n"
            f"Information gathered across {len(context)} searches:\n"
            + "\n---\n".join(context) +
            "\n\nProvide a comprehensive answer."
        )
        
        return str(final)

Query Rewriting

Let the agent reformulate queries when initial searches fail:

class AdaptiveRetriever:
    def __init__(self, vector_engine, llm):
        self.vector_engine = vector_engine
        self.llm = llm
    
    def retrieve_with_adaptation(
        self,
        query: str,
        max_retries: int = 3,
        relevance_threshold: float = 0.7
    ) -> str:
        current_query = query
        
        for attempt in range(max_retries):
            results = self.vector_engine.query(current_query)
            
            # Evaluate relevance
            relevance = self._evaluate_relevance(str(results), query)
            
            if relevance >= relevance_threshold:
                return str(results)
            
            # Rewrite query for next attempt
            current_query = self._rewrite_query(
                query, current_query, str(results), attempt
            )
        
        return f"Best results after {max_retries} attempts: {str(results)}"
    
    def _rewrite_query(
        self, original: str, current: str, results: str, attempt: int
    ) -> str:
        rewrite_prompt = f"""The following search query didn't return relevant results.

Original question: {original}
Current query: {current}
Attempt: {attempt + 1}

Results found:
{results[:500]}

Rewrite the search query to be more effective. Consider:
- Using different terminology
- Breaking into smaller concepts
- Adding specific context from the results"""
        
        response = self.llm.complete(rewrite_prompt)
        return str(response).strip()

Context Budgeting

Manage token budgets across multiple retrievals:

class ContextBudgetedRetriever:
    def __init__(self, total_budget: int = 8000):
        self.total_budget = total_budget
        self.used_budget = 0
    
    def retrieve_within_budget(
        self,
        retrieval_functions: list[callable],
        query: str
    ) -> str:
        results = []
        
        for func in retrieval_functions:
            remaining = self.total_budget - self.used_budget
            
            if remaining <= 0:
                break
            
            # Estimate token usage
            result = func(query)
            estimated_tokens = len(result.split()) * 1.3  # Rough estimate
            
            if estimated_tokens <= remaining:
                results.append(result)
                self.used_budget += estimated_tokens
        
        return "\n---\n".join(results)

Evaluation Metrics

Measure agentic retrieval quality:

@dataclass
class RetrievalMetrics:
    query_count: int
    total_retrieved: int
    relevant_retrieved: int
    irrelevant_retrieved: int
    answer_quality: float  # 0-1
    
    @property
    def precision(self) -> float:
        if self.total_retrieved == 0:
            return 0.0
        return self.relevant_retrieved / self.total_retrieved
    
    @property
    def recall(self) -> float:
        # Would need ground truth
        pass
    
    @property
    def efficiency(self) -> float:
        """Lower is better - ratio of queries needed."""
        return self.query_count / max(self.total_retrieved, 1)

def evaluate_agentic_retrieval(
    agent,
    test_cases: list[dict]
) -> RetrievalMetrics:
    metrics = {"queries": 0, "retrieved": 0, "relevant": 0}
    
    for case in test_cases:
        response = agent.chat(case["question"])
        # Count retrievals and evaluate relevance
        # ... (implementation depends on your tracing setup)
    
    return RetrievalMetrics(**metrics)

When to Use Agentic Retrieval

Use it when:

  • Queries are complex and require multiple searches
  • The information spans multiple documents or sources
  • Initial retrieval often misses relevant context
  • You need to synthesize across different data types

Skip it when:

  • Queries are simple and direct
  • A single retrieval step suffices
  • Latency is critical (agentic retrieval is slower)
  • Costs need to be minimized

Conclusion

Agentic retrieval transforms RAG from a static pipeline into an adaptive research process. By letting agents decide what to search, when to refine, and how to combine results, you get answers that static retrieval simply can’t produce. Start with a ReAct agent and multiple retrieval tools, add multi-hop capabilities for complex queries, and measure quality with retrieval-specific metrics.