How to Implement a Modular RAG System with Pluggable Components: A Complete Guide
A modular RAG system with pluggable components separates retrieval, processing, and generation into interchangeable layers bound by prompt contracts and protocol orchestration, enabling dynamic adaptation and independent scaling of each pipeline stage.
Implementing a modular retrieval-augmented generation (RAG) architecture requires moving beyond monolithic pipelines. In the davidkimai/context-engineering repository, the framework decomposes RAG into three independent layers—prompt templates, programming components, and protocol orchestration—that communicate through strict contracts. This design allows teams to swap retrieval algorithms, upgrade LLM prompts, or change orchestration logic without rewriting downstream code.
The Three-Layer Architecture of a Modular RAG System
The architecture defined in 00_COURSE/04_retrieval_augmented_generation/01_modular_architectures.md organizes functionality into distinct layers that can be developed, versioned, and deployed separately.
Prompt Templates Layer (Communication)
The prompt layer defines component interface contracts—structured prompt strings, error-handling templates, and metric specifications that dictate how layers communicate. Each component must honor these contracts to ensure interchangeability. According to the source specification, this layer includes versioned input-schema and output-schema definitions that validate data passing between retrieval and generation stages.
Programming Components Layer (Implementation)
This layer contains the concrete code executing retrieval, filtering, ranking, and synthesis. All components inherit from BaseRAGComponent, which standardizes the process pipeline, input validation, and metric recording. The source code in 00_COURSE/04_retrieval_augmented_generation/01_modular_architectures.md lines 93-118 defines this base class, while specialized implementations like AdaptiveRAGComponent and BasicGraphRAG extend it for specific retrieval strategies.
Protocol Orchestration Layer (Coordination)
The protocol layer describes workflows that wire prompts and components together, handling discovery, resource allocation, and fault tolerance. Protocol definitions such as /rag.component.basic{…} and /rag.component.adaptive{…} specify execution chains declaratively. This allows the orchestrator to dynamically select and sequence components without hard-coding dependencies.
Implementing Base RAG Components
All pluggable components in this modular RAG system derive from BaseRAGComponent, which enforces consistent interfaces and observability.
class BaseRAGComponent:
"""Foundation class for all RAG components"""
def __init__(self, config, prompt_templates):
self.config = config
self.templates = prompt_templates
self.metrics = ComponentMetrics()
def process(self, input_data):
validated_input = self.validate_input(input_data)
processed_result = self.execute(validated_input)
formatted_output = self.format_output(processed_result)
self.metrics.record_execution(input_data, formatted_output)
return formatted_output
As implemented in 00_COURSE/04_retrieval_augmented_generation/01_modular_architectures.md, the process method standardizes four phases: input validation, execution, output formatting, and metrics recording. This ensures that any component swapping maintains consistent observability and error-handling behavior.
Building Adaptive RAG Components for Dynamic Optimization
For scenarios requiring runtime optimization, AdaptiveRAGComponent extends the base class to enable context-aware strategy selection.
class AdaptiveRAGComponent(BaseRAGComponent):
"""Self-optimizing RAG component with context awareness"""
def __init__(self, config, prompt_templates, performance_history):
super().__init__(config, prompt_templates)
self.performance_history = performance_history
self.strategy_selector = StrategySelector(performance_history)
def execute(self, validated_input):
# Select strategy based on query complexity and latency constraints
strategy = self.strategy_selector.select_strategy(
query=validated_input,
constraints=self.config.get('constraints', {})
)
return strategy.execute(validated_input)
According to the source in 00_COURSE/04_retrieval_augmented_generation/01_modular_architectures.md lines 81-112, this component analyzes the query, predicts component performance, and dynamically selects execution strategies. If quality thresholds are not met, the orchestrator can trigger fallback mechanisms or re-route to alternative components without interrupting the pipeline.
Protocol-Based Orchestration
The modular RAG system uses declarative protocols to wire components together, enabling dynamic discovery and execution.
Basic Orchestration Protocol
The /rag.component.basic{…} protocol defines a linear execution chain where each component receives the previous output as its input.
/rag.component.basic{
intent="Coordinate basic RAG component execution",
input={ query="<user_query>", component_chain=["retriever","processor","generator"] },
process=[
/component.execute{
for_each="component in component_chain",
action="execute component with previous output as input",
error_handling="fallback_to_default_component"
}
],
output={ final_result="<processed_output>", execution_trace="<component_execution_log>" }
}
As specified in 00_COURSE/04_retrieval_augmented_generation/01_modular_architectures.md lines 122-144, this protocol handles resource allocation and fault tolerance through explicit error-handling directives.
Adaptive Orchestration Protocol
The advanced protocol (/rag.component.adaptive{…}) analyzes queries and predicts component performance to dynamically build execution pipelines, falling back or re-routing if quality thresholds are not met【/00_COURSE/04_retrieval_augmented_generation/01_modular_architectures.md#L140-L170】.
Extending with Graph-Enhanced RAG
The modular architecture accommodates specialized retrievers like knowledge-graph modules without modifying core orchestration logic.
The BasicGraphRAG class, defined in 00_COURSE/04_retrieval_augmented_generation/03_graph_enhanced_rag.md lines 18-45, plugs into the same component interface:
class BasicGraphRAG:
"""Foundation graph-enhanced RAG with basic relationship awareness"""
def __init__(self, knowledge_graph, text_corpus, graph_templates):
self.knowledge_graph = knowledge_graph
self.text_corpus = text_corpus
self.templates = graph_templates
self.entity_linker = EntityLinker()
self.graph_navigator = GraphNavigator()
def process_query(self, query):
entities = self.entity_linker.extract_entities(query)
graph_context = self.graph_navigator.explore_subgraph(entities)
return self.templates.render_response(query, graph_context)
Graph-aware prompt templates (lines 70-92 of the same file) define how the graph module communicates with the orchestrator, ensuring compatibility with the broader modular RAG system.
Practical Implementation Examples
Example 1: Assemble a Basic Modular RAG Pipeline
from modular_architecture import BaseRAGComponent, AdaptiveRAGComponent
# Load prompt contracts from markdown specification
prompt_templates = load_prompt_templates("component_interface_template.md")
# Instantiate concrete components
retriever = BaseRAGComponent(config=vec_cfg, prompt_templates=prompt_templates)
processor = AdaptiveRAGComponent(config=proc_cfg,
prompt_templates=prompt_templates,
performance_history=hist)
# Simple orchestrator implementing /rag.component.basic protocol
def run_basic_rag(query):
step1 = retriever.process({"query": query})
step2 = processor.process(step1)
# Generator component follows same pattern
return step2
Example 2: Integrate Graph-Enhanced Retrieval
from graph_enhanced_rag import BasicGraphRAG
# Initialize graph module with same prompt contract
graph_rag = BasicGraphRAG(knowledge_graph=my_graph,
text_corpus=my_corpus,
graph_templates=load_templates("graph_query_template.md"))
def run_graph_rag(query):
# Returns formatted output compatible with processor components
return graph_rag.process_query(query)
Example 3: Adaptive Orchestrator with Runtime Selection
def adaptive_orchestrator(query, context):
# Implements /rag.component.adaptive protocol logic
if "entity" in query.lower():
component = graph_rag
else:
component = retriever
# Unified interface allows interchangeable execution
result = component.process(query if hasattr(component, "process") else {"query": query})
return result
Summary
- Three-layer architecture separates prompt contracts, programming components, and protocol orchestration, enabling independent development and scaling of retrieval, processing, and generation stages.
- BaseRAGComponent enforces standardized interfaces for input validation, execution, and metrics, ensuring all pluggable components maintain consistent observability.
- Protocol definitions (
/rag.component.basicand/rag.component.adaptive) provide declarative workflow specifications that handle component discovery, chaining, and fault tolerance without hard-coded dependencies. - Graph-enhanced modules like
BasicGraphRAGdemonstrate how specialized retrievers integrate into the same modular RAG system by adhering to shared prompt contracts and the base component interface.
Frequently Asked Questions
What makes a RAG system "modular"?
A modular RAG system decomposes the pipeline into independent layers—prompt templates, programming components, and protocol orchestration—where each component implements a standardized interface (typically inheriting from BaseRAGComponent). This modularity allows any retrieval, processing, or generation module to be swapped, scaled, or updated without modifying dependent code, provided the new component honors the shared prompt contract and protocol definitions.
How does protocol orchestration differ from hard-coded pipelines?
Protocol orchestration uses declarative specifications (such as /rag.component.basic{…} or /rag.component.adaptive{…}) to define workflows, component chains, and error-handling strategies at runtime. Unlike hard-coded pipelines where execution order is fixed in source code, protocol-based orchestration enables dynamic discovery of components, adaptive selection based on query characteristics or performance history, and automatic fallback mechanisms when quality thresholds are not met.
Can I mix different retrieval strategies in the same modular RAG system?
Yes. The architecture supports heterogeneous retrieval strategies through the common BaseRAGComponent interface. For example, you can combine dense vector retrievers (inheriting from BaseRAGComponent) with knowledge-graph modules like BasicGraphRAG (defined in 00_COURSE/04_retrieval_augmented_generation/03_graph_enhanced_rag.md) within the same orchestration protocol. The adaptive orchestrator can route queries to the appropriate retriever based on entity detection, query complexity, or latency requirements.
What is the role of prompt templates in component interoperability?
Prompt templates serve as the communication contract between layers in a modular RAG system. They define structured input schemas, output formats, error-handling procedures, and metric specifications that all components must honor. By standardizing these interfaces through prompt templates (typically loaded from markdown specification files), the system ensures that a BasicGraphRAG module can seamlessly replace a standard retriever without breaking the orchestration protocol, as both adhere to the same prompt contract defined in the template layer.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →