Ga naar hoofdinhoud

Multimodale RAG: een praktische gids om te leren van documenten

Leer een multimodale RAG-pijplijn bouwen voor een visuele Q&A op IKEA-montagehandleidingen met de OpenAI API en ChromaDB-vector database.
· 11 min lezen

Verken met AI

Stel, je bent IKEA-meubels aan het monteren. Je staart naar stap 14 in de handleiding, maar je krijgt niet helder welke schroef waar hoort. Het diagram laat iets zien, maar het kwartje valt niet. 

Als je zou vragen: "Waar is het kleine metalen onderdeel in stap 14 voor?", dan schiet een standaard tekstgebaseerd retrieval-augmented generation (RAG)-systeem tekort. Het antwoord staat in het diagram zelf, niet in tekst op de pagina. IKEA-handleidingen zijn berucht woordloos.

Daar komt multimodale RAG om de hoek kijken.

In deze tutorial laat ik je stap voor stap zien hoe je een multimodaal RAG-systeem bouwt dat vragen kan beantwoorden over documenten met afbeeldingen, diagrammen en gemengde inhoud. We gebruiken IKEA-montagehandleidingen als testcase omdat die een goede stresstest zijn: vooral visueel, minimale tekst, en precies het soort materiaal waarbij mensen hulp nodig hebben.

We gebruiken overal de modellen van OpenAI: GPT-4o voor beeldbegrip en het genereren van antwoorden, en text-embedding-3-small voor het maken van doorzoekbare vectoren. Wil je RAG-pijplijnen bouwen met een ander framework, bekijk dan onze cursus RAG met LangChain.

Aan het eind heb je een werkend systeem dat je lokaal kunt draaien en kunt aanpassen voor je eigen documenten.

Wat is multimodale RAG?

Multimodale RAG breidt het standaard RAG-patroon uit om meer dan alleen tekst te verwerken. Het kernidee blijft hetzelfde, maar "context" omvat nu ook afbeeldingen, tabellen, diagrammen en andere niet-tekstuele content.

Waarom is dat belangrijk? Omdat echte documenten niet alleen uit tekst bestaan.

Denk aan wat je in de praktijk gebruikt: 

  • Technische handleidingen met bedradingsschema’s 
  • Onderzoeksartikelen met experimentele grafieken 
  • Financiële rapporten met omzetgrafieken
  • Productspecificaties met componentafbeeldingen 

Een systeem dat alleen tekst verwerkt, werkt met onvolledige informatie. Het is alsof je een recept probeert te begrijpen door alleen de ingrediëntenlijst te lezen en de foto’s over te slaan die laten zien hoe "voorzichtig omscheppen" eruitziet.

Traditionele RAG ziet een PDF als een stroom tekens. Het extraheert tekst, hakt die in stukken, embedt de stukken en klaar. Afbeeldingen worden volledig overgeslagen, of hooguit omgezet naar [IMAGE]-placeholdertokens. Tabellen veranderen in verminkte tekstregels die hun structuur verliezen.

Multimodale RAG pakt het anders aan: afbeeldingen worden beschreven of direct ge-embed, en tabellen worden geparsed naar gestructureerde formaten. Het retrievalsysteem kan een query als "netwerk-topologiediagram" koppelen aan zowel tekstuele beschrijvingen van netwerkarchitectuur als echte visuele diagrammen. Het generatiemodel kan een afbeelding bekijken en redeneren over wat erop staat.

Aspect

RAG met alleen tekst

Multimodale RAG

Invoertypen

Platte tekst, eventueel Markdown

Tekst, afbeeldingen, tabellen, diagrammen, gemengde documenten

Wat wordt ge-embed

Tekstchunks

Tekstchunks plus afbeeldingsbeschrijvingen of afbeeldingsembeddings

Generatiemodel

Elke LLM

Vision-language model (GPT-4o, Claude, Gemini)

Beste voor

Tekstzware documenten

Handleidingen, rapporten, slides, alles met visuele content

