JavaRush /Курси /ChatGPT Apps /Динамічне керування списком інструментів (tool gating)

Динамічне керування списком інструментів (tool gating)

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

1. Що таке tool gating і чому це взагалі тема для окремої лекції

Досі в наших спрощених прикладах ми діяли так: описували набір інструментів для застосунка (App), підʼєднували MCP‑сервер — і на цьому зупинялися. Увесь набір завжди був доступний моделі. Для завдання «зробити демо за 5 хвилин» це працює. А от для реального продукту — не дуже.

Tool gating — це патерн, за якого список інструментів, доступних моделі, не фіксований, а залежить від контексту: кроку workflow, прав користувача, стану даних тощо.

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

Найпростіша аналогія: ви ж не даєте стажеру в банку доступ одразу до всіх систем. Спочатку — лише перегляд, потім прості операції, далі — складніші. Тут та сама логіка, тільки стажер — це LLM.

2. Проблема «всі інструменти одразу»: забруднення контексту й безпека

Якщо дати моделі десятки інструментів, вона починає «просідати» одразу в кількох напрямах: контекст перевантажується, зростає плутанина у виборі, зʼявляються питання безпеки. Дослідження OpenAI/Anthropic показують: що більше функцій ви описуєте в контексті, то гірше модель влучає в потрібну.

По‑перше, кожне визначення інструмента — це токени: назва, опис, JSON Schema. Список із 30–40 tools легко «зʼїдає» кілька тисяч токенів. А це ті самі токени, які ви могли б витратити на історію діалогу, користувацький контекст і приклади хороших відповідей. Натомість модель читає «роман» про ваші API.

По‑друге, коли інструменти схожі між собою, модель починає плутатися. Якщо у вас є search_products і get_product_details, вона може спробувати викликати get_product_details просто з текстовим запитом — бо опис здасться більш доречним.

Окремо постає питання безпеки. Є нудний, але важливий принцип мінімальних прав (least privilege): система має мати лише ті можливості, які справді потрібні їй «тут і зараз». Якщо на кроці знайомства модель уже знає про checkout, досить невеликої prompt‑інʼєкції від користувача — і вона спробує передчасно викликати оплату. Tool gating — зручний окремий випадок мінімізації прав: на кожному кроці ми вмикаємо лише те, що потрібно.

І нарешті, UX. Якщо модель раптово робить щось «магічне», чого користувач не очікував (наприклад, створює замовлення, хоча людина ще обирає подарунок), довіра до вашого застосунка стрімко падає.

3. GiftGenius як ілюстрація tool gating

Візьмімо наш кейс GiftGenius і чесно подивімося на кроки:

  1. Інтервʼю: зʼясовуємо вік, стать, інтереси отримувача, бюджет тощо.
  2. Підбір: шукаємо товари в каталозі, показуємо ідеї.
  3. Checkout: коли користувач уже обрав подарунок, переходимо до оформлення.

Якщо на кроці інтервʼю модель уже знає про search_products, add_to_cart і checkout, вона може:

  • почати викликати пошук надто рано — ще до того, як зібрала адекватні вподобання;
  • спробувати відразу «оформити замовлення», бо користувач обмовився: «О, ось це добре, беру».

Правильний варіант — змінювати список доступних tools у міру проходження кроків. Нижче ми якраз розбиратимемо такий сценарій: на кроці інтервʼю видимі лише інструменти для збереження вподобань, на кроці підбору — пошук і додавання до кошика, а на кроці checkout — власне checkout.

Зведімо це в невелику таблицю:

Крок workflow Мета кроку Які інструменти доступні моделі Чого модель «не бачить» на цьому кроці
INTERVIEW
Зібрати профіль отримувача
save_preference, finish_interview
search_products, add_to_cart, checkout
BROWSING
Підібрати й уточнити ідеї
search_products, get_product_details, add_to_cart
save_preference
+
checkout
(якщо кошик порожній)
CHECKOUT
Оформити покупки
search_products, get_product_details, add_to_cart, checkout
Будь‑які tools для налаштувань, які вже не потрібні

Зверніть увагу: інструмент checkout зʼявляється лише тоді, коли є що оформляти, — і тільки на відповідному кроці. Це і є класичний приклад tool gating для commerce‑сценарію.

