CodeGym /Kursy /ChatGPT Apps /Błędy, idempotencja i projektowanie „bezpiecznych” narzęd...

Błędy, idempotencja i projektowanie „bezpiecznych” narzędzi

ChatGPT Apps
Poziom 4 , Lekcja 4
Dostępny

1. Błędy i idempotencja w ChatGPT App

W klasycznym webie wiele osób wciąż żyje w paradygmacie „użytkownik kliknął przycisk → jedno żądanie HTTP → jedna odpowiedź”. W świecie LLM już dawno tak nie jest. Model może zdecydować się wywołać wasze narzędzie kilka razy, może wygenerować odpowiedź ponownie po kliknięciu przez użytkownika Regenerate, może dopytać, może napotkać błąd sieci po drodze. W efekcie to samo narzędzie może zostać wywołane dwa lub trzy razy z bardzo podobnymi argumentami.

Jednocześnie każdy błąd nagle ma dwóch odbiorców. Z jednej strony – model, który potrzebuje zrozumiałego, możliwego do odczytania maszynowo wyjaśnienia, co poszło nie tak, aby mógł poprawić argumenty i spróbować ponownie. Z drugiej – UI użytkownika (widżet i sam czat), gdzie trzeba pokazać ludzką wiadomość i zaproponować dalsze działania, a nie „Error: 500 (see logs)”.

Jeszcze jedna ważna kwestia: klasyczna architektura rzadko zakłada, że ktoś będzie masowo naciskał „powtórz odpowiedź”, zwiększając tym samym liczbę powtórnych żądań (retries). W ChatGPT ten scenariusz to norma. Dodatkowo platforma sama może wykonać ponowne wywołanie przy tymczasowych problemach sieciowych. Dlatego koncepcja idempotencji w tym ekosystemie to nie dodatkowa opcja, lecz wymaganie podstawowe, zwłaszcza dla narzędzi, które robią coś „naprawdę” – tworzą zamówienia, pobierają płatności, wysyłają maile itd.

Ten wykład jest o tym, jak nie pozwolić, by jedno nieudane wywołanie narzędzia (tool call) popsuło użytkownikowi nastrój, a wam – produkcję.

Wskazówka

ChatGPT nie przekazuje argumentów do waszych funkcji, lecz raczej zgaduje zestaw argumentów do waszego schematu. Patrzy na JSON Schema, kontekst dialogu i statystycznie dobiera wartości – i dość często się myli. Błędy typu „zły typ”, „brak obowiązkowego pola”, „sprzeczne parametry” są normalną częścią życia wywołań narzędzi, a nie force majeure. Według danych publicznych i telemetrii takie pudła potrafią zajmować do ~30% wywołań dla złożonych schematów.

Dla modelu to nie problem: odbiera waszą odpowiedź jako sygnał „argumenty były złe” i po prostu próbuje jeszcze raz, być może dwa–trzy razy pod rząd, lekko zmieniając wejście. Dla was oznacza to co innego: każde narzędzie należy projektować tak, jakby niemal na pewno zostało wywołane kilka razy z bardzo podobnymi parametrami.

Dlatego idempotencja jest tak ważna. ChatGPT będzie w kółko próbował zgadnąć, z jakimi parametrami należy wywołać wasze funkcje. 2–3 próby na jedno wywołanie – to norma.

2. Bezpieczna konfiguracja widżetu: text/html+skybridge i _meta

Zanim przejdziemy do czysto serwerowych tematów (błędy, retries, idempotencja), zamkniemy jeden aspekt specyficzny dla Apps SDK dotyczący bezpieczeństwa UI: jak sprawić, aby wasz widżet renderował się w czacie bezpiecznie, a nie jak „straszna strona z internetu”.

registerResource i MIME-typ text/html+skybridge

Wasz widżet z punktu widzenia ChatGPT to specjalny zasób HTML, który trafia do piaskownicy klienta ChatGPT, a nie bezpośrednio do przeglądarki użytkownika. Aby platforma zrozumiała, że to właśnie widżet, a nie zwykły HTML, używa się MIME-typu text/html+skybridge.

Na poziomie MCP/serwera rejestrujecie zasób czymś w rodzaju (pseudo-TS):

// gdzieś w konfiguracji serwera MCP
registerResource({
  name: "giftgenius-widget",
  path: "/widget",
  mimeType: "text/html+skybridge", // ważne!
});

Ten mimeType to sygnał dla klienta ChatGPT: „to nie zwykły HTML, ale komponentowy szablon wbudowanego widżetu, który należy uruchomić w środowisku izolowanym”. Jeśli wskażecie zwykły text/html, platforma może pokazać surowy HTML albo w ogóle odmówić renderowania.

_meta i kontrola bezpieczeństwa: CSP, domena i ramka

Dalej wchodzą metadane przekazywane razem z odpowiedzią narzędzia lub zasobu – _meta. Przez nie kontrolujecie, jakie zasoby zewnętrzne widżet może ładować, jak zachowuje się wizualnie, a nawet jak model będzie go opisywał.

Typowy przykład struktury:

const toolResult = {
  content: "<!-- HTML widżetu -->",
  _meta: {
    "openai/widgetCSP": "default-src 'self'; img-src https://cdn.example.com",
    "openai/widgetDomain": "https://chatgpt.com",
    "openai/widgetPrefersBorder": true,
    "openai/widgetDescription": "GiftGenius pokazuje rekomendacje prezentów w formie kart."
  }
};

Omówmy kluczowe pola.

  • openai/widgetCSP ustawia Content Security Policy dla widżetu. To wasza mała zapora (firewall) dla przeglądarki wewnątrz ChatGPT: jawnie wymieniacie, skąd można ładować skrypty, style, obrazy, wykonywać XHR itd. Platforma oczekuje restrykcyjnej polityki bez wildcard *; należy jawnie wskazać używane domeny (czat, własne API, CDN).
  • openai/widgetDomain ustawia origin, w którego kontekście będzie działał wasz widżet. Zwykle jest to domena ChatGPT; nie podmieniacie jej na własny serwis, a jedynie informujecie, jak ma to wyglądać w środowisku izolowanym.
  • openai/widgetPrefersBorder – czysto wizualna flaga: rysować ramkę wokół widżetu czy nie. Dla GiftGenius sensownie jest zostawić ramkę, aby wizualnie oddzielić blok rekomendacji od zwykłych wiadomości w czacie.
  • openai/widgetDescription – opis tekstowy dla modelu. Zamiast prób „wymyślać” objaśnienie, model może użyć tego ciągu, kiedy tłumaczy użytkownikowi, jaki interfejs właśnie się otworzył. To zmniejsza ryzyko dziwnych lub nadmiarowych komentarzy modelu.

Wniosek praktyczny: raz porządnie konfigurując mimeType i _meta, otrzymujecie bezpieczny, izolowany UI, który nie wchodzi tam, gdzie nie trzeba, i zachowuje się przewidywalnie zarówno z punktu widzenia użytkownika, jak i platformy. Część frontendowa bezpieczeństwa jest ogarnięta: widżet żyje w piaskownicy i wychodzi tylko tam, gdzie mu pozwolicie. Dalej skupimy się na stronie serwerowej – typach błędów oraz tym, jak je opisywać i budować narzędzia idempotentne.

Wskazówka: keszowanie widżetu

ChatGPT keszuje HTML widżetu w momencie rejestracji aplikacji. HTML‑widżet ChatGPT to nie „żywy frontend”, lecz utrwalony artefakt builda. Przy publikacji aplikacji (Store lub Dev Mode) platforma odczytuje zasób HTML (text/html+skybridge) i potem zawsze używa właśnie tej wersji. Każda zmiana – nawet jedna linijka tekstu czy odstęp w karcie – w praktyce oznacza nowy release.

Wniosek: poprawki struktury HTML, slotów, atrybutów data-* i kontraktu structuredContentDOM to nie „szybki fix”, lecz pełnoprawna migracja frontendowa. Jeśli dziś renderujecie listę z items[], a jutro przechodzicie na results[], stary widżet się o tym nie dowie: nadal będzie dostawał dotychczasowy JSON i będzie działał niepoprawnie.

3. Typy błędów w pracy narzędzi

Przejdźmy teraz do sedna: jakie w ogóle bywają błędy narzędzia i czym różnią się z punktu widzenia UX i backendu. Wygodnie myśleć o czterech warstwach błędów.

Błędy walidacji wejścia