Als "multimodaal" nog vaag voelt als parapluterm, dan is deze blog over Multimodale AI een heldere uitleg met concrete voorbeelden.

Hoe werkt multimodale RAG?

De pijplijn heeft meer bewegende delen dan RAG met alleen tekst, maar de structuur is vergelijkbaar. Data wordt verwerkt en ge-embed, komt in een vectorstore terecht en wordt opgehaald zodra iemand een vraag stelt.

Multimodal RAG Pipeline

Zo loopt het proces:

Documentinname en parsing

Ruwe documenten (PDF’s, afbeeldingen, slides) worden opgesplitst in onderdelen. Je haalt tekst eruit, identificeert afbeeldingen, parseert tabellen en bepaalt hoe alles ruimtelijk samenhangt. Een diagram op pagina 5 kan een bijschrift eronder hebben en een verwijzing in de tekst op pagina 4. Die relaties moet je vastleggen.

Contentvoorbewerking

Verschillende contenttypen vragen om verschillende behandeling. Voor afbeeldingen heb je twee hoofdopties: tekstbeschrijvingen genereren met een visionmodel (zoals GPT-4o), of afbeeldingen direct embedden met een multimodale encoder (zoals CLIP). De beschrijvingsaanpak is eenvoudiger en werkt met standaard tekstembeddings. Tabellen zet je om naar gestructureerde formaten of vat je samen in natuurlijke taal.

Daarna wordt alles omgezet naar vectoren. Gebruik je de beschrijvingsaanpak, dan embed je simpelweg alle tekst (originele tekst plus gegenereerde afbeeldingsbeschrijvingen) met een standaard tekstembeddingmodel. Het belangrijkste is dat queries over visuele content nu op die beschrijvingen kunnen matchen.

Indexering en opslag 

Vectoren worden opgeslagen in een database met metadata, inclusief het brondocument, paginanummer, contenttype en, voor afbeeldingen, het pad naar het originele bestand. Dat afbeeldingspad heb je later nodig wanneer je het aan het generatiemodel wilt tonen.

Retrieval

Als er een query binnenkomt, embed je die met hetzelfde model, zoek je naar de dichtstbijzijnde buren en retourneer je de beste matches. De opgehaalde chunks kunnen verwijzen naar tekst, afbeeldingen of beide.

Generatie

Geef de query, samen met de opgehaalde context, aan een vision-language model. Als de opgehaalde chunks naar afbeeldingen verwijzen, laad die afbeeldingen dan en voeg ze toe aan de prompt. Het model ziet zowel de tekstcontext als de echte afbeeldingen, en genereert vervolgens een antwoord.

Bij de generatiestap blinkt GPT-4o echt uit. Het kan naar een IKEA-diagram met schroefplaatsing kijken en in gewone taal uitleggen wat er gebeurt. Het kan een grafiek lezen en de trend samenvatten. Het kan details in technische tekeningen opmerken die een tekstbeschrijving kan missen.

Multimodale RAG implementeren

We gaan bouwen. We maken een systeem dat vragen kan beantwoorden over IKEA-montagehandleidingen, stap voor stap van setup tot een werkende query-interface.

Stap 0: Framework en tools kiezen

Voor deze tutorial houden we het simpel:

Component

Keuze

Waarom

Embeddings

text-embedding-3-small

Goedkoop, goede kwaliteit, dezelfde API

Vectorstore

ChromaDB

Draait lokaal, geen setup, persistent

Visionmodel

GPT-4o

Topklasse voor beeldbegrip

Generatie

GPT-4o

Zelfde model, kan beide taken aan

Je kunt alternatieven inwisselen. Pinecone of Weaviate voor de vectorstore als je wilt opschalen. Claude of Gemini zijn goede alternatieven voor de generatie. De algemene architectuur blijft in die gevallen hetzelfde.

Waarom geen CLIP of ColPali voor embeddings? Dat kan. Daarmee embed je afbeeldingen direct zonder eerst tekstbeschrijvingen te genereren. Maar het voegt complexiteit toe en vereist vaak een GPU. De beschrijvingsaanpak werkt goed voor documentachtige content waarbij je semantische details wilt vastleggen, niet alleen visuele gelijkenis.