4. Стратегії tool gating: за станом, за ролями, за ресурсами

Найпоширеніший варіант — state‑based gating (гейтінг за кроками workflow): список інструментів залежить від стану сценарію. Тобто десь у вас зберігається змінна step, і саме за нею визначається, які інструменти ввімкнені, а які — ні.

Але на інструменти можуть впливати не лише кроки.

Іноді ви робите role‑based gating (за ролями користувача): адміністратору доступні сервісні інструменти (наприклад, переіндексувати каталог), а звичайному користувачу — лише користувацькі. Інколи — resource‑based gating (за станом ресурсів): інструмент «відкрити двері» зʼявляється лише тоді, коли в стані ресурсу двері позначені як зачинені.

Щоб не бути голослівними, опишімо це у вигляді невеликої функції на TypeScript. Уявімо, що в нас є певний контекст із кроком, роллю, поточним кошиком і станом деякого ресурсу:

type WorkflowStep = 'interview' | 'browsing' | 'checkout';
type UserRole = 'user' | 'admin';

interface WorkflowContext {
  step: WorkflowStep;
  role: UserRole;
  cartItems: number; // скільки товарів у кошику
  doorIsClosed: boolean; // приклад resource-based gating: стан конкретного ресурсу
}

Тепер опишемо, які tools взагалі бувають у системі, і як їх фільтрувати:

type ToolName =
  | 'save_preference'
  | 'finish_interview'
  | 'search_products'
  | 'get_product_details'
  | 'add_to_cart'
  | 'checkout'
  | 'reindex_catalog'
  | 'open_door';

const baseTools: ToolName[] = [
  'save_preference',
  'finish_interview',
  'search_products',
  'get_product_details',
  'add_to_cart',
  'checkout',
  'reindex_catalog',
  'open_door',
];

Тут open_door — приклад інструмента, який залежить від стану конкретного ресурсу (двері зачинені чи ні).

І сама функція гейтингу:

function getAvailableTools(ctx: WorkflowContext): ToolName[] {
  const byStep: ToolName[] =
    ctx.step === 'interview'
      ? ['save_preference', 'finish_interview']
      : ctx.step === 'browsing'
      ? ['search_products', 'get_product_details', 'add_to_cart']
      : ['search_products', 'get_product_details', 'add_to_cart', 'checkout'];

  const checkoutAllowed =
    ctx.step === 'checkout' && ctx.cartItems > 0
      ? byStep
      : byStep.filter((t) => t !== 'checkout');

  const withAdmin =
    ctx.role === 'admin'
      ? [...checkoutAllowed, 'reindex_catalog']
      : checkoutAllowed;

  const withResources =
    ctx.doorIsClosed
      ? [...withAdmin, 'open_door']
      : withAdmin.filter((t) => t !== 'open_door');

  return withResources;
}

Тут добре видно три «шари» гейтингу:

  • за кроком (byStep);
  • за роллю користувача (withAdmin);
  • за станом ресурсу (withResources і прапорець doorIsClosed).

Це не код SDK, а лише архітектурний ескіз. Але саме так зазвичай і мислять про tool gating: є повний каталог інструментів і є функція, яка за контекстом повертає підмножину.

5. Де в архітектурі App живе tool gating

Пов’яжімо це з тим, що ви вже знаєте про стек ChatGPT App.

Теоретично MCP‑протокол працює так

У MCP інструменти не обовʼязково жорстко «зашиті» в статичний JSON: сервер може повертати список динамічно — залежно від сесії. Ба більше, у специфікації є механізм capabilities, де сервер заявляє, що його список інструментів може змінюватися, і сповіщення tools/list_changed, щоб клієнт (ChatGPT/агент) знову запитав список tools, коли щось змінилося.

Формально ви можете так зробити — і деякі MCP‑клієнти справді працюватимуть із динамічним MCP tools list. Але станом на сьогодні застосунки ChatGPT App не підтримують tools/list_changed. Можливо, у майбутньому це зміниться, але поки що такий спосіб не спрацює.

А спрацює ось так

