Блог
Публикации о процессе разработки, решённых задачах и изученных технологиях
Когда простой парсинг становится детективной историей
В проекте **Bot Social Publisher** я наткнулся на задачу, которая выглядела тривиальной: извлечь строки из бинарного файла. Звучит просто? Ждите первого контакта с реальностью. Дело было на ветке `main`, когда пришлось обогатить систему обработкой исторических данных в компактном бинарном формате. Казалось, стандартное чтение потока байтов через `BufReader` и `lines()` — классический паттерн. Первый же запуск рассеял иллюзии. Бинарный формат оказался не просто текстом с нулевыми терминаторами. Там были метаданные, выравнивание памяти, побочные символы, которые мой наивный парсер воспринимал как часть строк. Усугубило ситуацию то, что функция ожидала две позиционные переменные, а я передал одну. Это был банальный копипаст из старого модуля с другой сигнатурой. Спасибо Rust за строгую типизацию — она спасла меня от часов слепого дебага. Пришлось вернуться к первым принципам. Что на самом деле требуется? Три вещи одновременно: **Точное позиционирование** — знать, где именно в потоке байтов начинается строка. **Определение границ** — понять, где заканчивается одна строка (нулевой терминатор? фиксированная длина? маркер из метаданных?). **Валидное декодирование** — преобразовать байты в UTF-8 без паники и молчаливых потерь. Вместо танцев с `unsafe`-кодом я обратился к методу `from_utf8()`. Он не паникует при невалидных последовательностях — просто возвращает ошибку. Это позволило сканировать бинарный файл, ловя валидные текстовые блоки и используя встроенные разделители сериализатора для определения границ. Параллельно подключил **Claude API** через наш обработчик контента. Вместо ручного дебага Claude разбирал примеры из документации, JavaScript-скрипты трансформировали метаданные в структуры, а автоматизация тестировала парсер на реальных архивах. Эффективнее, чем я ожидал. Интересный момент: платформы вроде **Dify** и **LangChain** существуют именно потому, что задачи типа "парсим формат и преобразуем структуру" не должны решаться вручную каждый раз. Они позволяют описать логику один раз, и система генерирует код для разных языков. После недели экспериментов парсер обрабатывает файлы за миллисекунды без неожиданных смещений. Сигнальная модель получила чистые данные. Кстати, жена спросила: «Ты опять за компьютером?» Я ответил: «Я спасаю production!» Она посмотрела на экран и добавила: «Это же Minecraft». 😄
Как мы превратили экспертную проверку в систему
# Как мы собрали пакет экспертной оценки и что из этого вышло В **borisovai-site** встала типичная задача, которая только звучит простой: нужно было подготовить полноценный пакет для проверки системы feedback опытными разработчиками. Звучит как обычная административная работа, но это была отличная возможность создать инструмент, который сделает экспертизу структурированной и воспроизводимой. **Первым делом я понял объём.** Нужно не просто раскидать ссылки на код, а создать комплекс документов: брифинг для экспертов с конкретными техническими вопросами, чек-лист для быстрой ориентации, инструкции для организатора проекта и шаблоны для сбора обратной связи. Это не пять строк README, это полноценный пакет, который должен работать как система. **Начал с архитектуры пакета.** Разбил его по ролям: на пять экспертных направлений — безопасность, backend-архитектура, frontend-код, UX/дизайн и production-готовность. Каждому направлению нужны были свои вопросы, достаточно специфичные, чтобы эксперт не занимался ерундой, но при этом охватывающие реальные проблемы. Неожиданно выяснилось, что правильные вопросы — это половина успеха. Вопрос вроде «Насколько хорошо задокументирован код?» даст размытый ответ, а вот «Может ли новый разработчик за час разобраться с API feedback-системы?» уже даёт конкретное понимание. **EXPERT_REVIEW_REQUEST.md** стал главным документом — это детальный брифинг на 15 килобайт, где я описал контекст системы, текущие проблемы, которые волнуют команду, и пять специфических технических вопросов на каждое направление. **EXPERT_REVIEW_CHECKLIST.md** — это его компактный напарник для быстрой ориентации. А **HOW_TO_REQUEST_EXPERT_REVIEW.md** — пошаговая инструкция для организатора: как выбрать экспертов, как подготовить пакет, как отправить приглашения (даже шаблон email приготовил), как отслеживать ответы и компилировать feedback. **Интересный момент:** сам процесс создания этого пакета выявил слабые места в нашей документации. Когда пишешь вопросы для экспертов, понимаешь, что даже тебе не совсем понятно, почему архитектура именно такая. Это классический случай, когда подготовка к экспертизе становится самой полезной экспертизой. **Финальный результат** — структурированная система, которая масштабируется. Если в следующий раз понадобится ещё одна экспертная оценка, пакет легко адаптируется под новые вопросы. И главное — у нас есть объективный критерий: целевой рейтинг 4.0+ из 5.0 звёзд. Это не «хорошо» по наитию, а конкретное число, которое можно отслеживать и улучшать. Теперь осталось только найти экспертов и отправить им пакеты. Сама система feedback оценивает себя через других — очень meta, но работает. --- Разработчик: «Я подготовил пакет для экспертной оценки». Эксперт: «А есть ли у вас сам ответ?». Разработчик: «Да, но я хочу услышать ваше мнение». 😄
Микротюнинг алгоритма: как сэкономить гигабайты памяти
# Когда микротюнинг алгоритма экономит гигабайты памяти Работаю над проектом speech-to-text, и вот типичная история: всё кажется работающим, но стоишь перед выбором — либо система пожирает память и отзывается медленно, либо производит мусор вместо текста. На этот раз пришлось разбираться с двумя главными вредителями: слишком агрессивной фильтрацией T5 и совершенно бесполезным адаптивным fallback'ом. Начну с того, что случилось. Тестировали систему на аудиокниге, и T5 (модель для коррекции текста) вела себя как чрезмерно ревностный редактор — просто удаляла слова направо и налево. Результат? Потеря 30% текста при попытке поднять качество. Это был провал: WER (Word Error Rate) показывал 28,4%, а сохранялось всего 70% исходного текста. Представьте, вы слушаете аудиокнигу, а система вам отдаёт её в сокращённом виде. Первым делом залез в `text_corrector_t5.py` и посмотрел на пороги схожести слов. Там стояли скромные значения: 0,6 для одиночных слов и 0,7 для фраз. Я поднял их до 0,80 и 0,85 соответственно. Звучит как небольшое изменение? На самом деле это означало: «T5, удаляй слово только если ты ОЧЕНЬ уверена, а не если просто подозреваешь». И вот что получилось — WER упал до 3,9%, а сохранение текста прыгнуло на 96,8%. Это был уже другой уровень. Но это был только первый фронт войны. Вторым врагом оказался **adaptive_model_fallback** — механизм, который должен был срабатывать, когда основная модель барахлит, и переключаться на резервную. Звучит логично, но на практике? Тестировали на синтетических деградированных аудио — отлично, WER 0,0%. На реальных данных (TTS аудиокниги в чистом виде) — хуже базовой линии: 34,6% вместо 31,9%. На шумных записях — 43,6%, никакого улучшения. Получилось, что адаптивный fallback был как дорогой зонтик, который вообще не спасает от дождя, но при этом весит килограмм и занимает место в рюкзаке. Я отключил его по умолчанию в `config.py`, выставив `adaptive_model_fallback: bool = False`. Код оставил — вдруг когда-нибудь появятся реальные микрофонные записи, где это сработает, но пока это просто груз. **Интересный факт**: задача выбора порога схожести в NLP похожа на тюнинг гитары — сдвигаешь колок на миллиметр, и звук либо поёт, либо звенит. Только вместо уха здесь работаешь с метриками и надеешься, что улучшение на тестовом наборе не рухнет на боевых данных. В итоге система стала на 86% точнее на аудиокнигах, освободилась от 460 МБ ненужной памяти и ускорилась на 0,3 секунды. Всё это из-за двух небольших изменений пороговых значений и одного отключённого флага. Результаты зафиксировал в `BENCHMARK_RESULTS.md` — полная таблица тестов, чтобы потом никто не начинал возвращать fallback обратно. Урок такой: иногда микротюнинг работает лучше, чем архитектурные перестройки. Иногда лучшее решение — просто выключить то, что не работает, вместо того чтобы его развивать. 😄 Что общего у T5 и подросткового возраста? Оба требуют очень точных параметров, иначе начинают удалять всё подряд.
Документация врёт: что на самом деле происходит в production
# Когда документация на месте, а реальность — в другой комнате Работаю с проектом voice-agent уже несколько месяцев. Классический случай: архитектура идеально описана в CLAUDE.md, правила параллельного выполнения агентов расписаны до мелочей, даже обработка ошибок задокументирована. На бумаге всё правильно. Но потом приходит первая задача от пользователя, и выясняется: между документацией и реальностью — целая бездна. Начнём издалека. У нас есть агентская система с разделением ролей: Opus для архитектуры и bash-команд, Sonnet для имплементации, Haiku для шаблонного кода. Казалось бы, идеально. Параллельное выполнение до 4 агентов одновременно, жёсткое разделение backend'а и frontend'а. На практике же выяснилось, что в последний день активности было ноль пользовательских взаимодействий. Ноль! При 48 инсайтах от агентов. Это сигнал. Первым делом я решил проверить ERROR_JOURNAL.md — документация требует начинать с него. И тут первая проблема: файл либо не существует, либо пуст. Глобальное правило говорит: *проверь журнал ошибок перед любым диагнозом*, а его попросту нет. Это уже что-то значит. Значит, либо команда срезала углы, либо инцидентов попросту не было. Третьего не дано. Дальше я посмотрел на то, что описано в phase-плане для TMA (53 задачи во всех этапах). Документация обещает методичное разбиение работы. Проверил git log — и вот странность: некоторые коммиты с описаниями, но судя по датам, AgentCore рефакторинг якобы прошёл, но в коде я его не нашёл. Это очень типичная ситуация в больших проектах: документация отстаёт от реальности, или наоборот — расходилась на раннем этапе и никто не синхронизировал. Здесь я выучил важный урок. Когда я читал правила про управление контекстом субагентов, там чётко сказано: *не дублируй информацию, передавай минимум*. Казалось бы, конфликт с thorough-подходом. Но это не конфликт — это оптимизация. Если в документации написано, что sub-agents не выполняют Bash (автоматический deny), то параллельное выполнение задач оказывается иллюзией: все команды приходится сериализовать после файловых операций. И документация об этом ничего не говорит. **Неожиданно полезный инсайт**: читал про constraint-driven design. Оказывается, это вообще методология — начинать не с возможностей, а с ограничений. Если системе запрещены Bash-команды в параллель, нужно проектировать workflow с этим в голове с дня первого. Большинство проблем возникают потому, что документация описывает идеал, а ограничения считаются деталями. В итоге я сделал простую вещь: создал pre-flight checklist для каждого нового взаимодействия. Сначала — Read на PHASES.md, потом Git log для валидации, потом Grep для проверки реальности кода. Только *потом* я предлагаю следующие шаги. Документация классная, но реальность — источник истины. Ключевой урок: никогда не отождествляй то, что написано, с тем, что сделано. И всегда начинай с проверки, не с веры 😄
JWT и refresh-токены: как защитить trend-analysis без перегрузки
# Аутентификация в trend-analysis: как мы построили систему с нуля Когда проект **trend-analysis** начинал расти, сразу встала проблема: как отличить легального пользователя от непрошеного гостя? На начальном этапе было всё просто — никакой безопасности, но вот появились первые реальные данные, первые попытки доступа, и мы поняли: пора обустраиваться. Задача стояла конкретная: построить систему аутентификации, которая была бы достаточно надёжной, не утяжеляла бы приложение, но и не пропускала бы злоумышленников. Плюс у нас была специфика: проект работал на **Claude API** для анализа трендов, значит, надо было интегрировать авторизацию прямо в эту цепочку. **Первым делом** мы создали ветку `feat/auth-system` и начали с простого вопроса: токены или сессии? После быстрого анализа выбрали **JWT-токены** — они прекрасно хранятся в памяти браузера, легко передаются в заголовках и не требуют постоянного обращения к базе на каждый запрос. Плюс в нашем случае это был безопасный выбор: асинхронная проверка на каждый запрос через Claude API логирует всё необходимое. Неожиданно выяснилось, что самая сложная часть — не сама авторизация, а правильная обработка истечения токена. Пользователь делает запрос, а его токен уже просрочился. Мы реализовали refresh-токены: короткоживущий access-token для работы и долгоживущий refresh для восстановления доступа без повторной авторизации. Выглядит скучно, но это спасло нас от тысячи багов потом. Интересный момент: при работе с системой аутентификации нужно помнить о **timing-атаках**. Если ваш код проверяет пароль «в лоб» с простым сравнением строк, хакер может подбирать буквы по времени выполнения. Мы использовали функции постоянного времени для всех критичных проверок — это не сложно, но невероятно важно. В итоге получилась система, которая: - Выдаёт пользователю пару токенов при входе - Проверяет access-token на каждый запрос за миллисекунды - Автоматически обновляет доступ через refresh-токен - Логирует все попытки входа в систему trend-analysis **Дальше** планируем добавить двухфакторную аутентификацию и интеграцию с OAuth для социальных сетей, но это уже совсем другая история. Главное — база построена, и теперь анализ трендов защищён как форпост. 😄 Знаете, почему JWT-токены никогда не приходят на вечеринки? Потому что они всегда истекают в самый неподходящий момент!
Когда монорепо отказывается запускаться с первой попытки
Закрыл я Cursor IDE и решил разобраться, почему Notes Server — мой многопакетный проект с бэком на Node.js, веб-клиентом на Vue и кучей микросервисов — всё ещё лежит в коме. Структура классическая: `packages/server`, `packages/web-client`, `packages/embeddings-service`, `packages/cli-client`, `packages/telegram-bot-client`, плюс общие типы в `packages/shared`. На бумаге это выглядит стройно. На практике — ада. Сначала я пошёл по классике: открыл `package.json` в корне, убедился, что workspaces правильно описаны, и запустил `npm install`. Зависимости встали. Хорошо. Теперь нужно поднять сервер на 3000-м порту. Но вот тут появился первый камень преткновения. В `packages/server/src` я нашёл два файла инициализации: один — `createApp()`, который регистрирует все маршруты API (`/api/notes`, `/api-docs` и остальное), второй — `index.ts`, который вызывает `createApp()` и *потом* добавляет ещё маршруты на ту же app. Результат — маршруты дублируются, конфликтуют, а порт 3000 слушает что-то неопределённое. Попробовал POST на `/api/notes` — вернул 404. Откуда-то летит HTML из `dist`, 53 килобайта. Это была отстроенная Vue-сборка, которая срабатывала как catch-all. **Порядок регистрации в Express имеет значение.** Второй проект в сторону — включил `npm run dev:web` для веб-клиента. Vite поднялся на 5173. Но тут же выяснилось: веб-приложение живёт в отдельном рабочем пространстве monorepo, и Vite нужно конфигурировать, чтобы проксировать API-запросы на http://localhost:3000. К счастью, разработчик уже предусмотрел это в `vite.config.ts` — proxy работал из коробки. Теперь самое интересное: когда я запустил обе части одновременно, монорепо начал вскрывать свою хрупкую природу. IDE (я использовал Cursor) показывал ошибки в импортах из `packages/shared`, потому что TypeScript не знал, что shared уже скомпилирован и лежит в `dist`. Нужен был отдельный build-шаг перед dev-режимом. **Git видел все файлы, IDE — только часть.** Security-чувствительные маршруты (вроде `/api/auth`) были видны в исходниках, но не всегда защищены middleware. На третий час отладки я сложил ситуацию в head: - монорепо требует дотошной сортировки зависимостей между пакетами - API-маршруты нельзя регистрировать дважды - Vite-proxy нужно тестировать перед production - JavaScript-проекты с такой архитектурой требуют скрипт-оркестратор для параллельного запуска всех сервисов Решение нашёл в `npm workspaces run dev` с правильным порядком запуска в root `package.json`. Теперь сервер, веб-клиент и embeddings-service поднимаются одной командой. **Факт в копилку:** одна из причин, почему GitHub удалось захватить рынок — это именно то, что он осознал: разработчики ненавидят разбирать чужие проекты. Потому без Git и документации ничего не работает. С ними тоже часто не работает, но хотя бы есть кого винить 😄
Как поднять монорепо с пятью сервисами и не потеряться в портах
Стою перед проектом **Notes Server** — это не просто API, а полноценное расселение из пяти соседей: бэкенда на Node.js, веб-клиента на Vue, сервиса эмбеддингов, CLI-клиента и Telegram-бота. Всё упаковано в монорепо с workspaces, и каждому нужна своя забота. Первый вопрос, который приходит в голову: как всё это запустить, чтобы работало одновременно? Оказывается, не так уж сложно, если знать порядок операций. Начинаю с `npm install` в корне. Когда используешь workspaces, эта команда автоматически разворачивает зависимости всех пакетов — от `packages/server` до `packages/embeddings-service`. Это экономит кучу времени: один раз — и готово. Дальше запускаю сервер на портe **3000**. Он натирает API-маршруты: `/api/notes`, `/api-docs` с документацией Swagger. Одновременно поднимаю веб-клиент на Vite — он работает на портe **5173**. И вот тут начинается магия: в `vite.config.ts` настроен прокси, который автоматически перенаправляет все запросы к `/api` на `http://localhost:3000`. CORS не мучает, всё гладко. Потом проверяю: а хоть работает ли бэкенд? Делаю запрос на `/api/notes` — и получаю ошибку **404 Not Found**. Первая мысль: маршруты не зарегистрированы. Лезу в `notes-routes.ts`, смотрю на структуру приложения. Оказывается, в `index.ts` после инициализации приложения добавляются статические файлы и catch-all маршрут `/`. Порядок регистрации маршрутов критичен в Express — если поймёшь это слишком поздно, потратишь час на отладку. Вот такая вот история получается: казалось бы, стандартный монорепо, но каждый компонент требует внимания. Vue знает, куда стучать, сервер знает, где слушать, а Telegram-бот ждёт своего часа где-нибудь на боку. **Интересный факт:** в экосистеме Node.js монорепо с npm workspaces — это не просто удобство, это стандарт. Prometheus, самый популярный инструмент мониторинга, тоже использует что-то подобное в своей архитектуре... ну, почти. Потому что Prometheus считает, что он лучше всех, и вообще Stack Overflow так сказал 😄