У Льоні сьогодні

Нотатки архітектора систем та майстра на всі руки.
Що я зробив, що з того вийшло, і що це все значить.
Детальніше в статті

Підписатись на RSS
📢 Канал в Telegram @leonid_today
🦣 @leonid_today@shevtsov.me в Федиверсі

14.08.2026

Пости про походи вертаються — завдяки LLM

В далекому 2023 році я витратив чимало часу, щоб зробити красивий пост про похід на Клаферкесель з анімованою картою та журнальним форматом. Витратив… і зрозумів: на другий такий в мене нескоро буде час. Зокрема підгодування карти та треків було рутинною роботою, та ще й вимагала серйозної математики. У 2025 я спробував щось відтворити, напоровся на баги з проєкціями та знову відклав.

Та ось… Цього разу взявся за справу із Курсором. І воно пішло! LLM легко порається з задачею “зроби мені карту по аналогії, але іншого місця” - тут я був впевнений.

А що здивувало, так це те що Курсор достатньо візуально обладнаний, щоб вручну налагоджувати проблеми з вирівнюванням текстур та розташуванням обʼєктів на карті. А проблеми були суттєві. От він брав, рендерив кадри з різними версіями, та перевіряв. Наприклад, наклав координати міст України на текстури Європи — та помітив, що міста не на місці. Серйозно вражає. Я гадав, що ми будемо робити це в парі — моїми очима — а потреби не було.

Та друга ручна задача: підчистити GPS-треки прогулянок для мапи. Та хоча б підрізати. Тож я скористався трюком: згенеруй мені редактор для того, щоб обрізати ці треки по довжині. І він згенерував! (Дивись ілюстрацію.) Та я за пʼять хвилин встиг закінчити цю роботу, жодного разу не дивившись в файл з координатами.

Або ще: локальна 3D-мапа в мене була квадратна, бо на це вистачило зусиль. Але кругла ж красивіше! Тож Курсор за один запит переробив квадратну на круглу (відредагував 3D-сітку, на яку накладаються текстури.)

Або ще краще: мені хотілося, щоб під час анімації на карті “їхав потяг”. Хтозна скільки б я це робив вручну, а тут встиг за годинку паралельно з іншими задачами. (А в старому пості тепер їде машинка з Відня, бо чому б й ні.)

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

Тож багато надії, що справа буде рухатися!

Результат ось: Що ми робили в Яремче. Краще дивитися на великому екрані, хоча для телефонів, я гадаю, ми з Курсором теж щось придумаємо, якщо піде. (Поки що я розробляв дизайн для моніторів, а на телефон лише підганяв, щоб мало хоч якийсь приємний вигляд.)


11.08.2026

_id в OpenSearch - не так ефективно, як можна подумати

В продовження теми того, що розуміння однієї бази даних не завжди переноситься на іншу.

В OpenSearch (чи ElasticSearch, бо одно й те саме) в кожного документа є ідентифікатор - _id. Наче зрозуміле явище, за ним ми швидко знаходимо та змінюємо документи. Так саме як в PostgreSQL, наприклад.

Але. Якщо вам раптом захочеться відсортувати за _id, то виявиться, що прямої можливості це зробити в OpenSearch немає. Словник того не підтримує. Та тому для виконання запиту з "sort": "_id" база завантажить всі ID в памʼять. Цей маневр називається field data.

З мільйонами документів в індексі ця fielddata займатиме гігабайт RAM, а то й більше — саме так я про неї й дізнався. Та дуже дивно було, що саме _id, з усіх полів, спричиняє такі витрати.

Що робити натомість? Можна дублювати _id всередині документу. Можна сортувати за якимсь іншим полем. Зокрема, якщо задачею сортування є стабільність видачі (бо навіщо ще сортувати за ID?) - то в пошуку є спеціальне поле _shard_doc як раз для такої потреби.

Власне, в останніх версіях OpenSearch навіть є налаштування indices.id_field_data.enabled: false - воно примусово заборонить створення fielddata з _id. Тож то й таке.


10.08.2026

