Steg 9 (R4): unified migrate-corpus.mjs --write over engineering/governance/ infrastructure/security. 327 filer mutert, verified=null, prosa byte-identisk (fra første ## seksjon), advisor urørt (0 endringer). To applier-fixes oppdaget under kjøring (TDD, RED→GREEN): - insertHeaderFields: anker faller nå tilbake når en meta-linje selv passerer 500B (2 filer pakket et avsnitt i **Status:** → Type/Source landet utenfor scan-vinduet, applierens post-write-assertion fanget + restaurerte). - normalizeStaleVerified: fjerner nå ALLE stale non-date **Verified:** i 500B-vinduet, inkl. stray body-dup rett under --- (9 mlops-genaiops-filer var ellers falskt "verified"/fresh, droppet fra worklist). Operatør-godkjent utvidelse av carve-out; kun stray metadata-linjer, aldri prosa. test-transform-criterion: precondition oppdatert til post-migrasjons-sannhet (fila bærer nå Source). Suite 728/728 grønn.
461 lines
17 KiB
Markdown
461 lines
17 KiB
Markdown
# Hybrid RAG Architecture
|
|
|
|
**Last updated:** 2026-02
|
|
**Status:** GA
|
|
**Category:** Hybrid Cloud & Edge AI
|
|
**Type:** reference
|
|
|
|
---
|
|
|
|
## Innhold
|
|
|
|
- [Introduksjon](#introduksjon)
|
|
- [Kjernekomponenter](#kjernekomponenter)
|
|
- [Local Embedding og Retrieval](#local-embedding-og-retrieval)
|
|
- [Federated Vector Search](#federated-vector-search)
|
|
- [Chunking Strategies for Split Data](#chunking-strategies-for-split-data)
|
|
- [Context Optimization Across Tiers](#context-optimization-across-tiers)
|
|
- [Norsk offentlig sektor](#norsk-offentlig-sektor)
|
|
- [Beslutningsrammeverk](#beslutningsrammeverk)
|
|
- [For Cosmo](#for-cosmo)
|
|
|
|
## 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<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
|
|
|
|
```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
|