Die Tool-Use-Schleife

Track KI · M4 Baustein 02 · ca. 55 Min. plus optionale Lokal-Übung

Worum es geht

Du weißt aus Teil 1, wann ein Agent statt eines festen Workflows sinnvoll ist: wenn Anzahl und Reihenfolge der Schritte vom Zwischenergebnis abhängen. Technisch ist er das Tool Calling aus Lektion 06, nur in einer Schleife (tool-use loop): Das Modell ruft ein Tool auf, bekommt das Ergebnis, entscheidet daraufhin den nächsten Schritt, bis es eine Endantwort gibt. Wer diese Schleife selbst gebaut hat, versteht jedes Agenten-Framework: Alle bauen dieselbe Schleife, nur mit mehr Komfort.

Was du aus Teil 1 brauchst: den Unterschied “dein Code kontrolliert die Schrittfolge” (Workflow) gegen “das Modell kontrolliert sie” (Agent), und die Idee, dass ein Agent ohne Grenzen teuer und unvorhersagbar ist. In dieser Lektion baust du die Schleife mit den drei Schutzgittern, die sie braucht: Schrittlimit, Token-Budget und Kostenbudget.

Was im Browser läuft: Kein echtes Modell. Ein simuliertes Modell (SkriptModell) spielt vorgegebene Antworten ab, darunter Tool-Aufrufe. Es prüft den Nachrichtenverlauf so streng wie eine echte API (falsche Reihenfolge oder fehlende tool_use_id gibt einen Fehler). So siehst du Verdrahtungsfehler sofort. Ein echter API-Lauf ist eine optionale Zusatzübung im Lernlabor (Schlüssel nur aus der Umgebungsvariable, ohne Schlüssel Trockenlauf mit dem Simulator).

Zeitplan ehrlich: etwa 20 Minuten Lesen, 35 Minuten für die drei Übungen. Die lokale Zusatzübung kommt obendrauf (optional, ca. 15 Minuten).

Von JS/TS her gedacht

Die Schleife ist in JavaScript dieselbe wie in Python. Ausgeführt mit Node 20 (simuliertes Modell, ein Tool-Wunsch, dann Endantwort):

const skript = [
  [{ type: "tool_use", id: "t1", name: "lagerbestand", input: { artikel: "Schraube M4" } }],
  [{ type: "text", text: "Lieferbar." }],
];
let aufruf = 0;
const modell = async () => skript[Math.min(aufruf++, skript.length - 1)];
const MAX_SCHRITTE = 5;
const messages = [{ role: "user", content: "Frage" }];
let fertig = false;
for (let schritt = 0; schritt < MAX_SCHRITTE; schritt++) {
  const content = await modell(messages);
  messages.push({ role: "assistant", content });
  const aufrufe = content.filter((b) => b.type === "tool_use");
  if (aufrufe.length === 0) { fertig = true; break; }
  messages.push({ role: "user", content: aufrufe.map((b) => ({ type: "tool_result", tool_use_id: b.id, content: "120" })) });
}
if (!fertig) console.warn("MAX_SCHRITTE erreicht");
console.log(fertig, messages.length);

Ausgabe: true 4. In Python sieht dieselbe Schleife so aus (siehe Schritt 2). Eine Besonderheit: Python hat mit for ... else eine eigene Syntax für “Schleife lief ohne break zu Ende”, in JS brauchst du ein Flag wie fertig. Die Quelle nutzt sie, in dieser Lektion steht stattdessen ein return.

Idee JavaScript / TypeScript Python
Schleife mit Obergrenze for (let i = 0; i < MAX; i++) for schritt in range(MAX)
Raus bei Endantwort break (oder return) break (oder return)
“Schleife lief ohne break zu Ende” Flag-Variable (fertig) for ... else: der else-Zweig läuft nur, wenn kein break kam
Tool-Wünsche herausfiltern content.filter(b => b.type === "tool_use") [b for b in content if b["type"] == "tool_use"]
Blöcke der Antwort Objekte im SDK (b.type) im SDK Objekte (b.type), hier im Browser dicts (b["type"])