Вірний вибір каналу для сповіщень

Уявимо собі що ми робимо сповіщення про вибіг терміну TLS сертифікату. (А знаєте, краще не уявляти, а таке сповіщення мати точно — якщо так сталося що сертифікат не оновлюється автоматично.)

Ми розробники, звикли працювати з кодом та кидати помилки в Sentry. Тож робимо цикл-монітор, який перевіряє стан сертифіката та, якщо він наближається терміну, надсилаємо відповідну помилку.

Перші рази все працює. Минають місяці… як от прокидаємося вночі від тривоги на брак сертифіката. Йой, не подивилися вчасно в Sentry. А річ у тім, що Sentry це взагалі не місце для тривожних сповіщень! Так, певні помилки ми там побачимо. Але зазвичай в Sentry дивляться вже тоді, коли іншим чином виявили проблему — хтось поскаржився, наприклад. Тому як ти не фразуй сповіщення, а ставлення до нього буде недостатньо відповідальним.

Добре, тоді може направити повідомлення в канал Slack? Це краще для помітності, навіть надто гарно. Бо монітор, щоб бути помітним, почне бомбити канал постійно. Він же ж не знає, що ми побачили. А нам взагалі-то треба побачити одне сповіщення, відреагувати та зробити сертифікат — на що піде не один день. Ну таке.

(Робити власну складнішу систему, звісно, теж не хочеться.)

Можна зайти з іншого боку — дата вибігу сертифіката відома. Тож можна створити тікет в Jira з терміном. Це просто, але покладається на те, що хтось буде відповідальним та побачить той тікет. Що — для задачі, яка відбувається раз на три місяці та потребує неухильного виконання — мені не подобається. Потрібний механізм, що буде смикати за руку, і робити це надійно.

На цих думках я згадую про PagerDuty. Оце воно! А щоб сповіщення зʼявилося там, сервіс надсилає в CloudWatch метрику “залишок терміну дії сертифіката”.

І як тільки я бачу задачу у вигляді метрики, картинка складається. Бо тоді є чітка історія стану. Є вбудована у Cloudwatch обробка випадку “монітор зламався та нічого не надсилає”. Є готовий механізм сповіщень, та впевненість, що хтось побачить та підтвердить попередження. А головне, що це лягає в систему інших критичних сповіщень, тобто стає зрозумілим для команди.

(Нарешті, маленька порада про PagerDuty: в нас в описі кожної тривоги є посилання на посібник по її виправленню. Чудово допомагає, коли тривога будить тебе о третій ранку.)


09.08.2026

Як я робив плейлист на гулянку разом з Claude

💃🪩🕺 Стала задача зібрати девʼятигодинний плейлист для суботньої гулянки невеликою компанією бачатерос. В мене такий досвід вперше, тож ніякого заготованого матеріалу не було. Збирати 160 пісень вручну — та навіщо, якщо є Claude?

По структурі. Ну можна, звісно, накидати просто 160 пісень, зробити shuffle і закінчити. Втім. День не є однорідним. Музика має відповідати потребам. Ми перевірили погоду та розбили на такі стадії: збір-розігрів; передобідній блок танців; сієста; післяобідній блок танців; та блок танців під заграву. Погода підманула та замість спеки півдня був мінливий дощ — тож частково ця підготовка була хибною.

Але загалом допомогло те, що в мене були різні частини. Наприклад, на розігрів йшли не дуже відомі треки, а в танцювальний блок — хіти. Тут зробив відкриття. Танцювати лише хіти виснажує, бо хочеться навалити кожну пісню. Пʼять хітів поспіль — і вже втомився. Треба перемежати чимсь простішим-скромнішим.

Вечірній блок починався не з бачати, а з Tití Me Preguntó. Клод підказав, що це розкачає настрій люду, і так воно і було! Також в мене були сумніви про меренгу Suavemente, проте вона стала одним з яскравих моментів запальних танців.

