En språkmodell vet ikke hva som er i dine interne rutinedokumenter, årsrapporten fra i fjor, eller kundebasen din. RAG løser dette — og det finnes mange måter å gjøre det på, fra enkle open source-løsninger til fullverdige pipelines.
RAG står for Retrieval-Augmented Generation — en teknikk der man gir en AI-modell tilgang til et eksternt kunnskapsgrunnlag i det øyeblikket den svarer på et spørsmål.
Uten RAG er en språkmodell som en svært lesekyndig kollega som sluttet å lese aviser for ett år siden. De vet mye, men de vet ingenting om det du nettopp la inn i systemet, og de har aldri sett bedriftens interne dokumenter.
RAG er ikke AI som husker mer — det er AI som slår opp i riktig kilde i samme øyeblikk den svarer.
I praksis betyr dette at modellen ikke "lærer" av dokumentene dine. Den henter relevant innhold i sanntid, bruker det som kontekst, og svarer deretter. Ingen finjustering, ingen retraining. Bare smart oppslag.
Det er vanlig å blande disse to. Finjustering (fine-tuning) betyr å trene modellen på nytt med dine data, slik at kunnskapen bakes inn i modellens vekter. Det er tidkrevende, dyrt og vanskelig å oppdatere.
| Egenskap | Fine-tuning | RAG |
|---|---|---|
| Kunnskap oppdateres | Krever ny trening | Oppdater kilden — ferdig |
| Kostnad | Høy (GPU-tid) | Lav (infrastruktur) |
| Sporbarhet | Vanskelig | Kan sitere kilden |
| Personlige / interne data | Risikabelt å inkludere | Trygt med riktig tilgangsstyring |
| Passer til | Tone, stil, domene-vokabular | Faktainformasjon som endres |
RAG-systemet har to faser: en indekserings-fase som skjer én gang (eller periodisk), og en spørringsfase som skjer i sanntid ved hver brukerinteraksjon.
En embedding er en matematisk representasjon av tekst som et punkt i et høydimensjonalt rom. Tekster som betyr det samme ender opp nær hverandre — selv om ordene er ulike. Setningen "returpolitikk" og "hvordan sender jeg tilbake en vare?" vil ha svært like vektorer, og søket vil finne begge.
Det er denne semantiske søkingen som skiller RAG fra vanlig nøkkelordssøk. Du trenger ikke å treffe de eksakte ordene — meningen er nok.
RAG passer ekstra godt når informasjonen er intern, hyppig oppdatert, eller for volumiøs til å inkludere i en prompt. Her er eksempler fra ulike sektorer.
"Hvilke kontraindikasjoner gjelder for metformin hos denne pasienten?" — svaret er basert på den faktiske journalen og nyeste legemiddelregister.
"Er det noen klausuler i kontraktene våre fra 2022 som begrenser videresalg i Norden?" — svar med kildehenvisning til de eksakte avsnittene.
"Sammenlign EBITDA-marginen i de fem siste kvartalsrapportene og beskriv trenden." — med referanse til de aktuelle sidetallene.
"Hvor mange feriedager har jeg rett på det første arbeidsåret, og hvordan søker jeg?" — AI-en henter fra personalhandboken.
"Hvilken versjon introduserte SSO-integrasjon og hva er setup-prosessen?" — hentet fra changelog og integrasjonsdokumentasjon.
"Feilkode E-217 på CNC-maskin modell X500 — hva er årsaken og hva er trinn-for-trinn-prosedyren?" — hentet fra servicemanualen.
"Hvilke studier fra 2020–2024 omhandler effekten av søvnmangel på kognitiv ytelse?" — med DOI-referanse til de faktiske artiklene.
"Hva ble besluttet om prismodellen i møtene fra Q3 i fjor?" — RAG-en henter relevante møtereferater og sammenstiller svaret.
RAG er enkelt å sette opp, men vanskelig å gjøre bra. De fleste problemene skyldes ikke AI-modellen — de skyldes dårlig datagrunnlag eller dårlig retrieval.
For store chunks gir for mye støy. For små chunks mister konteksten. En god tommelfingerregel: 300–600 ord per chunk, med 10–20 % overlapp mellom påfølgende chunks. Eksperimenter for ditt innhold.
Skannet PDF med dårlig OCR, tabeller i ustrukturert format, og rotete HTML gir dårlig retrieval. Bruk tid på dokumentforberedelse — det er den viktigste investeringen du gjør i et RAG-system.
Legg til metadata som dato, forfatter, avdeling og dokumenttype til hver chunk. Da kan du filtrere retrieval: "finn kun fra HR-dokumenter" eller "nyere enn 2023". Hybrid søk — vektor + filter — er langt mer presis enn ren semantisk søk.
Splitt problemet i to: Finner systemet de riktige chunkene? Og formulerer modellen et godt svar fra dem? Mange feil skyldes dårlig retrieval, ikke dårlig generering. Mål begge.
Et RAG-system uten kildehenvisning er et system ingen stoler på. Designet systemet slik at modellen alltid refererer til hvilken kilde svaret kommer fra — dokument, side, dato. Dette gir sporbarhet og gjør det enkelt å verifisere.
Hvis systemet ditt inneholder HR-dokumenter, finansdata og offentlig innhold, kan ikke alle spørre om alt. Implementer tilgangsstyring på chunk-nivå — ikke bare på applikasjonsnivå. En bruker skal kun få svar basert på dokumenter de har tilgang til.
Instruer modellen eksplisitt: om retrieval ikke finner noe relevant, skal den si det — ikke finne på noe. En god systemmelding: "Basér svaret utelukkende på de vedlagte dokumentene. Hvis svaret ikke finnes der, si at du ikke vet."
Et RAG-system er bare så godt som indeksen det søker i. Sett opp automatisk re-indeksering når dokumenter endres. Vurder versjonsstyring slik at du kan spore endringer over tid.
Det finnes mange måter å lagre embeddings på — fra dedikerte vektordatabaser som Qdrant, Chroma og Weaviate, til vektorutvidelser i databaser du kanskje allerede kjører: pgvector for PostgreSQL, Atlas Vector Search for MongoDB, og Elasticsearch med hybrid søk. Det finnes til og med løsninger uten server i det hele tatt, som SQLite + sqlite-vss for lokal prototyping.
Valget avhenger i stor grad av hva du allerede har. I neste seksjon tar vi et konkret dypdykk i to av de mest brukte open source-alternativene: Chroma for å komme raskt i gang, og pgvector for de som allerede kjører PostgreSQL.
Her er to konkrete veier fra null til fungerende RAG — én med Chroma (enklest, ingen server) og én med PostgreSQL + pgvector (for de som allerede har Postgres).
Chroma er en vektordatabase som kjører direkte i Python-prosessen og lagrer data lokalt på disk. Perfekt for å forstå RAG uten å sette opp infrastruktur.
# pip install langchain chromadb sentence-transformers anthropic from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.vectorstores import Chroma from langchain_community.embeddings import SentenceTransformerEmbeddings import anthropic, pathlib # 1. Les inn og del opp dokumenter tekst = pathlib.Path("rutiner.txt").read_text() splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50) chunks = splitter.create_documents([tekst]) # 2. Embed og lagre lokalt — ingen server nødvendig embeddings = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2") db = Chroma.from_documents(chunks, embeddings, persist_directory="./min_rag_db") # 3. Søk og generer svar def spor(sporsmaal: str) -> str: treff = db.similarity_search(sporsmaal, k=4) kontekst = " ".join(t.page_content for t in treff) client = anthropic.Anthropic() svar = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, system="Svar kun basert på dokumentene nedenfor. Si fra om du ikke finner svaret.", messages=[{"role": "user", "content": f"Dokumenter: {kontekst} Spørsmål: {sporsmaal}"}] ) return svar.content[0].text print(spor("Hva er prosedyren for å melde inn ferie?"))
Har du allerede en Postgres-database er pgvector en utvidelse som legger til en ny kolonnetype (vector) og en ny operatør (<->) for nærhetssøk. Du slipper en ekstra tjeneste, og data bor på samme sted som resten av systemet ditt.
-- Installer utvidelsen (én gang per database) CREATE EXTENSION IF NOT EXISTS vector; -- Tabell for chunks + embeddings CREATE TABLE dokumenter ( id SERIAL PRIMARY KEY, kilde TEXT, dato DATE, tekst TEXT, embedding vector(384) -- dimensjon avhenger av embedding-modellen ); -- Indeks for rask nærhetssøk (IVFFlat eller HNSW) CREATE INDEX ON dokumenter USING hnsw (embedding vector_cosine_ops); -- Søk: finn de 5 nærmeste naboene til en gitt vektor SELECT kilde, tekst, embedding <-> '[0.12, 0.34, ...]' AS distanse FROM dokumenter ORDER BY distanse LIMIT 5;
# pip install psycopg2-binary pgvector sentence-transformers anthropic import psycopg2 from pgvector.psycopg2 import register_vector from sentence_transformers import SentenceTransformer import anthropic model = SentenceTransformer("all-MiniLM-L6-v2") conn = psycopg2.connect("postgresql://bruker:passord@localhost/mindb") register_vector(conn) # Indekser en chunk def legg_til(tekst: str, kilde: str): vektor = model.encode(tekst).tolist() with conn.cursor() as cur: cur.execute( "INSERT INTO dokumenter (tekst, kilde, embedding) VALUES (%s, %s, %s)", (tekst, kilde, vektor) ) conn.commit() # Søk og svar def spor(sporsmaal: str) -> str: vektor = model.encode(sporsmaal).tolist() with conn.cursor() as cur: cur.execute( "SELECT kilde, tekst FROM dokumenter ORDER BY embedding <-> %s LIMIT 4", (vektor,) ) treff = cur.fetchall() kontekst = " ".join(f"[{r[0]}] {r[1]}" for r in treff) client = anthropic.Anthropic() svar = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, system="Svar kun basert på dokumentene nedenfor.", messages=[{"role": "user", "content": f"Dokumenter: {kontekst} Spørsmål: {sporsmaal}"}] ) return svar.content[0].text
Outlook er det de fleste faktisk bruker. La oss ta et konkret eksempel: du vil indeksere et e-postarkiv — kanskje en supportboks, et prosjektarkiv, eller en hel avdelings korrespondanse — og gjøre det søkbart med AI. Steg én er å få dataen ut.
Outlook lagrer e-post lokalt i .pst-filer (Personal Storage Table). Du eksporterer disse via innebygd funksjonalitet:
Velg "Eksporter til en fil", deretter "Outlook-datafil (.pst)". Velg hvilke mapper du vil eksportere — du kan velge én mappe, én konto, eller hele postkassen. Inkluder undermapper.
Bruker dere Exchange Online (Microsoft 365) kan administrator eksportere postkasser via PowerShell eller Exchange Admin Center til .pst — uavhengig av om Outlook er installert lokalt.
Microsoft Graph API lar deg hente e-post programmatisk uten å eksportere til fil. Mer teknisk, men den rette veien for løpende integrasjon fremfor en engangsdump.
En .pst-fil er et proprietært binærformat, men biblioteket pypff (eller libpff) lar deg lese det direkte. Alternativt kan du eksportere via Outlook til .msg-filer og lese disse med extract-msg.
# pip install pypff extract-msg # Merk: pypff krever libpff kompilert på systemet # Alternativt: konverter til .msg i Outlook og bruk extract-msg import pypff, pathlib def les_mappe(mappe, dybde=0): """Rekursivt gå gjennom alle mapper i .pst""" for i in range(mappe.number_of_sub_messages): melding = mappe.get_sub_message(i) yield { "emne": melding.subject or "", "fra": melding.sender_name or "", "dato": str(melding.delivery_time)[:10], "tekst": melding.plain_text_body or "", } for i in range(mappe.number_of_sub_folders): yield from les_mappe(mappe.get_sub_folder(i), dybde+1) pst = pypff.file() pst.open("eksport.pst") rotmappe = pst.get_root_folder() epost_liste = list(les_mappe(rotmappe)) print(f"Fant {len(epost_liste)} e-poster")
E-post er støyete: videresend-headers, signaturer, HTML-rester, og lange svar-tråder med gjentatt innhold. Du bør rydde opp før du indekserer.
import re def rens(tekst: str) -> str: # Fjern videresend-headers og svar-sitater tekst = re.sub(rr"Fra:.*? ", "", tekst) tekst = re.sub(rr"Den \d.*?skrev:", "", tekst, flags=re.DOTALL) tekst = re.sub(rr"_{3,}.*", "", tekst, flags=re.DOTALL) # signaturskillelinje # Fjern HTML-rester og normaliser whitespace tekst = re.sub(rr"<[^>]+>", " ", tekst) return re.sub(rr"\s+", " ", tekst).strip() def bygg_chunks(epost_liste: list) -> list: chunks = [] for e in epost_liste: tekst = rens(e["tekst"]) if len(tekst) < 30: # Hopp over tomme/trivielle e-poster continue # Inkluder emnet i teksten — hjelper retrieval chunk_tekst = f"Emne: {e['emne']} {tekst}" chunks.append({ "text": chunk_tekst, "metadata": { "emne": e["emne"], "fra": e["fra"], "dato": e["dato"], } }) return chunks chunks = bygg_chunks(epost_liste) print(f"{len(chunks)} chunks klare for indeksering")
Med chunkene klare setter vi dem inn i Postgres. Tabellen trenger en vector-kolonne for embeddings og vanlige kolonner for metadata — da kan du kombinere semantisk søk med SQL-filtrering på avsender, dato eller emne.
CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE epost_chunks ( id SERIAL PRIMARY KEY, emne TEXT, fra TEXT, dato DATE, tekst TEXT, embedding vector(384) ); -- HNSW-indeks for rask nærhetssøk CREATE INDEX ON epost_chunks USING hnsw (embedding vector_cosine_ops);
# pip install psycopg2-binary pgvector sentence-transformers anthropic import psycopg2 from pgvector.psycopg2 import register_vector from sentence_transformers import SentenceTransformer import anthropic model = SentenceTransformer("all-MiniLM-L6-v2") conn = psycopg2.connect("postgresql://bruker:passord@localhost/mindb") register_vector(conn) # Sett inn alle chunks i én batch with conn.cursor() as cur: for c in chunks: vektor = model.encode(c["text"]).tolist() cur.execute( "INSERT INTO epost_chunks (emne, fra, dato, tekst, embedding)" "VALUES (%s, %s, %s, %s, %s)", (c["metadata"]["emne"], c["metadata"]["fra"], c["metadata"]["dato"], c["text"], vektor) ) conn.commit() print("Indeksering ferdig")
Nå kan du kombinere vektorsøk med vanlig SQL. Det er her pgvector virkelig skiller seg fra Chroma: du kan filtrere på eksakt dato, avsender, eller hvilken som helst kombinasjon av metadata — i samme spørring som selve nærhetssøket.
def spor(sporsmaal: str, fra_dato: str = "2024-01-01") -> str: vektor = model.encode(sporsmaal).tolist() with conn.cursor() as cur: cur.execute(""" SELECT emne, fra, dato, tekst FROM epost_chunks WHERE dato >= %s ORDER BY embedding <-> %s LIMIT 5 """, (fra_dato, vektor)) treff = cur.fetchall() # Bygg kontekst med metadata synlig for modellen kontekst = "\n\n".join( f"[{r[2]} | Fra: {r[1]}]\nEmne: {r[0]}\n{r[3]}" for r in treff ) client = anthropic.Anthropic() svar = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, system="Svar kun basert på e-postene nedenfor. Referer til dato og avsender.", messages=[{ "role": "user", "content": f"E-poster:\n{kontekst}\n\nSpørsmål: {sporsmaal}" }] ) return svar.content[0].text # Eksempel print(spor("Hva ble besluttet om budsjettet for Q3?")) print(spor("Har noen rapportert problemer med innlogging?", fra_dato="2023-01-01"))
Det finnes ikke ett RAG-system. Det finnes tusenvis av varianter, fra én PDF lastet opp i en chat til distribuerte retrieval-pipelines med re-ranking, hybrid søk og agentisk orkestrering.
Det viktige å huske er at teknologien er sekundær. Kvaliteten på svarene avhenger nesten alltid mer av datakvaliteten, chunking-strategien, og systemprompten enn av hvilken modell eller vektordatabase du velger.
Start enkelt. Forstå hva retrieval faktisk returnerer. Evaluer ærlig. Og bygg videre derfra.
Det beste RAG-systemet er det du faktisk bruker — ikke det du planlegger å bygge.