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
Abschnitt betitelt „OVERVIEW“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.
WALKTHROUGH
Abschnitt betitelt „WALKTHROUGH“Schicht 1: Max Iterations
Abschnitt betitelt „Schicht 1: Max Iterations“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 zurueckgebenconsole.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.
Schicht 2: Timeout mit AbortController
Abschnitt betitelt „Schicht 2: Timeout mit AbortController“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 Abbruchconst 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.
Schicht 3: Cost Guard
Abschnitt betitelt „Schicht 3: Cost Guard“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.
Schicht 4: Alle Safeguards kombiniert
Abschnitt betitelt „Schicht 4: Alle Safeguards kombiniert“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 Limitsconst 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, }, };}
// Ausfuehrenconst 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ückCheckliste:
- Max-Iterations Guard implementiert (Loop bricht nach N Iterationen ab)
- Timeout mit
AbortControllerundsetTimeoutimplementiert -
abortSignalwird angenerateTextuebergeben -
AbortErrorwird im catch-Block abgefangen -
bestResultwird bei jedem Text-Ergebnis aktualisiert -
breakReasongibt 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 MessagesIteration 2: 974 Tokens, 5 MessagesSave-Tool erfolgreich. Loop beendet.
--- Ergebnis ---[Recherche-Text erscheint hier]Abbruchgrund: save-completeStats: {"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
Abschnitt betitelt „COMBINE“Uebung: Baue einen robusten Agenten mit ALLEN Safeguards. Kombiniere:
- Max Iterations (8.4) — maximal 10 Iterationen
- Timeout (8.4) — maximal 30 Sekunden
- Cost Guard (8.4) — maximal 5.000 Tokens
- Quality Check (8.3) — ein
checkQuality-Tool, das den Loop beendet wennscore >= 70 - 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.