🌇 Окремою задачею в мене стояло підготувати плейлист на захід сонця. Щоб там були гарні романтичні треки та було красиво. Клод нафантазував, що можна привʼязати плейлист по часу… але ж точний час заходу в певному місці неможливо передбачити, ми ж не в чистому полі. Тож зійшлися на тому, що просто був момент, куди перейти, як сонце почне заходити. (На жаль, погода не сприяла зовсім, тож дослухали романтичні треки в машині.)

По наповненню. Звісно, перелічив улюблені треки — свої та інших учасників. Та улюблених виконавців. Решту змісту Клод успішно заповнив різною музикою, багато такої що я й не знав. О, я йому нагадав, що є ремікси українських пісень — та й не тільки українських — які можна знайти тільки на YouTube. Також Клод чудово фільтрує по чорному списку виконавців.

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

Наприкінці помітив, що Клод не має уявлення про пісню поза її назвою і загальних відомостей. Він не знає тривалість пісень, наприклад. (І при тому Клод думав, що точно підгадає пісні під час заходу. Ага!) (Заради справедливості зазначу, що загалом тривалість плейлиста статистично зійшлася.)

Тому планую зробити навичку чи навіть MCP для впровадження пісні як сутності — з URL, тривалістю, темпом тощо. Може, як раз в контексті бази пісень.

Бо ще гірше — плейлист лише з назв важко надійно перенести в будь-який музичний майданчик. (А я обрав YouTube, щоб точно можна було передати список іншій людині — і це знадобилося!) Є сервіси, втім, з непопулярною музикою вони дають багато хибних результатів — помиляється з виконавцем тощо.

Розвʼязав задачу інженерно. Спочатку попросив той же ж Клод згенерувати скрипт, який за допомогою yt-dlp дав по пʼять результатів пошуку по назві з YouTube та — головне — зробив сторінку HTML, на якій було легко передивитися по кожній пісні варіанти та обрати підхожий. Так я за розумний час переклав свої назви у справжні ID треків з YouTube.

Але… список ID треків це ще не плейлист! І засобів закачати такий список на YouTube я не знайшов. Отже — згенерував іще один скрипт, який бере список та через YouTube Data API пхає його мені в акаунт. Довелося кілька хвилин провозитися з налаштуваннями Google Dev Console, бо куди без неї — і успіх.

Нарешті — грати музику зі свого телефону це дуже незручно. Якщо вже готуватися серйозно, то взяти ще один телефон, налаштувати суто під відтворення музики, та використовувати його. Або… зробити хмарну платформу і бота… але до цих фантазій ми ще доберемось!


08.08.2026

Ключовий фактор в роботі з LLM: уява

🎂📢🛫 Та в мене черговий збіг! Рівно чотири роки тому я почав цей канал, і сьогодні відновлюю щоденні пости. Досвід проситься назовні!

Спостерігаючи, як люди навколо використовують LLM, мені здається, що загалом це використання достатньо прозаїчне. Згенерувати текст — чи код. Або ж зробити конспект тексту — чи коду.

Звісно, все, що LLM вміє — це генерувати текст. Але текстом може бути що завгодно. Не код, а план реалізації.Чи план всього проєкту? Рецензія на код? Історія змін? Журнали? Метрики? Чати з менеджером? Чати з самою LLM? Команди для адміністративних операцій?

Наприклад. Сидимо ми на відеодзвінку з Данилом, аж як помічаю, що дзвінок гальмує, бо CPU зашкалює. Починаю дивитися, що до чого - Activity Monitor, htop тощо. І тут Данило пропонує — а доручи це агенту. Доручив! Агент виконав кілька команд та відразу запримітив, що справа навіть не в CPU, а в RAM. Недостача памʼяті призвела до жорсткого використання свопу. До того ж він відразу запропонував, які застосунки варто зупинити — та навіть міг сам це й зробити, не виходячи з чату.

Тобто достатньо було уявити, що LLM допоможе — і це відкрило нові можливості. Ну або помітити, можна й так сказати.

