1. Помилки та ідемпотентність у застосунках ChatGPT
У класичному вебі багато хто досі мислить у парадигмі «користувач натиснув кнопку → один HTTP-запит → одна відповідь». У світі LLM це давно не так. Модель може вирішити викликати ваш інструмент кілька разів.
Вона може згенерувати відповідь повторно після натискання користувачем Regenerate, може перепитати або натрапити на мережеву помилку в дорозі. У підсумку той самий інструмент цілком може бути викликаний двічі чи тричі з дуже схожими аргументами.
Водночас у будь-якої помилки раптом зʼявляються два «споживачі». З одного боку — модель, якій потрібне зрозуміле машиночитане пояснення: що пішло не так і як виправити аргументи, щоб повторити спробу. З іншого — користувацький UI (віджет і сам чат), де важливо показати зрозуміле повідомлення й запропонувати наступний крок, а не «Error: 500 (see logs)».
Є ще один важливий момент. Класична архітектура рідко враховує, що хтось масово натискатиме «повторити відповідь» і таким чином різко збільшуватиме кількість повторних запитів. У ChatGPT цей сценарій — норма. До того ж платформа може повторно викликати інструмент через тимчасові мережеві проблеми.
Тому ідемпотентність у цій екосистемі — не додаткова опція, а базова вимога. Особливо це важливо для інструментів, які роблять щось «по-справжньому»: створюють замовлення, списують гроші, надсилають листи тощо.
Ця лекція — про те, як не дозволити одному невдалому виклику інструмента (tool call) зіпсувати користувачеві настрій, а вам — бойове середовище.
Insight
ChatGPT не передає аргументи у ваші функції напряму — радше підбирає їх під вашу схему. Вона дивиться на JSON Schema, контекст діалогу й статистично добирає значення — і доволі часто помиляється.
Ситуації на кшталт «не той тип», «забув обовʼязкове поле», «суперечливі параметри» — звична частина життя tool calls, а не форс-мажор. За відкритими даними та телеметрією такі промахи для складних схем легко становлять до ~30 % викликів.
Для моделі це не проблема. Вона сприймає вашу відповідь як сигнал «аргументи були поганими» і просто пробує ще раз — інколи два-три рази поспіль, трохи змінюючи вхід. Для вас це означає інше: кожен інструмент потрібно проєктувати так, ніби його майже гарантовано викличуть кілька разів із дуже схожими параметрами.
Саме тому ідемпотентність така важлива. ChatGPT знову й знову намагатиметься «вгадати», з якими параметрами треба викликати ваші функції. Дві-три спроби на один виклик — це норма.
2. Безпечне налаштування віджета: text/html+skybridge і _meta
Перш ніж перейти до суто серверних питань (помилки, повторні спроби, ідемпотентність), закриємо один специфічний для Apps SDK момент, повʼязаний із безпекою UI. Поговоримо про те, як зробити так, щоб ваш віджет рендерився в чаті безпечно, а не як «жахлива сторінка з інтернету».
registerResource і MIME-тип text/html+skybridge
Ваш віджет із погляду ChatGPT — це спеціальний HTML-ресурс, який потрапляє до пісочниці клієнта ChatGPT, а не безпосередньо в браузер користувача. Щоб платформа зрозуміла, що це саме віджет, а не «просто HTML», використовується MIME-тип text/html+skybridge.
На рівні MCP-сервера ви реєструєте ресурс за допомогою чогось на кшталт (псевдо‑TS):
// десь у конфігурації MCP-сервера
registerResource({
name: "giftgenius-widget",
path: "/widget",
mimeType: "text/html+skybridge", // важливо!
});
Цей mimeType — сигнал клієнту ChatGPT: «це не просто HTML, а компонентний шаблон для вбудованого віджета, який треба запустити в ізольованому середовищі». Якщо вказати звичайний text/html, платформа може показати сирий HTML або взагалі відмовити в рендері.
_meta і керування безпекою: CSP, домен і рамка
Далі в хід ідуть метадані, які передаються разом із відповіддю інструмента або ресурсу, — _meta. Через них ви керуєте тим, які зовнішні ресурси віджет може завантажувати, як він поводиться візуально, і навіть тим, як модель його описуватиме.
Типовий приклад структури:
const toolResult = {
content: "<!-- HTML віджета -->",
_meta: {
"openai/widgetCSP": "default-src 'self'; img-src https://cdn.example.com",
"openai/widgetDomain": "https://chatgpt.com",
"openai/widgetPrefersBorder": true,
"openai/widgetDescription": "GiftGenius показує рекомендації подарунків у вигляді карток."
}
};
Розберімо ключові поля.
- openai/widgetCSP задає Content Security Policy для віджета. Це ваш невеликий брандмауер для браузера всередині ChatGPT: ви явно перелічуєте, звідки можна завантажувати скрипти, стилі, зображення, робити XHR тощо. Платформа очікує суворої політики без wildcard *, тож вам потрібно явно вказати домени, які ви використовуєте (чат, свій API, CDN).
- openai/widgetDomain задає origin, у контексті якого працюватиме ваш віджет. Зазвичай це домен ChatGPT: ви не підміняєте його на свій сайт, а лише повідомляєте, як це має виглядати в ізольованому середовищі.
- openai/widgetPrefersBorder — суто візуальний прапорець: малювати чи ні рамку навколо віджета. Для GiftGenius цілком логічно залишити рамку, щоб візуально відділяти блок рекомендацій від звичайних повідомлень у чаті.
- openai/widgetDescription — текстовий опис для моделі. Замість того, щоб намагатися самій «вигадати» пояснення, модель може використати цей рядок, коли розповідає користувачеві, який інтерфейс щойно відкрився. Це знижує ризик дивних або надмірних коментарів моделі.
Практичний висновок простий: якщо один раз акуратно налаштувати mimeType і _meta, ви отримаєте безпечний, ізольований UI, який не лізе, куди не треба, і поводиться передбачувано — і з точки зору користувача, і з точки зору платформи.
Із фронтенд-частиною безпеки розібралися: віджет живе в пісочниці й ходить лише туди, куди ви йому дозволили. Далі зосередимося на серверній стороні — типах помилок і тому, як їх описувати, а також на тому, як робити інструменти ідемпотентними.
Insight: кешування віджета
ChatGPT кешує HTML віджета в момент реєстрації застосунку. HTML‑віджет ChatGPT — це не «живий фронтенд», а зафіксований артефакт збірки. Під час публікації застосунку (Store або Dev Mode) платформа зчитує HTML‑ресурс (text/html+skybridge) і далі завжди використовує саме цю версію. Будь-яка зміна — навіть один рядок тексту або відступ у картці — фактично означає новий реліз.
Звідси висновок: правки HTML‑структури, слотів, data-* атрибутів і контракту structuredContent → DOM — це не «швидкий фікс», а повноцінна фронтенд‑міграція.
Якщо сьогодні ви рендерите список із items[], а завтра переходите на results[], старий віджет про це не дізнається. Він і далі отримуватиме попередній JSON і працюватиме некоректно.
3. Типи помилок у роботі інструментів
Тепер перейдемо до суті: які взагалі бувають помилки в інструментах і чим вони відрізняються з точки зору UX та бекенду. Зручно мислити чотирма шарами помилок.
Помилки валідації входу
Найбазовіший рівень — коли вхідні аргументи взагалі не відповідають контракту.
Приклади для нашого навчального застосунку GiftGenius і його інструмента suggest_gifts (підбір подарунків за інтересами та бюджетом):
- вік менший за нуль або більший за 120;
- бюджет відʼємний;
- обовʼязкове поле relationship_type відсутнє;
- budget_min > budget_max.
Сюди ж потрапляє банальний JSON, який не відповідає схемі. В ідеалі Apps SDK і JSON Schema відфільтрують «зовсім погані» виклики ще до вашого коду, але бізнес‑валідацію (на кшталт співвідношення budget_min/budget_max) усе одно потрібно робити самостійно.
Помилки бізнес‑логіки
Тут вхід начебто коректний, але за доменними правилами ви не можете дати нормальний результат.
Типові сценарії:
- за заданими інтересами та бюджетом ви не знайшли жодного подарунка;
- користувач перевищив денний ліміт підбірок;
- товар, який модель просить купити, більше не продається.
Це не «зламався сервер», а нормальні й очікувані ситуації. Їх треба подати користувачеві та моделі у зручній формі, а не як 500 Internal Server Error.
Помилки зовнішньої інфраструктури
Цей шар уже про «технічне пекло»: недоступна база, зовнішній API відповідає тайм-аутом, усередині вашого коду вилетів необроблений виняток.
Наприклад:
- запит до каталогу подарунків повертає 503 або не відповідає;
- MongoDB раптово «зависла»;
- у коді фільтрації подарунків ви ділите на нуль.
З точки зору UX це часто привід сказати: «Сервіс тимчасово недоступний, спробуйте пізніше». Інколи — запустити приховану повторну спробу. Але важливо не зникати мовчки й не показувати користувачеві сирий стек-трейс.
Помилки платформи/мережі
І нарешті, є шар, який може статися взагалі поза вашим кодом: tool‑call не дійшов, зʼєднання обірвалося посеред відповіді, стримінговий сценарій перервався. Таке трапляється частіше, ніж здається.
Наприклад, якщо використовувати безкоштовний тунель, то в години пікового навантаження його швидкість падає настільки, що ChatGPT tool calls «відвалюються» через тайм-аут.
Повністю контролювати це ви не можете. Але ви точно можете проєктувати інструменти й віджет так, щоб повторні виклики та переривання не перетворювали систему на хаос.
Саме тому ми говоримо про ідемпотентність і акуратне опрацювання помилок, а не просто «try/catch — і забули».
4. Як описувати й повертати помилки: і для моделі, і для UI
Важливий зсув мислення: помилка — це не лише те, що ви залогували в console.error. Це частина контракту інструмента, з якою працюватимуть і модель, і інтерфейс.
Структура помилки
Зазвичай зручно дотримуватися простої структури:
type ToolError = {
code: string; // "VALIDATION_ERROR", "NO_RESULTS", "UPSTREAM_TIMEOUT"
message: string; // людинозрозуміле або компактне для моделі
retryable: boolean; // чи є сенс пробувати ще раз
};
І результат інструмента можна загортати в дискримінований union:
type SuggestGiftsResult =
| { ok: true; gifts: GiftCard[] }
| { ok: false; error: ToolError };
У MCP‑протоколі є ще окремий прапорець «це помилка», але всередині зручно дотримуватися власного формату. Так UI і модель однаково інтерпретуватимуть, що сталося.
Стратегія «fail gracefully»
Не кожну неприємну ситуацію варто оформлювати як «жорстку» помилку. Інколи значно корисніше повернути порожній результат без помилки — просто з поясненням.
Наприклад, якщо подарунків не знайдено, розумно повернути ok: true, порожній масив gifts: [] і якесь поле noResultsReason для UI і моделі, замість "NO_RESULTS" як помилки. Тоді модель може продовжити діалог: «Я нічого не знайшов у цьому бюджеті. Хочете підвищити бюджет або уточнити інтереси?».
А от якщо зовнішній API повністю «ліг», то це радше ok: false з code: "UPSTREAM_UNAVAILABLE" і retryable: true, щоб модель мала шанс спробувати ще раз пізніше або з іншими параметрами.
Нагадаємо: з розділу 3 у нас є чотири шари помилок. Валідаційні помилки зазвичай ідуть як ok: false і retryable: false — моделі не варто повторювати той самий виклик із тими самими аргументами.
Бізнес‑ситуації на кшталт «нічого не знайдено» частіше оформлюються як ok: true із порожнім результатом і поясненням. Інфраструктурні збої зовнішніх сервісів — як ok: false з retryable: true, щоб модель могла безпечно пробувати ще раз.
А помилки платформи/мережі взагалі можуть трапитися до або після вашого коду. На практиці вони часто виглядають як повторний виклик інструмента — саме тому нам так потрібна акуратна ідемпотентність, про яку поговоримо далі.
Не виносимо внутрішні деталі назовні
У серверному коді легко спокуситися й просто прокинути error.toString() у відповідь. Для LLM‑інструментів це не найкраща ідея: ви отримаєте сміття в діалозі та потенційно розкриєте чутливі деталі (URL внутрішніх сервісів, стек-трейси, назви таблиць).
Краще перехоплювати винятки й перетворювати їх на компактні коди помилок та акуратні повідомлення.
Приклад мінімальної обгортки:
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: "Сервіс каталогу тимчасово недоступний",
retryable: true
}
};
}
Модель бачить акуратний сигнал, UI — зрозумілий текст, а подробиці залишаються в логах.
Відображення помилки у віджеті
З точки зору React‑віджета задача проста: перевірити ok, і якщо воно false, показати дружнє повідомлення та, за можливості, запропонувати варіанти продовження.
function GiftResults({ result }: { result: SuggestGiftsResult }) {
if (!result.ok) {
return (
<div>
<p>Не вдалося підібрати подарунки: {result.error.message}</p>
{result.error.retryable && <p>Спробуйте змінити параметри або повторити запит.</p>}
</div>
);
}
if (result.gifts.length === 0) {
return <p>Подарунків за такими умовами не знайдено. Спробуйте змінити бюджет або інтереси.</p>;
}
return <GiftCardsList gifts={result.gifts} />;
}
Саме в таких моментах просте й чесне повідомлення робить UX суттєво приємнішим, ніж «щось пішло не так».
Ми вже домовилися, що частину помилок можна чесно позначати як retryable: true і пропонувати користувачеві «спробувати ще раз».
Щойно в системі зʼявляються такі повторні спроби (явні в UI або приховані на стороні платформи), виникає наступне питання: що буде, якщо той самий інструмент викличуть двічі з тими самими даними? Це вже історія про ідемпотентність.
5. Ідемпотентність: захист від «ще одного такого самого виклику»
Тепер — до найцікавішого. Формально ідемпотентність — це властивість операції, за якої повторний виклик із тими самими вхідними даними не змінює стан системи й результат.
У строгому сенсі це і про відсутність повторних побічних ефектів, і про однакову відповідь. На практиці в ChatGPT Apps нас насамперед цікавить перше: щоб повторні виклики не псували дані та не створювали нові сутності, навіть якщо сама відповідь може трохи відрізнятися.
У контексті ChatGPT Apps ідемпотентність — це захист від усього того «добра», що супроводжує повторні спроби, Regenerate і непередбачувану логіку LLM.
Де ідемпотентність особливо важлива
Інструменти лише для читання зазвичай безпечні: скільки не кличте suggest_gifts з одними й тими самими параметрами, ви просто отримаєте ще один список подарунків. Навіть якщо він трохи відрізняється, це не змінює стан системи й не створює побічних ефектів.
Критичні інструменти — ті, що змінюють стан зовнішніх систем:
- створення замовлення (create_order);
- проведення платежу (charge_card, submit_payment);
- надсилання листів і сповіщень (send_email, send_sms);
- створення сутностей із побічними ефектами (наприклад, бронювань).
Якщо такий інструмент викликається двічі поспіль із практично однаковими аргументами, у вас можуть зʼявитися дублікати замовлень, подвійні списання та інші «радощі» бухгалтерії.
Патерн idempotency_key
Класичний підхід: додати до інструмента додатковий параметр idempotency_key — рядковий ідентифікатор операції. Якщо запит із таким ключем уже успішно оброблено, сервер не виконує дію ще раз, а повертає збережений результат.
Приклад розширеної схеми для гіпотетичного інструмента create_checkout_session у GiftGenius:
const CreateCheckoutSchema = {
type: "object",
properties: {
giftId: {
type: "string",
description: "ID вибраного подарунка"
},
idempotency_key: {
type: "string",
description: "Унікальний ключ операції для захисту від дублікатів"
}
},
required: ["giftId", "idempotency_key"]
} as const;
На сервері обробник робить приблизно таке:
async function createCheckoutSession(input: CreateCheckoutInput) {
const existing = await db.checkoutSessions.findOne({ idempotencyKey: input.idempotency_key });
if (existing) {
return existing; // повертаємо старий результат
}
const session = await paymentProvider.createSession({ giftId: input.giftId });
await db.checkoutSessions.insert({ idempotencyKey: input.idempotency_key, session });
return session;
}
Якщо модель із якоїсь причини викличе інструмент вдруге з тим самим idempotency_key, користувач не отримає другий платіж, а просто побачить ту саму checkout‑сесію.
Розділення prepare і commit
Для особливо чутливих дій (платежі, незворотні зміни) часто застосовують двофазний підхід: окремий інструмент для підготовки (prepare_*) і окремий — для коміту (commit_*).
Наприклад:
- prepare_order — перевіряє наявність товару, рахує вартість, повертає «чернетку замовлення»;
- commit_order — за ID чернетки створює справжнє замовлення й ініціює платіж.
Такий дизайн дає кілька бонусів. По‑перше, перший крок можна зробити повністю ідемпотентним: повторний prepare_order із тими самими параметрами поверне ту саму чернетку. По‑друге, commit_order можна дозволяти викликати лише після явного підтвердження користувача — це зручно і з точки зору UX, і з точки зору безпеки.
6. Безпечний дизайн інструментів
Ідемпотентність — необхідний, але не єдиний інгредієнт безпеки. Дуже багато визначає сам дизайн набору інструментів, який ви віддаєте моделі.
Принцип найменших привілеїв
Ідея проста: кожен інструмент має вміти рівно те, що потрібно для сценарію, — і ні рядком більше. Не потрібно однієї функції do_anything_with_user_account, яка:
- може читати, оновлювати й видаляти все підряд;
- приймає рядок operation і JSON payload «як пощастить».
Краще мати окремі, чітко описані інструменти:
- get_user_profile;
- update_user_preferences;
- create_order;
- cancel_order.
Та сама логіка і для GiftGenius: suggest_gifts лише підбирає варіанти; create_checkout_session не має «знати», як скасовувати замовлення або змінювати e‑mail користувача.
Розділення «read» і «write»-інструментів
Хороший патерн — чітко розділяти інструменти, що лише читають дані, і ті, що щось змінюють. Запит до каталогу подарунків (search_products, suggest_gifts) безпечний сам по собі, навіть якщо модель зловживає ним.
А от create_order або charge_payment уже вимагають обережнішого підходу.
В описах таких інструментів варто прямо зазначати, що вони роблять і в якому контексті їх можна викликати. Наприклад:
{
"name": "create_checkout_session",
"description": "Створює нову сесію оплати для одного подарунка. Викликай ЛИШЕ після того, як користувач явно підтвердив свій вибір.",
"parameters": { /* ... */ }
}
Це не стовідсотковий захист (LLM усе одно може помилитися), але ви принаймні даєте їй чіткий сигнал про ризики.
Human-in-the-loop і підтвердження
Для по‑справжньому «небезпечних» дій зручно будувати сценарій із підтвердженням. Наприклад, модель:
- Спочатку викликає інструмент, який готує дані для купівлі й повертає їх у зручному для UI вигляді (назва подарунка, ціна, адреса доставки).
- Платформа показує користувачеві віджет із кнопкою «Підтвердити покупку».
- Лише після кліку на кнопку викликається інструмент коміту, який і робить реальний платіж.
Так ви не даєте моделі можливості «тихцем» оформити замовлення без участі користувача — навіть якщо вона раптом вирішить, що це дуже розумне рішення.
Семантика ризику в описах і анотаціях
У деяких версіях платформи зʼявляються спеціальні анотації на кшталт destructiveHint, які сигналізують, що інструмент може виконувати незворотні дії. Навіть якщо таких полів немає або вони ще нестабільні, ви можете закласти цю семантику прямо в description і в назвах параметрів.
Наприклад, замість:
{
"name": "delete_user_data",
"description": "Видаляє дані користувача."
}
зробити:
{
"name": "request_user_data_deletion",
"description": "Позначає обліковий запис користувача для видалення його персональних даних відповідно до політики сервісу. Використовуй ЛИШЕ після того, як користувач явно запросив видалення."
}
І заодно побудувати навколо цього людяний підтверджувальний UX.
7. Невелике практичне доопрацювання GiftGenius
Давайте поєднаємо все це з нашим навчальним застосунком GiftGenius — застосунком для підбору подарунків. Припустімо, що ми додаємо до GiftGenius ще один інструмент — create_checkout_session, щоб користувач міг не лише підібрати подарунок, а й перейти до оформлення.
З точки зору JSON Schema і безпеки робимо таке.
По‑перше, додаємо idempotency_key і акуратний опис:
const CreateCheckoutTool = {
name: "create_checkout_session",
description:
"Створює сесію оплати для одного вибраного подарунка. " +
"Викликай лише після того, як користувач підтвердив, що хоче придбати цей подарунок.",
parameters: {
type: "object",
properties: {
gift_id: {
type: "string",
description: "Ідентифікатор подарунка з результату suggest_gifts."
},
idempotency_key: {
type: "string",
description: "Унікальний ключ операції. Використовуй один і той самий ключ під час повторного виклику."
}
},
required: ["gift_id", "idempotency_key"]
}
} as const;
По‑друге, на сервері реалізуємо ідемпотентний обробник:
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 };
}
По‑третє, враховуємо помилки:
try {
return await handleCreateCheckout(input);
} catch (err) {
console.error("create_checkout_session failed", err);
return {
ok: false,
error: {
code: "PAYMENT_PROVIDER_ERROR",
message: "Не вдалося створити сесію оплати. Спробуйте пізніше.",
retryable: true
}
};
}
А у віджеті показуємо зрозумілий стан помилки й, можливо, кнопку «Повторити» на рівні UI, яка ініціює новий діалог із моделлю.
Так, крок за кроком, наш милий навчальний проєкт перестає бути «іграшкою для демо» і поволі перетворюється на щось, що теоретично можна випускати в бойове середовище.
8. Типові помилки під час роботи з помилками та ідемпотентністю інструментів
Помилка №1: Помилка = просто throw і 500.
Якщо за будь-якого збою ваш інструмент просто викидає виняток, який перетворюється на «щось пішло не так», модель і UI залишаються без інформації. Модель не розуміє, чи варто повторювати виклик з іншими аргументами, а користувач не розуміє, що робити далі.
Набагато краще повертати структуровану помилку з кодом, коротким повідомленням і ознакою retryable, а всередині сервера вже логувати деталі.
Помилка №2: Немає різниці між типами помилок.
Змішувати валідаційні, бізнесові та інфраструктурні помилки в один казан — погана ідея. У підсумку ситуація «нічого не знайдено» виглядає для моделі та користувача так само, як «лягла база даних».
Це ламає UX і заважає моделі адекватно реагувати: замість пропозиції змінити запит вона переходитиме в режим «вибачте, сервіс зламаний». Особливо боляче це відчувається, коли ви змішуєте, наприклад, бізнес‑помилки й інфраструктурні помилки з розділу 3.
Помилка №3: Неідемпотентні операції у світі повторних спроб.
Проєктувати інструмент create_order так, наче його завжди викличуть рівно один раз, — прямий шлях до дублікатів замовлень. Особливо це помітно, коли користувач активно тисне Regenerate або коли зʼєднання рветься на півдорозі.
Якщо в інструменті є побічні ефекти, майже завжди варто додати idempotency_key і зберігати результати, щоб повторний виклик не створював нові сутності.
Помилка №4: Один монструозний «універсальний» інструмент.
Іноді розробники намагаються зробити один суперіструмент із параметром action, який уміє все: шукати, створювати, змінювати й видаляти.
Для LLM це майже гарантований спосіб зробити поведінку непередбачуваною: моделі складніше «вивчити», що і коли викликати, а наслідки помилок стають значно тяжчими. Правильніше ділити на маленькі, чітко описані, за можливості read‑only інструменти, і окремо — акуратно оформлені mutating‑інструменти з підтвердженнями.
Помилка №5: Витік внутрішніх деталей у відповіді.
Викидати в модель і UI raw stack trace або повні тексти винятків — типова інженерна лінь. Це незручно користувачеві, може розкривати внутрішню структуру системи й не допомагає моделі виправитися.
Варто перехоплювати винятки, мапити їх у компактні коди та прості повідомлення, а всі деталі залишати в логах і системі моніторингу.
Помилка №6: Немає звʼязки помилок із UX віджета.
Часто серверна частина акуратно повертає коди помилок, а віджет з UI просто падає в нескінченний spinner або показує порожній блок. Користувач бачить «нічого не сталося», модель бачить, що tool‑call завершився, і продовжує діалог так, ніби нічого не сталося.
Значно краще продумати окремі стани error і empty, показувати людині зрозумілі повідомлення та, за можливості, підказувати варіанти дій (змінити параметри, спробувати пізніше).
Помилка №7: Ігнорування принципу найменших привілеїв.
Навіть якщо ви зробили ідемпотентність і якісну обробку помилок, але при цьому описали інструмент на кшталт execute_sql_anywhere, який може робити все, ризик залишається величезним.
LLM може викликати його в неправильному контексті або з помилковими параметрами. Кожен інструмент має бути максимально вузьким і робити рівно одну зрозумілу дію — особливо коли йдеться про гроші чи персональні дані користувача.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