Read in:
Русский

Дороги, которые мы не выбрали

Фичи, которые мы построили и удалили — или посчитали и не стали строить. С числами, которые решили.

Стихотворение Фроста про две дороги обычно читают как гимн смелому выбору. У самого Фроста дороги были «примерно одинаковы», а «менее исхоженная» — красивая история, которую путник сочиняет потом, со вздохом. Здесь без вздохов. Эти дороги — не героические развилки: мы по ним прошли, померили и вернулись, обычно в тот же день. Не «дорога, которой шли немногие». Просто дорога, которую попробовали и оставили.

Страницы продуктов обычно перечисляют, что построено. Эта страница перечисляет, что построено и удалено — или оценено и отвергнуто. Не потому, что идеи глупые: почти все они — рекомендации из учебника. А потому, что мы померили их на нашей системе, и числа сказали «нет». Платформа, которая просит доверить ей ваши заметки, должна показывать, как принимает решения. Включая отмены.

Реранкер, который ухудшил поиск

Каждый учебник по RAG говорит одно: после поиска добавь cross-encoder-реранкер, это «главный рычаг качества». Мы добавили: bge-reranker-v2-m3 в отдельном процессе, пересортировка топа перед выдачей.

Поиск стал хуже. Померили дважды. На пассажах по 512 символов качество ранжирования (nDCG) упало с 0.92 до 0.89. На целых заметках — рухнуло до 0.39: заметки не влезали в окно реранкера и выглядели для него одинаково. Механизм поломки поучителен: cross-encoder награждает совпадение слов на поверхности. На запрос про медленную ферментацию теста он поднял заметку про квашеную капусту выше заметки про закваску — в капусте просто больше слов про ферментацию. Первая ступень, гибрид ключевых слов и векторов, уже ранжировала это правильно. Реранкер её работу отменил.

Сначала мы выключили его по умолчанию, потом удалили целиком — вместе с Python-процессом. Одним процессом меньше, одной моделью меньше. Отрицательный результат остался в документации, а бенчмарк, который его поймал, описан отдельно. Урок не «реранкеры плохие». Урок: вторая ступень — рычаг, только когда первая слабая. Наша слабой не была. Меряйте до релиза, даже когда учебник разрешает не мерить.

Трюк с полнотой, который её отравил

Тот же бенчмарк, та же неделя. Наш поиск по ключевым словам требует совпадения каждого слова запроса. Напрашивается улучшение: если результатов мало, повторить с «любое слово подходит» и слить — бесплатный прирост полноты.

Качество ранжирования упало на четверть: nDCG 0.93 → 0.67. Механизм: слияние ранжирует по позиции, а не по score. Двадцать рыхлых совпадений по одному слову вошли в слияние с сильными позициями и вытолкнули документы, которые векторный поиск нашёл правильно. Показательно: топ-1 выжил — сгнило всё, что ниже. Ровно тот вид ущерба, который без бенчмарка не замечаешь. Откатили байт в байт в тот же день.

В гибридной системе у двух каналов разные работы. Вектор несёт полноту, ключевые слова — точность. Канал ключевых слов, который молчит на семантических запросах — возвращает ничего вместо шума, — оказался несущей конструкцией.

Векторная база, которая решала не нашу проблему

В какой-то момент каждому проекту с эмбеддингами советуют «настоящую векторную базу». Мы сравнили Qdrant с нашим полным перебором в памяти.

Результаты совпали байт в байт: тот же топ-1, тот же топ-5, на каждом тестовом запросе. Qdrant решает проблему скорости, которой у нас на этом масштабе нет: эмбеддинги уже в памяти, перебор — не узкое место. А добавил бы Qdrant вот что: контейнер, пайплайн синхронизации и новый способ поиску упасть. Каждый пункт — точка отказа в продукте, чей питч — «один процесс на одном файле».

Настоящим рычагом качества оказалась модель эмбеддингов, а не хранилище. Мы обновили модель и оставили глупый перебор. (Забавная сноска: узел Qdrant сегодня всё-таки живёт в нашем публичном хабе — как внешний узел федерации, индексирующий Telegram-каналы. Он доказывает, что протокол не привязан к trip2g. Правильный инструмент — на правильном слое.)

Канбан, который дважды переехал

Мы хотели канбан-доску, где карточка — строка в заметке. Первая версия: компонент на нашем фронтенд-фреймворке, внутри основного репозитория. Потом переписали как самодостаточный React-шаблон. Потом удалили и его — и выпустили отдельным репозиторием, который ставится одной командой curl.

Каждый переезд — одно и то же осознание на новой глубине: доска — это шаблон, а не фича платформы. Она рендерит заметку; работа платформы заканчивается на «отдай заметку и прими правку». Пока доска жила в ядре, каждое её улучшение требовало релиза платформы. Как отдельный шаблон она версионируется, ставится и форкается сама — и заодно доказала, что система layout'ов выдерживает настоящее приложение, о котором ядро не знает.

Общее правило, которое мы отсюда вынесли: если фича может жить поверх платформы — она обязана жить поверх. Ядро остаётся маленьким, двери множатся.

Бот за $2000 до первого сообщения

Публиковать заметки в Telegram с кастомными эмодзи выглядело просто: Bot API их поддерживает. Потом мы прочитали мелкий шрифт. Бот может отправлять кастомные эмодзи, только если к нему привязан username с Fragment — а это порядка 1000 TON, примерно $2000, до первого сообщения.

Мы пошли другим путём: публикация через настоящий пользовательский аккаунт (MTProto) с обычной подпиской Telegram Premium — около $5 в месяц, платит владелец канала. Те же кастомные эмодзи, плюс длинные подписи к медиа, которые Bot API тоже режет. Бесплатный бот остался вариантом по умолчанию; userbot — опциональный премиум-путь.

Здесь не понадобился даже бенчмарк — хватило арифметики. Не все стены технические.

Мелкие расставания

Не каждая отмена заслуживает главы. По строчке на каждую — потому что суть в паттерне:

  • Нагрузочный стенд (k6): удалили, когда бенчмарки, ради которых он существовал, были сделаны и описаны. У инструмента работа может закончиться.
  • Python-скрипт предпросмотра layout'ов: заменили Node-наблюдателем без зависимостей. Меньше рантаймов ради одного цикла предпросмотра.
  • Смена конвенции: обёртки sql.Null* заменили обычными указателями Go по всей кодовой базе. Удалить целый слой конвертации оказалось лучше, чем хранить привычку.
  • Два встроенных rich-text-редактора: удалили как легаси. Контракт платформы — markdown-файлы; содержать WYSIWYG-редактор никогда не было нашей работой.

О чём говорит куча мёртвого кода

Если перечитать список, всю зачистку сделали три правила.

Мерь на своей системе, а не по литературе. Реранкер и трюк с полнотой — лучшие практики из учебника. Обе сделали нашу систему хуже — так, что увидел только бенчмарк.

Не добавляй движущуюся часть под проблему, которой нет. Qdrant, Python-процесс, лишние рантаймы — всё удалено по одной причине: продукт работает как один бинарник на одном файле.

Держи ядро меньше, чем хочется. Канбан-доска, редакторы, инструмент предпросмотра — всё, что могло жить поверх платформы, вытолкнуто поверх. Даже то, что мы уже успели построить внутри.

Ни от чего из этого не отказывались легко: почти везде был рабочий код и зелёные тесты. Это и стоит записать. Дорогая дисциплина — не строить. Дорогая дисциплина — удалить работающее, потому что числа, арифметика или архитектура сказали: оно здесь лишнее.