Jesse Burger
Terug naar blog
AI Agenten·Technologie

AI Agents Bouwen: Mijn Eerste Experimenten

Jesse Burger··14 min leestijd
§

Halverwege vorig jaar begon het concreet te worden. Niet langer als gebruiker van AI-tools, maar als iemand die er zelf eentje wilde bouwen. Niet voor de abstractie, maar voor een heel specifiek probleem.

Het resultaat: weken van opwinding, debugsessies tot laat, en een heleboel inzichten over waarom het bouwen van AI-agenten anders is dan het eruitziet.

Waarom ik ermee begon

De aanleiding was praktisch. Naast mijn reguliere werk heb ik een paar zijprojecten, en ik merkte dat ik elke week dezelfde saaie ronde maakte: repositories scannen op open issues, statussen bijwerken, samenvatten wat er was veranderd, en dat verwerken tot een overzicht voor mezelf.

Per week ging daar misschien een uur in zitten. Niet veel, maar het was het soort mentaal gefragmenteerde arbeid dat concentratie kost zonder er iets van te vragen. Ideaal dus voor een agent.

Daarnaast was er gewoon nieuwsgierigheid. De hype rondom AI-agenten was niet van de lucht. OpenAI, Anthropic, Google, iedereen had het erover. Maar wat een agent precies was onder de motorkap? Dat begrip was vaag. Genoeg reden om het uit te zoeken.

De tools die ik verkende

Het ecosysteem voor agenten is het afgelopen jaar flink gegroeid. In het begin probeerde ik bijna alles, wat op zichzelf al een les was.

LangChain was de eerste stop. Het framework is overal in agent-tutorials, en biedt een grote hoeveelheid abstracties: chains, agents, tools, memory, callbacks. Mijn eerste rudimentaire agent in Python werkte ermee, tot op zekere hoogte. Maar de abstractielagen zaten me regelmatig in de weg. Als er iets misging, was het lastig te achterhalen waar in de stapel het probleem zat.

from langchain.agents import initialize_agent, AgentType
from langchain.tools import Tool
from langchain_anthropic import ChatAnthropic

llm = ChatAnthropic(model="claude-3-5-sonnet-20241022")

tools = [
    Tool(
        name="fetch_github_issues",
        func=fetch_open_issues,
        description="Haalt open GitHub issues op voor een opgegeven repository. Input: 'owner/repo'"
    )
]

agent = initialize_agent(
    tools=tools,
    llm=llm,
    agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
    verbose=True
)

result = agent.run("Geef me een overzicht van open issues in mijn repositories")

Voor simpele gevallen deed het zijn werk. Zodra de taak iets complexer werd, meerdere stappen, conditionele logica, herstel na een mislukte tool-aanroep, begon het te haperen.

CrewAI probeerde ik daarna. Dit framework is gebouwd rondom een team van agenten met verschillende rollen: een researcher, een schrijver, een reviewer. Elk met een eigen doel en een eigen set tools. Voor bepaalde use cases is dat een logische aanpak. Voor mijn project was het overkill, en de overhead van het definiëren van crews en tasks voor relatief eenvoudige workflows voelde al snel vervelend.

from crewai import Agent, Task, Crew

researcher = Agent(
    role="Repository Analyst",
    goal="Analyseer GitHub repositories en identificeer prioritaire issues",
    backstory="Je bent een ervaren software engineer die snel de status van projecten kan beoordelen.",
    tools=[github_tool],
    verbose=True
)

analyse_taak = Task(
    description="Analyseer alle open issues in {repositories} en rangschik ze op prioriteit.",
    agent=researcher,
    expected_output="Een gerangschikte lijst van issues met toelichting."
)

crew = Crew(agents=[researcher], tasks=[analyse_taak])
result = crew.kickoff(inputs={"repositories": ["mijn-org/project-a", "mijn-org/project-b"]})