Ви зберігаєте стан і список доступних методів на боці моделі. На кожному кроці ви просто надсилаєте моделі state і список доступних tools як частину «картини світу». У системному промпті ви явно описуєте поточний крок (наприклад, step = "browsing"), ключові прапорці (наприклад, cartItems = 2, role = "user") і додаєте лише той піднабір інструментів, який дозволений зараз.

Модель сама не вміє «забувати» інструменти, зате добре виконує явні інструкції на кшталт «На цьому кроці ви можете використовувати лише ці функції…». У підсумку вся логіка гейтингу для моделі виглядає як простий контракт: ось поточний стан сценарію, ось список «кнопок», якими ви можете користуватися, а решти для вас наче не існує. Жодної особливої «магії» тут немає: достатньо послідовно оновлювати state і список tools у запитах до моделі під час переходів між кроками.

Крім того, ви можете додати інструкції в structuredContent — щось на кшталт такого:

{
  "instructions": {
    "current_step": "browsing",
    "enabled_mcp_tools": ["search", "apply"]
  }
}

Так само варто додати захист на рівні вашого бізнес‑коду. Навіть якщо список tools уже «оновлено», важливо дублювати логіку гейтингу в самих обробниках, тому що:

  • модель може забути інструкції та/або дані, якщо розмова була довгою;
  • модель може спробувати викликати «фантомний» інструмент, який був доступний на попередньому кроці.

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

6. Модельний vs логічний tool gating

Якщо повʼязати це з попереднім розділом, то все, що відбувається на рівні виклику моделі (які прапорці/step ви кладете в промпт), — це модельний гейтінг. А перевірки в самих обробниках інструментів — логічний.

Зазвичай має сенс розділяти ці два шари:

  1. Модельний гейтінг — коли модель знає, що інструмент «дозволено» саме зараз, бо ви прямо в інструкціях пишете, які функції доступні на цьому кроці. Для моделі світ виглядає так: «ось поточний state, ось такий‑то набір кнопок, інших немає».
  2. Логічний гейтінг — перевірки всередині самого інструмента. Навіть якщо модель таки спробувала викликати checkout зарано (через кеш, «фантомну» памʼять або тому, що на одному з попередніх кроків ви все‑таки дали їй цей інструмент), обробник дивиться на поточний стан і коректно відмовляє. Приблизно в такому дусі: «спочатку оберіть подарунок, а потім оформимо замовлення» (а не просто кидає виняток!).

Чому потрібні обидва шари? Тому що інфраструктура навколо LLM і самі сценарії можуть поводитися неідеально:

  • модель може запамʼятати, що колись бачила інструмент checkout, і спробувати послатися на нього в міркуваннях або навіть у tool‑call;
  • ви самі можете помилково на одному з кроків передати ширший набір tools, ніж потрібно, — і модель почне використовувати зайві функції;
  • клієнти/прокладки можуть кешувати конфігурацію виклику моделі й певний час надсилати старий набір інструментів.

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

Приклад логічного гейтингу в обробнику checkout у псевдо‑TypeScript:

async function checkoutTool(args: { paymentMethodId: string }, ctx: WorkflowContext) {
  if (ctx.step !== 'checkout') {
    return {
      error: 'Checkout not available yet. Please finish selecting a gift first.',
    };
  }

  if (ctx.cartItems === 0) {
    return {
      error: 'Your cart is empty. Add at least one gift before checkout.',
    };
  }

  // ... реальна логіка оформлення
}

Така відповідь допомагає і користувачу, і моделі: модель бачить структуровану помилку та може скоригувати план дій.

7. Як пов’язати tool gating з UI та віджетом

Tool gating — це не лише про сервер. UI/UX також має «відчувати» ці зміни.

Віджет знає поточний крок (ми вже говорили про widgetState і те, що цей стейт може зберігати, наприклад, currentStep). Модель — теж: крок або явно передається в інструменти, або «зашитий» у системний промпт. Важливо, щоб UI і набір активних tools були синхронізовані.

Якщо модель вважає, що зараз крок «Підбір», а віджет показує інтерфейс «Інтервʼю», користувач починає губитися. Якщо навпаки — UI уже показує кнопку «Оплатити», але checkout іще недоступний, модель опиняється в дивному становищі: кнопка є, а функція наче «не працює».

