← Blog

RAG

RAG op 14.000 MATLAB-scripts: een Delftse waterbouw-case

Een junior engineer moest een pump-curve fit uit 2019 opnieuw draaien. Het script stond op een Citrix-VM waar elf maanden niemand op had ingelogd. We bouwden een uitweg zonder VPN.

Jacob Molkenboer· Oprichter · A Brand New Company· 15 jun 2026· 9 min
Open eiken kaartenbak met crème indexkaarten, één chartreuse tab, koperen tussenschot, grootboekpapier op ivoorkleurig papier.

Het bericht van de junior engineer kwam binnen om 22:47 op een woensdag. Haar team moest een pump-curve fit uit 2019 verifiëren voor een onderhoudsbrief over de Oosterscheldekering. De oorspronkelijke auteur was met ouderschapsverlof. Het script stond op een Citrix-VM waar het bureau in elf maanden niet op had ingelogd, op een Windows Server-bak die nog MATLAB R2018b draaide onder een per-seat licentie die was verlopen.

Ze kon het script lezen. Draaien lukte niet.

Dat was de situatie waarvoor het bureau ons belde. Eenentwintig engineers in Delft, drie decennia waterbouwkundig werk, en een research-archief van ruwweg 14.000 MATLAB- en Octave-scripts waar niemand bij kon zonder VPN-tunnel en een admin-reset. Zes maanden na livegang beantwoordt de agent over dat archief 940 vragen per week, en die antwoorden bevatten output uit een herdraai, niet alleen citaties.

Een chatbox was niet de oplossing

De eerste reflex, wanneer een bureau veertienduizend scripts heeft en één zoekbalk, is een retrieval-agent op de codebase richten en klaar. Dat hebben we in week één in de goedkope variant gebouwd als sanity check. Bij de prompt "pump curve, centrifugaal, 2019" gaf hij vier op de vijf keer het juiste script terug. De reactie van de engineers: ja, grep kunnen we zelf ook. Wat we niet kunnen, is het ding draaien.

Het echte probleem was niet het script vinden. Het probleem was dat het antwoord op de meeste vragen in dit bureau geen paragraaf is. Het is een getal, een plot, of een fitted curve. Een retrieval-agent die het matchende .m-bestand met een confidence score teruggeeft, is voor een werkende engineer een iets snellere grep.

Dus pasten we de opdracht aan. De agent moest het relevante script vinden, het goed genoeg lezen om de inputs te identificeren, het draaien tegen de juiste inputs, en de output teruggeven die de engineer met de hand had geproduceerd. Dat laatste punt herdefinieerde de hele stack.

De vorm van het corpus

Het archief was drie decennia werk verdeeld over drie dialecten.

Ongeveer 9.300 bestanden waren modern MATLAB (R2014b en later, intensief gebruik van de Signal Processing, Curve Fitting en Simulink toolboxes). Ruim 3.100 waren ouder MATLAB (van vóór 2010, single-letter variabelen, geen comments, het soort dat geschreven werd door iemand die de vergelijkingen uit zijn hoofd kende). Ongeveer 1.600 waren Octave-scripts geschreven door PhD-studenten tussen 2008 en 2016 die thuis geen MATLAB-licentie hadden.

Er was overlap. Een gegeven pump-curve-berekening kon bestaan als 2009-Octave-prototype, een 2014-MATLAB-rewrite voor klantoplevering, en een 2019-hotfix afgesplitst van de 2014-versie met de verkeerde auteursnaam in de header. Geen van de drie was canoniek, en de engineers waren het oneens over welke te vertrouwen.

We hebben niet geprobeerd het corpus te dedupliceren. We indexeerden alles en lieten herkomst uit de retrieval rollen. De agent toont eerst de meest geciteerde versie, dan de meest recent bewerkte, en geeft een kleine diff-hint wanneer er twee versies bestaan voor hetzelfde probleem.

Pyodide in de browser, niet op de server

De reflex is hier om een Python-executie-sandbox op een backend-container te zetten, requests erdoor te routeren, en het modern te noemen. We hadden drie redenen om dat niet te doen.

Eén, licentie-positie. MATLAB zelf draaien op een server die wij beheren betekent floating licenses kopen voor een geautomatiseerde gebruiker. MathWorks vindt dat niet leuk en het inkoopteam van het bureau wilde dat gesprek niet voeren. Octave kan, maar Octave alleen dekt de toolbox-zware 2019-scripts niet.

Twee, blast radius. Een door een engineer getriggerde executie die het productie-cluster raakt, ligt één foute regex verwijderd van een loadprobleem. We wilden dat de execution-sandbox stierf zodra de gebruiker zijn tab sloot.

Drie, observability. We wilden dat de engineer precies zag wat er draaide en het kon aanpassen voordat hij opnieuw draaide. Een black-box-serverstap geeft je dat niet.

