JavaRush /Курси /ChatGPT Apps /Відмовостійкість: відкочування кроків, повтори, контроль ...

Відмовостійкість: відкочування кроків, повтори, контроль помилок

ChatGPT Apps
Рівень 11 , Лекція 3
Відкрита

1. Чому в ChatGPT App помилки — норма, а не надзвичайна подія

У минулій лекції ми говорили про те, як ділити завдання на кроки й будувати багатоетапний воркфлоу в ChatGPT App. Тепер додамо до цієї схеми чесну реальність: помилки, тайм-аути та переривання з боку користувача.

У класичному вебі логіку часто вибудовують навколо «щасливого шляху», а помилки сприймають як щось рідкісне й аварійне — на кшталт червоної сторінки 500. У ChatGPT App ситуація інша: ви працюєте в розподіленій системі з LLM, зовнішніми API, MCP, віджетом та ще й із користувачем, який може закрити вкладку будь-якої миті. Помилки й переривання тут — щоденна рутина.

Є кілька особливостей, які ускладнюють життя:

  • По‑перше, LLM — недетермінована. Навіть за однакового промпту вона може ухвалити трохи інше рішення: викликати інший інструмент, змінити параметри або взагалі вирішити, що краще «перепитати».
  • По‑друге, мережеві та інфраструктурні обмеження. Tool‑call від ChatGPT має тайм-аути (часто — десятки секунд), як і ваш бекенд на Next.js/Vercel. Якщо зовнішнє API гальмує, усе може обірватися посередині.
  • По‑третє, є UX‑фактор: користувач відволікся, закрив чат, повернувся за день, а ви ніяк не можете тримати відкритою транзакцію в базі весь цей час.

Звідси головна теза лекції:

Відмовостійкий воркфлоу = сценарій, у якому ми заздалегідь припускаємо, що будь-який крок може впасти, і явно визначаємо, що тоді відбувається.

Помилка — це не лише привід показати користувачеві повідомлення, а й сигнал для моделі. Вона може змінити стратегію, запропонувати відкочування, спробувати інший інструмент або акуратно завершити сценарій.

2. Ландшафт помилок у воркфлоу: які вони бувають

Щоб грамотно обробляти збої, спершу потрібно навчитися їх розрізняти. У LLM‑застосунку на базі ChatGPT Apps зазвичай трапляється кілька класів помилок.

Технічні помилки. Це вся класика розподілених систем: мережеві тайм‑аути, 5xx від ваших або зовнішніх API, падіння MCP‑сервера, баг у коді обробника інструмента. Наприклад, у GiftGenius ваш MCP‑tool search_products звертається до каталогу, а той відповідає 503 Service Unavailable. Це добрий кандидат на автоматичний повтор запиту (retry).

Логічні (модельні) помилки. Сюди входять відмови моделі (вона вирішила, що запит порушує політику), галюцинації або, наприклад, некоректний JSON у відповіді інструмента. Модель могла згенерувати некоректні аргументи для tool‑call, і ваша JSON‑валідація не пропустила їх. Найчастіше це помилка вхідних даних, а не інфраструктури.

Бізнес‑помилки. Вони про зміст: товар закінчився, бюджет користувача надто малий для вибраних фільтрів, промокод недійсний, бронювання вже минуло. У GiftGenius це ситуація «із 500 кандидатів жоден не підходить під задані обмеження». Тут retry рідко допомагає: потрібно або змінювати параметри, або пояснити користувачеві, що обмеження нереалістичні.

UX‑переривання. Користувач сам перериває сценарій: закриває ChatGPT, натискає «Назад» у віджеті, скасовує дію, змінює відповідь на попередньому кроці. Це теж варто вважати нормальним потоком, а не помилкою. Важливо вміти відновлювати стан і відкочуватися в таких випадках — про це ми поговоримо трохи пізніше.