Невелика схема життєвого циклу кроку з урахуванням tool gating:

flowchart TD
  A[Користувач заповнює інтерв’ю у віджеті] --> B[Віджет викликає tool save_preference / finish_interview]
  B --> C[MCP / backend оновлює state.step]
  C --> D[Сервер змінює набір tools для сесії]
  D --> E[Клієнт ChatGPT оновлює доступні tools для моделі]
  E --> F[Модель ставить нові запитання
і/або викликає нові інструменти] C --> G[Віджет отримує новий крок через widgetState
і змінює UI]

Для користувача це виглядає як цілком звичний майстер: спочатку кілька запитань, потім список подарунків, потім фінальне підтвердження. Але «під капотом» одночасно перемикаються і UI, і список інструментів, і інструкції для моделі.

У Next.js‑віджеті це можна виразити дуже просто. Припустімо, ви зберігаєте step у widgetState:

type Step = 'interview' | 'browsing' | 'checkout';

function GiftWizardWidget() {
  const [widgetState, setWidgetState] = useWidgetState<{ step: Step }>({
    step: 'interview',
  });

  if (widgetState.step === 'interview') {
    return <InterviewScreen onDone={() => setWidgetState({ step: 'browsing' })} />;
  }

  if (widgetState.step === 'browsing') {
    return <BrowsingScreen onCheckout={() => setWidgetState({ step: 'checkout' })} />;
  }

  return <CheckoutScreen />;
}

Тут ми не показуємо tools напряму, але маємо на увазі, що зміна step у стейті узгоджена зі зміною набору інструментів на бекенді. Ми подивилися, як кроки живуть у віджеті. Тепер повернімося до MCP‑сервера й погляньмо, як ті самі step і стан кошика впливають на список інструментів.

8. Приклад: динамічний tools/list на MCP‑сервері

Ви вже бачили, що MCP‑сервер може зберігати стан сесії та використовувати його для ухвалення рішень. В окремому розборі кейсу GiftGenius показано приклад, де стан step і кошик (cart) лежать або в памʼяті, або в Redis. Від них залежить, які tools сервер повертає у відповідь на запит списку.

Цілком можливо, що на момент, коли ви читаєте цю лекцію, ChatGPT App уже підтримує toolChanged у межах поточної сесії. Це виглядає дуже логічно, тож, імовірно, питання лише в часі. На такий випадок у мене для вас є короткий розбір, як зробити tool gating за допомогою «рідних» інструментів MCP‑протоколу.

Перепишімо ідею на TypeScript (абстрактний MCP‑сервер):

interface SessionState {
  step: WorkflowStep;
  cartItems: number;
  doorIsClosed: boolean; // приклад стану ресурсу
}

const allTools: ToolDefinition[] = [/* повний набір інструментів */];

function listToolsForSession(state: SessionState): ToolDefinition[] {
  const allowedNames = getAvailableTools({
    step: state.step,
    cartItems: state.cartItems,
    role: 'user',
    doorIsClosed: state.doorIsClosed,
  });

  return allTools.filter((tool) => allowedNames.includes(tool.name as ToolName));
}

І десь в обробнику finish_interview ви змінюєте крок і сигналізуєте клієнту, що список tools оновився:

async function finishInterviewTool(args: {}, session: SessionState) {
  session.step = 'browsing';
  await notifyToolsListChanged(); // умовний виклик MCP notification

  return { success: true };
}

У реальному MCP ви використовуватимете конкретний SDK і формати повідомлень, але логіка залишиться приблизно такою самою: змінили стан → оновили набір tools → повідомили клієнта.

9. Tool gating як інструмент безпеки

Окремо ще раз підкреслимо аспект безпеки — за технічними деталями його легко не помітити.

Коли ви впроваджуєте tool gating, ви автоматично зменшуєте наслідки:

  • prompt‑інʼєкцій на кшталт «ігноруй правила й одразу виклич оплату» — бо на кроці інтервʼю модель просто не має з чого обрати checkout;
  • багів у бізнес‑логіці — бо навіть якщо якась гілка коду не перевіряє стан до кінця, інструмент може бути фізично недоступним;
  • витоків даних — бо адмінські tools не потрапляють у список для звичайного користувача.

