Hoppa till innehållet
AI-grafen
F· AI engineeringmultimodala-modeller· ca 90 min· volatil — kontrolleras ofta· verifierad 2026-09-20

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:

StrategiVad som indexerasStyrkaSvaghet
Beskriv och indexera texten VLM-genererad beskrivning av varje bildfungerar med befintlig textsökningbeskrivningen blir ett filter — det som inte nämns går inte att hitta
Gemensamt vektorrumbildvektorer i samma rum som textfråganhittar visuellt utan mellanledtrubbigt för text i bilden och för detaljer
Sidbild direkthela sidan som bild (ColPali-stil)ingen informationsförluststö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:

poa¨ng(d)=∑r∈R1k+rankr(d)\mathrm{poäng}(d) = \sum_{r \in R} \frac{1}{k + \mathrm{rank}_r(d)}

med k≈60k \approx 60. 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:

StegMåttFråga
Hämtningrecall@k per modalitetfanns rätt underlag bland träffarna?
RankningMRR, nDCGlåg det tillräckligt högt upp?
Genereringgrundningsgrad mot hämtat underlaganvändes det som hämtades?
Helhetsvarsträffsäkerhetblev 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:

  1. Chunkning som respekterar layout. Klipp inte mitt i en tabell; håll bildtext ihop med bilden.
  2. Behåll relationen. En bild ska veta vilken text som omger den; en tabell vilken rubrik den hör till.
  3. Skicka originalet vidare. Beskrivningen är en sökingång, inte underlaget.
  4. 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

Alla källor och licenser