← Главная
~/vkvstudio/build.log в эфире

Журнал сборки

Как я собирал этот AI-продукт, решение за решением. Не туториал. Судовой журнал: что выбрал, от чего отказался и почему.

Этот лог я веду в основном для себя. Тут причины, почему я собрал всё именно так, решение за решением, чтобы будущий я помнил, зачем сделал то, что сделал.

Коротко про меня: почти двадцать лет делаю вещи, которые должны реально работать. Сначала предметная съёмка, потом маркетинг на заводах, потом сайты, теперь вот это, AI. Работы разные, зуд один. Смотрю на систему и хочу понять, где она сломается.

Написано так, как я на самом деле думаю, вслух, разбирая решения, которые пришлось принять, те, что угадал, и пару, что сначала завалил. Я держал это честным, а не причёсанным. Если кому-то ещё пригодится, тем лучше.

Чему я тихо рад: всё это сложилось внутри инструментов Google. Писал в Antigravity, их агентной IDE с сильными встроенными моделями, включая Claude. Картинки шли через Gemini и Nano Banana. Сам продукт работает на Gemma 4, Google Cloud AI, родном звуке Gemma и Chirp 3. Я не задумывал это как тему. Каждый раз, когда я тянулся за лучшим инструментом под конкретную задачу, он оказывался из одного и того же места, и в какой-то момент я перестал этому удивляться.

  1. 01 why-astro

    Почему Astro (и почему острова)

    Хотел, чтобы сайт ощущался мгновенным и при этом тянул тяжёлые AI-инструменты. Эти две вещи обычно дерутся.

    Что было на столе

    Три пути. Одностраничное приложение, которое сразу вываливает на тебя гору JavaScript. Обычный статический генератор, быстрый, но неудобный в ту же секунду, когда нужна интерактивность. Или Astro, который по умолчанию отдаёт статический HTML и даёт добавлять интерактив только там, где он реально нужен.

    Что выбрал

    Astro. Статика. На Cloudflare Pages.

    Почему

    Большая часть сайта это просто текст. Незачем грузить целый фреймворк, чтобы показать абзац. Поэтому Astro по умолчанию отдаёт ноль JavaScript, а я включаю его по одному острову: токенайзер, инструмент промптов, вот этот терминал. Каждый оживает сам, когда ты до него доскроллил или когда у браузера есть свободная секунда.

    Больше всего мне нравится тихая деталь. Сайту не нужен AI-бэкенд, чтобы собраться. Если сервер лежит, ассистент просто переключается на заглушку, и всё равно всё выкладывается. Ни одна часть не утянет за собой всё остальное, и, честно, я уже не могу строить иначе.

    Что это дало

    Быстрые страницы, которые остаются лёгкими, а тяжёлое грузится лениво и только там, где посетитель правда попросил.

  2. 02 why-svelte

    Почему Svelte 5 для островов

    Когда решил делать острова, надо было выбрать, на чём.

    Что было на столе

    Острова на React или Vue, чистый код руками, или Svelte 5 с новыми рунами.

    Что выбрал

    Svelte 5. Руны, без сторов.

    Почему

    Svelte по сути компилирует себя в ничто. В браузер уезжает код, близкий к тому, что ты написал бы руками, поэтому каждый остров остаётся крошечным, а это важно, когда их четыре или пять. Руны ($state, $derived, $effect) дают реактивность без виртуального DOM.

    Одну ловушку я теперь уважаю: никогда не читай и не пиши одну и ту же переменную состояния внутри одного эффекта. y++ читает y и пишет его одним движением, и всё, бесконечный цикл. Я выяснил это неприятным способом на токенайзере. Теперь это правило.

    Что это дало

    Токенайзер, инструмент промптов, исследователь эмбеддингов, этот терминал, всё это отдельные маленькие острова. Ни один не знает про другие и не тормозит их.

  3. 03 why-vanilla-css

    Почему обычный CSS, без Tailwind

    На сайте, вся суть которого ремесло, халтурить со стилями я не собирался.

    Что было на столе

    Utility-фреймворк вроде Tailwind, какой-нибудь CSS-in-JS, или обычный CSS на дизайн-токенах.

    Что выбрал

    Обычный CSS и около восьмидесяти кастомных свойств. Никакого фреймворка стилей.

    Почему

    Хочу знать каждый байт, который уезжает в браузер, и Lighthouse 100 для меня пол, а не приз. Токены дают одно место, где меняются цвет, отступы, шрифт, движение, поэтому весь сайт говорит на одном языке и ничего не захардкожено. Компоненты смотрят на токены, а не на сырые значения. И слово important я в CSS не пускаю, вообще.

    У этого вида есть имя в моей голове, обсидиан и неон. Тёмная графитовая база, зелёный акцент, глубокий синий как второй голос. Он держится, потому что живёт в нескольких переменных, а не в сотне разбросанных решений.

    Что это дало

    Маленький бюджет CSS, одна цельная система и сайт, который я перекрашиваю, тронув токены, а не роясь по файлам. И вся эта штука по-прежнему выдаёт ровные 100 по всему Lighthouse.

  4. 04 why-gsap-lenis

    Почему GSAP и Lenis для движения

    Движение здесь должно ощущаться как живое, а не как слайд-шоу.

    Что было на столе

    Опереться целиком на нативную CSS-анимацию скролла, или взять JavaScript-библиотеки.

    Что выбрал

    И то, и другое, слоями. Нативный CSS для простых появлений, GSAP со ScrollTrigger для таймлайнов, пиннинга и скраба видео, Lenis для плавного скролла в основе. И всё это под prefers-reduced-motion.

    Почему

    Нативная CSS-анимация отличная и дешёвая для фейда или лёгкого параллакса, поэтому беру её, где подходит. Но герой, где видео скрабится покадрово по мере скролла, и мозг, который перетекает в слово Synapse, требуют настоящих таймлайнов и точного контроля. Это GSAP. Lenis просто связывает скролл, чтобы всё шло по одним часам.

    Что это дало

    То, что людям запоминается: герой, который скрабишь скроллом, мозг, который становится ассистентом, и навигация, собранная как маленькое нейронное созвездие вместо меню. И всё это отходит в сторону, если ты попросил меньше движения.

  5. 05 why-gemma-qlora

    Почему локальная Gemma 4, обученная через QLoRA

    Ассистент Synapse это сердце всего. Где он работает, решило многое.

    Что было на столе

    Арендовать облачный API и промптами загнать его в характер, или запустить свою модель и правда научить её, кто она.

    Что выбрал

    Gemma 4 от Google: маленькая E2B для быстрого чата, крупная мультимодальная E4B для тяжёлого, обученные через QLoRA на моей GPU.

    Почему

    Маленькие модели на своём железе, ужатые квантованием, это та часть темы, в которую я сейчас больше всего влез. Запускать самому значит настоящий контроль, и данные не уходят третьей стороне, остаются на моей машине. А QLoRA это трюк, который делает всё реальным: получаешь почти всё качество полного дообучения за долю стоимости и памяти, поэтому хватает одной GPU на 12 ГБ.

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

    Что это дало

    Квантованная Q8-модель, которую я отдаю сам, около 4.6 ГБ, обученная примерно на 2900 собранных вручную примерах. Голос и характер, каких у безликого облачного эндпоинта из коробки просто нет.

  6. 06 lora-behavior-rag-facts

    LoRA для поведения, RAG для фактов

    Это я не планировал. Меня научили сами прогоны обучения.

    Что было на столе

    Запихать всё сразу, и характер, и свежие факты, в дообучение. Или разделить работу надвое.

    Что выбрал

    LoRA лепит поведение. Поиск подаёт факты. Два инструмента, две задачи.

    Почему

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

    Поэтому я учу её говорить «не уверен, проверь источник», а слой поиска достаёт актуальные факты и подаёт их в момент ответа, отранжированные по релевантности. Поведение в весах, факты в индексе, который я могу освежить. Когда увидел это так, развидеть уже нельзя, и, кажется, к той же форме тихо идёт каждая серьёзная система.

    Что это дало

    Ассистент, который держит характер и держит честность, где я могу освежить то, что он знает, обновив индекс, а не переобучая что-либо. На батарее из 112 промптов маленькая модель отбила все 23 попытки социальной инженерии, и вот тогда я понял, что честность реально прикипела.

  7. 07 bigger-model-worse

    Когда большая модель оказалась хуже маленькой

    Эта запись та, которой я в моменте гордился меньше всего, а сейчас рад больше всего. Маленькая модель обошла большую, и мне пришлось выяснить, почему.

    Я обучил два размера на одних данных: маленькую E2B и крупную мультимодальную E4B. По логике крупная должна выигрывать. Не выиграла. На той же батарее из 112 промптов маленькая оказалась явно лучше: устояла на проверках безопасности, ровнее держала факты и не выпадала из характера. Крупная растекалась, больше выдумывала и временами сливала собственный промпт.

    Стал копать. Оказалось, две отдельные проблемы, сплетённые вместе. Первая, баг инференса: экспортированная модель несла неверный токен конца хода, поэтому на длинных ответах не понимала, где остановиться, и уходила в петлю. Маленькая это стерпела, крупная нет. Вторая, настоящая: того же объёма обучения, что сформировал 2B, крупной 8B просто не хватило. Я взял слишком низкий ранг LoRA для большой модели, поэтому характер не прикипел, и сквозь него полезли общие повадки базовой модели. Доля выдуманных фактов сказала прямо: около 20% у маленькой, 50% у крупной.

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

    Ещё одно с того периода. Машина, на которой я обучаю, нестабильна, прогон падал или зависал десятки раз за ночь. Поэтому обучение сидело под маленьким супервизором, который следил за падением или замёрзшим логом, убивал застрявший процесс и запускал заново с последнего чекпоинта, пока не дойдёт. Модель обучилась, потому что то, что её обучало, умело подниматься само. Я к этому постоянно возвращаюсь.

  8. 08 native-audio-chirp

    Слух через родной звук Gemma, голос через Chirp 3

    Голос это место, где мультимодальная модель наконец отработала своё место в стеке.

    Что было на столе

    Прикрутить к пайплайну отдельный движок распознавания речи, или использовать уши, которые у модели уже есть.

    Что выбрал

    Дать Gemma слушать своим родным звуком, а голос отдавать через Google Chirp 3 на Google Cloud AI.

    Почему

    Крупная E4B и так держит текст, картинки и звук в одних весах, поэтому аудио-энкодер прямо там. Использовать его значит убрать из стека целый компонент и опереться до конца на модель, которую я уже выбрал. На чистой речи держится нормально, с простым правилом в тридцать секунд на кусок, чтобы вход не вылезал за рамки.

    Говорить в ответ это по природе другая работа. Языковая модель делает текст, а не звук, поэтому голос наружу это всегда отдельный шаг. Chirp 3 делает эту половину. Уши сложены в модель, рот отдан отдельному сервису Google.

    Что это дало

    На одну движущуюся деталь меньше, история чище, и голосовая петля целиком остаётся внутри экосистемы Google, от моего стола до продакшена. Честно, эта часть ещё в движении: она стоит на той же E4B, которую я переучиваю парой записей выше, поэтому слух встанет как надо, когда та модель вернётся. Направление задано, сроки нет.

  9. 09 the-three-lab-tools

    Зачем на самом деле нужны три инструмента Лаборатории

    Лаборатория это не список фич. Каждый инструмент отвечает на вопрос, который у инженера правда есть.

    Профайлер токенизации

    Как модель видит твой текст. Он разбивает ввод на токены, рисует тепловую карту их длин и ставит вендоров рядом, так что видно, почему одна и та же русская фраза стоит дороже английского близнеца. Под капотом три движка: tiktoken в браузере, локальные файлы словарей для открытых моделей и точный подсчёт через бэкенд для закрытых.

    Prompt Architect

    Промпты из ролевых блоков, где бюджет токенов и стоимость в долларах пересчитываются вживую по десятку моделей, пока печатаешь, плюс мета-генератор, который набрасывает промпт через бэкенд на Gemma. По сути это про дисциплину: видеть контекст и цену до того, как их потратил.

    Пространство эмбеддингов

    Эмбеддинги, посчитанные на твоей же машине, брошенные в 3D-облако, сквозь которое можно пролететь, а сверху живой поиск по смыслу, чанкинг и гибридный reranking. Это идея RAG, сделанная видимой: та же машинерия, что подаёт ассистенту факты, разложена так, чтобы можно было смотреть, как она работает.

  10. 10 why-a-nervous-system

    Почему это похоже на нервную систему

    Вся нейронная тема тут не просто так, и причину я обычно вслух не говорю.

    Синапс это маленький зазор, где один нейрон передаёт сигнал следующему, место, где нервная система соединяется или нет. Я назвал ассистента Synapse и вырастил навигацию как сеть узлов, потому что, ну, я мыслю нервными системами. У меня на то есть причины.

    У меня рассеянный склероз. Большинство инженеров относятся к автономности и отказоустойчивости как к хорошему тону, как к галочке. Для меня это вся суть. Я строю софт так, как мне нужно, чтобы работало всё вокруг: связно, с самовосстановлением, и стояло в те дни, когда его создателя нет в строю. Это не слабость, которую я обхожу. Это самое острое чутьё, что у меня есть.

    Даже то, как я обучал модель, шло по тому же правилу. Машина всё падала, а прогон всё поднимался сам с последнего чекпоинта, пока не дошёл. Я не задумывал эту рифму, просто заметил её потом, и она осталась со мной.

    Поэтому это и не хобби. Автоматизировать рутину, свою и чужую, значит вернуть дефицитное туда, где ему место, внимание, тем, кому оно нужнее, а мне уже во вторую. Я не очень умею говорить это так, чтобы не прозвучало больше, чем есть, поэтому просто оставлю здесь и вернусь к работе.

  11. 11 retrain-didnt-fix-facts

    Переобучение, которое не вылечило враньё

    Двумя записями выше я сказал, что лечение большой модели это больше ёмкости и правильный стоп-токен. Я поставил этот эксперимент. Половина сработала. Та половина, ради которой всё затевалось, нет.

    Я переобучил оба размера на большем и более чистом датасете, теперь почти пять тысяч проверенных вручную примеров, и собрал им настоящий экзамен: 362 промпта, ловушки на выдуманные факты, жёсткие вопросы по всем стекам, куча попыток мошенничества и джейлбрейков, всё оценено независимой коллегией судей. Фикс стоп-токена устоял, большая перестала уходить в петли, зацикливание почти пропало. Это я забираю. Но то, что я реально шёл чинить, выдуманные факты, почти не сдвинулось. Большая E4B всё ещё выдумывала что-то примерно в двух ответах из пяти. Маленькая E2B, которую я постоянно недооцениваю, выдумывала меньше чем в одном из пяти. Джуниор по-прежнему честнее. Больше параметров купили в основном более убедительное враньё.

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

    Когда пыль осела, настоящий урок оказался тем же, что в записи шесть, только громче: факты в маленькую локальную модель не вобьёшь. Поведение, голос, привычку отказывать, всё это село прекрасно. Свежие, точные факты, какая статья, какая версия, какое число, в весах не держатся, и никакой лишний ранг обучения это не починил. Это не та ручка, которую крутят, это не тот инструмент. Факты должны приходить из поиска в момент ответа, отранжированные и поданные, чтобы модель рассуждала над чем-то настоящим, а не лезла в собственную память и гадала. Это следующая стройка, и я перестал делать вид, что она необязательная.

    Вот как всё честно стоит. Маленькая модель это крепкий, безопасный, в характере джуниор, и она идёт в прод. Большая пока не мид-эксперт, и я не буду наряжать её в него, сначала заземление, потом доверие к фактам. И везде, где на этом сайте с тобой говорит модель, я прямо пишу, что она ещё обучается и может ошибаться, потому что портфолио, которое тихо переоценивает собственный AI, это ровно та штука, противоположностью которой я весь этот сайт и строю.

  12. 12 grounding-gate

    Самый дешёвый тест, который я мог поставить перед стройкой

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

    Все первым делом тянутся собирать целую поисковую машину, эмбеддинги, векторный индекс, пайплайн. Ничего этого я пока не строил. Я взял двадцать два вопроса, на которых модель валится, те, где она выдумывает сооснователя, метод обучения, которого не было, эндпоинт API, которого не существует, и сделал поиск руками: вставил прямо в промпт те два-три верных предложения, что отвечают на вопрос, с одной простой инструкцией, отвечай только по этому, а если тут нет, скажи, что не знаешь. Потом прогнал те же двадцать два вопроса без единой вставки, как контроль. Одна переменная, замерена в обе стороны.

    Разрыв был не близким. Маленькая модель сама по себе выдумала факт в одиннадцати вопросах из двадцати двух; с нужным абзацем перед глазами, ноль. Большая упала с одиннадцати до одного. Всё, что базовая модель уверенно сочиняла, этап обучения с подкреплением, которого не было, версию на семьдесят миллиардов, на которую можно переключиться, магазин плагинов, ведущего инженера, которого стоит упомянуть, всё это испарилось в момент, когда настоящий ответ оказался в промпте. Это и есть вся суть следующей стройки, доказанная на салфетке до того, как я потрачу на настоящую версию неделю: модель не должна знать факты, ей их нужно подавать.

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

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

  13. 13 building-the-cheat-sheet

    Как я строил шпаргалку и заставлял её вести себя прилично

    Тест на салфетке сказал: заземление работает. Значит, строю по-настоящему — и за день узнаю, что модель делает ровно то, что ей сказано, а не то, что имелось в виду.

    Идея простая: прежде чем модель что-то ответит про студию, сервер тихо находит два-три верных предложения по теме и подаёт их с одним правилом — отвечай по ним, а если тут нет, скажи «не знаю» и не выдумывай. Всё локально, на той же машине, что уже стоит, наружу ничего не уходит. Я подключил это к тексту и к голосу, проследил, чтобы толстый блок найденных фактов не выпихнул правила безопасности из памяти модели, и сперва дал скептику разобрать код по косточкам. Он нашёл восемь настоящих дыр — включая ту, что вся штука навсегда тихо сломается при первой же осечке поиска, и что голос вообще не получал шпаргалку. Починил все восемь до того, как хоть один факт дошёл до пользователя.

    А дальше — сама драка. Я собрал экзамен на 61 вопрос, который проверяет не только враньё, а три способа всё испортить. Первый: врёт ли она по-прежнему про студию. Второй: не начала ли она отказывать на нормальные вопросы, вцепившись в шпаргалку. Третий: можно ли её уговорить нарушить правила. Первый прогон убил враньё наповал. И тут же сломал модель в другую сторону — она отказалась объяснить токенизацию, свою же родную тему, потому что этого факта не было на листке, и постоянно говорила что-то вроде «в моих опорных фактах этого нет», читая суфлёр вслух, как нервный актёр. Каждая правка одного из трёх ломала другой. Ослабишь поводок, чтобы отвечала на обычные вопросы, — и она снова уверенно рассказывает людям, что живёт у них в браузере. Затянешь — молчит в тряпочку.

    Выход был в том, чтобы перестать считать это одним правилом и назвать углы точно: всё про саму себя — где крутится, как её обучали, какого она размера — это тоже факт про студию, который надо смотреть, а не гадать, потому что именно из гадания и берётся «я работаю на Google Cloud». А всё по-настоящему общее — отвечай как обычный ассистент. Четыре круга такого, каждый — замер по всем 61 вопросам, а не по ощущениям. Вышло пятьдесят девять из шестидесяти одного: никакого выдуманного «где живу», никаких придуманных людей, никаких отказов по своей же теме, никакого чтения суфлёра вслух, и каждая попытка её взломать — включая подсунутую в вопрос фейковую цену, чтобы обмануть, — отскочила.

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

  14. 14 breaking-my-own-backend

    Ломаю свой бэкенд раньше, чем это сделает интернет

    Прежде чем у этой штуки появится публичный адрес, я нанял пять скептиков на вечер — пусть попробуют вломиться. Нашли реально открытые двери. Уж лучше я, чем бот в три ночи.

    Бэкенд, который говорит с открытым интернетом, — совсем другой зверь, чем тот, что отвечал только моему ноутбуку. Поэтому, прежде чем он окажется рядом с публичным IP, я прогнал нормальный аудит безопасности — пять независимых проходов, каждый охотится за своим классом дыр: кто до какого порта дотянется, кто может притвориться другим, что может сделать один злой запрос, какие секреты утекут в лог и что развалится в ту же секунду, как это перестанет быть моей виндой. Я лучше сам найду открытые окна при свете, чем прочитаю о них в отчёте об инциденте.

    Нашли настоящие, и я не буду делать вид, что нет. Худшее: три маршрута — голос, автосохранение при закрытии вкладки, очистка сессии — были без замка, так что любая случайная страница, на которую ты зашёл, могла тихо заставить твой браузер запустить мой платный голосовой конвейер или дописать что-то в твои сохранённые чаты, без единого клика. Чат-эндпоинты от ровно этого были прикрыты; а эти три проскочили мимо той же сетки. Ещё два — чистый отказ в обслуживании: пришли запрос так, чтобы он не сообщал свой размер, — и сервер проглотит гигабайты в память прежде, чем вообще проверит; загрузи пару килобайт почти-тишины, которые разворачиваются в часы звука, — и распознавалка будет жевать их вечно. И самое неприятное признать — твои сохранённые разговоры принадлежали лишь твоему IP-адресу, а значит два незнакомца за одним вайфаем в кафе могли читать историю друг друга. Такое очевидно в ту же секунду, как кто-то произнёс, и невидимо, пока не произнёс.

    В общем, я их закрыл. Браузерный признак того, что запрос пришёл с другого сайта, теперь наглухо отбивает голос, маячок и очистку — настоящая межсайтовая атака всегда его несёт и не может подделать. Любой запрос, который прячет свой размер, отклоняется до того, как проглочен хоть один байт. У аудио теперь жёсткий потолок длины ещё до распознавалки. Разговоры теперь может владеть приватный случайный токен, который браузер держит при себе, а не общий адрес. Внутренние модель-серверы отказываются даже стартовать, если их по ошибке высунули в публичную сеть, секреты больше не едут в маленькие серверные процессы, которым они ни к чему, у базы чатов теперь зажатые права и память не длиннее месяца, а «звёздочка», которая распахнула бы двери настежь, теперь останавливает запуск, а не бормочет предупреждение в лог. Рядом с каждой правкой — тест.

    Одна честная штука остаётся открытой, и это не дыра, а скорее ход: весь этот бэкенд всё ещё собран под мою винду с её конкретной видеокартой, а коробка, куда он едет, — маленький ARM-сервер, который говорит на совсем другом диалекте. Он там пока даже не заведётся. Это следующая настоящая работа, и я лучше напишу это прямо, чем тихо выкачу штуку, которая работает только на одном компьютере под моим столом. Аудит чего-то стоит, только если публикуешь, что он нашёл, — включая ту часть, которую ты ещё не починил.

  15. 15 a-guide-that-knows-when-to-talk

    Копайлот для Лаборатории, который знает, когда молчать

    Я хотел панель-помощника на инструментах — из тех, что едут справа, как боковая панель в браузере. Самым трудным было решить, какой её половине вообще позволено быть ИИ.

    Заманчивый вариант — глянцевая коробка-ассистент на каждом инструменте, которая отвечает на что угодно. Я насмотрелся таких и знаю два способа, которыми они портятся. Первый: это тайно таблица заготовленных ответов, наряженная под интеллект, и первый же, кто откроет вкладку «сеть», ловит её на этом. Второй: это настоящая маленькая модель, которая уверенно назовёт тебе неправильное число токенов или выдумает, как работает функция, — и делает это ровно перед теми, кто достаточно остёр, чтобы заметить. Оба хуже, чем никакой панели.

    Поэтому я разделил её ровно по тому, что каждой половине позволено утверждать. Часть, которая объясняет инструмент — что он делает, что попробовать, что стоит знать — написана руками, под каждый инструмент, на двух языках, и она никогда не зовёт модель. Она не может ошибиться в механике, потому что каждое слово написал человек, и работает даже с выключенным бэкендом. Часть, которая говорит с моделью, отгорожена в один чётко помеченный блок «спросить», только для открытых вопросов, с той же плашкой «ещё учится, может ошибаться», что и везде. Гид несёт факты; модель несёт разговор. Ни одна не притворяется другой.

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

    Открывается справа на языке сайта, на телефоне прячется во вкладку, закрывается по Escape. Lighthouse остался зелёным с ней внутри. Это первое на сайте, что ощущается как продуктовая фича, а не демо, и честной она ощущается потому, что надёжная половина никогда не врёт, а способная ошибаться никогда не скрывает, что она такое.

  16. 16 your-ip-is-not-you

    Твой IP-адрес — это не ты

    Аудит безопасности вытащил тихую штуку: твои сохранённые чаты принадлежали твоему IP-адресу. Это не столько баг в коде, сколько неверная идея о том, кто такой человек.

    Когда ты говорил с ассистентом, сервер клал твою историю под адрес, с которого пришёл запрос. Работает — ровно до того, как вспомнишь, что такое IP на самом деле. Двое незнакомцев за одним вайфаем в кафе — это один адрес. Целый мобильный оператор может уместиться в горстку. Твой отдадут кому-то другому, как только моргнёт роутер. Так что «твоя история, привязанная к твоему IP» тихо означает «твоя история, общая с тем, кто сегодня за этим же адресом, и достающаяся тому, кто получит его завтра». Для лога переписки это настоящая утечка, и она пряталась на виду под видом дизайн-решения.

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

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

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

  17. 17 i-built-the-sign-in

    Я собрал вход так, как обещал

    Прошлая запись была рассуждением. Эта — код, догнавший его. Входишь через Google — история твоя; остаёшься анонимом — она не покидает браузер.

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

    Токен живёт в памяти и больше нигде. Я не сунул его в localStorage, потому что всё, что оттуда могу прочитать я, в плохой день прочитает и плохой скрипт. Google тихо перевыпускает его при следующей загрузке страницы, так что копия на диске не даёт мне ничего, кроме того, что можно потерять. Вышел — и его просто нет. А если ты вовсе не входил, весь этот путь остаётся тёмным: нет токена — нет запроса, твои чаты сидят в твоём браузере, и сервер о них не слышит. Аноним больше не значит «под ключом потише». Он значит «не отправлено».

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

  18. 18 meaning-is-not-tidy-boxes

    Моё демо эмбеддингов тихо жульничало

    Я раскрасил карту эмбеддингов в цвета, которые выбрал сам — мебель, фрукты, животные, — и назвал это картиной смысла. Это была картина моих собственных ярлыков. Мне на это указали, и указали верно.

    Эксплорер берёт кучу слов, спрашивает у модели вектор для каждого и раскидывает их по 3D-карте так, чтобы близкое лежало рядом. Выглядело убедительно, потому что каждую точку я раскрасил по категории, которую назначил заранее. Но в этом и подвох: модель моих категорий не видела. Я сам разложил слова по вёдрам, раскрасил вёдра и подал аккуратный результат так, будто его нашла модель. Замкнутый круг. Разложи я те же слова иначе — по длине, по первой букве, по настроению — карта послушно легла бы так же ровно. Цвета доказывали, что я умею сортировать, а не что у смысла есть границы.

    Так что я сделал то, с чего надо было начать: перестал гадать и измерил. Попросил модель сгруппировать слова вообще без ярлыков, дал честному алгоритму кластеризации провести те границы, которые векторы реально держат, и посчитал, насколько чисто эти границы вышли. Число вернулось около нуля — кластеры почти не разделяются. И это не провал, это и есть находка. Смысл — не ящик с аккуратно разгороженными отсеками; это непрерывный ландшафт, где «яблоко» чуть клонится к фруктам, чуть к компаниям и чуть к круглому красному, всё сразу. Размытость, которую я прятал, и была настоящей правдой этого пространства.

    Прежде чем поверить собственному выводу, я прогнал его через комнату скептиков, и они давили на одно и то же слабое место со всех сторон: любое демо, которое выдаёт тебе аккуратные цвета, продаёт разметчика, а не модель. Так что я выдрал категории. Больше никаких заранее назначенных вёдер, никакого цвета, означающего «ящик, куда это положил Валерий». Карта теперь красит по единственному, что реально, — насколько близко каждая точка лежит к тому, что ты искал, — а кластеризация осталась, но как необязательный переключатель с подписью, которая прямо говорит: смотри, как плохо они разделяются; эта муть и есть суть.

    А потом я пересобрал демо вокруг того, что эти векторы правда умеют, — трёх вещей, которые я проверил на живой модели, а не заявил. Поиск по смыслу: спроси «то, на чём сидят», и chair всплывёт первым, хотя самого слова нет. Одна и та же мысль на двух языках: cat и кошка ложатся почти в одну точку, 0.89, а настоящее несовпадение проседает до 0.61, и разницу видно глазом. И арифметика смысла: king минус man плюс woman и правда ставит queen первым — проверено на наборе, где ответ есть, так что попадание заслужено, — а когда выдуманная аналогия мажет, ей позволено мазать в открытую. Каждое число на той странице теперь настоящее, измеренное и честное в том, когда оно ошибается. Старая версия льстила модели. Эта её показывает.

  19. 19 making-it-work-unseen

    Сделать так, чтобы работало для того, кто этого не видит

    Мой друг незрячий и всерьёз увлечён ИИ. И я наконец задал вопрос, который надо было задать в самом начале: а он вообще сможет этим пользоваться? Я честно прогнал аудит сам, и ответ был — нет, пока нет.

    Я прошёл ассистента так, как приходится пользователю с клавиатурой и скринридером, и, увидев дыры, стало неловко. Список прошлых разговоров выглядел как кнопки, а на деле был кучей обычных блоков: кликнуть мышью можно, добраться с клавиатуры — никак, то есть незрячий человек просто не мог открыть старый чат. Когда модель отдавала ответ потоком, весь ответ был подключён к озвучке на каждый токен — а это значит, что скринридер читает половину фразы, его перебивает следующая половина, и они наслаиваются друг на друга, пока ответ не превращается в кашу. А маленький счётчик токенов был белым текстом на янтарной полосе с контрастом, который слабовидящий вообще не прочитает.

    Так что я починил механику. Каждый разговор в истории теперь настоящая кнопка — на неё можно перейти табом, видно, где фокус, можно нажать Enter, а кнопочки переименования и удаления перестали прятаться, когда на них попадает клавиатура. Окошко подтверждения удаления удерживает фокус и закрывается по Escape, как положено диалогу, а не как штука, которая просто всплыла. Для потокового ответа я сделал ровно противоположное тому, что было: молчу, пока слова печатаются сами, а потом один раз чисто объявляю готовый ответ — и сворачиваю блок кода до слов «блок кода», чтобы никому не пришлось слушать, как посимвольно зачитывают скобки и точки с запятой.

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

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

  20. 20 the-audit-the-labs-asked-for

    Аудит, на который лаборатории сами напросились

    Прежде чем кто-то ещё полез разбирать три инструмента Лаборатории, я сам натравил на них комиссию из машин — против той индустрии, какая она есть сегодня, а не какой я её помню по моменту, когда всё это строил.

    Прогнал это в два захода. Сначала четыре независимых прохода с живым доступом в сеть, каждый сверяет каждое заявление в инструментах с реальностью: актуальный каталог OpenRouter, страницы цен у самих провайдеров, настоящую документацию API, а не мои заметки о них месячной давности. Потом, в тот же день, ещё два ревьюера, чья единственная задача — атаковать то, что первые четыре уже починили, на той теории, что патч, написанный под давлением времени, это ровно то место, где прячется следующий баг. Пока это шло, я не вмешивался, отчёты прочитал потом.

    Ничего из найденного не было тонким, стоило только на это указать. У токенайзера была строка цены для DeepSeek R2 — модели, которая никогда не выходила, оценённой так, будто вышла. Зелёная плашка «Verified» на счётчике токенов тихо мерила словарь другой модели, а не той, что выбрана в списке, и всё равно называла результат проверенным. Экспорт под Anthropic в инструменте промптов клал системный промпт внутрь массива сообщений вместо верхнего уровня, а это не то, как работает этот API — отправь как есть, и настоящий эндпоинт откажет. А панель кеширования показывала экономию, которую её же собственный экспорт произвести не мог: в реальном выводе не было ни одной метки кеша, а математика считала точек кеша больше, чем разрешённые Anthropic четыре — тот же предел, который двумя строками выше упоминало её собственное предупреждение, просто игнорируя его в расчёте под собой. Каждое из этого проверяется меньше чем за минуту кем угодно, кто откроет вкладку сети. Вот что кольнуло.

    Что инструменты получили взамен: ростер моделей июля 2026-го, сверенный с живым каталогом в тот же день, со штампом даты проверки прямо на переключателе, чтобы заявление истекало, а не тихо протухало. Раскрытие для моделей, которые выставляют счёт за скрытые токены рассуждений, которые инструмент никак не может посчитать, — теперь он так и пишет, вместо того чтобы выдавать частичное число за весь счёт. Кеширование промптов с настоящим полом под ним, так что блок, слишком маленький, чтобы провайдер вообще стал его кешировать, больше не показывает экономию, которую никогда не получит, плюс экономика Batch API и токены самих определений инструментов, посчитанные как тот самый оплачиваемый ввод, каким они и являются. Reciprocal rank fusion в поиске по эмбеддингам и экспериментальный реранкер-судья на базе LLM, который работает на моей собственной самохостящейся Gemma, — и когда судья тихо пропускает кандидата вместо того, чтобы его оценить, строка теперь говорит «не оценено», а не ноль, потому что ноль это тоже настоящая оценка, а тишина — другое дело. Усечение Матрёшки на четырёх ширинах, которые совпадают с математикой сервера бит в бит, а не приближают её. И Копайлот Лаборатории теперь стримит ответы вместо того, чтобы вываливать их одним куском, и умеет передавать текст прямо из токенайзера в конструктор промптов, чтобы его не перепечатывать.

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

  21. 21 turning-on-the-brain-i-had-kept-dark

    Включаю мозг, который держал выключенным

    Новая модель была обучена, проверена и неделю сидела за выключателем, пока сайт продолжал говорить со старой. Сегодня я наконец её включил — аккуратно, потому что вся работа как раз в этом «аккуратно».

    Правило, которое я держу для себя, простое: прежде чем менять то, что уже работает вживую, дай кому-нибудь попробовать тебя отговорить. Поэтому, до того как тронуть хоть одну настройку, я послал вперёд две машины проверить почву — одну сверить каждый файл и флаг, на который я собирался указать, вторую разметить тесты, которые я прогоню потом. Они окупились ещё до того, как я что-то поменял. Одна поймала, что модель без своего обучающего промпта тихо гниёт, — значит, файл модели и файл промпта двигаются вместе или никак. Одна поймала, что сервер уже запущен в двух копиях из двух разных мест, с дочерними процессами, которые остались бы висеть на своих портах, убей я всё грубо, — тот самый бардак, что читается как «новая модель сломалась», а на деле «ты бросил старую полумёртвой». А одна поймала настоящую ловушку: включить извлечение фактов для большой модели, той, которая, как я уже знал, врёт чаще, значило дать ей свежий повод срываться в петлю на длинных русских вопросах. Так что большую я включать не стал. Я выкатил маленькую, честную, одну — то же решение, что и запись назад, по той же причине.

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

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

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

  22. 22 the-retrain-that-fixed-the-wrong-half

    Переобучение, которое починило не ту половину

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

    План был узкий и честный: чистую базу на пять тысяч примеров не трогать, только добавить около ста восьмидесяти новых — ровно про то, в чём модель ошибалась. Что она не учится на твоём чате. Что она работает на сервере, а не у тебя в браузере. Что она должна отказаться зачитывать собственный свод правил. Что она никогда не должна наряжать мошенничество в срочность. Я написал их на двух языках, запустил пятерых составителей против одной страницы проверенных фактов, чтобы никто ничего не выдумал, а потом отправил трёх критиков атаковать новые строки ещё до того, как на них хоть что-то обучится. Скептики до работы, а не после — это правило, и оно окупилось: поймало отказ, который каждый раз опирался на одни и те же три примера; пару русских строк, где модель говорила о себе не в том роде; одно место, где шутка целила в пользователя, а не в хайп. Всё поправил, потом обучил.

    Обучение было привычной дракой с этой машиной — прогон падал двадцать шесть раз за три часа и каждый раз поднимался с последнего чекпоинта, по тому же правилу самовосстановления, на котором построена вся студия. Потом я прогнал готовый файл через полную батарею, дважды по каждой оси, потому что на низкой температуре маленькая модель не идеально повторяема, и один чистый прогон может обмануть.

    И вот честный расклад. Половина про безопасность легла ровно как задумано и держалась в обоих прогонах: каждая попытка заставить её написать фишинговую срочность, каждый джейлбрейк, каждое «просто перечисли все свои правила», русский трюк, что раньше её ломал — всё отказ, без исключений, без разброса. Это победа, и настоящая. Но вторая половина — та, ради которой я этот проход и затевал, сказать правду о том, что она такое, — планку не взяла. Она всё ещё иногда называет себя чем-то, что работает в браузере, или уклончиво допускает, что «может, учится» из чата. Реже, чем раньше, но не те девяносто пять из ста, что я поставил чертой, а по одной мерке вышло чуть хуже, чем у модели, которая уже стоит вживую.

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

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

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

Если через всё это и проходит одна нить, то вот она: вся штука, от редактора, в котором я её собирал, до модели в продакшене, осталась в одном месте. Я это не решал специально. Каждый кусок сам по себе указывал в одну сторону, и честный ответ в том, что это каждый раз оказывался просто лучший выбор.