n4nAI

How to build an agentic RAG pipeline step by step

Step-by-step tutorial to build agentic RAG pipeline with routing, retrieval, and tool use. Runnable Python code and expected outputs included.

n4n Team2 min read528 words

Audio narration

Coming soon — every post will get a voice note here.

Most RAG demos break because they treat retrieval as a single fixed step before the model speaks. To build agentic RAG pipeline that routes queries, rewrites them, and calls tools inside a reasoning loop, you need the LLM to control retrieval calls. This tutorial implements a minimal but production-shaped version in Python.

Prerequisites

  • Python 3.10+ and pip install openai chromadb.
  • An OpenAI-compatible endpoint. If you want one key for 240+ models with automatic fallback when a provider is degraded, point the SDK at n4n.ai’s gateway.
  • A folder ./docs with a few .txt files to index.

We use Chroma’s built-in embedding model to avoid extra services. Swap in your own embeddings later.

1. Ingest documents into a vector store

Create a local Chroma collection and load text files. Keep chunking dumb simple: split on double newlines.

import chromadb, os, glob

client = chromadb.PersistentClient(path="./chroma")
coll = client.get_or_create_collection("docs")

paths = glob.glob("./docs/*.txt")
for i, p in enumerate(paths):
    text = open(p).read()
    chunks = [c for c in text.split("\n\n") if c.strip()]
    coll.add(
        ids=[f"{i}-{j}" for j in range(len(chunks))],
        documents=chunks,
        metadatas=[{"source": p}] * len(chunks),
    )
print("indexed", coll.count(), "chunks")

Expected output after running:

indexed 42 chunks

2. Define the retrieval tool

The agent calls this via function calling. Return concatenated matches with source tags so the model can cite.

def retrieve(query: str, top_k: int = 3) -> str:
    res = coll.query(query_texts=[query], n_results=top_k)
    docs = res["documents"][0]
    metas = res["metadatas"][0]
    out = []
    for d, m in zip(docs, metas):
        out.append(f"[{m['source']}] {d}")
    return "\n---\n".join(out)

Test it directly:

print(retrieve("what is the refund policy?"))

Expected shape:

[./docs/terms.txt] Refunds are issued within 30 days of purchase...
---
[./docs/faq.txt] If your item is defective, email support@...

3. Build the agent loop

Define the tool schema and run a loop: send messages, if the model emits tool_calls, execute them and feed results back. Stop when the model replies with text.

from openai import OpenAI
import json

client = OpenAI(base_url="https://api.n4n.ai/v1", api_key=os.environ["API_KEY"])

tools = [{
    "type": "function",
    "function": {
        "name": "retrieve",
        "description": "Search internal docs for a query",
        "parameters": {
            "type": "object",
            "properties": {"query": {"type": "string"}},
            "required": ["query"],
        },
    },
}]

system = "You are a research agent. Use retrieve to answer from internal docs. Cite sources."

def run_agent(question: str, max_steps: int = 5):
    msgs = [{"role": "system", "content": system},
            {"role": "user", "content": question}]
    for _ in range(max_steps):
        resp = client.chat.completions.create(
            model="gpt-4o-mini", messages=msgs, tools=tools)
        msg = resp.choices[0].message
        if not msg.tool_calls:
            return msg.content
        msgs.append(msg)
        for tc in msg.tool_calls:
            args = json.loads(tc.function.arguments)
            result = retrieve(args["query"])
            msgs.append({"role": "tool", "tool_call_id": tc.id,
                         "content": result})
    return "Agent exceeded step budget"

This is the core of how you build agentic RAG pipeline: the model decides when to fetch, not your glue code.

4. Add routing and query rewriting

A single retrieve call is rarely enough. Agentic behavior emerges when the model issues multiple calls with rephrased queries. Force that by tightening the system prompt and adding a second stub tool for external lookup.

tools.append({
    "type": "function",
    "function": {
        "name": "web_search",
        "description": "Search the public web",
        "parameters": {
            "type": "object",
            "properties": {"query": {"type": "string"}},
            "required": ["query"],
        },
    },
})

def web_search(query: str) -> str:
    # stub: replace with real API
    return f"Web results for '{query}' (not implemented)"

Now the agent can route between internal and external knowledge. In practice, the model will call retrieve with a decomposed sub-question, then web_search if the docs are silent.

To make rewriting explicit, log the arguments:

for tc in msg.tool_calls:
    print("TOOL:", tc.function.name, tc.function.arguments)

You’ll see output like:

TOOL: retrieve {"query": "refund timeframe for defective items"}
TOOL: web_search {"query": "EU consumer protection refund 2024"}

That transcript is your evidence the pipeline is agentic, not a linear retriever-generator.

5. Run end-to-end

Wire a CLI entry point and inspect the full exchange.

if __name__ == "__main__":
    ans = run_agent("How do I get a refund for a broken widget bought last week?")
    print("ANSWER:", ans)

Sample answer (truncated):

ANSWER: According to ./docs/terms.txt, refunds are issued within 30 days of purchase.
Since you bought it last week, email support@acme.com with your order number [source: faq.txt].

If the agent hit the web tool, you’d see combined citations. The loop terminated because the final message had no tool_calls.

6. Production hardening

Set max_steps to bound cost. Stream responses to avoid blocking the user: pass stream=True and iterate resp. Use a gateway that honors client routing directives and forwards provider cache-control hints—n4n.ai does this, so your retrieval prompt prefix can be cached by the upstream provider when you send cache_control markers.

Per-token usage metering matters when the agent self-loops. Capture resp.usage on each call and sum it:

usage = resp.usage
total_tokens += usage.total_tokens

Add a retry with exponential backoff around client.chat.completions.create. When a provider is rate-limited, an inference gateway with automatic fallback switches models silently; your code just sees a successful completion.

Finally, persist msgs to reconstruct sessions. Agentic RAG is stateful—the second question benefits from the first retrieval.

Where to take it further

Replace Chroma with a managed vector DB when docs exceed a few GB. Swap gpt-4o-mini for a cheaper model on simple rewrites and a stronger one on synthesis. The loop you built is the same primitive used in larger orchestration frameworks; you now know exactly what they hide.

Keep the tool contracts strict, log every tool_call, and you can build agentic RAG pipeline that survives contact with real documents.

Tagsagentic-ragragtutorialretrieval

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All agentic rag posts →