Demo Notebook en Dataset
Om je te helpen deze techniek te begrijpen en ermee te experimenteren, hebben we een demo notebook gemaakt die de implementatie van onze oplossing demonstreert met de receipts dataset van Hugging Face. De notebook laat je de twee hoofdonderdelen van onze aanpak zien:
- Hoe je het structured output schema definieert en implementeert.
- Hoe je meerdere antwoordkandidaten genereert en verwerkt.
De receipts dataset is bijzonder geschikt voor deze demonstratie omdat:
- Het echte voorbeelden bevat van het extraheren van gestructureerde data.
- De afbeeldingen van hoge kwaliteit en goed opgemaakt zijn.
- De dataset openbaar beschikbaar en eenvoudig toegankelijk is.
Je kunt de volledige blog lezen en stap voor stap meevolgen, of direct de uiteindelijke implementatie in de notebook in duiken.
Let op: we werken in de voorbeeldcode met Google Gemini Models, maar de kernlogica werkt voor alle LLM providers die structured outputs kunnen genereren.
De Uitdaging van Consistente AI Outputs
LLMs produceren, vanwege hun aard, inconsistente of onvoorspelbare outputs. Het is ook bekend dat wanneer je de temperature op 0 zet of de output van het model te veel inperkt, de kwaliteit van de output daalt[1][2]. Maar bij het bouwen van productie-waardige applicaties die afhankelijk zijn van AI-gegenereerde content, kan variabiliteit voor aanzienlijke uitdagingen zorgen. Voor kritische zakelijke toepassingen, zoals de offertevergelijkingstool die we onlangs voor een van onze klanten bouwden, hebben we vaak betrouwbare, gestructureerde data nodig die consistent kan worden verwerkt door downstream systemen.
Onze Oplossing: Structured Output met Response Sampling
Onze aanpak om betrouwbaardere en robuustere output uit LLMs te krijgen, combineert twee krachtige technieken:
- Structured Output met Pydantic Models: We definieren structured output schema's met Pydantic, wat type safety en validatie biedt. Dit is al gangbaar in de industrie.
- Generatie van Meerdere Kandidaten met Selectie op Basis van Frequentie: We genereren meerdere outputs en selecteren de meest frequente kandidaat als het 'definitieve' resultaat, met behulp van een hashable BaseModel.
Deze techniek gebruikt response sampling om de meest nauwkeurige output te identificeren. Het werkt vanuit de aanname dat het beste antwoord een hogere kans heeft om in meerdere generaties voor te komen, zelfs wanneer de LLM het niet consistent produceert. Dus, gegeven dat er een beste antwoord is, kunnen we het vinden door het antwoord te selecteren dat het vaakst voorkomt in onze response sample.
Laten we eens bekijken hoe je dit zou kunnen implementeren:
Structured Output met Pydantic
Voor een succesvolle integratie van LLMs in jouw applicaties is het meestal nodig dat ze goed gedefinieerde, gestructureerde outputs genereren. Daarom willen we structured schema's definieren voor verschillende AI-taken. Hier is bijvoorbeeld hoe we een schema definieren om de extractie van informatie uit bonnetjes te structureren:
Door gebruik te maken van de Field class van Pydantic, kunnen we duidelijke beschrijvingen geven voor elk element van onze output. Deze beschrijving begeleidt niet alleen de codegebruikers, maar dezelfde beschrijvingen kunnen ook direct een AI begeleiden bij het genereren van passende waarden voor elk veld. We kunnen deze modeldefinities daarom gebruiken als expliciete instructies die het model helpen precies te begrijpen welke informatie het uit de documenten moet halen. Aangezien de meeste LLMs getraind zijn op python code, kun je deze code direct kopieren en plakken in de prompt om de LLM richting de juiste structured outputs te leiden.
Meerdere Kandidaten Genereren
Zelfs met een relatief strikte instructie zal de output van de LLM vaak varieren wanneer je deze twee keer bevraagt. In ons voorbeeld is het bijvoorbeeld mogelijk dat de LLM het fooibedrag verwart met het belastingbedrag. In plaats van deze variabiliteit op te lossen door de LLM nog verder te beperken, kiezen we voor een alternatieve aanpak: we bevragen het model simpelweg meerdere keren om een robuuster antwoord te krijgen. Daarbij vertrouwen we op de aanname dat het model, met een correcte prompt, ook eerder het juiste antwoord zal genereren. Met Google's Gemini modellen kun je eenvoudig en efficient meerdere antwoorden op dezelfde prompt verkrijgen door het candidate_count attribuut van de GenerateContentConfig op een getal groter dan 1 in te stellen.
De Meest Frequente Kandidaat Selecteren
De kern van onze aanpak is het selecteren van het meest frequente antwoord onder de kandidaten. Je intuitie zal je vertellen dat de meest pythonic manier om het meest voorkomende item in een lijst te vinden, het gebruik van de collections.Counter class is. De BaseModel van Pydantic is echter standaard niet hashable. Gelukkig kunnen we dit eenvoudig implementeren:
Let op: we gebruiken model_dump_json omdat strings hashable zijn, in tegenstelling tot dictionaries.
Vervolgens werken we onze class van eerder als volgt bij:
Hier is hoe we de logica voor kandidaatselectie implementeren:
Deze implementatie telt de frequentie van elk uniek antwoord en selecteert het meest voorkomende. Het print ook de relatieve frequentie van het meest voorkomende antwoord, wat nuttig kan zijn om de kwaliteit van het antwoord te bepalen.
De Voordelen van Deze Aanpak
Onze op frequentie gebaseerde kandidaatselectie biedt verschillende mooie voordelen:
- Verbeterde Betrouwbaarheid: Door outputs te samplen en het meest frequente antwoord te selecteren, verkleinen we de impact van incidentele hallucinaties of fouten van het model.
- Kwantificeerbaar Vertrouwen: Het consensuspercentage geeft ons een maatstaf om het vertrouwen in onze resultaten te beoordelen.
- Mogelijkheid om Netjes te Falen: Wanneer de consensus laag is, kunnen we ervoor kiezen om het antwoord te markeren voor menselijke beoordeling.
In de demo notebook vind je een voorbeeld van hoe een data-extractietaak eenvoudig kan worden verbeterd met deze methode. Je kunt bijvoorbeeld zien dat deze aanpak hallucinaties effectief vermindert, zoals het genereren van een "denkbeeldige" valuta die niet daadwerkelijk op de bonnetjes voorkomt.
Voeg reasoning toe
Zoals we eerder schreven, is het niet ongebruikelijk om een LLM te laten redeneren voordat het een antwoord geeft, om de kwaliteit en betrouwbaarheid te verbeteren. De eenvoudigste manier om dit te doen is, in onze ervaring, om een veld aan je output schema toe te voegen met de naam chain_of_thought, als volgt:
Het is belangrijk om het chain_of_thought veld als eerste in je output schema te plaatsen, omdat dit de LLM aanmoedigt om te redeneren voordat het zijn definitieve antwoord geeft. Hoewel sommigen zouden kunnen aannemen dat deze aanpak achterhaald is met nieuwere reasoning-gerichte modellen, blijft het waardevol voor de meeste general-purpose LLMs, omdat het redeneren onderdeel wordt van het context window en het definitieve antwoord beter onderbouwt. Let op: sommige implementaties (zoals die van Google) kunnen velden in structured outputs automatisch alfabetisch sorteren, in welk geval je de veldnaam mogelijk moet laten beginnen met een teken zoals "0" om ervoor te zorgen dat het als eerste verschijnt.
Het toevoegen van het chain_of_thought veld aan het antwoord introduceert echter een nieuw probleem. Bekijk de volgende voorbeeldantwoorden
De geextraheerde informatie uit het bonnetje is in beide antwoorden hetzelfde, maar het gepresenteerde redeneren niet! Met onze huidige code zouden deze antwoorden dus niet als gelijk worden beschouwd, terwijl dat wel zou moeten. Om dit probleem op te lossen, hebben we een manier nodig om het chain_of_thought veld te negeren.
Er zijn verschillende oplossingen voor dit probleem:
1. Lieg tegen je LLM
Aangezien Pydantic's BaseModel.model_validate_json extra keys negeert, kun je je LLM simpelweg vertellen om de key te genereren zonder deze ooit daadwerkelijk te gebruiken.
2. Sluit het Attribuut Uit
Je kunt het attribuut uitsluiten in de Field functie Field(..., exclude=True, wat ervoor zorgt dat het attribuut wordt genegeerd in elke model_dump(_json) en __eq__ call. Het lastige hier is dat het vanaf dat moment vrijwel onmogelijk wordt om dat attribuut te model_dump'en, vanwege de exclusie-instellingen van Pydantic. Dit betekent dat als je het wilt gebruiken voor logging-doeleinden, je het expliciet zult moeten extraheren. Hoewel dit prima zou werken in het simpele voorbeeld, kan het een stuk lastiger worden bij het nesten van Pydantic models.
3. Gebruik onze Implementatie van ExcludableFieldsBaseModel
We hebben een eigen implementatie van Pydantic's BaseModel gebouwd die de private property _exclude_fields toevoegt, waar je kunt definieren welke velden genegeerd moeten worden bij het vergelijken van twee BaseModels. Door een eigen __hash__ en __eq__ methode te implementeren, zorgen we ervoor dat alle geneste ExcludeFieldsBaseModel's ook hun respectievelijke _exclude_fields uitsluiten, zodat ze correct gebruikt kunnen worden in een Counter (of andere vergelijkingscontext).
In onze aanpak stellen we een private attribuut in met de naam _exclude_fields, dat we gebruiken in de __hash__ en __eq__ calls. Onze Example class wordt dan:
Je kunt nu het _exclude_fields attribuut gebruiken in elk model waarin je velden wilt uitsluiten. Zorg er alleen voor dat elke 'parent' class ook ExcludeFieldsBaseModel gebruikt, zodat deze de hash en de gelijkheid correct berekent.
Naast de chain of thought kun je nu ook andere niet-deterministische velden uitsluiten van de uiteindelijke vergelijking en meerderheidsstemming. Bijvoorbeeld:
Afweging tussen Kwaliteit en Kosten
Bij het implementeren van de gepresenteerde aanpak met meerdere kandidaten is een belangrijke afweging het balanceren van modelkwaliteit, kosten en latency. We hebben enkele interessante dynamieken ontdekt in onze applicaties:
- Afweging Modelkwaliteit vs. Kwantiteit: Hoewel modellen van hogere kwaliteit zoals Gemini-1.5-Pro nauwkeurigere individuele antwoorden kunnen produceren, kunnen meerdere samples van een kosteneffectiever model een vergelijkbare betrouwbaarheid bereiken tegen lagere kosten. "gemini-2.0-flash-lite" kost bijvoorbeeld 0,30 per miljoen output tokens tegenover 5,00 voor gemini-1.5-pro, een verschil van 16x. Cruciaal is dat je alleen betaalt voor de extra output tokens wanneer je een multi-candidate response gebruikt.
- De Optimale Sample Size Vinden: Door te experimenteren ontdekten we dat het aantal benodigde kandidaten om betrouwbaarheid te bereiken een afnemend rendement kent. Dus, hoewel het optimaal lijkt om minder samples van een model van hogere kwaliteit te gebruiken, maakt het kostenverschil meerdere samples van een goedkoper model vaak economischer.
- Latency-overwegingen: Wanneer responstijd belangrijk is, is het cruciaal om meerdere kandidaten parallel te draaien. Deze aanpak voegt minimale latency toe terwijl het de betrouwbaarheid aanzienlijk verbetert, hoewel het de rekenlast wel verhoogt. De meeste LLM providers ondersteunen gelijktijdige requests, waardoor deze aanpak haalbaar is voor real-time applicaties.
- Consensusdrempels: Stel een passende consensusdrempel in op basis van de eisen van je applicatie. Voor kritische extractie van financiele data vereisen we ten minste 70% consensus; anders markeren we het antwoord voor menselijke beoordeling. Voor minder kritische applicaties kan een lagere drempel acceptabel zijn.
Onze aanbeveling is om te beginnen met een model van gemiddelde kwaliteit en 5 kandidaten, en vervolgens aan te passen op basis van je specifieke behoeften, kostenbeperkingen en betrouwbaarheidseisen. Houd je consensusscores bij over verschillende documenttypes om te identificeren waar de aanpak werkt of verfijning nodig heeft.
Geleerde Lessen en Best Practices
Door ons werk met deze technieken hebben we verschillende best practices geidentificeerd:
- Wees Specifiek in Schemadefinities: Gebruik beschrijvende veldnamen en geef gedetailleerde veldbeschrijvingen om de AI effectief te sturen.
- Balanceer het Aantal Kandidaten met Modelkwaliteit: Goedkopere en 'minder intelligente' modellen kunnen worden ingezet in plaats van sterkere, duurdere modellen als ze vaker worden gesampled.
- Neem Reasoning-velden op: Aparte velden voor het redeneren en het daadwerkelijke doel helpen de AI om zijn gedachten te ordenen voordat het het antwoord geeft dat je zoekt.
- Log en Monitor Consensuspercentages: Houd bij hoe vaak kandidaten het eens zijn om problematische queries of schema's te identificeren.
Conclusie
Structured output met selectie van meerdere kandidaten is een krachtige aanpak voor het bouwen van betrouwbaardere AI-applicaties. Door meerdere kandidaten te genereren en het meest frequente antwoord te selecteren, verbeteren we de betrouwbaarheid van en het vertrouwen in onze AI-outputs.
Deze technieken zijn centraal geweest in het succes van verschillende oplossingen die we aan onze klanten hebben geleverd, waardoor de verwerking van complexe documenten met hoge betrouwbaarheid en nauwkeurigheid mogelijk werd. Naarmate AI zich blijft ontwikkelen, zullen deze engineering-aanpakken essentiele tools blijven om de kloof tussen AI-mogelijkheden en productie-eisen te overbruggen.
Door deze patronen in je eigen AI-applicaties te implementeren, kun je de outputkwaliteit en betrouwbaarheid aanzienlijk verbeteren, waardoor AI een praktischere oplossing wordt voor complexe problemen uit de echte wereld.
- Zoals beschreven in "The Curious Case of Text DeGeneration", kan greedy output sampling leiden tot repetitieve en kwalitatief lage outputs. https://arxiv.org/pdf/1904.09751
- Onderzoek door TrustGraph toonde aan dat het gebruik van temperature 0 niet altijd optimale resultaten oplevert: https://dev.to/trustgraph/what-does-llm-temperature-actually-mean-18b
