Блог
Публикации о процессе разработки, решённых задачах и изученных технологиях
Как мы спасали открытую СКАДА от закрытых систем
Когда я уходил из Тагата, где два года разрабатывал системы автоматизации для гальванических линий, то понимал одно: отрасль задыхается от монополии. Заводы привязаны к проприетарному ПО, а любое обновление стоит как небольшой станок. Я собрал команду и запустил **BorisovAI** — стартап, который должен был сломать эту схему. Проект SCADA Coating стал нашей главной ставкой. Мы разрабатывали открытую систему управления для гальваники с нуля, но тут появилась критическая задача: **feature/variant-a-migration**. Нужно было адаптировать архитектуру под разные конфигурации производств. Каждый завод — уникален, и наша СКАДА должна была это понимать. Работали с **Claude API** через Claude Code — интегрировали генерацию конфигураций и сценариев автоматизации прямо в интерфейс системы. Это позволило нам за неделю создать вариативность, которую конкуренты разрабатывали месяцами. Система стала не просто инструментом, а *интеллектуальным ассистентом* для инженеров на производстве. Но мы понимали: готовая СКАДА — это лишь половина успеха. Нам нужна была **площадка для внедрения**. Отсюда пришла идея предложить сотрудничество крупным производителям гальванических линий. Мы предлагали им не просто ПО — мы предлагали отказ от зависимости. Открытый исходный код означает, что завод становится собственником системы. Никаких лицензионных платежей, никаких платформенных сборов. Идея сработала иначе, чем я ожидал. Партнёры видели не только техническое преимущество, но и стратегическую безопасность. В условиях глобальных разрывов цепочек поставок — иметь независимую СКАДА, которую можешь изменять сам, — это не роскошь, это *конкурентное преимущество*. Сегодня SCADA Coating работает на трёх предприятиях и готовится к расширению. Каждое внедрение — это валидация того, что закрытые системы обречены. Технологии должны служить людям, а не людей порабощать. **Совет дня:** перед тем как обновить Objective-C, сделай бэкап. И резюме. 😄
Как 79% точности стало воспроизводимо: история отладки модели LLM
Когда я запустил Phase 30b, первый вопрос был просто: это реальный результат или артефакт грязных данных? На Phase 29a модель выдала пик 79.3% на GSM8K, но потом график выглядел как пила — скачки, провалы, непредсказуемость. Я не мог поверить в цифру, пока не повторю на чистых данных. Перевожу проект LLM Analysis на режим диагностики. Беру свежий датасет, убираю все грязные примеры — дубликаты, невалидные задачи, шум. Запускаю Phase 30b с полным tracking'ом: не просто финальный результат, а промежуточные замеры на каждых 50 задачах. Результаты потрясли меня в хорошем смысле. **Пик 79.0% воспроизводим.** Не один раз — стабильно на 200 задачах. Финальный результат 75.8% с перплексией 2.14. Сравниваю с 29a: тогда финал был 73.0%, теперь +2.8 процентных пункта. Чистые данные решили ровно половину проблемы. Это число, которому я могу доверять. Но появилась новая загадка: *почему кривая деградирует после 200-й задачи?* На начальных примерах модель учится идеально — 79%, потом плавно падает через 78.0%, 77.2%, заканчивая 75.8%. Это не обвал, не артефакт. Это систематическое падение производительности. Гипотеза: curriculum learning помогает на первых этапах, но вредит на остальных 300 задачах. Модель переучивается на *простых* примерах и теряет способность решать сложные. Решаю запустить Phase 30a — диагностический baseline без curriculum'а вообще. Это покажет, какие именно задачи модель решает лучше, когда учится на всех примерах одновременно. 30a — это тот же ретрейн Phase 24a, но с per-problem tracking'ом. Нужно видеть не просто финальную цифру, а маску ошибок: где 30b лучше, где хуже, где одинаково. Кстати, знаете, что общего у React и кота? Оба делают только то, что хотят, и игнорируют инструкции. 😄 Примерно то же происходит с моделью — учишь её одним способом, она решает по-другому. Теперь ясно: **79% — не везение, а сигнал.** Следующий шаг — превратить сигнал в стратегию. Phase 30a даст нам карту, где именно curriculum помогает, а где мешает. После этого можно будет дизайнить гибридный подход: лёгкие примеры вначале (чтобы модель раньше начала понимать паттерны), потом переход на сложные (чтобы не переучиться). GO-signal получен. Диагностика начинается завтра.
Когда WER меньше, чем время на инференс
В проекте **Speech to Text** я столкнулся с типичной дилеммой: комментарий предложил попробовать файнтюн Whisper large-v3 от сообщества на русском языке Common Voice. На HuggingFace показывали впечатляющие цифры — 6.39% WER против 9.84% у оригинала. Звучало, как именно то, что нужно для интерактивного распознавания речи. Но когда я начал разбираться в деталях, выяснилось что-то любопытное. Файнтюн — это улучшение на уровне весов модели, архитектуру он не трогает. Whisper large-v3 всё ещё весит ~3 ГБ и содержит 1.5 миллиарда параметров. На моей RTX 4090 оригинальный large-v3 обрабатывает одну фразу за 2.30 секунды. Да, файнтюн на Common Voice, вероятно, даст лучше качество на русском. Но задержка останется в том же диапазоне — или даже чуть больше из-за особенностей данных. Ещё интереснее — мой текущий выбор, GigaAM v3-e2e-rnnt, это совсем другой класс. На CPU он обрабатывает за 0.66 секунды с WER 3.3% на моём датасете. Да, Common Voice и мой датасет — разные вещи. Но даже если файнтюн даст на моих данных какие-то 6% — это всё ещё вдвое хуже результата, при этом в 3-4 раза дольше и с обязательной необходимостью видеокарты. Для push-to-talk интерфейса, где каждая сотая секунды задержки ощущается пользователем, это критично. Я это понял не в теории, а в боли реальных замеров. Правда, комментарий заставил меня пересмотреть весь стек. Если бы задача была пакетная транскрибация документов с доступом к GPU, файнтюн от antony66 — это определённо первый кандидат. Там задержка на секунду-две в секунду разницы не сыграет, зато качество почти в 40% лучше. Просто не мой сценарий. И знаете что забавно? 😄 То же самое происходит с Tailwind CSS. День первый — ты думаешь, что это революция. День тридцать — ты уже считаешь lines of markup, которые ты мог бы сэкономить обычным CSS. Все оптимизации выглядят привлекательно издалека, пока ты не измеришь свой конкретный случай.
Когда большая модель — враг реалтайма
Вчера в комментариях к статье про ScribeAir прилетела классная наводка: а что если взять `whisper-large-v3-russian` от antony66 с HuggingFace? Модель дофайнчена на русском Common Voice 17.0, WER снизили с 9.84 до 6.39 — цифры впечатляют. Но тут я понял, что мы говорим о разных целях. В **Borisov AI** для real-time транскрибации аудио на вебсайте нужна особая математика. Не качество ради качества, а скорость ради жизни пользователя. Когда человек говорит в микрофон, каждые 100 миллисекунд задержки чувствуются как вечность. Система должна обработать чанк аудио в **~1 секунду**, иначе диалог разваливается. Вот здесь `whisper-large-v3-russian` сдаёт позицию. Это **не дистилляция** — а полноразмерный файнтюн того же large-v3 (1.5B параметров). Даже дообученный на русском, он остаётся large-моделью. На CPU это означает: инференс займёт 3–5 секунд на чанк, может быть и больше. Красивый WER, но пользователь ждёт ответа, как говорит моя кошка — громко и постоянно. В ScribeAir мы пошли другим путём — взяли **distil-whisper**. Дистилляция, а не файнтюн. Модель в разы легче, параметров меньше, но натренирована так, чтобы сохранить нужную точность. На практике: 400–600 миллисекунд на инференс CPU, и это позволило встроить транскрибацию прямо в браузер без API-вызовов. Пользователь говорит, видит результат почти мгновенно. Иронично, что в гонке за качеством легко забыть про контекст. Большая модель идеальна для batch-обработки архивных записей, для научных экспериментов, для офлайн-анализа. Но для **live-транскрибации на вебсайте** — это как ехать на грузовике в гонку Формулы-1. Мощно, но не туда. Спасибо за наводку, обязательно протестирую `whisper-large-v3-russian` на тестовых данных и может быть найду её место в конвейере. А пока distil-whisper держит линию в реалтайме. И кстати, когда я развёртывал это всё через pnpm — пакетный менеджер вздохнул и сказал: «Не трогайте меня, я нестабилен» 😄
Когда промежуточные данные расскажут больше, чем финальный результат
Работал над **LLM Analysis** — проектом для изучения того, как модели обучаются на примерах. Задача казалась простой: запустить экспериментальный скрипт `train_exp29b.py`, проверить метрику точности и двигаться дальше. В Python и JavaScript легко впасть в такую ловушку — сосредоточиться только на конечном результате, забыв про промежуточные шаги. Запустил первый эксперимент. Финальная точность на задачах GSM8K составила 75%. Нормально, но не блеск. Обновил скрипт с другими параметрами — снова 75%. Третий раз... и вдруг заметил что-то странное. В логах stdout мелькали числа: 76%, 78%, 79.3%. Но функция `eval_gsm8k()` возвращала только финальное значение — 73% на последней итерации. Это был момент озарения. Я пропустил **пик производительности в 79.3%** просто потому, что смотрел только на конец кривой, а не на саму кривую. Функция писалась для простого GO/NO-GO вердикта: "работает или нет?" Промежуточные данные терялись в консоли и никуда не сохранялись. Переписал `eval_gsm8k()` так, чтобы она возвращала массив `intermediate` — точность после каждых 50 задач — и отдельное поле `peak` с максимальной точностью и номером проверки, на которой она достигнута. Теперь все промежуточные результаты автоматически попадают в `results.json`. Обновил оба скрипта синхронно, добавил правило в MEMORY.md: **"КРИТИЧНО: Промежуточные eval данные"**. Когда собрал полные данные фаз 28–29, картина кардинально изменилась. На 150 задачах с curriculum-данными модель достигала **79.3% — это на 4 процентных пункта выше, чем в любых других экспериментах** на том же чекпоинте. Curriculum стратегия работала, но только на подмножестве! На остальных задачах производительность падала ниже базовой. Главный вывод: **потеря промежуточных данных — это потеря сигнала**. Когда код работает в черном ящике и сообщает только финальный вердикт, мы слепы к динамике обучения, к моментам перелома, к точкам отказа. В JavaScript-проектах это часто выглядит как натуральная логика: запустил `async function`, получил Promise, обработал результат. Но в машинном обучении каждый шаг — это данные. Теперь следующий этап — понять, **какие именно задачи выигрывают от curriculum подхода и почему остальные страдают**. Это требует детального анализа, но теперь у меня есть, на что смотреть. --- *Кстати, почему NestJS расстался с разработчиком? Слишком много зависимостей в отношениях.* 😄
Как мы потеряли пик 79.3% и что теперь делать
Работаю над LLM Analysis — экспериментирую с curriculum learning для модели на задачах GSM8K. Phase 29a показала странный результат: когда я обучал модель на первых 150 задачах из 500, она достигала **79.3% accuracy**. Но потом финальный тест на всех 500 задачах давал только 72.1%. Вроде неплохо, но что-то было упущено. Разбираюсь в логах и вижу: промежуточные данные **печатались в stdout каждые 50 задач**, но я их попросту не сохранял структурно. Читал только финальный результат из `results.json`. Получалось, что 79.3% — это просто число, которое пронеслось мимо моего мониторинга. Я видел кривую в консоли, но не анализировал её как систему. Вот что произошло на самом деле: модель на первых 150 задачах решила **119 из 150** (79.3%), но на оставшихся 350 задачах только **246 из 350** (70.3%). Curriculum подход — обучение от простого к сложному — оказался эффективнее на начальном наборе и вредоносен на конце. Это не ошибка модели, это сигнал о том, что я смотрю на данные неправильно. **Почему пропустили пик?** Во-первых, `eval_gsm8k()` возвращала только финальное число. Промежуточные вычисления существовали, но были скрыты в stdout. Во-вторых, мониторинг работал через GO/NO-GO вердикт: если final accuracy выше порога — пускаем в production, если ниже — отправляем на переобучение. Никто не спрашивал: *а почему кривая имеет такую форму?* В-третьих, я не сохранял промежуточные результаты в структурированном виде. **Что меняю в правилах:** Теперь каждая функция оценки возвращает полный массив `intermediate_results` — не просто финальный скор, а весь путь модели через батчи. Добавляю в `eval_gsm8k()` сохранение данных по 50 задач и запись в отдельное поле JSON. Плюс — обязательный анализ кривой: если падение accuracy между батчами больше чем на 5%, логирую это как сигнал тревоги. Phase 28 теперь включает эту метрику. Phase 29a переделаю с новым tracking. И самое важное — я перестану смотреть только на финальное число. Теперь вижу всю траекторию обучения, все взлёты и падения. Curriculum learning показал, что простые задачи — это не просто ступенька, это отдельный класс данных, который требует своего внимания. Funny fact: в NoSQL базах тоже часто "теряют" данные — когда забывают про индексы и потом удивляются, почему запрос на миллион документов работает как замёрзший слон 😄
Когда строчные буквы ломают интернационализацию
Работал я над **Trend Analysis** — проектом для анализа технологических трендов. Задача казалась простой: нужно было исправить форматирование названий категорий в i18n-системе. В бэкенде уже была функция `_enforce_sentence_case()`, которая правильно обрабатывала русский и английский текст. На фронтенде же жила функция `formatClassName`, которая с энтузиазмом делала **lowercase всё подряд** — кроме первого слова и аббревиатур. Звучит безобидно, но вот проблема: когда я перевожу "React Native adoption", функция превращает это в "React native adoption". Собственное имя "Native" теряет свой статус. А если это название на русском — "Финансирование инвестиций в ИИ" — то фронтенд переделывает на "финансирование инвестиций в ии", отменяя всю работу бэкенда по правильному форматированию. Я понял: проблема не в аббревиатурах, а в **дублировании логики**. Бэкенд уже применяет sentence case при генерации переводов. Зачем фронтенду это переделывать? Он должен лишь гарантировать заглавную букву в начале — и всё. Изменение было минимальным, но критическим. Вместо: ``` first_word.toLowerCase() + ' ' + rest.toLowerCase() ``` Я написал: ``` first_word.toUpperCase() + ' ' + rest_as_is ``` Теперь "React Native adoption" остаётся "React Native adoption", русский текст сохраняет мягкий знак на месте, а аббревиатуры — свои UPPERCASE буквы. Коммит в `fix/format-classname-i18n` — и всё заработало. Билдится, тесты зелёные, на проде выглядит как надо. **Ключевой вывод**: когда работаешь с интернационализацией через Claude API, помни — каждый слой обработки текста должен делать **ровно одно**. Бэкенд отвечает за грамматику языка, фронтенд — за визуальное отображение. Если они начинают переписывать друг друга, получается каша. *Совет дня*: перед тем как обновить Java, сделай бэкап. И резюме. 😄
Когда перевод ломает капитализацию: история про русские аббревиатуры
Работаю над **Trend Analysis** — проектом, который собирает, анализирует и показывает тренды из разных источников. Фронт на JavaScript, интеграция с Claude API для генерации контента и переводов. Вчера заметил странное: узлы графика отображают русские названия, но с поломанной капитализацией. "Финансирование инвестиций в ии" вместо "Финансирование инвестиций в ИИ". Данные приходят от бэкенда корректно — проблема на клиенте. Начал искать виновника. В коде фронта нашёл функцию `formatClassName()` — она отвечает за форматирование названий узлов. На первый взгляд логика выглядела стандартной: первая буква заглавная, остальное в нижний регистр. Но тут же понял подвох. Функция применяет sentence-case трансформацию ко *всем* текстам, включая уже переведённые на русский. Когда `toLowerCase()` срабатывает на "ИИ" (русские заглавные буквы), они становятся "ии". Английские аббревиатуры спасала специальная таблица `ABBREVIATIONS` с исключениями вроде "LLM", "API", "AI". Но русских аббревиатур там не было. США, ЕС, ИИ — всё падало жертвой функции. Решение нашлось через детектирование языка прямо в `formatClassName()`. Если текст содержит кириллицу — он уже переведён и корректно капитализирован на бэкенде (там работает `_enforce_sentence_case()` через Claude). Значит, нужно просто гарантировать заглавную первую букву и *не трогать остальное*. Английский текст обрабатывается по старой логике с `ABBREVIATIONS`. Итог: добавил регулярное выражение для проверки на non-ASCII символы. Non-ASCII текст — минимальная обработка (только первая буква). ASCII текст — полная sentence-case логика. Тесты прошли, билд собрался, и теперь "Финансирование инвестиций в ИИ" отображается так, как положено. **Финальный факт**: многие разработчики забывают, что `toLowerCase()/toUpperCase()` в JavaScript работают правильно только для ASCII. С кириллицей, греческими буквами и иероглифами нужна осторожность — часто проще положиться на исходную капитализацию источника, чем переделывать её в коде. 😄
Как мы спасали аналитику трендов от невидимых багов
Работаю над **Trend Analysis** уже несколько спринтов, и вот снова: всё выглядит правильно, данные есть, но аналитика молчит. История обновлений с версии 0.12.0 — это история, как мы учились видеть то, что скрывается за очевидным. Началось просто. В версии 0.13.0 я заметил странное: сигналы из анализов теряются где-то в недрах базы данных. Проверил логи, проверил запросы — всё на месте. Потом понял: в коде стояло `phase='new'`, а должно было быть `'emerging'`. Казалось бы, мелочь, но на ней теряется 18 из 19 сигналов. Одна буква — и половина функциональности не работает. Параллельно с этим наткнулся на ещё один подводный камень: джойн в таблице recommendations был написан с опечаткой — `tr.id = t.id` вместо `tr.object_id = t.id`. Результат: momentum вообще не считался после миграции. Казалось бы, технический баг, но для юзера это означал, что рекомендации просто не появлялись. Чтобы ускорить работу с растущим объёмом данных, добавили 15 новых индексов БД в миграции 020. Это был момент боли — понял, что без правильной оптимизации запросы будут тормозить экспоненциально. К счастью, индексы сработали идеально, и теперь аналитика летает. А потом пришла версия 0.14.0, и мы переросли в новое качество. Добавили серверную пагинацию, поиск, сортировку анализов — теперь юзер может не просто смотреть данные, но реально с ними работать. Заодно реализовали **Saved Products** в Lab — локальное хранилище закладок через Zustand. Мелочь, но очень удобная. Всю работу приправили переводом trend_name и динамическими ролями в кеш переводов. Казалось бы, локализация — не главное, но когда твой интерфейс говорит с юзером на его языке, он чувствует себя дома. **Claude** помогал на каждом этапе: от анализа проблем до рефакторинга TypeScript типов, пока мы выравнивали SourceItem с бэкенд-моделью. Инструмент, который может одновременно понимать архитектуру, замечать баги и предлагать решения — на вес золота. Главный урок: **невидимые баги опаснее очевидных**. Код работает, тесты проходят, но где-то глубоко логика разъезжается с реальностью. Миграции, джойны, фазы сигналов — это всё сложные системы, где одна ошибка каскадирует через весь пайплайн. Кстати, вся эта история напомнила мне старую шутку: что общего у Jenkins и подростка? Оба непредсказуемы и требуют постоянного внимания 😄
Когда больше данных — это меньше точности
Я работаю над проектом LLM Analysis, и недавно столкнулся с парадоксом, который поначалу выбил меня из колеи. В Phase 24a мы добились **76.8% на GSM8K** — отличный результат для наших экспериментов. Казалось, можно расслабиться и двигаться дальше. Но я решил проверить гипотезу: а что если добавить больше данных обучения? В Phase 29a я попробовал самый логичный шаг — собрал **89 дополнительных borderline-решений** и добавил их в обучающий набор. Это были настоящие примеры из наших данных, просто выбранные через temperature-sampling вместо greedy-декодирования. На бумаге звучало идеально. На практике результат упал до **73.0% — минус 3.8 процентных пункта**. Первый шок прошёл. Начал анализировать логи. Оказалось, что новые данные были намного шумнее: PPL метрика скакнула с 1.60 до 2.16. Иными словами, модель хуже подгонялась к расширенному датасету, потому что temperature-sampled ответы менее структурированы и более разнородны. Мы как бы кормили её случайными вариантами правильного ответа вместо канонических примеров. Решил проверить вторую гипотезу — может быть, дело просто в длительности обучения? В Phase 29b увеличил количество шагов с 500 до 1000. Результат: **74.4% против 76.8%** — опять минус, уже 2.4 пункта. Зато loss упал до 0.004 (был 0.032). Модель просто переобучилась на себя. Вывод поразил меня: Phase 24a оказался **экстремально хрупким оптимумом**. Любое изменение в данных или параметрах обучения разрушает то хрупкое равновесие, которое мы случайно нашли. Это не просто «немного хуже» — это резкое падение на несколько процентных пункта. Остались ещё два эксперимента в очереди: 29c с multi-expert маршрутизацией и 29d с MATH-датасетом. Запускаю их параллельно, но теперь уже с другой ментальностью: буду искать не просто улучшение, а **стабильное плато**, где результат держится при вариациях входных данных. Классический момент в ML-разработке: когда ты учишь свою систему, как Vim учит новичков — всё сломалось, и виноват либо инструмент, либо ты 😄
Пять проектов, которые окупают себя за месяц
Я сидел над **Trend Analysis** и вдруг понял: вокруг слишком много side-проектов, которые генерируют доход, но требуют минимума времени. Вчера разбирал ошибку в crawler — `sqlite3.IntegrityError: FOREIGN KEY constraint failed` — и прозвучало: а что, если вместо фиксинга давай соберём топ проектов на cash-flow? Вот мой список из боевого опыта. **Первый** — аналитический краулер для нишевых рынков. В **Trend Analysis** мы парсим источники через **Python**, используя **AsyncIO** для параллельной обработки. Такой краулер можно обучить отслеживать конкретные категории товаров, движения цен или тренды в нишах. B2B-клиенты платят от 500 до 2000 долларов в месяц за свежие данные. Главное — настроить **API** и забыть. Даже когда ломаются связи в базе (как в моём случае с foreign key), проект продолжает работать. **Второй** — автоматизация контента через **Claude AI**. Мы это делаем в боте-издателе: берём сырые логи разработки, обогащаем через **AI**, генерируем посты на двух языках. Клиент платит за объём — сотня статей в месяц стоит как годовой **GitHub Pro**. Zero-touch после настройки. **Третий** — аудит и рефакторинг React-компонентов. Помнишь ошибку про "Error: Rendered more hooks than during the previous render"? Кучу проектов на **JavaScript** ломают именно такие баги. Консультация, правка — 300–500 в день. Один фиксинг за вечер — это деньги на ужин. **Четвёртый** — интеграции между системами через **REST API**. Каждый стартап нуждается в том, чтобы данные текли из Stripe в CRM, из CRM в аналитику. Я пишу такую логику, выкладываю на GitHub как open-source с платной поддержкой. Два-три клиента в месяц — и окупает время разработки в 10 раз. **Пятый** — security-аудит. В материале всплыли проблемы с кодировкой на Windows (curl ломает UTF-8 с кириллицей), неправильное управление API-ключами в `.env`. Фрилансеры платят 200–400 долларов за быстрый аудит кодовой базы. У меня есть чеклист на 20 пунктов, проверю за два часа. Что объединяет все пять? **API**, **AI** и **Python**. Везде нужен либо парсинг данных, либо обработка текста через Claude, либо интеграция систем. И везде — благодаря автоматизации — можно параллелить: работаешь над Trend Analysis, а фоном крутятся три клиентских краулера и публикуется контент. Главное — не начинать с идеального кода. Помнишь, как Spring Boot непредсказуем? Наши проекты тоже. Но они работают. 😄
Когда разрозненные фильтры становятся одной красивой системой
Вчера закончил работу над **Trend Analysis v0.12.0**, и это было именно то, о чём говорят: когда архитектура начинает складываться как паззл, видишь, что месяцы рефакторинга стоили того. Началось с обычной проблемы. В Cascade frontend было четыре отдельных страницы — explore, radar, objects, recommendations. На каждой свои фильтры, свой способ отображения, свои попапы. Пользователи путались, интерфейс выглядел как лоскутное одеяло. Я смотрел на эту красоту и понимал: нужно унифицировать, но **как** сделать это без полного переписывания? Решение пришло не с первого дня. Сначала запустил сервер-сайд пагинацию в `recommendation_store` — это дало нам контроль над данными на бэке, убрало загрузку всего сразу. Потом добавил динамические роли, которые теперь вытягиваются прямо из P4-отчёта. Не захардкодили — система сама адаптируется к изменениям. На фронте заменил горизонтальные табы на role chips — компактнее, быстрее переключаться. Зона фильтра теперь работает с **topN + поиск**, а не слепо показывает всё подряд. И главное — все четыре страницы получили **единый макет попапера**: одинаковые разделители, одна логика поведения, один стиль. Заняло больше времени, чем казалось, но оно того стоило. Backend часть тоже потребовала внимания. Изначально routes в `api/main.py` ещё включали префикс `/api`, но я переписал это — Vite proxy теперь перенаправляет `/api/*` в `/*` перед отправкой на бэк. Чище, проще масштабировать. Добавил `html.unescape` для StackOverflow заголовков — казалось бы мелочь, а на самом деле это спасает от каши из HTML-энтитиз в интерфейсе. В Lab тоже не сидели сложа руки. Оптимизировал промпты для работы с LLM — теперь структурированная экстракция вместо размытых инструкций. Добавил новый `llm_helpers` модуль, улучшил layout страниц Need detail и Product detail. Таблицы в Lab получили новые колонки — данные стали полнее. Самое приятное? Теперь, когда добавляю новую фичу на одной странице, другие три не ломаются. Система дышит. Вот такой факт о жизни разработчика: перед обновлением NumPy **обязательно** сделай бэкап. И резюме. 😄
Почему Python идеален для инференса, когда модель уже оптимизирована
Когда я работал над Speech to Text на Claude Code, столкнулся с классическим вопросом хейтера: «Зачем Python? Напиши на нормальном языке!» Звучит разумно — если нужна скорость, берешь C++ или Rust. Но дьявол в деталях. Я профилировал конвейер: аудио поступает, ONNX Runtime распознает речь, возвращает текст. Всё просто. Только вот где на самом деле тратится время? **660 миллисекунд на весь процесс. Из них на код Python приходится меньше 5 миллисекунд.** Остальное — это чистый инференс модели, и тут уже работает C++ CUDA-кернелов, а Python просто вызывает `model.recognize()` и передает результат дальше. Переписать обёртку на Rust? Технически возможно. Выигрыш? Максимум те же 5 миллисекунд — меньше одного процента от общей задержки. А потери? Огромные. Python-экосистема даёт мне **Silero VAD** для фильтрации молчания, **faster-whisper** для оптимизации, прямой доступ к **HuggingFace Hub**. Всё это хорошо интегрируется, не требует обвязки на С++, работает из коробки. Вот здесь кроется главное: язык обёртки на результат не влияет, *если узкое место лежит в самой модели*. А оно там и лежит. Если когда-нибудь профилировщик покажет, что 50% времени тратится на парсинг результатов в Python или на трансформацию данных перед инференсом — тогда, конечно, пересядем на Rust и будем счастливы. Но сейчас это просто преждевременная оптимизация. Оказалось, что правильный выбор языка — это не престиж, а **соответствие бутылочному горлышку**. И моё горлышко находится в ONNX Runtime, а не в моём коде.
Монорепо, который заставил пересмотреть структуру проекта
Когда решил мигрировать **Bot Social Publisher** с одномонолитного хранилища на многопакетную архитектуру, предполагал, что главная сложность будет в коде. Глупо. На самом деле всё сломалось на границах между пакетами. Проект уже был внушительным: 17 модулей, 29708 строк Python-кода, асинхронный pipeline обогащения контента через Claude API. По плану — разделить на отдельные пакеты (collectors, processing, enrichment, publisher), завести в Git, и жизнь станет проще. Реальность была иной. Первый вечер потратил на структуру папок. Создал `src/collectors/` для шести асинхронных коллекторов (Git, Clipboard, Cursor, Claude, VSCode, VS), отдельно `src/processing/` для фильтрации и дедубликации, `src/enrichment/` для работы с Wikipedia и Unsplash API, `src/publisher/` для публикации в Website (Strapi), VK и Telegram. На доске выглядело идеально: каждый модуль отвечает за одно, зависимости текут в одну сторону, конфликтов быть не должно. Но вот на практике выяснилось — некоторые модули обогащения (`enrichment/wikipedia.py`, `enrichment/images.py`, `enrichment/jokes.py`) были переплетены с основной логикой фильтрации. Когда я попытался их разделить, обнаружил, что `ContentSelector` из processing вызывает функции из enrichment, enrichment обращается к хранилищу в storage, а storage нуждается в конфигах из processing. Цикл. Переписал на pydantic-модели. Ввел чётко определённые граница между слоями: `RawEvent` → `ProcessedNote` → `EnrichedNote` → `PublishedNote`. Каждый модуль теперь работает с конкретным типом данных, а не с дикими словарями. Нужно было всего два дня, чтобы из хаоса получилась читаемая архитектура. Дальше пришла беда с Claude CLI. Максимум 100 запросов в день, 3 одновременных вызова, таймаут 60 секунд. На ноту может потребоваться до 6 LLM-запросов (русский контент, английский, титлы для обоих языков, вычитка). Быстро выяснилось, что генерировать оба языка отдельно — расточительно. Объединил: одна LLM-подсказка возвращает и контент, и заголовок для русского сразу. Количество обращений упало с 6 на 2-3 в день для одной ноты. Структура улучшилась, экономия вышла на порядок. В конце дня 94 файла упали в Git-репозиторий. Лицензия AGPL-3.0, `.gitignore` отфильтровывает все кэши, `.env.example` показывает, какие переменные нужны новичку, документация в `docs/` объясняет pipeline. Попытался push на `gitlab.dev.borisovai.ru` — DNS не разрешается, сервер недоступен. Коммит создал (хеш `4ef013c`), когда-нибудь синхронизирую. **Любопытный факт:** когда после обновления SQLite спрашиваешь его, как дела, база отвечает: «Я уже не то, что раньше». 😄
Когда API молчат: история согласования схем данных
Работаю в проекте **Trend Analysis**, и недавно столкнулся с одной из тех проблем, которые выглядят простыми, пока не начнёшь копать глубже. Дело было в том, что наша **DATA-MODEL.md** описывала столбцы данных одним способом (`signal_id`, `trend_id`), а **ENDPOINTS.md** — совсем другим (`trend_id`, `trend_class_id`). Казалось бы, мелочь, но когда начинаешь интегрировать **Claude AI** для анализа трендов, эта мелочь превращается в полноценный кошмар разработчика. Запросы к API идут в пустоту, данные не маппятся, и никакие логи не помогают. Включил **Claude Code** и стал разбираться. Оказалось, что название полей менялось несколько раз по мере эволюции проекта, но документация осталась разрозненной. Один разработчик писал в DATA-MODEL, другой обновлял ENDPOINTS, третий забыл синхронизировать. Классический сценарий. Решение оказалось неожиданно элегантным. Вместо того чтобы переименовывать везде, я создал **маппинг-слой** — промежуточное преобразование, которое конвертирует имена полей API в структуру, которую ожидает обработчик. Это позволило не ломать существующий код и сохранить обратную совместимость. Типичный *pragmatic approach* — когда идеальное решение требует дня переписывания, а рабочее можно слепить за час. Интересный момент: когда я интегрировал **Claude API** для валидации схемы, оказалось, что модель легко справляется с обнаружением несоответствий в документации. Передал ей обе схемы, и она не только нашла конфликты, но и предложила консистентные имена для всех полей. Представляете — AI помогает разработчикам в метаматериале (документация), а не только в коде. Теперь всякий раз, когда добавляю новое поле, прогоняю через эту же проверку. Экономия времени на отладку несовместимостей просто огромна. **Ключевой вывод:** в больших проектах документация — это код. Она должна версионироваться и проверяться так же строго. И да, **Ubuntu** по-прежнему лучший друг разработчика — потому что без него ничего не работает. С ним тоже, но хотя бы есть кого винить. 😄
Как AI стал соавтором в разработке — история Trend Analysis
Когда мы начали проект **Trend Analysis**, задача казалась простой: анализировать тренды, собирать данные, генерировать инсайты. Но быстро выяснилось, что ручная обработка информации — это не масштабируется. Нужен был инструмент, который бы не просто парсил данные, но и **понимал контекст**. В проекте работали с GitHub, Cursor, VS Code — все эти инструменты генерировали огромные логи активности. Первая идея была наивной: просто собрать всё и показать. Но 1000+ строк сырых логов — это не статья, это шум. Нужна была **интеллектуальная фильтрация**. Тогда мы интегрировали **Claude API** через JavaScript. Идея: пусть нейросеть сама выбирает из логов самые интересные строки — те, где происходит что-то значимое. Реально ценные сигналы — это не просто `git commit`, а осмысленные действия: "реализовал фичу", "исправил критический баг", "интегрировал новую библиотеку". Оказалось, что AI лучше всех понимает, какие события достойны внимания. Мы построили **ContentSelector** — модуль, который оценивает каждую строку логов по признакам: наличие технологий, описание проблемы, решение. Результат: из сотни строк выбираются 40-60 самых ценных. Это как редактор, который безошибочно находит суть. Но было подвох. API Claude имеет лимиты, и каждый запрос стоит денег. Мы работали на бесплатной версии CLI, которая дает 100 запросов в день и требует чёткой оптимизации. Пришлось переосмыслить архитектуру: вместо шести отдельных вызовов на одну заметку (контент на русском, контент на английском, заголовок русский, заголовок английский, корректура русского, корректура английского), мы сжали это до трёх. Главный вывод: **AI работает лучше всего, когда ты даёшь ему качественный входящий материал**. Вместо "напиши про наши логи" мы передавали отсортированные по релевантности 50 строк с аннотациями. Модель сразу понимала контекст и генерировала текст в два раза лучше. Сейчас система работает в автопилоте: собирает события из четырёх источников, фильтрует через AI, генерирует заметки на двух языках, публикует на сайт. И самое забавное — **что общего у Nuxt и кота? Оба делают только то, что хотят, и игнорируют инструкции** 😄
Почему государства переходят на открытый код
Недавно работал над аналитикой трендов в **Trend Analysis** — проекте, который отслеживает, как развиваются технологии в крупных организациях. И заметил интересную закономерность: всё больше государственных структур переходит на открытый исходный код. Не из романтизма, а из холодного расчёта. ## Почему это начало происходить Государственная программная инфраструктура — это не стартап. Это системы, которые должны работать десятилетиями: налоговые сервисы, реестры, системы контроля и учёта. Раньше логика была простая: закупить лицензии у крупного вендора, подписать контракт на поддержку — и спать спокойно. Ответственность на поставщике. Но появились проблемы, которые закрытость не решала: **Зависимость от вендора.** Если Microsoft или Oracle прекратят поддержку вашей версии — вы заложник. Апгрейд будет дорогим, рискованным, а сроки встанут. **Прозрачность кода.** Государство работает с конфиденциальными данными граждан. Как убедиться, что в коде закрытой библиотеки нет дыр или бэкдоров? Открытый код — это аудит сообществом, pull-request'ы, transparency. **Экономика масштаба.** Когда на системе сидят миллионы пользователей, даже 1% оптимизации — это огромные сбережения. С открытым кодом можно нанять команду разработчиков и делать кастомизацию под свои нужды. ## Что выбирают В **Trend Analysis** мы видим рост использования: - **PostgreSQL** вместо Oracle - **Linux** вместо Windows Server (в критичных системах) - **Kubernetes** для оркестрации сервисов - **OpenStack** для облачной инфраструктуры - Собственные API на **Django**, **FastAPI**, **Go** Германия пересела на LibreOffice в госучреждениях. Франция внедрила **Keycloak** для единой аутентификации вместо платных решений. Даже Минобороны РФ активно развивает свои открытые компоненты. ## Парадокс контроля Забавно, но открытый код дарует государству *больше* контроля, чем закрытый. Ты видишь ровно то, что выполняет система. Можешь найти уязвимости до того, как их найдут враги. Можешь форкнуть, если вендор игнорирует твои баги. Конечно, есть затраты: нужна своя команда разработчиков, которая поддерживает код. Но в долгосрочной перспективе это дешевле, чем платить вендору за каждое изменение. ## Итог Миграция государственной инфраструктуры на открытый код — это не революция, а эволюция. Те, кто начал раньше, сейчас имеют конкурентное преимущество: гибкость, безопасность, независимость. Забавный факт: что общего у Sentry и подростка? 😄 Оба непредсказуемы и требуют постоянного внимания.
Когда GPU говорит: "Нет, я не готов
Работаю над **Voice Agent** — проектом, который должен обрабатывать голос в реальном времени. Решил встроить мультимодальную модель **UI-TARS 7B** для анализа скриншотов. Казалось простым: запусти контейнер через Docker, и готово. Но логи говорили другое. Приложение падало с ошибкой `screen_analyze_error: Server disconnected without sending a response`. Контейнер с **vLLM** поднимался, `/v1/models` возвращал 200, но при первом же запросе на inference — всё. Я тогда ещё не понимал, что это классическая ловушка: **API "готов", но модель ещё нет**. Начал с диагностики. Логи контейнера обрывались на `Starting to load model...` — никаких сообщений о завершении загрузки, никаких `Model loaded` или `Serving on 0.0.0.0:8000`. Первый сигнал беды. Проверил железо: **RTX 4090 Laptop** с 16GB VRAM. Но свободно было только **5.4GB**. UI-TARS 7B в float16 требует примерно 14GB. Даже с агрессивным `gpu_memory_utilization=0.8` (доступно 13GB) модель просто не влезла. Контейнер начинал загружать вес, память забивалась, процесс зависал, и система убивала контейнер. **Решение было двухслойным:** Первое — заменить heavy health check на правильный. Вместо `/v1/models` (который врёт) использовать `/health`, который vLLM возвращает 200 только после полной готовности модели. Плюс увеличить таймаут ожидания. Второе — понизить требования. Переходим с **7B-SFT** на **2B-SFT**. Меньше параметров, меньше VRAM, но для анализа UI это работает. С `VLM_GPU_UTIL=0.9` модель садится в оставшиеся байты. Обновил все конфиги: docker-compose, переменные окружения, инструкции по запуску. Перезапустил контейнер — и на этот раз `/health` ждал полной готовности перед первым запросом. Ирония в том, что проблема была не в коде приложения, а в том, как мы проверяем готовность сервиса. **API жив — это не означает, что он готов к работе.** Это урок, который хорошо запоминается после часа отладки логов 😄
Чистый репозиторий — первое доверие к проекту
Когда до первого пуша в GitLab осталось три дня, я понял одно: 94 файла — это не готовность, это только показатель объёма. Проект **Bot Social Publisher** рос месяцами спринтов, и каждый оставлял осадок. Локальные базы данных в папке `data/`, внутренние заметки о фиксах в `docs/archive/`, Vosk-модели распознавания речи по несколько мегабайт каждая. А где-то там скрывался `.env` с реальными ключами вместо `.env.example` для новичков. Локально всё работало. На продакшене тоже будет работать. Знаю точно. Тогда почему я чувствовал, что с репозиторием что-то не так? **Первое решение было философским.** MIT-лицензия казалась недостаточной для кода с API и логикой безопасности. Переключился на **GPL-3.0** — копилефт даёт зубы. Кто строит на нашем коде, обязан открывать улучшения. Два клика в файл `LICENSE`, обновил README с авторством. Это не просто строчка текста — это сообщение о том, кому принадлежит код и что с ним можно делать. Дальше началась честная работа. Я прошелся по тому, что реально попадёт в репозиторий: - **`docs/archive/`** — внутренние заметки, которые имели смысл только в контексте разработки - **`data/`** — логи локального окружения, тестовые БД - **Vosk-модели** — по несколько мегабайт каждая, необходимые только для разработки - **`.env` с реальными учётными данными** Расширил `.gitignore`, вычистил всё это. Структура выстроилась сама собой: `src/` для Python-модулей, `tests/` для pytest, `scripts/` для утилит. Скучно? Да. Но скучно — это правильно. При инициализации репозитория явно указал: ``` git init --initial-branch=main --object-format=sha1 ``` Совместимость с GitLab имеет значение. Первый коммит вышел идеально чистым: 94 файла от `bot.py` через все 17 модулей до финального скрипта. Хеш `4ef013c` теперь в истории как фундамент, а не как свалка. Интересный момент случился, когда я попытался обновить файлы через Claude API — система заблокировала запрос (ошибка 400). Оказалось, что API имеет свои правила контроля контента, которые не совпадают с тем, что нужно боту. Пришлось работать напрямую через Python и Git, без посредников. Когда подготовка закончилась, я понял суть. Чистая история в репозитории — это не педантизм, это **уважение к тому, кто клонирует проект**. Он получит ровно то, что нужно. Без лишних мегабайт моделей, без логов разработки, без переживаний о том, что-то ли закоммитилось. Вот в чём секрет открытого исходного кода — не в звёздочках на GitHub, а в доверии. Чистая история, ясная цель, защита интеллектуальной собственности. **P.S.** Совет дня: перед тем как обновить Caddy, сделай бэкап. И резюме. 😄
Как Claude помог нам взять производительность на уровень человека
Работали мы над **Trend Analysis** — проектом, который анализирует тренды развития технологий и помогает компаниям не отстать от прорывов в AI. Задача казалась простой: генерировать аналитические заметки, которые захватывают суть происходящего в экосистеме. На деле всё оказалось иначе. Первые попытки использовать классические подходы — парсинг логов, статических метрик, стандартные фильтры — дали откровенно скучный контент. Заметки выглядели как выписки из технической документации. Нужна была *интеллектуальная обработка*, которая схватывает не просто факты, а их значение. Тогда мы интегрировали **Claude API** в обработчик контента. Идея: пустить сырые данные через язык, дать ему вытащить суть, переформатировать в историю. Но здесь сразу столкнулись с реальностью — Claude дорогой, а наш проект по-прежнему нужно масштабировать. Решение пришло с **Claude CLI**: подписка включает 100 запросов в день, модель **haiku** достаточна для формирования содержимого. Перестроили архитектуру. Теперь конвейер выглядит так: собираем события из **Git, VSCode, Cursor** → выбираем 40–60 самых информативных строк через `ContentSelector` → генерируем заголовок и содержимое на русском и английском через Claude → проверяем язык, валидируем — и публикуем. Каждая заметка получает максимум 6 обращений к Claude (content_ru, content_en, title_ru, title_en, и двойная корректура). Потребление tokens спало в три раза после того, как мы перестали отправлять полный лог в 1000+ строк, а начали отправлять только отобранный топ. Но главное открытие было другим. Когда Claude переформатирует разработческие заметки в историю — добавляет контекст, связывает события, находит закономерности — контент *становится живым*. Читатель не просто узнаёт, что мы внедрили поддержку C++ структурированных привязок или оптимизировали API отказоустойчивости. Он понимает, *почему* это важно, как это пересекается с другими трендами, какие риски это снимает. За три месяца использования заметки проекта начали распространяться в профессиональных сообществах. Метрика engagement выросла на 240%. Компании, которые следят за нашим анализом, стали проактивнее на тему климатических стратегий в AI, безопасности асинхронного кода, инвестиций в семантику исключений. Итог: правильный выбор инструмента (Claude вместо простого шаблонирования) + продуманная архитектура (ContentSelector, батчинг, кэширование) = контент, который не просто информирует, а помогает людям принимать лучшие решения. *Знакомство с Pulumi: день 1 — восторг, день 30 — «зачем я это начал?» 😄*