1. Fehler und Idempotenz in der ChatGPT App
Im klassischen Web leben viele noch in der Paradigma „Nutzer klickt Button → eine HTTP-Anfrage → eine Antwort“. In der LLM-Welt ist das längst nicht mehr so. Das Modell kann entscheiden, Ihr Tool mehrfach aufzurufen, es kann die Antwort nach einem Klick des Nutzers auf Regenerate neu generieren, nachfragen oder unterwegs auf einen Netzwerkfehler stoßen. Am Ende kann ein und dasselbe Tool zwei- oder dreimal mit sehr ähnlichen Argumenten aufgerufen werden.
Gleichzeitig bekommt jeder Fehler plötzlich zwei Konsumenten. Einerseits das Modell, das eine verständliche, maschinenlesbare Erklärung braucht, was schiefgelaufen ist, damit es die Argumente korrigieren und einen erneuten Versuch starten kann. Andererseits das Nutzer-UI (Widget und der Chat selbst), wo eine verständliche Meldung mit weiteren Handlungsmöglichkeiten gezeigt werden soll – nicht „Error: 500 (see logs)“.
Ein weiterer wichtiger Punkt: Klassische Architekturen gehen selten davon aus, dass jemand massenhaft „Antwort wiederholen“ drückt und dadurch die Zahl der Wiederholungsanfragen (Retries) erhöht. In ChatGPT ist dieses Szenario der Normalfall. Außerdem kann die Plattform bei temporären Netzwerkproblemen selbst einen erneuten Aufruf ausführen. Deshalb ist das Konzept der Idempotenz in diesem Ökosystem kein optionales Extra, sondern eine Grundanforderung – insbesondere für Tools, die „echte“ Aktionen ausführen, also Bestellungen erstellen, Geld abbuchen, E-Mails versenden usw.
Diese Vorlesung dreht sich genau darum, wie man verhindert, dass ein fehlgeschlagener Tool-Call dem Nutzer die Laune und Ihnen den Produktionseinsatz ruiniert.
Insight
ChatGPT übergibt keine Argumente an Ihre Funktionen, sondern errät eher den Parametersatz zu Ihrem Schema. Es schaut auf die JSON Schema, den Dialogkontext und wählt statistisch Werte – und liegt ziemlich oft daneben. Fehler wie „falscher Typ“, „Pflichtfeld vergessen“, „widersprüchliche Parameter“ sind ein normaler Teil des Tool-Call-Lebens und kein Force Majeure. Laut offenen Daten und Telemetrie machen solche Fehlgriffe bei komplexen Schemata leicht bis zu ~30 % der Aufrufe aus.
Für das Modell ist das kein Problem: Es interpretiert Ihre Antwort als Signal „Argumente waren schlecht“ und versucht es einfach erneut, möglicherweise zwei- bis dreimal hintereinander, wobei es die Eingabe leicht variiert. Für Sie bedeutet das etwas anderes: Jedes Tool sollte so entworfen sein, als würde es mit hoher Wahrscheinlichkeit mehrfach mit sehr ähnlichen Parametern aufgerufen.
Genau deshalb ist Idempotenz so wichtig. ChatGPT wird immer wieder zu erraten versuchen, mit welchen Parametern Ihre Funktionen aufzurufen sind. 2–3 Versuche pro Aufruf sind normal.
2. Sichere Widget-Konfiguration: text/html+skybridge und _meta
Bevor wir in rein serverseitige Fragen (Fehler, Retries, Idempotenz) abtauchen, schließen wir einen apps-spezifischen UI-Sicherheitsaspekt: Wie sorgt man dafür, dass Ihr Widget im Chat sicher gerendert wird und nicht wie „eine gruselige Seite aus dem Internet“?
registerResource und der MIME-Typ text/html+skybridge
Ihr Widget ist aus Sicht von ChatGPT eine spezielle HTML-Ressource, die in der Sandboxed-Umgebung des ChatGPT-Clients landet und nicht direkt im Browser des Nutzers. Damit die Plattform versteht, dass es sich um ein Widget und nicht um beliebiges HTML handelt, wird der MIME-Typ text/html+skybridge verwendet.
Auf MCP-/Server-Ebene registrieren Sie die Ressource etwa so (Pseudo-TS):
// irgendwo in der Konfiguration des MCP-Servers
registerResource({
name: "giftgenius-widget",
path: "/widget",
mimeType: "text/html+skybridge", // wichtig!
});
Dieser mimeType ist das Signal an den ChatGPT-Client: „Das ist nicht einfach HTML, sondern eine komponentenbasierte Vorlage für ein eingebettetes Widget, die in einer isolierten Umgebung gestartet werden soll.“ Wenn Sie stattdessen gewöhnliches text/html angeben, kann die Plattform rohes HTML anzeigen oder das Rendering komplett verweigern.
_meta und Sicherheitssteuerung: CSP, Domain und Rahmen
Als Nächstes kommen Metadaten ins Spiel, die zusammen mit der Tool- oder Ressourcenantwort übermittelt werden — _meta. Darüber steuern Sie, welche externen Ressourcen das Widget laden darf, wie es sich visuell verhält und sogar, wie das Modell es beschreibt.
Ein typisches Strukturbeispiel:
const toolResult = {
content: "<!-- Widget-HTML -->",
_meta: {
"openai/widgetCSP": "default-src 'self'; img-src https://cdn.example.com",
"openai/widgetDomain": "https://chatgpt.com",
"openai/widgetPrefersBorder": true,
"openai/widgetDescription": "GiftGenius zeigt Geschenkempfehlungen als Karten an."
}
};
Schauen wir uns die wichtigsten Felder an.
- openai/widgetCSP legt die Content Security Policy für das Widget fest. Das ist Ihre kleine Firewall für den Browser innerhalb von ChatGPT: Sie listen explizit auf, woher Skripte, Styles, Bilder, XHRs usw. geladen werden dürfen. Die Plattform erwartet eine strikte Policy ohne das Wildcard-*; Sie müssen die verwendeten Domains explizit angeben (Chat, eigener API, CDN).
- openai/widgetDomain definiert den Origin, in dessen Kontext Ihr Widget läuft. Das ist üblicherweise die Domain von ChatGPT; Sie ersetzen sie nicht durch Ihre eigene Seite, sondern geben lediglich an, wie es in der isolierten Umgebung aussehen soll.
- openai/widgetPrefersBorder — ein rein visueller Schalter: Soll ein Rahmen um das Widget gezeichnet werden? Für GiftGenius ist ein Rahmen sinnvoll, um den Empfehlungsblock visuell von normalen Chat-Nachrichten abzuheben.
- openai/widgetDescription — eine Textbeschreibung für das Modell. Anstatt selbst eine Erklärung zu „erfinden“, kann das Modell diese Zeichenkette verwenden, wenn es dem Nutzer beschreibt, welche Oberfläche gerade geöffnet wurde. Das reduziert das Risiko seltsamer oder überflüssiger Kommentare.
Praktische Quintessenz: Wenn Sie mimeType und _meta einmal sorgfältig konfigurieren, erhalten Sie ein sicheres, isoliertes UI, das nicht dorthin zugreift, wo es nicht soll, und sich sowohl aus Nutzersicht als auch aus Plattformperspektive vorhersehbar verhält. Die Frontend-Sicherheitsseite ist damit abgehakt: Das Widget lebt in einer Sandbox und geht nur dorthin, wohin Sie es lassen. Als Nächstes fokussieren wir die Serverseite — Fehlertypen, ihre Beschreibung und idempotente Tools.
Insight: Widget-Caching
ChatGPT cached das HTML des Widgets zum Zeitpunkt der App-Registrierung. Das ChatGPT-HTML-Widget ist kein „lebendes Frontend“, sondern ein fixiertes Build-Artefakt. Bei der Veröffentlichung der App (Store oder Dev Mode) liest die Plattform die HTML-Ressource (text/html+skybridge) ein und verwendet fortan immer genau diese Version. Jede Änderung — selbst eine Zeile Text oder ein Abstand in einer Karte — bedeutet faktisch einen neuen Release.
Daraus folgt: Änderungen an HTML-Struktur, Slots, data-*-Attributen und dem Vertrag structuredContent → DOM sind kein „schneller Fix“, sondern eine vollwertige Frontend-Migration. Wenn Sie heute eine Liste aus items[] rendern und morgen auf results[] umstellen, weiß das alte Widget davon nichts: Es erhält weiterhin den alten JSON und wird inkorrekt arbeiten.
3. Fehlertypen bei der Arbeit mit Tools
Kommen wir nun zum Kern: Welche Fehler können Tools überhaupt haben, und wie unterscheiden sie sich aus UX- und Backend-Sicht? Es ist hilfreich, in vier Fehler-Schichten zu denken.
Eingabevalidierungsfehler
Die grundlegendste Ebene — wenn die Eingabeargumente dem Vertrag nicht entsprechen.
Beispiele für unsere Lern-App GiftGenius und das Tool suggest_gifts (Geschenk-Vorschläge anhand von Interessen und Budget):
- Alter kleiner als null oder größer als 120;
- negatives Budget;
- das Pflichtfeld relationship_type fehlt;
- budget_min > budget_max.
Hierzu gehört auch schlichter JSON, der dem Schema nicht entspricht. Idealerweise filtern Apps SDK und JSON Schema „ganz schlechte“ Aufrufe bereits vor Ihrem Code heraus, aber die Business-Validierung (z. B. Verhältnis von budget_min/budget_max) müssen Sie dennoch selbst vornehmen.
Fehler der Geschäftslogik
Hier ist die Eingabe zwar formal korrekt, aber gemäß Domänenregeln können Sie kein sinnvolles Ergebnis liefern.
Typische Wendungen:
- für die gegebenen Interessen und das Budget wurde kein einziges Geschenk gefunden;
- der Nutzer hat das Tageslimit für Vorschläge überschritten;
- der Artikel, den das Modell kaufen möchte, wird nicht mehr verkauft.
Das ist kein „Server ist kaputt“, sondern normale, erwartbare Situationen, die sowohl dem Nutzer als auch dem Modell in verständlicher Form präsentiert werden sollten — nicht als 500 Internal Server Error.
Fehler externer Infrastruktur
Diese Schicht ist „technische Hölle“: Datenbank nicht erreichbar, externer API-Call läuft in den Timeout, in Ihrem Code fliegt eine unbehandelte Exception.
Zum Beispiel:
- die Anfrage an den Geschenkekatalog liefert 503 oder antwortet nicht;
- MongoDB entscheidet sich plötzlich für eine Pause;
- in der Geschenk-Filterlogik teilen Sie durch null.
Aus UX-Sicht ist das oft ein Anlass zu sagen: „Service ist vorübergehend nicht verfügbar, bitte versuchen Sie es später erneut“, gelegentlich — ein versteckter Retry. Wichtig ist, nicht stumm abzutauchen und dem Nutzer keinen rohen Stacktrace zu zeigen.
Plattform-/Netzwerkfehler
Und schließlich gibt es eine Schicht, die völlig außerhalb Ihres Codes passieren kann: Der Tool-Call kam nicht an, die Verbindung brach mitten in der Antwort ab, ein Streaming-Szenario wurde unterbrochen. Das passiert häufiger, als man denkt. Wenn Sie z. B. einen kostenlosen Tunnel verwenden, kann dessen Geschwindigkeit zu Stoßzeiten so weit abfallen, dass ChatGPT Tool-Calls mit Timeout abbrechen.
Vollständig kontrollieren können Sie das nicht, aber Sie können Tools und Widget so entwerfen, dass Wiederholungen und Unterbrechungen das System nicht ins Chaos stürzen. Genau deshalb sprechen wir über Idempotenz und sorgfältige Fehlerbehandlung — nicht einfach „try/catch und vergessen“.
4. Fehler beschreiben und zurückgeben: für Modell und UI
Der wichtige Mindset-Wechsel: Ihr Fehler ist nicht nur das, was Sie in console.error geloggt haben. Er ist Teil des Tool-Vertrags, mit dem sowohl das Modell als auch das Interface arbeiten.
Fehlerstruktur
Üblicherweise ist es sinnvoll, sich an eine einfache Struktur zu halten:
type ToolError = {
code: string; // "VALIDATION_ERROR", "NO_RESULTS", "UPSTREAM_TIMEOUT"
message: string; // menschenlesbar oder kompakt für das Modell
retryable: boolean; // ergibt ein erneuter Versuch Sinn?
};
Und das Tool-Ergebnis kann als diskriminierende Union verpackt werden:
type SuggestGiftsResult =
| { ok: true; gifts: GiftCard[] }
| { ok: false; error: ToolError };
Im MCP-Protokoll gibt es zusätzlich ein separates Flag „das ist ein Fehler“, aber intern ist ein eigenes Format hilfreich, damit UI und Modell gleich interpretieren, was passiert ist.
Strategie „fail gracefully“
Nicht jede unschöne Situation muss als „harter“ Fehler ausgeflaggt werden. Manchmal ist es hilfreicher, ein leeres Ergebnis, aber ohne Fehler, nur mit einer Erklärung zurückzugeben.
Wenn etwa keine Geschenke gefunden wurden, ist es vernünftig, ok: true, ein leeres Array gifts: [] und ein Feld noResultsReason für UI und Modell zurückzugeben, statt "NO_RESULTS" als Fehler. Dann kann das Modell den Dialog fortführen: „Ich habe in diesem Budget nichts gefunden. Möchten Sie das Budget erhöhen oder die Interessen konkretisieren?“
Wenn hingegen der externe API-Anbieter komplett ausgefallen ist, ist das eher ok: false mit code: "UPSTREAM_UNAVAILABLE" und retryable: true, damit das Modell später oder mit anderen Parametern erneut versuchen kann.
Zur Erinnerung: Aus Abschnitt 3 haben wir vier Fehlerebenen. Validierungsfehler gehen üblicherweise als ok: false und retryable: false — das Modell sollte denselben Aufruf mit denselben Argumenten nicht wiederholen. Business-Situationen wie „nichts gefunden“ werden häufiger als ok: true mit leerem Ergebnis und Erklärung modelliert. Infrastrukturelle Ausfälle externer Dienste — als ok: false mit retryable: true, damit das Modell sicher erneut versuchen kann. Plattform-/Netzwerkfehler können vor oder nach Ihrem Code passieren und äußern sich in der Praxis oft als wiederholter Tool-Aufruf — genau deshalb ist uns sorgfältige Idempotenz so wichtig, über die wir gleich sprechen.
Interne Details nicht nach außen leaken
Im Servercode ist es verlockend, einfach error.toString() in die Antwort durchzureichen. Für LLM-Tools ist das keine gute Idee: Sie erhalten Müll im Dialog und geben potenziell sensible Details preis (URLs interner Services, Stacktraces, Tabellennamen). Empfehlung: Exceptions abfangen und in kompakte Fehlercodes und saubere Meldungen transformieren.
Beispiel einer minimalen Hülle:
try {
const gifts = await loadGiftsFromCatalog(input);
return { ok: true, gifts };
} catch (err) {
console.error("suggest_gifts failed", err);
return {
ok: false,
error: {
code: "UPSTREAM_ERROR",
message: "Catalog service is unavailable",
retryable: true
}
};
}
Das Modell sieht ein sauberes Signal, das UI — einen verständlichen Text, und Details bleiben in den Logs.
Fehleranzeige im Widget
Aus Sicht eines React-Widgets ist die Aufgabe banal: ok prüfen, und wenn es false ist, eine freundliche Meldung und wenn möglich eine Fortsetzungsmöglichkeit anzeigen.
function GiftResults({ result }: { result: SuggestGiftsResult }) {
if (!result.ok) {
return (
<div>
<p>Geschenke konnten nicht vorgeschlagen werden: {result.error.message}</p>
{result.error.retryable && <p>Versuchen Sie, die Parameter zu ändern oder die Anfrage zu wiederholen.</p>}
</div>
);
}
if (result.gifts.length === 0) {
return <p>Unter diesen Bedingungen wurden keine Geschenke gefunden. Passen Sie Budget oder Interessen an.</p>;
}
return <GiftCardsList gifts={result.gifts} />;
}
Das ist genau der Fall, in dem eine einfache, ehrliche Meldung die UX deutlich verbessert, anstatt „Es ist ein Fehler aufgetreten“.
Wir haben bereits vereinbart, dass ein Teil der Fehler ehrlich als retryable: true markiert werden kann und dem Nutzer „Erneut versuchen“ angeboten wird. Sobald es solche Retries im System gibt (explizit im UI oder versteckt auf Plattformseite), stellt sich die nächste Frage: Was passiert, wenn dasselbe Tool zweimal mit denselben Daten aufgerufen wird? Das ist die Geschichte der Idempotenz.
5. Idempotenz: Schutz vor einem weiteren identischen Aufruf
Jetzt zum spaßigsten Teil. Formal ist Idempotenz die Eigenschaft einer Operation, bei der ein erneuter Aufruf mit denselben Eingabedaten den Systemzustand und das Ergebnis nicht verändert. Im strengen Sinn geht es sowohl um das Ausbleiben doppelter Seiteneffekte als auch um identische Antworten. In der Praxis von ChatGPT Apps interessiert uns vor allem Ersteres: Wiederholte Aufrufe sollen Daten nicht verderben und keine neuen Entitäten erzeugen, selbst wenn die Antwort leicht variieren kann.
Im Kontext von ChatGPT Apps ist Idempotenz ein Schutz vor all dem, was bei Retries, Regenerate und der unvorhersehbaren Logik von LLMs passiert.
Wo Idempotenz besonders wichtig ist
Reine Lese-Tools sind per se meist sicher: Rufen Sie suggest_gifts mit denselben Parametern beliebig oft auf, erhalten Sie einfach eine weitere Geschenkeliste. Selbst wenn sie leicht abweicht, ändert das den Systemzustand nicht und erzeugt keine Seiteneffekte.
Kritisch sind Tools, die den Zustand externer Systeme verändern:
- Erstellen einer Bestellung (create_order);
- Durchführen einer Zahlung (charge_card, submit_payment);
- Versand von E-Mails und Benachrichtigungen (send_email, send_sms);
- Erstellen von Entitäten mit Seiteneffekten (z. B. Buchungen).
Wenn ein solches Tool zweimal hintereinander mit nahezu identischen Argumenten aufgerufen wird, können doppelte Bestellungen, doppelte Abbuchungen und andere buchhalterische „Freuden“ entstehen.
Pattern idempotency_key
Der klassische Ansatz: Dem Tool einen zusätzlichen Parameter idempotency_key hinzufügen — eine Zeichenkette als Operations-ID. Wenn eine Anfrage mit diesem Schlüssel bereits erfolgreich verarbeitet wurde, führt der Server die Aktion nicht erneut aus, sondern gibt das gespeicherte Ergebnis zurück.
Beispiel für ein erweitertes Schema des hypothetischen Tools create_checkout_session in GiftGenius:
const CreateCheckoutSchema = {
type: "object",
properties: {
giftId: {
type: "string",
description: "ID des ausgewählten Geschenks"
},
idempotency_key: {
type: "string",
description: "Eindeutiger Schlüssel der Operation zum Schutz vor Duplikaten"
}
},
required: ["giftId", "idempotency_key"]
} as const;
Auf dem Server tut der Handler ungefähr Folgendes:
async function createCheckoutSession(input: CreateCheckoutInput) {
const existing = await db.checkoutSessions.findOne({ idempotencyKey: input.idempotency_key });
if (existing) {
return existing; // vorheriges Ergebnis zurückgeben
}
const session = await paymentProvider.createSession({ giftId: input.giftId });
await db.checkoutSessions.insert({ idempotencyKey: input.idempotency_key, session });
return session;
}
Wenn das Modell das Tool aus irgendeinem Grund ein zweites Mal mit demselben idempotency_key aufruft, erhält der Nutzer keine zweite Zahlung, sondern sieht dieselbe Checkout-Session.
Aufteilung in prepare und commit
Für besonders heikle Aktionen (Zahlungen, irreversibile Änderungen) wird häufig ein zweiphasiger Ansatz verwendet: ein getrenntes Tool für die Vorbereitung (prepare_*), ein anderes — für den Commit (commit_*).
Zum Beispiel:
- prepare_order — prüft die Verfügbarkeit, berechnet die Kosten, gibt einen „Bestellentwurf“ zurück;
- commit_order — erstellt anhand der Entwurfs-ID die echte Bestellung und initiiert die Zahlung.
Dieses Design hat mehrere Vorteile. Erstens lässt sich der erste Schritt vollständig idempotent gestalten: Ein erneuter prepare_order mit denselben Parametern liefert denselben Entwurf. Zweitens kann commit_order nur nach ausdrücklicher Nutzerbestätigung erlaubt werden — praktisch sowohl aus UX- als auch aus Sicherheits-Sicht.
6. Sicheres Tool-Design
Idempotenz ist notwendig, aber nicht der einzige Sicherheitsbaustein. Sehr viel entscheidet das Design des Tool-Sets, das Sie dem Modell bereitstellen.
Prinzip der geringsten Privilegien
Die Idee ist einfach: Jedes Tool soll genau das können, was das Szenario verlangt — nicht mehr und nicht weniger. Sie brauchen kein einzelnes do_anything_with_user_account, das:
- alles beliebig lesen, aktualisieren und löschen kann;
- einen String operation und JSON payload „auf gut Glück“ annimmt.
Besser sind separate, klar beschriebene Tools:
- get_user_profile;
- update_user_preferences;
- create_order;
- cancel_order.
Die gleiche Logik gilt für GiftGenius: suggest_gifts schlägt nur Optionen vor; create_checkout_session weiß nichts darüber, wie man Bestellungen storniert oder die E‑Mail des Nutzers ändert.
Trennung von „read“- und „write“-Tools
Ein gutes Muster ist die klare Trennung der Tools, die nur Daten lesen, und derjenigen, die etwas verändern. Das Abfragen des Geschenkekatalogs (search_products, suggest_gifts) ist für sich genommen sicher, selbst wenn das Modell es überstrapaziert. create_order oder charge_payment erfordern hingegen mehr Vorsicht.
In den Beschreibungen solcher Tools sollte explizit stehen, was sie tun und in welchem Kontext sie aufgerufen werden dürfen. Zum Beispiel:
{
"name": "create_checkout_session",
"description": "Erstellt eine neue Zahlungssession für ein einzelnes Geschenk. Rufe sie NUR auf, nachdem der Nutzer seine Auswahl ausdrücklich bestätigt hat.",
"parameters": { /* ... */ }
}
Das ist kein hundertprozentiger Schutz (LLMs können sich dennoch irren), aber Sie geben zumindest ein klares Signal zu den Risiken.
Human-in-the-loop und Bestätigungen
Für wirklich „gefährliche“ Aktionen ist ein Bestätigungsszenario sinnvoll. Das Modell:
- ruft zunächst ein Tool auf, das die Kaufdaten vorbereitet und sie in einer für das UI geeigneten Form zurückgibt (Geschenktitel, Preis, Lieferadresse);
- die Plattform zeigt dem Nutzer ein Widget mit einem Button „Kauf bestätigen“;
- erst nach dem Klick wird das Commit-Tool aufgerufen, das die tatsächliche Zahlung vornimmt.
So verhindern Sie, dass das Modell „heimlich“ eine Bestellung auslöst, selbst wenn es plötzlich meint, das sei eine geniale Idee.
Risikosemantik in Beschreibungen und Annotationen
In einigen Plattformversionen tauchen spezielle Annotationen wie destructiveHint auf, die signalisieren, dass ein Tool irreversible Aktionen ausführen kann. Auch wenn solche Felder fehlen oder noch instabil sind, können Sie diese Semantik direkt in die description und die Parameternamen packen.
Zum Beispiel statt:
{
"name": "delete_user_data",
"description": "Löscht die Daten des Nutzers."
}
so:
{
"name": "request_user_data_deletion",
"description": "Markiert das Nutzerkonto zur Löschung seiner personenbezogenen Daten gemäß der Service-Policy. Verwende es NUR, nachdem der Nutzer die Löschung ausdrücklich angefordert hat."
}
Und bauen Sie darum herum einen bestätigenden, menschzentrierten UX-Flow.
7. Kleine praktische Erweiterung von GiftGenius
Verknüpfen wir das alles mit unserer Lern-App GiftGenius — einer App für Geschenkempfehlungen. Nehmen wir an, wir fügen GiftGenius ein weiteres Tool hinzu — create_checkout_session, damit der Nutzer nicht nur ein Geschenk auswählen, sondern auch zur Abwicklung übergehen kann.
Hinsichtlich JSON Schema und Sicherheit tun wir Folgendes.
Erstens fügen wir idempotency_key und eine sorgfältige Beschreibung hinzu:
const CreateCheckoutTool = {
name: "create_checkout_session",
description:
"Erstellt eine Zahlungssession für ein ausgewähltes Geschenk. " +
"Rufe sie nur auf, nachdem der Nutzer bestätigt hat, dass er dieses Geschenk kaufen möchte.",
parameters: {
type: "object",
properties: {
gift_id: {
type: "string",
description: "Identifikator des Geschenks aus dem Ergebnis von suggest_gifts."
},
idempotency_key: {
type: "string",
description: "Eindeutiger Schlüssel der Operation. Verwende bei einem erneuten Aufruf denselben Schlüssel."
}
},
required: ["gift_id", "idempotency_key"]
}
} as const;
Zweitens implementieren wir auf dem Server einen idempotenten Handler:
async function handleCreateCheckout(input: CreateCheckoutInput) {
const existing = await db.checkout.findOne({ idempotencyKey: input.idempotency_key });
if (existing) {
return { ok: true, checkout: existing };
}
const checkout = await payments.createSession({ giftId: input.gift_id });
await db.checkout.insert({ idempotencyKey: input.idempotency_key, ...checkout });
return { ok: true, checkout };
}
Drittens berücksichtigen wir Fehlerfälle:
try {
return await handleCreateCheckout(input);
} catch (err) {
console.error("create_checkout_session failed", err);
return {
ok: false,
error: {
code: "PAYMENT_PROVIDER_ERROR",
message: "Die Zahlungssession konnte nicht erstellt werden. Bitte versuchen Sie es später erneut.",
retryable: true
}
};
}
Und im Widget zeigen wir einen klaren Fehlerzustand und eventuell einen „Erneut versuchen“-Button auf UI-Ebene, der einen neuen Dialog mit dem Modell initiiert.
So wird unser hübsches Lernprojekt Schritt für Schritt weniger zur „reinen Demo-Spielerei“ und mehr zu etwas, das man theoretisch in Produktion bringen kann.
8. Häufige Fehler im Umgang mit Fehlern und Idempotenz von Tools
Fehler Nr. 1: Fehler = einfach throw und 500.
Wenn Ihr Tool bei jedem Ausfall einfach eine Exception wirft, die zu „Es ist ein Fehler aufgetreten“ wird, bleiben Modell und UI ohne verwertbare Information. Das Modell versteht nicht, ob es mit anderen Argumenten erneut versuchen sollte, und der Nutzer weiß nicht, was er als Nächstes tun soll. Viel besser ist es, eine strukturierte Fehlermeldung mit Code, kurzer Nachricht und dem Merkmal retryable zurückzugeben und die Details serverseitig zu loggen.
Fehler Nr. 2: Keine Unterscheidung zwischen Fehlertypen.
Validierungs-, Business- und Infrastrukturfehler in einen Topf zu werfen ist keine gute Idee. Am Ende sieht die Situation „nichts gefunden“ für Modell und Nutzer genauso aus wie „Datenbank abgestürzt“. Das ruiniert die UX und verhindert eine adäquate Reaktion des Modells: Statt vorzuschlagen, die Anfrage zu ändern, schaltet es in den Modus „Sorry, Service kaputt“. Besonders schmerzhaft ist es, wenn Sie z. B. Business- und Infrastrukturfehler aus Abschnitt 3 vermischen.
Fehler Nr. 3: Nicht-idempotente Operationen in einer Welt voller Retries.
Ein Tool wie create_order so zu entwerfen, als würde es immer genau einmal aufgerufen, ist der direkte Weg zu doppelten Bestellungen — insbesondere, wenn der Nutzer fleißig auf Regenerate klickt oder die Verbindung unterwegs abreißt. Wenn ein Tool Seiteneffekte hat, sollten Sie fast immer einen idempotency_key hinzufügen und Ergebnisse speichern, damit ein erneuter Aufruf keine neuen Entitäten erzeugt.
Fehler Nr. 4: Ein monströses „Universal“-Tool.
Manchmal versuchen Entwickler ein einziges Supertool mit dem Parameter action zu bauen, das alles kann: suchen, erstellen, ändern und löschen. Für LLMs ist das nahezu eine Garantie für unvorhersehbares Verhalten: Das Modell lernt schlechter, was wann aufzurufen ist, und die Konsequenzen von Fehlern werden deutlich schwerwiegender. Richtig ist es, in kleine, klar beschriebene, möglichst read-only Tools zu zerlegen und mutierende Tools separat, sorgfältig mit Bestätigungen zu gestalten.
Fehler Nr. 5: Interne Details in Antworten durchsickern lassen.
Rohe Stacktraces oder komplette Exception-Texte ins Modell und UI zu kippen ist typische Ingenieursbequemlichkeit. Das ist unfreundlich gegenüber dem Nutzer, kann interne Strukturen offenlegen und hilft dem Modell nicht, sich zu korrigieren. Fangen Sie Exceptions ab, mappen Sie sie auf kompakte Codes und einfache Meldungen, und lassen Sie alle Details in Logs und Monitoring.
Fehler Nr. 6: Keine Verknüpfung von Fehlern mit dem Widget-UX.
Oft liefert der Server sauber Fehlercodes, aber das UI-Widget verharrt in einem ewigen spinner oder einem leeren Block. Der Nutzer sieht „nichts ist passiert“, das Modell sieht, dass der Tool-Call beendet ist, und führt den Dialog fort, als wäre nichts gewesen. Viel besser ist es, separate Zustände error und empty zu entwerfen, dem Menschen verständliche Meldungen anzuzeigen und wenn möglich Handlungsoptionen vorzuschlagen (Parameter ändern, später versuchen).
Fehler Nr. 7: Ignorieren des Prinzips der geringsten Privilegien.
Selbst wenn Sie Idempotenz und gute Fehlerbehandlung umgesetzt haben, bleibt das Risiko groß, wenn Sie ein Tool wie execute_sql_anywhere beschreiben, das alles kann. Ein LLM kann es im falschen Kontext oder mit falschen Parametern aufrufen. Jedes Tool sollte so schmal wie möglich sein und genau eine klar verständliche Aktion ausführen — insbesondere wenn es um Geld oder personenbezogene Daten geht.
GO TO FULL VERSION