Окремий проблемний випадок на стику логічних і технічних помилок — нескінченні цикли агента: модель отримує помилку, думає «гм, спробую ще раз», знову помилка — і так, доки не закінчиться контекст або бюджет. Захиститися від такої поведінки — важлива частина дизайну помилок.

3. Базові стратегії: retry, fail‑fast, rollback, залучення користувача

Будь-яку помилку можна розглядати як точку розгалуження: ми або намагаємося повторити крок, або відкочуємося, або залучаємо користувача. І що важливо — ці стратегії можна комбінувати.

Для технічних і тимчасових збоїв (мережа моргнула, API повернуло 503) логічно робити обмежений retry з backoff. Для логічних і бізнес‑помилок («валідатор не прийняв бюджет», «товари закінчилися») повторювати безглуздо: потрібно fail‑fast і попросити користувача змінити введення або параметри.

Для операцій, які вже щось змінили у зовнішньому світі (створили замовлення, зробили бронювання), потрібен rollback — або як «крок назад» у UI/контексті, або як реальні компенсувальні дії (скасування замовлення, повернення коштів).

Нарешті, є рішення, які завідомо потребують участі користувача. Наприклад, у разі відмови платіжної системи з причини «картку відхилено банком» ви не можете автоматично все це виправити. Модель має коректно пояснити, що сталося, і запропонувати варіанти: спробувати іншу картку, знизити суму або відмовитися від покупки.

Для надійного воркфлоу дуже корисно прямо для кожного кроку прописати: які типи помилок тут можливі й що ви робите в кожному з них — auto‑retry, відкочування, запит до користувача або просто логування й завершення гілки.

4. Повтори (retry) і backoff: коли й як їх робити

Почнемо з найприроднішої реакції розробника: «Ну, давайте просто спробуємо ще раз». Ідея правильна, але, як завжди, диявол у деталях.

Які помилки можна повторювати

Добра евристика з практики інтеграцій звучить так: мережеві помилки та 5xx можна спробувати ще раз із паузою, а 4xx — найімовірніше, ні.

Тобто, якщо ви отримали 503, 504 або просто не дочекалися відповіді від зовнішнього API, повторити запит із невеликою затримкою має сенс. Якщо ж сервер повернув 400 Bad Request або 422 Unprocessable Entity, найімовірніше, проблема в даних. Повтор із тими самими параметрами нічого не змінить.

Проста утиліта callWithRetry на TypeScript

Напишімо невелику утиліту для MCP‑ або бекенд‑шару, яку можна буде використовувати в інструментах:

type RetryOptions = {
  maxRetries: number;
  baseDelayMs: number;
};

async function callWithRetry<T>(
  fn: () => Promise<T>,
  { maxRetries, baseDelayMs }: RetryOptions
): Promise<T> {
  let attempt = 0;

  // нескінченні цикли нам не потрібні
  while (true) {
    try {
      return await fn();
    } catch (err: any) {
      attempt++;
      const status = err?.status ?? err?.response?.status;

      // 4xx не повторюємо
      const isClientError = typeof status === "number" && status >= 400 && status < 500;
      if (attempt > maxRetries || isClientError) {
        throw err;
      }

      const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), 10_000);
      // невелика пауза, щоб не навалитися на API гуртом
      const jitter = Math.random() * 200;

      await new Promise((r) => setTimeout(r, delay + jitter));
    }
  }
}

Ця функція:

  • повторює виклик fn обмежену кількість разів;
  • використовує експоненційний backoff із невеликим випадковим шумом (jitter), щоб уникнути ефекту «стада» під час одночасних повторів;
  • припиняє повтори (retry) на 4xx.

Її добре використовувати, наприклад, усередині MCP‑інструмента, який ходить до каталогу товарів або до внутрішнього API рекомендацій.

Де саме робити retry

Поширена помилка — намагатися повторювати всі запити підряд, зокрема й на рівнях, які ви не контролюєте. В екосистемі ChatGPT у вас є кілька місць для повторів:

  • усередині власного бекенду/MCP (як ми зробили в callWithRetry);
  • усередині фонового воркера/черги (у майбутніх модулях ми докладніше обговоримо черги завдань і DLQ);
  • іноді — у самому віджеті, коли йдеться про легкий запит «оновити список» без побічних ефектів.