Stap 1: De omgeving opzetten

Laten we beginnen met dependencies en omgevingsvariabelen om onze projectomgeving op te zetten.

Dependencies installeren

Je hebt eerst poppler nodig voor PDF-verwerking.

  • Ubuntu/Debian: sudo apt-get install poppler-utils

  • macOS (via Homebrew): brew install poppler

  • Windows: Download van de releasepagina en voeg toe aan PATH

Installeer daarna de Python-packages. Naast ChromaDB hebben we pdf2image nodig om PDF’s naar afbeeldingen te converteren, pillow voor beeldbewerking en requests om de PDF-bestanden te downloaden.

pip install openai chromadb pdf2image pillow requests

API-sleutel en bestanden instellen

Om de modellen te gebruiken, stel je je OpenAI API-sleutel in als omgevingsvariabele.

Unix/Linux/macOS:

export OPENAI_API_KEY="your-api-key-here"

Windows: 

setx OPENAI_API_KEY="your-api-key-here"

Laten we nu de projectstructuur opzetten en de voorbeelddata ophalen. Het script bevat al de download-URL’s, maar als je de PDF’s eerst wilt bekijken, hier zijn de directe links naar zowel het MALM-bureau als het MALM-bedframe. 

Maak een bestand genaamd step1_setup.py:

"""
Step 1: Setup and Download Sample Data
"""
import os
import requests

# Create directories
for d in ["data", "data/images", "data/cache"]:
    os.makedirs(d, exist_ok=True)
print("✓ Created directories")

# Download IKEA assembly instructions
SAMPLE_PDFS = {
    "malm_desk.pdf": "https://www.ikea.com/us/en/assembly_instructions/malm-desk-white__AA-516949-7-2.pdf",
    "malm_bed.pdf": "https://www.ikea.com/us/en/assembly_instructions/malm-bed-frame-low__AA-75286-15_pub.pdf",
}

for filename, url in SAMPLE_PDFS.items():
    filepath = f"data/{filename}"
    if not os.path.exists(filepath):
        print(f"Downloading {filename}...")
        response = requests.get(url, timeout=30)
        with open(filepath, "wb") as f:
            f.write(response.content)
        print(f"  ✓ Saved ({len(response.content) / 1024:.1f} KB)")
    else:
        print(f"✓ {filename} already exists")

Voer het script uit om de initiële setup af te ronden:

python step1_setup.py

Je zou de IKEA-PDF’s moeten zien in je data/-map.

Stap 2: Documenten voorbewerken

IKEA-handleidingen zijn lastig. Ze bestaan vooral uit diagrammen met minimale tekst. We moeten PDF-pagina’s converteren naar afbeeldingen en dan GPT-4o gebruiken om te beschrijven wat elke pagina laat zien.

Maak step2_preprocess.py aan:

"""
Step 2: Preprocess Documents
Convert PDFs to images and generate descriptions with GPT-4o
"""
import os
import json
import base64
from pdf2image import convert_from_path
from openai import OpenAI

client = OpenAI()

DATA_DIR = "data"
IMAGES_DIR = "data/images"
CACHE_FILE = "data/cache/descriptions.json"


def convert_pdf_to_images(pdf_path):
    """Convert a PDF to page images."""
    pdf_name = os.path.splitext(os.path.basename(pdf_path))[0]
    images = convert_from_path(pdf_path, dpi=150)
    
    image_paths = []
    for i, image in enumerate(images):
        image_path = f"{IMAGES_DIR}/{pdf_name}_page_{i+1:03d}.png"
        image.save(image_path, "PNG")
        image_paths.append(image_path)
    
    return image_paths