Het resultaat zag er in de demo goed uit, maar in de praktijk was het moeilijk te sturen. Agenten namen beslissingen die logisch waren vanuit hun rol, maar niet aansloten bij wat ik eigenlijk wilde.

De directe Anthropic API (met tool use) was uiteindelijk de plek waar ik het meest productief werd. Niet omdat het de meeste features heeft, maar omdat het dichter bij de primitieven zit. Met tool use direct via de API heb ik volledige controle over de lus: stuur een bericht, het model beslist welke tool het wil aanroepen, ik voer die tool uit, stuur het resultaat terug, en zo verder.

import anthropic

client = anthropic.Anthropic()

tools = [
    {
        "name": "fetch_github_issues",
        "description": "Haalt open GitHub issues op voor een repository.",
        "input_schema": {
            "type": "object",
            "properties": {
                "repo": {
                    "type": "string",
                    "description": "Repository in het formaat owner/repo"
                },
                "state": {
                    "type": "string",
                    "enum": ["open", "closed", "all"],
                    "description": "Status van de issues"
                }
            },
            "required": ["repo"]
        }
    }
]

messages = [
    {"role": "user", "content": "Geef me een prioriteitsoverzicht van open issues in jesseburger/project-x"}
]

while True:
    response = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=4096,
        tools=tools,
        messages=messages
    )

    if response.stop_reason == "end_turn":
        print(response.content[0].text)
        break

    if response.stop_reason == "tool_use":
        tool_use = next(block for block in response.content if block.type == "tool_use")
        tool_result = execute_tool(tool_use.name, tool_use.input)

        messages.append({"role": "assistant", "content": response.content})
        messages.append({
            "role": "user",
            "content": [{"type": "tool_result", "tool_use_id": tool_use.id, "content": tool_result}]
        })

Die lus, simpel, expliciet, begrijpelijk, werd de basis van alles wat ik daarna bouwde.

Wat ik concreet bouwde

Mijn eerste serieuze project noemde ik intern "Patchwork". Doel: automatisch GitHub-repositories monitoren, open issues groeperen op thema, en elke vrijdag een samenvattende e-mail sturen met wat aandacht verdient.

Op papier simpel. In de praktijk: niet.

Ik begon met vijf stappen: authenticeren met GitHub, repositories ophalen, per repository de open issues laden, de issues groeperen op label of thema, en een samenvatting genereren. Vijf stappen, elke stap goed te omschrijven als een tool. Ik dacht een dag bezig te zijn.

Na een week was ik nog aan het debuggen.

De eerste versie werkte prachtig in mijn tests. Handmatig een lijstje repositories ingeven, issues ophalen, groeperen, nette samenvatting. Trots.

Daarna probeerde ik het te automatiseren. Dat is waar de problemen begonnen.

Probleem één: authenticatie. De GitHub API heeft rate limits, en de agent had er geen idee van wanneer die naderde. Requests begonnen te falen, en in plaats van graceful te degraderen bleef het retries sturen totdat de rate limit bereikt was. Geen foutafhandeling voor dit scenario.

Probleem twee: herhaalde tool-aanroepen. Het model besloot soms een tool meerdere keren aan te roepen voor dezelfde repository, blijkbaar omdat de vorige aanroep in zijn context verdachte uitvoer had geproduceerd. Het idee van "ik heb dit al gedaan" bestond niet. Elke iteratie was een nieuw moment.

Probleem drie: logische drift. Als de instructies niet precies goed waren, gaf het model zijn eigen invulling aan wat "prioriteit" betekende. Meerdere bugs in een productiesysteem kwamen soms lager uit dan een feature request met een enthousiaste opmerking eronder.

De illusie van de werkende demo

Een agent die in je testomgeving werkt, is niet hetzelfde als een agent die je kunt vertrouwen. In tests controleer je de inputs, zijn de outputs voorspelbaar, en zit je erbij. In productie is dat anders: de inputs zijn rommelig, de outputs gaan naar een systeem dat erop vertrouwt, en jij kijkt niet mee. Die kloof is groter dan je denkt.

Patchwork is nu in zijn vierde versie. Elke versie was een reactie op iets dat misging in de vorige. De huidige versie heeft expliciete foutafhandeling per tool-aanroep, een maximumaantal iteraties om oneindige loops te voorkomen, en een scoringssysteem voor prioriteiten dat ik zelf in code heb vastgelegd in plaats van aan het model over te laten.

Versie twee pakte de rate limiting aan met een wrapper die API-headers uitleest en wacht als de limiet nadert. Versie drie introduceerde een idempotentielaag: de agent houdt bij welke repositories hij al heeft verwerkt binnen een run. Versie vier, de huidige, is de versie waarbij ik voor het eerst dacht: dit zou ik een andere persoon kunnen laten gebruiken. Niet omdat het perfect is, maar omdat het op een voorspelbare manier faalt.

De demo versus productie kloof

Dit verdient een eigen sectie, want het is de les die het meeste tijd heeft gekost.

AI-agent demos zijn bijna altijd indrukwekkend. Je ziet een agent die tien stappen achter elkaar uitvoert, beslissingen neemt, en aan het einde een goed resultaat aflevert. Wat die demos niet laten zien: de uren die zijn gaan zitten in het kiezen van precies die invoer, precies dat model, precies die tools. De mislukte runs zijn weggeknipt. Het systeem is fijn afgesteld op dat ene scenario.

Zelf ben ik schuldig aan precies dit. In de eerste weken deelde ik schermopnames van Patchwork die perfect werkten. Wat ik niet liet zien: de vier mislukte runs ervoor.

In de echte wereld geef je een agent een taak en stap je weg. Dan komt het:

  • De tool geeft een foutmelding terug die het model niet herkent
  • Het model interpreteert een ambigue instructie anders dan bedoeld
  • Er zit ruis in de input die het plan ontregelt
  • De agent raakt vast in een lus omdat een tussenresultaat onverwacht leeg is
  • De API van een externe dienst verandert subtiel van gedrag

De robuustheid van een agent zit niet in het model. Het zit in de scaffolding eromheen: hoe je fouten opvangt, hoe je het model instrueert wat het moet doen als een tool faalt, hoe je output valideert voordat je verdergaat.

Er is ook een praktisch verschil in hoe je fouten opmerkt. Als een demo mislukt, zie je het direct. Als een geautomatiseerde agent stil faalt, kom je er soms pas dagen later achter. Dat vraagt om een andere aanpak.

Een patroon dat inmiddels standaard is in mijn code: elke tool-aanroep wordt omwikkeld met een validatiestap. Het model vraagt de tool aan, de tool geeft een resultaat, en voordat dat resultaat teruggaat aan het model controleer ik of het in het verwachte formaat is en of het zinvol is.

async function callToolSafely(
  toolName: string,
  toolInput: Record<string, unknown>
): Promise<{ success: boolean; result: string }> {
  try {
    const rawResult = await executeTool(toolName, toolInput);

    if (!isValidToolResult(toolName, rawResult)) {
      return {
        success: false,
        result: `Tool ${toolName} gaf onverwacht resultaat terug. Formaat klopt niet.`
      };
    }

    return { success: true, result: JSON.stringify(rawResult) };
  } catch (error) {
    return {
      success: false,
      result: `Tool ${toolName} mislukt: ${error instanceof Error ? error.message : "Onbekende fout"}`
    };
  }
}

Dit lijkt triviaal, maar het maakt een merkbaar verschil. Door het model expliciet te vertellen wat er misging, en niet alleen een lege string of een cryptische stack trace terug te geven, kan het zelf beslissen of het opnieuw probeert, een alternatieve aanpak kiest, of concludeert dat de taak niet te voltooien is.

Wat ik leerde over tool use

Tool use is de kern van wat agents bruikbaar maakt, en ook waar de meeste subtiliteit zit.

Toolnamen en -beschrijvingen zijn prompts. Het model beslist welke tool het aanroept op basis van de beschrijving die je geeft. Als die beschrijving ambigu is, of als twee tools overlappende beschrijvingen hebben, kiest het model soms de verkeerde. Meer tijd is gaan zitten in het schrijven van duidelijke tool-beschrijvingen dan in de implementatie van de tools zelf.

Houd tools klein en gefocust. De eerste neiging was om brede tools te bouwen: een github_tool die alles kon, issues ophalen, comments plaatsen, labels toevoegen. Dat bleek een vergissing. Kleinere, enkelvoudige tools (list_open_issues, add_label_to_issue, post_comment) geven het model en jou meer controle.

Geef het model een uitweg. Standaard voeg ik een report_completion tool toe die het model kan aanroepen als het denkt klaar te zijn, en een report_failure tool als het concludeert dat de taak niet te voltooien is. Zonder die uitwegen kan een agent in een onbedoelde lus blijven hangen.

exit_tools = [
    {
        "name": "report_completion",
        "description": "Gebruik dit als je de taak hebt voltooid. Geef een samenvatting van wat je hebt gedaan.",
        "input_schema": {
            "type": "object",
            "properties": {
                "summary": {"type": "string"},
                "result": {"type": "object"}
            },
            "required": ["summary"]
        }
    },
    {
        "name": "report_failure",
        "description": "Gebruik dit als je de taak niet kunt voltooien. Leg uit waarom.",
        "input_schema": {
            "type": "object",
            "properties": {
                "reason": {"type": "string"},
                "partial_result": {"type": "object"}
            },
            "required": ["reason"]
        }
    }
]

Dit patroon heeft meerdere keren voorkomen dat een agent eindeloos doorliep.

De verrassende moeilijkheid van betrouwbaarheid

Een taalmodel is niet-deterministisch. Dezelfde input levert niet gegarandeerd dezelfde output. Voor een chatbot is dat geen probleem. Voor een agent die acties uitvoert, is het dat wel. Als ik een agent vraag om een issue te labelen als "bug", wil ik niet dat het soms "Bug" doet, soms "bugfix", en soms vergeet het label toe te voegen.

De aanpak die het beste werkt: obsessief specifiek zijn in de systeem-prompt. Niet vaag ("wees nauwkeurig"), maar concreet ("gebruik altijd het exacte label zoals het in de repository bestaat; als het label niet bestaat, roep je report_failure aan met de reden"). Bij elke edge case die opduikt, voeg ik een expliciete instructie toe.

Na een tijdje begon mijn systeem-prompt meer op een technische specificatie te lijken dan op een prompt. Dat is, denk ik, precies wat het moet zijn.

Observability is het andere onderschatte aspect. In het begin had ik nauwelijks inzicht in wat de agent deed tijdens een run. Het eindigde, produceerde een output, en welke stappen het had gezet was niet te achterhalen. Dat maakt debuggen lastig.

De oplossing was simpel: elke tool-aanroep loggen, inclusief input en output, naar een tekstbestand. Geen fancy setup. Maar als een run een onverwacht resultaat geeft, kan ik teruglezen welke beslissingen het model heeft gemaakt en waar het afweek van wat verwacht was.

Agenten zijn software

Het bouwen van betrouwbare AI-agenten is uiteindelijk gewoon software engineering. Dezelfde principes gelden: kleine verantwoordelijkheden, expliciete foutafhandeling, testbaarheid, observability. Het enige verschil is dat een van je componenten een taalmodel is, en dat gedraagt zich anders dan een functie.

Wat er wel werkte

Na al het voorgaande is het eerlijk om ook te zeggen wat goed werkte.

Het model is beter in het redeneren over onverwachte situaties dan verwacht. Als een foutboodschap terugkwam die niet was voorzien, reageerde het model er in de meeste gevallen zinnig op: alternatieve aanpak proberen, of concluderen dat de taak niet te voltooien was.