Dus de executielaag is Pyodide, de CPython-build die in de browser draait via WebAssembly. De retrieval-pipeline vindt het relevante script. De translatie-pass converteert het naar Python met NumPy-, SciPy- en Matplotlib-equivalenten. Pyodide draait de vertaalde code in de browsertab van de engineer.

Dat betekent: geen VPN, geen Citrix, geen MATLAB-licentie die een server raakt die wij draaien, geen productie-executie-oppervlak. De engineer krijgt de output in hetzelfde chatpaneel dat de citatie gaf. Wil ze een input tweaken, dan past ze een code block aan en draait ter plekke opnieuw.

<script type="module">
  import { loadPyodide } from "https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.mjs";
  const py = await loadPyodide();
  await py.loadPackage(["numpy", "scipy", "matplotlib", "pandas"]);
  const src = await fetchTranslated(scriptId, inputs);
  const out = await py.runPythonAsync(src);
  renderPlot(out);
</script>

De Octave-brug

De translatie-laag is het rommelige deel, en het deel dat de meeste teams onderschatten.

Modern MATLAB en Octave overlappen sterk voor het soort werk dat dit bureau doet: lineaire algebra, signal processing, curve fitting. Maar die overlap is niet schoon. Een paar voorbeelden die we in de eerste maand tegenkwamen.

  • MATLAB's fit() uit de Curve Fitting Toolbox heeft geen eenregelig Octave-equivalent. We mappen het naar scipy.optimize.curve_fit met een wrapper die de fittype-syntax van MATLAB nabootst.
  • xlsread en xlswrite gedragen zich anders per MATLAB-versie en helemaal niet in Octave. We vervingen beide door pandas.read_excel.
  • Anonymous function handles (@(x) x.^2) vertalen direct naar Python-lambdas, maar alleen als de element-wise operators ook worden herschreven (.^ naar **, .* naar * op NumPy-arrays).

We hebben geen universele vertaler geschreven. We schreven een translatie-pass die de constructies aanpakt die daadwerkelijk in de scripts van dit bureau voorkomen, gevalideerd met een regressieset van 320 gepaarde MATLAB/Python-outputs van de engineers zelf. De vertaler slaagt wanneer de numerieke output binnen een tolerantie blijft waar de engineers per scripttype overeenstemming over hadden: 1e-6 voor lineaire algebra, 1e-3 voor fitted curves, exacte match voor indexing-operaties.

Retrieval over wetenschappelijke code

De retrieval-keuzes waren niet spannend en dat was het hele punt.

We chunkten op functieniveau, niet op bestandsniveau. Een MATLAB-script van 2.400 regels dat twaalf helperfuncties definieert, wordt twaalf chunks plus een top-level chunk voor de orchestratie. We probeerden eerst chunken op bestandsniveau en dat ging slecht. Het embedding-model kon "dit bestand bevat pump-curve-fittingcode" niet onderscheiden van "dit bestand bevat pump-curve-fittingcode tussen veel andere dingen in".

We voegden een gestructureerde metadata-laag toe naast de embeddings: auteur, laatst gewijzigd, toolbox-afhankelijkheden, variabelennamen die in headers voorkomen. Daarmee kan de agent filteren ("alleen scripts die de Signal Processing Toolbox gebruiken") voordat de semantische match draait.

We gebruiken pgvector voor de index, in dezelfde Postgres-instance als de metadata. Dat was bewust. Op eerdere projecten hadden we dedicated vector stores gebruikt, en de operationele kosten van nóg een stateful service draaien voor een team van eenentwintig was niet te verantwoorden. Er is één Postgres om te back-uppen, één om te monitoren.

De Postgres-keuze dwong ook een eerlijk gesprek over deletes af. Wanneer een script uit het archief wordt verwijderd of vervangen, herschrijven we de rijen in plaats van losse deletes aan elkaar te koppelen, omdat op een drukke tabel met een grote index een hot loop van deletes operationeel pijnlijk is. Het recente debat over schaalbare deletes in Postgres (met als pointe dat de enige écht schaalbare delete DROP TABLE is) is op deze schaal niet ons probleem, maar het heeft wel gevormd hoe we vervanging modelleren. Bulk-swap is de default. Volledige deletes zijn een zeldzaam onderhoudsmoment.

940 vragen per week

Na zes maanden krijgt de agent tussen 880 en 1.050 vragen per week, met een sterke piek op dinsdag en woensdag. Ruwweg:

  • 58% is retrieval-only ("welk script berekende de 2014-stormvloed-envelop voor het IJmuider getijmodel")
  • 31% is retrieval plus single-pass executie ("draai dat script opnieuw met deze nieuwe bovenwaterstanden")
  • 9% is multi-step ("draai 'm opnieuw, plot de residuen tegen de 2014-plot, en zeg of het verschil groter is dan twee standaarddeviaties")
  • 2% wordt afgewezen omdat het script afhankelijk is van een toolbox-call die we nog niet hebben vertaald