Najbardziej podstawowy poziom – kiedy argumenty wejściowe w ogóle nie odpowiadają kontraktowi.

Przykłady dla naszej szkoleniowej aplikacji GiftGenius i jej narzędzia suggest_gifts (dobór prezentów po zainteresowaniach i budżecie):

  • wiek mniejszy od zera albo większy niż 120;
  • budżet ujemny;
  • brak pola obowiązkowego relationship_type;
  • budget_min > budget_max.

W to miejsce wpada też zwykły JSON niezgodny ze schematem. W idealnym świecie Apps SDK i JSON Schema odfiltrują „zupełnie złe” wywołania jeszcze przed waszym kodem, ale walidację biznesową (np. relację budget_min/budget_max) i tak trzeba zrobić samemu.

Błędy logiki biznesowej

Tutaj wejście niby jest poprawne, ale według reguł domeny nie możecie zwrócić sensownego rezultatu.

Typowe scenariusze:

  • dla zadanych zainteresowań i budżetu nie znaleziono żadnego prezentu;
  • użytkownik przekroczył dzienny limit doborów;
  • produkt, który model prosi kupić, nie jest już sprzedawany.

To nie „serwer się zepsuł”, lecz normalne, oczekiwane sytuacje, które trzeba przedstawić użytkownikowi i modelowi w strawnej formie, a nie jako 500 Internal Server Error.

Błędy zewnętrznej infrastruktury

Ta warstwa to już „techniczne piekło”: baza jest niedostępna, zewnętrzne API time‑outuje, wewnątrz waszego kodu poleciał nieobsłużony wyjątek.

Na przykład:

  • żądanie do katalogu prezentów zwraca 503 albo nie odpowiada;
  • MongoDB nagle postanowiła zrobić pauzę;
  • w kodzie filtrowania prezentów dzielicie przez zero.

Z punktu widzenia UX to często powód, by powiedzieć: „Usługa tymczasowo niedostępna, spróbuj później”, czasem – spróbować ukrytego retry. Ważne jednak, by nie wyciszać problemu i nie pokazywać użytkownikowi surowego stack trace.

Błędy platformy/sieci

I wreszcie warstwa, która może wydarzyć się całkowicie poza waszym kodem: tool‑call nie dotarł, połączenie zerwało się w środku odpowiedzi, scenariusz streaming został przerwany. To zdarza się częściej, niż myślicie. Na przykład przy użyciu darmowego tunelu w godzinach szczytu jego przepustowość spada tak bardzo, że wywołania narzędzi ChatGPT padają przez timeout.

Nie macie nad tym pełnej kontroli, ale możecie projektować narzędzia i widżet tak, aby ponowne wywołania i przerwania nie zamieniały systemu w chaos. Dlatego mówimy o idempotencji i uważnej obsłudze błędów, a nie tylko „try/catch i po sprawie”.

4. Jak opisywać i zwracać błędy: jednocześnie dla modelu i dla UI

Ważna zmiana myślenia: wasz błąd to nie tylko to, co zalogowaliście w console.error. To część kontraktu narzędzia, z którą będą pracować zarówno model, jak i interfejs.

Struktura błędu

Zwykle warto trzymać się prostej struktury:

type ToolError = {
  code: string;        // "VALIDATION_ERROR", "NO_RESULTS", "UPSTREAM_TIMEOUT"
  message: string;     // tekst dla człowieka lub zwięzły dla modelu
  retryable: boolean;  // czy ma sens spróbować ponownie
};

I wynik narzędzia można opakować w rozłączną sumę (discriminated union):

type SuggestGiftsResult =
  | { ok: true; gifts: GiftCard[] }
  | { ok: false; error: ToolError };

W MCP‑protokole istnieje jeszcze osobna flaga „to jest błąd”, ale wewnątrz warto trzymać się własnego formatu, aby UI i model mogły jednakowo interpretować to, co zaszło.

Strategia „fail gracefully”

Nie każdą nieprzyjemną sytuację trzeba oznaczać jako „twardy” błąd. Czasem o wiele bardziej użytecznie jest zwrócić pusty wynik, ale bez błędu – za to z wyjaśnieniem.