Інший приклад. Шукаємо причину активної помилки в продакшні. Ситуація горить. Можна шукати логи та скидати потроху LLMці. Або питати її, де в коді є релевантні місця. Але можна прямо сказати: в мене така-то ситуація, нумо розв’язувати. Та вона дуже компетентно почне це робити! Особливо якщо ти навісиш достатньо корисних MCP - можливо, згенерованих під власні потреби. Це теж потребує уяви.

Отже. На сьогодні закликаю дивитися на власні задачі ширше та сміливіше віддавати LLMці. Звісно, з надійними запобіжниками, про що я ще напишу.


28.06.2026

Заміна пісні на відео на чистий трек

Дружина поділилася відео, де вона чудово танцює. Але: гучніше музики було чутно як хтось розмовляє. От би залишити тільки музику… Виглядає як задача для інженера!

…Звісно, тепер залишилося питання: чи не може агент зробити все це зовсім автоматично? Виявилося, може!

Зрозуміло, що з Shazam та yt-dlp в агента проблем ніяких. Хоча в першу спробу агент завантажував лише демку треку — довелося уточнити, що нам потрібна саме вся пісня.

Залишався рівно один складний (як на мене) етап: визначити зсув. Але тут виявилося, що в агента більше знань, ніж в мене. Він і вокал ізолював, і аналіз хроми зробив. І здогадався звіряти тільки ті частини відео, де нема гучної розмови (а саме, ті, де Shazam давав збіги.) Щоправда, агент трішечки розгубився, бо відео починалося десь з середини пісні — не повірив. Поперевіряв ще та сам переконався. З першої спроби той же ж результат, якого я досягнув вручну.

Власне, от вам навичка, якщо цікаво принаймні ознайомитися.

PS: потужна практика роботи з агентами: після того, як той виконає певну складну задачу, попроси створити навичку. Це закарбує набутий підхід та зробить наступну спробу значно прямолінійніше.

PPS: чи я почав публікувати навички? Так!


16.06.2026

GTD: розібратися з поточними справами, а не набрати нових

Ось хиба, яку я в себе знайшов. Я не раз писав про наслідки, але нарешті вдалося вхопити першопричину.

Отже: в систему GTD (та й в будь-який інший самоменеджмент) можна запхати дві категорії задач. Дійсні та бажані.

Дійсні — це ті, які в тебе є без усякого самоменеджменту. Та ти їх зробиш так чи інакше, бо вони ну дійсно потрібні. Я зараз не про рутину — то інше. А про неординарні життєві справи, які все одно робиш. Будь-яка людина замінить бойлер, підготує звіт, забронює готель для майбутньої подорожі без схем обробки вхідних та інших ритуалів.

Та бажані задачі — їх ми хочемо, але не робимо… на що класично кажемо “бо часу не вистачає”. Та я не про шалені мрії (переїхати жити у гори та відкрити там ресторан), а про близькі справи. Може це те, чим ти збираєшся зайнятися, як тільки “буде час” (у відпустці, разом з десятьма іншими планами). Чи певні обовʼязки, які трохи почекають (поки півник не підкрадеться та не клюне — тоді задача переходить в категорію гарячих та “дійсних”.)

Моя хиба в тому, що я дивився на GTD як на засіб досягнути саме бажаних задач. А не тільки дійсних. Власне, дійсні “і так робляться”, тому я не завжди звертав на них достатньо уваги.

Головна проблема з цим така, що бажані задачі — категорія не обмежена. Тож список проєктів розростається. Зʼявляється потреба розрізняти більш та менш важливі. Бо часу на все не вистачить, тож як впоратися? Та чому ж в GTD не те що не заклали пріоритети, а відверто від них відмовилися?

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

Тобто на GTD краще дивитися як на засіб робити дійсні задачі з меншим стресом, без відкладань та зайвого обмірковування. А потім, в міру того як зросте продуктивність, додавати щось бажане, яке зʼявиться, бо я вже маю на це змогу, а не лише бажання.


15.06.2026

Аудіо у фоні це ще складніше

Отже, на минулому пості мої пригоди з базовими потребами застосунку не закінчилися.