def describe_page(image_path):
    """Use GPT-4o to describe an instruction page."""
    with open(image_path, "rb") as f:
        base64_image = base64.b64encode(f.read()).decode("utf-8")
    
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": """Describe this IKEA assembly instruction page in detail.
Include: step numbers, parts shown, tools needed, actions demonstrated, 
quantities (like "2x"), warnings, and part numbers if visible.
This description will help people find this page when they have questions."""
            },
            {
                "role": "user",
                "content": [
                    {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64_image}"}},
                    {"type": "text", "text": "Describe this instruction page."}
                ]
            }
        ],
        max_tokens=500
    )
    return response.choices[0].message.content


def main():
    # Load cache if exists
    cache = {}
    if os.path.exists(CACHE_FILE):
        with open(CACHE_FILE, "r") as f:
            cache = json.load(f)
    
    all_pages = []
    
    # Process each PDF
    for pdf_file in os.listdir(DATA_DIR):
        if not pdf_file.endswith(".pdf"):
            continue
        
        print(f"\nProcessing {pdf_file}...")
        pdf_path = f"{DATA_DIR}/{pdf_file}"
        
        # Convert to images
        print("  Converting to images...")
        image_paths = convert_pdf_to_images(pdf_path)
        print(f"  ✓ {len(image_paths)} pages")
        
        # Generate descriptions
        print("  Generating descriptions with GPT-4o...")
        for i, image_path in enumerate(image_paths):
            if image_path in cache:
                description = cache[image_path]
                status = "cached"
            else:
                description = describe_page(image_path)
                cache[image_path] = description
                # Save cache after each page
                with open(CACHE_FILE, "w") as f:
                    json.dump(cache, f, indent=2)
                status = "generated"
            
            all_pages.append({
                "source_pdf": pdf_file,
                "page_number": i + 1,
                "image_path": image_path,
                "description": description
            })
            print(f"    Page {i+1}/{len(image_paths)} [{status}]")
    
    # Save all page data
    with open("data/cache/all_pages.json", "w") as f:
        json.dump(all_pages, f, indent=2)
    
    print(f"\n✓ Processed {len(all_pages)} pages total")


if __name__ == "__main__":
    main()

Dit doen de functies:

  • convert_pdf_to_images(pdf_path): rendert elke PDF-pagina als een PNG-afbeelding op 150 DPI met pdf2image. Zo haal je diagrammen zonder selecteerbare tekst eruit, met consistente bestandsnamen zoals malm_desk_page_001.png.

  • describe_page(image_path): codeert de afbeelding als base64 en stuurt die naar GPT-4o met een gerichte system prompt. Het model geeft een gedetailleerde tekstbeschrijving terug met stappen, onderdelen en aantallen voor semantisch zoeken.

  • main(): regisseert alles: laadt gecachete beschrijvingen om herverwerking te vermijden, converteert PDF’s naar afbeeldingen, genereert waar nodig nieuwe beschrijvingen, slaat incrementele cache op en schrijft all_pages.json met metadata voor indexering.

Voer het script zo uit:

python step2_preprocess.py

Deze stap duurt enkele minuten. GPT-4o analyseert elke pagina en genereert een gedetailleerde beschrijving. Beschrijvingen worden gecachet, dus als je het opnieuw draait, slaat het reeds verwerkte pagina’s over.

Wat gebeurt hier? We zetten visuele content om in tekst die doorzoekbaar is. Als iemand vraagt: "Hoe bevestig ik de poten?", dan zoekt de retriever in deze beschrijvingen en vindt pagina’s die het bevestigen van poten noemen, ook al staat er op de originele pagina geen tekst.

Heb je de basispijplijn eenmaal staan, dan komen de volgende winstpunten uit retrievaltrucs, zoals hybrid search, reranking of query rewriting. Wil je daarmee kennismaken, lees dan deze blog over Geavanceerde RAG-technieken.

Stap 3: Multimodale data opslaan en indexeren

Nu embedden we de beschrijvingen en slaan we ze op in ChromaDB.

Maak step3_index.py aan:

"""
Step 3: Create Embeddings and Index
"""
import os
import json
from openai import OpenAI
import chromadb

client = OpenAI()

PAGES_FILE = "data/cache/all_pages.json"
CHROMA_DIR = "data/chroma_db"
COLLECTION_NAME = "ikea_instructions"


def get_embedding(text):
    """Get embedding from OpenAI."""
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return response.data[0].embedding


def main():
    # Load page descriptions
    with open(PAGES_FILE, "r") as f:
        pages = json.load(f)
    print(f"Loaded {len(pages)} pages")
    
    # Initialize ChromaDB with persistent storage
    chroma_client = chromadb.PersistentClient(path=CHROMA_DIR)
    
    # Delete existing collection if present
    try:
        chroma_client.delete_collection(name=COLLECTION_NAME)
    except:
        pass
    
    collection = chroma_client.create_collection(
        name=COLLECTION_NAME,
        metadata={"hnsw:space": "cosine"}
    )
    print(f"Created collection: {COLLECTION_NAME}")
    
    # Index each page
    print("\nCreating embeddings...")
    for i, page in enumerate(pages):
        # Combine source info with description
        text_to_embed = f"Source: {page['source_pdf']}, Page {page['page_number']}\n\n{page['description']}"
        
        embedding = get_embedding(text_to_embed)
        
        collection.add(
            ids=[f"page_{i}"],
            embeddings=[embedding],
            documents=[page["description"]],
            metadatas=[{
                "source_pdf": page["source_pdf"],
                "page_number": page["page_number"],
                "image_path": page["image_path"]
            }]
        )
        
        if (i + 1) % 5 == 0 or i == len(pages) - 1:
            print(f"  Indexed {i + 1}/{len(pages)} pages")
    
    print(f"\n✓ Index created with {collection.count()} documents")
    
    # Quick retrieval test
    print("\nTesting retrieval...")
    test_query = "How do I attach the legs?"
    query_embedding = get_embedding(test_query)
    results = collection.query(query_embeddings=[query_embedding], n_results=2)
    
    print(f"Query: '{test_query}'")
    for meta in results["metadatas"][0]:
        print(f"  → {meta['source_pdf']}, Page {meta['page_number']}")


if __name__ == "__main__":
    main()

Hier zet de functie get_embedding(text) tekst uit de beschrijving en broninfo om naar een dichte vector met OpenAI’s text-embedding-3-small-model. Met 1536 dimensies kunnen die vectoren semantische betekenis vangen voor similarity search.

main() laadt verwerkte pagina’s uit all_pages.json van de vorige stap, initialiseert ChromaDB met persistente opslag, verwijdert/maakt de collectie opnieuw aan, genereert embeddings voor elke pagina, slaat ze op met rijke metadata (bron, paginanummer, afbeeldingspad) en draait een snelle retrievaltest om te verifiëren dat de indexering werkt.

Voer uit:

python step3_index.py

De metadata is hier belangrijk. We slaan het image_path op zodat we later de echte afbeelding kunnen ophalen en aan GPT-4o kunnen tonen tijdens de generatie. De beschrijving wordt doorzocht, maar de afbeelding wordt getoond.

Stap 4: De RAG-pijplijn samenstellen

Hier is de volledige multimodale RAG-pijplijn. Een query wordt ge-embed, relevante pagina’s worden opgehaald, afbeeldingen geladen, en GPT-4o genereert een antwoord op basis van tekstbeschrijvingen en visuals.

Maak step4_query.py aan:

"""
Step 4: Query the Multimodal RAG System
"""
import os
import sys
import base64
from openai import OpenAI
import chromadb

client = OpenAI()

CHROMA_DIR = "data/chroma_db"
COLLECTION_NAME = "ikea_instructions"
TOP_K = 3

# Initialize ChromaDB
chroma_client = chromadb.PersistentClient(path=CHROMA_DIR)


def get_embedding(text):
    """Get embedding from OpenAI."""
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return response.data[0].embedding


def retrieve(query, top_k=TOP_K):
    """Find relevant instruction pages."""
    collection = chroma_client.get_collection(name=COLLECTION_NAME)
    query_embedding = get_embedding(query)
    
    results = collection.query(
        query_embeddings=[query_embedding],
        n_results=top_k,
        include=["documents", "metadatas"]
    )
    return results


def generate_answer(query, retrieved_results):
    """Generate answer using GPT-4o with retrieved images."""
    if not retrieved_results["ids"][0]:
        return "No relevant pages found.", []
    
    # Build image content for GPT-4o
    image_content = []
    sources = []
    
    for doc, metadata in zip(retrieved_results["documents"][0], retrieved_results["metadatas"][0]):
        image_path = metadata["image_path"]
        
        if not os.path.exists(image_path):
            continue
        
        with open(image_path, "rb") as f:
            base64_image = base64.b64encode(f.read()).decode("utf-8")
        
        image_content.append({
            "type": "image_url",
            "image_url": {"url": f"data:image/png;base64,{base64_image}"}
        })
        image_content.append({
            "type": "text",
            "text": f"[Page {metadata['page_number']} from {metadata['source_pdf']}]"
        })
        
        sources.append(f"{metadata['source_pdf']}, Page {metadata['page_number']}")
    
    # Generate with GPT-4o
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": """You answer questions about IKEA assembly based on instruction pages.
Be specific, reference page numbers when helpful, and say if something isn't clear from the images."""
            },
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": f"Question: {query}\n\nRelevant instruction pages:"},
                    *image_content,
                    {"type": "text", "text": "Answer based on these pages."}
                ]
            }
        ],
        max_tokens=600
    )
    
    return response.choices[0].message.content, sources