De kwaliteit van de uiteindelijke samenvatting, het tekstuele eindproduct van Patchwork, is consistent goed. Dat is logisch: daar zijn taalmodellen sterk in. De moeilijkheid zit in de stappen ervoor, niet in de tekstgeneratie.

En er is iets aantrekkelijks aan het kijken naar een agent die een complexe taak uitvoert zonder tussenkomst. Zelfs na alle frustraties zit er iets interessants aan het zien van een model dat zelfstandig redeneert en beslissingen neemt.

Wat ik anders zou doen

Als het opnieuw begon, zouden een paar dingen er anders uitzien.

Eerder kiezen voor eenvoud. De impuls om frameworks te proberen en te stapelen kostte tijd. De directe API-aanpak was uiteindelijk het meest productief. Extra abstractielagen voelden als vooruitgang maar waren dat lang niet altijd. Begin bij de primitieven en voeg complexiteit pas toe als er een concrete reden voor is.

Van dag één een testsuite schrijven. Agents zijn lastig te testen omdat ze niet-deterministisch zijn. Wat uiteindelijk goed werkte: een set "golden tests" schrijven, vaste input-scenario's met beschreven verwacht gedrag, geen exacte outputs. Bij een grote wijziging aan de systeem-prompt of de tools, draaien die tests door en kijken of het gedrag nog steeds klopt. Geen volledige testdekking, maar wel vertrouwen.

Eerder nadenken over randgevallen. De meeste tijd bij het bouwen van Patchwork is niet gaan zitten in de normale werking. Die was relatief snel gebouwd. Het zat in de randgevallen: lege repositories, repositories zonder toegang, issues zonder labels, API-timeouts. Die randgevallen zijn de werkelijkheid.

Waar het heen gaat

Na deze experimenten is de conclusie niet somber. Maar wel realistischer.

De volgende stap in dit veld is waarschijnlijk niet een slimmer model, maar betere tooling rondom betrouwbaarheid en observability. Manieren om te begrijpen wat een agent doet terwijl het het doet, niet alleen achteraf. Standaard patronen voor foutafhandeling die niet elke ontwikkelaar opnieuw hoeft uit te vinden. En eerlijkere gesprekken over het verschil tussen een demo en iets dat productierijp is.

Er zijn veelbelovende ontwikkelingen. Het Model Context Protocol (MCP) van Anthropic is een stap richting gestandaardiseerde tool-integraties. De frameworks worden volwassener. De modellen worden beter in het nauwkeurig volgen van instructies, het verschil tussen vroege versies van Claude en de huidige generatie is merkbaar.

Maar de fundamentele vraag blijft: hoe bouw je systemen die je kunt vertrouwen als ze autonoom handelen? Dat is een vraag die niet alleen technisch is, maar ook te maken heeft met ontwerp, met testen, en met een eerlijke kijk op wat het model goed kan en wat niet.

Een agent is geen programma met een vaste executiestroom. Het is een systeem dat redeneren gebruikt om door onzekerheid te navigeren. Dat vraagt andere intuities van de bouwer: minder "wat doet de code" en meer "wat begrijpt het model" en "hoe faal ik graceful".

Patchwork wordt doorontwikkeld. Er zijn al ideeën voor een tweede agent die code reviews kan samenvatten en koppelen aan open issues. En de verwachting is, op basis van ervaring, dat de weg van demo naar iets wat echt te vertrouwen is langer zal zijn dan het op het eerste gezicht lijkt.

Dat is goed. Zo wordt goede software gebouwd.


Heb jij experimenten gedaan met het bouwen van AI-agenten? Ik ben benieuwd wat jouw ervaringen zijn. Stuur gerust een bericht.

§
JB

Jesse Burger

Schrijft over kunstmatige intelligentie, de impact op ons dagelijks leven, en de toekomst van technologie.