Na przykład, jeśli prezentów nie znaleziono, rozsądnie jest zwrócić ok: true, pustą tablicę gifts: [] i jakieś pole noResultsReason dla UI i modelu, zamiast zwracać błąd "NO_RESULTS". Wtedy model może kontynuować dialog: „Nic nie znalazłem w tym budżecie – chcesz podnieść budżet albo doprecyzować zainteresowania?”.

Za to jeśli zewnętrzne API padło całkowicie, to raczej ok: false z code: "UPSTREAM_UNAVAILABLE" i retryable: true, aby model miał szansę spróbować jeszcze raz później albo z innymi parametrami.

Przypomnijmy: z rozdziału 3 mamy cztery warstwy błędów. Błędy walidacyjne zwykle idą jako ok: false i retryable: false – model nie powinien powtarzać tego samego wywołania z tymi samymi argumentami. Sytuacje biznesowe w rodzaju „nic nie znaleziono” częściej oznacza się jako ok: true z pustym wynikiem i objaśnieniem. Awarie infrastrukturalne usług zewnętrznych – jako ok: false z retryable: true, by model mógł bezpiecznie spróbować ponownie. Z kolei błędy platformy/sieci mogą wystąpić przed albo po waszym kodzie i w praktyce często wyglądają jak ponowne wywołanie narzędzia – dlatego tak ważna jest uważna idempotencja, o której za chwilę.

Nie wypuszczamy szczegółów wewnętrznych na zewnątrz

W kodzie serwerowym łatwo ulec pokusie i po prostu przepchnąć error.toString() w odpowiedzi. Dla narzędzi LLM to kiepski pomysł: dostaniecie śmieci w dialogu i potencjalnie ujawnicie wrażliwe szczegóły (URL‑e usług wewnętrznych, stack trace’y, nazwy tabel). Rekomendacja – przechwytywać wyjątki i mapować je na zwarte kody błędów oraz zgrabne komunikaty.

Przykład minimalnej otoczki:

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
    }
  };
}

Model widzi schludny sygnał, UI – zrozumiały tekst, a szczegóły zostają w logach.

Wyświetlanie błędu w widżecie

Z punktu widzenia widżetu React zadanie jest banalne: sprawdzić ok, a jeśli jest false, pokazać przyjazny komunikat i – jeśli to możliwe – sposób na kontynuację.

function GiftResults({ result }: { result: SuggestGiftsResult }) {
  if (!result.ok) {
    return (
      <div>
        <p>Nie udało się dobrać prezentów: {result.error.message}</p>
        {result.error.retryable && <p>Spróbuj zmienić parametry lub ponowić żądanie.</p>}
      </div>
    );
  }

  if (result.gifts.length === 0) {
    return <p>Nie znaleziono prezentów dla tych warunków. Spróbuj zmienić budżet lub zainteresowania.</p>;
  }

  return <GiftCardsList gifts={result.gifts} />;
}

To właśnie ten przypadek, gdy prosta i szczera wiadomość czyni UX znacznie przyjemniejszym niż „coś poszło nie tak”.

Uzgodniliśmy już, że część błędów można uczciwie oznaczać jako retryable: true i proponować użytkownikowi „spróbuj ponownie”. Gdy tylko w systemie pojawiają się takie ponowienia (jawne w UI lub ukryte po stronie platformy), rodzi się kolejne pytanie: co się stanie, jeśli to samo narzędzie zostanie wywołane dwa razy z tymi samymi danymi? To już historia o idempotencji.

5. Idempotencja: ochrona przed „kolejnym takim samym wywołaniem”

Czas na najprzyjemniejszą część. Formalnie idempotencja to właściwość operacji, przy której powtórne wywołanie z tymi samymi danymi wejściowymi nie zmienia stanu systemu ani wyniku. W ścisłym sensie chodzi i o brak powtórnych efektów ubocznych, i o taki sam rezultat. W praktyce ChatGPT Apps interesuje nas przede wszystkim to pierwsze: by powtórne wywołania nie psuły danych i nie tworzyły nowych bytów, nawet jeśli sama odpowiedź może się nieco różnić.

W kontekście ChatGPT Apps idempotencja to ochrona przed całym tym dobrem, które dzieje się przy retries, Regenerate i nieprzewidywalnej logice LLM.

Gdzie idempotencja jest szczególnie ważna

