Retrieval-Augmented Generation (RAG) is not one architecture. It is a family of patterns for grounding model answers in your data.
This guide walks through the five patterns you will actually meet in enterprise designs — Naive, Advanced, Modular, Agentic, and GraphRAG — using one shared business scenario, a solution architecture, and a sample implementation sketch for each.
If you only remember one rule: complexity should follow failure modes, not LinkedIn trends.
Shared scenario (used for every pattern)
Company: Northwind Mobility, a global automotive OEM.
Use case: an internal assistant for dealer support and warranty analysts.
Questions look like:
- “What is the warranty policy for battery degradation in Germany for MY2024?”
- “Which systems own VIN → vehicle config → claim status?”
- “Summarise open severity-1 issues for model Apex in the EU region.”
Knowledge sources:
- Policy PDFs & SharePoint (warranty, regional exceptions)
- Service bulletins (unstructured)
- CRM / case history (Salesforce)
- Claim status API (MuleSoft / Anypoint)
- Org & product hierarchy (who owns which region/model)
We will solve the same question across patterns:
Q: “Can we approve a goodwill battery replacement for VIN WNW123 in Bavaria, and who must sign off?”
1. Naive RAG
Linear pipeline: chunk → embed → top‑k similarity → stuff into prompt → generate.
When it fits
POC on a clean corpus (e.g. one warranty PDF library). Breaks on multi-hop questions, ACL, and “similar but wrong” chunks.
Solution architecture
┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ Ingestion │───▶│ Vector DB │◀───│ Query Embed │
│ (chunk+embed)│ │ (policies) │ │ │
└──────────────┘ └──────┬──────┘ └──────┬───────┘
│ top-k │
▼ │
┌─────────────┐ │
│ Prompt+LLM │◀──────────┘
└──────┬──────┘
▼
Answer text
Components: object storage → chunker → embedding model → vector index (e.g. pgvector / OpenSearch kNN) → chat LLM. No rerank, no hybrid search, no tools.
Example behaviour on our question
Retrieves chunks about “battery goodwill” and “Germany warranty,” but may miss Bavaria sign-off matrix and live claim status. Risk: confident wrong answer.
Sample implementation
def naive_rag(question: str, k: int = 5) -> str:
q_vec = embed(question)
chunks = vector_db.similarity_search(q_vec, k=k)
context = "\n\n".join(c.text for c in chunks)
prompt = f"""Answer using ONLY the context. If missing, say you don't know.
Context:
{context}
Question: {question}
Answer:"""
return llm.generate(prompt)
Ingest sketch:
for doc in load_pdfs("s3://nw-warranty/"):
for chunk in split(doc, size=800, overlap=120):
vector_db.upsert(id=chunk.id, vector=embed(chunk.text),
metadata={"source": doc.path, "region": doc.region})
2. Advanced RAG
Same backbone, but quality controls at every stage: better chunking, query rewrite, hybrid retrieval (BM25 + vectors), reranking, compression, citations.
When it fits
Default production pattern for knowledge Q&A. This is where most enterprise assistants should live before agents or graphs.
Solution architecture
User Q
│
▼
┌─────────────────┐
│ Query transform │ (rewrite / expand / HyDE optional)
└────────┬────────┘
▼
┌─────────────────┐ ┌──────────────┐
│ Hybrid retrieve │────▶│ Reranker │
│ BM25 + vectors │ │ (cross-enc.) │
└─────────────────┘ └──────┬───────┘
▼
┌──────────────┐
│ Compress + │
│ cite chunks │
└──────┬───────┘
▼
┌──────────────┐
│ LLM answer │
│ + citations │
└──────────────┘
Enterprise add-ons: metadata filters (region=DE), document ACL from IdP, evaluation set (groundedness / citation precision).
Example behaviour
Query rewritten to “Germany MY2024 battery goodwill approval authority Bavaria.” Hybrid search finds the regional exception table; reranker promotes the sign-off matrix chunk; answer cites policy §4.2.
Sample implementation
def advanced_rag(question: str, user_region: str) -> dict:
rewritten = llm.generate(
f"Rewrite for retrieval. Keep entities. Question: {question}"
)
dense = vector_db.similarity_search(embed(rewritten), k=20,
filter={"region": ["DE", "GLOBAL"]})
sparse = bm25_index.search(rewritten, k=20)
candidates = rrf_fuse(dense, sparse) # reciprocal rank fusion
ranked = reranker.rank(question, candidates)[:6]
context = format_with_citations(ranked)
answer = llm.generate(f"""Use context. Cite sources like [1],[2].
If policy conflicts, state the conflict.
Context:
{context}
Question: {question}""")
return {"answer": answer, "sources": ranked}
3. Modular RAG
Composable pipelines with a router. Different question types hit different indexes/tools. Feels like an integration flow — because it is.
When it fits
Multiple domains (policy vs CRM vs product), strict permissions, or different SLAs per corpus.
Solution architecture
┌──────────────────┐
│ Intent router │
│ (LLM or rules) │
└────────┬─────────┘
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Policy RAG │ │ CRM RAG │ │ Live API │
│ (Advanced) │ │ + ACL │ │ (claims) │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
└────────────────┼─────────────────┘
▼
┌────────────┐
│ Synthesizer│──▶ Answer
└────────────┘
Platform view: API gateway / MuleSoft exposes GET /claims/{vin}; RAG orchestrator is just another consumer with scoped credentials. Identity (OIDC) propagates into metadata filters.
Example behaviour
Router emits: policy + claims_api + org_matrix. Policy module returns goodwill rules; claims API returns open claim state for VIN WNW123; synthesizer merges “eligible / not eligible + approver role.”
Sample implementation
def modular_rag(question: str, user: User) -> str:
plan = router.classify(question)
# plan.routes eg: ["policy", "claims", "org"]
bundles = []
if "policy" in plan.routes:
bundles.append(advanced_rag(question, user.region))
if "claims" in plan.routes:
vin = extract_vin(question)
bundles.append({"claims": claims_api.get_status(vin, token=user.token)})
if "org" in plan.routes:
bundles.append(org_index.search("battery goodwill approver Bavaria"))
return synthesizer.merge(question, bundles, cite=True)
# Router can be rules-first for production predictability:
RULES = [
(r"VIN|claim|status", ["claims", "policy"]),
(r"who (approves|signs)|sign-off", ["org", "policy"]),
(r".*", ["policy"]),
]
4. Agentic RAG
An agent decides when and how to retrieve, can loop, call tools, and stop when evidence is enough. Retrieve is a tool — not a single forced step.
When it fits
Multi-step investigate → gather → decide workflows. Higher cost/latency; needs guardrails.
Solution architecture
┌─────────────────────────────────────────┐
│ Agent orchestrator (ReAct / plan-exec) │
│ memory · policy guardrails · budget │
└───────────────┬─────────────────────────┘
│ tool calls
┌──────────┼──────────┬────────────┐
▼ ▼ ▼ ▼
search_policy claims_api org_lookup ticket_search
│ │ │ │
└──────────┴─────┬────┴────────────┘
▼
final answer + audit trail
Controls: max steps, allow-listed tools, human escalate if confidence low, full tool-call audit log.
Example behaviour
- Agent extracts VIN + region
- Calls
claims_api - Searches policy with rewritten query
- Looks up approver role for Bavaria
- Stops with a structured recommendation + citations + next human action
Sample implementation
TOOLS = {
"search_policy": lambda q: advanced_rag(q, "DE")["sources"],
"get_claim": lambda vin: claims_api.get_status(vin),
"get_approver": lambda region, topic: org_api.approver(region, topic),
}
def agentic_rag(question: str, user: User) -> str:
state = {"question": question, "notes": [], "steps": 0}
while state["steps"] < 6:
decision = llm.plan_next_action(state, tools=TOOLS.keys())
if decision.action == "answer":
return llm.final_answer(state, decision.draft)
result = TOOLS[decision.action](**decision.args)
state["notes"].append({"tool": decision.action, "result": result})
state["steps"] += 1
return escalate_to_human(state)
# Tool contract example (OpenAPI-ish)
# POST /tools/get_claim
# body: { "vin": "WNW123" }
# auth: user-delegated OAuth (no god-mode service account)
5. GraphRAG
Build a knowledge graph (entities + relationships + communities). Retrieve by neighbourhood / community summaries — not only vector similarity. Strong when the answer is in the links.
When it fits
“Who owns what,” dependency chains, org/product hierarchies, multi-document relationship questions. Costly to build/maintain — use when Advanced RAG keeps missing relationships.
Solution architecture
Documents ──▶ Entity/Rel extract ──▶ Graph DB (Neo4j etc.)
│ │
│ ├─ local search (entity neighbourhood)
│ └─ global search (community summaries)
▼
Vector index (optional hybrid)
│
▼
Graph+text context ──▶ LLM answer
Inspired by community-summary approaches (e.g. Microsoft GraphRAG style pipelines): extract → graph → Leiden/community summaries → local/global retrieval modes.
Example behaviour
Graph paths:
(VIN WNW123)-[:IN_REGION]->(Bavaria)-[:IN_COUNTRY]->(Germany)
(BatteryGoodwill)-[:REQUIRES_APPROVAL]->(Role: Regional Warranty Lead)
(Regional Warranty Lead)-[:FILLED_BY]->(Persona / queue)
Answer names the role and why — even if those facts never co-occur in one PDF chunk.
Sample implementation
def build_graph(docs):
for doc in docs:
ents, rels = llm.extract_entities_relations(doc.text)
graph.upsert_entities(ents)
graph.upsert_relations(rels)
graph.run_community_detection()
graph.materialize_community_summaries()
def graph_rag(question: str) -> str:
mode = "local" if looks_entity_specific(question) else "global"
if mode == "local":
seeds = entity_linker.link(question)
subgraph = graph.expand(seeds, hops=2)
context = render_subgraph(subgraph)
else:
communities = graph.top_communities_for(question, k=5)
context = "\n".join(c.summary for c in communities)
return llm.generate(f"Answer from graph context:\n{context}\n\nQ:{question}")
// Cypher-ish local retrieval
MATCH (v:VIN {id:$vin})-[:IN_REGION]->(r:Region)-[:IN_COUNTRY]->(c:Country)
MATCH (p:PolicyTopic {name:'BatteryGoodwill'})-[:REQUIRES_APPROVAL]->(role:Role)
WHERE c.code = 'DE'
RETURN r.name, role.name, p.clauseRef
Side-by-side: same question, different patterns
| Pattern | What you get for the VIN goodwill question | Main risk |
|---|---|---|
| Naive | Generic warranty blurb | Misses sign-off + live claim state |
| Advanced | Cited policy answer for DE/MY2024 | Still no live claim / org path unless in docs |
| Modular | Policy + claims API + org matrix merged | Router mistakes / integration auth |
| Agentic | Multi-step investigation with audit trail | Latency, cost, runaway loops |
| GraphRAG | Relationship-native “who approves” | Graph build & drift over time |
Reference enterprise architecture (production)
┌──────────── users / IdP (SSO) ────────────┐
│ Assistant UI → Orchestration API │
└─────────────┬─────────────────────────────┘
│
┌────────┴────────┐
│ Policy engine │ (PII redaction, tool allow-list)
└────────┬────────┘
│
┌────────────┼────────────┬──────────────┐
▼ ▼ ▼ ▼
Vector/BM25 Graph DB Systems APIs Eval/Obs
indexes (optional) (CRM/Claims) (traces,
groundedness)
Non-negotiables in enterprise RAG:
- User-delegated auth to tools (no shared god account)
- Document ACLs enforced at retrieval time
- Citations + refusal when evidence is weak
- Evaluation set before you celebrate a demo
- Ownership for ingestion freshness (stale index = silent failure)
This is why RAG is an API and integration problem as much as an LLM problem.
Adoption path that works
- Naive — prove value on one trusted corpus (1–2 weeks)
- Advanced — hybrid + rerank + citations + eval harness
- Modular — split domains / add live APIs behind the router
- Agentic — only when multi-step tool use is the real job
- GraphRAG — when relationship questions keep beating vector search
Final take
Don’t ask “Which RAG is best?” Ask “Which failure is killing trust today?”
- Wrong chunk? → Advanced (hybrid + rerank)
- Wrong system? → Modular (+ APIs)
- Wrong sequence of steps? → Agentic
- Wrong relationships? → GraphRAG
Start simple. Measure groundedness. Graduate on purpose.
That is how RAG becomes a durable capability — not a weekend demo.