Konzept

Schritt 1: Das simulierte Modell

Für die Schleife brauchst du ein Modell, das Tool-Aufrufe liefert. SkriptModell spielt eine Liste fester Antworten der Reihe nach ab (die letzte wiederholt sich). Jede Antwort ist ein dict mit content (Liste von Blöcken: text oder tool_use) und usage (input_tokens und output_tokens, wie bei der echten API aus Lektion 07). Die Zahlen schätzt der Simulator aus der Länge des Verlaufs: ein “Token” ist hier ein Viertel der Zeichenzahl des JSON-Textes. Das ist keine echte Token-Zählung, aber es zeigt dasselbe Verhalten: je länger der Verlauf, desto mehr Input.

Zusätzlich prüft pruefe_verlauf den Verlauf wie eine echte API es sinngemäß tut (Details der echten API bitte prüfen): Rollen wechseln sich ab, der Verlauf beginnt und endet mit user, und auf jeden Antwort-Block tool_use folgt in der nächsten user-Nachricht genau ein tool_result mit passender tool_use_id. Bei einem Verstoß wirft der Simulator ApiFehler. Der Simulator ist dabei strenger als die echte API (nach meinem Kenntnisstand, bitte prüfen): Die echte API fasst zum Beispiel zwei aufeinanderfolgende Nachrichten derselben Rolle zusammen und erlaubt als letzte Nachricht auch assistant (Vorbefüllung), sie lässt neben den tool_result-Blöcken auch Text in derselben user-Nachricht zu, und der content eines tool_result darf auch eine Liste von Blöcken statt eines Strings sein. Der Simulator verbietet das, damit Verdrahtungsfehler sofort auffallen. Die Hilfsfunktion tool_result_fuer aus Lektion 06 (Tool ausführen, Fehler als Ergebnis zurückgeben) ist hier fertig dabei.

Die Exception _ZuVieleAufrufe erbt von BaseException, damit ein breites except Exception sie nicht verschluckt: Sie bricht jede Endlosschleife mit dem Simulator sicher ab (wie schon in Lektion 06).

Schritt 2: Die Schleife von Hand durchgehen

Aufgabe: “Wann kann ich Schraube M4 bekommen?” Zwei Tools sind nötig, und das zweite braucht das Ergebnis des ersten (welches Lager hat die Schraube?). Das ist der Agenten-Fall: Das Modell kann nicht im Voraus wissen, welches Lager es abfragen muss.

Zeile für Zeile: Die Schleife ruft das Modell auf (m(messages)), hängt die Antwort unverändert als assistant-Nachricht an, sucht tool_use-Blöcke und führt sie aus. Das Ergebnis geht als user-Nachricht mit tool_result-Blöcken zurück, und der nächste Durchlauf schickt den ganzen Verlauf erneut. Gibt es keinen tool_use-Block mehr, ist die Antwort eine Endantwort, und die Schleife hört auf. Am Ende hat der Verlauf 6 Nachrichten: Frage, Wunsch 1, Ergebnis 1, Wunsch 2, Ergebnis 2, Endantwort.

Beachte die Ausgabe: input_tokens steigt von 17 über 98 auf 156, obwohl die Frage dieselbe blieb. Das ist die Stolperfalle der Quelle: Jeder Durchlauf ist ein voller API-Aufruf mit dem kompletten bisherigen Verlauf. Die Kosten wachsen nicht nur linear mit der Schrittzahl, sondern schneller, weil der Kontext jedes Mal länger wird.

Dieselbe Schleife als Funktion, mit einer festen Obergrenze. Die Quelle nutzt for ... else: Der else-Zweig läuft nur, wenn die Schleife ohne break zu Ende lief. Hier steht stattdessen ein return im Erfolgsfall und eines hinter der Schleife, die Wirkung ist gleich. Das Ergebnis ist ein dict, das auch den Grund des Endes nennt:

Die Quelle sagt es deutlich: Eine feste Obergrenze ist Pflicht, nicht optional. Ohne sie kann eine Schleife bei einem hartnäckigen Fehlerfall unbegrenzt weiterlaufen und dabei unbegrenzt Kosten verursachen. Das zweite Modell im Beispiel ist genau dieser Fall: Mit max_schritte=3 gibt es genau 3 Modellaufrufe und danach grund: "max_schritte" statt einer Endlosschleife.

Schritt 3: Token- und Kostenbudget

Ein Schrittlimit zählt Durchläufe, aber nicht, wie teuer sie waren. Drei Schritte mit kleinem Verlauf kosten weniger als zwei Schritte mit riesigen Tool-Ergebnissen. Darum gehört ein zweites Schutzgitter dazu: ein Budget in Tokens oder in Geld. Du addierst nach jedem Modellaufruf die Zahlen aus usage. Mit den Preis-Platzhaltern aus Lektion 07 (Beispielwerte der Quelle, je Modell prüfen, bitte prüfen):

Die Zahlen: Zusammen 271 Input- und 84 Output-Token, das sind 271/1000 mal 0,003 = 0,000813 plus 84/1000 mal 0,015 = 0,00126, also 0,002073. Die Rechnung selbst ist unspektakulär. Wichtig ist, wo du sie in der Schleife machst: nach jedem Modellaufruf, und die Prüfung “Budget überschritten?” bevor du das nächste teure Tool oder den nächsten Aufruf startest. Eine Endantwort liefert die Schleife trotzdem noch aus: Sie ist ja schon bezahlt.

Merke die drei Abbruchgründe einer Agenten-Schleife: fertig (Endantwort), max_schritte (Limit) und budget (Token oder Kosten). Gib den Grund immer mit zurück. Wenn ein Nutzer fragt, warum der Agent nicht fertig wurde, brauchst du genau diese Information (und ein Log mit allen Schritten, mehr dazu später in M4).

Falle

  1. Kein Schrittlimit. Der Klassiker: while True und die Hoffnung, das Modell werde schon aufhören. Bei einem Fehler im Tool (das Modell versucht es immer wieder) läuft die Schleife und das Konto leert sich.
  2. Die Assistant-Nachricht nicht anhängen. Dann fehlt dem Modell sein eigener Wunsch im Verlauf, und das tool_result hat nichts, worauf es antworten kann. Die echte API weist das mit einem Fehler ab.
  3. tool_result ohne oder mit falscher tool_use_id. Das Modell weiß nicht, welches Ergebnis zu welchem Wunsch gehört. Bei mehreren Wünschen in einer Antwort gehören alle Ergebnisse in eine user-Nachricht.
  4. Kosten falsch einschätzen. Der Verlauf wird bei jedem Schritt länger, und jeder Schritt schickt ihn komplett. Zehn Schritte kosten deutlich mehr als zehnmal ein Schritt.
  5. Nur den Erfolgsfall testen. Teste mit dem Simulator auch das Modell, das nie fertig wird, ein Tool, das einen Fehler wirft, und ein unbekanntes Tool. Das sind die Fälle, die in der Produktion teuer werden.

Übungen

Übung 1: Die Tool-Use-Schleife mit Schutzgittern (mittel)