Важливо не дублювати логіку: якщо ваш job‑воркер уже робить три повтори з backoff, немає сенсу зверху навішувати ще пʼять повторів у віджеті. І, звісно, ніколи не робіть while(true) { try ... } — це вірний спосіб улаштувати DDoS самому собі.

5. Ідемпотентність кроків: захист від дублів

Повтори створюють другу проблему: як не виконати одну й ту саму дію двічі. У світі LLM це особливо гостро: модель може випадково викликати один і той самий інструмент кілька разів, ChatGPT може повторити tool‑call після тайм‑ауту, користувач може натиснути «Regenerate», а потім UI або агент можуть по‑своєму додати ще один виклик.

Ідея ідемпотентності проста: крок вважається ідемпотентним, якщо його повторне виконання з тими самими вхідними даними не створює додаткових побічних ефектів. Запросити product feed — ок, перерахувати рекомендації — ок, а от повторно списати гроші або створити друге замовлення за тими самими даними — зовсім не ок.

Idempotency key у ChatGPT App

Класичний патерн: для кожного логічного кроку з побічними ефектами ви генеруєте idempotency_key (зазвичай UUID), передаєте його через модель до MCP‑інструмента і там зберігаєте відповідність «ключ → результат». Якщо інструмент викликали вдруге з тим самим ключем, він не повторює дію, а просто повертає вже збережений результат.

У нашому GiftGenius є крок create_order. Уявіть, що користувач натиснув кнопку «Оплатити», модель викликала інструмент, платіж пройшов, але десь дорогою відповідь загубилася. Модель або платформа вирішують повторити виклик — і якщо у нас немає ідемпотентності, ми отримаємо дубль замовлення або подвійне списання.

Простий приклад ідемпотентного інструмента на TypeScript

Зробімо дуже спрощений handler MCP‑інструмента create_order з idempotency‑ключем. Для простоти використаємо in‑memory Map; у реальному житті це буде БД або кеш.

type CreateOrderInput = {
  userId: string;
  items: Array<{ sku: string; qty: number }>;
  idempotencyKey: string;
};

type CreateOrderResult = { orderId: string; status: "created" };

const idempotencyStore = new Map<
  string,
  { paramsHash: string; result: CreateOrderResult }
>();

export async function createOrderTool(input: CreateOrderInput): Promise<CreateOrderResult> {
  const { idempotencyKey, ...rest } = input;
  const paramsHash = JSON.stringify(rest);

  const existing = idempotencyStore.get(idempotencyKey);
  if (existing) {
    // якщо ключ уже був, переконуємося, що параметри збігаються
    if (existing.paramsHash !== paramsHash) {
      throw new Error("Idempotency key reuse with different params");
    }
    return existing.result;
  }

  // тут ми виконуємо реальне створення замовлення та оплату
  const result: CreateOrderResult = {
    orderId: "order_" + Math.random().toString(36).slice(2),
    status: "created",
  };

  idempotencyStore.set(idempotencyKey, { paramsHash, result });
  return result;
}

Тут ми:

  • вимагаємо idempotencyKey у вхідних даних інструмента;
  • зберігаємо разом із ним хеш параметрів (тут для простоти JSON.stringify);
  • у разі повторного виклику з тим самим ключем, але іншими даними — вважаємо це помилкою;
  • у разі повторного виклику з тими самими даними — просто повертаємо попередній результат.

У реальному проєкті варто:

  • зберігати ключі в БД з TTL (щоб таблиця не розрослася до небес);
  • логувати idempotency_key і додавати його в _meta MCP‑повідомлень, щоб зручно відстежувати через Inspector і дашборди.

6. Відкочування кроків і патерн Saga

