Wat is een AI-agent?
Een AI-agent is een systeem waarbij een taalmodel autonoom beslissingen neemt over welke acties het uitvoert om een doel te bereiken. Het verschil met een gewone LLM-aanroep is de loop: het model krijgt het resultaat van zijn actie terug als nieuwe input en kan op basis daarvan nieuwe acties kiezen.
Doel -> Model -> Actie -> Resultaat -> Model -> Actie -> ... -> Einddoel
Drie dingen maken een systeem een echte agent:
- Autonomie: het model beslist zelf welke stappen te nemen.
- Toolgebruik: het model kan acties uitvoeren in de wereld (API-aanroepen, berekeningen, bestandsoperaties).
- Iteratief redeneren: het model verwerkt resultaten en past zijn plan aan.
Agent of workflow?
Anthropic maakt een nuttig onderscheid: een workflow orkestreert modellen en tools via vaste, vooraf geschreven codepaden, terwijl een agent het model zelf de volgorde en het toolgebruik laat bepalen. Begin altijd met de eenvoudigste oplossing. Soms is dat geen agent maar een vaste workflow, die voorspelbaarder en goedkoper is.
ReAct: de meestgebruikte architectuur
ReAct (Reasoning + Acting) combineert chain-of-thought redeneren met toolgebruik. Het model genereert afwisselend:
- Thought: het model denkt na over de situatie en wat te doen.
- Action: het model kiest een tool en parameters.
- Observation: het resultaat van de tool-aanroep.
Oorsprong
ReAct werd gepubliceerd door Yao et al. (arXiv 2022, gepresenteerd op ICLR 2023) bij Google Research. Het is sindsdien de basis voor de meeste agent-frameworks, waaronder LangChain, LlamaIndex en CrewAI.
ReAct in code
async function reactAgent(goal: string, tools: Tool[]): Promise<string> {
const messages: Message[] = [
{ role: "system", content: REACT_SYSTEM_PROMPT },
{ role: "user", content: goal },
];
for (let step = 0; step < MAX_STEPS; step++) {
const response = await llm.complete(messages, { tools });
if (response.stop_reason === "end_turn") {
return extractFinalAnswer(response);
}
if (response.stop_reason === "tool_use") {
const toolCall = extractToolCall(response);
const result = await executeTool(toolCall, tools);
messages.push({ role: "assistant", content: response.content });
messages.push({
role: "user",
content: [{ type: "tool_result", tool_use_id: toolCall.id, content: result }],
});
}
}
throw new Error(`Agent bereikte maximumaantal stappen (${MAX_STEPS})`);
}
Plan-and-Execute
Voor taken die meerdere onafhankelijke stappen vereisen, splitst Plan-and-Execute het werk in twee fasen:
- Planner: genereert een gestructureerd plan met stappen.
- Executor: voert elke stap uit, eventueel parallel.
interface Plan {
steps: { id: string; description: string; depends_on: string[] }[];
}
async function planAndExecute(goal: string): Promise<string> {
const plan: Plan = await planner.createPlan(goal);
const results = new Map<string, string>();
const queue = [...plan.steps];
while (queue.length > 0) {
const ready = queue.filter(step =>
step.depends_on.every(dep => results.has(dep))
);
const executed = await Promise.all(
ready.map(async (step) => {
const context = step.depends_on.map(dep => results.get(dep)).join("
");
const result = await executor.execute(step.description, context);
return { id: step.id, result };
})
);
executed.forEach(({ id, result }) => results.set(id, result));
queue.splice(0, queue.length, ...queue.filter(s => !ready.includes(s)));
}
return await synthesizer.combine(goal, results);
}
Reflexion
Reflexion voegt een zelfevaluatie-stap toe: na elke poging evalueert het model zijn eigen output en leert van fouten.
async function reflexionAgent(goal: string): Promise<string> {
let draft = await agent.attempt(goal);
for (let i = 0; i < MAX_REFLECTIONS; i++) {
const reflection = await evaluator.critique(goal, draft);
if (reflection.satisfied) break;
draft = await agent.revise(draft, reflection.feedback);
}
return draft;
}
LATS
LATS (Language Agent Tree Search) combineert ReAct, planning en reflectie met Monte Carlo Tree Search. Het model bemonstert meerdere ReAct-trajecten, vormt daar een zoekboom mee en laat een LLM elke toestand beoordelen om het beste pad te kiezen. Dat levert sterke resultaten bij complexe taken (bijvoorbeeld programmeren en webnavigatie), maar tegen flink hogere kosten. Het is gepubliceerd op ICML 2024.
Architectuurkeuze
Kies de architectuur op basis van het soort taak:
- Eenvoudige taak: ReAct. Voor sequentiële stappen zonder dat je vooraf een plan hoeft te kennen. Voorbeelden: klantenservice, datasearches, eenvoudige automatisering.
- Complexe taak: Plan-and-Execute. Voor taken met duidelijke deeldoelen die parallel uitgevoerd kunnen worden. Voorbeelden: rapport genereren, codebase analyseren, multi-stap research.
- Kwaliteitskritisch: Reflexion. Wanneer de output aan hoge kwaliteitseisen moet voldoen. Voorbeelden: juridische documenten, code reviews, technische specificaties.
- Maximale kwaliteit, kosten secundair: LATS. Wanneer je het beste pad wilt vinden en de extra LLM-aanroepen acceptabel zijn. Voorbeelden: research en lastige decision-making.
Vergelijking
| Architectuur | Sterkte | Zwakte | Gebruik |
|---|---|---|---|
| ReAct | Eenvoudig, flexibel | Inefficient bij parallelliseerbare stappen | Algemeen |
| Plan-and-Execute | Parallel, reproduceerbaar | Rigide plan als doel onduidelijk is | Gestructureerde taken |
| Reflexion | Hoge kwaliteit output | Duur door meerdere LLM-aanroepen | Kwaliteitskritisch |
| LATS | Beste pad zoeken | Zeer hoge kosten | Research |
Houd je tokens in de gaten
Agentische systemen ruilen latency en kosten in voor betere taakprestaties. Volgens Anthropic verbruiken agents al snel een veelvoud van de tokens van een gewone chat-aanroep, en multi-agent opstellingen nog meer. Zet alleen een agent in als die flexibiliteit echt waarde toevoegt, en bewaak je tokenbudget actief.
Token-gebruik beheersen
Agents kunnen snel grote hoeveelheden tokens consumeren door de groeiende message-history. Een eenvoudige strategie is het oudere midden van de geschiedenis samenvatten en de laatste berichten ongewijzigd laten:
function compressHistory(messages: Message[], maxTokens: number): Message[] {
const systemMessage = messages[0];
const lastN = messages.slice(-6);
const summary = summarizeMiddleMessages(messages.slice(1, -6));
return [systemMessage, { role: "user", content: summary }, ...lastN];
}
Begin klein, voeg pas complexiteit toe als het loont
Bouw eerst een ReAct-agent met een harde stappenlimiet en goede logging. Stap pas over op Plan-and-Execute, Reflexion of LATS als je in de praktijk ziet dat je het echt nodig hebt. Elke extra laag kost tokens, latency en debug-tijd.
Hoeveel stappen mag een ReAct-agent nemen?
Stel een limiet in van ongeveer 10 tot 20 stappen, afhankelijk van de taak. Zonder limiet kan een agent in een oneindige loop raken. Log het aantal stappen en monitor op terugkerende patronen.
Wanneer gebruik ik agents in plaats van gewone LLM-aanroepen?
Gebruik agents als de taak meerdere tool-aanroepen vereist waarbij het resultaat van de ene de invoer van de volgende bepaalt. Voor enkelvoudige lookups is een directe tool-aanroep efficienter.
Hoe betrouwbaar zijn agents?
Minder betrouwbaar dan deterministische code. Zet agents in voor taken waarbij flexibiliteit en redeneren meer waard zijn dan voorspelbaarheid, en voeg validatiestappen toe voor kritieke acties.
Wat is het verschil met een workflow?
Een workflow is deterministisch: stap A leidt altijd naar stap B. Een agent is non-deterministisch: het model beslist zelf de volgorde. Gebruik een workflow als je vooraf weet hoe het pad eruitziet.
Welke frameworks ondersteunen deze architecturen?
LangChain, LlamaIndex en CrewAI ondersteunen ReAct out of the box en bieden bouwstenen voor plannen en reflectie. Voor LATS bestaan losse implementaties die je bovenop een ReAct-laag zet.
Kan ik architecturen combineren?
Ja. In de praktijk combineer je vaak een planner met ReAct-uitvoerders, of voeg je een Reflexion-stap toe aan een ReAct-loop. LATS is feitelijk al zo'n combinatie van plannen, handelen en reflecteren.