Skip to main content
RapidDev - Software Development Agency
build-ai-agent

How to Build an Agentic RAG System

Agentic RAG means the agent decides when and how many times to retrieve, not just retrieving once before generating an answer. This guide builds a documentation Q&A agent with a search_docs tool over a Supabase pgvector table, wired into LangChain's create_agent with a system prompt that tells it to refine and re-search when the first retrieval doesn't fully answer the question. Budget 50-60 minutes and a Supabase project with pgvector enabled.

What you'll learn

  • Tell the difference between plain RAG (retrieve once, generate) and agentic RAG (retrieve, judge, maybe retrieve again)
  • Wrap a Supabase pgvector similarity search in a tool the agent calls, instead of a fixed retrieval step before the model runs
  • Write a system prompt that pushes the agent to refine and re-search instead of answering from a weak first retrieval
  • Cap retrieval rounds so an agentic RAG loop can't re-query indefinitely on an unanswerable question
Book a free consultation
4.9Clutch rating ⭐
1,000+Happy partners
20+Countries served
200+Team members
Advanced9 min read50-60 minPython 3.10+, LangChain 1.x, Supabase with pgvector enabled, Claude or OpenAI API keyLast updated November 2026RapidDev Engineering Team
TL;DR

Agentic RAG means the agent decides when and how many times to retrieve, not just retrieving once before generating an answer. This guide builds a documentation Q&A agent with a search_docs tool over a Supabase pgvector table, wired into LangChain's create_agent with a system prompt that tells it to refine and re-search when the first retrieval doesn't fully answer the question. Budget 50-60 minutes and a Supabase project with pgvector enabled.

Quick facts about this guide
FactValue
ToolAny LLM or framework
DifficultyAdvanced
Time required50-60 min
CompatibilityPython 3.10+, LangChain 1.x, Supabase with pgvector enabled, Claude or OpenAI API key
Last updatedNovember 2026

What you'll build

You'll build a documentation Q&A agent for an internal API. Ask it something the docs only partly cover in one section, and instead of answering from whatever the first search returned, it notices the gap, searches again with a refined query, and only then answers, citing which sections it actually used.

That's the whole difference agentic RAG makes over plain RAG. Plain RAG retrieves a fixed number of chunks once, stuffs them into the prompt, and generates, no judgment involved. Agentic RAG puts retrieval behind a tool call: the agent decides whether to search, what to search for, and whether the results are good enough to answer from, the same way it would decide whether to call any other tool. The retrieval logic (your embeddings, your pgvector table, your similarity function) doesn't change, what changes is who's in control of when it runs.

This guide uses LangChain's create_agent with a single search_docs tool, because the agentic behavior comes entirely from the system prompt's instructions and the model's own judgment, not from a specialized RAG framework. Reach for a heavier RAG-specific stack (LlamaIndex's FunctionAgent, a dedicated reranker, a query planner) once you need routing across multiple document sets or sub-query decomposition this single-tool pattern can't express cleanly.

Prerequisites

  • A Supabase project with the pgvector extension enabled and a documents table of embedded chunks
  • Python 3.10+ and a terminal
  • pip install langchain langgraph supabase
  • An embedding provider already used to populate the documents table (so query embeddings match its dimensions)

Step-by-step guide

1

Decide you actually need agentic RAG, not plain RAG

Plain RAG (retrieve once, generate) is simpler, cheaper, and fine when questions map cleanly to one relevant chunk, FAQ-style lookups. Agentic RAG earns its extra tool-calling turns when questions are ambiguous, span multiple sections, or a weak first retrieval is common enough that blind generation produces wrong answers often. If your docs Q&A rarely needs a second search, plain RAG is the better default.

Expected result: A specific reason ('questions here often span two sections' or similar) for taking on agentic RAG's extra complexity and cost.

2

Confirm your Supabase match function exists

Agentic RAG doesn't change your retrieval infrastructure, it changes who calls it. You need a documents table with an embedding column and a Postgres function (commonly named match_documents) that does the pgvector similarity search. If you already have this from a plain-RAG setup, skip ahead; if not, this is the standard shape.

typescript
1create or replace function match_documents(
2 query_embedding vector(1536),
3 match_count int default 5
4) returns table (id bigint, content text, section text, similarity float)
5language sql stable as $$
6 select id, content, section, 1 - (embedding <=> query_embedding) as similarity
7 from documents
8 order by embedding <=> query_embedding
9 limit match_count;
10$$;
11
12-- Remember RLS: documents likely needs a read policy, even if writes are service-role only.

Expected result: select * from match_documents(<some embedding>, 5) returns rows directly in the Supabase SQL editor.

3

Wrap retrieval in a tool the agent calls