def answer_question(query):
    """Full RAG pipeline."""
    print(f"\nQuery: {query}")
    print("-" * 50)
    
    print("Retrieving relevant pages...")
    results = retrieve(query)
    print(f"  Found {len(results['ids'][0])} pages")
    
    print("Generating answer...")
    answer, sources = generate_answer(query, results)
    
    return answer, sources


def main():
    # Check index exists
    try:
        collection = chroma_client.get_collection(name=COLLECTION_NAME)
        print(f"Connected to index ({collection.count()} documents)")
    except:
        print("ERROR: Index not found. Run step2_index.py first.")
        return
    
    # Single query from command line
    if len(sys.argv) > 1:
        query = " ".join(sys.argv[1:])
        answer, sources = answer_question(query)
        print(f"\nAnswer:\n{answer}")
        print(f"\nSources: {', '.join(sources)}")
        return
    
    # Interactive mode
    print("\nAsk questions about IKEA assembly. Type 'quit' to exit.\n")
    
    while True:
        try:
            query = input("Question: ").strip()
        except KeyboardInterrupt:
            break
        
        if not query or query.lower() in ["quit", "exit"]:
            break
        
        answer, sources = answer_question(query)
        print(f"\nAnswer:\n{answer}")
        print(f"\nSources: {', '.join(sources)}\n")