Schreibe lauf_agent(modell, aufgabe, werkzeuge, max_schritte=5, max_tokens=None, max_kosten=None). Das ist die Schleife aus Schritt 2 mit den Budgets aus Schritt 3. Im Setup stehen SkriptModell, text_von, tool_result_fuer, kosten_von und PREIS_JE_1K. Achtung: max_tokens ist hier ein Gesamtbudget über alle Modellaufrufe, nicht wie bei der API das Längenlimit der Antwort eines einzelnen Aufrufs. In jedem Durchlauf (höchstens max_schritte Durchläufe) tust du Folgendes, in dieser Reihenfolge:

  1. Modell mit messages aufrufen. antwort["usage"] zu tokens (Summe aus Input und Output) und kosten (mit kosten_von und PREIS_JE_1K) addieren.
  2. Die Antwort unverändert als assistant-Nachricht anhängen.
  3. Gibt es keinen tool_use-Block: Endantwort. Gib zurück {"antwort": <Text>, "grund": "fertig", ...}. Das gilt auch, wenn das Budget gerade überschritten wurde.
  4. Ist max_tokens gesetzt und tokens größer als max_tokens, oder ist max_kosten gesetzt und kosten größer als max_kosten: gib {"antwort": None, "grund": "budget", ...} zurück, ohne die Tools auszuführen.
  5. Sonst alle Tools ausführen (tool_result_fuer) und alle Ergebnisse in einer user-Nachricht anhängen.

Die Tools laufen in jedem Durchlauf, in dem kein Abbruch eintritt, also auch im letzten erlaubten. Lief die Schleife max_schritte Mal durch, ohne dass einer der Fälle 3 oder 4 eintrat: {"antwort": None, "grund": "max_schritte", ...}. Jedes zurückgegebene dict hat zusätzlich die Schlüssel "schritte" (Zahl der Modellaufrufe), "tokens" und "kosten".

Gehe die fünf Punkte als Code in genau dieser Reihenfolge durch. Wo brauchst du ein return, und was ändert sich, wenn du die Budgetprüfung vor die Frage “Endantwort?” setzt?

def lauf_agent(modell, aufgabe, werkzeuge, max_schritte=5, max_tokens=None, max_kosten=None):
    messages = [{"role": "user", "content": aufgabe}]
    tokens = 0
    kosten = 0.0
    for schritt in range(max_schritte):
        antwort = modell(messages)
        u = antwort["usage"]
        tokens += u["input_tokens"] + u["output_tokens"]
        kosten += kosten_von(u["input_tokens"], u["output_tokens"], PREIS_JE_1K)
        messages.append({"role": "assistant", "content": antwort["content"]})
        aufrufe = [b for b in antwort["content"] if b["type"] == "tool_use"]
        if not aufrufe:
            return {"antwort": text_von(antwort["content"]), "grund": "fertig",
                    "schritte": schritt + 1, "tokens": tokens, "kosten": kosten}
        if (max_tokens is not None and tokens > max_tokens) or (max_kosten is not None and kosten > max_kosten):
            return {"antwort": None, "grund": "budget",
                    "schritte": schritt + 1, "tokens": tokens, "kosten": kosten}
        ergebnisse = [tool_result_fuer(b, werkzeuge) for b in aufrufe]
        messages.append({"role": "user", "content": ergebnisse})
    return {"antwort": None, "grund": "max_schritte",
            "schritte": max_schritte, "tokens": tokens, "kosten": kosten}

lauf_agent

Übung 2: Fehler im Loop finden (mittel)

Die folgende Schleife läuft ohne Syntaxfehler an und bricht dann im Simulator mit einem ApiFehler ab, oder sie läuft endlos. Es stecken mindestens drei Fehler drin, die dir in Praxis und Code-Review begegnen werden (Verlauf, Tool-Ergebnis, Beenden). Repariere sie. Im Setup stehen SkriptModell, text_von und (falls du es brauchst) tool_result_fuer. Die fertige Funktion soll den Text der Endantwort (alle Textblöcke, das erledigt text_von) zurückgeben und None, wenn die Schleife max_schritte Mal durchlief, ohne dass das Modell fertig wurde.

Spiele die Schleife im Kopf mit einem Modell durch, das gleich eine Endantwort gibt: Was steht danach in messages, und wann endet die Schleife? Dann mit einem Tool-Wunsch: Wem gehört das Ergebnis?