Die 2%-bak is voor ons het interessantst. Het is de queue die ons vertelt wat we vervolgens moeten vertalen.

De principal engineer vertelde ons na de derde maand dat de hiërarchie van wie-vraagt-wie op kantoor was verschoven. Juniors stelden nu aan de agent de vragen die seniors eerst op de gang beantwoordden. Seniors stelden aan de agent de vragen die ze anders aan een stagiair hadden gegeven. Dat was geen metric die we volgden, maar het klopt.

Wat we anders zouden doen

Drie dingen.

We zouden eerst de translatie-regressieset bouwen. Wij begonnen met retrieval, schroefden er executie op vast, en kwamen er toen achter dat we een gepaarde-output-validatieset nodig hadden om te weten of de vertaling daadwerkelijk klopte. Die set achteraf bouwen kostte drie weken. Doe je dit, bouw dan de regressieset in week één, met de engineers in de kamer.

We zouden eerder met Pyodide zijn begonnen. Het eerste prototype draaide vertaalde Python op een kleine server. De latency was prima. Het inkoop- en auditgesprek rond die server was dat niet. Naar in-browser-executie verhuizen sneed twee maanden compliance-review weg waar we nooit aan hadden moeten beginnen. Pyodide levert het grootste deel van SciPy al mee, en de laadtijd op een moderne laptop is voor engineeringwerk acceptabel.

We zouden de git-history geïndexeerd hebben, niet alleen de bestanden. De helft van de vragen die de engineers stellen heeft een impliciete tijddimensie: "de versie die we de klant in maart 2019 stuurden" of "vóór we de head-loss-coëfficiënt aanpasten". We voegden git-bewuste retrieval toe in maand vier. Dat had er in week één moeten zitten.

Waarschuwing

Beloof geen in-browser-executielaag voordat je de cold-start-tijd hebt gemeten voor jouw specifieke packagelijst. Pyodide plus NumPy plus SciPy plus Matplotlib is een paar seconden op een moderne laptop. Voeg je een zwaardere toolbox toe, dan verandert het gesprek.

Het kleinste wat je deze week kunt doen

Heb je zo'n archief op een verouderde bak staan, dan is het kleinst-nuttige wat je vandaag kunt doen: tel de bestanden, tel de dialecten, en schrijf op wat de laatst-gewijzigd-datum is van het oudste bestand dat iemand de afgelopen zes maanden echt heeft geopend. Dat getal vertelt je of je een kennisbank hebt of een kerkhof.

Toen we deze RAG-agent voor het Delftse team bouwden, was wat we niet hadden zien aankomen hoeveel van het werk geen retrieval was, maar executie: het antwoord teruggeven dat de engineer met de hand had geproduceerd, in hetzelfde paneel, in een browsertab. We hebben het opgelost door de runtime naar Pyodide te verplaatsen en een translatie-pass te schrijven die smal genoeg is om daadwerkelijk te valideren tegen door engineers gepaarde outputs.

Kern

RAG over wetenschappelijke code is geen retrieval-probleem. Het is een executie-probleem. Geef het getal terug dat de engineer met de hand had geproduceerd, in hetzelfde paneel.

FAQ

Waarom Python in de browser draaien in plaats van op een server?

Licentie-positie, blast radius en observability. Geen MATLAB-equivalente licentie raakt onze servers, de sandbox sterft met de tab, en de engineer kan de code lezen en aanpassen voordat hij opnieuw draait.

Hoe houd je de vertaling van MATLAB naar Python correct?

Met een gepaarde-output-regressieset, opgebouwd samen met de engineers. De vertaler slaagt als de numerieke outputs binnen een afgesproken tolerantie per scripttype blijven: 1e-6 voor lineaire algebra, 1e-3 voor fitted curves, exact voor indexing.

Waarom pgvector in plaats van een dedicated vector database?

Eén stateful service om te draaien voor eenentwintig engineers. De metadata die de pre-filter aanstuurt staat al in Postgres, dus de embeddings co-loceren verwijderde een operationele afhankelijkheid in plaats van er één toe te voegen.

Wat gebeurt er als een script afhankelijk is van een toolbox-call die je nog niet hebt vertaald?

De agent wijst af en logt de call in een translatie-queue. Die queue is ongeveer 2% van het wekelijkse verkeer en vertelt ons welk toolbox-oppervlak we hierna moeten vertalen.

Hoe lang duurde het om dit live te krijgen?

Ruwweg veertien weken van kickoff tot bureau-brede uitrol, met nog vier weken follow-up om git-bewuste retrieval toe te voegen en de translatie-pass aan te scherpen.

ragcase studyknowledge baseai agentsarchitecturelegacy sites

Iets bouwen?

Start een project