Ідемпотентність захищає від дублів, але не розвʼязує інший випадок: що робити, якщо один із кроків посеред сценарію впав.

У e-commerce це класична проблема: ви вже створили замовлення і забронювали товар на складі, а на етапі оплати щось пішло не так. Ви не можете просто «забути про це» — потрібно якось відкочувати попередній стан.

Логічне й технічне відкочування

У ChatGPT‑воркфлоу є два рівні відкочування.

Логічне відкочування — це повернення до попереднього кроку сценарію й коригування контексту. Наприклад, на кроці «оплата» сталася помилка, і ви вирішуєте відкочуватися до кроку «вибір способу оплати» або навіть «вибір подарунка». Тоді важливо:

  • оновити WorkflowContext на бекенді (поточний крок, вибрані параметри);
  • повідомити модель про зміну кроку через tool‑call/ToolOutput, щоб вона «забула» стару гілку й підлаштувала подальшу поведінку;
  • оновити UI віджета, щоб кроки й кнопки відповідали новому стану.

Технічне відкочування — це вже бізнес‑рівень: скасування створених сутностей, компенсація зовнішніх ефектів. Наприклад: скасувати замовлення, зняти резерв зі складу, ініціювати повернення оплати. Це і є патерн Saga: для кожного «небезпечного» кроку ви заздалегідь продумуєте компенсувальну дію.

Схема forward/compensate для GiftGenius

Для спрощеного GiftGenius‑checkout можна уявити таку послідовність:

flowchart TD
  A[Крок 1: create_order] --> B[Крок 2: reserve_items]
  B --> C[Крок 3: charge_card]

  C -->|успіх| D[Статус: completed]

  C -->|помилка| E[Компенсація: cancel_reservation]
  E --> F[Компенсація: cancel_order]
  F --> G[Статус: failed + повідомлення користувачу]

Кожній дії, яка змінює зовнішній світ (створення замовлення, резервування, оплата), відповідає компенсувальна дія (скасування замовлення, зняття резерву, повернення коштів). Вони не завжди симетричні й не завжди можливі «один до одного», але загальний принцип саме такий.

Міні‑приклад із компенсацією в коді

Подивімося на невеликий фрагмент коду, який виконує ці кроки:

async function completeCheckout(ctx: { userId: string }) {
  const order = await createOrderInDb(ctx.userId);

  try {
    await reserveItems(order.id);
    await chargeCard(order.id);
    return { orderId: order.id, status: "paid" as const };
  } catch (err) {
    // компенсаційні дії
    await safeCancelReservation(order.id);
    await safeCancelOrder(order.id);
    throw err;
  }
}

Тут:

  • createOrderInDb, reserveItems, chargeCard — forward‑кроки;
  • safeCancelReservation і safeCancelOrder — компенсувальні кроки, які самі по собі мають бути ідемпотентними (якщо ми спробуємо скасувати вже скасоване, нічого страшного не станеться).

Зверніть увагу: у разі помилки ми не приховуємо її, а прокидаємо далі. Модель (через ToolOutput) має отримати зрозуміле повідомлення про помилку й уже в людському вигляді пояснити це користувачеві та запропонувати наступний крок.

7. Відкочування кроків і синхронізація стану: як не допустити розсинхрону

Є особливий різновид «помилки», який легко недооцінити: розсинхрон стану між UI, бекендом і моделлю.

Типовий сценарій:

  1. Користувач проходить кроки 1 → 2 → 3.
  2. На кроці 3 щось іде не так, користувач натискає кнопку «Назад» у віджеті.
  3. Віджет чесно повертає свій локальний стан на крок 2.
  4. Але модель «памʼятає», що ми були на кроці 3 і вже намагалися оплатити. У наступному повідомленні вона продовжує говорити про оплату, хоча користувач бачить екран вибору подарунка.

Щоб такого не відбувалося, корисно вводити явну подію відкочування кроку. Її надсилає віджет у MCP/модель — або як виклик інструмента, або як ToolOutput.