Narzędzia tylko do odczytu z definicji są zwykle bezpieczne: ile by nie wołać suggest_gifts z tymi samymi parametrami, po prostu dostaniecie kolejną listę prezentów. Nawet jeśli nieco się różni, nie zmienia to stanu systemu i nie tworzy efektów ubocznych.

Krytyczne są narzędzia, które modyfikują stan systemów zewnętrznych:

  • tworzenie zamówienia (create_order);
  • realizacja płatności (charge_card, submit_payment);
  • wysyłka maili i powiadomień (send_email, send_sms);
  • tworzenie bytów z efektami ubocznymi (np. rezerwacji).

Jeśli takie narzędzie jest wywoływane dwa razy pod rząd z niemal identycznymi argumentami, mogą pojawić się duplikaty zamówień, podwójne obciążenia itd.

Wzorzec idempotency_key

Klasyczne podejście: dodać do narzędzia dodatkowy parametr idempotency_key – tekstowy identyfikator operacji. Jeśli żądanie z takim kluczem zostało już pomyślnie obsłużone, serwer nie wykonuje działania jeszcze raz, tylko zwraca zapamiętany rezultat.

Przykład rozszerzonego schematu dla hipotetycznego narzędzia create_checkout_session w GiftGenius:

const CreateCheckoutSchema = {
  type: "object",
  properties: {
    giftId: {
      type: "string",
      description: "ID wybranego prezentu"
    },
    idempotency_key: {
      type: "string",
      description: "Unikalny klucz operacji chroniący przed duplikatami"
    }
  },
  required: ["giftId", "idempotency_key"]
} as const;

Po stronie serwera handler robi mniej więcej to:

async function createCheckoutSession(input: CreateCheckoutInput) {
  const existing = await db.checkoutSessions.findOne({ idempotencyKey: input.idempotency_key });
  if (existing) {
    return existing; // zwracamy poprzedni wynik
  }

  const session = await paymentProvider.createSession({ giftId: input.giftId });
  await db.checkoutSessions.insert({ idempotencyKey: input.idempotency_key, session });
  return session;
}

Jeśli model z jakiegoś powodu wywoła narzędzie po raz drugi z tym samym idempotency_key, użytkownik nie dostanie drugiej płatności – zobaczy ten sam checkout.

Rozdzielenie prepare i commit

Dla szczególnie czułych działań (płatności, nieodwracalne zmiany) często stosuje się podejście dwuetapowe: osobne narzędzie do przygotowania (prepare_*), osobne – do commitu (commit_*).

Na przykład:

  • prepare_order – sprawdza dostępność produktu, liczy koszt, zwraca „brudnopis zamówienia”;
  • commit_order – na podstawie ID brudnopisu tworzy właściwe zamówienie i inicjuje płatność.

Taki design daje kilka bonusów. Po pierwsze, można uczynić pierwszy krok w pełni idempotentnym: powtórny prepare_order z tymi samymi parametrami zwróci ten sam brudnopis. Po drugie, commit_order można pozwolić wywołać tylko po wyraźnym potwierdzeniu użytkownika, co jest wygodne zarówno z punktu widzenia UX, jak i bezpieczeństwa.

6. Bezpieczny projekt narzędzi

Idempotencja jest konieczna, ale nie jedyna. Bardzo dużo zależy od samego projektu zestawu narzędzi, który oddajecie modelowi.

Zasada najmniejszych uprawnień

Idea jest prosta: każde narzędzie powinno umieć dokładnie to, czego potrzeba w danym scenariuszu – i ani linijki więcej. Nie róbcie jednej funkcji do_anything_with_user_account, która:

  • może czytać, aktualizować i usuwać wszystko wokoło;
  • przyjmuje ciąg operation i JSON payload „na dobrą wiarę”.

Lepiej mieć osobne, jasno opisane narzędzia:

  • get_user_profile;
  • update_user_preferences;
  • create_order;
  • cancel_order.

Ta sama logika dotyczy GiftGenius: suggest_gifts jedynie dobiera opcje; create_checkout_session nie ma pojęcia, jak anulować zamówienia ani jak zmienić e‑mail użytkownika.

Rozdzielenie narzędzi „read” i „write”

Dobry wzorzec – wyraźnie rozdzielać narzędzia, które tylko czytają dane, od tych, które coś zmieniają. Zapytanie katalogu prezentów (search_products, suggest_gifts) jest samo w sobie bezpieczne, nawet jeśli model z niego nadużywa. Za to create_order czy charge_payment wymagają już większej ostrożności.