def agent(modell, aufgabe, werkzeuge, max_schritte=5):
    messages = [{"role": "user", "content": aufgabe}]
    for _ in range(max_schritte):
        antwort = modell(messages)
        messages.append({"role": "assistant", "content": antwort["content"]})
        aufrufe = [b for b in antwort["content"] if b["type"] == "tool_use"]
        if not aufrufe:
            return text_von(antwort["content"])
        ergebnisse = []
        for b in aufrufe:
            wert = werkzeuge[b["name"]](**b["input"])
            ergebnisse.append({"type": "tool_result", "tool_use_id": b["id"], "content": str(wert)})
        messages.append({"role": "user", "content": ergebnisse})
    return None

agent

Übung 3: Eine mehrstufige Aufgabe lösen (mittel)

Jetzt arbeitet ein regelbasierter Simulator. Er entscheidet anhand der Tool-Ergebnisse, was als Nächstes kommt, wie ein Agent. Die Frage lautet immer: “Was kostet das Buch zu ‘stichwort’ in Euro?” Er geht so vor:

  1. Er ruft suche_buch(stichwort) auf.
  2. Liegt der Preis schon in EUR, antwortet er sofort. Sonst ruft er wechselkurs(waehrung) auf und rechnet um.
  3. Liefert ein Tool einen Fehler (is_error), antwortet er, dass er nichts gefunden hat.

Die Schrittfolge ist also nicht fest: Bei einem Preis in EUR genügt ein Tool, bei einem Preis in TRY braucht es zwei, bei einem unbekannten Stichwort bricht es früh ab. Genau das ist der Grund für einen Agenten.

Deine Aufgabe sind die beiden Tools und zwei Protokoll-Funktionen. Im Setup stehen KATALOG, KURSE (erfundene Testkurse), lauf_agent (die Schleife aus Übung 1, fertig, gibt auch messages zurück) und der Simulator.

  • suche_buch(stichwort): Gib den ersten Eintrag aus KATALOG zurück, dessen titel das Stichwort enthält, ohne auf Groß- und Kleinschreibung zu achten. Der Rückgabewert ist der Eintrag als dict (titel, preis, waehrung). Gibt es keinen Treffer: wirf einen ValueError mit einem hilfreichen Text. Die Schleife macht daraus ein tool_result mit is_error.
  • wechselkurs(waehrung): Gib den Kurs aus KURSE zurück. Bei einer unbekannten Währung: ValueError.
  • schrittfolge(messages): Gib die Namen aller aufgerufenen Tools in der Reihenfolge der Aufrufe zurück, gelesen aus dem Verlauf (Liste von Namen, zum Beispiel ["suche_buch", "wechselkurs"]). Das ist dein Protokoll: Wer später fragt, was der Agent getan hat, liest es hier.
  • abbruchgrund(messages): Gib aus demselben Verlauf zurück, warum der Lauf endete: "fertig", wenn die letzte assistant-Nachricht keinen Tool-Wunsch enthält (das Modell hat geantwortet), sonst "max_schritte" (das Modell wollte noch ein Tool, das Limit hat den Lauf beendet). Der Verlauf enthält keine Nachricht mit dem Grund, du musst ihn aus den Nachrichten ableiten.

Was passiert in der Schleife, wenn ein Tool eine Exception wirft, und was, wenn es stillschweigend None zurückgibt? Und in welchen Nachrichten des Verlaufs stehen die Namen der Tools?

def suche_buch(stichwort):
    for buch in KATALOG:
        if stichwort.lower() in buch["titel"].lower():
            return buch
    raise ValueError(f"kein Buch zum Stichwort {stichwort!r}")

def wechselkurs(waehrung):
    if waehrung not in KURSE:
        raise ValueError(f"unbekannte Währung {waehrung!r}")
    return KURSE[waehrung]

def schrittfolge(messages):
    return [b["name"]
            for n in messages if n["role"] == "assistant"
            for b in n["content"] if b["type"] == "tool_use"]

def abbruchgrund(messages):
    letzte = [n for n in messages if n["role"] == "assistant"][-1]
    wuensche = [b for b in letzte["content"] if b["type"] == "tool_use"]
    return "max_schritte" if wuensche else "fertig"

