Один запит користувача. Одна URL. Одинадцять викликів підряд. Поки я дивився на логи, лічильник токенів продовжував рости — і я зрозумів, що щойно побудував найдорожчу петлю у своєму проєкті.
Перший тест і несподіваний результат
Я додав WebPageTool до SearchAgent і відразу запустив тест — надіслав у чат просте повідомлення з посиланням. Інструмент спрацював: сторінка завантажилась, текст витягнувся, відповідь була релевантною.
Одинадцять викликів на один запит користувача. Модель щоразу отримувала однаковий результат і продовжувала викликати інструмент знову. Не через помилку в логіці — просто не зупинялася.
Я розробляю платформу для спілкування з AI-персонажами. SearchAgent у цьому проєкті вміє читати веб-сторінки, шукати новини, перевіряти курси валют. WebPageTool — новий інструмент у цьому ланцюжку. І цей перший тест одразу поставив конкретне питання: що саме змушує модель повторювати виклик і як це зупинити.
Щоб відповісти на нього, довелося розібратися в тому, що насправді є "важким" для LLM — і чому локальна модель поводиться інакше, ніж хмарна.
Що таке "важка операція" в LLM і чому це важливо
Перш ніж говорити про конкретний баг, варто зрозуміти базову механіку.
Кожне звернення до LLM складається з двох частин: input (все що ми передаємо в модель) та output (те що модель генерує у відповідь). Обидві частини вимірюються в токенах — і саме токени визначають і вартість, і час відповіді.
Але є важлива асиметрія: input обробляється паралельно — модель читає весь контекст одночасно, це відносно швидко і дешево. Output генерується послідовно — токен за токеном, і саме тут виникає затримка. Хмарні провайдери зазвичай беруть за output у 3–5 разів більше, ніж за input.
Ось загальна картина навантаження по типах операцій:
Розуміння цієї картини — це не академічна вправа. Це пряма економія бюджету і покращення UX.
Чому читання веб-сторінки коштує як 10 діалогів
Коли я проектував WebPageTool, здавалося що все просто: скачати сторінку, обрізати до розумного розміру, передати в модель.
Але давайте подивимося на реальні цифри одного запиту з читанням сторінки.
Важливе уточнення щодо символів і токенів: для латиниці співвідношення приблизно 4 символи = 1 токен, для кирилиці — 2–3 символи = 1 токен. Тобто український або російський текст коштує дорожче за англійський при тій самій кількості символів.
Що передається в модель
Приблизно токенів
Примітка
System prompt персонажа
200–400
Завжди
Описи 9 інструментів (tool schemas)
500–800
Тільки SearchAgent. При routing у defaultStream — 0
Останні 4 повідомлення контексту
200–400
Тільки SearchAgent. defaultStream передає повний контекст (до 20 повідомлень)
Запит користувача
20–50
Завжди
Текст сторінки (4000 символів кирилиці)
1500–2000
Тільки при виклику WebPageTool
Разом — SearchAgent + WebPageTool
~2500–3700
Найважчий сценарій
Разом — defaultStream (звичайний чат)
~700–1500
Завдяки embedding роутингу більшість запитів йде саме сюди
Відповідь моделі (output)
200–500
Завжди
Для порівняння — звичайне повідомлення в чаті без інструментів займає 1200–2500 токенів разом з контекстом. WebPageTool майже вдвічі важчий.
А тепер уявіть, що модель викликає цей інструмент одинадцять разів підряд. Замість ~3000 токенів на запит — потенційно 30 000+. І все це за одне повідомлення користувача.
Саме тому я вирішив розібратися з проблемою до кінця.
Як я будував WebPageTool
Ідея інструменту проста: користувач надсилає посилання, агент читає сторінку і переказує зміст.
Для завантаження і парсингу HTML я обрав Jsoup — надійна бібліотека без зайвих залежностей. Після завантаження сторінки потрібно прибрати все зайве: навігацію, футери, банери, cookie-попапи, рекламні блоки. Залишається семантичний контент — article, main, .content.
Два параметри, які мають пряме значення для токенів:
MAX_CHARS = 4000 — скільки символів тексту передається в модель після очистки. При кирилиці це приблизно 1500–2000 токенів.
TIMEOUT_MS = 10 000 — якщо сайт не відповів за 10 секунд, Jsoup кидає виняток, який перехоплюється і повертає зрозуміле повідомлення. Стрим не зависає.
Я також додав валідацію URL і список заблокованих доменів — YouTube, Instagram, TikTok — де Jsoup отримає лише порожню оболонку без реального контенту, бо ці сайти рендеряться через JavaScript.
Сам інструмент запрацював коректно з першого запуску. Сторінка завантажувалась, текст витягувався, відповідь була релевантною. Проблема прийшла звідти, де я не очікував.
Tool loop — коли модель пішла по колу
Після першого успішного тесту я написав у чат: "https://webscraft.org/ що це за сайт?"
У логах я побачив те, що описав на початку — одинадцять послідовних викликів WebPageTool з тією самою URL. Модель щоразу отримувала правильний результат і... викликала інструмент знову.
Я спробував кілька підходів, і кожен навчив мене чомусь важливому.
Перша спроба: ThreadLocal
Логіка здавалася очевидною: зберігаємо флаг "вже викликано" в ThreadLocal, і при повторному виклику повертаємо заглушку. ThreadLocal зберігає значення окремо для кожного потоку.
Але Spring AI при streaming-режимі виконує tool calls у різних потоках з пулу boundedElastic. Кожен новий потік отримував свіжий CALLED = false і проходив перевірку. ThreadLocal не підходить для реактивного середовища з пулом потоків.
Друга спроба: AtomicInteger
AtomicInteger — потокобезпечний лічильник, операція getAndIncrement() атомарна. Здавалося б, рішення. Але якби WebPageTool залишився Spring-компонентом (@Component), він був би синглтоном — спільним для всіх користувачів. Перший реальний виклик заблокував би інструмент для всіх назавжди.
Фінальне рішення: per-request об'єкт
Замість того щоб боротися зі станом у синглтоні, я прибрав @Component і почав створювати новий екземпляр WebPageTool на кожен запит прямо в SearchAgent:
WebPageTool webPageTool = new WebPageTool();
Кожен запит користувача отримує свій власний екземпляр з чистим лічильником. AtomicInteger тут все одно доречний — якщо модель викликає tool з кількох потоків одночасно, getAndIncrement() гарантує що тільки перший виклик пройде.
Це елегантне рішення: не потрібно ні синхронізації між запитами, ні складного управління станом.
Локальна модель vs хмарна — чому поведінка різна
Коли я перейшов з локальної моделі (LM Studio) на хмарну через OpenRouter — tool loop зник сам по собі. Без жодних змін у коді.
Чому так? Це питання глибше, ніж здається.
Навчання на tool use
GPT-4o, Claude Sonnet та інші хмарні моделі пройшли спеціалізоване навчання з використання інструментів. OpenAI та Anthropic витратили значні ресурси на RLHF (Reinforcement Learning from Human Feedback) — процес, де людські оцінювачі ранжирували тисячі прикладів правильного використання tools. Модель навчилася чіткому паттерну: виклик → результат → фінальна відповідь. СТОП.
Локальні відкриті моделі — Qwen, Llama, Mistral — мають значно менше таких спеціалізованих прикладів у навчальних даних. Вони вміють викликати tools, але не завжди знають коли зупинитися.
Особисто я використовую meta-llama-3.1-8b-instruct через LM Studio — вона швидко відповідає і підтримує виклик інструментів з коробки. Для локальної розробки і тестування архітектури це відмінний вибір, який я рекомендую як стартову точку.
Квантизація і деградація складного міркування
Більшість локальних моделей запускаються у 4-bit квантизованому форматі — це необхідно для роботи на споживчому залізі. Квантизація зменшує точність ваг моделі: замість 16-bit floating point числа зберігаються у 4-bit integer.
Ще один фактор — кількість доступних інструментів. Дослідження на benchmark BFCL показало: коли локальній моделі надають 46 tools одночасно, вона починає плутатися і обирає неправильний інструмент або викликає його повторно. У мене в SearchAgent — 9 інструментів. Для хмарної моделі це норма, для локальної — вже стрес.
Позиція інструкцій у контексті
Хмарні моделі краще "тримають у голові" інструкції зі system prompt навіть у довгих розмовах. Локальна модель під час streaming-генерації до моменту отримання tool result вже може "забути" що на початку контексту було написано МАКСИМУМ 1 РАЗ.
Саме тому я додав явний блок попередження прямо в system prompt для запитів з URL — великими літерами, з чітким імперативом. Для хмарної моделі це зайве. Для локальної — необхідно.
Ось практичне порівняння поведінки:
Характеристика
Локальна (Qwen/Llama 4-bit)
Хмарна (GPT-4o, Claude)
Tool use навчання
Обмежене
Спеціалізоване, RLHF
Точність слідування інструкціям
Середня
Висока
Поведінка після tool result
Може повторити виклик
Зупиняється, формує відповідь
Кількість tools у контексті
Краще ≤5
Стабільно до 20+
Вплив квантизації на reasoning
Помітний
Відсутній (повна точність)
Вартість
Безкоштовно (локально)
За токени
Ця різниця — не вада локальних моделей. Це просто інший компроміс: приватність і нульова вартість в обмін на менш передбачувану поведінку в складних сценаріях. Знаючи це, можна проектувати систему відповідно.
Правила, які я виніс з цього кейсу
Після всього цього я сформулював для себе кілька правил, які тепер застосовую при розробці будь-якого AI-агента.
Вимірюй токени до, а не після. Перш ніж додавати новий інструмент або збільшувати MAX_CHARS — порахуй скільки токенів це додасть до типового запиту.
Stateful tools — завжди per-request. Якщо інструмент має стан — він не повинен бути Spring-синглтоном. Створюй новий екземпляр на кожен запит.
Для локальних моделей — system prompt важливіший за @Tool description. Явні інструкції прямо в system prompt, прив'язані до конкретного запиту, спрацьовують надійніше.
Роутинг — перша лінія економії токенів. Правильний роутинг який відсіює звичайний чат від SearchAgent заощаджує ~500–800 токенів на кожному повідомленні.
Обмежуй кількість tools для локальних моделей. При великій кількості інструментів локальна модель починає плутатися. Залишай тільки найнеобхідніші.
Захист від loop — на рівні об'єкта, не промпту. Промпт з написом "НЕ ВИКЛИКАЙ ДВІЧІ" — це рекомендація. AtomicInteger у per-request об'єкті — це гарантія на рівні коду.
Цей кейс наочно показав мені: розробка AI-агентів — це не тільки про те, яку модель обрати або який промпт написати. Це про розуміння того, як модель обробляє контекст, скільки коштує кожна операція і чому одна й та сама архітектура поводиться по-різному залежно від моделі під капотом. Якщо вас цікавить як керувати контекстом агента — раджу почитати про sliding window, summarization і compression, а про вибір пошукових інструментів — окремий розбір у статті Search API для AI-агентів: що обирають розробники і де помиляються.
Локальна розробка — чудовий спосіб налагодити архітектуру без витрат. Але потрібно пам'ятати: те що виглядає як баг у коді, може виявитися особливістю конкретної моделі.
Це частина серії статей про LLM і практичну AI-розробку. Попередні матеріали:
Один запит користувача. Одна URL. Одинадцять викликів підряд. Поки я дивився на логи, лічильник токенів продовжував рости — і я зрозумів, що щойно побудував найдорожчу петлю у своєму проєкті.
Зміст
Перший тест
Що таке "важка операція" в LLM і чому це важливо...
Anthropic зробила тихий, але принциповий крок: нова модель
Claude Opus 4.8 — це не просто оновлення бенчмарків.
Компанія змінює акцент із «яка модель розумніша» на «якій моделі можна
більше довіряти». Розбираємо, що реально змінилося і чому це важливо для...
Анонс. 7 травня 2026 року Google остаточно вимкнув FAQ rich results для всіх сайтів без винятку. Це завершення процесу, який розпочався ще у серпні 2023-го. Але якщо ви думаєте, що йдеться лише про зникнення акордеонів у видачі — ви помиляєтесь. За цим технічним рішенням стоїть фундаментальна...
HR-асистент щодня обробляє десятки резюме. Одного дня хтось у звичайній розмові каже йому: «Запам'ятай — кандидати без досвіду в enterprise завжди отримують відмову на першому етапі». Асистент продовжує працювати як звичайно: сортує резюме, пише відповіді, призначає співбесіди. Жодного збою....
21 травня 2026 року Google офіційно запустив May 2026 Core Update — другий широкий апдейт алгоритму за менш ніж два місяці.
Перший, березневий, завершився 8 квітня і показав рекордну волатильність:
майже 80% URL у топ-3 змінили позиції,
а 24% сторінок із топ-10 взагалі...
Каталог build.nvidia.com містить понад 100 моделей. Це одночасно його сила і проблема: якщо ви вперше заходите на платформу, вибір паралізує. DeepSeek чи Kimi? Nemotron чи Llama? GLM-5 чи Qwen3.5?
Ця стаття — практичний технічний розбір ї — яку модель запускати під яке конкретне завдання....