if __name__ == "__main__":
    main()

Uitleg van de kernfuncties:

  • get_embedding(text): genereert een vectorembedding voor de query met text-embedding-3-small, voor semantische similarity search in ChromaDB.​

  • retrieve(query, top_k=TOP_K): embedt de query, bevraagt de collectie op de top-k dichtste buren en retourneert documenten en metadata (bijv. afbeeldingspaden) via include=["documents", "metadatas"].​

  • generate_answer(query, retrieved_results): laadt topafbeeldingen als base64, bouwt een multimodale GPT-4o-prompt met afwisselend paginalabels en afbeeldingen, en genereert een gedetailleerd antwoord met IKEA-specifieke instructies.​

  • answer_question(query): regisseert retrieval en generatie, met voortgangslog.

  • main(): controleert of de index bestaat, ondersteunt CLI-single queries of interactieve chatmodus ("quit", "exit" of Ctrl+C om te stoppen).

Stap 5: De RAG-pijplijn draaien

Je kunt het script draaien en je vraag intypen, of het direct uitvoeren met een specifieke vraag, zo:

python step4_query.py "How do I attach the legs to the desk?"
Retrieving relevant pages...
Found 3 pages
Generating answer...

Answer:
To attach the legs to the MALM desk, follow these steps shown on pages 5-6:

1. First, insert the wooden dowels into the pre-drilled holes on the leg 
   pieces (you'll see "4x" indicating four dowels are needed)

2. Apply the cam locks to the desk top using the Phillips screwdriver

3. Align each leg with the corresponding holes on the underside of the desk

4. Tighten the cam locks by rotating them clockwise until the legs are 
   firmly attached

The instructions show this should be done with the desk top face-down on 
a soft surface to avoid scratching.

Sources: malm_desk.pdf, Page 5, malm_desk.pdf, Page 6, malm_desk.pdf, Page 7

Ziet er goed uit! Het systeem haalt pagina’s op op basis van de beschrijvingen, laadt de daadwerkelijke afbeeldingen en GPT-4o genereert een antwoord door direct naar de diagrammen te kijken.

Evaluatie van multimodale RAG

Een multimodaal RAG-systeem bouwen is één ding. Weten of het goed werkt is iets anders. En het betrouwbaar in productie draaien voegt nog een laag uitdagingen toe. 

De standaard RAG-metrics blijven gelden, maar je moet ze uitbreiden voor visuele content.

Retrievalkwaliteit

Precision@k meet hoeveel van je opgehaalde pagina’s daadwerkelijk relevant waren. Het bestraft irrelevante pagina’s in top-k, maar voor generatie is het hebben van de sleutelafbeelding belangrijker dan perfecte top-k.

Recall@k meet hoe vaak de juiste pagina’s in je top k-resultaten verschijnen. Dit is relevanter voor onze IKEA-RAG usecase, omdat we willen dat het systeem minstens één relevante pagina vindt in de top-k (bijv. k=3) resultaten. Zo krijgt GPT-4o visuele context om correct te antwoorden, zelfs als andere resultaten ruis zijn. 

Voor ons IKEA-systeem bouw je een testset met bijvoorbeeld 50 vragen en label je de "juiste" pagina’s per vraag. Vervolgens kun je evalueren met de volgende functie:

def evaluate_retrieval(test_cases, top_k=3):
    """
    test_cases: [{"query": str, "relevant_pages": ["image_path1", ...]}]
    """
    hits = 0
    for test in test_cases:
        results = retrieve(test["query"], top_k=top_k)
        retrieved = [m["image_path"] for m in results["metadatas"][0]]
        if any(p in retrieved for p in test["relevant_pages"]):
            hits += 1
    return hits / len(test_cases)

Generatiekwaliteit

Iets wat lastiger automatisch te meten is, is de kwaliteit van de generatie. 

Je kunt mensen antwoorden laten beoordelen op juistheid en behulpzaamheid, of een LLM als jurylid gebruiken om te evalueren of antwoorden kloppen gegeven de bronafbeeldingen. 

Controleer bij diagrammen met genummerde stappen of het model de juiste stapnummers toekent.

Multimodaal-specifieke checks

Verdere evaluaties kunnen de volgende vragen beantwoorden:

  • Vindt het systeem beeldgebaseerde antwoorden wanneer het antwoord visueel is? 

  • Als het model iets in een afbeelding aanhaalt, is die verwijzing dan accuraat? 

Deze vereisen handmatige review, in elk geval voor een steekproef aan queries.

Best practices voor multimodale RAG

Wil je een productieklare multimodale RAG-pijplijn bouwen, houd dan het volgende in gedachten:

  • Voorbewerkingskosten lopen op: Vision-calls voor afbeeldingsbeschrijving zijn het dure deel. Voor een grote documentencorpus: batchverwerken buiten piekuren en beschrijvingen agressief cachen. Overweeg een goedkoper model (in ons geval bijv. GPT-4o-mini) voor beschrijvingsgeneratie als de kwaliteit voldoet aan je usecase.

  • Query-latency telt: GPT-4o met afbeeldingen kost 5 tot 10 seconden per query. Streaming helpt gebruikers snel deelresultaten zien, en het cachen van veelgestelde vragen helpt bij herhaalde queries. Twee pagina’s ophalen in plaats van vijf kan schelen qua tokens als dat al genoeg context is.

  • Monitor wat ertoe doet: Volg retrieval-latency (moet onder 100 ms liggen voor kleine collecties), generatie-latency (reken op 5 tot 10 seconden met afbeeldingen), API-kosten per query en gebruikersfeedback als je die verzamelt.

  • Foutafhandeling redt je: Er gaat wel eens iets mis. Bouw fallbacks, zoals in de onderstaande code.

def answer_question_safe(query):
    """Wrapper with error handling."""
    try:
        return answer_question(query)
    except Exception as e:
        print(f"Error: {e}")
        # Fall back to text-only using descriptions
        results = retrieve(query)
        if results["documents"][0]:
            context = results["documents"][0][0][:500]
            return f"Based on the instructions: {context}...", []
        return "Sorry, couldn't process that question.", []

Als je pijplijn meerdere stappen moet nemen (zoeken → verifiëren → opnieuw zoeken → antwoorden), dan helpen agentic RAG-patronen je om dat netjes te structureren.

Conclusie

Multimodale RAG breidt retrieval-augmented generation uit om het volledige spectrum aan content in real-world documenten aan te kunnen, inclusief afbeeldingen, diagrammen, tabellen en grafieken. 

We hebben het succesvol gebruikt om IKEA-handleidingen te bevragen, die notoir visueel zijn, waar een aanpak met alleen tekst niet had gewerkt. De hele pijplijn draait lokaal behalve de API-calls.

De kerninzichten: je hebt geen complexe multimodale embeddings nodig om dit te laten werken. Door goede tekstbeschrijvingen van visuele content te genereren, kun je standaard tekstretrieval gebruiken om relevante afbeeldingen te vinden en vervolgens een vision-language model het zware werk laten doen tijdens de generatie.

Wil je voortbouwen op wat we hier hebben behandeld? Start dan vandaag nog met de AI Engineer-carrièreroute!

Multimodal RAG FAQs

What types of data can multimodal RAG handle?

Afbeeldingen, PDF’s met grafieken, tabellen, diagrammen, audio (via transcripties) en videokeyframes. Kortom: alle content waar je wilt dat AI naar kan verwijzen bij het beantwoorden van vragen.

Do I need separate vector databases for different modalities?

Niet per se. De meeste productiesystemen gebruiken één enkele vectorstore met één embeddingruimte. Je kunt alles in dezelfde dimensionale ruimte embedden zodat retrieval werkt over contenttypes heen.

Can I use OpenAI for the entire pipeline?

Ja. GPT-4o verwerkt zowel beeldbegrip als antwoordgeneratie, terwijl text-embedding-3-small de vectorskant afhandelt. Je kunt een complete multimodale RAG bouwen binnen het OpenAI-ecosysteem.

What's the biggest challenge in multimodal RAG?

Afstemming. Tekst, afbeeldingen en gestructureerde data in dezelfde embeddingruimte krijgen zodat retrieval echt werkt over modaliteiten heen. De chunkingstrategie komt op een goede tweede plaats.

How much does it cost to run?

Voor de IKEA-tutorial hier, reken op zo’n $2 totaal. Het dure deel zijn de GPT-4o-calls voor afbeeldingsbeschrijving tijdens de voorbewerking. Eenmaal geïndexeerd zijn queries goedkoop.

Josep Ferrer

Auteur

Josep Ferrer

Josep is een freelance Data Scientist die zich richt op Europese projecten, met expertise in dataopslag, -verwerking, geavanceerde analyses en impactvolle data storytelling. 

Als docent geeft hij Big Data in de masteropleiding aan de Universiteit van Navarra en deelt hij inzichten via artikelen op platforms als Medium, KDNuggets en DataCamp. Josep schrijft ook over Data en Tech in zijn nieuwsbrief Databites (databites.tech). 

Hij heeft een bachelor in Engineering Physics van de Polytechnische Universiteit van Catalonië en een master in Intelligent Interactive Systems van de Pompeu Fabra-universiteit.

Onderwerpen

Gerelateerd