RAG System Design: Enterprise-Grade Retrieval-Augmented Generation Architecture
Retrieval-Augmented Generation (RAG) is one of the most important architectural patterns in enterprise AI — it enables LLMs to generate reliable, well-sourced answers grounded in enterprise private data. This article systematically covers RAG architecture design across four core pillars: document chunking strategies, vector retrieval optimization, context assembly, and generation quality assurance, with production-ready design patterns and code examples.
RAG System Design: Enterprise-Grade Retrieval-Augmented Generation Architecture
“Series: S13 AI Engineering · Article 2 | Level: Advanced | Reading Time: 18 min
#TL;DR
Retrieval-Augmented Generation (RAG) is one of the most important architectural patterns in enterprise AI — it enables LLMs to generate reliable, well-sourced answers grounded in enterprise private data. This article systematically covers RAG architecture design across four core pillars: document chunking strategies, vector retrieval optimization, context assembly, and generation quality assurance, with production-ready design patterns and code examples.
#1. Why Enterprises Need RAG
Large Language Models (LLMs) possess powerful language understanding and generation capabilities, but face three fundamental limitations in enterprise settings:
- Knowledge cutoff: Model training data has temporal boundaries and cannot access the latest information
- Knowledge gaps: Models lack enterprise private data (internal documents, business rules, customer information, etc.)
- Hallucination risk: When asked about unfamiliar topics, models "fabricate" plausible-sounding answers
RAG systematically addresses these problems by retrieving relevant documents before generation, "anchoring" model outputs to real data.
User Query → Retrieve Relevant Docs → Assemble Context → LLM Generation → Output Validation → Response
#2. RAG Architecture Overview
#2.1 Core Components
A production-grade RAG system comprises these core components:
┌─────────────────────────────────────────────────────┐
│ RAG Pipeline │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ │
│ │ Document │→ │ Chunking │→ │ Embedding│→ │Vector│ │
│ │ Ingestion │ │ Engine │ │ Service │ │ Store│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ │
│ │ Query │→ │ Retriever│→ │ Context │→ │ LLM │ │
│ │ Processor│ │ │ │ Assembler│ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────┘ │
└─────────────────────────────────────────────────────┘
#2.2 Offline and Online Pipelines
RAG systems naturally divide into two pipelines:
- Offline Pipeline (Indexing): Handles document processing, chunking, vectorization, and index building — typically runs in batch or incremental mode
- Online Pipeline (Query): Handles query processing, retrieval, context assembly, and generation — requires low latency and high availability
from dataclasses import dataclass
from enum import Enum
class PipelineMode(Enum):
OFFLINE = "offline" # Indexing pipeline
ONLINE = "online" # Query pipeline
@dataclass
class RAGConfig:
"""RAG system configuration"""
# Chunking config
chunk_size: int = 512
chunk_overlap: int = 64
chunking_strategy: str = "semantic" # semantic | fixed | recursive
# Retrieval config
top_k: int = 10
rerank_top_k: int = 5
similarity_threshold: float = 0.7
# Generation config
model_name: str = "gpt-4"
max_output_tokens: int = 2048
temperature: float = 0.1
# Quality assurance
enable_citation: bool = True
enable_hallucination_check: bool = True
enable_answer_relevance_check: bool = True
#3. Document Processing and Chunking Strategies
#3.1 Document Parsing
Enterprise documents come in diverse formats — PDF, Word, PPT, Excel, HTML, Markdown, code files, and more. Document parsing is the first step of the RAG pipeline and the most commonly underestimated.
from abc import ABC, abstractmethod
from pathlib import Path
class DocumentParser(ABC):
"""Base document parser"""
@abstractmethod
def parse(self, file_path: Path) -> list[DocumentSection]:
"""Parse document into structured sections"""
...
class PDFParser(DocumentParser):
"""PDF parser — preserves structural information"""
def parse(self, file_path: Path) -> list[DocumentSection]:
sections = []
# 1. Extract text with layout information
pages = self._extract_pages_with_layout(file_path)
for page in pages:
# 2. Detect headings, paragraphs, tables, images
elements = self._detect_elements(page)
for element in elements:
sections.append(DocumentSection(
content=element.text,
element_type=element.type, # heading, paragraph, table, etc.
page_number=page.number,
metadata={
"source": str(file_path),
"bbox": element.bounding_box,
"font_size": element.font_size,
}
))
return sections
#3.2 Chunking Strategy Comparison
Chunking is one of the most critical design decisions in a RAG system. Different strategies suit different scenarios:
| Strategy | Principle | Pros | Cons | Use Case |
|---|---|---|---|---|
| Fixed-length | Split by character/token count | Simple, predictable | May break semantics | Uniformly formatted docs |
| Recursive | Recursive split by delimiter hierarchy | Preserves paragraph structure | Uneven chunk sizes | Markdown/code |
| Semantic | Split by semantic similarity | Best semantic integrity | Higher compute cost | Knowledge base Q&A |
| Document-structure | Split by document structure (headings, paragraphs) | Preserves document hierarchy | Depends on parsing quality | Technical documents |
class SemanticChunker:
"""Semantic similarity-based chunker"""
def __init__(self, embedding_model, similarity_threshold: float = 0.5):
self.embedding_model = embedding_model
self.threshold = similarity_threshold
def chunk(self, sentences: list[str]) -> list[Chunk]:
"""Group sentences by semantic boundaries"""
if not sentences:
return []
# 1. Generate embeddings for each sentence
embeddings = self.embedding_model.encode(sentences)
# 2. Compute cosine similarity between adjacent sentences
similarities = []
for i in range(len(embeddings) - 1):
sim = cosine_similarity(embeddings[i], embeddings[i + 1])
similarities.append(sim)
# 3. Split where similarity falls below threshold
chunks = []
current_chunk_sentences = [sentences[0]]
for i, sim in enumerate(similarities):
if sim < self.threshold:
# Semantic breakpoint — create new chunk
chunks.append(Chunk(
text="\n".join(current_chunk_sentences),
metadata={"start_sentence": i - len(current_chunk_sentences) + 1}
))
current_chunk_sentences = [sentences[i + 1]]
else:
current_chunk_sentences.append(sentences[i + 1])
# Handle last chunk
if current_chunk_sentences:
chunks.append(Chunk(text="\n".join(current_chunk_sentences)))
return chunks
#3.3 Chunk Metadata Strategy
Every document chunk should carry rich metadata — this is critical for subsequent retrieval, filtering, and citation:
@dataclass
class ChunkMetadata:
"""Document chunk metadata"""
# Source information
source_document: str # Original document path/URI
document_title: str # Document title
section_title: str | None # Section heading
page_number: int | None # Page number
# Position information
chunk_index: int # Sequence number within document
total_chunks: int # Total chunks in document
char_offset_start: int # Start position in original text
char_offset_end: int # End position in original text
# Temporal information
document_created_at: str # Document creation time
document_updated_at: str # Document update time
indexed_at: str # Indexing time
# Classification information
document_type: str # Document type (policy, manual, report...)
access_level: str # Access level
department: str # Owning department
# Context window
parent_chunk_id: str | None # Parent chunk ID (for hierarchical retrieval)
sibling_chunk_ids: list[str] # Adjacent chunk IDs (for context expansion)
#4. Vector Retrieval Architecture
#4.1 Embedding Model Selection
Embedding models transform text into high-dimensional vectors. Selecting the right embedding model requires considering:
- Language support: Chinese, English, multilingual
- Dimensions: Affects storage cost and retrieval speed
- Quality: Semantic representation capability in target domain
- Speed: Inference latency and throughput
| Model | Dimensions | Multilingual | Notes |
|---|---|---|---|
| text-embedding-3-large | 3072 | Good | OpenAI latest, supports dimension truncation |
| bge-large-zh-v1.5 | 1024 | Chinese-optimized | BAAI Chinese-specialized |
| multilingual-e5-large | 1024 | Excellent | Unified multilingual representation |
| nomic-embed-text-v1.5 | 768 | Fair | Open source, locally deployable |
#4.2 Vector Index Types
class VectorIndexConfig:
"""Vector index configuration"""
@staticmethod
def hnsw_config() -> dict:
"""HNSW — high recall, high memory"""
return {
"index_type": "HNSW",
"metric_type": "COSINE",
"params": {
"M": 16, # Max connections per node
"ef_construction": 200, # Search width during construction
"ef_search": 128, # Search width during query
},
"use_case": "Real-time queries, data size < 10M",
}
@staticmethod
def ivf_pq_config() -> dict:
"""IVF_PQ — large scale, low memory"""
return {
"index_type": "IVF_PQ",
"metric_type": "L2",
"params": {
"nlist": 4096, # Number of cluster centers
"m": 16, # PQ subspaces
"nbits": 8, # Bits per subspace
"nprobe": 64, # Clusters scanned during query
},
"use_case": "Massive datasets, acceptable recall trade-off",
}
#4.3 Hybrid Retrieval Strategy
Pure vector retrieval performs poorly in certain scenarios (e.g., exact matching of product IDs, dates). Hybrid retrieval combines the strengths of vector and keyword search:
class HybridRetriever:
"""Hybrid retriever: vector + keyword"""
def __init__(self, vector_store, keyword_store, alpha: float = 0.7):
self.vector_store = vector_store
self.keyword_store = keyword_store
self.alpha = alpha # Vector score weight
def retrieve(self, query: str, top_k: int = 10) -> list[RetrievalResult]:
# 1. Vector retrieval
vector_results = self.vector_store.search(
query_embedding=self.embed(query),
top_k=top_k * 2, # Over-retrieve
)
# 2. Keyword retrieval (BM25)
keyword_results = self.keyword_store.search(
query=query,
top_k=top_k * 2,
)
# 3. Normalize scores
vector_scores = self._normalize_scores(vector_results)
keyword_scores = self._normalize_scores(keyword_results)
# 4. Weighted fusion (Reciprocal Rank Fusion)
fused = self._reciprocal_rank_fusion(
vector_results=vector_scores,
keyword_results=keyword_scores,
k=60,
)
return fused[:top_k]
def _reciprocal_rank_fusion(self, vector_results, keyword_results, k=60):
"""RRF fusion algorithm"""
scores = {}
for rank, result in enumerate(vector_results):
scores[result.id] = scores.get(result.id, 0) + 1 / (k + rank + 1)
for rank, result in enumerate(keyword_results):
scores[result.id] = scores.get(result.id, 0) + 1 / (k + rank + 1)
sorted_results = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return [RetrievalResult(id=doc_id, score=score) for doc_id, score in sorted_results]
#5. Query Processing and Enhancement
#5.1 Query Understanding
User queries are often imprecise or ambiguous. The query processing layer optimizes queries to improve retrieval quality:
class QueryProcessor:
"""Query processor"""
def __init__(self, llm_client):
self.llm = llm_client
async def process(self, raw_query: str) -> ProcessedQuery:
"""Process user query"""
# 1. Query classification
intent = await self._classify_intent(raw_query)
# 2. Query rewriting
rewritten = await self._rewrite_query(raw_query, intent)
# 3. Query expansion (generate multiple retrieval queries)
expanded_queries = await self._expand_query(rewritten)
# 4. Extract filter conditions
filters = await self._extract_filters(raw_query)
return ProcessedQuery(
original=raw_query,
rewritten=rewritten,
expanded=expanded_queries,
intent=intent,
filters=filters,
)
async def _expand_query(self, query: str) -> list[str]:
"""HyDE: Generate hypothetical documents as retrieval queries"""
prompt = f"""Given the question: "{query}"
Generate 3 different versions of this question that capture different aspects
and would help retrieve relevant documents. Return as JSON array."""
response = await self.llm.complete(prompt)
return json.loads(response)
#5.2 Multi-Hop Retrieval
Complex questions require combining information from multiple documents. Multi-hop retrieval iteratively collects needed context:
class MultiHopRetriever:
"""Multi-hop retriever for complex cross-document queries"""
def __init__(self, retriever, llm, max_hops: int = 3):
self.retriever = retriever
self.llm = llm
self.max_hops = max_hops
async def retrieve(self, query: str) -> list[RetrievalResult]:
collected_context = []
current_query = query
for hop in range(self.max_hops):
# 1. Retrieve documents for current query
results = self.retriever.retrieve(current_query, top_k=5)
collected_context.extend(results)
# 2. Assess whether more retrieval is needed
assessment = await self._assess_completeness(
query, collected_context
)
if assessment.is_sufficient:
break
# 3. Generate follow-up query
current_query = await self._generate_followup_query(
original_query=query,
current_context=collected_context,
missing_info=assessment.missing_information,
)
return self._deduplicate(collected_context)
#6. Context Assembly and Prompt Engineering
#6.1 Context Window Management
LLM context windows are limited (even 128K models need space reserved for output). Context assembly requires careful arrangement of retrieved chunks:
class ContextAssembler:
"""Context assembler"""
def __init__(self, max_context_tokens: int = 8000):
self.max_tokens = max_context_tokens
def assemble(
self,
query: str,
retrieved_chunks: list[Chunk],
system_prompt: str,
) -> AssembledContext:
"""Assemble LLM input context"""
# 1. Already sorted by relevance from retriever
# 2. Deduplicate and merge adjacent chunks
merged = self._merge_adjacent_chunks(retrieved_chunks)
# 3. Token budget allocation
system_tokens = self._count_tokens(system_prompt)
query_tokens = self._count_tokens(query)
available = self.max_tokens - system_tokens - query_tokens - 200 # Buffer
# 4. Greedy filling (prioritize high-relevance chunks)
selected_chunks = []
used_tokens = 0
for chunk in merged:
chunk_tokens = self._count_tokens(chunk.text)
if used_tokens + chunk_tokens <= available:
selected_chunks.append(chunk)
used_tokens += chunk_tokens
# 5. Re-sort by original document order (maintain reading coherence)
selected_chunks.sort(key=lambda c: (c.metadata.source_document, c.metadata.chunk_index))
return AssembledContext(
chunks=selected_chunks,
total_tokens=used_tokens,
coverage_ratio=len(selected_chunks) / len(retrieved_chunks),
)
#6.2 Prompt Template Design
RAG_PROMPT_TEMPLATE = """You are an enterprise knowledge assistant. Answer the user's question
based on the following retrieved document excerpts.
## Rules
1. Answer ONLY based on the provided documents — do not use your training knowledge
2. If documents lack sufficient information, explicitly state "Based on available documents,
I cannot answer this question"
3. Every key assertion must cite the source document: [Source: document name, page]
4. If different documents contain contradictory information, note the contradiction
and present each document's position
## Retrieved Documents
{context}
## User Question
{query}
## Answer
"""
#7. Generation Quality Assurance
#7.1 Hallucination Detection
class HallucinationDetector:
"""Verifies whether generated content is supported by source documents"""
def __init__(self, nli_model):
self.nli_model = nli_model # Natural Language Inference model
def detect(self, generated_answer: str, source_chunks: list[Chunk]) -> list[HallucinationFlag]:
flags = []
# Split answer into independent claims
claims = self._extract_claims(generated_answer)
for claim in claims:
# Check each claim against source documents
support_scores = []
for chunk in source_chunks:
score = self.nli_model.predict_entailment(
premise=chunk.text,
hypothesis=claim.text,
)
support_scores.append(score)
max_support = max(support_scores) if support_scores else 0
if max_support < 0.5:
flags.append(HallucinationFlag(
claim=claim.text,
confidence=1 - max_support,
suggestion="This claim lacks sufficient support in source documents",
))
return flags
#7.2 Answer Relevance Evaluation
class AnswerRelevanceEvaluator:
"""Evaluates relevance of generated answer to user question"""
async def evaluate(self, query: str, answer: str) -> float:
"""Returns a 0-1 relevance score"""
prompt = f"""Rate the relevance of the following answer to the question.
Score from 0 (completely irrelevant) to 1 (perfectly relevant).
Question: {query}
Answer: {answer}
Return only the numeric score."""
score = float(await self.llm.complete(prompt))
return min(max(score, 0), 1)
#7.3 Citation Verification
Every citation should be traceable to a specific source document and location — this is a hard requirement for enterprise RAG:
class CitationVerifier:
"""Citation verifier"""
def verify(self, answer: str, chunks: list[Chunk]) -> VerificationResult:
citations = self._extract_citations(answer)
verified = []
unverified = []
for citation in citations:
match = self._find_in_chunks(citation.text, chunks)
if match and match.similarity > 0.85:
verified.append(VerifiedCitation(
citation=citation,
source_chunk=match.chunk,
similarity=match.similarity,
))
else:
unverified.append(citation)
return VerificationResult(
verified=verified,
unverified=unverified,
verification_rate=len(verified) / len(citations) if citations else 1.0,
)
#8. Production Deployment Considerations
#8.1 Performance Optimization
- Embedding caching: Cache embedding vectors for common queries
- Index warm-up: Load hot document indices into memory
- Async pipelines: Execute document processing and indexing asynchronously
- Batch inference: Batch embedding computation to leverage GPU parallelism
#8.2 Fault Tolerance Design
- Retrieval degradation: Automatically fall back to keyword search when vector retrieval times out
- Model degradation: Switch to backup model when primary model is unavailable
- Cache fallback: Return cached similar historical answers
- Circuit breaking: Prevent cascading failure propagation
#8.3 Monitoring Metrics
| Category | Metric | Target |
|---|---|---|
| Latency | End-to-end P95 | < 3s |
| Quality | Answer relevance | > 0.85 |
| Quality | Hallucination rate | < 5% |
| Quality | Citation accuracy | > 95% |
| Availability | System uptime | 99.9% |
| Cost | Per-query cost | < $0.05 |
#9. Advanced Patterns
#9.1 Agentic RAG
Combining RAG with agent capabilities, enabling the system to autonomously decide when to retrieve, what to retrieve, and how to combine information:
class AgenticRAG:
"""Agent-enhanced RAG system"""
def __init__(self, retriever, llm, tools):
self.retriever = retriever
self.llm = llm
self.tools = tools # Calculator, database query, API calls, etc.
async def answer(self, query: str) -> AgentResponse:
plan = await self._plan(query)
results = []
for step in plan.steps:
if step.action == "retrieve":
docs = self.retriever.retrieve(step.query)
results.append(("context", docs))
elif step.action == "compute":
output = await self.tools.execute(step.tool, step.args)
results.append(("computation", output))
elif step.action == "synthesize":
answer = await self._synthesize(query, results)
results.append(("answer", answer))
return AgentResponse(answer=results[-1][1], trace=results)
#9.2 GraphRAG
Combining knowledge graphs for structured reasoning, particularly suited for scenarios requiring relationship reasoning.
#9.3 Adaptive RAG
Dynamically adjusting retrieval strategies and model selection based on query complexity, achieving optimal balance between quality and cost.
#Key Takeaways
- RAG is the core architecture pattern for enterprise AI — Anchoring generation through retrieval systematically addresses LLM knowledge gaps and hallucination
- Chunking strategy determines retrieval quality — Semantic > recursive > fixed-length, but flexibility is needed based on document type
- Hybrid retrieval is the production best practice — Vector retrieval for semantic matching, keyword retrieval for exact matching
- Query processing matters — Query rewriting and expansion can significantly improve retrieval recall
- Quality assurance is an enterprise hard requirement — Hallucination detection, citation verification, and answer relevance evaluation are all essential
- Production deployment requires comprehensive fault tolerance and monitoring — Optimize across latency, quality, and cost dimensions simultaneously
#Next Article
The next article S13-03: Embedding Pipeline will dive deep into embedding model selection, fine-tuning, deployment, and pipeline operations — the core infrastructure of any RAG system.
Tags: #RAG #RetrievalAugmentedGeneration #LLM #VectorSearch #DocumentChunking #HallucinationDetection #EnterpriseAI #AIArchitecture