Agentic RAG
Classic RAG is one retrieval, one generation. Agentic RAG lets the model decide when to retrieve, what to retrieve, whether the result is good enough, and whether to try again — a control loop wrapped around the same primitives.
Why move past one-shot RAG
A single retrieval can't handle multi-hop questions ("Who succeeded the CEO of the company that bought Whole Foods?" needs two lookups), can't recover from a bad retrieval, and can't decide that the question doesn't need retrieval at all. Agentic RAG adds three missing capabilities: plan, judge, and retry.
Minimal agentic loop
def agentic_answer(question, max_steps=4):
notes = [] # accumulated context
for step in range(max_steps):
plan = llm.complete(PLAN_PROMPT.format(q=question, notes=notes))
if plan["action"] == "ANSWER":
return llm.complete(ANSWER_PROMPT.format(q=question, notes=notes))
if plan["action"] == "SEARCH":
hits = vector_db.search(embed(plan["query"]), k=5)
judge = llm.complete(JUDGE_PROMPT.format(q=plan["query"], hits=hits))
if judge["useful"]:
notes.extend(hits)
# else: loop, try a different sub-query
return llm.complete(ANSWER_PROMPT.format(q=question, notes=notes))max_steps, cache retrievals, and fall back to single-shot for easy queries (route by question complexity).CRAG — Corrective RAG
CRAG (Yan et al., 2024) adds a lightweight retrieval evaluator that tags each retrieval as correct, ambiguous, or incorrect:
The key insight: don't trust the vector store blindly. When the index doesn't actually contain the answer, returning the top-k anyway leads the LLM into a confident hallucination. A cheap classifier that rejects bad retrievals is one of the highest-leverage additions you can make.
Self-RAG — let the model decide when to retrieve
Self-RAG (Asai et al., 2023) trains the LLM to emit special reflection tokens: [Retrieve] when it wants context, [IsRel] to rate each retrieved passage, [IsSup] to mark whether its generation is supported by the evidence, and [IsUse] for overall usefulness. Retrieval happens on demand instead of at every turn, and the model can choose to ignore irrelevant context.
# Conceptual decoding loop (Self-RAG)
tokens = []
while not done:
next_tok = model.generate(prompt + tokens)
if next_tok == "[Retrieve]":
hits = vector_db.search(embed(current_subquery(tokens)), k=3)
rated = [model.rate(h, current_subquery(tokens)) for h in hits]
tokens.extend(format(rated))
else:
tokens.append(next_tok)Multi-hop with query decomposition
For genuinely multi-step questions, decompose first, retrieve per sub-question, then synthesize. This is what powers most "deep research" features in production.
DECOMPOSE = """Break this question into 2–4 atomic sub-questions whose
answers, combined, fully answer the original. Output JSON list.
Q: {q}"""
def multi_hop(q):
subs = json.loads(llm.complete(DECOMPOSE.format(q=q)))
evidence = {s: vector_db.search(embed(s), k=3) for s in subs}
return llm.complete(SYNTHESIS_PROMPT.format(q=q, evidence=evidence))Quick Quiz
Test yourself · 3 questionsWhat's the main downside of an agentic RAG loop vs. single-shot RAG?
What does CRAG's retrieval evaluator try to prevent?
What's distinctive about Self-RAG?