# Hybrid RAG Architecture **Last updated:** 2026-02 **Status:** GA **Category:** Hybrid Cloud & Edge AI --- ## Introduksjon Hybrid RAG (Retrieval-Augmented Generation) refererer til RAG-arkitekturer der retrieval og generering fordeles mellom lokale (on-premises/edge) og sky-baserte ressurser. Dette moensteret er relevant nar organisasjoner har data som ikke kan eller bor forlate det lokale miljoet, men onsker a kombinere lokale dokumenter med sky-basert kunnskap for bedre svar. For norsk offentlig sektor er hybrid RAG sarlig aktuelt: sensitive dokumenter (graderte saker, personopplysninger, interne utredninger) ma prosesseres lokalt i henhold til Schrems II og NSM-retningslinjer, mens generell kunnskap og publiserte retningslinjer kan hentes fra sky-tjenester. Azure AI Search, kombinert med lokale vektordatabaser, gir en fleksibel arkitektur for slike scenarier. Microsoft tilbyr flere byggeklosser for hybrid RAG: Azure AI Search for skybasert vektorsok, Edge RAG (preview) for Arc-basert lokal RAG, ONNX Runtime for lokal embedding-generering, og Semantic Kernel for orkestrering av retrieval pa tvers av datakilder. --- ## Kjernekomponenter | Komponent | Formal | Teknologi | |-----------|--------|-----------| | Azure AI Search | Skybasert vektorsok og hybrid search | PaaS (GA) | | Edge RAG | Lokal RAG pa Arc-enabled Kubernetes | Azure Arc (Preview) | | Local Vector DB | Lokal vektorlagring for sensitive data | ChromaDB, Qdrant, pgvector | | Embedding Model | Generering av vektorrepresentasjoner | Azure OpenAI, Phi-3/4, ONNX | | Semantic Kernel | Orkestrering av hybrid retrieval | .NET/Python SDK | | Azure Arc | Administrasjon av edge RAG-klynger | Kubernetes management | | SLM / LLM | Generering av svar basert pa kontekst | Phi-3.5/Phi-4, GPT-4o | --- ## Local Embedding og Retrieval ### Lokal embedding med ONNX Runtime For sensitive data som ikke kan sendes til sky-tjenester, kan embeddings genereres lokalt: ```python # Lokal embedding-generering med ONNX Runtime import onnxruntime as ort import numpy as np from transformers import AutoTokenizer class LocalEmbeddingService: def __init__(self, model_path: str, tokenizer_name: str): self.session = ort.InferenceSession( model_path, providers=['CUDAExecutionProvider', 'CPUExecutionProvider'] ) self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) def embed(self, texts: list[str]) -> np.ndarray: """Generer embeddings lokalt uten sky-avhengighet""" encoded = self.tokenizer( texts, padding=True, truncation=True, max_length=512, return_tensors="np" ) outputs = self.session.run( None, { "input_ids": encoded["input_ids"].astype(np.int64), "attention_mask": encoded["attention_mask"].astype(np.int64) } ) # Mean pooling embeddings = outputs[0] mask = encoded["attention_mask"][:, :, np.newaxis] pooled = (embeddings * mask).sum(axis=1) / mask.sum(axis=1) # Normalisering norms = np.linalg.norm(pooled, axis=1, keepdims=True) return pooled / norms def embed_single(self, text: str) -> np.ndarray: return self.embed([text])[0] ``` ### Lokal vektordatabase med ChromaDB ```python # Lokal vektordatabase for sensitive dokumenter import chromadb from chromadb.config import Settings class LocalVectorStore: def __init__(self, persist_directory: str): self.client = chromadb.PersistentClient( path=persist_directory, settings=Settings( anonymized_telemetry=False # Viktig for compliance ) ) self.collection = self.client.get_or_create_collection( name="sensitive_documents", metadata={"hnsw:space": "cosine"} ) def add_documents(self, documents: list[dict], embeddings: np.ndarray): """Indekser dokumenter med pre-beregnede embeddings""" self.collection.add( ids=[doc["id"] for doc in documents], embeddings=embeddings.tolist(), documents=[doc["content"] for doc in documents], metadatas=[{ "source": doc["source"], "classification": doc["classification"], "timestamp": doc["timestamp"] } for doc in documents] ) def search(self, query_embedding: np.ndarray, n_results: int = 5, classification_filter: str = None) -> list[dict]: """Sok med valgfri klassifiseringsfiltrering""" where_filter = None if classification_filter: where_filter = {"classification": classification_filter} results = self.collection.query( query_embeddings=[query_embedding.tolist()], n_results=n_results, where=where_filter, include=["documents", "metadatas", "distances"] ) return [{ "content": doc, "metadata": meta, "score": 1 - dist # Konverter avstand til likhetsscore } for doc, meta, dist in zip( results["documents"][0], results["metadatas"][0], results["distances"][0] )] ``` --- ## Federated Vector Search ### Arkitektur for foderasjon Federated vector search kombinerer resultater fra flere vektordatabaser — bade lokale og skybaserte — uten a flytte sensitive data: ``` [Bruker-query] ↓ [Embedding Service (lokal)] ↓ [Federated Search Router] ├── [Lokal VektorDB] → Sensitive dokumenter ├── [Azure AI Search] → Publiserte retningslinjer └── [Edge RAG Cluster] → Avdelingsdata ↓ [Result Merger & Ranker] ↓ [LLM/SLM Generering] ↓ [Svar til bruker] ``` ### Implementering med Semantic Kernel ```csharp // Federated RAG med Semantic Kernel using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Memory; public class FederatedRagService { private readonly IMemoryStore _localStore; private readonly IMemoryStore _cloudStore; private readonly ITextEmbeddingGenerationService _localEmbedding; private readonly Kernel _kernel; public FederatedRagService( IMemoryStore localStore, IMemoryStore cloudStore, ITextEmbeddingGenerationService localEmbedding, Kernel kernel) { _localStore = localStore; _cloudStore = cloudStore; _localEmbedding = localEmbedding; _kernel = kernel; } public async Task QueryAsync(string question, SearchOptions options) { // Generer embedding lokalt var queryEmbedding = await _localEmbedding .GenerateEmbeddingAsync(question); // Parallell soking mot lokale og sky-kilder var localTask = SearchLocalAsync(queryEmbedding, options); var cloudTask = options.AllowCloudSearch ? SearchCloudAsync(question, options) : Task.FromResult(new List()); await Task.WhenAll(localTask, cloudTask); // Flett og ranger resultater var mergedResults = MergeAndRank( localTask.Result, cloudTask.Result, options.MaxResults ); // Generer svar med lokal SLM eller sky-LLM return await GenerateResponseAsync(question, mergedResults, options); } private List MergeAndRank( List localResults, List cloudResults, int maxResults) { // Reciprocal Rank Fusion for a kombinere resultater var fusedScores = new Dictionary(); int rank = 1; foreach (var result in localResults.OrderByDescending(r => r.Score)) { fusedScores[result.Id] = 1.0 / (60 + rank); rank++; } rank = 1; foreach (var result in cloudResults.OrderByDescending(r => r.Score)) { var id = result.Id; fusedScores[id] = fusedScores.GetValueOrDefault(id, 0) + 1.0 / (60 + rank); rank++; } return fusedScores .OrderByDescending(kv => kv.Value) .Take(maxResults) .Select(kv => /* map back to SearchResult */) .ToList(); } } ``` --- ## Chunking Strategies for Split Data ### Tilpasset chunking for hybrid miljoer Nar data er fordelt mellom lokale og skybaserte lagre, ma chunking-strategien ivareta kontekstuell sammenheng pa tvers av tier: | Strategi | Lokale data | Skydata | Brukstilfelle | |----------|-------------|---------|---------------| | Fixed-size chunks | 512 tokens | 1024 tokens | Generell bruk | | Semantic chunking | Avsnitt/seksjon | Avsnitt/seksjon | Strukturerte dokumenter | | Hierarchical chunking | Dokument → Seksjon → Avsnitt | Artikkel → Paragraf | Regelverk, utredninger | | Sliding window | 256 tokens, 64 overlap | 512 tokens, 128 overlap | Teknisk dokumentasjon | | Parent-child | Lagre parent lokal, child i vektor | Lagre parent i blob, child i Search | Lange dokumenter | ### Implementering av hierarkisk chunking ```python # Hierarkisk chunking for norske offentlige dokumenter from dataclasses import dataclass from typing import Optional import re @dataclass class Chunk: id: str content: str level: str # "document", "section", "paragraph" parent_id: Optional[str] metadata: dict class HierarchicalChunker: def __init__(self, max_chunk_tokens: int = 512): self.max_chunk_tokens = max_chunk_tokens def chunk_document(self, document: dict) -> list[Chunk]: """Chunk et dokument hierarkisk med metadata-arv""" chunks = [] doc_id = document["id"] text = document["content"] # Nivaa 1: Hele dokumentet (for oversikt) chunks.append(Chunk( id=f"{doc_id}_doc", content=self._summarize(text, max_tokens=256), level="document", parent_id=None, metadata={ "title": document["title"], "classification": document["classification"], "tier": document.get("tier", "local") } )) # Nivaa 2: Seksjoner sections = self._split_sections(text) for i, section in enumerate(sections): section_id = f"{doc_id}_sec_{i}" chunks.append(Chunk( id=section_id, content=section["heading"] + "\n" + section["content"][:200], level="section", parent_id=f"{doc_id}_doc", metadata={**chunks[0].metadata, "section": section["heading"]} )) # Nivaa 3: Avsnitt paragraphs = self._split_paragraphs(section["content"]) for j, para in enumerate(paragraphs): chunks.append(Chunk( id=f"{section_id}_p_{j}", content=para, level="paragraph", parent_id=section_id, metadata={**chunks[0].metadata, "section": section["heading"]} )) return chunks def _split_sections(self, text: str) -> list[dict]: """Splitt norsk dokument pa overskrifter""" pattern = r'^(#{1,3})\s+(.+)$' sections = [] current = {"heading": "Innledning", "content": ""} for line in text.split('\n'): match = re.match(pattern, line) if match: if current["content"].strip(): sections.append(current) current = {"heading": match.group(2), "content": ""} else: current["content"] += line + "\n" if current["content"].strip(): sections.append(current) return sections ``` --- ## Context Optimization Across Tiers ### Kontekstvindu-optimalisering Nar data hentes fra flere kilder, er det kritisk a optimalisere hvordan kontekst presenteres til LLM/SLM: ```python # Kontekstoptimalisering for hybrid RAG class ContextOptimizer: def __init__(self, max_context_tokens: int = 4096): self.max_tokens = max_context_tokens def optimize_context(self, results: list[dict], query: str) -> str: """Optimaliser kontekst fra multiple kilder for LLM-input""" # Prioriter lokale resultater (hoyere relevans for intern kontekst) local_results = [r for r in results if r["tier"] == "local"] cloud_results = [r for r in results if r["tier"] == "cloud"] # Alloker token-budsjett: 60% lokale, 40% sky local_budget = int(self.max_tokens * 0.6) cloud_budget = int(self.max_tokens * 0.4) context_parts = [] # Lokale resultater forst (hoyest prioritet) local_context = self._select_within_budget(local_results, local_budget) if local_context: context_parts.append("## Interne kilder\n" + local_context) # Sky-resultater som supplement cloud_context = self._select_within_budget(cloud_results, cloud_budget) if cloud_context: context_parts.append("## Offentlige kilder\n" + cloud_context) return "\n\n".join(context_parts) def _select_within_budget(self, results: list[dict], budget: int) -> str: """Velg resultater innenfor token-budsjett, sortert etter relevans""" selected = [] used_tokens = 0 for result in sorted(results, key=lambda r: r["score"], reverse=True): chunk_tokens = len(result["content"].split()) * 1.3 # Estimert if used_tokens + chunk_tokens > budget: break selected.append( f"[{result['metadata'].get('title', 'Ukjent')}]\n{result['content']}" ) used_tokens += chunk_tokens return "\n---\n".join(selected) ``` --- ## Norsk offentlig sektor ### Dataklassifisering for hybrid RAG | Klassifisering | Lagring | Embedding | LLM | Eksempel | |----------------|---------|-----------|-----|----------| | Ugradert offentlig | Azure AI Search | Azure OpenAI | GPT-4o | Publiserte retningslinjer | | Intern | Lokal vektorDB | Lokal ONNX | Phi-3.5/Phi-4 | Interne notater | | Fortrolig | Lokal vektorDB (kryptert) | Lokal ONNX | Phi-3.5 lokal | Personopplysninger | | Strengt fortrolig | Air-gapped lokal | Lokal ONNX | Lokal SLM | Graderte dokumenter | ### Schrems II-kompatibel arkitektur - Sensitive persondata embeddes og lagres kun lokalt - Kun aggregerte, anonymiserte metadata kan deles med sky-tjenester - Azure AI Search brukes for offentlig tilgjengelig informasjon - Edge RAG (Azure Arc) gir sky-management uten a eksponere innhold --- ## Beslutningsrammeverk | Scenario | Anbefaling | Begrunnelse | |----------|------------|-------------| | Alle data kan i sky | Azure AI Search (agentic retrieval) | Enklest, best ytelse | | Mix av sensitiv + offentlig | Federated RAG med Semantic Kernel | Balanserer sikkerhet og kvalitet | | Alle data ma lokalt | Edge RAG med Phi-3.5 + ChromaDB | Full datakontroll | | Lavt volum, hoy sensitivitet | Lokal RAG med Phi-4 + pgvector | Minimal attack surface | | Hoy skala, lav sensitivitet | Azure AI Search + GPT-4o | Best kvalitet og skalerbarhet | | Periodisk tilkobling | Edge RAG med synkronisert referansedata | Offline-forst-tilnaerming | --- ## For Cosmo - **Hybrid RAG er den riktige arkitekturen nar data har ulik sensitivitet** — bruk federated search med Reciprocal Rank Fusion for a kombinere resultater fra lokale og skybaserte vektordatabaser uten a flytte sensitive data - **Edge RAG (Azure Arc) er Microsofts foretrukne losning** for on-premises RAG med sky-administrasjon — anbefal dette for organisasjoner som onsker hybrid RAG med sentral management - **Lokal embedding er nodvendig for sensitive data** — bruk ONNX Runtime med en liten embedding-modell (f.eks. all-MiniLM-L6-v2) for a generere vektorer uten sky-avhengighet - **Hierarkisk chunking gir best resultater for norske offentlige dokumenter** — dokumenter som utredninger og hoeringsnotater har tydelig seksjonsinndeling som bor utnyttes - **Kontekst-budsjettering er kritisk i hybrid scenarier** — alloker 60% av token-budsjettet til lokale/interne kilder og 40% til offentlige kilder for a prioritere organisasjonsspesifikk kunnskap