search_docs embeds the query, calls match_documents through the Supabase client, and formats the results as text with section tags so the model can cite them. embed_query() is whatever embedding call already populated your documents table, kept as a separate function so you can swap providers without touching the tool's shape.

typescript
1from langchain.tools import tool
2from supabase import create_client
3import os
4
5supabase = create_client(os.environ["SUPABASE_URL"], os.environ["SUPABASE_SERVICE_ROLE_KEY"])
6
7def embed_query(text: str) -> list[float]:
8 # Call whichever embedding API populated your documents table.
9 ...
10
11@tool
12def search_docs(query: str, match_count: int = 5) -> str:
13 """Search the internal API docs for sections relevant to a query. Returns matching sections with content."""
14 embedding = embed_query(query)
15 res = supabase.rpc("match_documents", {"query_embedding": embedding, "match_count": match_count}).execute()
16 if not res.data:
17 return "No matching documentation found. Try a more specific or differently worded query."
18 return "\n\n".join(f"[{row['section']}] {row['content']}" for row in res.data)

Expected result: search_docs('rate limits') returns one or more [section] content blocks when called directly, no agent involved.

4

Write the system prompt that makes retrieval agentic

The tool alone doesn't make this agentic RAG, the instructions do. Tell the model explicitly to judge whether the returned chunks answer the question, to refine and search again when they don't, and to cite the sections it actually used rather than answering from memory. Cap the behavior at a fixed number of searches so it doesn't re-query indefinitely on a question the docs simply don't cover.

typescript
1SYSTEM_PROMPT = (
2 "You answer questions about our API using search_docs. Call it first. "
3 "If the results only partly answer the question, refine your query and "
4 "call search_docs again, up to 3 times total. Always cite the [section] "
5 "tags you actually used. If you still can't find the answer after 3 "
6 "searches, say so honestly instead of guessing."
7)

Expected result: A system prompt that turns a passive retrieval tool into an agent that decides when one search isn't enough.

5

Wire it into create_agent and run an ambiguous question

Pass search_docs and the system prompt into create_agent. Test with a question that genuinely spans two documentation sections, and watch the trace: you should see search_docs called once, then a second call with a refined query, before the final cited answer.

typescript
1from langchain.agents import create_agent
2
3agent = create_agent(
4 model="anthropic:claude-sonnet-5-5",
5 tools=[search_docs],
6 system_prompt=SYSTEM_PROMPT,
7)
8
9result = agent.invoke({"messages": [
10 {"role": "user", "content": "How do rate limits interact with the batch endpoint?"}
11]})
12for m in result["messages"]:
13 print(type(m).__name__, getattr(m, "content", ""))

Expected result: Two search_docs calls visible in the trace (rate limits, then batch endpoint), followed by an answer citing both sections.

6

Harden it: dedupe chunks and cap cost

Two real risks show up at scale: the same chunk coming back from two refined searches (dedupe by id before formatting results), and retrieval rounds compounding cost on every ambiguous question (embedding calls plus an extra model turn per search). Log how many searches the average question takes; if most need just one, consider tightening the prompt's threshold for when a second search is worth it.

typescript
1seen = set()
2deduped = []
3for row in res.data:
4 if row["id"] not in seen:
5 seen.add(row["id"])
6 deduped.append(row)

Expected result: A search_docs that never returns the same chunk twice across refined queries, and a rough number for average searches per question.

Complete working example

docs_rag_agent.py
1"""Agentic RAG docs Q&A agent: search_docs over Supabase pgvector, with refinement.
2Run: python docs_rag_agent.py (pip install langchain langgraph supabase; set env vars)
3"""
4import os
5
6from langchain.agents import create_agent
7from langchain.tools import tool
8from supabase import create_client
9
10supabase = create_client(os.environ["SUPABASE_URL"], os.environ["SUPABASE_SERVICE_ROLE_KEY"])
11
12
13def embed_query(text: str) -> list[float]:
14 """Call whichever embedding API populated the documents table."""
15 raise NotImplementedError("Wire this up to your embedding provider")
16
17
18@tool
19def search_docs(query: str, match_count: int = 5) -> str:
20 """Search the internal API docs for sections relevant to a query. Returns matching sections with content."""
21 embedding = embed_query(query)
22 res = supabase.rpc(
23 "match_documents", {"query_embedding": embedding, "match_count": match_count}
24 ).execute()
25 if not res.data:
26 return "No matching documentation found. Try a more specific or differently worded query."
27
28 seen = set()
29 chunks = []
30 for row in res.data:
31 if row["id"] in seen:
32 continue
33 seen.add(row["id"])
34 chunks.append(f"[{row['section']}] {row['content']}")
35 return "\n\n".join(chunks)
36
37
38SYSTEM_PROMPT = (
39 "You answer questions about our API using search_docs. Call it first. "
40 "If the results only partly answer the question, refine your query and "
41 "call search_docs again, up to 3 times total. Always cite the [section] "
42 "tags you actually used. If you still can't find the answer after 3 "
43 "searches, say so honestly instead of guessing."
44)
45
46agent = create_agent(
47 model="anthropic:claude-sonnet-5-5",
48 tools=[search_docs],
49 system_prompt=SYSTEM_PROMPT,
50)
51
52if __name__ == "__main__":
53 result = agent.invoke({"messages": [
54 {"role": "user", "content": "How do rate limits interact with the batch endpoint?"}
55 ]})
56 for m in result["messages"]:
57 print(type(m).__name__, "->", getattr(m, "content", ""))
58

Common mistakes

Why it's a problem: Calling this 'agentic RAG' when it's really one fixed retrieval step before generation

How to avoid: Agentic RAG requires the model to decide whether and how to re-query. If there's no instruction or mechanism for a second search, it's plain RAG with a tool-shaped wrapper.

Why it's a problem: Not capping the number of retrieval rounds

How to avoid: Cap it explicitly in the system prompt (and ideally enforce it in code) or an unanswerable question can trigger search after search with no exit condition.

Why it's a problem: Letting duplicate chunks from refined searches pile up in context

How to avoid: Dedupe by chunk id across all searches in a conversation before formatting results, or the model burns context re-reading the same passage.

Why it's a problem: Mismatching the query embedding's dimension against the table's stored embeddings

How to avoid: This fails at the SQL level with a dimension-mismatch error, not a helpful application error. Confirm your embedding provider matches what populated the table.

Why it's a problem: Forgetting a read policy on the documents table

How to avoid: Supabase tables are inaccessible by default without RLS policies. A service-role key bypasses RLS, but confirm that's actually the key this tool uses before assuming reads will just work.

Best practices

  • Default to plain RAG; only add agentic retrieval once you can point at real questions a single retrieval answers badly.
  • Cap retrieval rounds explicitly, both in the prompt and, if the stakes justify it, in code that counts tool calls.
  • Dedupe chunks across refined searches in the same conversation before they reach the model's context.
  • Require citations (section tags, doc IDs) in the final answer so a wrong answer is traceable to a specific retrieval.
  • Log how many searches real questions take; a high average search count is a sign your chunking or embeddings need work, not just your prompt.
  • Keep embed_query() isolated from the tool logic so swapping embedding providers doesn't touch the retrieval or agent code.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I have a LangChain agent with a search_docs tool over Supabase pgvector that refines and re-searches up to 3 times (code below). Add a second tool, search_changelog, and make the agent check it when a question mentions 'recently' or 'changed'. [paste complete_code]

Build AI Agents Prompt

In Claude Code, open docs_rag_agent.py and ask: 'Add a reranking step inside search_docs that re-orders results by keyword overlap with the query before formatting them, and cap total searches per conversation at 3 in code, not just in the prompt.'

Frequently asked questions

What's the difference between RAG and agentic RAG?

Plain RAG retrieves a fixed set of chunks once and generates from them. Agentic RAG puts retrieval behind a tool call the model decides to use, including deciding whether to search again when the first results don't fully answer the question.

Do I need a specialized RAG framework to build agentic RAG?

No. This guide builds it with a single custom tool and a system prompt on top of LangChain's create_agent. A specialized stack (LlamaIndex, a dedicated reranker) earns its complexity once you need routing across multiple document sets or multi-step query decomposition.

How many times should the agent be allowed to re-search?

Three is a reasonable default for most docs Q&A. Cap it explicitly in the prompt, and if the cost or latency matters, enforce the cap in code by counting tool calls rather than trusting the model to stop on its own.

Why does my agent sometimes answer without citing any sources?

Usually the system prompt doesn't require citations explicitly enough, or the model answered from its own training knowledge instead of calling search_docs first. State the citation requirement and the 'always search first' rule as separate, explicit instructions.

How much does an agentic RAG query cost compared to plain RAG?

Roughly the cost of one extra model turn and one extra embedding call per additional search, on top of the same per-chunk context cost plain RAG already pays. Two searches on a $2/M-input model adds a few thousand extra input tokens, typically under a cent.

Can someone build this for me instead of doing it myself?

Yes. RapidDev builds production RAG and agentic RAG systems on Supabase pgvector and LangChain, including the chunking, reranking, and citation work most tutorials skip. A free 30-minute consultation is the fastest way to scope your version.

RapidDev

Talk to an Expert

Our team has built 1,000+ apps. Get personalized help with your project.

Book a free consultation
Matt Graham

Written by

Matt Graham · CEO & Founder, RapidDev

1,000+ client projects delivered. Columbia University & Harvard Business School alumnus, U.S. Navy veteran. About the author →

Learning is great. Shipping is faster with help.

Our engineers have built 1,000+ apps on your platform and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.