ms-ai-architect/skills/ms-ai-infrastructure/references/hybrid-edge/hybrid-rag-architecture.md
Kjell Tore Guttormsen 3a73eeafdc refactor(ms-ai-architect): R13 del 1 — nøytraliser Cosmo-personaen i ref-korpusets headinger, etter å ha rettet en gate som var målt usann to ganger
Ordre 20260912T193441Z-7358817909. Steg 1 var ikke transformen, men å rette
roadmapens R13-gate og få den ratifisert. Gaten `grep -rl "Cosmo"
skills/*/references -> 0` var usann på to uavhengige måter:

1. Ordren fanget den første: 451 av forekomstene er Azure Cosmos DB, ekte
   produktinnhold. Diskriminatoren er ikke bokstaven «s» — `Cosmos <norsk
   substantiv>` er genitiv av personaen (`### Cosmos tonalitet`), mens
   `Cosmos DB`/`CosmosClient`/`cosmos_ru` er produkt.
2. Denne økten fant den andre: 132 persona-forekomster ligger i prosa,
   tabeller, dialog-replikker og proveniens-linjer. Heading-nøytralisering
   kan ikke nå dem, så «0 persona» er uoppnåelig også under den ratifiserte
   formen. Operatøren ratifiserte alternativ A: gaten speiler formen, og de
   132 bokføres til R13b/R14.

Tre korreksjoner av premisser som sto i ordren og STATE:
  «ca 320 produkt»   -> 451 (case-sensitivt nett manglet 327 lowercase
                        TOC-ankre + 99 identifikatorer; sann nevner 1 638)
  «169 headinger»    -> 401. 169 var `^## For Cosmo`-prefikset (168) og var
                        internt inkonsistent med sin egen topp-variant (204)
  «417 matcher ingen
   populasjon»       -> 417 er cosmo-headinger utenfor kodefences; briefens
                        nevner var reell hele tiden

Fence-bevissthet er målt skadelig, ikke nødvendig: begge toggle-regler er
gale på dette korpuset (naiv toggle skjuler en ekte heading i
chain-of-thought-prompting.md, CommonMark-regelen ubalanserer
service-level-documentation-dr.md). Fence-agnostisk deteksjon finner 401
heading-linjer i nøyaktig de samme 40 variantene som fence-bevisst finner
400 i — ingen kodeblokk-linje er byte-identisk til en persona-heading. Derfor
nøkles transformen på 40 enumererte heading-tekster og ignorerer fences. En
ukjent variant kaster; en slug-kollisjon kaster. Ingenting auto-fikses.

TOC-en regenereres ikke, den rettes kirurgisk: alle 327 persona-lenker hadde
lenketekst lik én av de 40 heading-tekstene og anker lik slugify av den
(327/327, 0 avvik), så heading og TOC-entry skrives i samme operasjon og
ingen mellomtilstand etterlater en død lenke.

Ratifisert målform: `For Cosmo`, `For Cosmo Skyberg` og `For arkitekten
(Cosmo)` konvergerer på `For arkitekten`. To filer kolliderte og er adjudisert
ved å lese dem, ikke ved regel.

Verifisering (alle 7 kriterier fra ordren):
  G1 persona på heading-linjer   401 -> 0
  G2 døde fragmentlenker         1 -> 1 (pre-eksisterende, unntatt)
  G3 produkt-forekomster         451 -> 451; `Cosmos DB|Azure Cosmos` 308 = 308
  de 3 kun-produkt-filene        byte-identiske
  nettet validert begge veier    injisert persona feller G1; genitiv feller G1;
                                 produkt-heading og de 3 filene passerer
  hele diffen                    802 heading-linjer + 654 TOC-linjer, ANNET = 0
  linjeantall                    728 lagt til = 728 slettet
  suite                          1120/1120 (1097 + 23 nye)
  validate-plugin                250 PASS / 0 FAIL
  stikkprøve                     10 filer, alle 5 skills, inkl. de 3 mest
                                 produkt-tunge (26/20/19) — kun heading+TOC

Utenfor scope, urørt: de 4 SKILL.md, de 23 commands, CLAUDE.md, README.md,
NOTICE.md, docs/ (alt R14).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:12:28 +02:00

17 KiB

Hybrid RAG Architecture

Last updated: 2026-02 Status: GA Category: Hybrid Cloud & Edge AI Type: reference


Innhold

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:

# 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

# 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]
        )]

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

// 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<string> 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<SearchResult>());

        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<SearchResult> MergeAndRank(
        List<SearchResult> localResults,
        List<SearchResult> cloudResults,
        int maxResults)
    {
        // Reciprocal Rank Fusion for a kombinere resultater
        var fusedScores = new Dictionary<string, double>();

        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

# 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:

# 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 arkitekten

  • 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