W opisach takich narzędzi warto jawnie pisać, co robią i w jakim kontekście wolno je wywoływać. Na przykład:

{
  "name": "create_checkout_session",
  "description": "Tworzy nową sesję płatności dla jednego prezentu. Wywołuj WYŁĄCZNIE po tym, jak użytkownik wyraźnie potwierdził swój wybór.",
  "parameters": { /* ... */ }
}

To nie jest stuprocentowa ochrona (LLM i tak może się pomylić), ale przynajmniej wysyłacie jej wyraźny sygnał o ryzykach.

Human-in-the-loop i potwierdzenia

Dla naprawdę „niebezpiecznych” działań warto budować scenariusz z potwierdzeniem. Na przykład model:

  1. Najpierw wywołuje narzędzie, które przygotowuje dane do zakupu i zwraca je w formie wygodnej dla UI (nazwa prezentu, cena, adres dostawy).
  2. Platforma pokazuje użytkownikowi widżet z przyciskiem „Potwierdź zakup”.
  3. Dopiero po kliknięciu przycisku wywoływane jest narzędzie commitu, które realizuje rzeczywistą płatność.

W ten sposób nie dajecie modelowi możliwości „po cichu” złożyć zamówienia bez udziału użytkownika, nawet jeśli uzna, że to bardzo sprytne rozwiązanie.

Semantyka ryzyka w opisach i adnotacjach

W niektórych wersjach platformy pojawiają się specjalne adnotacje, takie jak destructiveHint, sygnalizujące, że narzędzie może wykonywać nieodwracalne działania. Nawet jeśli takich pól nie ma lub są niestabilne, możecie umieszczać tę semantykę bezpośrednio w description i nazwach parametrów.

Zamiast:

{
  "name": "delete_user_data",
  "description": "Usuwa dane użytkownika."
}

zróbcie:

{
  "name": "request_user_data_deletion",
  "description": "Oznacza konto użytkownika do usunięcia jego danych osobowych zgodnie z polityką serwisu. Używaj WYŁĄCZNIE po tym, jak użytkownik wyraźnie zażądał usunięcia."
}

I przy okazji zbudujcie wokół tego ludzki UX potwierdzający.

7. Małe praktyczne usprawnienie GiftGenius

Połączmy to wszystko z naszą szkoleniową aplikacją GiftGenius – App do doboru prezentów. Załóżmy, że dodajemy do GiftGenius jeszcze jedno narzędzie – create_checkout_session, aby użytkownik mógł nie tylko dobrać prezent, ale i przejść do zakupu.

Z punktu widzenia JSON Schema i bezpieczeństwa robimy następujące rzeczy.

Po pierwsze, dodajemy idempotency_key i staranny opis:

const CreateCheckoutTool = {
  name: "create_checkout_session",
  description:
    "Tworzy sesję płatności dla jednego wybranego prezentu. " +
    "Wywołuj tylko po tym, jak użytkownik potwierdził, że chce kupić ten prezent.",
  parameters: {
    type: "object",
    properties: {
      gift_id: {
        type: "string",
        description: "Identyfikator prezentu z wyniku suggest_gifts."
      },
      idempotency_key: {
        type: "string",
        description: "Unikalny klucz operacji. Użyj tego samego klucza przy ponownym wywołaniu."
      }
    },
    required: ["gift_id", "idempotency_key"]
  }
} as const;

Po drugie, na serwerze implementujemy idempotentny 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 };
}

Po trzecie, uwzględniamy błędy:

try {
  return await handleCreateCheckout(input);
} catch (err) {
  console.error("create_checkout_session failed", err);
  return {
    ok: false,
    error: {
      code: "PAYMENT_PROVIDER_ERROR",
      message: "Nie udało się utworzyć sesji płatności. Spróbuj później.",
      retryable: true
    }
  };
}

W widżecie pokazujemy zrozumiały stan błędu i – być może – przycisk „Powtórz” na poziomie UI, który inicjuje nowy dialog z modelem.

Tak krok po kroku nasz miły projekt szkoleniowy przestaje być „zabawką do dema” i powoli zamienia się w coś, co teoretycznie można wypuścić na produkcję.

