← Blog

RAG

Gemma 4 QAT op een MacBook: fallback-brein voor zorg-RAG

Het is dinsdag 16:12 in Utrecht. De cloud-key geeft 529. De triageverpleegkundige wacht. Dit is het fallback-brein dat we bouwden zodat het systeem blijft antwoorden.

Jacob Molkenboer· Oprichter · A Brand New Company· 6 jun 2026· 9 min
Houten kaartenbak met messing tab, felgroene papieren vlag op een kaart, linnen grootboek en rode lakzegel op ivoor papier.

Het is dinsdag 16:12 in Utrecht. De dienstdoende verpleegkundige bij een regionaal huisartsencollectief typt een vraag in de triage-assistent die we vorige winter bouwden: "Patiënt 64j, NOAC sinds 2 maanden, INR niet relevant, bloeding tandvleesrand, hoeveel uur stoppen?" De cloud-key geeft 529. Alweer. Ergens staat een statuspagina op rood, de wachtrij loopt op, en een jaar geleden hadden we een spinner laten draaien tot de verpleegkundige het opgaf en het papieren protocol uit de kast trok.

Dit is de field guide bij het fallback-brein dat we in een lang weekend in april in het systeem hebben gehangen. Het brein draait op de MacBook Air van de receptie. Het is trager dan het cloudmodel. Het is dommer. Het is ook genoeg om de wachtrij door te laten lopen terwijl de incidentteller bij de upstream-provider de twee uur passeert.

Waarom een lokaal fallback-brein

Drie redenen, in volgorde van gewicht.

Eén: uptime in de zorg is geen marketinggetal. Een huisartsenpraktijk die op piekmomenten 12 spreekkamers draait, wil geen assistent voor klinische beslissingen die op een spinner blijft hangen. We hadden een antwoordpad nodig dat niet afhangt van de statuspagina van één provider.

Twee: de AVG behandelt medische gegevens als bijzondere categorie. Dezelfde vraag door een lokaal model halen als de cloud eruit ligt verandert de juridische positie niet (voor beide paden ligt er een verwerkersovereenkomst), maar het betekent wel dat tijdens het fallback-venster geen patiëntgegevens het pand verlaten. Dat leg je makkelijker uit aan een privacy officer dan "we hebben omgeleid naar een backup-provider in Frankfurt waar je nog nooit van gehoord hebt".

Drie: kosten. Het fallback-pad is een MacBook die de praktijk toch al had, plus stroom. Het cloudpad rekent per call af. Toen we dit doorrekenden voor een collectief van 40 huisartsen, verdiende het lokale fallback-pad het bouwwerk in acht weken terug, alleen al door rate-limit retries op te vangen.

Waarom Gemma 4 QAT en niet de rest

Quantization-aware training is niet nieuw. De truc is dat je het model traint met quantization in de loop, zodat de 4-bits gewichten waarmee het uiteindelijk live gaat ook de gewichten zijn waar het mee heeft leren werken. Het resultaat is een klein model dat veel minder hard inzakt dan een achteraf gequantizeerde variant. Google publiceerde eind mei de Gemma 4 QAT-familie, en dat we erop sprongen heeft een simpele reden: de 8B QAT-variant draait op bruikbare snelheid op een base-spec Apple silicon-laptop, en scoort op instruction-following benchmarks vlakbij het ongequantizeerde 8B-model.

We hebben drie alternatieven afgewogen voordat we kozen.

Een kleiner gehost model als fallback. Klinkt logisch op papier, maar dan verschuif je de afhankelijkheid naar de uptime van een tweede provider en heb je in plaats van één privacyverhaal er twee. Geschrapt.

Een groter lokaal model op een Mac Studio. Betere antwoorden, lastiger uit te rollen. De meeste van onze zorgklanten hebben een vloot MacBooks; niemand heeft een ongebruikte Mac Studio op de receptie staan. Geschrapt.

Een 4B QAT-model. Sneller, past ruim op een machine met 16 GB, maar zijn Nederlandse medische redenatie kelderde in onze acceptatietests. De 8B QAT haalde de ondergrens; de 4B niet.

De MacBook zelf

De referentiemachine in de praktijk is een MacBook Air uit 2024, M3, 16 GB unified memory, 512 GB SSD. We hebben hem bewust gekozen met de oude machine van de receptie in het achterhoofd: dit is middenmoot, geen workstation. Hij draait tegelijk het afsprakensysteem van de praktijk, een Chrome-venster, én het fallback-brein.

Verwachtingen, gemeten op precies die machine met het model geladen en de laptop op netstroom:

  • First-token latency: 700 tot 1100 ms koud, 250 tot 400 ms warm.
  • Doorvoersnelheid stabiel: 22 tot 28 tokens per seconde.
  • Resident memory met het 8B QAT-model geladen en een 4k-context: ongeveer 6,2 GB.
  • Ventilatorgeluid tijdens een lange generatie: hoorbaar, niet gênant.

Voor een triage-antwoord van 120 tot 180 tokens ziet de gebruiker een compleet antwoord binnen 6 tot 9 seconden. Het cloudpad doet er op een goede dag 1,8 seconden over. De verpleegkundigen waar we tijdens de pilot mee meeliepen klaagden niet over dat verschil; ze klaagden over spinners.

Aansluiten met Ollama

Je kunt het model serveren met Ollama, direct met llama.cpp, of met MLX. Wij kozen Ollama om één reden: het is het pakket dat de IT-leverancier van de praktijk al ondersteunt. De runtime van llama.cpp is sneller als je hem tuned, maar de tijd die je wint op het draaien van het model verlies je weer aan uitleggen hoe iemand anders hem moet herstarten.

brew install ollama
brew services start ollama

# Pull the 8B instruction-tuned QAT variant
ollama pull gemma4:8b-it-qat-q4_0

# Smoke test in Dutch
ollama run gemma4:8b-it-qat-q4_0 \
  "Wat is de standaard wachttijd na een NOAC-stop voor een tandheelkundige ingreep?"

Twee dingen die je goed moet zetten voordat je dit voor gebruikers neerzet.

Eén: zet het Ollama-proces vast zodat de OS hem niet uit het geheugen swapt als de receptie 40 Chrome-tabs opent. Een kleine launchd-plist met een hogere Nice-prioriteit is genoeg; cgroups hadden we niet nodig.

Twee: vergroot het contextvenster. De default is ruim voor chat, maar krap voor een RAG die zes tot acht opgehaalde chunks richtlijntekst meestuurt:

cat > ~/Modelfile <<'EOF'
FROM gemma4:8b-it-qat-q4_0
PARAMETER num_ctx 8192
PARAMETER temperature 0.2
PARAMETER repeat_penalty 1.05
SYSTEM "Je bent een klinische triage-assistent voor Nederlandse huisartsen. Antwoord beknopt in het Nederlands. Citeer altijd de bron uit de gegeven context."
EOF

ollama create triage-fallback -f ~/Modelfile

De router die beslist wanneer hij omschakelt

De interessante code is niet de model-call. Het is de router die vóór beide breinen zit en bepaalt welke deze specifieke vraag beantwoordt.

Onze regel is simpel: probeer de cloud, geef hem een harde 6 seconden voor het eerste token, en als dat niet lukt, stream je vanaf het lokale model. Niet opnieuw de cloud proberen zodra je voor lokaal hebt gekozen. De hapering die de gebruiker voelt als je een stream opnieuw start is erger dan op het tragere brein afmaken.

import asyncio, httpx, json
from typing import AsyncIterator

CLOUD_URL = "https://api.anthropic.com/v1/messages"
LOCAL_URL = "http://127.0.0.1:11434/api/chat"
FIRST_TOKEN_BUDGET = 6.0  # seconds

async def stream_cloud(messages, key) -> AsyncIterator[str]:
    headers = {"x-api-key": key, "anthropic-version": "2023-06-01"}
    body = {"model": "claude-sonnet-4-7", "messages": messages,
            "max_tokens": 600, "stream": True}
    async with httpx.AsyncClient(timeout=30.0) as c:
        async with c.stream("POST", CLOUD_URL, headers=headers, json=body) as r:
            r.raise_for_status()
            async for line in r.aiter_lines():
                if line.startswith("data: "):
                    yield line[6:]

async def stream_local(messages) -> AsyncIterator[str]:
    body = {"model": "triage-fallback", "messages": messages, "stream": True}
    async with httpx.AsyncClient(timeout=120.0) as c:
        async with c.stream("POST", LOCAL_URL, json=body) as r:
            r.raise_for_status()
            async for line in r.aiter_lines():
                if line:
                    yield json.loads(line).get("message", {}).get("content", "")

async def answer(messages, key) -> AsyncIterator[str]:
    cloud = stream_cloud(messages, key)
    try:
        first = await asyncio.wait_for(cloud.__anext__(), FIRST_TOKEN_BUDGET)
        yield first
        async for chunk in cloud:
            yield chunk
        return
    except (asyncio.TimeoutError, httpx.HTTPError, StopAsyncIteration):
        pass  # fall through to local
    async for chunk in stream_local(messages):
        yield chunk

Die 6 seconden zijn een afgestemd getal, geen default. Onder de 4 seconden schakel je te vaak over op gezonde dagen waarop de cloud gewoon traag is. Boven de 8 seconden merkt de verpleegkundige dat het brein overgaat en vraagt ze wat er net gebeurde.

Let op

Laat de failover zichtbaar zijn voor de gebruiker. Wij renderen een klein cursief regeltje onder het antwoord: "Antwoord gegeven door lokaal model, cloud niet bereikbaar." Als je de overdracht verbergt, leer je gebruikers dat ze twee verschillende antwoordkwaliteiten als één moeten vertrouwen, en dan krijgt het verkeerde model de schuld als er een fout valt.

Acceptatietests voor Nederlandse medische inhoud

De fallback mag slechter zijn dan de cloud. Hij mag niet op gevaarlijke manieren fout zijn. We hebben een vaste set van 86 evaluatievragen, gehaald uit geanonimiseerde echte triagevragen, elk met een goudstandaard-antwoord van een huisarts en een lijst feiten die in het antwoord moeten zitten.

Twee metrics bepalen of we van model wisselen.

Fact recall. Van de 86 vragen moet het lokale model 90% van de must-contain-feiten raken. Het cloudmodel zit rond de 96%. Onder de 90% gaat het fallback-pad niet live; dan routen we direct door naar een verpleegkundige.

Hazard rate. Van de 86 vragen mag geen enkel antwoord een klinisch gevaarlijke uitspraak bevatten (verkeerde dosering, verkeerde contra-indicatie, verkeerd stop-venster). Dit wordt beoordeeld met een tweede pass door een sterker model en een handmatige steekproef. Is de hazard rate niet nul, dan gaat het model niet live als fallback voor die vraagcategorie, einde discussie.

De 8B QAT haalde 91% fact recall en nul hazards op de huidige eval. We draaien hem wekelijks opnieuw tegen de live retrieval-index, want de index verandert ook als het model dat niet doet.

Wat je inlevert met een lokale fallback

Het lokale model is op drie punten slechter, in volgorde van hoe zwaar ze wegen in de zorg.

Synthese over lange context. Haal acht chunks richtlijntekst op en vraag om één antwoord met correct geciteerde bronnen. De 8B QAT laat de citatie-accuratesse zo'n zes punten zakken vergeleken met de cloud. We compenseren dat door de retriever vooraf harder te laten werken: minder, maar betere chunks.

Meerstaps redeneren. "Bereken de dosisaanpassing voor een 72-jarige met eGFR 38 op rivaroxaban." De cloud doet dat foutloos. Het QAT-model laat soms de juiste redenatie zien en eindigt op een verkeerd getal. Voor vragen met rekenwerk routen we eerst door een deterministische calculator en laat het model de tekst eromheen opbouwen.

Gestructureerde output. Minder relevant in een klinische frontend, maar als je dezelfde router hergebruikt voor een tool voor developers, heeft het QAT-model strikte JSON-schema-validatie aan de uitgang nodig.

Het kleinste dat je vandaag kunt doen

Als je een RAG draait en nog geen fallback-pad hebt, is de goedkoopste eerste stap een audit van vijf minuten: open de error logs van de laatste 30 dagen, tel de cloud-timeouts en 5xx-responses, vermenigvuldig met het aantal getroffen gebruikers. Als dat getal je gênant voorkomt, is de volgende stap brew install ollama op de dichtstbijzijnde laptop. Het model is gratis, de runtime is gratis, en het enige tussen jou en een werkende fallback in is een uur router-code en een weekend evaluatiewerk.

Toen we dit in de triage-assistent voor een huisartsencollectief in de Randstad hingen, zat het zware werk niet in het model of de router; het zat in het opschrijven van die 86 evaluatievragen samen met een arts die haar werk nog nooit op die manier had bekeken. Wil je hulp bij het bouwen van AI-agents die moeten blijven antwoorden als de cloud rood kleurt: we hebben het gedaan.

Kern

Een lokaal fallback-brein is geen kleiner cloudmodel. Het is een ander product met een ander kwaliteitscontract. Lever het zo, of lever het niet.

FAQ

Kan ik dit draaien op een MacBook Air met 8 GB?

Nee. 8 GB is krap voor een 8B QAT-model met een 4k-context. De OS gaat swappen en je throughput zakt van 25 tokens per seconde naar enkele cijfers. 16 GB is de praktische ondergrens voor de 8B-variant.

Is een lokaal model standaard AVG-conform?

Lokale verwerking is één stuk compliance, niet het hele verhaal. Je hebt nog steeds verwerkersovereenkomsten, toegangslogs, bewaartermijnen en een DPIA nodig. Een model dat on-device draait vervangt het papierwerk niet.

Waarom niet gewoon een kleiner gehost model als fallback?

Twee providers betekent twee storingen en twee privacyverhalen. Een lokaal model brengt de failure modes terug naar één machine die je zelf beheert, en tijdens het fallback-venster verlaten patiëntgegevens het pand niet.

Hoe vaak schakelt die failover in productie eigenlijk in?

Ongeveer 0,4% van de queries de afgelopen zes maanden bij het huisartsencollectief. Meestal zijn het rate-limit-vensters van 60 tot 90 seconden, geen volledige storingen, maar het zijn wel de momenten waarop het vertrouwen van gebruikers het hardst weglekt.

ragai agentsknowledge basearchitectureoperationstooling

Iets bouwen?

Start een project