suche_buch, wechselkurs, schrittfolge, abbruchgrund

Zusatzübung (lokal): echter Agent mit zwei Tools (optional, 15 Min.)

Die Quellenübung verlangt: die Schleife mit zwei einfachen Tools bauen und eine Aufgabe lösen lassen, die beide nacheinander braucht. Die Datei lernlabor/uebung/ki/ki_14_echter_agent.py baut genau das (Textsuche in einer festen Dokumentliste plus Wechselkurs, Testkurse erfunden). Sie hat zwei Betriebsarten:

  • Trockenlauf (Standard): Ohne gesetzte Umgebungsvariable ANTHROPIC_API_KEY liefert ein Simulator die Antworten im Format der echten API (Objekte mit .type, .id, .name, .input). Das läuft sofort und kostet nichts.
  • Echter Aufruf: Mit gesetztem Schlüssel und installiertem Paket anthropic. Das Paket steht nicht in der pyproject.toml des Lernlabors, installiere es nicht ohne Rückfrage (uv add anthropic). Fehlt es, meldet das Skript es und läuft im Trockenlauf weiter. Der Schlüssel gehört nur in deine Shell, nie in eine Datei. Echte Aufrufe kosten Geld, aktuelle Preise bitte beim Anbieter prüfen. Das Skript hat MAX_SCHRITTE, und bei Erreichen schreibt es eine Warnung, wie in der Quelle.
cd lernlabor && uv run python uebung/ki/ki_14_echter_agent.py

Deine Aufgabe: Starte den Trockenlauf, lies das Protokoll der Schritte und trage in aufgabe die Zahl der Modellaufrufe ein, die der Lauf gebraucht hat. Mit Schlüssel (optional): Vergleiche, ob das echte Modell dieselbe Schrittfolge wählt, und beobachte, wie input_tokens pro Schritt wächst.

Zähle im Protokoll die Schritte: Ein Modellaufruf pro Durchlauf, die Endantwort zählt mit.

aufgabe = 3

Zwei Tool-Wünsche nacheinander (Dokument suchen, dann Kurs holen) und danach die Endantwort: drei Modellaufrufe.

Merksatz

Die Tool-Use-Schleife schickt bei jedem Schritt den ganzen Verlauf erneut: Darum braucht sie ein Schrittlimit, ein Token- oder Kostenbudget und einen Grund für jedes Ende (fertig, max_schritte, budget).

Prüfstein

Ein Kollege baut einen Agenten mit einer while True-Schleife und sagt: “Das Modell hört schon auf, wenn es fertig ist.” Nenne drei Szenarien, in denen es nicht aufhört, und welche drei Schutzgitter du einbaust (denke an Schritte, Tokens und den Verlauf, der bei jedem Aufruf mitgeschickt wird).


Quelle: quellen/kursbuch-lerninhalte.md, Modul M4, Baustein “02 Die Tool-Use-Schleife” (Warum, Kernidee mit Schleifencode und MAX_SCHRITTE, Merksatz, Stolperfalle, Übungen; der Modellname und die Felder des SDK stehen in der Quelle, der Modellname claude-sonnet-4-5 bitte prüfen, ob er aktuell ist). Über die Quelle hinaus (allgemeines Fachwissen, bitte gegen die aktuelle Literatur und API-Doku prüfen): das Feld is_error im tool_result, die Prüfung des Nachrichtenverlaufs durch den Simulator (die echte API prüft sinngemäß ähnlich, Details bitte prüfen), das Token- und Kostenbudget in der Schleife, die Einordnung der Abbruchgründe. Die Preise (0,003 und 0,015 je 1000 Token) sind Platzhalter wie in Lektion 07 (je Modell prüfen). Der Simulator, die Beispielantworten, der Katalog und die Wechselkurse sind von Hand geschrieben und erfunden, die “Token” des Simulators sind ein Viertel der Zeichenzahl und keine echte Zählung.