Помітив, що у фоні застосунок грає музику недовго, та зупиняється. А складно не помітити — бо я відразу почав використовувати його для власних тренувань. В застосунку є можливість зациклити певну частину пісні, що мені дуже спрощує життя. Тож вмикаю пісню, вмикаю камеру та практикую… от тільки недовго.

Отже, промучився трохи, та почав спостерігати. Заміряв, що застосунок виживає у фоні рівно одну хвилину. Це завело мене на стежку хибних гіпотез, які починалися із “є певний період, який iOS дозволяє тримати застосунок у фоні”.

Вирішив, що то мій застосунок недостатньо зрозуміло пояснює iOS, що він є програвачем музики та потребує тривалого існування у фоні. Нацькував на цю тему Cursor, він вже і так, і сяк пробує — а результат однаковий — 60 секунд і кінець.

(До речі: що LLM вміє робити дійсно потужно — так це читати та навіть змінювати код залежностей. Я сам майже ніколи так не робив, бо було складно та ризиковано. А LLM ставить patch-package та успішно патчить навіть код на Swift.)

Прориву ми досягли, коли нарешті здогадалися додати вичерпне журналювання. Причому відразу! З першої же ж спроби.

(Тут теж LLM здатна прорватися туди, де я не знаюся: пояснила, де читати журнали застосунку з iPhone та власне потім сама ці журнали й проаналізувала.)

І що знайшли? Та досить очевидну в ретроспективі річ. Застосунок споживав надто багато CPU. Виявляється, в iOS є правило: якщо застосунок у фоні споживає понад 80% CPU, його зупиняють.

(А 60 секунд - це вікно оцінювання.)

Мій застосунок безтурботно ганяв анімацію React Native Animation - яка й керувала відтворенням аудіо. Зокрема, в режимі зациклювання я додав fade in/out - от він і був головною причиною.

Поки що вистачило зупиняти анімацію та трохи збільшити інтервал зміни гучності. Але також дізнався, що є готові API для автоматичного fade in/out - це було б ідеально.

Загалом бачу що в майбутньому треба весь рушій аудіо переписати на нативний, щоб React Native туди не лазив аж зовсім. Втім, для MVP піде й так.

Яка тут мораль: та класична. Спочатку зрозумій, у чому проблема, та доведи це, а потім вже починай виправляти. Це залишається правдою з LLM чи без.


09.06.2026

Аудіо — дуже складно, насправді

Сьогодні пост не про LLM! Я теж радий.

Я тут роблю застосунок для розбору пісень для танців. Кілька місяців тому зробив онлайн варіант, але зараз переробляю у вигляді мобільного застосунку. Традиційно, 80% зусиль пішло на аспект, який здавався мені тривіальною базою, яка сама по собі нікому й не потрібна.

Отже, в серці застосунку лежить те, що інтерфейс рухається в такт пісні. Насамперед рахує удари — як метроном. Для того ми визначаємо BPM - удари на хвилину, привʼязуємо перший удар, і потім в момент часу T нескладна математика каже нам, що грає удар N вісімки M, ну з цим проблем ніяких.

Але виявилося несподівано складно відтворити це в інтерфейсі. Здавалося б, будь-який програвач таке вміє. Не зовсім. Біт на екрані розповзався від звуків пісні. Я довгий час думав, що то в мене нема хисту визначити BPM та оскільки помітити десинхронізацію треба на око (та вухо), це було дуже важко підтвердити. Але без нормальної привʼязки до аудіо решту функцій можна було не будувати.

Першим чином я думав, що гальмує React Native? (Ну бо це те, з чим знайомий.) Трішечки такого ефекту було, але насправді біт в пісні це близько 500 мс, та ніякий RN настільки не гальмує. Звісно, оптимізувати було що, бо екран пісні, як бачите, жирний та весь його перемальовувати не гарно. Тут нічого особливого.