Наприклад, можна зробити простий інструмент user_navigated_to_step, який фіксує поточний крок і його стан:

type NavigateInput = {
  workflowId: string;
  stepId: string;
};

export async function userNavigatedToStep(input: NavigateInput) {
  await workflowRepo.setCurrentStep(input.workflowId, input.stepId);
  return {
    message: `User moved to step ${input.stepId}`,
  };
}

Віджет під час натискання «Назад» викликає цей tool; модель бачить його результат в історії tool‑callʼів і розуміє, що тепер потрібно продовжувати діалог, виходячи з нового кроку.

На боці UI це приблизно такий обробник:

async function handleBackClick() {
  const { workflowId, prevStepId } = widgetState;

  await window.openai.tools.call("user_navigated_to_step", {
    workflowId,
    stepId: prevStepId,
  });

  setWidgetState((s) => ({ ...s, currentStepId: prevStepId }));
}

Важливий момент: саме бекенд/агент — джерело істини щодо поточного кроку, а модель бачить його через tools. Тоді навіть під час відновлення сесії пізніше ви можете правильно синхронізувати контекст.

8. UX помилок: що бачить користувач і що бачить модель

Ми вже навчилися технічно переживати помилки (повтори, відкочування, ідемпотентність, синхронізацію стану). Залишилося зробити так, щоб усе це виглядало адекватно і для користувача, і для моделі.

Навіть ідеально реалізовані retry та rollback не врятують, якщо UX помилок буде «як у старих Java‑сервлетах»: червоний текст, stack trace і загадкове «Unexpected error».

Для ChatGPT App є дві аудиторії повідомлення про помилку:

  • користувач, який має зрозуміти, що сталося і що він може зробити далі;
  • модель, яка має отримати достатньо структуровану інформацію, щоб ухвалити рішення: повторювати, змінювати параметри, пропонувати альтернативу або завершувати сценарій.

Хороша практика:

  • на рівні MCP/інструментів повертати структуровану помилку з кодом, типом, прапорцем retryable і коротким технічним текстом;
  • давати моделі саме цю структуру (наприклад, у result.structuredContent), а не кілометровий stack trace;
  • у UI показувати користувачеві людське, коротке повідомлення.

Міні‑приклад структури помилки, яку повертає інструмент:

type ToolError = {
  code: string;          // e.g. "PAYMENT_TIMEOUT"
  message: string;       // короткий технічний опис
  retryable: boolean;    // чи можна намагатися ще раз
};

throw {
  isError: true,
  error: <ToolError>{
    code: "PAYMENT_TIMEOUT",
    message: "Payment provider did not respond in time",
    retryable: true,
  },
};

Модель бачить retryable: true і може спробувати інший інструмент або запропонувати користувачеві повторити спробу.

На боці віджета ви просто зіставляєте ці коди зі зрозумілими користувачу текстами:

function ErrorBanner({ code }: { code: string }) {
  const text =
    code === "PAYMENT_TIMEOUT"
      ? "Платіжний сервіс не відповів вчасно. Спробуйте ще раз за хвилину."
      : "Щось пішло не так. Будь ласка, спробуйте ще раз.";

  return <div className="error-banner">{text}</div>;
}

І ще один важливий момент: не показуйте користувачеві стеки винятків, токени й секрети. Це і некрасиво, і небезпечно. Технічну інформацію логуйте у себе, а користувачеві давайте коротке, безпечне повідомлення.

Спостереження

У LLM‑системах на кшталт ChatGPT некоректні виклики інструментів — радше норма, ніж виняток. Модель регулярно генерує аргументи, які не проходять валідацію: переплутані типи, відсутні поля, неправильні значення, поламані структури. Це не помилка у звичному інженерному сенсі — це частина природи стохастичної моделі, і під неї потрібно адаптувати весь інтерфейс помилок.

