Har du lanceret et enkelt RAG-system, kender du mønstret: Opdel dokumenter, opret embeddings, gem dem i en vektordatabase, hent top-k for en forespørgsel, og læg resultaterne i prompten. Det fungerer i demoer. I produktion skuffer det ofte.
Afstanden mellem en RAG-demo og RAG i produktion er stor. Virkelige dokumenter er rodede. Virkelige forespørgsler er tvetydige. Kvaliteten varierer voldsomt mellem forespørgselstyper. Omkostninger og latenstid er reelle begrænsninger. Opdateringer og versionering betyder noget.
Denne artikel gennemgår en RAG-pipeline i produktionskvalitet — de seks faser, mønstrene i hver fase og den evalueringsdisciplin, der adskiller velfungerende systemer fra skuffende løsninger.
Pipelinen
Et produktionsklart RAG-system har seks logiske faser:
[Documents]
↓
1. Ingestion (parsing, cleaning, metadata extraction)
↓
2. Chunking (splitting into retrieval units)
↓
3. Embedding (vectors + storage)
↓
[Query]
↓
4. Retrieval (semantic + lexical + filters)
↓
5. Reranking (top-k → top-N)
↓
6. Generation (LLM + context)
↓
[Response]
Hver fase har sine egne kvalitetsudfordringer. En forbedring af én fase forbedrer helheden.
Vi gennemgår dem én efter én.
Fase 1: Indlæsning
Dårlige input giver dårlige output. Kvaliteten af indlæsningen sætter loftet for alt efterfølgende.
Kildetyper, du vil møde:
- PDF’er (som regel de værste).
- Word-dokumenter.
- HTML-sider.
- Markdown.
- Regneark.
- Præsentationer (PowerPoint, Google Slides).
- E-mails.
- Kode.
- Strukturerede data (CSV, JSON).
Hver type har sine egne udfordringer ved parsing.
Parsing af PDF’er. PDF er et præsentationsformat, ikke et dataformat. Det er notorisk vanskeligt at parse. Strategier:
- Tekstbaserede PDF’er:
pdfplumber,pymupdf,unstructured. Fungerer til ren tekst. - Scannede PDF’er: OCR med Tesseract, Google Cloud Vision eller AWS Textract.
- Layoutbevidst:
LayoutLM, Mathpix eller LLM-baseret parsing (GPT-4 vision) til komplekse layout (tabeller, flere kolonner).
En moderne tilgang til vanskelige PDF’er er at bruge en LLM med synsfunktioner til OCR og strukturering. Det koster mere, men kvaliteten er markant bedre end ved traditionel OCR.
Håndtering af tabeller. Tabeller i ustruktureret tekst er vanskelige. Du kan:
- Konvertere dem til Markdown-tabeller (bevarer strukturen).
- Linearisere dem som prosa (“Række 1: Kunde A havde 50 ordrer …”).
- Behandle dem som separate søgbare enheder med et struktureret skema.
Det rette valg afhænger af de forventede forespørgsler.
Udtrækning af metadata. Hvert dokument har relevante metadata:
- Titel, forfatter, dato og version.
- Emne, kategori og tags.
- Kilde-URL eller placering.
- Tilladelser/synlighed.
Registrér dem ved indlæsning. De bliver til filterparametre under søgning.
Rensning. Fjern standardskabeloner:
- Sidehoveder og sidefødder, der gentages på hver side.
- Navigation, annoncer og cookiebannere.
- Sider med “indholdsfortegnelse”.
- Tomme eller duplikerede afsnit.
Et rent korpus giver bedre søgeresultater. Standardskabeloner skaber falske match.
Kvalitetskontrol.
- Udtrak parseren faktisk tekst? (Nogle PDF’er returnerer tomme strenge.)
- Er der problemer med tegnkodning?
- Er tabeller bevaret?
- Har figurer billedtekster, eller springes de over?
Byg plausibilitetskontrol ind i indlæsningspipelinen. Opdag defekt parsing, før den forurener indekset.
Fase 2: Opdeling
Du har rene dokumenter. Nu opdeles de i søgeenheder (chunks).
Den grundlæggende afvejning:
- Små chunks: præcis søgning (fokuseret på spørgsmålet), men manglende kontekst.
- Store chunks: mere kontekst, men mindre præcision (det relevante er begravet).
Begge yderpunkter sænker kvaliteten. Det optimale punkt afhænger af indholdstypen.
Almindelige strategier:
Chunks med fast størrelse og overlap. Opdel i chunks på N tokens (300-800 tokens) med et overlap på 50-100 tokens. Enkelt og velegnet som udgangspunkt.
Sætningsbevidst opdeling. Opdel ved sætningsgrænser (med nltk, spaCy eller lignende). Undgår opdeling midt i en sætning.
Afsnitsbaseret. Hvert afsnit er en chunk. Fungerer til dokumenter med velstrukturerede afsnit.
Hierarkisk (små + store). To indeks:
- Små chunks (300 tokens) til præcis søgning.
- Store chunks (1500 tokens) eller hele sektioner til kontekst. Ved søgning findes den lille chunk, mens dens store overordnede chunk gives til LLM’en.
Bevidst om dokumentstruktur. Brug dokumentets struktur (overskrifter, sektioner) til at forme chunks. Hver sektion bliver en chunk, og hierarkiet bevares.
Semantisk opdeling. Brug embeddings til at finde naturlige brud (steder, hvor emnet skifter). Det er dyrere, men giver bedre chunks for visse indholdstyper.
LLM-baseret opdeling med opsummering. Lange dokumenter opsummeres i hierarkiske chunks på flere niveauer (afsnits-, sektions- og dokumentresumé). LLM’en opretter dem én gang ved indlæsning.
Den rette strategi afhænger af indholdet. Artikler og wikier: afsnitsbaseret eller hierarkisk. Kode: funktionsniveau. Samtaler: efter tur. Teknisk dokumentation: strukturbevidst.
Metadata for chunks. Hver chunk bør indeholde:
- Kildedokumentets id og URL.
- Sektionssti (kapitel > sektion > undersektion).
- Sidetal (til kildehenvisning).
- Overskrifter over den pågældende chunk (som kontekst).
- Metadata på dokumentniveau (dato, forfatter, type).
Disse metadata muliggør filtrering og kildehenvisning i det endelige svar.
Fase 3: Embedding
Konvertér chunks til vektorer.
Valg af embeddingmodel.
I 2026:
- OpenAI text-embedding-3-large: stærk allroundmodel, dyr.
- Voyage Voyage-3: konkurrencedygtig, ofte bedre til teknisk indhold.
- Cohere embed-v4: stærk flersproget model.
- BAAI bge-large: stærk open source-model.
- Nomic embed-text: god open source-model, gratis at hoste selv.
- Domænespecialiserede modeller: til kode (Voyage Code; OpenAI text-embedding-3-large fungerer også til kode), jura, medicin osv.
Valg af model: Test på dit eget domæne. Antag ikke, at vinderen på en rangliste er bedst til dine data.
Embeddingdimension. Modeller tilbyder 256-3072 dimensioner. Flere dimensioner = bedre kvalitet, større omkostning og lagerbehov. Til de fleste formål er 768-1536 et godt kompromis.
Versionering. Embeddingmodeller opdateres, og du kan få brug for at skifte. Planlæg det:
- Registrér, hvilken modelversion der producerede hver vektor.
- Genberegn alle embeddings ved migrering (eller brug en overgang med to indeks).
- Bland ikke vektorer fra forskellige modeller i samme søgning.
Omkostning. Embedding af millioner af chunks koster penge. €0.10-€0.50 pr. million tokens (afhænger af udbyder). Beregn det på forhånd for store korpora.
Lager. Vektorer fylder meget. 1M chunks × 1536 dimensioner × 4 bytes = 6GB. Planlæg lageret derefter.
Fase 4: Søgning
Forespørgslen modtages. Nu skal de relevante chunks findes.
Vektorsøgning (dense retrieval). Opret en embedding af forespørgslen, og find de nærmeste naboer. Indfanger semantisk lighed. Standardmetoden.
Nøgleordssøgning (BM25/leksikalsk). Traditionel tekstsøgning. Indfanger præcise match, sjældne termer og specifikke navne. Supplerer ofte vektorsøgning.
Hybridsøgning. Kør begge dele, og flet resultaterne. Reciprocal Rank Fusion (RRF) er standardmetoden.
def reciprocal_rank_fusion(rankings, k=60):
scores = {}
for ranking in rankings:
for rank, doc_id in enumerate(ranking):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
return sorted(scores.items(), key=lambda x: -x[1])
I praksis slår hybridsøgning som regel ren vektor- eller nøgleordssøgning. Brug den som standard.
Metadatafiltrering. Filtrér efter metadata før scoring. “Kun dokumenter fra 2025+”. “Kun dokumenter i brugerens tilgængelige sæt.” “Kun dokumenter af typen politik.”
I systemer med flere tenants er det afgørende, at hver forespørgsel afgrænses til dokumenter, som den pågældende tenant må tilgå.
Forståelse af forespørgslen. Forbehandl forespørgslen:
- Stavekorrektion. “engineerin” → “engineering.”
- Udvidelse af forkortelser. “MFA” → “Multi-Factor Authentication MFA.”
- Synonymer/forespørgselsudvidelse. Generér alternative formuleringer, og søg med hver af dem.
- Dekomponering. Opdel spørgsmål med flere dele i underforespørgsler, og søg særskilt efter hver.
Disse trin forbedrer søgningen markant for virkelige forespørgsler.
HyDE (Hypothetical Document Embeddings). Generér et fiktivt “ideelt svar” med en LLM. Opret en embedding af det, og brug resultatet som søgeforespørgsel. Det fungerer ofte bedre end en direkte embedding af spørgsmålet, fordi det fiktive svar ligner de faktiske svardokumenter mere.
def hyde_retrieve(query, k=20):
hypothetical = llm("Write a passage answering: " + query)
embedding = embed(hypothetical)
return vector_search(embedding, k=k)
Afvejning: Et ekstra LLM-kald pr. forespørgsel (latenstid, omkostning). Det kan betale sig for svære forespørgsler, men er overdrevet for enkle.
Søgning med flere vektorer. Brug flere vektorer pr. chunk i stedet for én (f.eks. én for selve chunken, én for et resumé og én for hypotetiske spørgsmål). Det øger lagerbehov og kompleksitet, men forbedrer søgningen for visse typer indhold.
Fase 5: Reranking
Efter den første søgning (top-k, k=20-50) bruges en reranker til at score kandidaterne mere grundigt.
Hvorfor reranke?
Den første søgning er hurtig, men upræcis. Vektorlighed er kun en grov indikator for relevans. En reranker (typisk en cross-encoder-model) læser forespørgslen og hver kandidat sammen og beregner en mere præcis score.
Mulige rerankere:
- Cohere Rerank. Branchestandard. God kvalitet, hostet.
- Voyage Rerank. Stærk konkurrent.
- BGE Rerank-large. Open source, gratis at hoste selv.
- MiniLM cross-encoders. Mindre og hurtigere, men lavere kvalitet.
- LLM-baseret reranking. Brug en lille LLM til at score par af forespørgsel og kandidat. Højeste kvalitet, højeste pris.
Effekten.
Reranking forbedrer typisk slutkvaliteten i de fleste RAG-systemer; betragt det ofte citerede interval på 10-20% som et planlægningsestimat, der skal verificeres mod egne evalueringer. Den ekstra latenstid og omkostning er næsten altid pengene værd.
Typisk opsætning:
- Hent 30-50 kandidater med hybridsøgning.
- Rerank til top 5-10.
- Giv de bedste resultater til LLM’en.
Adaptiv reranking.
Spring reranking over, når det bedste første resultat har høj vektorlighed (et klart match). Rerank aggressivt, når scorerne ligger tæt (flere kandidater står lige).
Fase 6: Generering
LLM’en producerer svaret ud fra den fundne kontekst.
Promptstruktur:
You are an assistant answering questions based on the provided context.
Context:
[Document 1 - title, URL]
[chunk 1 content]
[Document 2 - title, URL]
[chunk 2 content]
...
User question: {query}
Instructions:
- Answer based only on the provided context.
- If the context doesn't contain the answer, say so. Don't make up information.
- Cite sources using [Source N] notation.
- Be concise but complete.
De vigtige promptmønstre:
- Kildemærkning. Hver kontekst-chunk mærkes med sin kilde til kildehenvisning.
- Instruktioner om forankring. Fortæl eksplicit modellen, at den kun må bruge konteksten.
- Krav om kildehenvisning. Kræv kildehenvisning. Det afslører hallucinationer.
- Fallbackinstruktion. “Hvis konteksten ikke indeholder svaret, så sig det.” Det forhindrer opdigtning.
Håndtering af kildehenvisninger.
Et almindeligt mønster er at medtage kildelinks i svaret. Brugergrænsefladen viser dem som klikbare links.
"The company's remote work policy allows up to 4 days/week from home [1].
[1]: https://wiki.company.com/policies/remote-work"
Det gør svaret verificerbart. Brugerne stoler mere på forankrede svar.
Styring af kontekststørrelse.
Lange kontekster forringer kvaliteten. De fleste modeller fungerer bedst med fokuseret kontekst (5-10 meget relevante chunks) frem for at få alt hældt ind. Overfyldt kontekst sænker kvaliteten.
Hvis meget kontekst er nødvendig, skal mindre relevante elementer opsummeres frem for at medtages fuldt ud.
Evaluering i hver fase
Du kan ikke optimere det, du ikke måler. Hver fase kræver egne evalueringer.
Evaluering af indlæsning. Blev dokumenterne parset korrekt? Udtag dokumenter, og kontrollér, at centralt indhold er bevaret.
Evaluering af opdeling. Har chunks den rette størrelse? Bevarer de konteksten? Bryder de ved fornuftige grænser?
Evaluering af embeddings. Ligger beslægtede begreber tæt? Stemmer cosinusligheden med forventningerne på et testsæt med kendte par, der henholdsvis ligner og ikke ligner hinanden?
Evaluering af søgning. Findes de relevante chunks i top-K for en forespørgsel? En almindelig måling er recall@K (hvor stor en procentdel af forespørgslerne har mindst én relevant chunk i top K).
Evaluering af reranking. Placerer rerankeren den mest relevante kandidat først? Måling: NDCG (normaliseret diskonteret kumulativ gevinst) eller MRR (gennemsnitlig reciprok rang).
Evaluering af generering. Producerer LLM’en et korrekt svar ud fra kontekst og forespørgsel? Målinger: forankring (bruger svaret konteksten?), korrekthed (er svaret rigtigt?), hjælpsomhed (opfylder det brugerens hensigt?).
End-to-end-evaluering. Producerer systemet det rette svar på en reel forespørgsel? Den vigtigste evaluering afhænger af alle faser.
Du skal bruge testsæt til hver fase. Et almindeligt udgangspunkt:
- Byg 50-100 forespørgsler med kendte, gode svar.
- Identificér for hver forespørgsel de dokumenter, der indeholder svaret.
- Brug dem til at evaluere søgningen (finder vi de rette dokumenter?) og genereringen (er svaret korrekt?).
Værktøjer: Ragas, TruLens og egne suiter. Det konkrete værktøj betyder mindre end regelmæssige evalueringer.
Driftsmæssige hensyn
Nogle praktiske forhold i produktion:
Indekseringspipelines. Nye dokumenter ankommer. Opret nye embeddings. Opdatér indeks. Håndtér sletninger og opdateringer. Denne pipeline kører løbende, ikke kun under opsætning. Byg den som et system, ikke et engangsscript.
Latenstid.
Typisk fordeling:
- Embedding (forespørgsel): 50-200ms.
- Vektorsøgning: 50-200ms.
- Reranking: 200-500ms (afhænger af K).
- Generering: 1-5s (afhænger af kontekst og model).
- I alt: 1.5-6s.
Optimering:
- Cache embeddings for hyppige forespørgsler.
- Cache reranking af gentagne par af forespørgsel og kandidat.
- Stream genereringen.
- Parallelisér, hvor det er muligt.
Latenstid under 2s kræver typisk en hurtig embeddingmodel, en hurtig vektordatabase og enten en hurtig reranker eller ingen reranking for cachede/enkle forespørgsler.
Omkostning.
Omkostninger pr. forespørgsel:
- Embedding: ~€0.0001.
- Vektorsøgning: ~€0.0001 (afhænger af infrastrukturen).
- Reranking: €0.001-0.01 (afhænger af modellen).
- Generering: €0.005-0.05 (afhænger af model + kontekst).
- I alt: ~€0.01-0.05 pr. forespørgsel.
I stor skala løber det op. En million forespørgsler = €10K-50K.
Omkostningsoptimering:
- Cache.
- Brug billigere modeller, hvor kvaliteten tillader det.
- Komprimér konteksten (resuméer frem for hele chunks).
Opdateringer. Dokumenter ændres. Mønstre:
- Versionering: Hvert dokument har en version; gamle versioner bevares eller slettes efter politikken.
- Inkrementelt: Nye versioner opdeles og får nye embeddings; gamle chunks slettes.
- Forskelbevidst: Kun ændrede sektioner behandles igen.
Det har stor betydning ved hyppige opdateringer.
Tilladelser. RAG over private data: Dokumenterne har adgangskontrol, som søgningen skal respektere.
- Metadatabaseret filtrering (hver chunk har metadata om, hvem der må tilgå den).
- Tenantisolerede indeks til hård isolation.
- Revisionslog over, hvem der har tilgået hvad.
Stol ikke på, at LLM’en håndhæver tilladelser. Håndhæv dem under søgningen.
Almindelige fejltyper
Her er nogle mønstre i RAG-systemer, der fejler:
Fejl 1: Dårlig opdeling. Chunks deles midt i en tanke, tabeller splittes mellem chunks, og overskrifter adskilles fra deres indhold. Løsning: En bedre opdelingsstrategi.
Fejl 2: Søgningen overser svaret. Den relevante chunk findes, men bliver ikke hentet. Ofte skyldes det uoverensstemmelse mellem forespørgsel og dokument. Løsning: HyDE, forespørgselsudvidelse og bedre embeddings.
Fejl 3: Rette chunks, forkert rækkefølge. Den relevante chunk hentes, men rangeres lavt. LLM’en bruger irrelevante chunks med højere rang. Løsning: Reranking.
Fejl 4: Rette chunks, hallucination. Chunks er korrekte, men LLM’en opfinder yderligere oplysninger. Løsning: Strengere promptforankring, krav om kildehenvisning og lavere temperatur.
Fejl 5: Rette chunks, forkert svar. Chunks indeholder svaret, men LLM’en udtrækker den forkerte del. Løsning: En bedre genereringsprompt og eventuelt en større model.
Fejl 6: Forældede data. Indekset er ikke blevet opdateret. Løsning: En kontinuerlig indlæsningspipeline.
Fejl 7: Læk af tilladelser. Brugere ser chunks, de ikke må se. Løsning: Håndhæv filtrering under søgning, og før revisionslog.
Fejl 8: Omkostninger løber løbsk. Latenstid og omkostninger voksede med korpus og trafik. Løsning: Caching, modelrouting og eventuelt arkitekturændringer.
Et gennemarbejdet eksempel: RAG til virksomhedens vidensbase
Et konkret eksempel er et typisk RAG-system til en virksomhedsvidensbase.
Korpus: ~50,000 dokumenter (wikisider, politikker, runbooks, mødenoter, Slack-tråde).
Pipeline:
-
Indlæsning:
- Notion: API-eksport.
- Slack: arkiveksport filtreret til relevante kanaler.
- Google Drive: API-eksport af godkendte mapper.
- Rensning: Fjern beskeder, der kun består af emojis, samt standardsignaturer.
-
Opdeling:
- Hierarkisk: Små chunks på afsnitsniveau og overordnede chunks på sektionsniveau.
- ~250K chunks i alt på det lille niveau.
- Metadata: kilde, sektionssti, last_updated, accessible_to.
-
Embedding:
- Voyage-3-embeddings, 1024 dimensioner.
- Lagret i Pinecone (administreret).
- Nye embeddings ugentligt for ændrede dokumenter.
-
Søgning:
- Hybrid: Vektorsøgning + BM25 (via Pinecones hybridfunktion).
- HyDE til forespørgslen.
- Metadatafilter for adgang.
- Top 30 hentes.
-
Reranking:
- Cohere Rerank.
- Top 30 → top 8.
-
Generering:
- Claude Sonnet 5.
- De bedste 8 overordnede chunks (ikke de små chunks) gives som kontekst.
- Kildehenvisning kræves i svaret.
Evaluering:
- 100 håndskrevne par af forespørgsel og svar.
- Recall@30: for søgning ~92%.
- Det endelige svars korrekthed: ~85%.
- Forankring (ingen hallucination): ~95%.
Drift:
- Daglig inkrementel indlæsning.
- Ugentlig fuld genberegning af embeddings for ændrede dokumenter.
- Observabilitet pr. forespørgsel.
- Månedlig genkørsel af evaluering og overvågning af udviklingen.
- Kvartalsvis gennemgang af forespørgselslogge for nye fejlmønstre.
Omkostninger:
- Embedding: ~€500/month (stabil drift).
- Hosting af vektordatabase: ~€800/month (Pinecone).
- Søgning + generering pr. forespørgsel: ~€0.02.
- I alt: ~€2,500/month + €0.02 × antal forespørgsler.
For de fleste virksomheder er det fuldt ud berettiget af produktivitetsgevinsten.
En 90-dages plan for opbygning af RAG
Hvis du begynder fra nul:
Dag 1-30: MVP.
- Identificér korpus og primær brugssituation.
- Byg basal indlæsning (én kilde).
- Basal opdeling (fast størrelse med overlap).
- Standardmodel til embeddings.
- Kun vektorsøgning (ingen reranking).
- Enkel genereringsprompt.
Dag 31-60: Kvalitet.
- Byg manuelt et evalueringssæt (50-100 forespørgsler).
- Identificér fejltyper.
- Tilføj reranking.
- Tilføj hybridsøgning.
- Forbedr opdelingen.
Dag 61-90: Drift.
- Kontinuerlig indlæsningspipeline.
- Observabilitet.
- Automatisering af evaluering.
- Tilladelser/godkendelse.
- Omkostningsovervågning.
Efter 90 dage har du et system, der ikke kun kan demonstreres, men faktisk bruges. Rigtige brugere kan stole på det.
Kvaliteten findes i hver fase
RAG i produktion er en pipeline med seks faser og kvalitetsudfordringer i hver fase. Disse mønstre adskiller velfungerende systemer fra skuffende løsninger:
- Indlæsning: Grundig parsing, bevarelse af struktur og registrering af metadata.
- Opdeling: En strategi, der passer til indholdstypen, ofte hierarkisk.
- Embedding: En kvalitetsmodel, versionering og planlagt migrering.
- Søgning: Hybrid som standard, forståelse af forespørgslen og metadatafiltrering.
- Reranking: Næsten altid indsatsen værd.
- Generering: Forankring, kildehenvisninger og fallback ved ukendt svar.
- Evaluering: I hver fase og løbende.
Intet af dette er valgfrit. Forskellen mellem RAG med 60% svarkvalitet og RAG med 90% ligger i disse detaljer.
Investeringen er reel — en produktionsklar RAG-udbygning tager måneder, ikke uger. Til gengæld får du et system, som brugerne faktisk kan stole på. Det er den afgørende tærskel.



