Multimodal RAG
Kunna indexera och hämta bilder och tabeller tillsammans med text.
Förkunskaper
Intuition
Vanlig RAG hämtar textstycken. Men svaret på «hur ser trenden ut sedan 2020?» står ofta i ett diagram, och svaret på «vad kostar abonnemanget?» i en tabell.
Tre strategier för icke-text:
| Strategi | Vad som indexeras | Styrka | Svaghet |
|---|---|---|---|
| Beskriv och indexera text | en VLM-genererad beskrivning av varje bild | fungerar med befintlig textsökning | beskrivningen blir ett filter — det som inte nämns går inte att hitta |
| Gemensamt vektorrum | bildvektorer i samma rum som textfrågan | hittar visuellt utan mellanled | trubbigt för text i bilden och för detaljer |
| Sidbild direkt | hela sidan som bild (ColPali-stil) | ingen informationsförlust | större index, dyrare |
I praktiken vinner en hybrid nästan alltid: indexera både beskrivningen och bildvektorn, hämta från båda, och slå ihop.
Gyllene regeln: spara alltid originalet. Hämta på beskrivningen, men skicka bilden till modellen som ska svara. Annars svarar den på en sammanfattning av en sammanfattning.
Formellt
Reciprocal rank fusion (RRF) är standardsättet att slå ihop rankningar från olika index:
med . Den har två egenskaper som gör den svårslagen i praktiken: den behöver inga jämförbara poäng mellan indexen (bara ordningen), och den belönar konsensus — ett dokument som ligger tvåa i två index slår ett som ligger etta i ett och fyra i ett annat.
Utvärdera hämtning och generering var för sig. Annars vet du inte vad du ska laga:
| Steg | Mått | Fråga |
|---|---|---|
| Hämtning | recall@k per modalitet | fanns rätt underlag bland träffarna? |
| Rankning | MRR, nDCG | låg det tillräckligt högt upp? |
| Generering | grundningsgrad mot hämtat underlag | användes det som hämtades? |
| Helhet | svarsträffsäkerhet | blev svaret rätt? |
Mät recall per modalitet separat. Det vanligaste dolda felet i multimodal RAG är att textrecallen är 0,9 och bildrecallen 0,3, medan totalsiffran ser acceptabel ut — och alla frågor som kräver ett diagram misslyckas tyst.
Fyra praktiska beslut:
- Chunkning som respekterar layout. Klipp inte mitt i en tabell; håll bildtext ihop med bilden.
- Behåll relationen. En bild ska veta vilken text som omger den; en tabell vilken rubrik den hör till.
- Skicka originalet vidare. Beskrivningen är en sökingång, inte underlaget.
- Blanda inte rum. Två olika embeddingmodeller ger inte jämförbara avstånd — håll indexen isär och slå ihop på rangordning.
Kod
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
kl = QdrantClient(url="http://qdrant:6333")
for namn, dim in (("text", 768), ("bild", 512)):
kl.recreate_collection(namn, vectors_config=VectorParams(size=dim, distance=Distance.COSINE))
def indexera(sida):
# 1. text som vanligt
for i, stycke in enumerate(sida["stycken"]):
kl.upsert("text", [PointStruct(id=f"{sida['id']}-t{i}", vector=text_emb(stycke),
payload={"sida": sida["id"], "typ": "text", "innehall": stycke})])
# 2. bilder: BÅDE beskrivning i textindexet OCH bildvektor i bildindexet
for j, bild in enumerate(sida["bilder"]):
besk = vlm_beskriv(bild, sida["bildtext"][j])
kl.upsert("text", [PointStruct(id=f"{sida['id']}-bt{j}", vector=text_emb(besk),
payload={"sida": sida["id"], "typ": "bild",
"bildsokvag": bild.path, "beskrivning": besk})])
kl.upsert("bild", [PointStruct(id=f"{sida['id']}-b{j}", vector=clip_bild(bild),
payload={"sida": sida["id"], "typ": "bild",
"bildsokvag": bild.path})])
def rrf(rankningar, k=60, topp=8):
poang = {}
for r in rankningar:
for pos, d in enumerate(r):
poang[d] = poang.get(d, 0.0) + 1.0 / (k + pos + 1)
return sorted(poang, key=lambda d: (-poang[d], d))[:topp]
def hamta(fraga, k=8):
t = [p.id for p in kl.search("text", text_emb(fraga), limit=k)]
b = [p.id for p in kl.search("bild", clip_text(fraga), limit=k)]
return rrf([t, b], topp=k)
# Recall per modalitet — det som avslöjar tyst bildmisslyckande
def recall_per_modalitet(facit, k=8):
ut = {}
for typ in ("text", "bild", "tabell"):
f = [x for x in facit if x["typ"] == typ]
ut[typ] = round(sum(x["ratt_id"] in hamta(x["fraga"], k) for x in f) / max(len(f), 1), 3)
return ut
print(recall_per_modalitet(facit))
# {'text': 0.91, 'bild': 0.34, 'tabell': 0.62} ← totalen hade dolt detta
Behärskning innebär
- Indexerar flera modaliteter för hämtning
- Slår ihop rankningar från olika index
- Utvärderar hämtning och svar separat
Logga in för att göra övningarna och bygga upp din behärskning.
Källor
- arXiv — ColPali: Efficient Document Retrieval with Vision Language Models — arXiv (öppen åtkomst; licens per artikel)
- Cormack m.fl. — Reciprocal Rank Fusion (SIGIR 2009) — abstrakt fritt; författarkopior finns
- Qdrant — dokumentation (Apache-2.0) — Apache-2.0