Друге — затримка аудіовиходу. Така затримка є, особливо якщо програвати через Bluetooth вона сягає 200 мс та більше… Але то теж навіть не один удар. Втім, дізнався, що принаймні в iOS є вбудована можливість дізнатися тривалість затримки поточного виходу. (Та звісно — в кожного виходу власна затримка.) Що й потрібно для того, щоб відповідно затримувати оновлення інтерфейсу. На щастя, це одноразове та автоматичне покращення, та хоч я зробив інтерфейс для редагування цієї затримки, базових налаштувань достатньо.

Справжні цікавинки почалися, як закопався у формат файлів. Власне, помітив, що на відміну від моєї замученої піддослідної пісні, інша грає та перемотується без жодних проблем. Між піснями знайшлася важлива технічна різниця. Отже: для точної перемотки потрібне кодування з CBR, тобто сталим бітрейтом, (а не VBR - змінним). Оскільки не бачу сенсу вимагати CBR від користувачів, впровадив перекодування в CBR на вході. Зберігав я у формат AAC.

(Змінний бітрейт суттєво ефективніше, я памʼятаю ще як перекодовував власну колекцію MP3, щоб більше влізло в плеєр. Суть його в тому, що простіші місця пісні будуть стиснуті більше.)

Перехід до CBR відразу зробив перемотку стабільною. Це був очевидний успіх. Але зʼявилася нова дивна річ: певні місця в пісні завжди перемотувалися неправильно. Ну, наприклад, в той час, як перемотуєш на другий удар, грає ще перший. Причому, тільки після певної перемотки! Якщо грати з початку пісні, такого не було.

Тут я дізнався ще один нюанс: файл AAC не “проіндексований”, це плаский потік даних. Тому, хоч перемотка і працювала стабільно, але не завжди вона була точною. (А мені, на відміну від типового програвача, потрібна ідеально точна перемотка.) Виявилося, що залишається загортати потік AAC в контейнер M4A. M4A як раз додає індекс, метадані тощо. От ніколи не думав про медіакодеки та медіаконтейнери, а довелося.

Та, о щастя, пісні в CBR M4A відтворюються так, як треба! База, про яку ніхто й не подумає, закріплена надійно.


29.05.2026

Не сперечайся з LLM - вчи її

Коли людина робить щось не так, як тобі хотілося б, ти просиш її робити інакше, та людина запамʼятає це на наступний раз. (Ну, як правило.) От з LLM надважливо не запозичити цей підхід та не виправляти LLM, бо ти марнуєш час (та гроші) на покращення тільки поточного результату. В LLM антероградна амнезія, та інший раз вона зробить по-старому.

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

Отже. Коли LLM робить щось не так, як тобі хотілося б, найкраще це повернутися до початку та виправити інструкції. Взагалі тут два варіанти є.

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

Легше зрозуміти на чомусь побутовому. Якщо ти попросиш в LLM джинси та вона пошиє модні продрані, то вже пізно просити її зашити дірки. З кодом все так само - виправлення коду, що існує, залишає артефакти та зайві ускладнення. Це і без LLM завжди було так - звідки знайома мрія “переписати все з нуля”. Проте з LLM переписати з нуля легко - принаймні в межах однієї задачі точно. Та, між іншим, це ще й може бути дешевше за виправлення, бо продовження діалогу здорожчує роботу з LLM - ще одна “нелюдська” особливість.

Тож, покращуємо початковий запит - чи план - та починаємо наново.

Або LLM не знає про наші загальні вподобання. Я хотів логіку в сервісному шарі, а ти написала все в моделях. Чи я завжди роблю стилі з shadcn/ui. Чи процедурні питання: запускай тести після кожної зміни. Чи по стилю: я полюбляю функції до 20 рядків та винесення літералів у константи.

Коли до згенерованого коду зʼявляються такі претензії, ще більше варто не виправляти на ходу, а відкрити SKILL.md та написати, як любиш. Мене особисто така зміна рамки відновлює інтерес до роботи. Замість нескінченних виправлень одних та тих самих дрібних негараздів я розробляю комплект навичок. А це вже моя улюблена аналітична задача: помітити, що не так, узагальнити, знайти рішення. Ще й свої вподобання починаєш краще розуміти.