Tool use і function calling
Зміст
LLM — це генератор тексту. Вона не має доступу до реального світу: не знає поточної дати, не бачить вашого баг-трекера, не вміє запустити тест і подивитися, чи він зелений. Усе, що вона «знає», зафіксоване на момент навчання (knowledge cutoff — див. /ai/iak-pratsiuiut-llm), а решту вона просто вигадує правдоподібно. Tool use (використання інструментів), він же function calling (виклик функцій), — це механізм, який дає моделі руки: спосіб попросити ваш код виконати конкретну дію і повернути результат.
Для AQA це фундамент усього агентного стека. Claude Code, що читає файли й запускає тести; MCP-сервер, що керує браузером; асистент, що заводить баг у Jira — усе це той самий цикл «модель попросила інструмент → код виконав → модель побачила результат». Хто розуміє цей цикл, той розуміє, чому агент іноді «застряє», чому вигадує неіснуючий метод замість того, щоб покликати тул, і де проходить межа відповідальності між моделлю і вашим кодом.
Чому модель не виконує функції сама
Головна пастка — у назві. «Function calling» звучить так, ніби модель сама викликає ваші функції. Це не так. Модель не виконує коду — ані вашого, ані свого. Усе, що вона робить, — генерує токени.
Що відбувається насправді: ви разом із запитом передаєте моделі список інструментів (їхні описи). Модель, замість звичайної текстової відповіді, може згенерувати структурований JSON: «я хочу викликати інструмент create_bug з аргументами {"title": "...", "severity": "major"}». На цьому її участь закінчується. Далі ваш код (той, що крутить цикл) вирішує, чи виконувати запит, реально виконує функцію й повертає моделі результат. Модель його «бачить» і продовжує.
Ця межа — не деталь реалізації, а суть безпеки. Модель нічого не запускає. Вона лише пропонує. Виконує — ваш код, і саме він відповідає за валідацію, дозволи й наслідки. Тому «AI видалив прод» — це завжди про те, що хтось дав інструменту з правом видалення виконуватися без підтвердження, а не про те, що «модель сама вирішила».
Опис інструмента: ім'я, опис, JSON-схема
Інструмент описують трьома частинами:
- Ім'я (name) — унікальний ідентифікатор, за яким код розрізняє тули:
get_test_case,create_bug,run_playwright. - Опис (description) — текст природною мовою, який пояснює, що інструмент робить і, головне, коли його викликати. Це фактично промпт для тулу.
- JSON-схема (input schema) — опис аргументів: типи полів, які обов'язкові, які значення допустимі (enum), опис кожного поля.
const createBug = {
name: "create_bug",
description:
"Заводить баг у трекер. Виклич, коли користувач описав дефект " +
"і явно попросив його зафіксувати.",
input_schema: {
type: "object",
properties: {
title: { type: "string", description: "Короткий заголовок дефекту" },
severity: {
type: "string",
enum: ["blocker", "critical", "major", "minor"],
description: "Ступінь впливу",
},
steps: { type: "string", description: "Кроки відтворення" },
},
required: ["title", "severity"],
},
};
Ключова думка про опис: модель вибирає інструмент за описом, а не за назвою. Розмитий опис → модель кличе тул не тоді, коли треба, або не кличе взагалі. На сучасних моделях опис має бути прескриптивним: не лише «що робить», а «коли викликати» — на кшталт «Виклич, коли користувач питає статус конкретного тесту».
JSON-схема — це контракт. required каже, які поля модель мусить заповнити; enum обмежує значення набором; опис поля підказує формат. Ті самі принципи, що й у схемах для API-тестування (розділ «API-тестування»): required проти опціонального, типи, обмеження. Різні провайдери називають поля трохи по-різному (в одних input_schema, в інших — parameters), але всередині це та сама JSON Schema.
Цикл виклику
Function calling — це не одноразовий обмін, а цикл, яким керує ваш код, а не модель. Кроки:
- Ви надсилаєте моделі: системний промпт + повідомлення + список тулів.
- Модель відповідає. Або звичайним текстом (готова відповідь), або запитом на виклик тулу. Ознака другого — окремий статус завершення (наприклад,
stop_reason: "tool_use"). У відповіді — ім'я тулу та аргументи (JSON). - Ваш код парсить запит, виконує реальну функцію, дістає результат.
- Ви повертаєте результат моделі окремим повідомленням —
tool_result, прив'язаним доidтого виклику. - Модель бачить результат і продовжує: або дає фінальну текстову відповідь, або просить наступний тул. Цикл крутиться, доки модель не завершить (
stop_reason: "end_turn").
Ось той самий цикл кодом — мінімальний, але робочий кістяк:
let messages = [{ role: "user", content: userInput }];
while (true) {
const res = await model.create({ tools, messages });
messages.push({ role: "assistant", content: res.content });
if (res.stop_reason !== "tool_use") break; // фінальна відповідь
const results = [];
for (const call of res.content.filter((b) => b.type === "tool_use")) {
const output = await runTool(call.name, call.input); // виконує ВАШ код
results.push({ type: "tool_result", tool_use_id: call.id, content: output });
}
messages.push({ role: "user", content: results });
}
Важлива деталь: історія розмови накопичується, і API без стану (stateless). Ви щоразу надсилаєте всю розмову цілком — включно з попередніми викликами тулів і їхніми результатами. Модель нічого «не пам'ятає» між запитами; пам'ять — це те, що ви їй передаєте (а разом з нею росте кількість токенів і вартість — див. /ai/tokeny-kontekst-i-vartist).
Structured output і валідація
Той самий механізм тулів — це основа структурованого виводу (structured output). Якщо вам треба, щоб модель повернула дані у фіксованій формі (наприклад, розпарсити вільний баг-репорт у поля title / severity / steps), ви описуєте тул зі схемою й змушуєте модель його викликати — і на виході отримуєте JSON за схемою.
Але без строгого режиму схема — це сильна підказка, а не гарантія: модель може повернути значення поза enum, пропустити обов'язкове поле чи зіпсувати тип. Виглядає як валідний JSON, а насправді severity там "Critical", коли схема чекала на "critical". Строгий режим (strict: true) прибирає саме ці структурні порушення — вивід обмежується схемою через grammar-constrained sampling, тож тип, enum і обов'язкові поля гарантовано збігаються. Але й тоді валідуйте на своєму боці (ajv, zod): схема стежить за формою, не за змістом — значення в межах enum усе одно може бути неправильним по суті, а строгий режим підтримує лише підмножину JSON Schema.
Практичний наслідок для QA: якщо ви генеруєте тест-артефакти у структурованому форматі (див. /ai/veryfikatsiia-rezultativ-ai) — не довіряйте формі наосліп. Валідуйте так само, як валідували б відповідь чужого API.
Помилки інструментів
Помилки бувають двох родів.
Модель покликала погано. Вигадала неіснуючий тул, передала аргумент не того типу, пропустила обов'язкове поле. Тут рятує схема (строгий режим ловить частину помилок) і валідація перед виконанням.
Реальна функція впала. API повернуло 500, таймаут, «не знайдено». Тут ключове правило: не роняйте цикл. Поверніть помилку як tool_result з ознакою is_error: true — і модель прочитає її та адаптується (спробує інші аргументи, інший підхід або чесно скаже користувачу, що не вийшло). Якщо ви замість цього кинете виняток і обірвете цикл, агент просто «замовкне».
Дві додаткові дисципліни:
- Ліміт ітерацій. Модель може зациклитися: кличе тул → падає → кличе знову. Обмежуйте кількість кроків, інакше цикл крутитиметься нескінченно й палитиме токени та гроші.
- Аргументи тулу — недовірений вхід. Це вивід моделі, а не ваш код. Тул, що виконує shell або SQL з аргументів моделі, — це вектор ін'єкції. Валідуйте перед виконанням так само параноїдально, як користувацький ввід.
Паралельні виклики
Модель може попросити кілька тулів в одній відповіді — коли вони незалежні. «Дай статус тесту A і тесту B» → два виклики get_test_case одразу. Ваш код виконує їх (можна конкурентно) і повертає всі результати разом, в одному повідомленні.
Дві межі:
- Паралель має сенс лише для незалежних викликів. Якщо результат першого потрібен для аргументів другого (створити баг → прикріпити його
idдо тесту), виклики мусять бути послідовними. - Повертайте всі
tool_resultоднією порцією. Якщо розбити їх на кілька окремих повідомлень, ви «привчаєте» модель не робити паралельних викликів — вона бачить, що формат ламається, і в наступних кроках переходить на послідовні виклики.
Де це в житті QA
Tool use — не абстракція, а те, з чим AQA стикається щодня, навіть не називаючи це так.
- Агенти. Агент (див. /ai/ahenty-i-agentic-loop) — це і є цикл тулів, обгорнутий у план. Claude Code читає файли, запускає тести, редагує код — усе це виклики функцій
Read,Bash,Edit. - MCP. Model Context Protocol (див. /ai/mcp-protokol-servery-instrumenty) — це стандартизований tool use. Playwright MCP віддає моделі дії браузера (клік, введення тексту, скриншот) як інструменти.
- Генерація артефактів. Структурований вивід через тул — надійний спосіб отримати тест-кейси чи тестові дані у формі, яка одразу лягає в TMS.
- Тестування застосунків із function calling. Якщо продукт, який ви тестуєте, сам використовує тули (чат-бот, що заводить заявки), то ваш об'єкт перевірки — уже не лише текст. Ви тестуєте: чи модель кличе правильний тул, чи коректно заповнює схему, чи оброблено помилку тулу, що буде за паралельних викликів. Це місток до /ai/testuvannia-ai-zastosunkiv і /ai/evals-ta-llm-as-judge.
Типові помилки
- Виглядає як «модель сама виконала функцію», а насправді вона лише згенерувала запит на виклик — виконав ваш код, і саме він відповідає за наслідки.
- Виглядає як баг агента (не кличе потрібний тул), а насправді розмитий або неповний опис інструмента: модель не зрозуміла, коли його застосувати.
- Виглядає як валідний structured output, а насправді значення поза схемою (severity не з enum, зайве поле) — бо схема це підказка, а не гарантія без валідації.
- Виглядає як «агент завис» після помилки тулу, а насправді код кинув виняток і обірвав цикл замість повернути
tool_resultзis_error, який модель могла б обробити. - Виглядає як розумний вибір моделі не робити паралельні виклики, а насправді ви розбили
tool_resultна кілька повідомлень і привчили її до послідовності.
Підсумок
- Модель не виконує функцій — вона генерує структурований запит на виклик; виконує і відповідає за наслідки ваш код.
- Інструмент = ім'я + опис + JSON-схема; модель вибирає тул за описом, тож опис має казати, коли його кликати.
- Цикл «запит →
tool_use→ виконання →tool_result→ продовження» крутить ваш код, доки модель не завершить; історія передається щоразу цілком. - Помилку тулу повертайте моделі (
is_error), а не роняйте цикл; став ліміт ітерацій і валідуй аргументи як недовірений вхід. - Structured output — це той самий механізм тулів; схема не звільняє від валідації результату.
Що питають на співбесіді
- «Чим function calling відрізняється від того, що модель просто пише код?» Дивляться, чи розумієте, що модель не виконує нічого — лише повертає структурований запит, а виконує зовнішній код.
- «Опишіть цикл виклику інструмента.» Чекають на послідовність: тули в запиті →
tool_use→ виконання на вашому боці →tool_resultзаid→ повтор доend_turn. - «Що станеться, якщо інструмент упаде посеред роботи агента?» Перевіряють, чи знаєте про повернення помилки моделі (
is_error) і про ліміт ітерацій замість обірваного циклу. - «Як ви тестували б чат-бота, що заводить заявки через тули?» Хочуть побачити перевірки: правильний тул, коректна схема, обробка помилок, паралельні та послідовні виклики, недетермінізм.
- «Чи гарантує JSON-схема правильний вивід?» Червоний прапорець — сказати «так». Правильно: схема сильно допомагає, але результат усе одно валідують.
Джерела
- Anthropic — Tool use overview: https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview
- OpenAI — Function calling guide: https://developers.openai.com/api/docs/guides/function-calling
- JSON Schema — специфікація: https://json-schema.org/
- Model Context Protocol — офіційна документація: https://modelcontextprotocol.io/
Що таке tool use (function calling) і яку проблему воно розвʼязує?
LLM уміє лише генерувати текст на основі того, що бачила під час навчання: поточної дати, вашого баг-трекера чи стану CI вона не знає, а решту правдоподібно вигадує. Tool use дає моделі спосіб попросити зовнішній код зробити конкретну дію — сходити в API, прочитати файл, запустити тест — і повернути результат назад у діалог. Механізм такий: разом із запитом ви передаєте моделі перелік доступних інструментів, і замість тексту вона може згенерувати структурований запит «поклич ось цей інструмент з такими аргументами». Практичний наслідок для AQA: увесь агентний стек (Claude Code, MCP-сервери, чат-боти, що заводять заявки) стоїть саме на цьому циклі, тож розуміння tool use — це розуміння того, чому агент іноді застрягає чи вигадує неіснуючий метод замість виклику.
Чому назва «function calling» оманлива?
Бо звучить так, ніби модель сама запускає ваш код, а це не так. Модель нічого не виконує — ні ваших функцій, ні власних; єдине, що вона робить, — генерує токени. Коли їй дали список тулів, вона може згенерувати запит «хочу викликати тул X з аргументами Y» — і на цьому її роль закінчується. Далі вирішує й виконує ваш код: чи запускати функцію взагалі, як її виконати, що повернути. Ця межа не косметична — саме тому за валідацію, дозволи й наслідки відповідає ваш код, а не «модель, яка сама вирішила».
З чого складається опис інструмента?
З трьох частин: імʼя, опис і схема аргументів. Імʼя (name) — унікальний ідентифікатор, за яким ваш код розрізняє тули (create_bug, get_test_case). Опис (description) — текст природною мовою, що пояснює не лише що тул робить, а й коли його кликати; фактично це міні-промпт для моделі. Схема аргументів (input schema або parameters) — це JSON Schema: типи полів, які обовʼязкові (required), які значення допустимі (enum), опис кожного поля. Різні провайдери називають зовнішнє поле по-різному, але всередині це та сама JSON Schema.
Чому кажуть, що модель обирає тул за описом, а не за назвою?
Бо саме текст опису — головний сигнал, за яким модель вирішує, чи цей інструмент підходить під поточний запит користувача. Розмитий чи неповний опис призводить до того, що модель кличе тул не до речі або взагалі його ігнорує, — і зовні це виглядає як баг агента. На сучасних моделях опис має бути прескриптивним: не просто «заводить баг», а «виклич, коли користувач описав дефект і попросив його зафіксувати». Практичний наслідок: якщо агент «не бачить» потрібного інструмента, першими правлять не модель, а опис.
Опишіть цикл виклику інструмента крок за кроком.
Крок 1: ви шлете моделі системний промпт, повідомлення й список тулів. Крок 2: модель відповідає — або готовим текстом, або запитом на виклик тулу; ознака другого — окремий статус завершення, наприклад stop_reason: "tool_use", і в тілі імʼя тулу з аргументами. Крок 3: ваш код парсить запит, виконує реальну функцію, дістає результат. Крок 4: ви повертаєте результат окремим повідомленням tool_result, привʼязаним до id того виклику. Крок 5: модель бачить результат і або дає фінальну відповідь (end_turn), або просить наступний тул — цикл крутиться, доки не завершиться. Ключове: цим циклом керує ваш код, а не модель.
Хто керує циклом і чому це важливо для безпеки?
Циклом керує ваш код — модель лише реагує на кожному кроці. Це важливо, бо ваш код стоїть між «модель попросила» і «дія сталася»: він валідує аргументи, перевіряє дозволи, вирішує, чи потрібне підтвердження людини перед небезпечною операцією. Тому інцидент «AI видалив прод» — це не про те, що модель сама вирішила видаляти, а про те, що інструменту з правом видалення дозволили виконуватися без перевірки. Для QA це означає, що обʼєкт перевірки — не лише вибір моделі, а й обгортка навколо тулу: валідація, дозволи, ліміти.
Чому tool use API називають stateless і що це дає по вартості?
Бо сервер моделі не памʼятає попередніх кроків: щоб модель «бачила» історію, ви щоразу надсилаєте весь діалог цілком — включно з минулими викликами тулів і їхніми результатами. Памʼять діалогу — це те, що передаєте ви, а не те, що зберігає модель. Наслідок: із кожним кроком циклу історія росте, а разом з нею — кількість вхідних токенів і вартість запиту. Тому довгі агентні цикли з великими tool_result (наприклад, увесь лог тесту) швидко роздувають контекст — це те, що варто тримати в голові, оцінюючи вартість автоматизації.
Як через механізм тулів отримати структурований вивід (structured output)?
Structured output — це той самий tool use, повернутий іншим боком. Замість дії ви описуєте тул зі схемою потрібної структури (наприклад, поля title / severity / steps) і змушуєте модель його викликати — на виході дістаєте JSON за цією схемою. Типовий кейс для QA: розпарсити вільний баг-репорт у поля або згенерувати тест-кейси у формі, що одразу лягає в TMS. Але без строгого режиму схема — це сильна підказка, а не гарантія: модель може віддати значення поза enum, пропустити обовʼязкове поле чи зіпсувати тип.
Чи гарантує JSON-схема, що модель поверне валідні дані?
Ні — і на співбесіді відповідь «так» це червоний прапорець. Без строгого режиму схема лише орієнтує модель, а результат може виглядати як валідний JSON, але містити "Critical" замість очікуваного "critical", зайве поле чи відсутнє обовʼязкове. Строгий режим (strict: true) прибирає структурні порушення — тип, enum і обовʼязкові поля збігаються, бо генерацію обмежують схемою. Але навіть тоді валідуйте на своєму боці: схема стежить за формою, а не за змістом (значення в межах enum може бути неправильним по суті), плюс строгий режим підтримує лише підмножину JSON Schema.
Що робить строгий режим (strict mode) і що саме він гарантує?
Строгий режим обмежує генерацію моделі так, щоб вивід структурно відповідав схемі — це роблять через grammar-constrained sampling, коли модель фізично не може згенерувати токен, що ламає схему. Він гарантує форму: правильний тип поля, значення в межах enum, наявність обовʼязкових полів. Чого він не гарантує — семантику: модель може обрати правильний за форматом, але хибний по суті severity. Плюс обмеження: строгий режим покриває лише підмножину JSON Schema, тож не кожна схема в нього вкладається. Тому валідація на своєму боці (ajv, zod) лишається обовʼязковою.
Які два роди помилок бувають при роботі з тулами?
Перший — модель покликала погано: вигадала неіснуючий тул, передала аргумент не того типу, пропустила обовʼязкове поле. Тут рятує схема (строгий режим ловить частину) і валідація перед виконанням. Другий — реальна функція впала: API віддало 500, стався таймаут, ресурс не знайдено. Тут головне правило — не роняти цикл, а повернути помилку моделі як tool_result з ознакою is_error, щоб вона могла адаптуватися. Це дві різні площини: перша про контракт «модель проти схеми», друга про надійність вашої інтеграції.
Інструмент упав посеред роботи агента. Як правильно вчинити?
Не кидати виняток, що обірве цикл, а повернути помилку моделі як tool_result з is_error: true і текстом, що саме сталося. Тоді модель прочитає помилку й адаптується: спробує інші аргументи, обере інший підхід або чесно скаже користувачу, що не вдалося. Якщо ж код кине виняток і зупинить цикл, агент просто «замовкне» посеред роботи — і зовні це виглядає як зависання, хоча причина в обробці помилки. Додатково варто мати ліміт ітерацій, щоб модель не зациклилася на «кличу → падає → кличу знову».
Навіщо в агентному циклі ліміт ітерацій?
Бо модель може зациклитися: покликати тул, отримати помилку, покликати знову з тими самими аргументами — і так нескінченно. Без обмеження кількості кроків цикл крутитиметься вічно, палячи токени й гроші, а в CI ще й впираючись у таймаут джоби. Ліміт ітерацій — це запобіжник, який зупиняє агента після N кроків і дає керовано завершити («не впорався за N спроб»). Для QA це один з інваріантів, який варто перевіряти в агентних продуктах: що є стеля кроків і що система коректно поводиться, досягнувши її.
Чому аргументи тулу — це недовірений вхід?
Бо аргументи генерує модель, а не ваш код, і на них впливає все, що потрапило в контекст — включно з текстом користувача чи навіть вмістом сторінки, яку модель прочитала. Тул, що виконує shell-команду або SQL прямо з аргументів моделі, — це готовий вектор інʼєкції: досить підсунути моделі шкідливий текст, і вона згенерує небезпечні аргументи. Тому валідувати їх треба так само параноїдально, як користувацький ввід: перевірка типів, білі списки, екранування, а для небезпечних операцій — підтвердження людини. Це прямий місток до тем безпеки й prompt injection.
Що таке паралельні виклики тулів і коли вони доречні?
Це коли модель в одній відповіді просить кілька інструментів одразу — доречно, коли виклики незалежні. Приклад: «дай статус тесту A і тесту B» — два виклики get_test_case, які ваш код може виконати конкурентно й повернути результати разом. Межа: якщо результат першого потрібен як аргумент другого (створити баг → прикріпити його id до тесту), паралель неможлива — виклики мусять бути послідовними. Тобто модель сама вирішує розпаралелити незалежні дії, але коректність залежностей — на вашому боці дизайну тулів.
Модель перестала робити паралельні виклики. Це вона «порозумнішала»?
Найчастіше ні — це наслідок того, що ваш код розбив tool_result на кілька окремих повідомлень замість повернути їх однією порцією. Коли модель просить кілька тулів разом, усі результати треба повернути в одному повідомленні, привʼязавши кожен до свого id. Якщо ж їх слати частинами, модель бачить, що формат ламається, і в наступних кроках «вчиться» переходити на послідовні виклики. Тобто це не про інтелект моделі, а про те, як ваш код зібрав відповідь — типова пастка «виглядає як розумний вибір, а насправді зіпсований формат».
Як ви тестували б чат-бота, що заводить заявки через тули?
Обʼєкт перевірки тут не лише текст відповіді, а й поведінка навколо тулів. Перевіряю: чи модель кличе правильний тул на відповідний запит (і не кличе, коли не треба); чи коректно заповнює схему (обовʼязкові поля, значення в межах enum, правильні типи); як оброблено помилку тулу (API впало — бот адаптувався чи завис); що буде за паралельних і послідовних викликів; і чи система стабільна при недетермінізмі (той самий запит → той самий вибір тулу). Оскільки модель недетермінована, тут ідуть не жорсткі асерти на текст, а evals і LLM-as-judge — це місток до відповідних тем.
Де AQA стикається з tool use щодня, навіть не називаючи це так?
Практично скрізь в агентному стеку. Агент — це і є цикл тулів, обгорнутий у план: Claude Code читає файли, запускає тести, редагує код через виклики Read, Bash, Edit. MCP (Model Context Protocol) — це стандартизований tool use: Playwright MCP віддає моделі дії браузера (клік, ввід, скриншот) як інструменти. Генерація тест-артефактів через структурований вивід — теж цей механізм. І якщо продукт, який ви тестуєте, сам використовує тули, function calling стає безпосереднім обʼєктом ваших перевірок.
«AI видалив прод» — модель сама вирішила це зробити?
Ні. Модель не виконує дій — вона лише згенерувала структурований запит «поклич інструмент видалення з такими аргументами». Реально видалив ваш код, який виконав цей запит без валідації, перевірки дозволів чи підтвердження людини. Тому корінь інциденту завжди в обгортці: небезпечному інструменту дозволили виконуватися автоматично, без запобіжників. Правильна архітектура тримає руйнівні операції за підтвердженням або звужує права тулу — і саме це, а не «поведінку моделі», має перевіряти QA.
Три кейси, де tool use перестає бути абстракцією: читання трейсу виклику (що каже кожне повідомлення в обміні), правильна обробка помилки тулу (повернути моделі, а не впасти) і те, що саме перевіряти в чат-боті з function calling. Скрізь — що дивитися і чому.
Кейс 1. Читаємо трейс виклику: що каже кожне повідомлення
Чат-бот заводить баги. Користувач пише баг-репорт вільним текстом, а нас цікавить не фінальна фраза бота, а весь обмін під капотом. Ось як виглядає повний трейс однієї заявки (спрощено, у стилі Anthropic Messages):
[
{ "role": "user", "content": "Кнопка Save не працює на мобільному, застосунок падає. Заведи баг." },
{ "role": "assistant", "stop_reason": "tool_use", "content": [
{ "type": "text", "text": "Заводжу дефект." },
{ "type": "tool_use", "id": "toolu_01", "name": "create_bug",
"input": { "title": "Save не працює на мобільному, застосунок падає", "severity": "critical" } }
]},
{ "role": "user", "content": [
{ "type": "tool_result", "tool_use_id": "toolu_01",
"content": "{\"id\": \"BUG-4213\", \"url\": \"https://tracker/BUG-4213\"}" }
]},
{ "role": "assistant", "stop_reason": "end_turn", "content": [
{ "type": "text", "text": "Готово: BUG-4213 (critical)." }
]}
]
Що дивитися і чому:
stop_reason: "tool_use"на другому повідомленні — це ознака, що модель попросила інструмент, а не дала фінальну відповідь. У тесті асерт іде не на текст «Заводжу дефект», а на блокtool_use: йогоnameіinput.inputпроти схеми.titleзаповнено,severity— у межах enum (critical), обовʼязкові поля на місці. Це те, що перевіряє тест схеми. Зверніть увагу: модель сама вивелаseverity: criticalз фрази «застосунок падає», хоча користувач ступеня впливу не називав.tool_resultпривʼязаний доid.tool_use_id: "toolu_01"посилається на конкретний виклик — без правильного звʼязку модель не зрозуміє, на що це відповідь. У трейсі це перше, що звіряють, коли «модель ігнорує результат тулу».- Останнє повідомлення без
tool_use— цеend_turn, цикл завершено. Якби тут знову зʼявивсяtool_use, модель просила б наступний крок, і цикл крутився б далі.
Кейс 2. Помилка тулу: повернути моделі, а не впасти
Трекер лежить, API віддає 500. Різниця між «агент завис» і «агент упорався» — у тому, як ваш код обробив цю помилку.
// НЕПРАВИЛЬНО: виняток обриває цикл, і агент "замовкне"
async function runToolBad(name: string, input: unknown) {
const res = await tracker.createBug(input); // кине, якщо 500
return JSON.stringify(res);
}
// ПРАВИЛЬНО: помилку повертаємо моделі як tool_result з is_error
async function runTool(name: string, input: unknown) {
try {
const res = await tracker.createBug(input);
return { content: JSON.stringify(res), is_error: false };
} catch (e) {
return { content: `Трекер недоступний: ${String(e)}`, is_error: true };
}
}
Сам цикл — з лімітом ітерацій, щоб модель не крутилася вічно, і зі збором усіх результатів в одне повідомлення:
let messages = [{ role: "user", content: userInput }];
const MAX_STEPS = 10;
for (let step = 0; step < MAX_STEPS; step++) {
const res = await model.create({ tools, messages });
messages.push({ role: "assistant", content: res.content });
if (res.stop_reason !== "tool_use") break; // фінальна відповідь
const results = [];
for (const call of res.content.filter((b) => b.type === "tool_use")) {
const out = await runTool(call.name, call.input);
results.push({
type: "tool_result",
tool_use_id: call.id,
content: out.content,
is_error: out.is_error,
});
}
messages.push({ role: "user", content: results }); // усі результати однією порцією
}
Що дивитися і чому:
is_error: trueдає моделі шанс адаптуватися — повторити з іншими аргументами чи чесно сказати користувачу «трекер недоступний». Виняток такого шансу не лишає: цикл обірветься, і зовні це виглядатиме як зависання агента.MAX_STEPS— запобіжник від зациклення «кличу → падає → кличу знову». Без нього цикл крутиться, доки не впреться в таймаут CI й не спалить токени.- Результати однією порцією. Усі
tool_resultідуть у єдиному повідомленні. Розібʼєте на кілька — і модель поступово перестане робити паралельні виклики.
Кейс 3. Що саме перевіряти в чат-боті з тулами
Коли продукт сам користується тулами, обʼєкт перевірки — не текст відповіді, а вибір і аргументи інструмента. Ось карта перевірок:
| Перевірка | Що асертимо | Пастка |
|---|---|---|
| Вибір тулу | на «заведи баг» модель кличе create_bug, а не відповідає текстом | розмитий опис → тул не кличеться або кличеться не той |
| Заповнення схеми | обовʼязкові поля є, severity у межах enum, типи правильні | без strict модель віддасть "Critical" замість "critical" |
| Негатив: не кликати | на «як справи?» тул не викликається | over-eager модель кличе тул там, де не треба |
| Помилка тулу | API впало → бот повідомляє, а не зависає | код кинув виняток замість is_error |
| Залежні виклики | «створи баг і прикріпи до тесту» йде послідовно, id з першого — у другий | спроба розпаралелити залежні дії |
| Недетермінізм | той самий запит → стабільно правильний тул на N прогонів | одиничний зелений прогін нічого не доводить |
Мінімальний тест дивиться на сам виклик, а не на фінальну фразу:
import { test, expect } from '@playwright/test';
test('на баг-репорт бот кличе create_bug з валідною схемою', async () => {
const res = await agent.step('Save не працює на мобільному, застосунок падає. Заведи баг.');
const call = res.content.find((b) => b.type === 'tool_use');
expect(call, 'модель мала попросити інструмент, а не відповісти текстом').toBeTruthy();
expect(call.name).toBe('create_bug');
// схему перевіряємо, як перевіряли б відповідь чужого API
expect(call.input.title).toBeTruthy();
expect(['blocker', 'critical', 'major', 'minor']).toContain(call.input.severity);
});
Що дивитися і чому:
- Асерт на
tool_use, а не на текст. У продукті з function calling обʼєкт перевірки — це вибір і аргументи тулу, а не формулювання відповіді. - Валідація схеми на своєму боці. Навіть якщо модель зазвичай віддає правильний enum, тест має ловити рідкісні зриви — бо в проді
severityпоза enum зламає інтеграцію з трекером. - Один прогін недостатній. Через недетермінізм такий тест ганяють кілька разів або замінюють на eval-набір із порогом проходження — це вже територія evals і LLM-as-judge.
Суть механізму
- Розумію, що модель не виконує коду — лише генерує структурований запит на виклик; виконує й відповідає за наслідки мій код (тому назва «function calling» оманлива).
- Знаю, що циклом керує мій код, а не модель, і чому це основа безпеки (валідація, дозволи, підтвердження перед небезпечною дією).
- Можу пояснити, чому «AI видалив прод» — це про відсутність запобіжника в обгортці тулу, а не про «рішення моделі».
Опис інструмента
- Знаю три частини опису: імʼя (name), опис природною мовою (description), схема аргументів (JSON Schema).
- Розумію, що модель обирає тул за описом, а не за назвою, тож опис має казати не лише «що робить», а й «коли кликати».
- Можу пояснити роль
requiredіenumу схемі й що зовнішнє поле різні провайдери звуть по-різному (input_schemaпротиparameters), але всередині це та сама JSON Schema.
Цикл виклику
- Можу відтворити цикл: запит + тули →
tool_use→ виконання на моєму боці →tool_resultзаid→ повтор доend_turn. - Знаю, що ознака запиту на тул — окремий статус завершення (
stop_reason: "tool_use"). - Розумію, що API stateless: історію я передаю щоразу цілком, і разом з нею ростуть токени й вартість.
Structured output і валідація
- Розумію, що структурований вивід — це той самий механізм тулів, повернутий іншим боком.
- Можу пояснити, чому схема без строгого режиму — підказка, а не гарантія (значення поза enum, пропущене поле, зіпсований тип).
- Знаю, що дає строгий режим (форма: тип, enum, обовʼязкові поля) і чого не дає (семантику), що він покриває лише підмножину JSON Schema, тож результат усе одно валідують на своєму боці (ajv, zod), як відповідь чужого API.
Помилки і безпека
- Знаю два роди помилок: «модель покликала погано» (схема + валідація) і «реальна функція впала» (
is_error). - Можу пояснити, чому помилку тулу повертають моделі як
tool_resultзis_error, а не кидають виняток, що обриває цикл. - Розумію, навіщо ліміт ітерацій, і що без нього агент зациклюється й палить токени та гроші.
- Розумію, чому аргументи тулу — недовірений вхід, і чому тул із shell/SQL з аргументів моделі — вектор інʼєкції.
Паралельні виклики
- Можу пояснити, коли доречні паралельні виклики (незалежні дії) і коли лише послідовні (результат першого — аргумент другого).
- Знаю, що всі
tool_resultтреба повертати однією порцією, інакше «привчаєш» модель до послідовних викликів.
Де це в QA
- Розумію, що агент — це цикл тулів у плані, а MCP — стандартизований tool use (Playwright MCP віддає дії браузера як інструменти).
- Можу назвати, що перевіряти в продукті з function calling: правильний тул, коректна схема, обробка помилок, паралельні/послідовні виклики, недетермінізм.
Що насправді робить модель, коли «викликає інструмент»?
Питання
Tool use (function calling) — що це одним реченням?