8. Typowe błędy przy pracy z błędami i idempotencją narzędzi

Błąd nr 1: Błąd = po prostu throw i 500.
Jeśli przy każdym potknięciu wasz tool po prostu wyrzuca wyjątek, który zmienia się w „coś poszło nie tak”, model i UI zostają bez informacji. Model nie rozumie, czy warto powtórzyć wywołanie z innymi argumentami, a użytkownik nie wie, co zrobić dalej. Znacznie lepiej zwracać strukturalny błąd z kodem, krótkim komunikatem i znacznikiem retryable, a w serwerze logować szczegóły.

Błąd nr 2: Brak rozróżnienia typów błędów.
Mieszanie walidacyjnych, biznesowych i infrastrukturalnych błędów do jednego kotła to zły pomysł. W efekcie sytuacja „nic nie znaleziono” wygląda dla modelu i użytkownika tak samo, jak „padła baza danych”. To psuje UX i przeszkadza modelowi reagować adekwatnie: zamiast zaproponować zmianę zapytania, będzie wpadał w tryb „sorry, usługa jest zepsuta”. To szczególnie boli, gdy mieszacie np. błędy biznesowe i infrastrukturalne z rozdziału 3.

Błąd nr 3: Nieidempotentne operacje w świecie retries.
Projektowanie narzędzia create_order tak, jakby zawsze było wywołane dokładnie raz, to prosta droga do duplikatów zamówień – zwłaszcza gdy użytkownik aktywnie klika Regenerate albo połączenie zrywa się w połowie. Jeśli narzędzie ma efekty uboczne, niemal zawsze warto dodać idempotency_key i przechowywać wyniki, aby powtórne wywołanie nie tworzyło nowych bytów.

Błąd nr 4: Jeden monstrualny „uniwersalny” tool.
Czasem deweloperzy próbują zrobić jeden super‑tool z parametrem action, który potrafi wszystko: szukać, tworzyć, zmieniać i kasować. Dla LLM to niemal gwarancja nieprzewidywalnego zachowania: modelowi trudniej nauczyć się, co kiedy wywołać, a konsekwencje błędów są znacznie cięższe. Lepiej rozbijać na małe, jasno opisane – w miarę możliwości read‑only – narzędzia oraz osobno starannie zaprojektowane narzędzia mutujące z potwierdzeniami.

Błąd nr 5: Wyciek szczegółów wewnętrznych do odpowiedzi.
Wrzucanie do modelu i UI surowych stack trace’ów albo pełnych tekstów wyjątków to typowe inżynierskie lenistwo. Jest to niewygodne dla użytkownika, może ujawniać wewnętrzną strukturę systemu i nie pomaga modelowi się poprawić. Należy przechwytywać wyjątki, mapować je na zwarte kody i proste komunikaty, a wszystkie szczegóły zostawiać w logach i systemie monitoringu.

Błąd nr 6: Brak powiązania błędów z UX widżetu.
Często backend grzecznie zwraca kody błędów, a widżet w UI po prostu wpada w wieczny spinner albo pusty blok. Użytkownik widzi „nic się nie stało”, model widzi, że tool‑call się zakończył, i kontynuuje dialog jak gdyby nigdy nic. Dużo lepiej przemyśleć osobne stany error i empty, pokazywać człowiekowi zrozumiałe komunikaty i – jeśli to możliwe – podpowiadać możliwe działania (zmienić parametry, spróbować później).

Błąd nr 7: Ignorowanie zasady najmniejszych uprawnień.
Nawet jeśli zrobiliście idempotencję i dobrą obsługę błędów, ale jednocześnie opisaliście narzędzie w rodzaju execute_sql_anywhere, które potrafi wszystko, ryzyko pozostaje ogromne. LLM może wywołać je w niewłaściwym kontekście albo z błędnymi parametrami. Każde narzędzie powinno być możliwie wąskie i robić dokładnie jedno, zrozumiałe działanie – szczególnie gdy mowa o pieniądzach lub danych osobowych użytkownika.

1
Ankieta/quiz
Narzędzia App i <code><span class="text-user">callTool</span></code>, poziom 4, lekcja 4
Niedostępny
Narzędzia App i callTool
Narzędzia App i callTool: połączenie UI ↔ backend
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION