Fehlerbehandlung, Tracing und M4-Abschluss
Track KI · M4 Bausteine 05 und Abschluss-Check 06 · ca. 55 Min. plus Projektaufgabe
Worum es geht
Was du aus Teil 1 brauchst: Tools können über einen MCP-Server kommen, und folgenreiche Aktionen laufen durch ein Gate, das den Menschen fragt. In diesem Teil geht es um den Rest, der aus einer Schleife etwas Verantwortbares macht:
- Fehlerbehandlung: Ein Tool-Fehler bricht nicht alles ab, aber ein Modell, das dreimal in Folge denselben Fehler provoziert, wird gestoppt.
- Tracing (Ablaufprotokoll): Jeder Schritt wird so aufgezeichnet, dass du den Lauf danach rekonstruieren kannst, ohne Geheimnisse ins Log zu schreiben.
Am Ende steht der Abschluss-Check von M4 als Projektaufgabe: ein Agent mit mindestens 2 Tools und 3 Schritten, vollständig protokolliert, der bei einer mehrdeutigen Aufgabe nachfragt.
Was im Browser läuft: Ein Fake-Modell, das feste Antworten abspielt (wie in Lektion 06), und die Schleife als kleiner Simulator. Ihre Grundlagen stehen in Lektion 14, Teil 2.
Zeitplan ehrlich: etwa 20 Minuten Lesen und 35 Minuten für die drei Übungen. Die Projektaufgabe kommt obendrauf (die Quelle plant für MCP, HITL, Logging und Abschluss-Check zusammen 13 bis 18 Stunden ein, siehe Projektaufgabe). “(Quelle)” im Text meint das Kursbuch quellen/kursbuch-lerninhalte.md.
Von JS/TS her gedacht
Die eigene Fehlerklasse und das gezielte Abfangen gibt es in JavaScript genauso. Ausgeführt mit Node 20:
class WerkzeugFehler extends Error {}
function fuehreAus(tools, name, eingabe) {
try {
return { text: String(tools[name](...eingabe)), isError: false };
} catch (e) {
if (!(e instanceof WerkzeugFehler)) throw e; // nur den eigenen Fehler abfangen
return { text: `Fehler: ${e.message}`, isError: true };
}
}
const tools = {
teilen: (a, b) => { if (b === 0) throw new WerkzeugFehler("Division durch 0"); return a / b; },
kaputt: () => ({}).fehlt.x, // Programmierfehler: TypeError
};
console.log(fuehreAus(tools, "teilen", [6, 0]));
try { fuehreAus(tools, "kaputt", []); } catch (e) { console.log("durchgeschlagen:", e.constructor.name); }Ausgabe: { text: 'Fehler: Division durch 0', isError: true } und durchgeschlagen: TypeError. In Python heißt das except WerkzeugFehler statt instanceof mit erneutem throw: Alles, was nicht dazu passt, läuft einfach weiter nach oben.
| Idee | Web / TypeScript | Hier |
|---|---|---|
| Eigene Fehlerklasse | class X extends Error |
class X(Exception) |
| Nur den eigenen Fehler abfangen | if (!(e instanceof X)) throw e |
except X as e |
| Spans | OpenTelemetry-Spans, console.time |
Liste von dicts, ein dict pro Schritt |
| Geheimnisse im Log | Redaction-Optionen im Logger, Source Maps ohne Secrets | Maskieren und kürzen, bevor ein Span gespeichert wird |
Konzept
Schritt 1: Fehlerbehandlung in der Schleife
Quelle, Baustein 05: Ein Tool-Fehler muss nicht die ganze Schleife abbrechen. Als tool_result mit Fehlermeldung zurückgegeben, kann das Modell selbst entscheiden, es anders zu versuchen. Und: Eine eigene WerkzeugFehler-Klasse lässt sich gezielt abfangen, ein pauschales except Exception würde auch Programmierfehler verschlucken (Exception-Hierarchie aus M0, Lektion 04).
Die kleine Schleife als Simulator. Die Hilfsfunktionen kennst du aus Lektion 06 (Fake-Modell, tool_use, tool_result), die Schleife aus Lektion 14. Neu ist fuehre_aus mit dem gezielten except. Es gibt immer einen tool_result-Block zurück und setzt is_error (hier immer vorhanden, damit der Code r["is_error"] lesen kann). Ein Tool-Name, den es nicht gibt, ist ebenfalls ein Fehlerergebnis, denn Modelle erfinden manchmal Namen:
Ein Fehler wird zum Ergebnis, ein Programmierfehler bleibt eine Exception. Probiere beides:
Mit der Schleife zusammen: Ein Wechselkurs-Tool, das bei CHF scheitert. Das Modell liest den Fehlertext und versucht USD. Die Schleife hat ein Schrittlimit (max_schritte), sonst zahlt ein Modell, das nie fertig wird, für eine Endlosschleife. Schon hier siehst du, wo das Protokoll gebraucht wird: Ohne Spur weißt du nicht, in welchem Schritt es schiefging.
Schritt 2: Tracing, ein Span pro Schritt
Quelle: Jeder Schritt mit Tool, Input und Ergebnis geloggt ergibt im Nachhinein eine vollständige Spur (Trace) des Agentenlaufs. Die Quelle loggt mit logger.info("Schritt %d: Tool=%s Input=%s", ...) (Logging-Grundlagen: Lektion 04). Wir gehen einen Schritt weiter und sammeln pro Schritt einen Span (ein dict mit Schrittnummer, Tool, Status, Tokens). Eine Liste von Spans lässt sich auswerten, ein Textlog nur lesen.
Die Token-Zahlen sind hier nur eine Näherung: Länge des JSON-Textes geteilt durch 4. Ein echtes Modell liefert die echten Zahlen in seiner Antwort (Feld usage, bitte in der aktuellen Doku prüfen). Wichtig ist das Muster: tokens_ein ist der gesamte bisherige Verlauf, den das Modell in diesem Schritt wieder lesen muss.
Lies den Trace wie ein Detektiv: Schritt 1 ist ein fehler (CHF), Schritt 2 behebt ihn, Schritt 3 nutzt das zweite Tool, Schritt 4 ist die Antwort ohne Tool. Und sieh dir die Spalte tokens_ein an: 18, 94, 168, 238. Sie wächst von Schritt zu Schritt, weil jeder Schritt den ganzen Verlauf erneut mitschickt. Je länger die Schleife, desto teurer jeder weitere Schritt, und ein gescheiterter Schritt kostet genauso viel wie ein erfolgreicher. Wie du aus Tokens Kosten machst (Eingabe und Ausgabe haben verschiedene Preise), kennst du aus Lektion 07. Daraus folgt die Frage, die du in Übung 2 beantwortest: Welcher Schritt war der teuerste?
Schritt 3: Spans vor dem Loggen säubern
Ein Trace mit kompletten Eingaben und Ergebnissen ist nützlich und gefährlich zugleich: Er enthält Kontonummern, Namen und Schlüssel. Die Regel: erst maskieren, dann kürzen. Kürzt du zuerst, kann der Schnitt mitten durch ein Geheimnis gehen, und der Rest steht dann unmaskiert im Log, weil er nicht mehr zum Geheimnis passt. Ein Beispiel mit einer erfundenen IBAN:
Ausgabe: Zahlung von 12 EUR an *** ausg… und darunter Zahlung von 12 EUR an DE021203…. In der zweiten Zeile steht der Anfang der IBAN im Klartext. Ein Span ist verschachtelt (Eingabe als dict, Listen von Werten), also muss die Säuberung jeden String finden, egal wie tief, und darf das Original nicht verändern, weil dasselbe Span-Objekt noch für die Auswertung gebraucht wird. Das schreibst du in Übung 3.
Falle
- Pauschales
except Exception. Es verschluckt auch Tippfehler im Tool-Code. Fange die eigeneWerkzeugFehler-Klasse und lass den Rest laufen. - Wiederholen ohne Grenze. Ein Modell, das dreimal in Folge denselben Fehler provoziert, tut es beim vierten Mal meist auch. Zähle Fehler in Folge, nicht alle Fehler zusammen, und brich mit einem eigenen Status ab. Zusätzlich bleibt das Schrittlimit.
- Protokoll nur im Fehlerfall. Wer nur Fehler loggt, kann einen Lauf, der “irgendwie falsch” war, nicht rekonstruieren. Jeder Schritt bekommt einen Span, auch die erfolgreichen.
- Geheimnisse im Log. Ein Trace mit kompletten Eingaben und Ergebnissen kann Schlüssel, Namen oder Kontodaten enthalten. Maskiere oder kürze, bevor du loggst.
Übungen
Übung 1: Fehler in Folge als Abbruchkriterium (mittel)
FakeModell, tool_use, text, WerkzeugFehler und fuehre_aus aus Schritt 1 liegen bereit. Die Schleife unten hat schon Schrittlimit und Fehlerergebnisse. Dir fehlt das Abbruchkriterium für Fehler in Folge:
- Ein Schritt gilt als fehlgeschlagen, wenn mindestens ein
tool_resultdes Schrittsis_errorist. - Ein Schritt ohne Fehler setzt den Zähler wieder auf 0.
- Erreicht der Zähler
max_fehler, endet die Schleife sofort mit{"status": "fehlerlimit", "schritte": <Nummer des Schritts>}. - Antwortet das Modell ohne Tool-Wunsch:
{"status": "fertig", ...}. Läuft die Schleife bis zum Ende durch:{"status": "schrittlimit", ...}.
Programmierfehler im Tool (nicht WerkzeugFehler) sollen weiterhin als Exception nach oben durchschlagen, die Schleife darf sie nicht schlucken.
Du brauchst pro Schritt genau eine Entscheidung: war er fehlerhaft oder nicht? Was passiert mit dem Zähler in jedem der beiden Fälle, und wann prüfst du ihn?
def agent(modell, tools, aufgabe, max_schritte=6, max_fehler=2):
messages = [{"role": "user", "content": aufgabe}]
fehler_in_folge = 0
for schritt in range(1, max_schritte + 1):
antwort = modell(messages)
aufrufe = [b for b in antwort if b["type"] == "tool_use"]
if not aufrufe:
return {"status": "fertig", "schritte": schritt}
messages.append({"role": "assistant", "content": antwort})
ergebnisse = [fuehre_aus(tools, a) for a in aufrufe]
messages.append({"role": "user", "content": ergebnisse})
if any(r["is_error"] for r in ergebnisse):
fehler_in_folge += 1
if fehler_in_folge >= max_fehler:
return {"status": "fehlerlimit", "schritte": schritt}
else:
fehler_in_folge = 0
return {"status": "schrittlimit", "schritte": max_schritte}
agentÜbung 2: Den Trace auswerten (mittel)
Ein Lauf hat diese Spans erzeugt (jeder wie in Schritt 2, dazu die Felder tokens_ein und tokens_aus). Schreibe auswerten(spans, preis_ein, preis_aus), die ein Tupel liefert:
- die Nummer des teuersten Schritts (Feld
schritt, nicht der Listenindex). Kosten eines Schritts:tokens_ein * preis_ein + tokens_aus * preis_aus. Bei Gleichstand gewinnt der kleinereschritt. Fehlgeschlagene Schritte zählen mit. - die Anzahl der Spans mit
status == "fehler". - die Gesamtkosten aller Spans.
Bei einer leeren Liste: (None, 0, 0). Die Preise sind frei gewählte Beispielwerte (Ausgabe-Tokens kosten meist mehr als Eingabe-Tokens, aktuelle Preise bei Lektion 07 und beim Anbieter nachsehen).
Beispiel: Zwei Spans, Schritt 1 mit tokens_ein=100, tokens_aus=10, Schritt 2 mit tokens_ein=50, tokens_aus=40, Preise 1 und 3. Kosten: 100 + 30 = 130 für Schritt 1, 50 + 120 = 170 für Schritt 2. Der teuerste ist Schritt 2, obwohl Schritt 1 mehr Tokens insgesamt hat.
Berechne zuerst die Kosten je Span, getrennt nach Eingabe und Ausgabe. Wie wählst du bei Gleichstand das Kleinere, und was gibst du bei einer leeren Liste zurück, bevor max an ihr scheitert?
def auswerten(spans, preis_ein, preis_aus):
if not spans:
return (None, 0, 0)
def kosten(s):
return s["tokens_ein"] * preis_ein + s["tokens_aus"] * preis_aus
teuerster = min(spans, key=lambda s: (-kosten(s), s["schritt"]))
fehler = sum(1 for s in spans if s["status"] == "fehler")
return (teuerster["schritt"], fehler, sum(kosten(s) for s in spans))
auswertenÜbung 3: Spans säubern (mittel)
Schreibe maskiere_span(span, geheimnisse, max_laenge) nach Schritt 3. span ist ein verschachteltes Objekt aus dicts, Listen, Strings, Zahlen, Wahrheitswerten und None. Die Regeln:
- Gib eine neue Struktur zurück. Das übergebene Objekt (auch die inneren dicts und Listen) darf sich nicht verändern.
- Jeder String-Wert in dicts und Listen, beliebig tief, wird erst maskiert (jedes Geheimnis aus der Liste durch
***ersetzen, in der Reihenfolge der Liste) und dann gekürzt: Ist der maskierte Text länger alsmax_laenge, bleiben seine erstenmax_laengeZeichen plus ein…übrig. - Schlüssel der dicts bleiben unverändert. Zahlen, Wahrheitswerte und
Nonebleiben, was sie sind. - Leere Strings in
geheimnissewerden ignoriert.
Beispiel: maskiere_span({"ergebnis": "Zahlung an DE02 ausgelöst"}, ["DE02"], 15) ergibt {"ergebnis": "Zahlung an *** …"} (der maskierte Text Zahlung an *** ausgelöst hat mehr als 15 Zeichen, die ersten 15 sind Zahlung an *** mit abschließendem Leerzeichen).
Die Struktur ist verschachtelt: Welche Technik besucht jeden Knoten, egal wie tief? Und was würde ein Schnitt vor dem Ersetzen mit einem Geheimnis machen, das über die Schnittstelle hinausragt?
def maskiere_span(span, geheimnisse, max_laenge):
def text(t):
for g in geheimnisse:
if g:
t = t.replace(g, "***")
return t if len(t) <= max_laenge else t[:max_laenge] + "…"
def rekursiv(x):
if isinstance(x, str):
return text(x)
if isinstance(x, dict):
return {k: rekursiv(v) for k, v in x.items()}
if isinstance(x, list):
return [rekursiv(v) for v in x]
return x
return rekursiv(span)
maskiere_spanProjektaufgabe: M4-Abschluss-Check
Die Quelle (Baustein 06) fordert: Ein Agent löst eine selbst gewählte, mehrstufige Aufgabe (mindestens 2 verschiedene Tools, mindestens 3 Schritte), jeder Schritt ist protokolliert, und bei einer bewusst mehrdeutigen Testaufgabe fragt der Agent nach, statt zu raten. Dazu gehören aus der Aufgabenliste von M4 unter anderem: einen MCP-Server anbinden und dessen Tools nutzen (4 bis 6 h), Human-in-the-Loop-Rückfrage bei kritischen Aktionen (3 bis 4 h), Fehlerbehandlung und vollständiges Schritt-Logging (4 bis 5 h), Abschluss-Check end-to-end (2 bis 3 h). Das sind 13 bis 18 Stunden für diese vier Punkte. Die Punkte “Agent-vs-Workflow-Entscheidung” und “Tool-Use-Schleife” stammen aus Lektion 14 und gehören zum selben Projekt.
Aufgabe
Baue auf deiner Schleife aus Lektion 14 auf und ergänze:
- Tools vom MCP-Server. Mindestens zwei Tools kommen per
tools/listvon einem Server (der Mini-Server aus Teil 1 reicht für den ersten Durchlauf, ein echter MCP-Server ist die Steigerung). Aufrufe laufen alstools/call. - Gate. Eine feste Liste
KRITISCHE_AKTIONEN(mindestens eine folgenreiche Aktion, die laufen kann: stornieren, löschen, senden) und die Schwelle für die Sicherheit. Der Mensch wird per Konsolen-Rückfrage[j/n]gefragt, eine Ablehnung geht als Fehlerergebnis an das Modell zurück. - Nachfragen bei Mehrdeutigkeit. Ein Tool
nachfragen(frage), das die Schleife pausiert. Teste mit einer bewusst unklaren Aufgabe (“Storniere die Rechnung von gestern”). - Fehlerbehandlung. Eigene
WerkzeugFehler-Klasse, gezieltesexcept, ein Schrittlimit und ein Abbruch nachmax_fehlerFehlern in Folge mit eigenem Status. - Trace. Ein Span pro Schritt (Schritt, Tool, Status, Tokens ein und aus) und eine kleine Auswertung: teuerster Schritt, Anzahl Fehler.
Fertig, wenn
- Die Testaufgabe läuft end-to-end durch und nutzt mindestens 2 verschiedene Tools in mindestens 3 Schritten.
- Aus dem Trace allein lässt sich der Lauf rekonstruieren (welches Tool, mit welcher Eingabe, mit welchem Ergebnis, in welchem Schritt es scheiterte).
- Die mehrdeutige Aufgabe endet mit einer Rückfrage und nicht mit einer ausgeführten Aktion.
- Ein künstlich ausgelöster Tool-Fehler bricht den Lauf nicht ab, ein Programmierfehler schon (Exception sichtbar).
- Es steht in keiner Datei ein API-Schlüssel.
Selbstcheck
Optionale Lokal-Datei
Die Datei lernlabor/uebung/ki/ki_15_abschluss_agent.py ist ein lauffähiges Gerüst mit allen fünf Teilen (Mini-Server, Gate, Rückfrage, Fehler in Folge, Trace-Tabelle). Sie hat zwei Betriebsarten:
- Trockenlauf (Standard): Ohne gesetzte Umgebungsvariable
ANTHROPIC_API_KEYliefert ein Simulator feste Antworten, und der Mensch wird mit festen Antworten simuliert. Das läuft sofort. - Echter Aufruf: Mit gesetztem Schlüssel und installiertem Paket
anthropic(steht nicht in derpyproject.toml, installiere es bewusst selbst mituv add anthropic). Der Schlüssel gehört nur in deine Shell (export ANTHROPIC_API_KEY=...), nie in eine Datei. Der echte Modus ist in dieser Lektion nicht gegen die echte API getestet (bitte prüfen). Die echte API meldet keine “Sicherheit”. Im echten Modus setzt das Skript darum 1.0 ein, und das Gate entscheidet nur über die ListeKRITISCHE_AKTIONEN. Wie das Modell eine Sicherheit mitliefern könnte, ist Teil deiner Aufgabe.
cd lernlabor && uv run python uebung/ki/ki_15_abschluss_agent.pyAusgabe des Trockenlaufs (ausgeführt mit Python 3.13.9, ohne Schlüssel):
Kein ANTHROPIC_API_KEY gesetzt: Trockenlauf mit Simulator.
Modus: Trockenlauf
Tools vom Server: ['rechnung_abfragen', 'rechnung_stornieren', 'nachfragen']
Aufgabe 'klar': Prüfe R-99 und R-17, und storniere R-17, wenn sie offen ist.
[Mensch (simuliert)] Aktion rechnung_stornieren {'nummer': 'R-17'} ausführen? [j/n] -> j
{'status': 'fertig', 'text': 'R-99 gibt es nicht, R-17 war offen und ist storniert.'}
Schritt | Tool | Status | Tokens ein | Tokens aus
1 | rechnung_abfragen | fehler | 23 | 27
2 | rechnung_abfragen | ok | 91 | 27
3 | rechnung_stornieren | ok | 161 | 28
4 | None | ok | 226 | 20
Meiste Tokens: Schritt 4 (Preise bitte selbst einsetzen)
Aufgabe 'mehrdeutig': Storniere die Rechnung von gestern.
[Mensch (simuliert)] Welche Rechnungsnummer meinst du? -> R-18
{'status': 'fertig', 'text': 'Danke, ich habe die Nummer, melde mich mit dem Ergebnis.'}
Schritt | Tool | Status | Tokens ein | Tokens aus
1 | nachfragen | rueckfrage | 17 | 33
2 | None | ok | 83 | 21
Meiste Tokens: Schritt 2 (Preise bitte selbst einsetzen)
Deine Aufgaben an der Datei: Ersetze die Rechnungs-Aufgabe durch eine eigene, ersetze den Mini-Server durch einen echten MCP-Server (die Anbindung und das nötige Paket zuerst in der aktuellen MCP-Doku prüfen), und gib bei der mehrdeutigen Aufgabe dem Modell die Chance, wirklich nachzufragen. Fragt es nach, oder rät es?
Merksatz
Ein Tool-Fehler wird als Ergebnis zurückgegeben statt die Schleife abzubrechen, Fehler in Folge begrenzen den Lauf, und jeder Schritt hinterlässt einen gesäuberten Span, damit du den Lauf danach rekonstruieren kannst.
Prüfstein
Dein Agent hat gestern bei einem Kunden eine Rechnung storniert, die er nicht hätte stornieren sollen, und im Log steht nur “Lauf beendet, Status ok”. Nenne drei Dinge, die in deinem Protokoll hätten stehen müssen, um den Fall nachzuvollziehen, und zwei Stellen im Aufbau (Gate, Liste kritischer Aktionen, Rückfrage), die das hätten verhindern können.
Quelle: quellen/kursbuch-lerninhalte.md, Modul M4, Bausteine “05 Fehlerbehandlung & Tracing in Schleifen” und “06 Abschluss-Check” (Warum, Kernidee, Merksatz, Stolperfalle, Übungen, Aufgabenliste mit Stundenangaben). Aus der Quelle stammen: die Schleife mit WerkzeugFehler und Logging, der Schritt-Log als Spur des Laufs. Über die Quelle hinaus (allgemeines Fachwissen, bitte gegen die aktuelle Doku prüfen): der Begriff Span, das Feld usage für echte Token-Zahlen, das Maskieren vor dem Kürzen. Die Token-Näherung “4 Zeichen sind 1 Token” ist eine grobe Faustregel, keine echte Zählung (bitte prüfen). Die IBAN im Beispiel ist erfunden, alle Spans und Fälle sind ausgedacht.