Ключова ідея: повідомлення про помилку — це не сигнал «зламалося», а інструкція для того, щоб виправити наступну спробу. Його головна аудиторія — сама модель. Якщо повідомлення структуроване й містить точні вказівки, модель здатна автоматично скоригувати параметри та повторити виклик уже правильно. Саме на цьому принципі засновані техніки Tool‑Reflection: коректний зворотний звʼязок покращує наступну дію агента без участі людини.

Рекомендую дотримуватися таких вимог до формату помилок:

  • повідомлення має вказувати конкретне поле, яке не пройшло валідацію, — без узагальнень рівня «Invalid parameters»;
  • важливо явно описувати очікуваний формат або допустимі значення, щоб модель могла вибрати відповідні;
  • повідомлення має бути коротким, формальним і структурованим: поля на кшталт error_type, field, expected або allowed_values суттєво допомагають моделі;
  • за можливості варто дати мінімальний приклад коректного введення — це часто підвищує точність відновлення моделі.

Ідеальний error feedback для моделі містить два факти: що пішло не так, і інструкцію, як це виправити.

9. Логування й метрики помилок воркфлоу

Навіть якщо UX помилок акуратний, щоб зрозуміти, що у вас насправді ламається, одних повідомлень для користувача замало. Потрібні структуровані логи і метрики за кроками.

Мінімально корисний набір під час логування кожного кроку воркфлоу:

  • user_id або хоча б session_id;
  • workflow_id і step_id;
  • статус кроку (success, failed, retry, rolled_back);
  • error_code (якщо був);
  • idempotency_key і correlation_id, якщо крок повʼязаний із зовнішніми викликами.

У MCP і Agents є _meta‑поля; туди зручно класти idempotency_key і correlation_id, щоб їх було видно і в логах, і в Inspector.

Найпростіший приклад логування на Node.js/TypeScript (можна використовувати console, можна — winston/pino):

function logStepFailure(params: {
  userId?: string;
  workflowId: string;
  stepId: string;
  errorCode: string;
  idempotencyKey?: string;
}) {
  console.error(
    JSON.stringify({
      level: "error",
      event: "workflow_step_failed",
      ...params,
      timestamp: new Date().toISOString(),
    })
  );
}

Такі логи легко обробляти, будувати за ними дашборди й рахувати:

  • конверсію між кроками;
  • найчастіші типи помилок;
  • частку кроків, що завершилися retry проти остаточного провалу.

Не кожна помилка має ставати алертом у продакшені. Критичні — падіння MCP, систематичні тайм‑аути, масові провали на певному кроці — так, потрібно виносити в моніторинг. А от «немає результатів за пошуком подарунків» — це бізнес‑подія, а не інцидент.

10. Розвиваємо GiftGenius: стійкий checkout‑крок

Тепер зберімо все разом: повтори, ідемпотентність, Saga, синхронізацію стану, UX помилок і логування — на прикладі одного кроку в нашому навчальному застосунку GiftGenius, а саме оформлення замовлення.

Що вже є

На цей момент у нас уже:

  • є багатоетапний воркфлоу: збирання інформації → підбір ідей → вибір подарунка → checkout;
  • налаштований tool gating: на кроці checkout доступний лише набір commerce‑інструментів (create_order, get_payment_methods тощо);
  • є WorkflowContext, у якому зберігаються вибраний подарунок, бюджет, userId і поточний крок.

Що додамо на цій лекції

Для кроку checkout запровадимо:

  1. idempotency_key для інструмента create_order;
  2. retry за тимчасових помилок платіжного провайдера;
  3. компенсацію у разі частково успішних операцій;
  4. коректний UX помилок у віджеті.

Генерація idempotency‑ключа у віджеті під час натискання кнопки «Оплатити»:

import { v4 as uuid } from "uuid";

async function handlePayClick() {
  const idempotencyKey = uuid();
  setWidgetState((s) => ({ ...s, idempotencyKey }));

  await window.openai.tools.call("create_order", {
    userId: widgetState.userId,
    items: [/* ... */],
    idempotencyKey,
  });
}

