AI agents
vLLM en Qdrant migratie: weg van OpenAI Assistants in 4 weken
Hosted AI werd twee jaar lang elk kwartaal goedkoper, en toen niet meer. Hier is het vierwekenplan om een Rotterdamse logistiek-SaaS van OpenAI Assistants naar vLLM en Qdrant te halen.

Een factuur van 17.000 euro waar niemand op had gerekend
De CTO van een Rotterdamse logistiek-SaaS van 38 man stuurde de OpenAI-factuur door om 22:47 op een dinsdag in april. De regel voor hun Assistants-gebaseerde zendingclassificatie-agent was 41% gestegen ten opzichte van de maand ervoor, terwijl het gebruik gelijk bleef. Hun CFO had al een thread geopend.
Het team had eind 2024 op de Assistants API gebouwd, omdat de file_search tool betekende dat niemand over embeddings of chunking hoefde na te denken. Het werkte. Achttien maanden lang daalden ook de facturen op schema, twee verlagingen per jaar, als een klok. Toen stopten ze in Q1 2026, en de curve boog de andere kant op. Hacker News pikte het frame AI vertraagt op in dezelfde week dat de factuur binnenkwam. De capaciteit was krap, prijzen van frontier-modellen waren stilletjes naar boven bijgesteld, en het verwachte kortingspad was uitgebleven.
Dit is het draaiboek waarmee we dat bedrijf in vier weken van OpenAI Assistants naar zelf-gehoste vLLM plus Qdrant verhuisden. De kostencijfers van beide kanten staan onderaan. Net als de twee storingen die het project bijna sloopten in week twee en week vier.
Wat de agent eigenlijk deed
De agent las ongestructureerde zendingsdocumenten in (vrachtbrieven, douaneformulieren, leveranciersfacturen, vooral PDF en JPG) en leverde een gestructureerd JSON-record voor het dashboard van het operations team. Hij beantwoordde ook open vragen van operators in het magazijn via een dunne chat-UI. Drie dingen telden:
- ~24.000 documenten per maand, met pieken tot ~1.800 per uur tijdens het ochtendvenster voor de douane.
- Retrieval tegen een kennisbank van 380k chunks met tariefcodes, landenregels en de eigen SOP's van het bedrijf.
- Gemiddeld 11 tool calls per agent-run (PDF parsen, code opzoeken, valideren, terugschrijven).
De OpenAI-factuur op het beslismoment was 11.400 euro in maart 2026, met een projectie van 14.200 voor april. Het bestuur wilde dat onder de 4.000.
Eerst meten, dan migreren
Je kunt niet bijsturen wat je niet meet. Week één ging niet over migratie. Het ging over instrumentatie. We zetten een dunne OpenTelemetry-wrapper rond elke Assistants-call en stuurden traces naar een lokale Tempo-instance.
from opentelemetry import trace
tracer = trace.get_tracer("agent.shipment")
with tracer.start_as_current_span("assistant.run") as span:
span.set_attribute("doc.id", doc_id)
span.set_attribute("doc.pages", page_count)
run = client.beta.threads.runs.create_and_poll(
thread_id=thread.id,
assistant_id=ASSISTANT_ID,
)
span.set_attribute("run.tokens.prompt", run.usage.prompt_tokens)
span.set_attribute("run.tokens.completion", run.usage.completion_tokens)
span.set_attribute("run.tool_calls", len(run.tool_calls or []))
Na vijf dagen wisten we de dingen die de factuur alleen je niet kan vertellen. De p50-run was 3,2 seconden, de p99 was 41. 8% van de runs waren herhalingen, veroorzaakt door een retry-loop op een tijdelijke timeout. 23% van alle opgehaalde chunks werd door het model nooit geciteerd. De agent betaalde voor context die hij nooit gebruikte.
Die drie getallen werden onze SLO's voor de migratie. De mediane latency mocht 3,5s niet overschrijden, de p99 niet boven de 45s, en de retrieval recall@10 niet onder de baseline van 0,81 zakken, gemeten tegen een golden set van 240 vragen.
De nieuwe stack op papier
vLLM als inference server, met Qwen2.5-72B-Instruct gequantiseerd naar AWQ 4-bit op twee H100's, gehuurd via Hetzner's GPU-aanbod voor 2.890 euro per maand all-in. Qdrant op een bare-metal node met 16 vCPU's voor de vector store. BGE-M3 als embedding-model, geserveerd vanuit hetzelfde vLLM-cluster als secundair model. LiteLLM als OpenAI-compatibele proxy voor vLLM, zodat de applicatiecode niet hoefde te weten wat erachter zat.
We kozen deze stack niet omdat we er verliefd op waren. We kozen 'm omdat vLLM het OpenAI chat-completions schema spreekt, Qdrant een stabiele Python client heeft en goede HNSW-prestaties op onze schaal, en omdat BGE-M3 het embedding-model was dat op onze golden set het dichtst bij text-embedding-3-large kwam (0,79 vs 0,81). Saai is prima.
Week één: shadow mode
Elke productie-request naar OpenAI werd asynchroon gedupliceerd naar de nieuwe stack, met resultaten die we in een aparte tabel vergeleken. Geen wijziging die de gebruiker zag.
async def run_agent(doc):
primary = await openai_run(doc)
asyncio.create_task(shadow_run(doc, primary))
return primary
async def shadow_run(doc, primary):
try:
candidate = await vllm_run(doc)
await shadow_log.insert({
"doc_id": doc.id,
"primary_json": primary.json,
"candidate_json": candidate.json,
"fields_match": diff_fields(primary.json, candidate.json),
"latency_ms": candidate.latency_ms,
})
except Exception as e:
await shadow_log.insert({"doc_id": doc.id, "error": str(e)})
De shadow-data na vijf dagen: 91,2% van de gestructureerde velden kwam exact overeen met de OpenAI-output, 6,1% was equivalent (andere opmaak, zelfde waarde), 2,7% waren echte verschillen. Het team beoordeelde die 2,7% met de hand. Twee derde daarvan waren gevallen waarin de nieuwe stack gelijk had en OpenAI fout zat. Dat noteerden we en we gingen door.
Week twee: de Qdrant-overgang en de eerste storing
Het overzetten van retrieval zou de makkelijke helft zijn. Op een zondag berekenden we de embeddings opnieuw, 380k chunks geladen in één Qdrant-collectie met HNSW, m=16, ef_construct=200. De productie-reads gingen om 04:00 op maandag om achter een feature flag.
Om 09:14 zag het operations team dat ongeveer 40% van de zendingsclassificaties 'onbekend tarief' teruggaf. De recall@10 was ingestort van 0,81 naar 0,47.
De oorzaak was niet Qdrant. Het was de wissel van embedding-model. text-embedding-3-large produceert vectors van 3.072 dimensies. BGE-M3 produceert er 1.024. Het corpus hadden we correct opnieuw ge-embed, maar het productie-querypad riep nog steeds OpenAI aan voor de query-embedding, die vervolgens werd afgekapt naar 1.024 dimensies door een verkeerd geconfigureerde numpy-cast in een helper waar al een jaar niemand aan had gezeten. De helft van het semantische signaal werd stilletjes weggegooid.
Als je van embedding-model wisselt, doe dan dual-write naar de nieuwe vectors, maar houd de oude collectie live tot beide kanten van retrieval (corpus én queries) op het nieuwe model draaien. Een mismatched embedding-pipeline is veruit de meest voorkomende manier waarop een RAG-migratie stilletjes faalt.
De fix kostte 90 minuten zodra we 'm doorhadden. We draaiden de flag terug, lieten de query-embedder wijzen naar het BGE-M3 endpoint op vLLM, en flipten opnieuw. De recall kwam terug op 0,79. De storing kostte ongeveer vier uur verslechterde retrieval en één excuus in de Slack-kanaal met de klant. Het leverde ook een permanente CI-check op die controleert of de dimensies van query- en corpus-embeddings overeenkomen, voordat een deploy door mag.
Week drie: inference omzetten
Inference was het deel waar iedereen bang voor was. We deden drie dingen om het risico te verlagen.
Eén: we huurden één extra H100 voor de cutover-week. Speling is goedkoop als het alternatief een dinsdagochtend-incident is.
Twee: we draaiden vLLM met conservatieve concurrency-limieten en expliciete swap space.
vllm serve Qwen/Qwen2.5-72B-Instruct-AWQ \
--quantization awq_marlin \
--tensor-parallel-size 2 \
--max-model-len 32768 \
--max-num-seqs 32 \
--gpu-memory-utilization 0.90 \
--swap-space 16 \
--enable-prefix-caching \
--served-model-name shipment-classifier
Drie: de LiteLLM-proxy stond ervoor met een fallback-regel. Elke 5xx of elke latency boven de 30s viel voor die ene request terug op OpenAI. De cutover vond plaats om 03:00 woensdag en de fallback ging af op 1,4% van de requests in het eerste uur. Donderdag was het 0,2%.
Week vier: de tweede storing
De agent draaide negen dagen op de nieuwe stack toen vLLM zichzelf begon te OOM-killen om 11:20 op een vrijdag. Het douanevenster begon en het GPU-proces gaf 500's terug.
De oorzaak was prefix caching. We hadden het aan staan omdat het de p50-latency in tests met zo'n 28% verlaagde. Maar het douanevenster mengde lange, unieke prefixes (elke zendings-PDF is anders) met onze gedeelde system prompt, en de KV-cache groeide sneller dan het evictiebeleid kon opruimen. Met max-num-seqs op 32 en contexten tot 28k tokens raakte de cache het plafond van het GPU-geheugen.
De 11-minutenfix was max-num-seqs naar 24 zetten en swap-space verhogen naar 32GB. De echte fix kostte de rest van de dag. We splitsten de workload. Classificatie-requests met lange PDF-context gingen naar een queue met concurrency 16 en prefix caching uit. Chat-requests gingen naar een aparte vLLM-pool met concurrency 48 en caching aan. We voegden een Prometheus-alert toe op vllm:gpu_cache_usage_perc > 92 en een runbook-entry die tien minuten kostte om te schrijven en die ons die dag een uur had gescheeld.
Wat we van OpenAI hebben behouden
Twee dingen, bewust. Eén: de LiteLLM-fallback naar GPT-4.1-mini bij 5xx of timeout, die nu op ongeveer 0,18% van de requests afgaat. De kosten zijn een afrondfout en de on-call slaapt beter. Twee: onze golden-set evals draaien 's nachts nog steeds tegen drie modellen: Qwen2.5-72B (het productiemodel), GPT-4.1-mini (de fallback) en o4-mini (de referentie). Als Qwen2.5 op de structured-extraction taak meer dan twee procentpunt onder de referentie zakt, krijgen we een Slack-ping. Die ping ging één keer af in mei en wees ons op een aanpassing in de system prompt die JSON-mode op lange inputs had gesloopt.
Je hoeft geen religieus standpunt in te nemen over self-hosting. Het OpenAI-account blijft open, de API-sleutel blijft warm, de eval-suite meet het gat. De goedkopere stack draagt het verkeer. De dure houdt 'm eerlijk.
Wat het kostte, wat het bespaarde
Vier weken werk. Twee engineers fulltime, één DevOps halftime. De vendor-rekening voor mei 2026, de eerste volle maand op de nieuwe stack:
- Hetzner GPU-server, 3x H100 (gaat in juli terug naar 2x): 4.335 euro
- Qdrant-node: 189 euro
- OpenAI-fallback plus golden-set evals: 214 euro
- Totaal: 4.738 euro
Tegen 11.400 in maart is dat een run-rate besparing van zo'n 6.600 euro per maand, ofwel ongeveer 80k op jaarbasis. De migratie zelf kostte ongeveer 38k aan engineering-tijd, dus de terugverdientijd was ongeveer zes maanden. Geen schreeuwerig getal. Wel een verdedigbaar getal.
Wat niemand op de spreadsheet zette: het team weet nu precies waar hun tokens heen gaan en hoe hun retrieval eruitziet. Als de volgende prijsmail binnenkomt, kunnen ze 'm lezen zonder te schrikken.
Wat we anders zouden doen
Eén ding. We zouden de golden-set eval-suite in week nul bouwen, voor we aan de migratie beginnen. Wij bouwden de onze in week één, naast de OTel-instrumentatie. Dat was laat genoeg dat de eerste drie dagen shadow-data niet direct vergelijkbaar waren. Als je één ding meeneemt uit dit stuk, neem dan dat mee.
Toen we de documenten-agent bouwden voor dat Rotterdamse team, was de inference-cutover het deel waar iedereen bang voor was, en de makkelijke helft. De valkuilen zaten aan weerszijden. Mismatches in embedding-dimensies die de recall stilletjes halveerden, en prefix caching die de GPU in de fik zette zodra het productieverkeer niet leek op de benchmark. Overweeg je een vergelijkbare migratie van je eigen AI-agents? Dan zijn de vier weken hierboven de eerlijke versie, niet de slide-versie.
Het kleinste wat je vandaag kunt doen: trek de OpenAI-factuur van vorige maand erbij en split 'm op metadata.user. Als je metadata.user niet op elke Assistants-run hebt staan, is dat je week-nul-klus.
Kern
Het moeilijke aan weggaan bij de Assistants API is niet de inference-move. Het zijn de wissel van embedding-model en het KV-cache rekenwerk waar niemand je voor waarschuwt.
FAQ
Waarom vLLM en niet Ollama of TGI?
vLLM gaf ons OpenAI-compatibele chat completions, volwassen paged-attention scheduling, en tensor parallelism op multi-GPU. Ollama is prima voor een laptop. TGI was OK, maar trager bij onze concurrency-doelen.
Kan dit ook met kleinere modellen?
Ja. We probeerden eerst Qwen2.5-32B. Het kostte 40% minder om te draaien en bleef 6 punten achter op onze golden set. Voor structured extraction over rommelige PDF's telden die punten. Test tegen je eigen evals.
Wat als gehuurde H100's te duur zijn?
Ga terug naar één L40S en een 14B-model. Je levert wat accuracy en concurrency in, maar de som klopt nog steeds beter dan hosted bij gemiddelde volumes. Onder de ~5k runs per maand blijf je op de hosted API.
Bestaat de Assistants API überhaupt nog?
Ja, maar OpenAI heeft het einde aangekondigd ten gunste van de Responses API. Begin je in 2026 een nieuw project op Assistants, dan bouw je op een afgeschreven oppervlak. Plan daarnaar.