Zum Inhalt springen
EN DE

Challenge 8.4: Breaking the Loop

Was passiert wenn Dein Agent in einer Endlosschleife haengt — z.B. weil kein Tool das richtige Ergebnis liefert und das LLM immer wieder dieselbe Suche startet?

Overview: Agent Loop läuft, Break Conditions prüfen Max Iterations, Timeout, Cost Guard und Quality Check, bei Fehler Break mit Partial Result, bei Erfolg Return Ergebnis

Vier Break Conditions schuetzen Deinen Agent-Loop vor unkontrolliertem Verhalten: Max-Iterations verhindert endlose Schleifen, Timeout stoppt nach einer definierten Zeit, Cost Guard begrenzt den Token-Verbrauch und Quality Check beendet den Loop, wenn das Ergebnis gut genug ist.

Ohne Break Conditions: Endlosschleifen, explodierende Kosten, haengende App. Das LLM ruft immer wieder dasselbe Tool auf, verbraucht hunderte Tokens pro Iteration und laeuft Minuten lang — waehrend der User wartet. Im schlimmsten Fall: eine API-Rechnung über hunderte Dollar für einen einzigen fehlgelaufenen Loop.

Mit Break Conditions: Kontrollierter Abbruch. Nach maximal 50 Messages, 30 Sekunden oder 10.000 Tokens ist Schluss — egal was das LLM macht. Du bekommst das beste bisherige Ergebnis (Partial Result) statt gar nichts. Und Dein Budget ist geschuetzt.

Die einfachste Break Condition — zaehle die Messages oder Iterationen:

import { generateText, tool } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const model = anthropic('claude-sonnet-4-5-20250514');
const MAX_ITERATIONS = 10;
const MAX_MESSAGES = 50;
const searchTool = tool({
description: 'Search for information',
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => ({
results: [`Ergebnisse für "${query}"...`],
}),
});
const messages: Array<any> = [
{ role: 'user', content: 'Recherchiere Edge Computing ausfuehrlich.' },
];
let done = false;
let iterations = 0;
let bestResult = '';
while (!done) {
iterations++;
const result = await generateText({
model,
tools: { search: searchTool },
messages,
});
// Bestes bisheriges Ergebnis speichern
if (result.text) {
bestResult = result.text;
}
// Break Condition 1: Max Iterations
if (iterations >= MAX_ITERATIONS) {
console.log(`Max Iterations (${MAX_ITERATIONS}) erreicht. Abbruch.`);
done = true;
break;
}
// Break Condition 2: Max Messages (verhindert zu grossen Context)
if (messages.length > MAX_MESSAGES) {
console.log(`Max Messages (${MAX_MESSAGES}) erreicht. Abbruch.`);
done = true;
break;
}
if (result.finishReason === 'stop') {
done = true;
bestResult = result.text;
} else {
messages.push(...result.response.messages);
}
}
// Partial Result zurueckgeben
console.log('--- Ergebnis ---');
console.log(bestResult || 'Kein Ergebnis generiert.');
console.log(`Iterationen: ${iterations}, Messages: ${messages.length}`);

Wichtig: bestResult speichert das beste bisherige Ergebnis. Wenn der Loop wegen Max Iterations abbricht, hast Du trotzdem einen Output — kein leeres Ergebnis.

Für zeitkritische Anwendungen — der Loop muss innerhalb einer bestimmten Zeit fertig werden:

import { generateText, tool } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const model = anthropic('claude-sonnet-4-5-20250514');
const TIMEOUT_MS = 30_000; // 30 Sekunden
const searchTool = tool({
description: 'Search for information',
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => ({
results: [`Ergebnisse für "${query}"...`],
}),
});
const messages: Array<any> = [
{ role: 'user', content: 'Recherchiere Edge Computing ausfuehrlich.' },
];
let done = false;
let bestResult = '';
const startTime = Date.now();
// AbortController für sauberen Abbruch
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, TIMEOUT_MS);
try {
while (!done) {
// Timeout prüfen vor jedem Call
if (Date.now() - startTime > TIMEOUT_MS) {
console.log(`Timeout (${TIMEOUT_MS}ms) erreicht. Abbruch.`);
break;
}
const result = await generateText({
model,
tools: { search: searchTool },
messages,
abortSignal: controller.signal, // ← AbortSignal an generateText
});
if (result.text) {
bestResult = result.text;
}
if (result.finishReason === 'stop') {
done = true;
} else {
messages.push(...result.response.messages);
}
}
} catch (error) {
if ((error as Error).name === 'AbortError') {
console.log('Abgebrochen durch Timeout.');
} else {
throw error;
}
} finally {
clearTimeout(timeout); // ← Timeout aufräumen
}
const duration = Date.now() - startTime;
console.log(`Ergebnis: ${bestResult.slice(0, 200)}...`);
console.log(`Dauer: ${duration}ms`);

abortSignal: controller.signal uebergibt das Signal an generateText. Wenn der Timeout zuschlaegt, wird der aktuelle API-Call abgebrochen. Der try/catch faengt den AbortError ab, und im finally-Block raumen wir den Timeout auf.

Tracke den Token-Verbrauch und stoppe, wenn das Budget erschoepft ist:

import { generateText, tool } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const model = anthropic('claude-sonnet-4-5-20250514');
const MAX_TOKENS = 10_000;
const searchTool = tool({
description: 'Search for information',
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => ({
results: [`Ergebnisse für "${query}"...`],
}),
});
const messages: Array<any> = [
{ role: 'user', content: 'Recherchiere Edge Computing.' },
];
let done = false;
let totalTokens = 0;
let bestResult = '';
while (!done) {
const result = await generateText({
model,
tools: { search: searchTool },
messages,
});
// Tokens tracken
totalTokens += result.usage.totalTokens;
if (result.text) {
bestResult = result.text;
}
// Cost Guard: Token-Budget prüfen
if (totalTokens > MAX_TOKENS) {
console.log(`Token-Budget (${MAX_TOKENS}) ueberschritten: ${totalTokens} Tokens. Abbruch.`);
done = true;
break;
}
if (result.finishReason === 'stop') {
done = true;
} else {
messages.push(...result.response.messages);
}
}
console.log(`Ergebnis: ${bestResult.slice(0, 200)}...`);
console.log(`Tokens verbraucht: ${totalTokens} / ${MAX_TOKENS}`);

Der Cost Guard ist besonders wichtig in Production. Ein einzelner generateText-Call verbraucht vielleicht 500 Tokens. Aber ein Loop mit 20 Iterationen? 10.000 Tokens. Ohne Budget-Limit kann das teuer werden.

In Production kombinierst Du alle Break Conditions:

import { generateText, tool } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const model = anthropic('claude-sonnet-4-5-20250514');
// Konfigurierbare Limits
const LIMITS = {
maxIterations: 10,
maxMessages: 50,
maxTokens: 10_000,
timeoutMs: 30_000,
};
const searchTool = tool({
description: 'Search for information',
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => ({
results: [`Ergebnisse für "${query}"...`],
}),
});
type BreakReason = 'complete' | 'max-iterations' | 'max-messages' | 'max-tokens' | 'timeout' | 'error';
async function robustAgentLoop(prompt: string) {
const messages: Array<any> = [{ role: 'user', content: prompt }];
let iterations = 0;
let totalTokens = 0;
let bestResult = '';
let breakReason: BreakReason = 'complete';
const startTime = Date.now();
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), LIMITS.timeoutMs);
try {
while (true) {
iterations++;
// Pre-Check: Limits prüfen BEVOR der nächste Call startet
if (iterations > LIMITS.maxIterations) {
breakReason = 'max-iterations';
break;
}
if (messages.length > LIMITS.maxMessages) {
breakReason = 'max-messages';
break;
}
if (totalTokens > LIMITS.maxTokens) {
breakReason = 'max-tokens';
break;
}
if (Date.now() - startTime > LIMITS.timeoutMs) {
breakReason = 'timeout';
break;
}
const result = await generateText({
model,
tools: { search: searchTool },
messages,
abortSignal: controller.signal,
});
totalTokens += result.usage.totalTokens;
if (result.text) {
bestResult = result.text;
}
if (result.finishReason === 'stop') {
breakReason = 'complete';
break;
}
messages.push(...result.response.messages);
}
} catch (error) {
if ((error as Error).name === 'AbortError') {
breakReason = 'timeout';
} else {
breakReason = 'error';
console.error('Unerwarteter Fehler:', error);
}
} finally {
clearTimeout(timeout);
}
return {
result: bestResult,
breakReason,
stats: {
iterations,
totalTokens,
durationMs: Date.now() - startTime,
messageCount: messages.length,
},
};
}
// Ausfuehren
const output = await robustAgentLoop('Recherchiere Edge Computing ausfuehrlich.');
console.log('--- Ergebnis ---');
console.log(output.result.slice(0, 300));
console.log('\n--- Abbruchgrund ---');
console.log(output.breakReason);
console.log('\n--- Statistik ---');
console.log(JSON.stringify(output.stats, null, 2));

Die Funktion gibt immer ein Ergebnis zurück — inklusive breakReason. Der Caller weiss, ob der Agent fertig wurde (complete) oder ob ein Limit gegriffen hat. Das ist wichtig für Monitoring und Debugging.

Aufgabe: Erweitere den Custom Loop aus Challenge 8.3 mit einem Timeout und einem Max-Iterations Guard.

Erstelle die Datei guarded-loop.ts und fuehre sie aus mit npx tsx guarded-loop.ts.

import { generateText, tool } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const model = anthropic('claude-sonnet-4-5-20250514');
// TODO 1: Definiere LIMITS (maxIterations: 5, timeoutMs: 30000)
// TODO 2: Definiere search-Tool und save-Tool
// TODO 3: Initialisiere messages, state, startTime, AbortController
// TODO 4: Implementiere den Loop mit ALLEN Safeguards:
// - Max Iterations prüfen
// - Timeout prüfen
// - abortSignal an generateText uebergeben
// - try/catch für AbortError
// - bestResult speichern
// TODO 5: Gib das Ergebnis mit breakReason und Statistik zurück

Checkliste:

  • Max-Iterations Guard implementiert (Loop bricht nach N Iterationen ab)
  • Timeout mit AbortController und setTimeout implementiert
  • abortSignal wird an generateText uebergeben
  • AbortError wird im catch-Block abgefangen
  • bestResult wird bei jedem Text-Ergebnis aktualisiert
  • breakReason gibt an, warum der Loop beendet wurde