На боці інструмента create_order — той самий ідемпотентний handler, який ми писали вище: він зберігає ключ і результат, а під час повтору не створює нове замовлення.

Код взаємодії з платіжним API можна обгорнути в callWithRetry, щоб кілька разів спробувати списати гроші за мережевих збоїв. І не забудьте додати прапорець retryable: true у помилку, щоб модель розуміла, що можна запропонувати повтор.

Якщо після успішного створення замовлення та списання грошей щось ламається (наприклад, зовнішній webhook не приходить вчасно), ми логуватимемо це з correlation_id і workflow_id, а далі:

  • пробуємо фоновий retry (у майбутньому модулі про черги та події);
  • або явно позначаємо крок як failed, викликаємо компенсувальні дії та пояснюємо користувачеві, що сталося.

11. Типові помилки під час проєктування відмовостійких воркфлоу

Помилка № 1: «Повторюємо все, доки не спрацює».
Автоматично повторювати будь-який крок до «переможного» — надійний спосіб улаштувати собі локальне пекло. Мережеві та 5xx‑помилки можна спробувати ще раз із backoff і лімітом спроб. Але 4xx, бізнес‑помилки й логічні провали моделі потрібно або виправляти даними, або пояснювати користувачеві. Інакше ви отримаєте нестабільну поведінку, дивні рахунки й засмічені логи.

Помилка № 2: Відсутність ідемпотентності там, де є гроші й замовлення.
Якщо інструмент на кшталт create_order або charge_card не є ідемпотентним, будь-який повторний виклик (через тайм‑аут, Regenerate, помилку в агенті) може призвести до дублів. У LLM‑сценаріях повтори трапляються помітно частіше, ніж у класичному REST‑фронтенді, тому idempotency_key — не «приємний бонус», а обовʼязкова умова для платіжних та інших критичних кроків.

Помилка № 3: Немає компенсувальних дій (Saga відсутня).
Створили замовлення, забронювали товар, а на оплаті впали — і просто показали користувачеві «щось пішло не так». У результаті в системі висять неповні замовлення, резерви, фінансові «хвости». Для кожного кроку, який змінює зовнішній світ, варто продумати, що ви робитимете у разі провалу наступного кроку: скасовувати, повертати, позначати як «expired» тощо.

Помилка № 4: Дозволити агенту піти в нескінченний цикл повторів.
Якщо ви не обмежите кількість спроб (наприклад, через maxRetries у helperʼах або через max_iterations в агентній логіці) і не позначатимете помилки як retryable: false там, де повтори марні, модель може зациклитися: «Спробую ще раз… ще раз…». Це спалює токени, час і нерви.

Помилка № 5: Розсинхрон стану між UI і моделлю під час відкочування.
Часто розробники реалізують кнопку «Назад» лише в UI, забуваючи синхронізувати крок із бекендом і моделлю. У підсумку користувач бачить крок 2, а модель продовжує «жити» на кроці 3 і робити дивні пропозиції. Рішення — явні події на кшталт user_navigated_to_step і оновлення WorkflowContext під час кожного переходу.

Помилка № 6: Технічні повідомлення для користувача та відсутність логів для розробників.
Користувач отримує «Error: ECONNRESET at TcpSocket.onEnd…», а ви — нуль інформації про те, який саме крок і для якого workflow_id зламався. Грамотний підхід: для користувача — короткий, зрозумілий текст і пропозиція, що робити далі; для розробника — структурований лог із workflow_id, step_id, error_code, idempotency_key і correlation_id.

Помилка № 7: Відсутність стратегії щодо алертів.
Або алерти летять на все підряд, включно з «немає відповідних подарунків під ваш дуже вузький фільтр», або не алертить нічого, включно зі справжнім падінням MCP. Намагайтеся відокремлювати критичні системні збої (падіння сервісу, масові тайм‑аути, втрата webhookʼів) від очікуваних бізнес‑подій. Перші йдуть у моніторинг і on‑call, другі — просто рахуються в аналітиці.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