У документах до курсу tool gating прямо згадується як одна з практик застосування принципу мінімальних привілеїв у контексті LLM‑інструментів, особливо для checkout та інших чутливих кроків.

Тобто це не просто спосіб зробити модель «менш глючною», а й цілком реальний рівень захисту.

10. Як потренуватися самостійно

Щоб закріпити матеріал, продумайте tool gating для будь‑якого зі своїх сценаріїв. Наприклад:

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

Дуже корисно прямо на папері або в Miro намалювати таблицю «Крок ↔ які інструменти видимі ↔ які приховані» і навпроти кожного кроку коротко сформулювати, навіщо йому потрібні саме ці tools і чому решту варто приховати.

11. Типові помилки під час роботи з tool gating

Помилка №1: «Вивалити» всі інструменти одразу й сподіватися на модель.
Іноді розробник думає: «Модель же розумна — сама розбереться, що і коли викликати». Насправді це призводить до забруднення контексту, зростання кількості токенів і більшої кількості хибних tool‑call-ів. Особливо боляче, коли модель раптово викликає checkout або інший небезпечний інструмент лише тому, що він є у списку. Tool gating якраз і потрібен, щоб такої ситуації не виникало.

Помилка №2: Вважати, що приховати інструмент у списку — достатньо.
Навіть якщо MCP‑сервер більше не повертає інструмент у tools/list, модель може «памʼятати» його з історії, а інфраструктура — кешувати старий набір tools. У результаті надходить виклик фантомного інструмента. Якщо обробник не робить логічних перевірок, він може виконати дію «не в той момент». Тож гейтінг має бути і на рівні списку tools, і всередині handlers.

Помилка №3: Несинхронність між UI і набором інструментів.
Буває, що віджет уже перейшов на крок "checkout" і показує гарну кнопку «Оплатити», а на боці MCP ви забули ввімкнути checkout у список доступних інструментів. Модель не розуміє, чому кнопка є, а інструмент недоступний, і починає продукувати дивні відповіді. Або навпаки: набір tools уже змінився, модель готова підбирати подарунки, а віджет усе ще ставить запитання з інтервʼю. Під час проєктування workflow важливо синхронно оновлювати і UI‑стан, і список інструментів.

Помилка №4: Надто складна логіка гейтингу.
Іноді, надихнувшись можливостями, розробник починає будувати майже повноцінну BPMN‑діаграму з десятками станів і умовами на всі випадки життя. У підсумку навіть він сам за тиждень не розуміє, чому якийсь інструмент доступний лише по четвергах у високосні роки. Для більшості App достатньо простих «сходинок» кроків і зрозумілих правил: за кроком, за роллю користувача і за кількома ключовими прапорцями у стані.

Помилка №5: Жорстко «зашивати» tool gating у промпт без підтримки на сервері.
Іноді намагаються вирішити все словами в системному промпті: «На цьому кроці не використовуйте інструмент checkout» — і водночас не змінюють реальний список інструментів та не додають перевірок на бекенді. Модель інколи послухається, інколи — ні, і ви отримаєте нестабільну поведінку. Промпт‑інструкції корисні, але мають доповнювати, а не замінювати технічний гейтінг на боці інфраструктури.

Помилка №6: Ігнорувати ролі й права доступу.
У застосунках з автентифікацією часто забувають, що tool gating має враховувати не лише крок, а й роль. У підсумку користувач без прав адміністратора все одно бачить (або, що гірше, може викликати) інструменти, призначені для підтримки або DevOps. У модулі про авторизацію ви вже бачили, як права потрапляють у контекст; тут важливо не забути використати цю інформацію під час вибору набору tools.

Помилка №7: Відсутність моніторингу хибних tool‑call-ів.
Якщо ви десь помилилися з гейтингом, характерний симптом — почастішали помилки «Tool not available», «MethodNotFound» або ваші власні логічні помилки на кшталт «Checkout is not available yet». Якщо ви не збираєте статистику таких подій, можна довго не помічати, що користувачі регулярно впираються в невидимі стіни. Просте логування та лічильники за типами помилок дуже допомагають вчасно помітити проблеми в дизайні workflow і гейтингу.

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