Lösung anzeigen
import { generateText, tool } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const model = anthropic('claude-sonnet-4-5-20250514');
const LIMITS = {
maxIterations: 5,
timeoutMs: 30_000,
};
const searchTool = tool({
description: 'Search for information on a topic',
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => ({
results: [`Fakten zu "${query}": Wichtige Entwicklungen...`],
}),
});
const saveTool = tool({
description: 'Save the final research result',
inputSchema: z.object({ content: z.string() }),
execute: async ({ content }) => ({ saved: true }),
});
type BreakReason = 'complete' | 'save-complete' | 'max-iterations' | 'timeout' | 'error';
async function guardedLoop(prompt: string) {
const messages: Array<any> = [{ role: 'user', content: prompt }];
let iterations = 0;
let totalTokens = 0;
let bestResult = '';
let breakReason: BreakReason = 'complete';
const startTime = Date.now();
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), LIMITS.timeoutMs);
try {
while (true) {
iterations++;
// Guard: Max Iterations
if (iterations > LIMITS.maxIterations) {
breakReason = 'max-iterations';
console.log(`Max Iterations (${LIMITS.maxIterations}) erreicht.`);
break;
}
// Guard: Timeout (zusätzlich zum AbortController)
if (Date.now() - startTime > LIMITS.timeoutMs) {
breakReason = 'timeout';
console.log(`Timeout (${LIMITS.timeoutMs}ms) erreicht.`);
break;
}
const result = await generateText({
model,
tools: { search: searchTool, save: saveTool },
messages,
abortSignal: controller.signal,
});
totalTokens += result.usage.totalTokens;
if (result.text) {
bestResult = result.text;
}
// Pruefe ob save-Tool aufgerufen wurde
for (const tr of result.toolResults) {
if (tr.toolName === 'save' && tr.result.saved) {
breakReason = 'save-complete';
console.log('Save-Tool erfolgreich. Loop beendet.');
break;
}
}
if (breakReason === 'save-complete') break;
if (result.finishReason === 'stop') {
breakReason = 'complete';
bestResult = result.text;
break;
}
messages.push(...result.response.messages);
console.log(`Iteration ${iterations}: ${totalTokens} Tokens, ${messages.length} Messages`);
}
} catch (error) {
if ((error as Error).name === 'AbortError') {
breakReason = 'timeout';
console.log('Abbruch durch AbortController.');
} else {
breakReason = 'error';
console.error('Fehler:', error);
}
} finally {
clearTimeout(timeout);
}
return {
result: bestResult,
breakReason,
stats: {
iterations,
totalTokens,
durationMs: Date.now() - startTime,
},
};
}
const output = await guardedLoop(
'Recherchiere Edge Computing. Nutze search für Recherche, dann save um das Ergebnis zu speichern.',
);
console.log('\n--- Ergebnis ---');
console.log(output.result || 'Kein Ergebnis.');
console.log(`Abbruchgrund: ${output.breakReason}`);
console.log(`Stats: ${JSON.stringify(output.stats)}`);

Erwarteter Output (ungefaehr):

Iteration 1: 487 Tokens, 3 Messages
Iteration 2: 974 Tokens, 5 Messages
Save-Tool erfolgreich. Loop beendet.
--- Ergebnis ---
[Recherche-Text erscheint hier]
Abbruchgrund: save-complete
Stats: {"iterations":2,"totalTokens":974,"durationMs":4521}

Erklärung: Zwei Safeguards schuetzen den Loop: Max Iterations (pre-check vor jedem Call) und Timeout (via AbortController + manueller Check). Der Loop gibt immer ein Ergebnis zurück — auch bei Abbruch. Der breakReason zeigt an, ob der Agent fertig wurde oder ein Limit gegriffen hat.

Combine: User Prompt in Robust Agent Loop mit generateText, Break Conditions und State Tracking, Abbruch ergibt Partial Result, Fertig ergibt Final Result

Uebung: Baue einen robusten Agenten mit ALLEN Safeguards. Kombiniere:

  1. Max Iterations (8.4) — maximal 10 Iterationen
  2. Timeout (8.4) — maximal 30 Sekunden
  3. Cost Guard (8.4) — maximal 5.000 Tokens
  4. Quality Check (8.3) — ein checkQuality-Tool, das den Loop beendet wenn score >= 70
  5. Progress Streaming (8.2) — nach jeder Iteration ein Data Part mit Iteration, Tokens und aktuellem Guard-Status

Der Agent soll recherchieren, die Qualität prüfen und abbrechen, sobald eine der fuenf Bedingungen erfuellt ist. Das Ergebnis enthaelt immer breakReason und stats.

Optional Stretch Goal: Fuege ein “Budget Warning” Data Part hinzu, das gesendet wird, wenn 80% des Token-Budgets verbraucht sind. Das Frontend kann dann eine gelbe Warnung anzeigen, bevor der Cost Guard greift.

Part of AI Learning — free courses from prompt to production. Jan on LinkedIn